From 4bfee1351df45699f727067adbdd3b45f8122085 Mon Sep 17 00:00:00 2001 From: Aditya Pimpalkar Date: Fri, 14 Jun 2024 10:23:37 +0100 Subject: [PATCH] Support orderBy as array (#5681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes: #4301 --------- Co-authored-by: FĂ©lix Malfait --- ...ThreadMessagesOperationSignatureFactory.ts | 8 +-- .../activities/files/hooks/useAttachments.tsx | 8 +-- .../hooks/__tests__/useActivities.test.tsx | 10 ++-- ...ctivityTargetsForTargetableObject.test.tsx | 2 +- .../usePrepareFindManyActivitiesQuery.ts | 2 +- .../activities/notes/hooks/useNotes.ts | 2 +- .../activities/tasks/hooks/useTasks.ts | 4 +- .../FindManyTimelineActivitiesOrderBy.ts | 8 +-- .../makeTimelineActivitiesQueryVariables.ts | 8 +-- .../hooks/useTimelineActivities.tsx | 8 +-- .../utils/sortCachedObjectEdges.ts | 2 +- .../useCreateOneObjectMetadataItem.ts | 2 +- .../useGetObjectOrderByField.test.tsx | 6 +-- .../__tests__/getObjectOrderByField.test.ts | 6 +-- .../utils/getObjectOrderByField.ts | 28 ++++++----- .../types/RecordGqlOperationOrderBy.ts | 4 +- .../useFindManyRecordsQuery.test.tsx | 2 +- ...useGenerateCombinedFindManyRecordsQuery.ts | 4 +- .../__tests__/turnSortsIntoOrderBy.test.tsx | 31 ++++++------ .../utils/turnSortsIntoOrderBy.ts | 29 +++++------ .../__tests__/useMultiObjectSearch.test.tsx | 8 +-- .../hooks/useOrderByFieldPerMetadataItem.ts | 4 +- .../utils/generateFindManyRecordsQuery.ts | 4 +- .../__mocks__/useFilteredSearchEntityQuery.ts | 8 +-- .../hooks/useFilteredSearchEntityQuery.ts | 6 +-- .../__tests__/args-string.factory.spec.ts | 25 +++++----- .../factories/args-string.factory.ts | 49 ++++++++++++------- .../interfaces/record.interface.ts | 4 +- .../utils/__tests__/get-resolver-args.spec.ts | 6 ++- .../utils/get-resolver-args.util.ts | 1 + .../factories/find-many-query.factory.ts | 2 +- .../__tests__/order-by-input.factory.spec.ts | 36 +++++++------- .../input-factories/order-by-input.factory.ts | 14 ++++-- .../__tests__/check-fields.utils.spec.ts | 18 +++++++ .../utils/check-order-by.utils.ts | 47 ++++++++++++++++++ 35 files changed, 249 insertions(+), 157 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/check-order-by.utils.ts diff --git a/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts b/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts index 619a99b0dd23..ee5e93da74fa 100644 --- a/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts +++ b/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts @@ -10,9 +10,11 @@ export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperation eq: messageThreadId || '', }, }, - orderBy: { - receivedAt: 'AscNullsLast', - }, + orderBy: [ + { + receivedAt: 'AscNullsLast', + }, + ], limit: 10, }, fields: { diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx index 2d8539889d27..cb8a1837d81e 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useAttachments.tsx @@ -17,9 +17,11 @@ export const useAttachments = (targetableObject: ActivityTargetableObject) => { eq: targetableObject.id, }, }, - orderBy: { - createdAt: 'DescNullsFirst', - }, + orderBy: [ + { + createdAt: 'DescNullsFirst', + }, + ], }); return { diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx index 4e149d280fbf..b3ca9e5349d2 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx @@ -49,7 +49,7 @@ const mocks: MockedResponse[] = [ query: gql` query FindManyActivityTargets( $filter: ActivityTargetFilterInput - $orderBy: ActivityTargetOrderByInput + $orderBy: [ActivityTargetOrderByInput] $lastCursor: String $limit: Int ) { @@ -103,7 +103,7 @@ const mocks: MockedResponse[] = [ query: gql` query FindManyActivities( $filter: ActivityFilterInput - $orderBy: ActivityOrderByInput + $orderBy: [ActivityOrderByInput] $lastCursor: String $limit: Int ) { @@ -142,7 +142,7 @@ const mocks: MockedResponse[] = [ variables: { filter: { id: { in: ['234'] } }, limit: undefined, - orderBy: {}, + orderBy: [{}], }, }, result: jest.fn(() => ({ @@ -178,7 +178,7 @@ describe('useActivities', () => { useActivities({ targetableObjects: [], activitiesFilters: {}, - activitiesOrderByVariables: {}, + activitiesOrderByVariables: [{}], skip: false, }), { wrapper: Wrapper }, @@ -202,7 +202,7 @@ describe('useActivities', () => { { targetObjectNameSingular: 'company', id: '123' }, ], activitiesFilters: {}, - activitiesOrderByVariables: {}, + activitiesOrderByVariables: [{}], skip: false, }); return { activities, setCurrentWorkspaceMember }; diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetsForTargetableObject.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetsForTargetableObject.test.tsx index 9426e1cb7fbb..21f429c6b8c6 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetsForTargetableObject.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetsForTargetableObject.test.tsx @@ -34,7 +34,7 @@ const mocks: MockedResponse[] = [ query: gql` query FindManyActivityTargets( $filter: ActivityTargetFilterInput - $orderBy: ActivityTargetOrderByInput + $orderBy: [ActivityTargetOrderByInput] $lastCursor: String $limit: Int ) { diff --git a/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts b/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts index 7f5d9c492c8a..2951b7522ce0 100644 --- a/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts +++ b/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts @@ -109,7 +109,7 @@ export const usePrepareFindManyActivitiesQuery = () => { objectRecordsToOverwrite: filteredActivities, queryVariables: { ...nextFindManyActivitiesQueryFilter, - orderBy: { createdAt: 'DescNullsFirst' }, + orderBy: [{ createdAt: 'DescNullsFirst' }], }, recordGqlFields: FIND_ACTIVITIES_OPERATION_SIGNATURE.fields, computeReferences: true, diff --git a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts index b50231f9e413..383061c5e04f 100644 --- a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts +++ b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts @@ -24,7 +24,7 @@ export const useNotes = (targetableObject: ActivityTargetableObject) => { const { activities, loading } = useActivities({ activitiesFilters: notesQueryVariables.filter ?? {}, - activitiesOrderByVariables: notesQueryVariables.orderBy ?? {}, + activitiesOrderByVariables: notesQueryVariables.orderBy ?? [{}], targetableObjects: [targetableObject], }); diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts index bb8fb16566ac..f7938d17bc94 100644 --- a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts +++ b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts @@ -110,13 +110,13 @@ export const useTasks = ({ const { activities: completeTasksData } = useActivities({ targetableObjects, activitiesFilters: completedQueryVariables.filter ?? {}, - activitiesOrderByVariables: completedQueryVariables.orderBy ?? {}, + activitiesOrderByVariables: completedQueryVariables.orderBy ?? [{}], }); const { activities: incompleteTaskData } = useActivities({ targetableObjects, activitiesFilters: incompleteQueryVariables.filter ?? {}, - activitiesOrderByVariables: incompleteQueryVariables.orderBy ?? {}, + activitiesOrderByVariables: incompleteQueryVariables.orderBy ?? [{}], }); const todayOrPreviousTasks = incompleteTaskData?.filter((task) => { diff --git a/packages/twenty-front/src/modules/activities/timeline/constants/FindManyTimelineActivitiesOrderBy.ts b/packages/twenty-front/src/modules/activities/timeline/constants/FindManyTimelineActivitiesOrderBy.ts index 4e48c6dc5f65..ea6c4763571a 100644 --- a/packages/twenty-front/src/modules/activities/timeline/constants/FindManyTimelineActivitiesOrderBy.ts +++ b/packages/twenty-front/src/modules/activities/timeline/constants/FindManyTimelineActivitiesOrderBy.ts @@ -1,6 +1,8 @@ import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy'; export const FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY: RecordGqlOperationOrderBy = - { - createdAt: 'DescNullsFirst', - }; + [ + { + createdAt: 'DescNullsFirst', + }, + ]; diff --git a/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts b/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts index c8a7030d2d99..4a9c67f5b54c 100644 --- a/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts +++ b/packages/twenty-front/src/modules/activities/timeline/utils/makeTimelineActivitiesQueryVariables.ts @@ -13,8 +13,10 @@ export const makeTimelineActivitiesQueryVariables = ({ in: [...activityIds].sort(sortByAscString), }, }, - orderBy: { - createdAt: 'DescNullsFirst', - }, + orderBy: [ + { + createdAt: 'DescNullsFirst', + }, + ], }; }; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.tsx index a6173b5d9953..e18dff4befe6 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.tsx @@ -23,9 +23,11 @@ export const useTimelineActivities = ( eq: targetableObject.id, }, }, - orderBy: { - createdAt: 'DescNullsFirst', - }, + orderBy: [ + { + createdAt: 'DescNullsFirst', + }, + ], recordGqlFields: { id: true, createdAt: true, diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/sortCachedObjectEdges.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/sortCachedObjectEdges.ts index aa8434f0ba80..6f76dd66a5a8 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/sortCachedObjectEdges.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/sortCachedObjectEdges.ts @@ -16,7 +16,7 @@ export const sortCachedObjectEdges = ({ orderBy: RecordGqlOperationOrderBy; readCacheField: ReadFieldFunction; }) => { - const [orderByFieldName, orderByFieldValue] = Object.entries(orderBy)[0]; + const [orderByFieldName, orderByFieldValue] = Object.entries(orderBy[0])[0]; const [orderBySubFieldName, orderBySubFieldValue] = typeof orderByFieldValue === 'string' ? [] diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts index f142bffbde25..ce4d5cfbe41b 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts @@ -24,7 +24,7 @@ export const query = gql` export const findManyViewsQuery = gql` query FindManyViews( $filter: ViewFilterInput - $orderBy: ViewOrderByInput + $orderBy: [ViewOrderByInput] $lastCursor: String $limit: Int ) { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectOrderByField.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectOrderByField.test.tsx index 646806c9762a..21ec56ad60b4 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectOrderByField.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectOrderByField.test.tsx @@ -26,8 +26,8 @@ describe('useGetObjectOrderByField', () => { }, ); - expect(result.current).toEqual({ - name: { firstName: 'AscNullsLast', lastName: 'AscNullsLast' }, - }); + expect(result.current).toEqual([ + { name: { firstName: 'AscNullsLast', lastName: 'AscNullsLast' } }, + ]); }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectOrderByField.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectOrderByField.test.ts index 7cecf5668b25..baa61c853751 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectOrderByField.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectOrderByField.test.ts @@ -9,8 +9,8 @@ describe('getObjectOrderByField', () => { (item) => item.nameSingular === 'person', )!; const res = getOrderByFieldForObjectMetadataItem(objectMetadataItem); - expect(res).toEqual({ - name: { firstName: 'AscNullsLast', lastName: 'AscNullsLast' }, - }); + expect(res).toEqual([ + { name: { firstName: 'AscNullsLast', lastName: 'AscNullsLast' } }, + ]); }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectOrderByField.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectOrderByField.ts index 97d31c9a6b55..4b3b080f848a 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectOrderByField.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectOrderByField.ts @@ -15,20 +15,26 @@ export const getOrderByFieldForObjectMetadataItem = ( if (isDefined(labelIdentifierFieldMetadata)) { switch (labelIdentifierFieldMetadata.type) { case FieldMetadataType.FullName: - return { - [labelIdentifierFieldMetadata.name]: { - firstName: orderBy ?? 'AscNullsLast', - lastName: orderBy ?? 'AscNullsLast', + return [ + { + [labelIdentifierFieldMetadata.name]: { + firstName: orderBy ?? 'AscNullsLast', + lastName: orderBy ?? 'AscNullsLast', + }, }, - }; + ]; default: - return { - [labelIdentifierFieldMetadata.name]: orderBy ?? 'AscNullsLast', - }; + return [ + { + [labelIdentifierFieldMetadata.name]: orderBy ?? 'AscNullsLast', + }, + ]; } } else { - return { - createdAt: orderBy ?? 'DescNullsLast', - }; + return [ + { + createdAt: orderBy ?? 'DescNullsLast', + }, + ]; } }; diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationOrderBy.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationOrderBy.ts index 67592770d176..b1180d6c6860 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationOrderBy.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationOrderBy.ts @@ -1,5 +1,5 @@ import { OrderBy } from '@/object-metadata/types/OrderBy'; -export type RecordGqlOperationOrderBy = { +export type RecordGqlOperationOrderBy = Array<{ [fieldName: string]: OrderBy | { [subFieldName: string]: OrderBy }; -}; +}>; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecordsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecordsQuery.test.tsx index 21cfb7f0f2e9..47082dec0f2f 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecordsQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecordsQuery.test.tsx @@ -5,7 +5,7 @@ import { RecoilRoot } from 'recoil'; import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; const expectedQueryTemplate = ` - query FindManyPeople($filter: PersonFilterInput, $orderBy: PersonOrderByInput, $lastCursor: String, $limit: Int) { + query FindManyPeople($filter: PersonFilterInput, $orderBy: [PersonOrderByInput], $lastCursor: String, $limit: Int) { people(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor) { edges { node { diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts index a60d7d5b67aa..9e7935792c6c 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts @@ -32,9 +32,9 @@ export const useGenerateCombinedFindManyRecordsQuery = ({ const orderByPerMetadataItemArray = operationSignatures .map( ({ objectNameSingular }) => - `$orderBy${capitalize(objectNameSingular)}: ${capitalize( + `$orderBy${capitalize(objectNameSingular)}: [${capitalize( objectNameSingular, - )}OrderByInput`, + )}OrderByInput]`, ) .join(', '); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx index 0abde78e58e3..8534f11cabc3 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx @@ -30,9 +30,11 @@ describe('turnSortsIntoOrderBy', () => { it('should sort by recordPosition if no sorts', () => { const fields = [{ id: 'field1', name: 'createdAt' }] as FieldMetadataItem[]; expect(turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, [])).toEqual( - { - position: 'AscNullsFirst', - }, + [ + { + position: 'AscNullsFirst', + }, + ], ); }); @@ -47,10 +49,7 @@ describe('turnSortsIntoOrderBy', () => { const fields = [{ id: 'field1', name: 'field1' }] as FieldMetadataItem[]; expect( turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, sorts), - ).toEqual({ - field1: 'AscNullsFirst', - position: 'AscNullsFirst', - }); + ).toEqual([{ field1: 'AscNullsFirst' }, { position: 'AscNullsFirst' }]); }); it('should create OrderByField with multiple sorts', () => { @@ -72,11 +71,11 @@ describe('turnSortsIntoOrderBy', () => { ] as FieldMetadataItem[]; expect( turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, sorts), - ).toEqual({ - field1: 'AscNullsFirst', - field2: 'DescNullsLast', - position: 'AscNullsFirst', - }); + ).toEqual([ + { field1: 'AscNullsFirst' }, + { field2: 'DescNullsLast' }, + { position: 'AscNullsFirst' }, + ]); }); it('should ignore if field not found', () => { @@ -87,9 +86,9 @@ describe('turnSortsIntoOrderBy', () => { definition: sortDefinition, }, ]; - expect(turnSortsIntoOrderBy(objectMetadataItem, sorts)).toEqual({ - position: 'AscNullsFirst', - }); + expect(turnSortsIntoOrderBy(objectMetadataItem, sorts)).toEqual([ + { position: 'AscNullsFirst' }, + ]); }); it('should not return position for remotes', () => { @@ -102,6 +101,6 @@ describe('turnSortsIntoOrderBy', () => { ]; expect( turnSortsIntoOrderBy({ ...objectMetadataItem, isRemote: true }, sorts), - ).toEqual({}); + ).toEqual([]); }); }); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts index b30f245707f6..b33c7ea1e8a6 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts @@ -15,28 +15,23 @@ export const turnSortsIntoOrderBy = ( ): RecordGqlOperationOrderBy => { const fields: Pick[] = objectMetadataItem?.fields ?? []; const fieldsById = mapArrayToObject(fields, ({ id }) => id); - const sortsOrderBy = Object.fromEntries( - sorts - .map((sort) => { - const correspondingField = fieldsById[sort.fieldMetadataId]; + const sortsOrderBy = sorts + .map((sort) => { + const correspondingField = fieldsById[sort.fieldMetadataId]; - if (isUndefinedOrNull(correspondingField)) { - return undefined; - } + if (isUndefinedOrNull(correspondingField)) { + return undefined; + } - const direction: OrderBy = - sort.direction === 'asc' ? 'AscNullsFirst' : 'DescNullsLast'; + const direction: OrderBy = + sort.direction === 'asc' ? 'AscNullsFirst' : 'DescNullsLast'; - return [correspondingField.name, direction]; - }) - .filter(isDefined), - ); + return { [correspondingField.name]: direction }; + }) + .filter(isDefined); if (hasPositionField(objectMetadataItem)) { - return { - ...sortsOrderBy, - position: 'AscNullsFirst', - }; + return [...sortsOrderBy, { position: 'AscNullsFirst' }]; } return sortsOrderBy; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx index a84dfcce9497..63868f068611 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx @@ -11,7 +11,7 @@ import { FieldMetadataType } from '~/generated/graphql'; const query = gql` query CombinedFindManyRecords( $filterNameSingular: NameSingularFilterInput - $orderByNameSingular: NameSingularOrderByInput + $orderByNameSingular: [NameSingularOrderByInput] $lastCursorNameSingular: String $limitNameSingular: Int ) { @@ -50,7 +50,7 @@ const mocks = [ query, variables: { filterNameSingular: { id: { in: ['1'] } }, - orderByNameSingular: { createdAt: 'DescNullsLast' }, + orderByNameSingular: [{ createdAt: 'DescNullsLast' }], limitNameSingular: 60, }, }, @@ -63,7 +63,7 @@ const mocks = [ query, variables: { filterNameSingular: { and: [{}, { id: { in: ['1'] } }] }, - orderByNameSingular: { createdAt: 'DescNullsLast' }, + orderByNameSingular: [{ createdAt: 'DescNullsLast' }], limitNameSingular: 60, }, }, @@ -77,7 +77,7 @@ const mocks = [ variables: { limitNameSingular: 60, filterNameSingular: { not: { id: { in: ['1'] } } }, - orderByNameSingular: { createdAt: 'DescNullsLast' }, + orderByNameSingular: [{ createdAt: 'DescNullsLast' }], }, }, result: jest.fn(() => ({ diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem.ts index 07f60b012fce..ec20862e0807 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem.ts @@ -16,9 +16,7 @@ export const useOrderByFieldPerMetadataItem = ({ return [ `orderBy${capitalize(objectMetadataItem.nameSingular)}`, - { - ...orderByField, - }, + [...orderByField], ]; }) .filter(isDefined), diff --git a/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts index ea68c231cbad..ec966410baa1 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateFindManyRecordsQuery.ts @@ -21,9 +21,9 @@ query FindMany${capitalize( objectMetadataItem.namePlural, )}($filter: ${capitalize( objectMetadataItem.nameSingular, -)}FilterInput, $orderBy: ${capitalize( +)}FilterInput, $orderBy: [${capitalize( objectMetadataItem.nameSingular, -)}OrderByInput, $lastCursor: String, $limit: Int) { +)}OrderByInput], $lastCursor: String, $limit: Int) { ${ objectMetadataItem.namePlural }(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){ diff --git a/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts b/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts index d8a0481242c5..3b08ec0cec01 100644 --- a/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts +++ b/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts @@ -3,7 +3,7 @@ import { gql } from '@apollo/client'; export const query = gql` query FindManyPeople( $filter: PersonFilterInput - $orderBy: PersonOrderByInput + $orderBy: [PersonOrderByInput] $lastCursor: String $limit: Int = 60 ) { @@ -166,7 +166,7 @@ export const variables = { { not: { id: { in: ['1', '2'] } } }, ], }, - orderBy: { name: 'AscNullsLast' }, + orderBy: [{ name: 'AscNullsLast' }], }, filteredSelectedEntities: { limit: 60, @@ -176,12 +176,12 @@ export const variables = { { id: { in: ['1'] } }, ], }, - orderBy: { name: 'AscNullsLast' }, + orderBy: [{ name: 'AscNullsLast' }], }, selectedEntities: { limit: 60, filter: { id: { in: ['1'] } }, - orderBy: { name: 'AscNullsLast' }, + orderBy: [{ name: 'AscNullsLast' }], }, }; diff --git a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts index b4d337c007a8..d43468a3ff37 100644 --- a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts +++ b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts @@ -49,7 +49,7 @@ export const useFilteredSearchEntityQuery = ({ useFindManyRecords({ objectNameSingular, filter: selectedIdsFilter, - orderBy: { [orderByField]: sortOrder }, + orderBy: [{ [orderByField]: sortOrder }], skip: !selectedIds.length, }); @@ -93,7 +93,7 @@ export const useFilteredSearchEntityQuery = ({ } = useFindManyRecords({ objectNameSingular, filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]), - orderBy: { [orderByField]: sortOrder }, + orderBy: [{ [orderByField]: sortOrder }], skip: !selectedIds.length, }); @@ -106,7 +106,7 @@ export const useFilteredSearchEntityQuery = ({ objectNameSingular, filter: makeAndFilterVariables([...searchFilters, notFilter]), limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT, - orderBy: { [orderByField]: sortOrder }, + orderBy: [{ [orderByField]: sortOrder }], }); return { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/__tests__/args-string.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/__tests__/args-string.factory.spec.ts index 4cc9394e1adb..f4be0b8fb204 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/__tests__/args-string.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/__tests__/args-string.factory.spec.ts @@ -68,10 +68,7 @@ describe('ArgsStringFactory', () => { it('when orderBy is present, should return an array of objects', () => { const args = { - orderBy: { - id: 'AscNullsFirst', - name: 'AscNullsFirst', - }, + orderBy: [{ id: 'AscNullsFirst' }, { name: 'AscNullsFirst' }], }; argsAliasCreate.mockReturnValue(args); @@ -85,11 +82,11 @@ describe('ArgsStringFactory', () => { it('when orderBy is present with position criteria, should return position at the end of the list', () => { const args = { - orderBy: { - position: 'AscNullsFirst', - id: 'AscNullsFirst', - name: 'AscNullsFirst', - }, + orderBy: [ + { position: 'AscNullsFirst' }, + { id: 'AscNullsFirst' }, + { name: 'AscNullsFirst' }, + ], }; argsAliasCreate.mockReturnValue(args); @@ -103,11 +100,11 @@ describe('ArgsStringFactory', () => { it('when orderBy is present with position in the middle, should return position at the end of the list', () => { const args = { - orderBy: { - id: 'AscNullsFirst', - position: 'AscNullsFirst', - name: 'AscNullsFirst', - }, + orderBy: [ + { id: 'AscNullsFirst' }, + { position: 'AscNullsFirst' }, + { name: 'AscNullsFirst' }, + ], }; argsAliasCreate.mockReturnValue(args); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-string.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-string.factory.ts index d28c124868a3..d4b8efd77ed6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-string.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-string.factory.ts @@ -36,11 +36,16 @@ export class ArgsStringFactory { typeof computedArgs[key] === 'object' && computedArgs[key] !== null ) { - // If it's an object (and not null), stringify it - argsString += `${key}: ${this.buildStringifiedObject( - key, - computedArgs[key], - )}, `; + if (key === 'orderBy') { + argsString += `${key}: ${this.buildStringifiedOrderBy( + computedArgs[key], + )}, `; + } else { + // If it's an object (and not null), stringify it + argsString += `${key}: ${stringifyWithoutKeyQuote( + computedArgs[key], + )}, `; + } } else { // For other types (number, boolean), add as is argsString += `${key}: ${computedArgs[key]}, `; @@ -55,22 +60,30 @@ export class ArgsStringFactory { return argsString; } - private buildStringifiedObject( - key: string, - obj: Record, + private buildStringifiedOrderBy( + keyValuePairArray: Array>, ): string { - // PgGraphql is expecting the orderBy argument to be an array of objects - if (key === 'orderBy') { - const orderByString = Object.keys(obj) - .sort((_, b) => { - return b === 'position' ? -1 : 0; - }) - .map((orderByKey) => `{${orderByKey}: ${obj[orderByKey]}}`) - .join(', '); + if ( + keyValuePairArray.length !== 0 && + Object.keys(keyValuePairArray[0]).length === 0 + ) { + return `[]`; + } + // if position argument is present we want to put it at the very last + let orderByString = keyValuePairArray + .sort((_, obj) => (Object.hasOwnProperty.call(obj, 'position') ? -1 : 0)) + .map((obj) => { + const [key] = Object.keys(obj); + const value = obj[key]; + + return `{${key}: ${value}}`; + }) + .join(', '); - return `[${orderByString}]`; + if (orderByString.endsWith(', ')) { + orderByString = orderByString.slice(0, -2); } - return stringifyWithoutKeyQuote(obj); + return `[${orderByString}]`; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/record.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/record.interface.ts index b9847baed1da..32ee60f567c6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/record.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/record.interface.ts @@ -16,9 +16,9 @@ export enum OrderByDirection { DescNullsLast = 'DescNullsLast', } -export type RecordOrderBy = { +export type RecordOrderBy = Array<{ [Property in keyof Record]?: OrderByDirection; -}; +}>; export interface RecordDuplicateCriteria { objectName: string; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-resolver-args.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-resolver-args.spec.ts index b66087fe89f2..669424515291 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-resolver-args.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-resolver-args.spec.ts @@ -13,7 +13,11 @@ describe('getResolverArgs', () => { before: { type: GraphQLString, isNullable: true }, after: { type: GraphQLString, isNullable: true }, filter: { kind: InputTypeDefinitionKind.Filter, isNullable: true }, - orderBy: { kind: InputTypeDefinitionKind.OrderBy, isNullable: true }, + orderBy: { + kind: InputTypeDefinitionKind.OrderBy, + isNullable: true, + isArray: true, + }, limit: { type: GraphQLInt, isNullable: true }, }, findOne: { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts index 5eaa8f314ce1..e064c21f02db 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts @@ -38,6 +38,7 @@ export const getResolverArgs = ( orderBy: { kind: InputTypeDefinitionKind.OrderBy, isNullable: true, + isArray: true, }, }; case 'findOne': diff --git a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/find-many-query.factory.ts b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/find-many-query.factory.ts index bd6bd1e458c1..4f3cb7c72dfc 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/find-many-query.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/find-many-query.factory.ts @@ -14,7 +14,7 @@ export class FindManyQueryFactory { return ` query FindMany${capitalize(objectNamePlural)}( $filter: ${objectNameSingular}FilterInput, - $orderBy: ${objectNameSingular}OrderByInput, + $orderBy: [${objectNameSingular}OrderByInput], $startingAfter: String, $endingBefore: String, $limit: Int = 60 diff --git a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/order-by-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/order-by-input.factory.spec.ts index ddd8b3eff533..d7ee7c1e8e1d 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/order-by-input.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/__tests__/order-by-input.factory.spec.ts @@ -26,7 +26,7 @@ describe('OrderByInputFactory', () => { it('should return default if order by missing', () => { const request: any = { query: {} }; - expect(service.create(request, objectMetadata)).toEqual({}); + expect(service.create(request, objectMetadata)).toEqual([{}]); }); it('should create order by parser properly', () => { @@ -36,10 +36,10 @@ describe('OrderByInputFactory', () => { }, }; - expect(service.create(request, objectMetadata)).toEqual({ - fieldNumber: OrderByDirection.AscNullsFirst, - fieldText: OrderByDirection.DescNullsLast, - }); + expect(service.create(request, objectMetadata)).toEqual([ + { fieldNumber: OrderByDirection.AscNullsFirst }, + { fieldText: OrderByDirection.DescNullsLast }, + ]); }); it('should choose default direction if missing', () => { @@ -49,9 +49,9 @@ describe('OrderByInputFactory', () => { }, }; - expect(service.create(request, objectMetadata)).toEqual({ - fieldNumber: OrderByDirection.AscNullsFirst, - }); + expect(service.create(request, objectMetadata)).toEqual([ + { fieldNumber: OrderByDirection.AscNullsFirst }, + ]); }); it('should handler complex fields', () => { @@ -61,9 +61,9 @@ describe('OrderByInputFactory', () => { }, }; - expect(service.create(request, objectMetadata)).toEqual({ - fieldCurrency: { amountMicros: OrderByDirection.AscNullsFirst }, - }); + expect(service.create(request, objectMetadata)).toEqual([ + { fieldCurrency: { amountMicros: OrderByDirection.AscNullsFirst } }, + ]); }); it('should handler complex fields with direction', () => { @@ -73,9 +73,9 @@ describe('OrderByInputFactory', () => { }, }; - expect(service.create(request, objectMetadata)).toEqual({ - fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast }, - }); + expect(service.create(request, objectMetadata)).toEqual([ + { fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } }, + ]); }); it('should handler multiple complex fields with direction', () => { @@ -86,10 +86,10 @@ describe('OrderByInputFactory', () => { }, }; - expect(service.create(request, objectMetadata)).toEqual({ - fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast }, - fieldLink: { label: OrderByDirection.AscNullsLast }, - }); + expect(service.create(request, objectMetadata)).toEqual([ + { fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } }, + { fieldLink: { label: OrderByDirection.AscNullsLast } }, + ]); }); it('should throw if direction invalid', () => { diff --git a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory.ts b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory.ts index 2b44cd279a22..de073693ccc0 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/factories/input-factories/order-by-input.factory.ts @@ -7,7 +7,7 @@ import { RecordOrderBy, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; -import { checkFields } from 'src/engine/api/rest/rest-api-core-query-builder/utils/check-fields.utils'; +import { checkArrayFields } from 'src/engine/api/rest/rest-api-core-query-builder/utils/check-order-by.utils'; export const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst; @@ -17,12 +17,12 @@ export class OrderByInputFactory { const orderByQuery = request.query.order_by; if (typeof orderByQuery !== 'string') { - return {}; + return [{}]; } //orderByQuery = field_1[AscNullsFirst],field_2[DescNullsLast],field_3 const orderByItems = orderByQuery.split(','); - let result = {}; + let result: Array> = []; let itemDirection = ''; let itemFields = ''; @@ -65,10 +65,14 @@ export class OrderByInputFactory { } }, itemDirection); - result = { ...result, ...fieldResult }; + const resultFields = Object.keys(fieldResult).map((key) => ({ + [key]: fieldResult[key], + })); + + result = [...result, ...resultFields]; } - checkFields(objectMetadata.objectMetadataItem, Object.keys(result)); + checkArrayFields(objectMetadata.objectMetadataItem, result); return result; } diff --git a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/__tests__/check-fields.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/__tests__/check-fields.utils.spec.ts index c54bf49bfcf6..4dbbfdde09b8 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/__tests__/check-fields.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/__tests__/check-fields.utils.spec.ts @@ -1,5 +1,6 @@ import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { checkFields } from 'src/engine/api/rest/rest-api-core-query-builder/utils/check-fields.utils'; +import { checkArrayFields } from 'src/engine/api/rest/rest-api-core-query-builder/utils/check-order-by.utils'; describe('checkFields', () => { it('should check field types', () => { @@ -13,4 +14,21 @@ describe('checkFields', () => { checkFields(objectMetadataItemMock, ['fieldNumber', 'wrongField']), ).toThrow(); }); + + it('should check field types from array of fields', () => { + expect(() => + checkArrayFields(objectMetadataItemMock, [{ fieldNumber: undefined }]), + ).not.toThrow(); + + expect(() => + checkArrayFields(objectMetadataItemMock, [{ wrongField: undefined }]), + ).toThrow(); + + expect(() => + checkArrayFields(objectMetadataItemMock, [ + { fieldNumber: undefined }, + { wrongField: undefined }, + ]), + ).toThrow(); + }); }); diff --git a/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/check-order-by.utils.ts b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/check-order-by.utils.ts new file mode 100644 index 000000000000..c97e9368db6f --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/rest-api-core-query-builder/utils/check-order-by.utils.ts @@ -0,0 +1,47 @@ +import { BadRequestException } from '@nestjs/common'; + +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; + +import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; + +export const checkArrayFields = ( + objectMetadata: ObjectMetadataInterface, + fields: Array>, +): void => { + const fieldMetadataNames = objectMetadata.fields + .map((field) => { + if (isCompositeFieldMetadataType(field.type)) { + const compositeType = compositeTypeDefintions.get(field.type); + + if (!compositeType) { + throw new BadRequestException( + `Composite type '${field.type}' not found`, + ); + } + + return [ + field.name, + compositeType.properties.map( + (compositeProperty) => compositeProperty.name, + ), + ].flat(); + } + + return field.name; + }) + .flat(); + + for (const fieldObj of fields) { + for (const fieldName in fieldObj) { + if (!fieldMetadataNames.includes(fieldName)) { + throw new BadRequestException( + `field '${fieldName}' does not exist in '${computeObjectTargetTable( + objectMetadata, + )}' object`, + ); + } + } + } +};