diff --git a/apps/core/src/__tests__/e2e/api/records/records.test.ts b/apps/core/src/__tests__/e2e/api/records/records.test.ts index 8d5f29be7..a268692e7 100644 --- a/apps/core/src/__tests__/e2e/api/records/records.test.ts +++ b/apps/core/src/__tests__/e2e/api/records/records.test.ts @@ -272,6 +272,7 @@ describe('Records', () => { const sfTestLibTreeId = 'records_sort_filter_test_lib_tree'; const testTreeId = 'records_sf_test_tree'; const testSimpleAttrId = 'records_sort_filter_test_attr_simple'; + const testSimpleAttrId2 = 'records_sort_filter_test_attr_simple2'; const testSimpleExtAttrId = 'records_sort_filter_test_attr_simple_extended'; const testSimpleLinkAttrId = 'records_sort_filter_test_attr_simple_link'; const testAdvAttrId = 'records_sort_filter_test_attr_adv'; @@ -315,6 +316,12 @@ describe('Records', () => { label: 'test', format: AttributeFormats.TEXT }); + await gqlSaveAttribute({ + id: testSimpleAttrId2, + type: AttributeTypes.SIMPLE, + label: 'test 2', + format: AttributeFormats.TEXT + }); await gqlSaveAttribute({ id: testSimpleExtAttrId, type: AttributeTypes.SIMPLE, @@ -375,6 +382,7 @@ describe('Records', () => { // Save attributes on libs await gqlSaveLibrary(sfTestLibId, 'Test', [ testSimpleAttrId, + testSimpleAttrId2, testSimpleExtAttrId, testAdvAttrId, testSimpleLinkAttrId, @@ -484,6 +492,21 @@ describe('Records', () => { recordId: "${sfRecord3}", attribute: "${testSimpleAttrId}", value: {payload: "B"}) { id_value } + v4: saveValue( + library: "${sfTestLibId}", + recordId: "${sfRecord1}", + attribute: "${testSimpleAttrId2}", + value: {payload: "1"}) { id_value } + v5: saveValue( + library: "${sfTestLibId}", + recordId: "${sfRecord2}", + attribute: "${testSimpleAttrId2}", + value: {payload: "1"}) { id_value } + v6: saveValue( + library: "${sfTestLibId}", + recordId: "${sfRecord3}", + attribute: "${testSimpleAttrId2}", + value: {payload: "2"}) { id_value } }`); }); @@ -517,6 +540,78 @@ describe('Records', () => { expect(res.data.data.records.list[1].id).toBe(sfRecord3); expect(res.data.data.records.list[2].id).toBe(sfRecord1); }); + + describe('Multiple sorts', () => { + // +--------------+---------------------+--------------------+ + // | | testSimpleAttribute | testSimpleAttrId2 | + // +--------------+---------------------+--------------------+ + // | sfRecord1 | C | 1 | + // | sfRecord2 | A | 1 | + // | sfRecord3 | B | 2 | + // +--------------+---------------------+--------------------+ + + const _makeCall = async ( + testSimpleAttrId2Order: 'asc' | 'desc', + testSimpleAttrIdOrder: 'asc' | 'desc' + ) => + makeGraphQlCall(`{ + records( + library: "${sfTestLibId}", + multipleSort: [ + {field: "${testSimpleAttrId2}", order: ${testSimpleAttrId2Order}}, + {field: "${testSimpleAttrId}", order: ${testSimpleAttrIdOrder}} + ] + ) { + list { + id + } + } + }`); + + test('Sort on multiple attributes asc / asc', async () => { + const res = await _makeCall('asc', 'asc'); + + expect(res.data.errors).toBeUndefined(); + expect(res.data.data.records.list.map((record: {id: string}) => record.id)).toEqual([ + sfRecord2, + sfRecord1, + sfRecord3 + ]); + }); + + test('Sort on multiple attributes desc / asc', async () => { + const res = await _makeCall('desc', 'asc'); + + expect(res.data.errors).toBeUndefined(); + expect(res.data.data.records.list.map((record: {id: string}) => record.id)).toEqual([ + sfRecord3, + sfRecord2, + sfRecord1 + ]); + }); + + test('Sort on multiple attributes asc / desc', async () => { + const res = await _makeCall('asc', 'desc'); + + expect(res.data.errors).toBeUndefined(); + expect(res.data.data.records.list.map((record: {id: string}) => record.id)).toEqual([ + sfRecord1, + sfRecord2, + sfRecord3 + ]); + }); + + test('Sort on multiple attributes desc / desc', async () => { + const res = await _makeCall('desc', 'desc'); + + expect(res.data.errors).toBeUndefined(); + expect(res.data.data.records.list.map((record: {id: string}) => record.id)).toEqual([ + sfRecord3, + sfRecord1, + sfRecord2 + ]); + }); + }); }); describe('On simple extended attribute', () => { diff --git a/apps/core/src/app/core/recordApp/_types.ts b/apps/core/src/app/core/recordApp/_types.ts index 68eb85d93..e9dd86d8c 100644 --- a/apps/core/src/app/core/recordApp/_types.ts +++ b/apps/core/src/app/core/recordApp/_types.ts @@ -38,7 +38,9 @@ export interface IRecordsQueryPagination { export interface IRecordsQueryVariables { library: string; filters?: IRecordFilterLight[]; + // @deprecated Should use multipleSort. They are both here for backward compatibility. Eventually, multipleSort will replace sort. sort?: IRecordSortLight; + multipleSort?: IRecordSortLight[]; version?: IRecordsQueryVersion[]; pagination?: IRecordsQueryPagination; retrieveInactive?: boolean; diff --git a/apps/core/src/app/core/recordApp/recordApp.ts b/apps/core/src/app/core/recordApp/recordApp.ts index bfba3d1d2..1f7e526f4 100644 --- a/apps/core/src/app/core/recordApp/recordApp.ts +++ b/apps/core/src/app/core/recordApp/recordApp.ts @@ -246,7 +246,8 @@ export default function ({ records( library: ID!, filters: [RecordFilterInput], - sort: RecordSortInput + sort: RecordSortInput @deprecated(reason: "Should use multipleSort. They are both here for backward compatibility. Eventually, multipleSort will replace sort."), + multipleSort: [RecordSortInput!] version: [ValueVersionInput], pagination: RecordsPagination, retrieveInactive: Boolean, @@ -274,6 +275,7 @@ export default function ({ library, filters, sort, + multipleSort, version, pagination, retrieveInactive = false, @@ -302,7 +304,7 @@ export default function ({ const params: IFindRecordParams = { library, filters, - sort, + sort: multipleSort ?? [sort], pagination: pagination?.cursor ? (pagination as ICursorPaginationParams) : (pagination as IPaginationParams), diff --git a/apps/core/src/domain/record/_types.ts b/apps/core/src/domain/record/_types.ts index df450f8f9..15ab00513 100644 --- a/apps/core/src/domain/record/_types.ts +++ b/apps/core/src/domain/record/_types.ts @@ -9,7 +9,7 @@ import {IValue, IValuesOptions, IValueVersion} from '_types/value'; export interface IFindRecordParams { library: string; filters?: IRecordFilterLight[]; - sort?: IRecordSortLight; + sort?: IRecordSortLight[]; options?: IValuesOptions; pagination?: IPaginationParams | ICursorPaginationParams; withCount?: boolean; diff --git a/apps/core/src/domain/record/recordDomain.ts b/apps/core/src/domain/record/recordDomain.ts index 068728d82..5b05ba080 100644 --- a/apps/core/src/domain/record/recordDomain.ts +++ b/apps/core/src/domain/record/recordDomain.ts @@ -1029,7 +1029,7 @@ export default function ({ const {library, sort, pagination, withCount, retrieveInactive = false} = params; const {filters = [] as IRecordFilterLight[], fulltextSearch} = params; const fullFilters: IRecordFilterOption[] = []; - let fullSort: IRecordSort; + let fullSort: IRecordSort[] = []; await validateHelper.validateLibrary(library, ctx); @@ -1123,39 +1123,43 @@ export default function ({ } // Check sort fields - if (sort) { - const sortAttributes = await getAttributesFromField({ - field: sort.field, - condition: null, - deps: { - 'core.domain.attribute': attributeDomain, - 'core.infra.library': libraryRepo, - 'core.infra.tree': treeRepo - }, - ctx - }); + if (sort?.length) { + fullSort = await Promise.all( + sort.filter(Boolean).map(async s => { + const sortAttributes = await getAttributesFromField({ + field: s.field, + condition: null, + deps: { + 'core.domain.attribute': attributeDomain, + 'core.infra.library': libraryRepo, + 'core.infra.tree': treeRepo + }, + ctx + }); - const sortAttributesRepo = (await Promise.all( - sortAttributes.map(async a => - !!a.reverse_link - ? { - ...a, - reverse_link: await attributeDomain.getAttributeProperties({ - id: a.reverse_link as string, - ctx - }) - } - : a - ) - )) as IAttributeWithRevLink[]; - - fullSort = { - attributes: sortAttributesRepo, - order: sort.order - }; + const sortAttributesRepo = (await Promise.all( + sortAttributes.map(async a => + !!a.reverse_link + ? { + ...a, + reverse_link: await attributeDomain.getAttributeProperties({ + id: a.reverse_link as string, + ctx + }) + } + : a + ) + )) as IAttributeWithRevLink[]; + + return { + attributes: sortAttributesRepo, + order: s.order + }; + }, []) + ); } - const records = await recordRepo.find({ + return recordRepo.find({ libraryId: library, filters: fullFilters, sort: fullSort, @@ -1165,8 +1169,6 @@ export default function ({ fulltextSearch, ctx }); - - return records; }, getRecordIdentity: _getRecordIdentity, async getRecordFieldValue({library, record, attributeId, options, ctx}) { diff --git a/apps/core/src/infra/attributeTypes/__snapshots__/attributeAdvancedLinkRepo.spec.ts.snap b/apps/core/src/infra/attributeTypes/__snapshots__/attributeAdvancedLinkRepo.spec.ts.snap index be1483ced..1c0e47018 100644 --- a/apps/core/src/infra/attributeTypes/__snapshots__/attributeAdvancedLinkRepo.spec.ts.snap +++ b/apps/core/src/infra/attributeTypes/__snapshots__/attributeAdvancedLinkRepo.spec.ts.snap @@ -300,7 +300,7 @@ exports[`AttributeAdvancedLinkRepo sortQueryPart Should return advanced link sor "value2": "linked", "value3": "ASC", }, - "query": "SORT FIRST( + "query": "FIRST( FOR v, e IN 1 OUTBOUND r._id @@value0 FILTER e.attribute == @value1 RETURN v.@value2 diff --git a/apps/core/src/infra/attributeTypes/__snapshots__/attributeAdvancedRepo.spec.ts.snap b/apps/core/src/infra/attributeTypes/__snapshots__/attributeAdvancedRepo.spec.ts.snap index 7fad2ff7d..2dfcbeddb 100644 --- a/apps/core/src/infra/attributeTypes/__snapshots__/attributeAdvancedRepo.spec.ts.snap +++ b/apps/core/src/infra/attributeTypes/__snapshots__/attributeAdvancedRepo.spec.ts.snap @@ -135,7 +135,7 @@ exports[`AttributeStandardRepo sortQueryPart Should return advanced filter 1`] = "value1": "label", "value2": "ASC", }, - "query": "SORT FIRST( + "query": "FIRST( FOR v, e IN 1 OUTBOUND r._id @@value0 FILTER e.attribute == @value1 RETURN v.value diff --git a/apps/core/src/infra/attributeTypes/__snapshots__/attributeSimpleLinkRepo.spec.ts.snap b/apps/core/src/infra/attributeTypes/__snapshots__/attributeSimpleLinkRepo.spec.ts.snap index 49d5d4389..a27e2863f 100644 --- a/apps/core/src/infra/attributeTypes/__snapshots__/attributeSimpleLinkRepo.spec.ts.snap +++ b/apps/core/src/infra/attributeTypes/__snapshots__/attributeSimpleLinkRepo.spec.ts.snap @@ -113,7 +113,7 @@ exports[`AttributeSimpleLinkRepo sortQueryPart Should return simple link sort 1` "value2": "linked", "value3": "ASC", }, - "query": "SORT + "query": " FIRST(FOR l IN @value0 FILTER TO_STRING(r.@value1) == l._key RETURN l.@value2) diff --git a/apps/core/src/infra/attributeTypes/__snapshots__/attributeSimpleRepo.spec.ts.snap b/apps/core/src/infra/attributeTypes/__snapshots__/attributeSimpleRepo.spec.ts.snap index 8b77b956d..ae814c3a8 100644 --- a/apps/core/src/infra/attributeTypes/__snapshots__/attributeSimpleRepo.spec.ts.snap +++ b/apps/core/src/infra/attributeTypes/__snapshots__/attributeSimpleRepo.spec.ts.snap @@ -90,6 +90,6 @@ exports[`AttributeSimpleRepo sortQueryPart Should return simple sort 1`] = ` "value0": "_key", "value1": "ASC", }, - "query": "SORT r.@value0 @value1", + "query": "r.@value0 @value1", } `; diff --git a/apps/core/src/infra/attributeTypes/__snapshots__/attributeTreeRepo.spec.ts.snap b/apps/core/src/infra/attributeTypes/__snapshots__/attributeTreeRepo.spec.ts.snap index 02aceaf50..7a93d793d 100644 --- a/apps/core/src/infra/attributeTypes/__snapshots__/attributeTreeRepo.spec.ts.snap +++ b/apps/core/src/infra/attributeTypes/__snapshots__/attributeTreeRepo.spec.ts.snap @@ -170,7 +170,7 @@ exports[`AttributeTreeRepo sortQueryPart Should return tree filter 1`] = ` "value3": "linked", "value4": "ASC", }, - "query": "SORT FIRST( + "query": "FIRST( FOR v, e IN 1 OUTBOUND r._id @@value0, @@value1 FILTER e.attribute == @value2 diff --git a/apps/core/src/infra/attributeTypes/attributeAdvancedLinkRepo.spec.ts b/apps/core/src/infra/attributeTypes/attributeAdvancedLinkRepo.spec.ts index 014030205..e4f2e530b 100644 --- a/apps/core/src/infra/attributeTypes/attributeAdvancedLinkRepo.spec.ts +++ b/apps/core/src/infra/attributeTypes/attributeAdvancedLinkRepo.spec.ts @@ -1066,7 +1066,6 @@ describe('AttributeAdvancedLinkRepo', () => { order: 'ASC' }); - expect(filter.query).toMatch(/^SORT/); expect(filter).toMatchSnapshot(); }); }); diff --git a/apps/core/src/infra/attributeTypes/attributeAdvancedLinkRepo.ts b/apps/core/src/infra/attributeTypes/attributeAdvancedLinkRepo.ts index 7e3a8e33b..ebff0f823 100644 --- a/apps/core/src/infra/attributeTypes/attributeAdvancedLinkRepo.ts +++ b/apps/core/src/infra/attributeTypes/attributeAdvancedLinkRepo.ts @@ -1,7 +1,7 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {aql, AqlQuery, GeneratedAqlQuery, join, literal} from 'arangojs/aql'; +import {aql, GeneratedAqlQuery, join, literal} from 'arangojs/aql'; import {IFilterTypesHelper} from 'infra/record/helpers/filterTypes'; import {IUtils} from 'utils/utils'; import {ILinkValue, IValueEdge} from '_types/value'; @@ -10,7 +10,7 @@ import {AttributeFormats, AttributeTypes, IAttribute} from '../../_types/attribu import {IRecord} from '../../_types/record'; import {IDbService} from '../db/dbService'; import {IDbUtils} from '../db/dbUtils'; -import {BASE_QUERY_IDENTIFIER, IAttributeTypeRepo, IAttributeWithRevLink} from './attributeTypesRepo'; +import {BASE_QUERY_IDENTIFIER, IAttributeTypeRepo} from './attributeTypesRepo'; import {GetConditionPart} from './helpers/getConditionPart'; interface ISavedValueResult { @@ -296,7 +296,7 @@ export default function ({ return _buildLinkValue(dbUtils.cleanup(res[0].linkedRecord), res[0].edge, !!attribute.reverse_link); }, - sortQueryPart({attributes, order}: {attributes: IAttributeWithRevLink[]; order: string}): AqlQuery { + sortQueryPart({attributes, order}) { const collec = dbService.db.collection(VALUES_LINKS_COLLECTION); const linked = !attributes[1] ? {id: '_key', format: AttributeFormats.TEXT} @@ -324,12 +324,9 @@ export default function ({ `; } - const query = - linked.format !== AttributeFormats.EXTENDED - ? aql`SORT ${linkedValue} ${order}` - : aql`SORT ${_getExtendedFilterPart(attributes, linkedValue)} ${order}`; - - return query; + return linked.format !== AttributeFormats.EXTENDED + ? aql`${linkedValue} ${order}` + : aql`${_getExtendedFilterPart(attributes, linkedValue)} ${order}`; }, filterValueQueryPart(attributes, filter, parentIdentifier = BASE_QUERY_IDENTIFIER) { const collec = dbService.db.collection(VALUES_LINKS_COLLECTION); diff --git a/apps/core/src/infra/attributeTypes/attributeAdvancedRepo.spec.ts b/apps/core/src/infra/attributeTypes/attributeAdvancedRepo.spec.ts index beb639735..ee6d84a8f 100644 --- a/apps/core/src/infra/attributeTypes/attributeAdvancedRepo.spec.ts +++ b/apps/core/src/infra/attributeTypes/attributeAdvancedRepo.spec.ts @@ -658,7 +658,6 @@ describe('AttributeStandardRepo', () => { order: 'ASC' }); - expect(filter.query).toMatch(/^SORT/); expect(filter).toMatchSnapshot(); }); }); diff --git a/apps/core/src/infra/attributeTypes/attributeAdvancedRepo.ts b/apps/core/src/infra/attributeTypes/attributeAdvancedRepo.ts index 77da91393..4638a7754 100644 --- a/apps/core/src/infra/attributeTypes/attributeAdvancedRepo.ts +++ b/apps/core/src/infra/attributeTypes/attributeAdvancedRepo.ts @@ -1,14 +1,14 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {aql, AqlQuery, GeneratedAqlQuery, join, literal} from 'arangojs/aql'; +import {aql, GeneratedAqlQuery, join, literal} from 'arangojs/aql'; import {DocumentCollection, EdgeCollection} from 'arangojs/collection'; import {IDbUtils} from 'infra/db/dbUtils'; import {IDbDocument, IDbEdge} from 'infra/db/_types'; import {IFilterTypesHelper} from 'infra/record/helpers/filterTypes'; import {VALUES_COLLECTION, VALUES_LINKS_COLLECTION} from '../../infra/value/valueRepo'; import {AttributeFormats, IAttribute} from '../../_types/attribute'; -import {IRecord, IRecordSort} from '../../_types/record'; +import {IRecord} from '../../_types/record'; import {IValue, IValueEdge} from '../../_types/value'; import {IDbService} from '../db/dbService'; import {BASE_QUERY_IDENTIFIER, IAttributeTypeRepo} from './attributeTypesRepo'; @@ -250,7 +250,7 @@ export default function ({ created_by: valueLinks[0].created_by }; }, - sortQueryPart({attributes, order}: IRecordSort): AqlQuery { + sortQueryPart({attributes, order}) { const collec = dbService.db.collection(VALUES_LINKS_COLLECTION); const advancedValue = aql`FIRST( @@ -259,12 +259,9 @@ export default function ({ FILTER e.attribute == ${attributes[0].id} RETURN v.value )`; - const query = - attributes[0].format === AttributeFormats.EXTENDED && attributes.length > 1 - ? aql`SORT ${_getExtendedFilterPart(attributes, advancedValue)} ${order}` - : aql`SORT ${advancedValue} ${order}`; - - return query; + return attributes[0].format === AttributeFormats.EXTENDED && attributes.length > 1 + ? aql`${_getExtendedFilterPart(attributes, advancedValue)} ${order}` + : aql`${advancedValue} ${order}`; }, filterValueQueryPart(attributes, filter, parentIdentifier = BASE_QUERY_IDENTIFIER) { const vIdentifier = literal(parentIdentifier + 'v'); diff --git a/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.spec.ts b/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.spec.ts index 73c1ae5ea..f863e4125 100644 --- a/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.spec.ts +++ b/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.spec.ts @@ -381,7 +381,6 @@ describe('AttributeSimpleLinkRepo', () => { order: 'ASC' }); - expect(filter.query).toMatch(/^SORT/); expect(filter).toMatchSnapshot(); }); }); diff --git a/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.ts b/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.ts index 2c2f23d85..ce217fe31 100644 --- a/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.ts +++ b/apps/core/src/infra/attributeTypes/attributeSimpleLinkRepo.ts @@ -1,7 +1,7 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {aql, AqlQuery, GeneratedAqlQuery, join, literal} from 'arangojs/aql'; +import {aql, GeneratedAqlQuery, join, literal} from 'arangojs/aql'; import {IDbDocument} from 'infra/db/_types'; import {IFilterTypesHelper} from 'infra/record/helpers/filterTypes'; import {IRecord} from '_types/record'; @@ -129,7 +129,7 @@ export default function ({ modified_by: null })); }, - sortQueryPart({attributes, order}: {attributes: IAttribute[]; order: string}): AqlQuery { + sortQueryPart({attributes, order}) { const linkedLibCollec = dbService.db.collection(attributes[0].linked_library); const linked = !attributes[1] ? {id: '_key', format: AttributeFormats.TEXT} @@ -143,12 +143,9 @@ export default function ({ RETURN l.${linked.id}) `; - const query: AqlQuery = - linked.format !== AttributeFormats.EXTENDED - ? aql`SORT ${linkedValue} ${order}` - : aql`SORT ${_getExtendedFilterPart(attributes, linkedValue)} ${order}`; - - return query; + return linked.format !== AttributeFormats.EXTENDED + ? aql`${linkedValue} ${order}` + : aql`${_getExtendedFilterPart(attributes, linkedValue)} ${order}`; }, filterValueQueryPart(attributes, filter, parentIdentifier = BASE_QUERY_IDENTIFIER) { const isCountFilter = filterTypesHelper.isCountFilter(filter); diff --git a/apps/core/src/infra/attributeTypes/attributeSimpleRepo.spec.ts b/apps/core/src/infra/attributeTypes/attributeSimpleRepo.spec.ts index 648f40ee1..01f80f1da 100644 --- a/apps/core/src/infra/attributeTypes/attributeSimpleRepo.spec.ts +++ b/apps/core/src/infra/attributeTypes/attributeSimpleRepo.spec.ts @@ -128,7 +128,6 @@ describe('AttributeSimpleRepo', () => { order: 'ASC' }); - expect(filter.query).toMatch(/^SORT/); expect(filter).toMatchSnapshot(); }); }); diff --git a/apps/core/src/infra/attributeTypes/attributeSimpleRepo.ts b/apps/core/src/infra/attributeTypes/attributeSimpleRepo.ts index 6acd0efc6..aa36adcbb 100644 --- a/apps/core/src/infra/attributeTypes/attributeSimpleRepo.ts +++ b/apps/core/src/infra/attributeTypes/attributeSimpleRepo.ts @@ -1,7 +1,7 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {aql, AqlQuery, GeneratedAqlQuery, literal} from 'arangojs/aql'; +import {aql, GeneratedAqlQuery, literal} from 'arangojs/aql'; import {IFilterTypesHelper} from 'infra/record/helpers/filterTypes'; import {IQueryInfos} from '_types/queryInfos'; import {AttributeFormats, IAttribute} from '../../_types/attribute'; @@ -102,15 +102,12 @@ export default function ({ } ]; }, - sortQueryPart({attributes, order}: {attributes: IAttribute[]; order: string}): AqlQuery { + sortQueryPart({attributes, order}) { attributes[0].id = attributes[0].id === 'id' ? '_key' : attributes[0].id; - const query: AqlQuery = - attributes[0].format === AttributeFormats.EXTENDED && attributes.length > 1 - ? aql`SORT ${_getExtendedFilterPart(attributes)} ${order}` - : aql`SORT r.${attributes[0].id} ${order}`; - - return query; + return attributes[0].format === AttributeFormats.EXTENDED && attributes.length > 1 + ? aql`${_getExtendedFilterPart(attributes)} ${order}` + : aql`r.${attributes[0].id} ${order}`; }, filterValueQueryPart(attributes, filter, parentIdentifier = BASE_QUERY_IDENTIFIER) { let recordValue: GeneratedAqlQuery; diff --git a/apps/core/src/infra/attributeTypes/attributeTreeRepo.spec.ts b/apps/core/src/infra/attributeTypes/attributeTreeRepo.spec.ts index f58a1f6dc..4344bfd9d 100644 --- a/apps/core/src/infra/attributeTypes/attributeTreeRepo.spec.ts +++ b/apps/core/src/infra/attributeTypes/attributeTreeRepo.spec.ts @@ -712,7 +712,6 @@ describe('AttributeTreeRepo', () => { order: 'ASC' }); - expect(filter.query).toMatch(/^SORT/); expect(filter).toMatchSnapshot(); }); }); diff --git a/apps/core/src/infra/attributeTypes/attributeTreeRepo.ts b/apps/core/src/infra/attributeTypes/attributeTreeRepo.ts index e18822967..60c1b9a91 100644 --- a/apps/core/src/infra/attributeTypes/attributeTreeRepo.ts +++ b/apps/core/src/infra/attributeTypes/attributeTreeRepo.ts @@ -1,7 +1,7 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {aql, AqlQuery, GeneratedAqlQuery, literal, join} from 'arangojs/aql'; +import {aql, GeneratedAqlQuery, join, literal} from 'arangojs/aql'; import {IDbDocument} from 'infra/db/_types'; import {IFilterTypesHelper} from 'infra/record/helpers/filterTypes'; import {IUtils} from 'utils/utils'; @@ -264,7 +264,7 @@ export default function ({ res[0].edge ); }, - sortQueryPart({attributes, order}: {attributes: IAttribute[]; order: string}): AqlQuery { + sortQueryPart({attributes, order}) { const valuesLinksCollec = dbService.db.collection(VALUES_LINKS_COLLECTION); const treeCollec = dbService.db.collection(getEdgesCollectionName(attributes[0].linked_tree)); @@ -285,12 +285,9 @@ export default function ({ RETURN record.${linked.id} )`; - const query = - linked.format !== AttributeFormats.EXTENDED - ? aql`SORT ${linkedValue} ${order}` - : aql`SORT ${_getExtendedFilterPart(attributes, linkedValue)} ${order}`; - - return query; + return linked.format !== AttributeFormats.EXTENDED + ? aql`${linkedValue} ${order}` + : aql`${_getExtendedFilterPart(attributes, linkedValue)} ${order}`; }, filterValueQueryPart(attributes, filter, parentIdentifier = BASE_QUERY_IDENTIFIER) { const valuesLinksCollec = dbService.db.collection(VALUES_LINKS_COLLECTION); diff --git a/apps/core/src/infra/attributeTypes/attributeTypesRepo.ts b/apps/core/src/infra/attributeTypes/attributeTypesRepo.ts index da02fb161..3d6e52787 100644 --- a/apps/core/src/infra/attributeTypes/attributeTypesRepo.ts +++ b/apps/core/src/infra/attributeTypes/attributeTypesRepo.ts @@ -164,7 +164,7 @@ export interface IAttributeTypeRepo { /** * Return AQL query part to sort on this attribute */ - sortQueryPart({attributes, order}: {attributes: IAttributeWithRevLink[]; order: string}): AqlQuery; + sortQueryPart({attributes, order}: {attributes: IAttributeWithRevLink[]; order: string}): GeneratedAqlQuery; /** * Clear all values of given attribute. Can be used to cleanup values when an attribute is deleted for example. diff --git a/apps/core/src/infra/record/__snapshots__/recordRepo.spec.ts.snap b/apps/core/src/infra/record/__snapshots__/recordRepo.spec.ts.snap index 7c4d4b2ab..8af56592b 100644 --- a/apps/core/src/infra/record/__snapshots__/recordRepo.spec.ts.snap +++ b/apps/core/src/infra/record/__snapshots__/recordRepo.spec.ts.snap @@ -20,6 +20,34 @@ RETURN MERGE(r, {library: @value1})", } `; +exports[`RecordRepo find Should aggregate sorts 1`] = ` +{ + "ctx": { + "requestId": "123465", + "userId": "0", + }, + "query": { + "_source": [Function], + "bindVars": { + "@value0": "test_lib", + "value1": { + "query": "sortQueryPart", + }, + "value2": { + "query": "sortQueryPart", + }, + "value3": "test_lib", + }, + "query": "FOR r IN (@@value0) +SORT +@value1, @value2 +FILTER r.active == true +RETURN MERGE(r, {library: @value3})", + }, + "withTotalCount": undefined, +} +`; + exports[`RecordRepo find Should find records 1`] = ` { "ctx": { diff --git a/apps/core/src/infra/record/recordRepo.spec.ts b/apps/core/src/infra/record/recordRepo.spec.ts index 2e40c30c2..964b44de6 100644 --- a/apps/core/src/infra/record/recordRepo.spec.ts +++ b/apps/core/src/infra/record/recordRepo.spec.ts @@ -13,6 +13,8 @@ import {IDbUtils} from '../db/dbUtils'; import {IFilterTypesHelper} from './helpers/filterTypes'; import recordRepo, {IRecordRepoDeps} from './recordRepo'; import {ToAny} from 'utils/utils'; +import {SortOrder} from '../../_types/list'; +import {mockAttrSimple} from '../../__tests__/mocks/attribute'; const depsBase: ToAny = { 'core.infra.db.dbService': jest.fn(), @@ -540,6 +542,50 @@ describe('RecordRepo', () => { list: mockCleanupRes }); }); + + test('Should aggregate sorts', async () => { + const mockDbServ = { + db: new Database(), + execute: global.__mockPromise([]) + }; + + const mockAttributeTypes: Mockify = { + getTypeRepo: jest.fn(() => ({ + sortQueryPart: jest.fn(() => ({ + query: 'sortQueryPart' + })) + })) + }; + + const mockDbUtils: Mockify = { + cleanup: jest.fn() + }; + + const recRepo = recordRepo({ + ...depsBase, + 'core.infra.db.dbService': mockDbServ, + 'core.infra.db.dbUtils': mockDbUtils as IDbUtils, + 'core.infra.attributeTypes': mockAttributeTypes as IAttributeTypesRepo + }); + + await recRepo.find({ + libraryId: 'test_lib', + sort: [ + { + order: SortOrder.DESC, + attributes: [{...mockAttrSimple, reverse_link: undefined, id: 'attribute1'}] + }, + { + order: SortOrder.ASC, + attributes: [{...mockAttrSimple, reverse_link: undefined, id: 'attribute2'}] + } + ], + ctx + }); + + expect(mockDbServ.execute.mock.calls.length).toBe(1); + expect(mockDbServ.execute.mock.calls[0][0]).toMatchSnapshot(); + }); }); describe('find with filters', () => { diff --git a/apps/core/src/infra/record/recordRepo.ts b/apps/core/src/infra/record/recordRepo.ts index 4e5138d15..69e7e1f46 100644 --- a/apps/core/src/infra/record/recordRepo.ts +++ b/apps/core/src/infra/record/recordRepo.ts @@ -58,7 +58,7 @@ export interface IRecordRepo { find(params: { libraryId: string; filters?: IRecordFilterOption[]; - sort?: IRecordSort; + sort?: IRecordSort[]; pagination?: IPaginationParams | ICursorPaginationParams; withCount?: boolean; retrieveInactive?: boolean; @@ -90,6 +90,14 @@ export default function ({ 'core.infra.indexation.helpers.getSearchQuery': getSearchQuery, 'core.infra.attribute': attributeRepo }: IRecordRepoDeps): IRecordRepo { + const _isOffsetPagination = ( + pagination: IPaginationParams | ICursorPaginationParams + ): pagination is IPaginationParams => 'offset' in pagination; + + const _isCursorPagination = ( + pagination: IPaginationParams | ICursorPaginationParams + ): pagination is ICursorPaginationParams => 'cursor' in pagination; + const _generateCursor = (from: number, direction: CursorDirection): string => Buffer.from(`${direction}:${from}`).toString('base64'); @@ -230,12 +238,12 @@ export default function ({ } // If we have a full text search query and no specific sort, sorting by relevance is already handled. - if (sort || !fulltextSearchQuery) { - const sortQueryPart = sort - ? attributeTypesRepo.getTypeRepo(sort.attributes[0]).sortQueryPart(sort) - : aql`SORT ${literal('TO_NUMBER(r._key) DESC')}`; + if (!fulltextSearchQuery && !sort?.length) { + queryParts.push(aql`SORT ${literal('TO_NUMBER(r._key) DESC')}`); + } else if (sort?.length) { + const sortParts = sort.map(s => attributeTypesRepo.getTypeRepo(s.attributes[0]).sortQueryPart(s)); - queryParts.push(sortQueryPart as GeneratedAqlQuery); + queryParts.push(aql`SORT `, join(sortParts, ', ')); } if (!retrieveInactive && !isFilteringOnActive) { @@ -243,23 +251,21 @@ export default function ({ } if (pagination) { - if (!(pagination as IPaginationParams).offset && !(pagination as ICursorPaginationParams).cursor) { - (pagination as IPaginationParams).offset = 0; - } - - if (typeof (pagination as IPaginationParams).offset !== 'undefined') { - queryParts.push(aql`LIMIT ${(pagination as IPaginationParams).offset}, ${pagination.limit}`); - } else if ((pagination as ICursorPaginationParams).cursor) { - const {direction, from} = _parseCursor((pagination as ICursorPaginationParams).cursor); + if (_isOffsetPagination(pagination)) { + queryParts.push(aql`LIMIT ${pagination.offset}, ${pagination.limit}`); + } else if (_isCursorPagination(pagination)) { + const {direction, from} = _parseCursor(pagination.cursor); // When looking for previous records, first sort in reverse order to get the last records if (direction === CursorDirection.PREV) { - queryParts.push(aql`SORT r.created_at ASC, r._key ASC`); + queryParts.push(aql`SORT ${literal('TO_NUMBER(r._key) ASC')}`); } const operator = direction === CursorDirection.NEXT ? '<' : '>'; queryParts.push(aql`FILTER r._key ${literal(operator)} ${from}`); queryParts.push(aql`LIMIT ${pagination.limit}`); + } else { + (pagination as IPaginationParams).offset = 0; } } @@ -284,13 +290,11 @@ export default function ({ } : null; - const returnVal = { + return { totalCount, list: list.map(dbUtils.cleanup), cursor }; - - return returnVal; }, async createRecord({libraryId, recordData, ctx}): Promise { const collection = dbService.db.collection(libraryId);