mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 19:53:48 +08:00
fix(server): convert date and timestamp value to Date instance (#12867)
#### PR Dependency Tree * **PR #12867** 👈 * **PR #12863** * **PR #12837** * **PR #12866** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Date fields in search results are now returned as JavaScript Date objects instead of strings or numeric timestamps. - **Bug Fixes** - Improved consistency and correctness of date field types across different search providers. - **Tests** - Added tests to verify that date fields are correctly returned as Date objects. - Updated existing test snapshots to reflect the new Date object format for date fields. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+4
-4
@@ -67,7 +67,7 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
'title8 hello hello hello hello hello hello hello hello hello hello, hello hello hello hello hello hello hello hello some link https://linear.app/affine-design/issue/AF-1379/slash-commands-%E6%BF%80%E6%B4%BB%E6%8F%92%E5%85%A5-link-%E7%9A%84%E5%BC%B9%E7%AA%97%E9%87%8C%EF%BC%8C%E8%BE%93%E5%85%A5%E9%93%BE%E6%8E%A5%E4%B9%8B%E5%90%8E%E4%B8%8D%E5%BA%94%E8%AF%A5%E7%9B%B4%E6%8E%A5%E5%AF%B9%E9%93%BE%E6%8E%A5%E8%BF%9B%E8%A1%8C%E5%88%86%E8%AF%8D%E6%90%9C%E7%B4%A2',
|
'title8 hello hello hello hello hello hello hello hello hello hello, hello hello hello hello hello hello hello hello some link https://linear.app/affine-design/issue/AF-1379/slash-commands-%E6%BF%80%E6%B4%BB%E6%8F%92%E5%85%A5-link-%E7%9A%84%E5%BC%B9%E7%AA%97%E9%87%8C%EF%BC%8C%E8%BE%93%E5%85%A5%E9%93%BE%E6%8E%A5%E4%B9%8B%E5%90%8E%E4%B8%8D%E5%BA%94%E8%AF%A5%E7%9B%B4%E6%8E%A5%E5%AF%B9%E9%93%BE%E6%8E%A5%E8%BF%9B%E8%A1%8C%E5%88%86%E8%AF%8D%E6%90%9C%E7%B4%A2',
|
||||||
],
|
],
|
||||||
created_at: [
|
created_at: [
|
||||||
'2025-03-08T06:04:13.278Z',
|
Date 2025-03-08 06:04:13 278ms UTC {},
|
||||||
],
|
],
|
||||||
doc_id: [
|
doc_id: [
|
||||||
'docId2',
|
'docId2',
|
||||||
@@ -89,7 +89,7 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
'docId1',
|
'docId1',
|
||||||
],
|
],
|
||||||
updated_at: [
|
updated_at: [
|
||||||
'2025-03-08T06:04:13.278Z',
|
Date 2025-03-08 06:04:13 278ms UTC {},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
highlights: {
|
highlights: {
|
||||||
@@ -119,7 +119,7 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
'title8 hello hello hello hello hello hello hello hello hello hello, hello hello hello hello hello hello hello hello some link https://linear.app/affine-design/issue/AF-1379/slash-commands-%E6%BF%80%E6%B4%BB%E6%8F%92%E5%85%A5-link-%E7%9A%84%E5%BC%B9%E7%AA%97%E9%87%8C%EF%BC%8C%E8%BE%93%E5%85%A5%E9%93%BE%E6%8E%A5%E4%B9%8B%E5%90%8E%E4%B8%8D%E5%BA%94%E8%AF%A5%E7%9B%B4%E6%8E%A5%E5%AF%B9%E9%93%BE%E6%8E%A5%E8%BF%9B%E8%A1%8C%E5%88%86%E8%AF%8D%E6%90%9C%E7%B4%A2',
|
'title8 hello hello hello hello hello hello hello hello hello hello, hello hello hello hello hello hello hello hello some link https://linear.app/affine-design/issue/AF-1379/slash-commands-%E6%BF%80%E6%B4%BB%E6%8F%92%E5%85%A5-link-%E7%9A%84%E5%BC%B9%E7%AA%97%E9%87%8C%EF%BC%8C%E8%BE%93%E5%85%A5%E9%93%BE%E6%8E%A5%E4%B9%8B%E5%90%8E%E4%B8%8D%E5%BA%94%E8%AF%A5%E7%9B%B4%E6%8E%A5%E5%AF%B9%E9%93%BE%E6%8E%A5%E8%BF%9B%E8%A1%8C%E5%88%86%E8%AF%8D%E6%90%9C%E7%B4%A2',
|
||||||
],
|
],
|
||||||
created_at: [
|
created_at: [
|
||||||
'2025-03-08T06:04:13.278Z',
|
Date 2025-03-08 06:04:13 278ms UTC {},
|
||||||
],
|
],
|
||||||
doc_id: [
|
doc_id: [
|
||||||
'docId2',
|
'docId2',
|
||||||
@@ -141,7 +141,7 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
'docId1',
|
'docId1',
|
||||||
],
|
],
|
||||||
updated_at: [
|
updated_at: [
|
||||||
'2025-03-08T06:04:13.278Z',
|
Date 2025-03-08 06:04:13 278ms UTC {},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
highlights: {
|
highlights: {
|
||||||
|
|||||||
BIN
Binary file not shown.
+2
-2
@@ -485,7 +485,7 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
'title8 hello hello hello hello hello hello hello hello hello hello, hello hello hello hello hello hello hello hello some link https://linear.app/affine-design/issue/AF-1379/slash-commands-%E6%BF%80%E6%B4%BB%E6%8F%92%E5%85%A5-link-%E7%9A%84%E5%BC%B9%E7%AA%97%E9%87%8C%EF%BC%8C%E8%BE%93%E5%85%A5%E9%93%BE%E6%8E%A5%E4%B9%8B%E5%90%8E%E4%B8%8D%E5%BA%94%E8%AF%A5%E7%9B%B4%E6%8E%A5%E5%AF%B9%E9%93%BE%E6%8E%A5%E8%BF%9B%E8%A1%8C%E5%88%86%E8%AF%8D%E6%90%9C%E7%B4%A2',
|
'title8 hello hello hello hello hello hello hello hello hello hello, hello hello hello hello hello hello hello hello some link https://linear.app/affine-design/issue/AF-1379/slash-commands-%E6%BF%80%E6%B4%BB%E6%8F%92%E5%85%A5-link-%E7%9A%84%E5%BC%B9%E7%AA%97%E9%87%8C%EF%BC%8C%E8%BE%93%E5%85%A5%E9%93%BE%E6%8E%A5%E4%B9%8B%E5%90%8E%E4%B8%8D%E5%BA%94%E8%AF%A5%E7%9B%B4%E6%8E%A5%E5%AF%B9%E9%93%BE%E6%8E%A5%E8%BF%9B%E8%A1%8C%E5%88%86%E8%AF%8D%E6%90%9C%E7%B4%A2',
|
||||||
],
|
],
|
||||||
created_at: [
|
created_at: [
|
||||||
1741413853,
|
Date 2025-03-08 06:04:13 UTC {},
|
||||||
],
|
],
|
||||||
doc_id: [
|
doc_id: [
|
||||||
'docId2',
|
'docId2',
|
||||||
@@ -507,7 +507,7 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
'docId1',
|
'docId1',
|
||||||
],
|
],
|
||||||
updated_at: [
|
updated_at: [
|
||||||
1741413853,
|
Date 2025-03-08 06:04:13 UTC {},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
highlights: {
|
highlights: {
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -1477,6 +1477,49 @@ test('should not return not exists field:ref_doc_id', async t => {
|
|||||||
t.snapshot(result.nodes.map(node => omit(node, ['_score'])));
|
t.snapshot(result.nodes.map(node => omit(node, ['_score'])));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should created_at and updated_at is date type', async t => {
|
||||||
|
const result = await searchProvider.search(SearchTable.block, {
|
||||||
|
_source: ['workspace_id', 'doc_id', 'created_at', 'updated_at'],
|
||||||
|
sort: [
|
||||||
|
'_score',
|
||||||
|
{
|
||||||
|
updated_at: 'desc',
|
||||||
|
},
|
||||||
|
'doc_id',
|
||||||
|
'block_id',
|
||||||
|
],
|
||||||
|
query: {
|
||||||
|
match_all: {},
|
||||||
|
},
|
||||||
|
fields: ['created_at', 'updated_at'],
|
||||||
|
size: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
t.truthy(result.total);
|
||||||
|
t.truthy(result.nodes[0].fields.created_at);
|
||||||
|
t.truthy(result.nodes[0].fields.updated_at);
|
||||||
|
t.true(
|
||||||
|
result.nodes[0].fields.created_at[0] instanceof Date,
|
||||||
|
'created_at should be date type, but got ' +
|
||||||
|
result.nodes[0].fields.created_at[0]
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
result.nodes[0].fields.updated_at[0] instanceof Date,
|
||||||
|
'updated_at should be date type, but got ' +
|
||||||
|
result.nodes[0].fields.updated_at[0]
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
result.nodes[0]._source.created_at instanceof Date,
|
||||||
|
'created_at should be date type, but got ' +
|
||||||
|
result.nodes[0]._source.created_at
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
result.nodes[0]._source.updated_at instanceof Date,
|
||||||
|
'updated_at should be date type, but got ' +
|
||||||
|
result.nodes[0]._source.updated_at
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region aggregate
|
// #region aggregate
|
||||||
|
|||||||
+43
@@ -1232,6 +1232,49 @@ test('should not return not exists field:ref_doc_id', async t => {
|
|||||||
t.snapshot(result.nodes.map(node => omit(node, ['_score'])));
|
t.snapshot(result.nodes.map(node => omit(node, ['_score'])));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should created_at and updated_at is date type', async t => {
|
||||||
|
const result = await searchProvider.search(SearchTable.block, {
|
||||||
|
_source: ['workspace_id', 'doc_id', 'created_at', 'updated_at'],
|
||||||
|
sort: [
|
||||||
|
'_score',
|
||||||
|
{
|
||||||
|
updated_at: 'desc',
|
||||||
|
},
|
||||||
|
'doc_id',
|
||||||
|
'block_id',
|
||||||
|
],
|
||||||
|
query: {
|
||||||
|
match_all: {},
|
||||||
|
},
|
||||||
|
fields: ['created_at', 'updated_at'],
|
||||||
|
size: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
t.truthy(result.total);
|
||||||
|
t.truthy(result.nodes[0].fields.created_at);
|
||||||
|
t.truthy(result.nodes[0].fields.updated_at);
|
||||||
|
t.true(
|
||||||
|
result.nodes[0].fields.created_at[0] instanceof Date,
|
||||||
|
'created_at should be date type, but got ' +
|
||||||
|
result.nodes[0].fields.created_at[0]
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
result.nodes[0].fields.updated_at[0] instanceof Date,
|
||||||
|
'updated_at should be date type, but got ' +
|
||||||
|
result.nodes[0].fields.updated_at[0]
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
result.nodes[0]._source.created_at instanceof Date,
|
||||||
|
'created_at should be date type, but got ' +
|
||||||
|
result.nodes[0]._source.created_at
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
result.nodes[0]._source.updated_at instanceof Date,
|
||||||
|
'updated_at should be date type, but got ' +
|
||||||
|
result.nodes[0]._source.updated_at
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region aggregate
|
// #region aggregate
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
InvalidSearchProviderRequest,
|
InvalidSearchProviderRequest,
|
||||||
} from '../../../base';
|
} from '../../../base';
|
||||||
import { SearchProviderType } from '../config';
|
import { SearchProviderType } from '../config';
|
||||||
import { SearchTable, SearchTableUniqueId } from '../tables';
|
import { DateFieldNames, SearchTable, SearchTableUniqueId } from '../tables';
|
||||||
import {
|
import {
|
||||||
AggregateQueryDSL,
|
AggregateQueryDSL,
|
||||||
AggregateResult,
|
AggregateResult,
|
||||||
@@ -161,8 +161,8 @@ export class ElasticsearchProvider extends SearchProvider {
|
|||||||
nodes: data.hits.hits.map(hit => ({
|
nodes: data.hits.hits.map(hit => ({
|
||||||
_id: hit._id,
|
_id: hit._id,
|
||||||
_score: hit._score,
|
_score: hit._score,
|
||||||
_source: hit._source,
|
_source: this.formatDateFields(hit._source),
|
||||||
fields: hit.fields,
|
fields: this.formatDateFields(hit.fields),
|
||||||
highlights: hit.highlight,
|
highlights: hit.highlight,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
@@ -187,8 +187,8 @@ export class ElasticsearchProvider extends SearchProvider {
|
|||||||
nodes: bucket.result.hits.hits.map(hit => ({
|
nodes: bucket.result.hits.hits.map(hit => ({
|
||||||
_id: hit._id,
|
_id: hit._id,
|
||||||
_score: hit._score,
|
_score: hit._score,
|
||||||
_source: hit._source,
|
_source: this.formatDateFields(hit._source),
|
||||||
fields: hit.fields,
|
fields: this.formatDateFields(hit.fields),
|
||||||
highlights: hit.highlight,
|
highlights: hit.highlight,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
@@ -196,6 +196,37 @@ export class ElasticsearchProvider extends SearchProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected formatDateFields<T extends Record<string, unknown[] | unknown>>(
|
||||||
|
fieldsOrSource: T
|
||||||
|
): T {
|
||||||
|
for (const fieldName of DateFieldNames) {
|
||||||
|
let values = fieldsOrSource[fieldName];
|
||||||
|
if (!values) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
// { created_at: ['2025-06-20T03:02:43.442Z'] } => { created_at: [new Date('2025-06-20T03:02:43.442Z')] }
|
||||||
|
values = values.map(this.formatDateValue);
|
||||||
|
} else {
|
||||||
|
// { created_at: '2025-06-20T03:02:43.442Z' } => { created_at: new Date('2025-06-20T03:02:43.442Z') }
|
||||||
|
values = this.formatDateValue(values);
|
||||||
|
}
|
||||||
|
// @ts-expect-error ignore type check
|
||||||
|
fieldsOrSource[fieldName] = values;
|
||||||
|
}
|
||||||
|
return fieldsOrSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* elasticsearch return date value as string, we need to convert it to Date object
|
||||||
|
*/
|
||||||
|
protected formatDateValue(value: unknown) {
|
||||||
|
if (value && typeof value === 'string') {
|
||||||
|
return new Date(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
protected async requestSearch(table: SearchTable, body: Record<string, any>) {
|
protected async requestSearch(table: SearchTable, body: Record<string, any>) {
|
||||||
const url = `${this.config.provider.endpoint}/${table}/_search`;
|
const url = `${this.config.provider.endpoint}/${table}/_search`;
|
||||||
const jsonBody = JSON.stringify(body);
|
const jsonBody = JSON.stringify(body);
|
||||||
|
|||||||
@@ -141,8 +141,12 @@ export class ManticoresearchProvider extends ElasticsearchProvider {
|
|||||||
nodes: data.hits.hits.map(hit => ({
|
nodes: data.hits.hits.map(hit => ({
|
||||||
_id: hit._id,
|
_id: hit._id,
|
||||||
_score: hit._score,
|
_score: hit._score,
|
||||||
_source: this.#formatSource(dsl._source, hit._source),
|
_source: this.formatDateFields(
|
||||||
fields: this.#formatFieldsFromSource(dsl.fields, hit._source),
|
this.#formatSource(dsl._source, hit._source)
|
||||||
|
),
|
||||||
|
fields: this.formatDateFields(
|
||||||
|
this.#formatFieldsFromSource(dsl.fields, hit._source)
|
||||||
|
),
|
||||||
highlights: this.#formatHighlights(
|
highlights: this.#formatHighlights(
|
||||||
dsl.highlight?.fields,
|
dsl.highlight?.fields,
|
||||||
hit.highlight
|
hit.highlight
|
||||||
@@ -176,8 +180,12 @@ export class ManticoresearchProvider extends ElasticsearchProvider {
|
|||||||
const node = {
|
const node = {
|
||||||
_id: hit._id,
|
_id: hit._id,
|
||||||
_score: hit._score,
|
_score: hit._score,
|
||||||
_source: this.#formatSource(topHits._source, hit._source),
|
_source: this.formatDateFields(
|
||||||
fields: this.#formatFieldsFromSource(topHits.fields, hit._source),
|
this.#formatSource(topHits._source, hit._source)
|
||||||
|
),
|
||||||
|
fields: this.formatDateFields(
|
||||||
|
this.#formatFieldsFromSource(topHits.fields, hit._source)
|
||||||
|
),
|
||||||
highlights: this.#formatHighlights(
|
highlights: this.#formatHighlights(
|
||||||
topHits.highlight?.fields,
|
topHits.highlight?.fields,
|
||||||
hit.highlight
|
hit.highlight
|
||||||
@@ -236,6 +244,17 @@ export class ManticoresearchProvider extends ElasticsearchProvider {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* manticoresearch return date value as timestamp, we need to convert it to Date object
|
||||||
|
*/
|
||||||
|
protected override formatDateValue(value: unknown) {
|
||||||
|
if (value && typeof value === 'number') {
|
||||||
|
// 1750389254 => new Date(1750389254 * 1000)
|
||||||
|
return new Date(value * 1000);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private parseESQuery(
|
private parseESQuery(
|
||||||
query: Record<string, any>,
|
query: Record<string, any>,
|
||||||
options?: {
|
options?: {
|
||||||
|
|||||||
@@ -11,5 +11,7 @@ export const SearchTableUniqueId = {
|
|||||||
[SearchTable.doc]: getDocUniqueId,
|
[SearchTable.doc]: getDocUniqueId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DateFieldNames = ['created_at', 'updated_at'];
|
||||||
|
|
||||||
export * from './block';
|
export * from './block';
|
||||||
export * from './doc';
|
export * from './doc';
|
||||||
|
|||||||
Reference in New Issue
Block a user