Support orderBy as array (#5681)

closes: #4301

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Aditya Pimpalkar
2024-06-14 10:23:37 +01:00
committed by GitHub
parent 85fd801480
commit 4603999d1c
35 changed files with 249 additions and 157 deletions

View File

@ -10,9 +10,11 @@ export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperation
eq: messageThreadId || '', eq: messageThreadId || '',
}, },
}, },
orderBy: { orderBy: [
receivedAt: 'AscNullsLast', {
}, receivedAt: 'AscNullsLast',
},
],
limit: 10, limit: 10,
}, },
fields: { fields: {

View File

@ -17,9 +17,11 @@ export const useAttachments = (targetableObject: ActivityTargetableObject) => {
eq: targetableObject.id, eq: targetableObject.id,
}, },
}, },
orderBy: { orderBy: [
createdAt: 'DescNullsFirst', {
}, createdAt: 'DescNullsFirst',
},
],
}); });
return { return {

View File

@ -49,7 +49,7 @@ const mocks: MockedResponse[] = [
query: gql` query: gql`
query FindManyActivityTargets( query FindManyActivityTargets(
$filter: ActivityTargetFilterInput $filter: ActivityTargetFilterInput
$orderBy: ActivityTargetOrderByInput $orderBy: [ActivityTargetOrderByInput]
$lastCursor: String $lastCursor: String
$limit: Int $limit: Int
) { ) {
@ -103,7 +103,7 @@ const mocks: MockedResponse[] = [
query: gql` query: gql`
query FindManyActivities( query FindManyActivities(
$filter: ActivityFilterInput $filter: ActivityFilterInput
$orderBy: ActivityOrderByInput $orderBy: [ActivityOrderByInput]
$lastCursor: String $lastCursor: String
$limit: Int $limit: Int
) { ) {
@ -142,7 +142,7 @@ const mocks: MockedResponse[] = [
variables: { variables: {
filter: { id: { in: ['234'] } }, filter: { id: { in: ['234'] } },
limit: undefined, limit: undefined,
orderBy: {}, orderBy: [{}],
}, },
}, },
result: jest.fn(() => ({ result: jest.fn(() => ({
@ -178,7 +178,7 @@ describe('useActivities', () => {
useActivities({ useActivities({
targetableObjects: [], targetableObjects: [],
activitiesFilters: {}, activitiesFilters: {},
activitiesOrderByVariables: {}, activitiesOrderByVariables: [{}],
skip: false, skip: false,
}), }),
{ wrapper: Wrapper }, { wrapper: Wrapper },
@ -202,7 +202,7 @@ describe('useActivities', () => {
{ targetObjectNameSingular: 'company', id: '123' }, { targetObjectNameSingular: 'company', id: '123' },
], ],
activitiesFilters: {}, activitiesFilters: {},
activitiesOrderByVariables: {}, activitiesOrderByVariables: [{}],
skip: false, skip: false,
}); });
return { activities, setCurrentWorkspaceMember }; return { activities, setCurrentWorkspaceMember };

View File

@ -34,7 +34,7 @@ const mocks: MockedResponse[] = [
query: gql` query: gql`
query FindManyActivityTargets( query FindManyActivityTargets(
$filter: ActivityTargetFilterInput $filter: ActivityTargetFilterInput
$orderBy: ActivityTargetOrderByInput $orderBy: [ActivityTargetOrderByInput]
$lastCursor: String $lastCursor: String
$limit: Int $limit: Int
) { ) {

View File

@ -109,7 +109,7 @@ export const usePrepareFindManyActivitiesQuery = () => {
objectRecordsToOverwrite: filteredActivities, objectRecordsToOverwrite: filteredActivities,
queryVariables: { queryVariables: {
...nextFindManyActivitiesQueryFilter, ...nextFindManyActivitiesQueryFilter,
orderBy: { createdAt: 'DescNullsFirst' }, orderBy: [{ createdAt: 'DescNullsFirst' }],
}, },
recordGqlFields: FIND_ACTIVITIES_OPERATION_SIGNATURE.fields, recordGqlFields: FIND_ACTIVITIES_OPERATION_SIGNATURE.fields,
computeReferences: true, computeReferences: true,

View File

@ -24,7 +24,7 @@ export const useNotes = (targetableObject: ActivityTargetableObject) => {
const { activities, loading } = useActivities({ const { activities, loading } = useActivities({
activitiesFilters: notesQueryVariables.filter ?? {}, activitiesFilters: notesQueryVariables.filter ?? {},
activitiesOrderByVariables: notesQueryVariables.orderBy ?? {}, activitiesOrderByVariables: notesQueryVariables.orderBy ?? [{}],
targetableObjects: [targetableObject], targetableObjects: [targetableObject],
}); });

View File

@ -110,13 +110,13 @@ export const useTasks = ({
const { activities: completeTasksData } = useActivities({ const { activities: completeTasksData } = useActivities({
targetableObjects, targetableObjects,
activitiesFilters: completedQueryVariables.filter ?? {}, activitiesFilters: completedQueryVariables.filter ?? {},
activitiesOrderByVariables: completedQueryVariables.orderBy ?? {}, activitiesOrderByVariables: completedQueryVariables.orderBy ?? [{}],
}); });
const { activities: incompleteTaskData } = useActivities({ const { activities: incompleteTaskData } = useActivities({
targetableObjects, targetableObjects,
activitiesFilters: incompleteQueryVariables.filter ?? {}, activitiesFilters: incompleteQueryVariables.filter ?? {},
activitiesOrderByVariables: incompleteQueryVariables.orderBy ?? {}, activitiesOrderByVariables: incompleteQueryVariables.orderBy ?? [{}],
}); });
const todayOrPreviousTasks = incompleteTaskData?.filter((task) => { const todayOrPreviousTasks = incompleteTaskData?.filter((task) => {

View File

@ -1,6 +1,8 @@
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy'; import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
export const FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY: RecordGqlOperationOrderBy = export const FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY: RecordGqlOperationOrderBy =
{ [
createdAt: 'DescNullsFirst', {
}; createdAt: 'DescNullsFirst',
},
];

View File

@ -13,8 +13,10 @@ export const makeTimelineActivitiesQueryVariables = ({
in: [...activityIds].sort(sortByAscString), in: [...activityIds].sort(sortByAscString),
}, },
}, },
orderBy: { orderBy: [
createdAt: 'DescNullsFirst', {
}, createdAt: 'DescNullsFirst',
},
],
}; };
}; };

View File

@ -23,9 +23,11 @@ export const useTimelineActivities = (
eq: targetableObject.id, eq: targetableObject.id,
}, },
}, },
orderBy: { orderBy: [
createdAt: 'DescNullsFirst', {
}, createdAt: 'DescNullsFirst',
},
],
recordGqlFields: { recordGqlFields: {
id: true, id: true,
createdAt: true, createdAt: true,

View File

@ -16,7 +16,7 @@ export const sortCachedObjectEdges = ({
orderBy: RecordGqlOperationOrderBy; orderBy: RecordGqlOperationOrderBy;
readCacheField: ReadFieldFunction; readCacheField: ReadFieldFunction;
}) => { }) => {
const [orderByFieldName, orderByFieldValue] = Object.entries(orderBy)[0]; const [orderByFieldName, orderByFieldValue] = Object.entries(orderBy[0])[0];
const [orderBySubFieldName, orderBySubFieldValue] = const [orderBySubFieldName, orderBySubFieldValue] =
typeof orderByFieldValue === 'string' typeof orderByFieldValue === 'string'
? [] ? []

View File

@ -24,7 +24,7 @@ export const query = gql`
export const findManyViewsQuery = gql` export const findManyViewsQuery = gql`
query FindManyViews( query FindManyViews(
$filter: ViewFilterInput $filter: ViewFilterInput
$orderBy: ViewOrderByInput $orderBy: [ViewOrderByInput]
$lastCursor: String $lastCursor: String
$limit: Int $limit: Int
) { ) {

View File

@ -26,8 +26,8 @@ describe('useGetObjectOrderByField', () => {
}, },
); );
expect(result.current).toEqual({ expect(result.current).toEqual([
name: { firstName: 'AscNullsLast', lastName: 'AscNullsLast' }, { name: { firstName: 'AscNullsLast', lastName: 'AscNullsLast' } },
}); ]);
}); });
}); });

View File

@ -9,8 +9,8 @@ describe('getObjectOrderByField', () => {
(item) => item.nameSingular === 'person', (item) => item.nameSingular === 'person',
)!; )!;
const res = getOrderByFieldForObjectMetadataItem(objectMetadataItem); const res = getOrderByFieldForObjectMetadataItem(objectMetadataItem);
expect(res).toEqual({ expect(res).toEqual([
name: { firstName: 'AscNullsLast', lastName: 'AscNullsLast' }, { name: { firstName: 'AscNullsLast', lastName: 'AscNullsLast' } },
}); ]);
}); });
}); });

View File

@ -15,20 +15,26 @@ export const getOrderByFieldForObjectMetadataItem = (
if (isDefined(labelIdentifierFieldMetadata)) { if (isDefined(labelIdentifierFieldMetadata)) {
switch (labelIdentifierFieldMetadata.type) { switch (labelIdentifierFieldMetadata.type) {
case FieldMetadataType.FullName: case FieldMetadataType.FullName:
return { return [
[labelIdentifierFieldMetadata.name]: { {
firstName: orderBy ?? 'AscNullsLast', [labelIdentifierFieldMetadata.name]: {
lastName: orderBy ?? 'AscNullsLast', firstName: orderBy ?? 'AscNullsLast',
lastName: orderBy ?? 'AscNullsLast',
},
}, },
}; ];
default: default:
return { return [
[labelIdentifierFieldMetadata.name]: orderBy ?? 'AscNullsLast', {
}; [labelIdentifierFieldMetadata.name]: orderBy ?? 'AscNullsLast',
},
];
} }
} else { } else {
return { return [
createdAt: orderBy ?? 'DescNullsLast', {
}; createdAt: orderBy ?? 'DescNullsLast',
},
];
} }
}; };

View File

@ -1,5 +1,5 @@
import { OrderBy } from '@/object-metadata/types/OrderBy'; import { OrderBy } from '@/object-metadata/types/OrderBy';
export type RecordGqlOperationOrderBy = { export type RecordGqlOperationOrderBy = Array<{
[fieldName: string]: OrderBy | { [subFieldName: string]: OrderBy }; [fieldName: string]: OrderBy | { [subFieldName: string]: OrderBy };
}; }>;

View File

@ -5,7 +5,7 @@ import { RecoilRoot } from 'recoil';
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
const expectedQueryTemplate = ` 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) { people(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor) {
edges { edges {
node { node {

View File

@ -32,9 +32,9 @@ export const useGenerateCombinedFindManyRecordsQuery = ({
const orderByPerMetadataItemArray = operationSignatures const orderByPerMetadataItemArray = operationSignatures
.map( .map(
({ objectNameSingular }) => ({ objectNameSingular }) =>
`$orderBy${capitalize(objectNameSingular)}: ${capitalize( `$orderBy${capitalize(objectNameSingular)}: [${capitalize(
objectNameSingular, objectNameSingular,
)}OrderByInput`, )}OrderByInput]`,
) )
.join(', '); .join(', ');

View File

@ -30,9 +30,11 @@ describe('turnSortsIntoOrderBy', () => {
it('should sort by recordPosition if no sorts', () => { it('should sort by recordPosition if no sorts', () => {
const fields = [{ id: 'field1', name: 'createdAt' }] as FieldMetadataItem[]; const fields = [{ id: 'field1', name: 'createdAt' }] as FieldMetadataItem[];
expect(turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, [])).toEqual( expect(turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, [])).toEqual(
{ [
position: 'AscNullsFirst', {
}, position: 'AscNullsFirst',
},
],
); );
}); });
@ -47,10 +49,7 @@ describe('turnSortsIntoOrderBy', () => {
const fields = [{ id: 'field1', name: 'field1' }] as FieldMetadataItem[]; const fields = [{ id: 'field1', name: 'field1' }] as FieldMetadataItem[];
expect( expect(
turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, sorts), turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, sorts),
).toEqual({ ).toEqual([{ field1: 'AscNullsFirst' }, { position: 'AscNullsFirst' }]);
field1: 'AscNullsFirst',
position: 'AscNullsFirst',
});
}); });
it('should create OrderByField with multiple sorts', () => { it('should create OrderByField with multiple sorts', () => {
@ -72,11 +71,11 @@ describe('turnSortsIntoOrderBy', () => {
] as FieldMetadataItem[]; ] as FieldMetadataItem[];
expect( expect(
turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, sorts), turnSortsIntoOrderBy({ ...objectMetadataItem, fields }, sorts),
).toEqual({ ).toEqual([
field1: 'AscNullsFirst', { field1: 'AscNullsFirst' },
field2: 'DescNullsLast', { field2: 'DescNullsLast' },
position: 'AscNullsFirst', { position: 'AscNullsFirst' },
}); ]);
}); });
it('should ignore if field not found', () => { it('should ignore if field not found', () => {
@ -87,9 +86,9 @@ describe('turnSortsIntoOrderBy', () => {
definition: sortDefinition, definition: sortDefinition,
}, },
]; ];
expect(turnSortsIntoOrderBy(objectMetadataItem, sorts)).toEqual({ expect(turnSortsIntoOrderBy(objectMetadataItem, sorts)).toEqual([
position: 'AscNullsFirst', { position: 'AscNullsFirst' },
}); ]);
}); });
it('should not return position for remotes', () => { it('should not return position for remotes', () => {
@ -102,6 +101,6 @@ describe('turnSortsIntoOrderBy', () => {
]; ];
expect( expect(
turnSortsIntoOrderBy({ ...objectMetadataItem, isRemote: true }, sorts), turnSortsIntoOrderBy({ ...objectMetadataItem, isRemote: true }, sorts),
).toEqual({}); ).toEqual([]);
}); });
}); });

View File

@ -15,28 +15,23 @@ export const turnSortsIntoOrderBy = (
): RecordGqlOperationOrderBy => { ): RecordGqlOperationOrderBy => {
const fields: Pick<Field, 'id' | 'name'>[] = objectMetadataItem?.fields ?? []; const fields: Pick<Field, 'id' | 'name'>[] = objectMetadataItem?.fields ?? [];
const fieldsById = mapArrayToObject(fields, ({ id }) => id); const fieldsById = mapArrayToObject(fields, ({ id }) => id);
const sortsOrderBy = Object.fromEntries( const sortsOrderBy = sorts
sorts .map((sort) => {
.map((sort) => { const correspondingField = fieldsById[sort.fieldMetadataId];
const correspondingField = fieldsById[sort.fieldMetadataId];
if (isUndefinedOrNull(correspondingField)) { if (isUndefinedOrNull(correspondingField)) {
return undefined; return undefined;
} }
const direction: OrderBy = const direction: OrderBy =
sort.direction === 'asc' ? 'AscNullsFirst' : 'DescNullsLast'; sort.direction === 'asc' ? 'AscNullsFirst' : 'DescNullsLast';
return [correspondingField.name, direction]; return { [correspondingField.name]: direction };
}) })
.filter(isDefined), .filter(isDefined);
);
if (hasPositionField(objectMetadataItem)) { if (hasPositionField(objectMetadataItem)) {
return { return [...sortsOrderBy, { position: 'AscNullsFirst' }];
...sortsOrderBy,
position: 'AscNullsFirst',
};
} }
return sortsOrderBy; return sortsOrderBy;

View File

@ -11,7 +11,7 @@ import { FieldMetadataType } from '~/generated/graphql';
const query = gql` const query = gql`
query CombinedFindManyRecords( query CombinedFindManyRecords(
$filterNameSingular: NameSingularFilterInput $filterNameSingular: NameSingularFilterInput
$orderByNameSingular: NameSingularOrderByInput $orderByNameSingular: [NameSingularOrderByInput]
$lastCursorNameSingular: String $lastCursorNameSingular: String
$limitNameSingular: Int $limitNameSingular: Int
) { ) {
@ -50,7 +50,7 @@ const mocks = [
query, query,
variables: { variables: {
filterNameSingular: { id: { in: ['1'] } }, filterNameSingular: { id: { in: ['1'] } },
orderByNameSingular: { createdAt: 'DescNullsLast' }, orderByNameSingular: [{ createdAt: 'DescNullsLast' }],
limitNameSingular: 60, limitNameSingular: 60,
}, },
}, },
@ -63,7 +63,7 @@ const mocks = [
query, query,
variables: { variables: {
filterNameSingular: { and: [{}, { id: { in: ['1'] } }] }, filterNameSingular: { and: [{}, { id: { in: ['1'] } }] },
orderByNameSingular: { createdAt: 'DescNullsLast' }, orderByNameSingular: [{ createdAt: 'DescNullsLast' }],
limitNameSingular: 60, limitNameSingular: 60,
}, },
}, },
@ -77,7 +77,7 @@ const mocks = [
variables: { variables: {
limitNameSingular: 60, limitNameSingular: 60,
filterNameSingular: { not: { id: { in: ['1'] } } }, filterNameSingular: { not: { id: { in: ['1'] } } },
orderByNameSingular: { createdAt: 'DescNullsLast' }, orderByNameSingular: [{ createdAt: 'DescNullsLast' }],
}, },
}, },
result: jest.fn(() => ({ result: jest.fn(() => ({

View File

@ -16,9 +16,7 @@ export const useOrderByFieldPerMetadataItem = ({
return [ return [
`orderBy${capitalize(objectMetadataItem.nameSingular)}`, `orderBy${capitalize(objectMetadataItem.nameSingular)}`,
{ [...orderByField],
...orderByField,
},
]; ];
}) })
.filter(isDefined), .filter(isDefined),

View File

@ -21,9 +21,9 @@ query FindMany${capitalize(
objectMetadataItem.namePlural, objectMetadataItem.namePlural,
)}($filter: ${capitalize( )}($filter: ${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}FilterInput, $orderBy: ${capitalize( )}FilterInput, $orderBy: [${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}OrderByInput, $lastCursor: String, $limit: Int) { )}OrderByInput], $lastCursor: String, $limit: Int) {
${ ${
objectMetadataItem.namePlural objectMetadataItem.namePlural
}(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){ }(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){

View File

@ -3,7 +3,7 @@ import { gql } from '@apollo/client';
export const query = gql` export const query = gql`
query FindManyPeople( query FindManyPeople(
$filter: PersonFilterInput $filter: PersonFilterInput
$orderBy: PersonOrderByInput $orderBy: [PersonOrderByInput]
$lastCursor: String $lastCursor: String
$limit: Int = 60 $limit: Int = 60
) { ) {
@ -166,7 +166,7 @@ export const variables = {
{ not: { id: { in: ['1', '2'] } } }, { not: { id: { in: ['1', '2'] } } },
], ],
}, },
orderBy: { name: 'AscNullsLast' }, orderBy: [{ name: 'AscNullsLast' }],
}, },
filteredSelectedEntities: { filteredSelectedEntities: {
limit: 60, limit: 60,
@ -176,12 +176,12 @@ export const variables = {
{ id: { in: ['1'] } }, { id: { in: ['1'] } },
], ],
}, },
orderBy: { name: 'AscNullsLast' }, orderBy: [{ name: 'AscNullsLast' }],
}, },
selectedEntities: { selectedEntities: {
limit: 60, limit: 60,
filter: { id: { in: ['1'] } }, filter: { id: { in: ['1'] } },
orderBy: { name: 'AscNullsLast' }, orderBy: [{ name: 'AscNullsLast' }],
}, },
}; };

View File

@ -49,7 +49,7 @@ export const useFilteredSearchEntityQuery = ({
useFindManyRecords({ useFindManyRecords({
objectNameSingular, objectNameSingular,
filter: selectedIdsFilter, filter: selectedIdsFilter,
orderBy: { [orderByField]: sortOrder }, orderBy: [{ [orderByField]: sortOrder }],
skip: !selectedIds.length, skip: !selectedIds.length,
}); });
@ -93,7 +93,7 @@ export const useFilteredSearchEntityQuery = ({
} = useFindManyRecords({ } = useFindManyRecords({
objectNameSingular, objectNameSingular,
filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]), filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]),
orderBy: { [orderByField]: sortOrder }, orderBy: [{ [orderByField]: sortOrder }],
skip: !selectedIds.length, skip: !selectedIds.length,
}); });
@ -106,7 +106,7 @@ export const useFilteredSearchEntityQuery = ({
objectNameSingular, objectNameSingular,
filter: makeAndFilterVariables([...searchFilters, notFilter]), filter: makeAndFilterVariables([...searchFilters, notFilter]),
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT, limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
orderBy: { [orderByField]: sortOrder }, orderBy: [{ [orderByField]: sortOrder }],
}); });
return { return {

View File

@ -68,10 +68,7 @@ describe('ArgsStringFactory', () => {
it('when orderBy is present, should return an array of objects', () => { it('when orderBy is present, should return an array of objects', () => {
const args = { const args = {
orderBy: { orderBy: [{ id: 'AscNullsFirst' }, { name: 'AscNullsFirst' }],
id: 'AscNullsFirst',
name: 'AscNullsFirst',
},
}; };
argsAliasCreate.mockReturnValue(args); 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', () => { it('when orderBy is present with position criteria, should return position at the end of the list', () => {
const args = { const args = {
orderBy: { orderBy: [
position: 'AscNullsFirst', { position: 'AscNullsFirst' },
id: 'AscNullsFirst', { id: 'AscNullsFirst' },
name: 'AscNullsFirst', { name: 'AscNullsFirst' },
}, ],
}; };
argsAliasCreate.mockReturnValue(args); 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', () => { it('when orderBy is present with position in the middle, should return position at the end of the list', () => {
const args = { const args = {
orderBy: { orderBy: [
id: 'AscNullsFirst', { id: 'AscNullsFirst' },
position: 'AscNullsFirst', { position: 'AscNullsFirst' },
name: 'AscNullsFirst', { name: 'AscNullsFirst' },
}, ],
}; };
argsAliasCreate.mockReturnValue(args); argsAliasCreate.mockReturnValue(args);

View File

@ -36,11 +36,16 @@ export class ArgsStringFactory {
typeof computedArgs[key] === 'object' && typeof computedArgs[key] === 'object' &&
computedArgs[key] !== null computedArgs[key] !== null
) { ) {
// If it's an object (and not null), stringify it if (key === 'orderBy') {
argsString += `${key}: ${this.buildStringifiedObject( argsString += `${key}: ${this.buildStringifiedOrderBy(
key, computedArgs[key],
computedArgs[key], )}, `;
)}, `; } else {
// If it's an object (and not null), stringify it
argsString += `${key}: ${stringifyWithoutKeyQuote(
computedArgs[key],
)}, `;
}
} else { } else {
// For other types (number, boolean), add as is // For other types (number, boolean), add as is
argsString += `${key}: ${computedArgs[key]}, `; argsString += `${key}: ${computedArgs[key]}, `;
@ -55,22 +60,30 @@ export class ArgsStringFactory {
return argsString; return argsString;
} }
private buildStringifiedObject( private buildStringifiedOrderBy(
key: string, keyValuePairArray: Array<Record<string, any>>,
obj: Record<string, any>,
): string { ): string {
// PgGraphql is expecting the orderBy argument to be an array of objects if (
if (key === 'orderBy') { keyValuePairArray.length !== 0 &&
const orderByString = Object.keys(obj) Object.keys(keyValuePairArray[0]).length === 0
.sort((_, b) => { ) {
return b === 'position' ? -1 : 0; return `[]`;
}) }
.map((orderByKey) => `{${orderByKey}: ${obj[orderByKey]}}`) // if position argument is present we want to put it at the very last
.join(', '); let orderByString = keyValuePairArray
.sort((_, obj) => (Object.hasOwnProperty.call(obj, 'position') ? -1 : 0))
.map((obj) => {
const [key] = Object.keys(obj);
const value = obj[key];
return `[${orderByString}]`; return `{${key}: ${value}}`;
})
.join(', ');
if (orderByString.endsWith(', ')) {
orderByString = orderByString.slice(0, -2);
} }
return stringifyWithoutKeyQuote(obj); return `[${orderByString}]`;
} }
} }

View File

@ -16,9 +16,9 @@ export enum OrderByDirection {
DescNullsLast = 'DescNullsLast', DescNullsLast = 'DescNullsLast',
} }
export type RecordOrderBy = { export type RecordOrderBy = Array<{
[Property in keyof Record]?: OrderByDirection; [Property in keyof Record]?: OrderByDirection;
}; }>;
export interface RecordDuplicateCriteria { export interface RecordDuplicateCriteria {
objectName: string; objectName: string;

View File

@ -13,7 +13,11 @@ describe('getResolverArgs', () => {
before: { type: GraphQLString, isNullable: true }, before: { type: GraphQLString, isNullable: true },
after: { type: GraphQLString, isNullable: true }, after: { type: GraphQLString, isNullable: true },
filter: { kind: InputTypeDefinitionKind.Filter, 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 }, limit: { type: GraphQLInt, isNullable: true },
}, },
findOne: { findOne: {

View File

@ -38,6 +38,7 @@ export const getResolverArgs = (
orderBy: { orderBy: {
kind: InputTypeDefinitionKind.OrderBy, kind: InputTypeDefinitionKind.OrderBy,
isNullable: true, isNullable: true,
isArray: true,
}, },
}; };
case 'findOne': case 'findOne':

View File

@ -14,7 +14,7 @@ export class FindManyQueryFactory {
return ` return `
query FindMany${capitalize(objectNamePlural)}( query FindMany${capitalize(objectNamePlural)}(
$filter: ${objectNameSingular}FilterInput, $filter: ${objectNameSingular}FilterInput,
$orderBy: ${objectNameSingular}OrderByInput, $orderBy: [${objectNameSingular}OrderByInput],
$startingAfter: String, $startingAfter: String,
$endingBefore: String, $endingBefore: String,
$limit: Int = 60 $limit: Int = 60

View File

@ -26,7 +26,7 @@ describe('OrderByInputFactory', () => {
it('should return default if order by missing', () => { it('should return default if order by missing', () => {
const request: any = { query: {} }; const request: any = { query: {} };
expect(service.create(request, objectMetadata)).toEqual({}); expect(service.create(request, objectMetadata)).toEqual([{}]);
}); });
it('should create order by parser properly', () => { it('should create order by parser properly', () => {
@ -36,10 +36,10 @@ describe('OrderByInputFactory', () => {
}, },
}; };
expect(service.create(request, objectMetadata)).toEqual({ expect(service.create(request, objectMetadata)).toEqual([
fieldNumber: OrderByDirection.AscNullsFirst, { fieldNumber: OrderByDirection.AscNullsFirst },
fieldText: OrderByDirection.DescNullsLast, { fieldText: OrderByDirection.DescNullsLast },
}); ]);
}); });
it('should choose default direction if missing', () => { it('should choose default direction if missing', () => {
@ -49,9 +49,9 @@ describe('OrderByInputFactory', () => {
}, },
}; };
expect(service.create(request, objectMetadata)).toEqual({ expect(service.create(request, objectMetadata)).toEqual([
fieldNumber: OrderByDirection.AscNullsFirst, { fieldNumber: OrderByDirection.AscNullsFirst },
}); ]);
}); });
it('should handler complex fields', () => { it('should handler complex fields', () => {
@ -61,9 +61,9 @@ describe('OrderByInputFactory', () => {
}, },
}; };
expect(service.create(request, objectMetadata)).toEqual({ expect(service.create(request, objectMetadata)).toEqual([
fieldCurrency: { amountMicros: OrderByDirection.AscNullsFirst }, { fieldCurrency: { amountMicros: OrderByDirection.AscNullsFirst } },
}); ]);
}); });
it('should handler complex fields with direction', () => { it('should handler complex fields with direction', () => {
@ -73,9 +73,9 @@ describe('OrderByInputFactory', () => {
}, },
}; };
expect(service.create(request, objectMetadata)).toEqual({ expect(service.create(request, objectMetadata)).toEqual([
fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast }, { fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } },
}); ]);
}); });
it('should handler multiple complex fields with direction', () => { it('should handler multiple complex fields with direction', () => {
@ -86,10 +86,10 @@ describe('OrderByInputFactory', () => {
}, },
}; };
expect(service.create(request, objectMetadata)).toEqual({ expect(service.create(request, objectMetadata)).toEqual([
fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast }, { fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } },
fieldLink: { label: OrderByDirection.AscNullsLast }, { fieldLink: { label: OrderByDirection.AscNullsLast } },
}); ]);
}); });
it('should throw if direction invalid', () => { it('should throw if direction invalid', () => {

View File

@ -7,7 +7,7 @@ import {
RecordOrderBy, RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; } 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; export const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst;
@ -17,12 +17,12 @@ export class OrderByInputFactory {
const orderByQuery = request.query.order_by; const orderByQuery = request.query.order_by;
if (typeof orderByQuery !== 'string') { if (typeof orderByQuery !== 'string') {
return {}; return [{}];
} }
//orderByQuery = field_1[AscNullsFirst],field_2[DescNullsLast],field_3 //orderByQuery = field_1[AscNullsFirst],field_2[DescNullsLast],field_3
const orderByItems = orderByQuery.split(','); const orderByItems = orderByQuery.split(',');
let result = {}; let result: Array<Record<string, OrderByDirection>> = [];
let itemDirection = ''; let itemDirection = '';
let itemFields = ''; let itemFields = '';
@ -65,10 +65,14 @@ export class OrderByInputFactory {
} }
}, itemDirection); }, 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; return result;
} }

View File

@ -1,5 +1,6 @@
import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock'; 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 { 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', () => { describe('checkFields', () => {
it('should check field types', () => { it('should check field types', () => {
@ -13,4 +14,21 @@ describe('checkFields', () => {
checkFields(objectMetadataItemMock, ['fieldNumber', 'wrongField']), checkFields(objectMetadataItemMock, ['fieldNumber', 'wrongField']),
).toThrow(); ).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();
});
}); });

View File

@ -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<Record<string, any>>,
): 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`,
);
}
}
}
};