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

@ -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);

View File

@ -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<string, any>,
private buildStringifiedOrderBy(
keyValuePairArray: Array<Record<string, any>>,
): 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 `[${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',
}
export type RecordOrderBy = {
export type RecordOrderBy = Array<{
[Property in keyof Record]?: OrderByDirection;
};
}>;
export interface RecordDuplicateCriteria {
objectName: string;

View File

@ -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: {

View File

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

View File

@ -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

View File

@ -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', () => {

View File

@ -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<Record<string, OrderByDirection>> = [];
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;
}

View File

@ -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();
});
});

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`,
);
}
}
}
};