[groupBy] Order by within records (#15404)
Closes https://github.com/twentyhq/core-team-issues/issues/1727 <img width="1415" height="625" alt="image" src="https://github.com/user-attachments/assets/cd132582-4a95-463c-8946-87b77670e023" />
This commit is contained in:
@@ -6,6 +6,7 @@ import { parseAggregateFieldsRestRequest } from 'src/engine/api/rest/input-reque
|
||||
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
|
||||
import { parseGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/group-by-parser-utils/parse-group-by-rest-request.util';
|
||||
import { parseIncludeRecordsSampleRestRequest } from 'src/engine/api/rest/input-request-parsers/group-by-with-records/parse-include-records-sample-rest-request.util';
|
||||
import { parseOrderByForRecordsWithGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/order-by-with-group-by-parser-utils/parse-order-by-for-records-rest-request.util';
|
||||
import { parseOrderByWithGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/order-by-with-group-by-parser-utils/parse-order-by-with-group-by-rest-request.util';
|
||||
import { parseViewIdRestRequest } from 'src/engine/api/rest/input-request-parsers/view-id-parser-utils/parse-view-id-rest-request.util';
|
||||
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
@@ -31,6 +32,7 @@ export class RestApiGroupByHandler extends RestApiBaseHandler {
|
||||
objectMetadataMaps,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
includeRecords,
|
||||
orderByForRecords,
|
||||
} = await this.parseRequestArgs(request);
|
||||
|
||||
return await this.commonGroupByQueryRunnerService.execute(
|
||||
@@ -41,6 +43,7 @@ export class RestApiGroupByHandler extends RestApiBaseHandler {
|
||||
groupBy,
|
||||
selectedFields,
|
||||
includeRecords,
|
||||
orderByForRecords,
|
||||
},
|
||||
{
|
||||
authContext,
|
||||
@@ -58,6 +61,8 @@ export class RestApiGroupByHandler extends RestApiBaseHandler {
|
||||
await this.buildCommonOptions(request);
|
||||
|
||||
const orderByWithGroupBy = parseOrderByWithGroupByRestRequest(request);
|
||||
const orderByForRecordsWithGroupBy =
|
||||
parseOrderByForRecordsWithGroupByRestRequest(request);
|
||||
const filter = parseFilterRestRequest(request);
|
||||
const viewId = parseViewIdRestRequest(request);
|
||||
const groupBy = parseGroupByRestRequest(request);
|
||||
@@ -82,6 +87,7 @@ export class RestApiGroupByHandler extends RestApiBaseHandler {
|
||||
objectMetadataMaps,
|
||||
filter,
|
||||
orderBy: orderByWithGroupBy,
|
||||
orderByForRecords: orderByForRecordsWithGroupBy,
|
||||
viewId,
|
||||
groupBy,
|
||||
selectedFields,
|
||||
|
||||
+2
-7
@@ -62,16 +62,11 @@ describe('parseAggregateFieldsRestRequest', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if aggregate parameter is undefined', () => {
|
||||
it('should early return if aggregate parameter is undefined', () => {
|
||||
const request: any = {
|
||||
query: {},
|
||||
};
|
||||
|
||||
expect(() => parseAggregateFieldsRestRequest(request)).toThrow(
|
||||
new RestInputRequestParserException(
|
||||
'Invalid aggregate query parameter - should be a valid array of string - ex: ["countNotEmptyId", "countEmptyField"]',
|
||||
RestInputRequestParserExceptionCode.INVALID_AGGREGATE_FIELDS_QUERY_PARAM,
|
||||
),
|
||||
);
|
||||
expect(parseAggregateFieldsRestRequest(request)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
+4
@@ -1,3 +1,5 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type';
|
||||
import {
|
||||
RestInputRequestParserException,
|
||||
@@ -10,6 +12,8 @@ export const parseAggregateFieldsRestRequest = (
|
||||
): CommonSelectedFields => {
|
||||
const aggregateFieldsQuery = request.query.aggregate;
|
||||
|
||||
if (!isDefined(aggregateFieldsQuery)) return {};
|
||||
|
||||
if (typeof aggregateFieldsQuery !== 'string') {
|
||||
throw new RestInputRequestParserException(
|
||||
`Invalid aggregate query parameter - should be a valid array of string - ex: ["countNotEmptyId", "countEmptyField"]`,
|
||||
|
||||
+2
-68
@@ -1,80 +1,14 @@
|
||||
//TODO : Refacto-common - remove this comment - This parser is a copy of the OrderByInputFactory without objectMetadata dependency. Validation will be done in common layer
|
||||
|
||||
import { OrderByDirection } from 'twenty-shared/types';
|
||||
|
||||
import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { addDefaultOrderById } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/add-default-order-by-id.util';
|
||||
import {
|
||||
RestInputRequestParserException,
|
||||
RestInputRequestParserExceptionCode,
|
||||
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||
import { parseOrderByRestRequestCommon } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util';
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
export const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst;
|
||||
|
||||
export const parseOrderByRestRequest = (
|
||||
request: AuthenticatedRequest,
|
||||
): ObjectRecordOrderBy => {
|
||||
const orderByQuery = request.query.order_by;
|
||||
|
||||
if (typeof orderByQuery !== 'string') {
|
||||
return addDefaultOrderById([{}]);
|
||||
}
|
||||
|
||||
//orderByQuery = field_1[AscNullsFirst],field_2[DescNullsLast],field_3
|
||||
const orderByItems = orderByQuery.split(',');
|
||||
let result: Array<Record<string, OrderByDirection>> = [];
|
||||
let itemDirection = '';
|
||||
let itemFields = '';
|
||||
|
||||
for (const orderByItem of orderByItems) {
|
||||
// orderByItem -> field_1[AscNullsFirst]
|
||||
if (orderByItem.includes('[') && orderByItem.includes(']')) {
|
||||
const [fieldName, directionWithRightBracket] = orderByItem.split('[');
|
||||
const direction = directionWithRightBracket.replace(']', '');
|
||||
|
||||
// fields -> [field_1] ; direction -> AscNullsFirst
|
||||
if (!(direction in OrderByDirection)) {
|
||||
throw new RestInputRequestParserException(
|
||||
`'order_by' direction '${direction}' invalid. Allowed values are '${Object.values(
|
||||
OrderByDirection,
|
||||
).join(
|
||||
"', '",
|
||||
)}'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3`,
|
||||
RestInputRequestParserExceptionCode.INVALID_ORDER_BY_QUERY_PARAM,
|
||||
);
|
||||
}
|
||||
|
||||
itemDirection = direction;
|
||||
itemFields = fieldName;
|
||||
} else {
|
||||
// orderByItem -> field_3
|
||||
itemDirection = DEFAULT_ORDER_DIRECTION;
|
||||
itemFields = orderByItem;
|
||||
}
|
||||
|
||||
let fieldResult = {};
|
||||
|
||||
itemFields
|
||||
.split('.')
|
||||
.reverse()
|
||||
.forEach((field) => {
|
||||
if (Object.keys(fieldResult).length) {
|
||||
fieldResult = { [field]: fieldResult };
|
||||
} else {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
fieldResult[field] = itemDirection;
|
||||
}
|
||||
}, itemDirection);
|
||||
|
||||
const resultFields = Object.keys(fieldResult).map((key) => ({
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
[key]: fieldResult[key],
|
||||
}));
|
||||
|
||||
result = [...result, ...resultFields];
|
||||
}
|
||||
|
||||
return addDefaultOrderById(result);
|
||||
return parseOrderByRestRequestCommon(orderByQuery);
|
||||
};
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
//TODO : Refacto-common - remove this comment - This parser is a copy of the OrderByInputFactory without objectMetadata dependency. Validation will be done in common layer
|
||||
|
||||
import { OrderByDirection } from 'twenty-shared/types';
|
||||
|
||||
import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import type { ParsedQs } from 'qs';
|
||||
|
||||
import { addDefaultOrderById } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/add-default-order-by-id.util';
|
||||
import {
|
||||
RestInputRequestParserException,
|
||||
RestInputRequestParserExceptionCode,
|
||||
} from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception';
|
||||
|
||||
const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst;
|
||||
|
||||
export const parseOrderByRestRequestCommon = (
|
||||
orderByQuery: string | string[] | ParsedQs | ParsedQs[] | undefined,
|
||||
): ObjectRecordOrderBy => {
|
||||
if (typeof orderByQuery !== 'string') {
|
||||
return addDefaultOrderById([{}]);
|
||||
}
|
||||
|
||||
//orderByQuery = field_1[AscNullsFirst],field_2[DescNullsLast],field_3
|
||||
const orderByItems = orderByQuery.split(',');
|
||||
let result: Array<Record<string, OrderByDirection>> = [];
|
||||
let itemDirection = '';
|
||||
let itemFields = '';
|
||||
|
||||
for (const orderByItem of orderByItems) {
|
||||
// orderByItem -> field_1[AscNullsFirst]
|
||||
if (orderByItem.includes('[') && orderByItem.includes(']')) {
|
||||
const [fieldName, directionWithRightBracket] = orderByItem.split('[');
|
||||
const direction = directionWithRightBracket.replace(']', '');
|
||||
|
||||
// fields -> [field_1] ; direction -> AscNullsFirst
|
||||
if (!(direction in OrderByDirection)) {
|
||||
throw new RestInputRequestParserException(
|
||||
`'order_by' direction '${direction}' invalid. Allowed values are '${Object.values(
|
||||
OrderByDirection,
|
||||
).join(
|
||||
"', '",
|
||||
)}'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3`,
|
||||
RestInputRequestParserExceptionCode.INVALID_ORDER_BY_QUERY_PARAM,
|
||||
);
|
||||
}
|
||||
|
||||
itemDirection = direction;
|
||||
itemFields = fieldName;
|
||||
} else {
|
||||
// orderByItem -> field_3
|
||||
itemDirection = DEFAULT_ORDER_DIRECTION;
|
||||
itemFields = orderByItem;
|
||||
}
|
||||
|
||||
let fieldResult = {};
|
||||
|
||||
itemFields
|
||||
.split('.')
|
||||
.reverse()
|
||||
.forEach((field) => {
|
||||
if (Object.keys(fieldResult).length) {
|
||||
fieldResult = { [field]: fieldResult };
|
||||
} else {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
fieldResult[field] = itemDirection;
|
||||
}
|
||||
}, itemDirection);
|
||||
|
||||
const resultFields = Object.keys(fieldResult).map((key) => ({
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
[key]: fieldResult[key],
|
||||
}));
|
||||
|
||||
result = [...result, ...resultFields];
|
||||
}
|
||||
|
||||
return addDefaultOrderById(result);
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { parseOrderByRestRequestCommon } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util';
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
export const parseOrderByForRecordsWithGroupByRestRequest = (
|
||||
request: AuthenticatedRequest,
|
||||
): ObjectRecordOrderBy | undefined => {
|
||||
const orderByForRecordsWithGroupByQuery = request.query.order_by_for_records;
|
||||
|
||||
return parseOrderByRestRequestCommon(orderByForRecordsWithGroupByQuery);
|
||||
};
|
||||
Reference in New Issue
Block a user