From fc7304bc5a98c5a6d74f413e35e91f9ee0fc93e5 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Mon, 13 Oct 2025 14:48:50 +0200 Subject: [PATCH] Common api - findMany query (#15004) Done in this PR : - simplify rest input parsing by removing metadata validation (for filter and orderBy - to add in common) (1st commit) - simplify result getter handlers signature (array of objectRecord only for input) closes https://github.com/twentyhq/core-team-issues/issues/1614 closes https://github.com/twentyhq/core-team-issues/issues/1615 closes https://github.com/twentyhq/core-team-issues/issues/1616 --- .../common-selected-fields.handler.ts | 6 +- .../common-base-query-runner.service.ts | 32 +- .../common-find-many-query-runner.service.ts | 286 ++++++++++++++++++ .../common-find-one-query-runner.service.ts | 19 +- .../common-query-runners.ts | 6 +- .../errors/common-query-runner.exception.ts | 3 + ...r-to-graphql-api-exception-handler.util.ts | 3 + ...nner-to-rest-api-exception-handler.util.ts | 3 + .../common-result-getters.service.ts | 155 ++++++++++ .../api/common/core-common-api.module.ts | 4 + .../api/common/types/common-page-info.type.ts | 8 + .../common/types/common-query-args.type.ts | 18 +- .../common-selected-fields-result.type.ts | 5 +- .../api/common/utils/get-page-info.util.ts | 32 ++ ...ct-records-to-graphql-connection.helper.ts | 1 + ...raphql-query-find-many-resolver.service.ts | 4 +- .../utils/cursors.util.ts | 3 +- .../factories/find-many-resolver.factory.ts | 75 ++++- .../factories/find-one-resolver.factory.ts | 18 +- .../handlers/rest-api-create-many.handler.ts | 3 +- .../handlers/rest-api-create-one.handler.ts | 3 +- .../handlers/rest-api-find-many.handler.ts | 89 +++++- .../handlers/rest-api-find-one.handler.ts | 21 +- .../handlers/rest-api-update-one.handler.ts | 3 +- .../core/interfaces/rest-api-base.handler.ts | 33 +- .../factories/get-variables.factory.ts | 19 +- .../__tests__/parse-filter.utils.spec.ts | 2 +- .../utils/filter-utils/parse-filter.utils.ts | 12 +- .../selected-fields-handler.ts | 6 +- .../core/services/rest-api-core.service.ts | 27 +- .../ending-before-input.factory.spec.ts | 33 -- .../__tests__/limit-input.factory.spec.ts | 49 --- .../starting-before-input.factory.spec.ts | 33 -- .../input-factories/depth-input.factory.ts | 32 -- .../api/rest/input-factories/factories.ts | 15 +- .../input-factories/filter-input.factory.ts | 4 +- .../input-factories/limit-input.factory.ts | 29 -- .../starting-after-input.factory.ts | 16 - .../constants/max-depth.constant.ts | 1 + .../parse-depth-rest-request.util.ts | 26 ++ ...se-ending-before-rest-request.util.spec.ts | 15 + .../parse-ending-before-rest-request.util.ts | 14 + .../add-default-conjunction.util.spec.ts} | 2 +- .../check-filter-enum-values.util.spec.ts} | 2 +- .../check-filter-query.util.spec.ts} | 2 +- .../format-field-values.util.spec.ts} | 2 +- .../__tests__/parse-base-filter.util.spec.ts} | 2 +- .../parse-filter-content.util.spec.ts} | 2 +- ...er-without-metdata-validation.util.spec.ts | 88 ++++++ .../add-default-conjunction.util.ts} | 0 .../check-filter-enum-values.util.ts} | 0 .../check-filter-query.util.ts} | 0 .../format-field-values.util.ts} | 0 .../parse-base-filter.util.ts} | 0 .../parse-filter-content.util.ts} | 0 .../parse-filter-rest-request.util.ts | 22 ++ ...filter-without-metadata-validation.util.ts | 55 ++++ .../parse-limit-rest-request.util.spec.ts | 31 ++ .../parse-limit-rest-request.util.ts | 27 ++ .../parse-order-by-rest-request.util.spec.ts | 94 ++++++ .../add-default-order-by-id.util.ts | 12 + .../parse-order-by-rest-request.util.ts | 77 +++++ ...e-starting-after-rest-request.util.spec.ts | 15 + .../parse-starting-after-rest-request.util.ts | 14 + .../input-request-parsers/types/depth.type.ts | 1 + .../get-metadata-variables.factory.ts | 22 +- ...osite-field-pagination.integration-spec.ts | 4 +- 67 files changed, 1291 insertions(+), 349 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts create mode 100644 packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts create mode 100644 packages/twenty-server/src/engine/api/common/types/common-page-info.type.ts create mode 100644 packages/twenty-server/src/engine/api/common/utils/get-page-info.util.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/__tests__/ending-before-input.factory.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/__tests__/limit-input.factory.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/__tests__/starting-before-input.factory.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/depth-input.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/limit-input.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/starting-after-input.factory.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/constants/max-depth.constant.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/ending-before-parser-utils/__tests__/parse-ending-before-rest-request.util.spec.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util.ts rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/__tests__/add-default-conjunction.utils.spec.ts => input-request-parsers/filter-parser-utils/__tests__/add-default-conjunction.util.spec.ts} (85%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts => input-request-parsers/filter-parser-utils/__tests__/check-filter-enum-values.util.spec.ts} (94%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/__tests__/check-filter-query.utils.spec.ts => input-request-parsers/filter-parser-utils/__tests__/check-filter-query.util.spec.ts} (88%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/__tests__/format-field-values.utils.spec.ts => input-request-parsers/filter-parser-utils/__tests__/format-field-values.util.spec.ts} (94%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/__tests__/parse-base-filter.utils.spec.ts => input-request-parsers/filter-parser-utils/__tests__/parse-base-filter.util.spec.ts} (91%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/__tests__/parse-filter-content.utils.spec.ts => input-request-parsers/filter-parser-utils/__tests__/parse-filter-content.util.spec.ts} (92%) create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-filter-without-metdata-validation.util.spec.ts rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/add-default-conjunction.utils.ts => input-request-parsers/filter-parser-utils/add-default-conjunction.util.ts} (100%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/check-filter-enum-values.ts => input-request-parsers/filter-parser-utils/check-filter-enum-values.util.ts} (100%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/check-filter-query.utils.ts => input-request-parsers/filter-parser-utils/check-filter-query.util.ts} (100%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/format-field-values.utils.ts => input-request-parsers/filter-parser-utils/format-field-values.util.ts} (100%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/parse-base-filter.utils.ts => input-request-parsers/filter-parser-utils/parse-base-filter.util.ts} (100%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/filter-utils/parse-filter-content.utils.ts => input-request-parsers/filter-parser-utils/parse-filter-content.util.ts} (100%) create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/limit-parser-utils/__tests__/parse-limit-rest-request.util.spec.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/__tests__/parse-order-by-rest-request.util.spec.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/add-default-order-by-id.util.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/starting-after-parser-utils/__tests__/parse-starting-after-rest-request.util.spec.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util.ts create mode 100644 packages/twenty-server/src/engine/api/rest/input-request-parsers/types/depth.type.ts diff --git a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts index aae9985d9e3..25b09b03a20 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts @@ -4,10 +4,8 @@ import { FieldMetadataType, ObjectsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type'; -import { - Depth, - MAX_DEPTH, -} from 'src/engine/api/rest/input-factories/depth-input.factory'; +import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant'; +import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type'; import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index 660056a2e1a..acd9becd69e 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -3,11 +3,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; -import { type ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; -import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler'; +import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service'; import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements'; import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; @@ -30,13 +29,7 @@ import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/wo import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() -export abstract class CommonBaseQueryRunnerService< - Response extends - | ObjectRecord - | ObjectRecord[] - | IConnection> - | IConnection>[], -> { +export abstract class CommonBaseQueryRunnerService { @Inject() protected readonly workspaceQueryHookService: WorkspaceQueryHookService; @Inject() @@ -57,6 +50,8 @@ export abstract class CommonBaseQueryRunnerService< protected readonly selectedFieldsHandler: CommonSelectedFieldsHandler; @Inject() protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService; + @Inject() + protected readonly commonResultGettersService: CommonResultGettersService; public async prepareQueryRunnerContext({ authContext, @@ -107,18 +102,19 @@ export abstract class CommonBaseQueryRunnerService< objectMetadataItemWithFieldMaps, objectMetadataMaps, }: { - results: Response; + results: ObjectRecord[]; operationName: CommonQueryNames; authContext: WorkspaceAuthContext; objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; objectMetadataMaps: ObjectMetadataMaps; - }) { - const resultWithGetters = await this.queryResultGettersFactory.create( - results, - objectMetadataItemWithFieldMaps, - authContext.workspace.id, - objectMetadataMaps, - ); + }): Promise { + const resultWithGetters = + await this.commonResultGettersService.processQueryResult( + results, + objectMetadataItemWithFieldMaps.id, + objectMetadataMaps, + authContext.workspace.id, + ); await this.workspaceQueryHookService.executePostQueryHooks( authContext, diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts new file mode 100644 index 00000000000..b6aa41f990f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts @@ -0,0 +1,286 @@ +import { Injectable } from '@nestjs/common'; + +import { isDefined } from 'class-validator'; +import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; +import { FindOptionsRelations, ObjectLiteral } from 'typeorm'; + +import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; +import { + ObjectRecord, + ObjectRecordFilter, + ObjectRecordOrderBy, + OrderByDirection, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { CommonPageInfo } from 'src/engine/api/common/types/common-page-info.type'; +import { + CommonQueryNames, + FindManyQueryArgs, +} from 'src/engine/api/common/types/common-query-args.type'; +import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util'; +import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util'; +import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; +import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; +import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; +import { getCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; +import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; + +@Injectable() +export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService { + async run({ + args, + authContext: toValidateAuthContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }: { + args: FindManyQueryArgs; + authContext: AuthContext; + objectMetadataMaps: ObjectMetadataMaps; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + }): Promise<{ + records: ObjectRecord[]; + aggregatedValues: Record; + totalCount: number; + pageInfo: CommonPageInfo; + }> { + this.validate(args); + const authContext = toValidateAuthContext; + + if (!isWorkspaceAuthContext(authContext)) { + throw new CommonQueryRunnerException( + 'Invalid auth context', + CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, + ); + } + + const { + workspaceDataSource, + repository, + roleId, + shouldBypassPermissionChecks, + } = await this.prepareQueryRunnerContext({ + authContext, + objectMetadataItemWithFieldMaps, + }); + + const processedArgs = await this.processQueryArgs({ + authContext, + objectMetadataItemWithFieldMaps, + args, + }); + + const queryBuilder = repository.createQueryBuilder( + objectMetadataItemWithFieldMaps.nameSingular, + ); + + const aggregateQueryBuilder = queryBuilder.clone(); + + let appliedFilters = processedArgs.filter ?? ({} as ObjectRecordFilter); + + const commonQueryParser = new GraphqlQueryParser( + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + ); + + commonQueryParser.applyFilterToBuilder( + aggregateQueryBuilder, + objectMetadataItemWithFieldMaps.nameSingular, + appliedFilters, + ); + + commonQueryParser.applyDeletedAtToBuilder( + aggregateQueryBuilder, + appliedFilters, + ); + + const orderByWithIdCondition = [ + ...(processedArgs.orderBy ?? []), + { id: OrderByDirection.AscNullsFirst }, + ] as ObjectRecordOrderBy; + + const isForwardPagination = !isDefined(processedArgs.before); + + const cursor = getCursor(processedArgs); + + if (cursor) { + const cursorArgFilter = computeCursorArgFilter( + cursor, + orderByWithIdCondition, + objectMetadataItemWithFieldMaps, + isForwardPagination, + ); + + appliedFilters = (processedArgs.filter + ? { + and: [processedArgs.filter, { or: cursorArgFilter }], + } + : { or: cursorArgFilter }) as unknown as ObjectRecordFilter; + } + + commonQueryParser.applyFilterToBuilder( + queryBuilder, + objectMetadataItemWithFieldMaps.nameSingular, + appliedFilters, + ); + + commonQueryParser.applyOrderToBuilder( + queryBuilder, + orderByWithIdCondition, + objectMetadataItemWithFieldMaps.nameSingular, + isForwardPagination, + ); + + commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters); + + ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({ + selectedAggregatedFields: processedArgs.selectedFieldsResult.aggregate, + queryBuilder: aggregateQueryBuilder, + objectMetadataNameSingular: objectMetadataItemWithFieldMaps.nameSingular, + }); + + const limit = + processedArgs.first ?? processedArgs.last ?? QUERY_MAX_RECORDS; + + const columnsToSelect = buildColumnsToSelect({ + select: processedArgs.selectedFieldsResult.select, + relations: processedArgs.selectedFieldsResult.relations, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + }); + + const objectRecords = (await queryBuilder + .setFindOptions({ + select: columnsToSelect, + }) + .take(limit + 1) + .getMany()) as ObjectRecord[]; + + const pageInfo = getPageInfo( + objectRecords, + orderByWithIdCondition, + limit, + isForwardPagination, + ); + + if (objectRecords.length > limit) { + objectRecords.pop(); + } + + if (!isForwardPagination) { + objectRecords.reverse(); + } + + const parentObjectRecordsAggregatedValues = + await aggregateQueryBuilder.getRawOne(); + + if (processedArgs.selectedFieldsResult.relations) { + await this.processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: objectRecords, + parentObjectRecordsAggregatedValues, + //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common + relations: processedArgs.selectedFieldsResult.relations as Record< + string, + FindOptionsRelations + >, + aggregate: processedArgs.selectedFieldsResult.aggregate, + limit: QUERY_MAX_RECORDS, + authContext, + workspaceDataSource, + roleId, + shouldBypassPermissionChecks, + selectedFields: processedArgs.selectedFieldsResult.select, + }); + } + + const enrichedRecords = await this.enrichResultsWithGettersAndHooks({ + results: objectRecords, + operationName: CommonQueryNames.findMany, + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + }); + + return { + records: enrichedRecords, + aggregatedValues: parentObjectRecordsAggregatedValues, + totalCount: parentObjectRecordsAggregatedValues?.totalCount, + pageInfo, + }; + } + + validate(args: FindManyQueryArgs) { + if (args.first && args.last) { + throw new CommonQueryRunnerException( + 'Cannot provide both first and last', + CommonQueryRunnerExceptionCode.ARGS_CONFLICT, + ); + } + if (args.before && args.after) { + throw new CommonQueryRunnerException( + 'Cannot provide both before and after', + CommonQueryRunnerExceptionCode.ARGS_CONFLICT, + ); + } + if (args.before && args.first) { + throw new CommonQueryRunnerException( + 'Cannot provide both before and first', + CommonQueryRunnerExceptionCode.ARGS_CONFLICT, + ); + } + if (args.after && args.last) { + throw new CommonQueryRunnerException( + 'Cannot provide both after and last', + CommonQueryRunnerExceptionCode.ARGS_CONFLICT, + ); + } + if (args.first !== undefined && args.first < 0) { + throw new CommonQueryRunnerException( + 'First argument must be non-negative', + CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST, + ); + } + if (args.last !== undefined && args.last < 0) { + throw new CommonQueryRunnerException( + 'Last argument must be non-negative', + CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST, + ); + } + } + + async processQueryArgs({ + authContext, + objectMetadataItemWithFieldMaps, + args, + }: { + authContext: WorkspaceAuthContext; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + args: FindManyQueryArgs; + }): Promise { + const hookedArgs = + (await this.workspaceQueryHookService.executePreQueryHooks( + authContext, + objectMetadataItemWithFieldMaps.nameSingular, + CommonQueryNames.findMany, + args, + //TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency + )) as FindManyQueryArgs; + + return { + ...hookedArgs, + filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( + hookedArgs.filter, + objectMetadataItemWithFieldMaps, + ), + }; + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts index 9b55554110e..bc9ba310506 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts @@ -21,14 +21,13 @@ import { } from 'src/engine/api/common/types/common-query-args.type'; import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() -export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService { +export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService { async run({ args, authContext: toValidateAuthContext, @@ -137,23 +136,15 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic }); } - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - const results = typeORMObjectRecordsParser.processRecord({ - objectRecord: objectRecords[0], - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }) as ObjectRecord; - - return this.enrichResultsWithGettersAndHooks({ - results, + const enrichedResults = await this.enrichResultsWithGettersAndHooks({ + results: objectRecords, authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps, operationName: CommonQueryNames.findOne, }); + + return enrichedResults[0]; } async processQueryArgs({ diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-query-runners.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-query-runners.ts index f946c9616ef..9723f7e2e85 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-query-runners.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-query-runners.ts @@ -1,3 +1,7 @@ +import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service'; import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service'; -export const CommonQueryRunners = [CommonFindOneQueryRunnerService]; +export const CommonQueryRunners = [ + CommonFindOneQueryRunnerService, + CommonFindManyQueryRunnerService, +]; diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts index af4eaaefde0..b24ae6eed02 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts @@ -6,4 +6,7 @@ export enum CommonQueryRunnerExceptionCode { RECORD_NOT_FOUND = 'RECORD_NOT_FOUND', INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT', INVALID_AUTH_CONTEXT = 'INVALID_AUTH_CONTEXT', + ARGS_CONFLICT = 'ARGS_CONFLICT', + INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST', + INVALID_ARGS_LAST = 'INVALID_ARGS_LAST', } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts index f8b2adc008e..e3352799ae8 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts @@ -16,6 +16,9 @@ export const commonQueryRunnerToGraphqlApiExceptionHandler = ( switch (error.code) { case CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND: throw new NotFoundError(error); + case CommonQueryRunnerExceptionCode.ARGS_CONFLICT: + case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST: + case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST: case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT: throw new UserInputError(error); case CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT: diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts index 413f82d446b..55f39ddcbb3 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts @@ -15,6 +15,9 @@ export const commonQueryRunnerToRestApiExceptionHandler = ( error: CommonQueryRunnerException, ): never => { switch (error.code) { + case CommonQueryRunnerExceptionCode.ARGS_CONFLICT: + case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST: + case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST: case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT: throw new BadRequestException(error.message); case CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND: diff --git a/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts new file mode 100644 index 00000000000..c1a6012c249 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts @@ -0,0 +1,155 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value'; +import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; + +import { ActivityQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler'; +import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler'; +import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler'; +import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler'; +import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; + +// TODO: find a way to prevent conflict between handlers executing logic on object relations +// And this factory that is also executing logic on object relations +// Right now the factory will override any change made on relations by the handlers +@Injectable() +export class CommonResultGettersService { + private readonly logger = new Logger(CommonResultGettersService.name); + private handlers: Map; + + constructor(private readonly fileService: FileService) { + this.initializeHandlers(); + } + + private initializeHandlers() { + this.handlers = new Map([ + ['attachment', new AttachmentQueryResultGetterHandler(this.fileService)], + ['person', new PersonQueryResultGetterHandler(this.fileService)], + [ + 'workspaceMember', + new WorkspaceMemberQueryResultGetterHandler(this.fileService), + ], + ['note', new ActivityQueryResultGetterHandler(this.fileService)], + ['task', new ActivityQueryResultGetterHandler(this.fileService)], + ]); + } + + private async processRecordArray( + recordArray: ObjectRecord[], + objectMetadataItemId: string, + objectMetadataMaps: ObjectMetadataMaps, + workspaceId: string, + ) { + return await Promise.all( + recordArray.map( + async (record: ObjectRecord) => + await this.processRecord( + record, + objectMetadataItemId, + objectMetadataMaps, + workspaceId, + ), + ), + ); + } + + private async processRecord( + record: ObjectRecord, + objectMetadataItemId: string, + objectMetadataMaps: ObjectMetadataMaps, + workspaceId: string, + ): Promise { + const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId]; + + if (!isDefined(objectMetadataMapItem)) { + throw new Error('Object metadata map item is not defined'); + } + + const handler = this.getHandler(objectMetadataMapItem.nameSingular); + + const relationFields = Object.keys(record) + .map( + (recordFieldName) => + objectMetadataMapItem.fieldsById[ + objectMetadataMapItem.fieldIdByName[recordFieldName] + ], + ) + .filter(isDefined) + .filter((fieldMetadata) => + isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION), + ); + + const relationFieldsProcessedMap = {} as Record< + string, + QueryResultFieldValue + >; + + for (const relationField of relationFields) { + if (!isDefined(relationField.relationTargetObjectMetadataId)) { + throw new Error('Relation target object metadata id is not defined'); + } + + const recordFieldValue = record[relationField.name]; + + if (!isDefined(recordFieldValue)) { + continue; + } + + relationFieldsProcessedMap[relationField.name] = + relationField.settings?.relationType === RelationType.ONE_TO_MANY + ? await this.processRecordArray( + record[relationField.name], + relationField.relationTargetObjectMetadataId, + objectMetadataMaps, + workspaceId, + ) + : await this.processRecord( + record[relationField.name], + relationField.relationTargetObjectMetadataId, + objectMetadataMaps, + workspaceId, + ); + } + + const objectRecordProcessedWithoutRelationFields = await handler.handle( + record, + workspaceId, + ); + + const processedRecord = { + ...objectRecordProcessedWithoutRelationFields, + ...relationFieldsProcessedMap, + }; + + return processedRecord; + } + + async processQueryResult( + queryResultField: ObjectRecord[], + objectMetadataItemId: string, + objectMetadataMaps: ObjectMetadataMaps, + workspaceId: string, + ): Promise { + return await this.processRecordArray( + queryResultField, + objectMetadataItemId, + objectMetadataMaps, + workspaceId, + ); + } + + private getHandler(objectType: string): QueryResultGetterHandlerInterface { + return ( + this.handlers.get(objectType) || { + handle: (result: ObjectRecord): Promise => + Promise.resolve(result), + } + ); + } +} diff --git a/packages/twenty-server/src/engine/api/common/core-common-api.module.ts b/packages/twenty-server/src/engine/api/common/core-common-api.module.ts index 8acc5b706ee..981aa30a2b5 100644 --- a/packages/twenty-server/src/engine/api/common/core-common-api.module.ts +++ b/packages/twenty-server/src/engine/api/common/core-common-api.module.ts @@ -3,12 +3,14 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CommonArgsHandlers } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers'; import { CommonQueryRunners } from 'src/engine/api/common/common-query-runners/common-query-runners'; +import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service'; import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper'; import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module'; import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module'; import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; +import { FileModule } from 'src/engine/core-modules/file/file.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @@ -23,6 +25,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor UserRoleModule, ApiKeyModule, WorkspacePermissionsCacheModule, + FileModule, ], providers: [ ProcessNestedRelationsHelper, @@ -30,6 +33,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor ...CommonArgsHandlers, ProcessAggregateHelper, ...CommonQueryRunners, + CommonResultGettersService, ], exports: [...CommonQueryRunners], }) diff --git a/packages/twenty-server/src/engine/api/common/types/common-page-info.type.ts b/packages/twenty-server/src/engine/api/common/types/common-page-info.type.ts new file mode 100644 index 00000000000..82bb2395c3e --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/types/common-page-info.type.ts @@ -0,0 +1,8 @@ +import { type PageInfo } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; + +export type CommonPageInfo = { + hasNextPage: NonNullable; + hasPreviousPage: NonNullable; + startCursor: string | null; + endCursor: string | null; +}; diff --git a/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts b/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts index 052dab93c83..793981e1bca 100644 --- a/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts +++ b/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts @@ -1,9 +1,13 @@ -import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { + type ObjectRecordFilter, + type ObjectRecordOrderBy, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { type CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type'; export enum CommonQueryNames { findOne = 'findOne', + findMany = 'findMany', } export interface FindOneQueryArgs { @@ -11,4 +15,14 @@ export interface FindOneQueryArgs { filter?: ObjectRecordFilter; } -export type CommonQueryArgs = FindOneQueryArgs; +export interface FindManyQueryArgs { + selectedFieldsResult: CommonSelectedFieldsResult; + filter?: ObjectRecordFilter; + orderBy?: ObjectRecordOrderBy; + first?: number; + last?: number; + before?: string; + after?: string; +} + +export type CommonQueryArgs = FindOneQueryArgs | FindManyQueryArgs; diff --git a/packages/twenty-server/src/engine/api/common/types/common-selected-fields-result.type.ts b/packages/twenty-server/src/engine/api/common/types/common-selected-fields-result.type.ts index baac2e6eab0..a36bd10156a 100644 --- a/packages/twenty-server/src/engine/api/common/types/common-selected-fields-result.type.ts +++ b/packages/twenty-server/src/engine/api/common/types/common-selected-fields-result.type.ts @@ -1,3 +1,5 @@ +import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; + interface SelectedFields { [key: string]: boolean | SelectedFields; } @@ -5,5 +7,6 @@ interface SelectedFields { export type CommonSelectedFieldsResult = { select: SelectedFields; relations: SelectedFields; - aggregate: SelectedFields; + //TODO = Refacto-common - to update when rest api will handle aggregates + aggregate: Record; }; diff --git a/packages/twenty-server/src/engine/api/common/utils/get-page-info.util.ts b/packages/twenty-server/src/engine/api/common/utils/get-page-info.util.ts new file mode 100644 index 00000000000..107f1f82348 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/utils/get-page-info.util.ts @@ -0,0 +1,32 @@ +import { + type ObjectRecord, + type ObjectRecordOrderBy, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { type CommonPageInfo } from 'src/engine/api/common/types/common-page-info.type'; +import { + encodeCursor, + getPaginationInfo, +} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; + +export const getPageInfo = ( + records: ObjectRecord[], + orderBy: ObjectRecordOrderBy, + limit: number, + isForwardPagination: boolean, +): CommonPageInfo => { + const { hasNextPage, hasPreviousPage } = getPaginationInfo( + records, + limit, + isForwardPagination, + ); + + const startCursor = + records.length > 0 ? encodeCursor(records[0], orderBy) : null; + const endCursor = + records.length > 0 + ? encodeCursor(records[records.length - 1], orderBy) + : null; + + return { startCursor, endCursor, hasNextPage, hasPreviousPage }; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts index 91cbd87fef7..6a4dc837341 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts @@ -25,6 +25,7 @@ import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modu import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { isPlainObject } from 'src/utils/is-plain-object'; +// TODO: Refacto-common - Rename CommonRecordsToGraphqlConnectionHelper export class ObjectRecordsToGraphqlConnectionHelper { private objectMetadataMaps: ObjectMetadataMaps; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts index 647c7b80dee..4fae1403970 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts @@ -175,7 +175,9 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); return typeORMObjectRecordsParser.createConnection({ - objectRecords, + objectRecords: isForwardPagination + ? objectRecords + : objectRecords.reverse(), objectRecordsAggregatedValues: parentObjectRecordsAggregatedValues, selectedAggregatedFields: executionArgs.graphqlQuerySelectedFieldsResult.aggregate, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts index 18ab130c8fe..4b40e5150e5 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts @@ -64,8 +64,7 @@ export const getCursor = ( }; export const getPaginationInfo = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - objectRecords: any[], + objectRecords: ObjectRecord[], limit: number, isForwardPagination: boolean, ) => { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts index 11b44e5afa5..3336b5bfdd8 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts @@ -1,15 +1,23 @@ import { Injectable } from '@nestjs/common'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; +import graphqlFields from 'graphql-fields'; +import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; + import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type FindManyResolverArgs, type Resolver, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service'; +import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; +import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @Injectable() export class FindManyResolverFactory @@ -18,7 +26,9 @@ export class FindManyResolverFactory public static methodName = RESOLVER_METHOD_NAMES.FIND_MANY; constructor( + private readonly commonFindManyQueryRunnerService: CommonFindManyQueryRunnerService, private readonly graphqlQueryRunnerService: GraphqlQueryFindManyResolverService, + private readonly featureFlagService: FeatureFlagService, ) {} create( @@ -27,17 +37,64 @@ export class FindManyResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: + const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_COMMON_API_ENABLED, + internalContext.authContext.workspace?.id as string, + ); + + if (isCommonApiEnabled) { + const graphqlQueryParser = new GraphqlQueryParser( internalContext.objectMetadataItemWithFieldMaps, - }; + internalContext.objectMetadataMaps, + ); + + const selectedFieldsResult = graphqlQueryParser.parseSelectedFields( + internalContext.objectMetadataItemWithFieldMaps, + graphqlFields(info), + internalContext.objectMetadataMaps, + ); + + try { + const { records, aggregatedValues, totalCount, pageInfo } = + await this.commonFindManyQueryRunnerService.run({ + args: { ...args, selectedFieldsResult }, + authContext: internalContext.authContext, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, + }); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, + ); + + return typeORMObjectRecordsParser.createConnection({ + objectRecords: records, + objectRecordsAggregatedValues: aggregatedValues, + selectedAggregatedFields: selectedFieldsResult.aggregate, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: args.first ?? args.last ?? QUERY_MAX_RECORDS, + totalCount, + order: args.orderBy, + hasNextPage: pageInfo.hasNextPage, + hasPreviousPage: pageInfo.hasPreviousPage, + }); + } catch (error) { + return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + } return await this.graphqlQueryRunnerService.execute( args, - options, + { + authContext: internalContext.authContext, + info, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, + }, FindManyResolverFactory.methodName, ); }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts index 72b45fde1fa..bdc1bfc794d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts @@ -11,6 +11,7 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace- import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; +import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service'; import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; @@ -53,15 +54,28 @@ export class FindOneResolverFactory internalContext.objectMetadataMaps, ); - return await this.commonFindOneQueryRunnerService.run({ + const record = await this.commonFindOneQueryRunnerService.run({ args: { ...args, selectedFieldsResult }, authContext: internalContext.authContext, objectMetadataMaps: internalContext.objectMetadataMaps, objectMetadataItemWithFieldMaps: internalContext.objectMetadataItemWithFieldMaps, }); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, + ); + + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); + return workspaceQueryRunnerGraphqlApiExceptionHandler(error); } } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts index 9521b024442..c21e3509e5a 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts @@ -9,6 +9,7 @@ import { isDefined } from 'twenty-shared/utils'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; @@ -85,7 +86,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { recordIds: createdRecordsIds, repository, objectMetadata, - depth: this.depthInputFactory.create(request), + depth: parseDepthRestRequest(request), restrictedFields, }); diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts index 220238c4c1b..29775089a99 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts @@ -9,6 +9,7 @@ import { isDefined } from 'twenty-shared/utils'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; @@ -66,7 +67,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { recordIds: [createdRecord.id], repository, objectMetadata, - depth: this.depthInputFactory.create(request), + depth: parseDepthRestRequest(request), restrictedFields, }); diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts index 607eb2f6d36..2c5dcc0f03f 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts @@ -1,11 +1,29 @@ import { Injectable } from '@nestjs/common'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { + PageInfo, + RestApiBaseHandler, +} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; +import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util'; +import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; +import { parseLimitRestRequest } from 'src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util'; +import { parseOrderByRestRequest } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util'; +import { parseStartingAfterRestRequest } from 'src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; +import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; @Injectable() export class RestApiFindManyHandler extends RestApiBaseHandler { + constructor( + private readonly commonFindManyQueryRunnerService: CommonFindManyQueryRunnerService, + ) { + super(); + } + async handle(request: AuthenticatedRequest) { const { repository, @@ -73,4 +91,73 @@ export class RestApiFindManyHandler extends RestApiBaseHandler { totalCount, }); } + + async commonHandle(request: AuthenticatedRequest) { + try { + const parsedArgs = this.parseRequestArgs(request); + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + } = await this.buildCommonOptions(request); + + const selectedFieldsResult = await this.computeSelectedFields({ + depth: parsedArgs.depth, + objectMetadataMapItem: objectMetadataItemWithFieldMaps, + objectMetadataMaps, + authContext, + }); + + const { records, aggregatedValues, pageInfo } = + await this.commonFindManyQueryRunnerService.run({ + args: { ...parsedArgs, selectedFieldsResult }, + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }); + + return this.formatRestResponse( + records, + aggregatedValues, + objectMetadataItemWithFieldMaps.namePlural, + pageInfo, + ); + } catch (error) { + workspaceQueryRunnerRestApiExceptionHandler(error); + } + } + + private formatRestResponse( + records: ObjectRecord[], + aggregatedValues: Record, + objectNamePlural: string, + pageInfo: PageInfo, + ) { + return { + data: { + [objectNamePlural]: records, + }, + totalCount: aggregatedValues.totalCount, + pageInfo, + }; + } + + private parseRequestArgs(request: AuthenticatedRequest) { + const depth = parseDepthRestRequest(request); + const limit = parseLimitRestRequest(request); + const orderBy = parseOrderByRestRequest(request); + const filter = parseFilterRestRequest(request); + const endingBefore = parseEndingBeforeRestRequest(request); + const startingAfter = parseStartingAfterRestRequest(request); + + return { + filter, + orderBy, + first: !endingBefore ? limit : undefined, + last: endingBefore ? limit : undefined, + before: endingBefore, + after: startingAfter, + depth, + }; + } } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts index fc12e36c670..5c6df547f67 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts @@ -7,6 +7,7 @@ import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service'; import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; @@ -95,29 +96,11 @@ export class RestApiFindOneHandler extends RestApiBaseHandler { private async parseRequestArgs(request: AuthenticatedRequest) { const { id: recordId } = parseCorePath(request); const filter = { id: { eq: recordId } }; - const depth = this.depthInputFactory.create(request); + const depth = parseDepthRestRequest(request); return { filter, depth, }; } - - private async buildCommonOptions(request: AuthenticatedRequest) { - const { object: parsedObject } = parseCorePath(request); - - const { objectMetadataMaps, objectMetadataMapItem } = - await this.coreQueryBuilderFactory.getObjectMetadata( - request, - parsedObject, - ); - - const authContext = this.getAuthContextFromRequest(request); - - return { - authContext: authContext, - objectMetadataItemWithFieldMaps: objectMetadataMapItem, - objectMetadataMaps: objectMetadataMaps, - }; - } } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts index 01554da9902..2fa3f5858d2 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts @@ -10,6 +10,7 @@ import { isDefined } from 'twenty-shared/utils'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; @@ -62,7 +63,7 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler { recordIds: [updatedRecordId], repository, objectMetadata, - depth: this.depthInputFactory.create(request), + depth: parseDepthRestRequest(request), restrictedFields, }); diff --git a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts index 9d5e063f3df..fae621a7720 100644 --- a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts @@ -1,6 +1,5 @@ import { BadRequestException, Inject } from '@nestjs/common'; -import { type Request } from 'express'; import chunk from 'lodash.chunk'; import isEmpty from 'lodash.isempty'; import { @@ -23,11 +22,9 @@ import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/fact import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; import { RestToCommonSelectedFieldsHandler } from 'src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler'; import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type'; -import { - DepthInputFactory, - MAX_DEPTH, - type Depth, -} from 'src/engine/api/rest/input-factories/depth-input.factory'; +import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; +import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; @@ -93,8 +90,6 @@ export abstract class RestApiBaseHandler { @Inject() protected readonly getVariablesFactory: GetVariablesFactory; @Inject() - protected readonly depthInputFactory: DepthInputFactory; - @Inject() protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService; @Inject() protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService; @@ -392,7 +387,7 @@ export abstract class RestApiBaseHandler { extraFilters, restrictedFields, }: { - request: Request; + request: AuthenticatedRequest; recordId?: string; repository: WorkspaceRepository; objectMetadata: { @@ -463,7 +458,7 @@ export abstract class RestApiBaseHandler { recordIds: recordIds.map((record) => record.id), repository, objectMetadata, - depth: this.depthInputFactory.create(request), + depth: parseDepthRestRequest(request), restrictedFields, }); @@ -610,4 +605,22 @@ export abstract class RestApiBaseHandler { depth, }); } + + async buildCommonOptions(request: AuthenticatedRequest) { + const { object: parsedObject } = parseCorePath(request); + + const { objectMetadataMaps, objectMetadataMapItem } = + await this.coreQueryBuilderFactory.getObjectMetadata( + request, + parsedObject, + ); + + const authContext = this.getAuthContextFromRequest(request); + + return { + authContext, + objectMetadataItemWithFieldMaps: objectMetadataMapItem, + objectMetadataMaps: objectMetadataMaps, + }; + } } diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/get-variables.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/get-variables.factory.ts index 1df2ec25875..5617c6ed505 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/get-variables.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/get-variables.factory.ts @@ -1,30 +1,27 @@ import { Injectable } from '@nestjs/common'; -import { type Request } from 'express'; import { isDefined } from 'twenty-shared/utils'; import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type'; -import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory'; import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory'; -import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory'; import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory'; -import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory'; +import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util'; +import { parseLimitRestRequest } from 'src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util'; +import { parseStartingAfterRestRequest } from 'src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util'; +import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class GetVariablesFactory { constructor( - private readonly startingAfterInputFactory: StartingAfterInputFactory, - private readonly endingBeforeInputFactory: EndingBeforeInputFactory, - private readonly limitInputFactory: LimitInputFactory, private readonly orderByInputFactory: OrderByInputFactory, private readonly filterInputFactory: FilterInputFactory, ) {} create( id: string | undefined, - request: Request, + request: AuthenticatedRequest, objectMetadata: { objectMetadataMaps: ObjectMetadataMaps; objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; @@ -35,10 +32,10 @@ export class GetVariablesFactory { } const filter = this.filterInputFactory.create(request, objectMetadata); - const limit = this.limitInputFactory.create(request); + const limit = parseLimitRestRequest(request); const orderBy = this.orderByInputFactory.create(request, objectMetadata); - const endingBefore = this.endingBeforeInputFactory.create(request); - const startingAfter = this.startingAfterInputFactory.create(request); + const endingBefore = parseEndingBeforeRestRequest(request); + const startingAfter = parseStartingAfterRestRequest(request); return { filter, diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts index 34005e10d2e..7deeb4a2fe5 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts @@ -127,7 +127,7 @@ describe('parseFilter', () => { }); }); - it('should handler not', () => { + it('should handle not', () => { expect( parseFilter( 'and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))', diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils.ts index 550a5f114fa..e581163cf12 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils.ts @@ -1,12 +1,12 @@ import { BadRequestException } from '@nestjs/common'; import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils'; -import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values'; -import { formatFieldValue } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils'; -import { parseBaseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils'; -import { parseFilterContent } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter-content.utils'; import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils'; import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type'; +import { checkFilterEnumValues } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util'; +import { formatFieldValue } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/format-field-values.util'; +import { parseBaseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util'; +import { parseFilterContent } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-content.util'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; export enum Conjunctions { @@ -19,7 +19,7 @@ export const parseFilter = ( filterQuery: string, objectMetadataItem: ObjectMetadataItemWithFieldMaps, ): Record => { - const result = {}; + const result: Record = {}; const match = filterQuery.match( `^(${Object.values(Conjunctions).join('|')})\\((.+)\\)$`, ); @@ -42,10 +42,8 @@ export const parseFilter = ( `'filter' invalid. 'not' conjunction should contain only 1 condition. eg: not(field[eq]:1)`, ); } - // @ts-expect-error legacy noImplicitAny result[conjunction] = subResult[0]; } else { - // @ts-expect-error legacy noImplicitAny result[conjunction] = subResult; } diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts index 86925b44407..4b7735669e7 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts @@ -4,10 +4,8 @@ import { FieldMetadataType, ObjectsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type'; -import { - Depth, - MAX_DEPTH, -} from 'src/engine/api/rest/input-factories/depth-input.factory'; +import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant'; +import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type'; import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; diff --git a/packages/twenty-server/src/engine/api/rest/core/services/rest-api-core.service.ts b/packages/twenty-server/src/engine/api/rest/core/services/rest-api-core.service.ts index cbaf20b5f4c..355c471fd73 100644 --- a/packages/twenty-server/src/engine/api/rest/core/services/rest-api-core.service.ts +++ b/packages/twenty-server/src/engine/api/rest/core/services/rest-api-core.service.ts @@ -27,6 +27,13 @@ export class RestApiCoreService { private readonly featureFlagService: FeatureFlagService, ) {} + private async isCommonApiEnabled(request: AuthenticatedRequest) { + return await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_COMMON_API_ENABLED, + request.workspace.id, + ); + } + async delete(request: AuthenticatedRequest) { return await this.restApiDeleteOneHandler.handle(request); } @@ -49,20 +56,20 @@ export class RestApiCoreService { async get(request: AuthenticatedRequest) { const { id: recordId } = parseCorePath(request); + const isCommonApiEnabled = await this.isCommonApiEnabled(request); - if (isDefined(recordId)) { - const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_COMMON_API_ENABLED, - request.workspace.id, - ); - - if (isCommonApiEnabled) { + if (isCommonApiEnabled) { + if (isDefined(recordId)) { return await this.restApiFindOneHandler.commonHandle(request); + } else { + return await this.restApiFindManyHandler.commonHandle(request); } - - return await this.restApiFindOneHandler.handle(request); } else { - return await this.restApiFindManyHandler.handle(request); + if (isDefined(recordId)) { + return await this.restApiFindOneHandler.handle(request); + } else { + return await this.restApiFindManyHandler.handle(request); + } } } } diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/ending-before-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/ending-before-input.factory.spec.ts deleted file mode 100644 index 64c441f29ce..00000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/ending-before-input.factory.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Test, type TestingModule } from '@nestjs/testing'; - -import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory'; - -describe('EndingBeforeInputFactory', () => { - let service: EndingBeforeInputFactory; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [EndingBeforeInputFactory], - }).compile(); - - service = module.get(EndingBeforeInputFactory); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('create', () => { - it('should return default if ending_before missing', () => { - const request: any = { query: {} }; - - expect(service.create(request)).toEqual(undefined); - }); - - it('should return ending_before', () => { - const request: any = { query: { ending_before: 'uuid' } }; - - expect(service.create(request)).toEqual('uuid'); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/limit-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/limit-input.factory.spec.ts deleted file mode 100644 index 0b76244e3a3..00000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/limit-input.factory.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Test, type TestingModule } from '@nestjs/testing'; - -import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory'; - -describe('LimitInputFactory', () => { - let service: LimitInputFactory; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [LimitInputFactory], - }).compile(); - - service = module.get(LimitInputFactory); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('create', () => { - it('should return default if limit missing', () => { - const request: any = { query: {} }; - - expect(service.create(request)).toEqual(60); - }); - - it('should return limit', () => { - const request: any = { query: { limit: '10' } }; - - expect(service.create(request)).toEqual(10); - }); - - it('should throw if not integer', () => { - const request: any = { query: { limit: 'aaa' } }; - - expect(() => service.create(request)).toThrow( - "limit 'aaa' is invalid. Should be an integer", - ); - }); - - it('should throw if limit negative', () => { - const request: any = { query: { limit: -1 } }; - - expect(() => service.create(request)).toThrow( - "limit '-1' is invalid. Should be an integer", - ); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/starting-before-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/starting-before-input.factory.spec.ts deleted file mode 100644 index a5804072b4b..00000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/starting-before-input.factory.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Test, type TestingModule } from '@nestjs/testing'; - -import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory'; - -describe('StartingAfterInputFactory', () => { - let service: StartingAfterInputFactory; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [StartingAfterInputFactory], - }).compile(); - - service = module.get(StartingAfterInputFactory); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('create', () => { - it('should return default if starting_after missing', () => { - const request: any = { query: {} }; - - expect(service.create(request)).toEqual(undefined); - }); - - it('should return starting_after', () => { - const request: any = { query: { starting_after: 'uuid' } }; - - expect(service.create(request)).toEqual('uuid'); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/depth-input.factory.ts b/packages/twenty-server/src/engine/api/rest/input-factories/depth-input.factory.ts deleted file mode 100644 index d959eb42a3a..00000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/depth-input.factory.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; - -import { type Request } from 'express'; - -export const MAX_DEPTH = 2; - -export type Depth = 0 | 1 | 2; - -const ALLOWED_DEPTH_VALUES: Depth[] = [0, 1]; - -@Injectable() -export class DepthInputFactory { - create(request: Request): Depth { - if (!request.query.depth) { - return 0; - } - - const depth = +request.query.depth as Depth; - - if (isNaN(depth) || !ALLOWED_DEPTH_VALUES.includes(depth)) { - throw new BadRequestException( - `'depth=${ - request.query.depth - }' parameter invalid. Allowed values are ${ALLOWED_DEPTH_VALUES.join( - ', ', - )}`, - ); - } - - return depth; - } -} diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/factories.ts b/packages/twenty-server/src/engine/api/rest/input-factories/factories.ts index 56043dae873..335ff460d3d 100644 --- a/packages/twenty-server/src/engine/api/rest/input-factories/factories.ts +++ b/packages/twenty-server/src/engine/api/rest/input-factories/factories.ts @@ -1,15 +1,4 @@ -import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory'; -import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory'; -import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory'; -import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory'; import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory'; -import { DepthInputFactory } from 'src/engine/api/rest/input-factories/depth-input.factory'; +import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory'; -export const inputFactories = [ - DepthInputFactory, - EndingBeforeInputFactory, - FilterInputFactory, - LimitInputFactory, - OrderByInputFactory, - StartingAfterInputFactory, -]; +export const inputFactories = [FilterInputFactory, OrderByInputFactory]; diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/filter-input.factory.ts b/packages/twenty-server/src/engine/api/rest/input-factories/filter-input.factory.ts index 510c4241cf1..07a7c47c605 100644 --- a/packages/twenty-server/src/engine/api/rest/input-factories/filter-input.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/input-factories/filter-input.factory.ts @@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common'; import { type Request } from 'express'; -import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils'; -import { checkFilterQuery } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-query.utils'; import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils'; import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type'; +import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util'; +import { checkFilterQuery } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/limit-input.factory.ts b/packages/twenty-server/src/engine/api/rest/input-factories/limit-input.factory.ts deleted file mode 100644 index 549ba07be4d..00000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/limit-input.factory.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; - -import { - QUERY_DEFAULT_LIMIT_RECORDS, - QUERY_MAX_RECORDS, -} from 'twenty-shared/constants'; - -import { type RequestContext } from 'src/engine/api/rest/types/RequestContext'; - -@Injectable() -export class LimitInputFactory { - create( - request: RequestContext, - defaultLimit = QUERY_DEFAULT_LIMIT_RECORDS, - ): number { - if (!request.query?.limit) { - return defaultLimit; - } - const limit = +request.query.limit; - - if (isNaN(limit) || limit < 0) { - throw new BadRequestException( - `limit '${request.query.limit}' is invalid. Should be an integer`, - ); - } - - return Math.min(limit, QUERY_MAX_RECORDS); - } -} diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/starting-after-input.factory.ts b/packages/twenty-server/src/engine/api/rest/input-factories/starting-after-input.factory.ts deleted file mode 100644 index 24119c0a7f5..00000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/starting-after-input.factory.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type RequestContext } from 'src/engine/api/rest/types/RequestContext'; - -@Injectable() -export class StartingAfterInputFactory { - create(request: RequestContext): string | undefined { - const cursorQuery = request.query?.starting_after; - - if (typeof cursorQuery !== 'string') { - return undefined; - } - - return cursorQuery; - } -} diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/constants/max-depth.constant.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/constants/max-depth.constant.ts new file mode 100644 index 00000000000..29dab1f07c2 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/constants/max-depth.constant.ts @@ -0,0 +1 @@ +export const MAX_DEPTH = 2; diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util.ts new file mode 100644 index 00000000000..06d82af9c7f --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util.ts @@ -0,0 +1,26 @@ +import { BadRequestException } from '@nestjs/common'; + +import { type Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type'; +import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; + +export const parseDepthRestRequest = (request: AuthenticatedRequest): Depth => { + if (!request.query.depth) { + return 0; + } + + const depth = +request.query.depth as Depth; + + const ALLOWED_DEPTH_VALUES: Depth[] = [0, 1]; + + if (isNaN(depth) || !ALLOWED_DEPTH_VALUES.includes(depth)) { + throw new BadRequestException( + `'depth=${ + request.query.depth + }' parameter invalid. Allowed values are ${ALLOWED_DEPTH_VALUES.join( + ', ', + )}`, + ); + } + + return depth; +}; diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/ending-before-parser-utils/__tests__/parse-ending-before-rest-request.util.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/ending-before-parser-utils/__tests__/parse-ending-before-rest-request.util.spec.ts new file mode 100644 index 00000000000..e761d82bab7 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/ending-before-parser-utils/__tests__/parse-ending-before-rest-request.util.spec.ts @@ -0,0 +1,15 @@ +import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util'; + +describe('parseEndingBeforeRestRequest', () => { + it('should return default if ending_before missing', () => { + const request: any = { query: {} }; + + expect(parseEndingBeforeRestRequest(request)).toEqual(undefined); + }); + + it('should return ending_before', () => { + const request: any = { query: { ending_before: 'uuid' } }; + + expect(parseEndingBeforeRestRequest(request)).toEqual('uuid'); + }); +}); diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util.ts new file mode 100644 index 00000000000..c3a46479c9a --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util.ts @@ -0,0 +1,14 @@ +import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; +import { type RequestContext } from 'src/engine/api/rest/types/RequestContext'; + +export const parseEndingBeforeRestRequest = ( + request: AuthenticatedRequest | RequestContext, +): string | undefined => { + const cursorQuery = request.query?.ending_before; + + if (typeof cursorQuery !== 'string') { + return undefined; + } + + return cursorQuery; +}; diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/add-default-conjunction.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/add-default-conjunction.util.spec.ts similarity index 85% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/add-default-conjunction.utils.spec.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/add-default-conjunction.util.spec.ts index 1b949c85519..e1a2c69af19 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/add-default-conjunction.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/add-default-conjunction.util.spec.ts @@ -1,4 +1,4 @@ -import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils'; +import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util'; describe('addDefaultConjunctionIfMissing', () => { it('should add default conjunction if missing', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-enum-values.util.spec.ts similarity index 94% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-enum-values.util.spec.ts index 8c9ee46fdf5..bc2fba7b388 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-enum-values.util.spec.ts @@ -4,7 +4,7 @@ import { fieldSelectMock, objectMetadataItemMock, } from 'src/engine/api/__mocks__/object-metadata-item.mock'; -import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values'; +import { checkFilterEnumValues } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util'; import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-query.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-query.util.spec.ts similarity index 88% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-query.utils.spec.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-query.util.spec.ts index 7cba92df6d0..1fe66fc0f88 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-query.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-query.util.spec.ts @@ -1,4 +1,4 @@ -import { checkFilterQuery } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-query.utils'; +import { checkFilterQuery } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util'; describe('checkFilterQuery', () => { it('should check filter query', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/format-field-values.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/format-field-values.util.spec.ts similarity index 94% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/format-field-values.utils.spec.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/format-field-values.util.spec.ts index 99febb2de7f..ad1d98d4464 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/format-field-values.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/format-field-values.util.spec.ts @@ -1,6 +1,6 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { formatFieldValue } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils'; +import { formatFieldValue } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/format-field-values.util'; describe('formatFieldValue', () => { it('should format fieldNumber value', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-base-filter.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-base-filter.util.spec.ts similarity index 91% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-base-filter.utils.spec.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-base-filter.util.spec.ts index dc41296b7ac..cac0eec4fa5 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-base-filter.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-base-filter.util.spec.ts @@ -1,4 +1,4 @@ -import { parseBaseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils'; +import { parseBaseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util'; describe('parseBaseFilter', () => { it('should parse simple filter string test 1', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter-content.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-filter-content.util.spec.ts similarity index 92% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter-content.utils.spec.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-filter-content.util.spec.ts index 884c672395d..bf02c82790a 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter-content.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-filter-content.util.spec.ts @@ -1,4 +1,4 @@ -import { parseFilterContent } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter-content.utils'; +import { parseFilterContent } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-content.util'; describe('parseFilterContent', () => { it('should parse query filter test 1', () => { diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-filter-without-metdata-validation.util.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-filter-without-metdata-validation.util.spec.ts new file mode 100644 index 00000000000..f1944c9dfd7 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-filter-without-metdata-validation.util.spec.ts @@ -0,0 +1,88 @@ +import { parseFilterWithoutMetadataValidation } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util'; + +describe('parseFilterWithoutMetadataValidation', () => { + it('should parse string filter test 1', () => { + expect( + parseFilterWithoutMetadataValidation( + 'and(fieldNumber[eq]:1,fieldNumber[eq]:2)', + ), + ).toEqual({ + and: [{ fieldNumber: { eq: '1' } }, { fieldNumber: { eq: '2' } }], + }); + }); + + it('should parse string filter test 2', () => { + expect( + parseFilterWithoutMetadataValidation( + 'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3))', + ), + ).toEqual({ + and: [ + { fieldNumber: { eq: '1' } }, + { or: [{ fieldNumber: { eq: '2' } }, { fieldNumber: { eq: '3' } }] }, + ], + }); + }); + + it('should parse string filter test 3', () => { + expect( + parseFilterWithoutMetadataValidation( + 'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3,and(fieldNumber[eq]:6,fieldNumber[eq]:7)),or(fieldNumber[eq]:4,fieldNumber[eq]:5))', + ), + ).toEqual({ + and: [ + { fieldNumber: { eq: '1' } }, + { + or: [ + { fieldNumber: { eq: '2' } }, + { fieldNumber: { eq: '3' } }, + { + and: [{ fieldNumber: { eq: '6' } }, { fieldNumber: { eq: '7' } }], + }, + ], + }, + { or: [{ fieldNumber: { eq: '4' } }, { fieldNumber: { eq: '5' } }] }, + ], + }); + }); + + it('should parse string filter test 4', () => { + expect( + parseFilterWithoutMetadataValidation( + 'and(fieldText[gt]:"val,ue",or(fieldNumber[is]:NOT_NULL,not(fieldText[startsWith]:"val"),and(fieldNumber[eq]:6,fieldText[ilike]:"%val%")),or(fieldNumber[eq]:4,fieldText[is]:NULL))', + ), + ).toEqual({ + and: [ + { fieldText: { gt: 'val,ue' } }, + { + or: [ + { fieldNumber: { is: 'NOT_NULL' } }, + { not: { fieldText: { startsWith: 'val' } } }, + { + and: [ + { fieldNumber: { eq: '6' } }, + { fieldText: { ilike: '%val%' } }, + ], + }, + ], + }, + { or: [{ fieldNumber: { eq: '4' } }, { fieldText: { is: 'NULL' } }] }, + ], + }); + }); + + it('should handle not', () => { + expect( + parseFilterWithoutMetadataValidation( + 'and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))', + ), + ).toEqual({ + and: [ + { fieldNumber: { eq: '1' } }, + { + not: { fieldNumber: { eq: '2' } }, + }, + ], + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util.ts similarity index 100% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util.ts diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util.ts similarity index 100% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util.ts diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-query.utils.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util.ts similarity index 100% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-query.utils.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util.ts diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/format-field-values.util.ts similarity index 100% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/format-field-values.util.ts diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util.ts similarity index 100% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util.ts diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter-content.utils.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-content.util.ts similarity index 100% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter-content.utils.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-content.util.ts diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util.ts new file mode 100644 index 00000000000..e96da03b4e3 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util.ts @@ -0,0 +1,22 @@ +//TODO : Refacto-common - remove this comment - This parser is a copy of the filter input factory without objectMetadata dependency. Validation will be done in common layer +import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type'; +import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util'; +import { checkFilterQuery } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util'; +import { parseFilterWithoutMetadataValidation } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util'; +import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; + +export const parseFilterRestRequest = ( + request: AuthenticatedRequest, +): Record => { + let filterQuery = request.query.filter; + + if (typeof filterQuery !== 'string') { + return {}; + } + + checkFilterQuery(filterQuery); + + filterQuery = addDefaultConjunctionIfMissing(filterQuery); + + return parseFilterWithoutMetadataValidation(filterQuery); +}; diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util.ts new file mode 100644 index 00000000000..3a991cafa72 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util.ts @@ -0,0 +1,55 @@ +import { BadRequestException } from '@nestjs/common'; + +import { Conjunctions } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils'; +import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type'; +import { formatFieldValue } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/format-field-values.util'; +import { parseBaseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-base-filter.util'; +import { parseFilterContent } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-content.util'; + +//TODO : Refacto-common - Rename after deleting parseFilter +export const parseFilterWithoutMetadataValidation = ( + filterQuery: string, +): Record => { + const result = {}; + const match = filterQuery.match( + `^(${Object.values(Conjunctions).join('|')})\\((.+)\\)$`, + ); + + if (match) { + const conjunction = match?.[1]; + + if (!conjunction) { + throw new BadRequestException( + 'Error while matching filter query. Conjunction not found', + ); + } + const subResult = parseFilterContent(filterQuery).map((elem) => + parseFilterWithoutMetadataValidation(elem), + ); + + if (conjunction === Conjunctions.not) { + if (subResult.length > 1) { + throw new BadRequestException( + `'filter' invalid. 'not' conjunction should contain only 1 condition. eg: not(field[eq]:1)`, + ); + } + // @ts-expect-error legacy noImplicitAny + result[conjunction] = subResult[0]; + } else { + // @ts-expect-error legacy noImplicitAny + result[conjunction] = subResult; + } + + return result; + } + const { fields, comparator, value } = parseBaseFilter(filterQuery); + + const formattedValue = formatFieldValue(value, undefined, comparator); + + return fields.reverse().reduce( + (acc, currentValue) => { + return { [currentValue]: acc }; + }, + { [comparator]: formattedValue }, + ); +}; diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/limit-parser-utils/__tests__/parse-limit-rest-request.util.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/limit-parser-utils/__tests__/parse-limit-rest-request.util.spec.ts new file mode 100644 index 00000000000..826b0dd6a2a --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/limit-parser-utils/__tests__/parse-limit-rest-request.util.spec.ts @@ -0,0 +1,31 @@ +import { parseLimitRestRequest } from 'src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util'; + +describe('parseLimitRestRequest', () => { + it('should return default if limit missing', () => { + const request: any = { query: {} }; + + expect(parseLimitRestRequest(request)).toEqual(60); + }); + + it('should return limit', () => { + const request: any = { query: { limit: '10' } }; + + expect(parseLimitRestRequest(request)).toEqual(10); + }); + + it('should throw if not integer', () => { + const request: any = { query: { limit: 'aaa' } }; + + expect(() => parseLimitRestRequest(request)).toThrow( + "limit 'aaa' is invalid. Should be an integer", + ); + }); + + it('should throw if limit negative', () => { + const request: any = { query: { limit: -1 } }; + + expect(() => parseLimitRestRequest(request)).toThrow( + "limit '-1' is invalid. Should be an integer", + ); + }); +}); diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util.ts new file mode 100644 index 00000000000..18b237422ee --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util.ts @@ -0,0 +1,27 @@ +import { BadRequestException } from '@nestjs/common'; + +import { + QUERY_DEFAULT_LIMIT_RECORDS, + QUERY_MAX_RECORDS, +} from 'twenty-shared/constants'; + +import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; +import { type RequestContext } from 'src/engine/api/rest/types/RequestContext'; + +export const parseLimitRestRequest = ( + request: AuthenticatedRequest | RequestContext, + defaultLimit = QUERY_DEFAULT_LIMIT_RECORDS, +): number => { + if (!request.query?.limit) { + return defaultLimit; + } + const limit = +request.query.limit; + + if (isNaN(limit) || limit < 0) { + throw new BadRequestException( + `limit '${request.query.limit}' is invalid. Should be an integer`, + ); + } + + return Math.min(limit, QUERY_MAX_RECORDS); +}; diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/__tests__/parse-order-by-rest-request.util.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/__tests__/parse-order-by-rest-request.util.spec.ts new file mode 100644 index 00000000000..c385f0d962c --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/__tests__/parse-order-by-rest-request.util.spec.ts @@ -0,0 +1,94 @@ +import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { parseOrderByRestRequest } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util'; + +describe('parseOrderByRestRequest', () => { + it('should return default if order by missing', () => { + const request: any = { query: {} }; + + expect(parseOrderByRestRequest(request)).toEqual([ + {}, + { id: OrderByDirection.AscNullsFirst }, + ]); + }); + + it('should create order by parser properly', () => { + const request: any = { + query: { + order_by: 'fieldNumber[AscNullsFirst],fieldText[DescNullsLast]', + }, + }; + + expect(parseOrderByRestRequest(request)).toEqual([ + { fieldNumber: OrderByDirection.AscNullsFirst }, + { fieldText: OrderByDirection.DescNullsLast }, + { id: OrderByDirection.AscNullsFirst }, + ]); + }); + + it('should choose default direction if missing', () => { + const request: any = { + query: { + order_by: 'fieldNumber', + }, + }; + + expect(parseOrderByRestRequest(request)).toEqual([ + { fieldNumber: OrderByDirection.AscNullsFirst }, + { id: OrderByDirection.AscNullsFirst }, + ]); + }); + + it('should handle complex fields', () => { + const request: any = { + query: { + order_by: 'fieldCurrency.amountMicros', + }, + }; + + expect(parseOrderByRestRequest(request)).toEqual([ + { fieldCurrency: { amountMicros: OrderByDirection.AscNullsFirst } }, + { id: OrderByDirection.AscNullsFirst }, + ]); + }); + + it('should handle complex fields with direction', () => { + const request: any = { + query: { + order_by: 'fieldCurrency.amountMicros[DescNullsLast]', + }, + }; + + expect(parseOrderByRestRequest(request)).toEqual([ + { fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } }, + { id: OrderByDirection.AscNullsFirst }, + ]); + }); + + it('should handle multiple complex fields with direction', () => { + const request: any = { + query: { + order_by: + 'fieldCurrency.amountMicros[DescNullsLast],fieldText.label[AscNullsLast]', + }, + }; + + expect(parseOrderByRestRequest(request)).toEqual([ + { fieldCurrency: { amountMicros: OrderByDirection.DescNullsLast } }, + { fieldText: { label: OrderByDirection.AscNullsLast } }, + { id: OrderByDirection.AscNullsFirst }, + ]); + }); + + it('should throw if direction invalid', () => { + const request: any = { + query: { + order_by: 'fieldText[invalid]', + }, + }; + + expect(() => parseOrderByRestRequest(request)).toThrow( + "'order_by' direction 'invalid' invalid. Allowed values are 'AscNullsFirst', 'AscNullsLast', 'DescNullsFirst', 'DescNullsLast'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3", + ); + }); +}); diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/add-default-order-by-id.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/add-default-order-by-id.util.ts new file mode 100644 index 00000000000..97157690260 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/add-default-order-by-id.util.ts @@ -0,0 +1,12 @@ +import { + type ObjectRecordOrderBy, + OrderByDirection, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +export const addDefaultOrderById = (orderBy: ObjectRecordOrderBy) => { + const hasIdOrder = orderBy.some((o) => Object.keys(o).includes('id')); + + return hasIdOrder + ? orderBy + : [...orderBy, { id: OrderByDirection.AscNullsFirst }]; +}; diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util.ts new file mode 100644 index 00000000000..1408359c2ae --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/order-by-parser-utils/parse-order-by-rest-request.util.ts @@ -0,0 +1,77 @@ +//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 { BadRequestException } from '@nestjs/common'; + +import { + type ObjectRecordOrderBy, + OrderByDirection, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/input-factories/order-by-input.factory'; +import { addDefaultOrderById } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/add-default-order-by-id.util'; +import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; + +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> = []; + 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 BadRequestException( + `'order_by' direction '${direction}' invalid. Allowed values are '${Object.values( + OrderByDirection, + ).join( + "', '", + )}'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3`, + ); + } + + 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); +}; diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/starting-after-parser-utils/__tests__/parse-starting-after-rest-request.util.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/starting-after-parser-utils/__tests__/parse-starting-after-rest-request.util.spec.ts new file mode 100644 index 00000000000..dae16e00618 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/starting-after-parser-utils/__tests__/parse-starting-after-rest-request.util.spec.ts @@ -0,0 +1,15 @@ +import { parseStartingAfterRestRequest } from 'src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util'; + +describe('parseStartingAfterRestRequest', () => { + it('should return default if starting_after missing', () => { + const request: any = { query: {} }; + + expect(parseStartingAfterRestRequest(request)).toEqual(undefined); + }); + + it('should return starting_after', () => { + const request: any = { query: { starting_after: 'uuid' } }; + + expect(parseStartingAfterRestRequest(request)).toEqual('uuid'); + }); +}); diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util.ts new file mode 100644 index 00000000000..ebd6365c027 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util.ts @@ -0,0 +1,14 @@ +import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; +import { type RequestContext } from 'src/engine/api/rest/types/RequestContext'; + +export const parseStartingAfterRestRequest = ( + request: AuthenticatedRequest | RequestContext, +): string | undefined => { + const cursorQuery = request.query?.starting_after; + + if (typeof cursorQuery !== 'string') { + return undefined; + } + + return cursorQuery; +}; diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/types/depth.type.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/types/depth.type.ts new file mode 100644 index 00000000000..4a9191ead63 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/types/depth.type.ts @@ -0,0 +1 @@ +export type Depth = 0 | 1 | 2; diff --git a/packages/twenty-server/src/engine/api/rest/metadata/query-builder/factories/get-metadata-variables.factory.ts b/packages/twenty-server/src/engine/api/rest/metadata/query-builder/factories/get-metadata-variables.factory.ts index f3ca1daedfc..a1524e8cff4 100644 --- a/packages/twenty-server/src/engine/api/rest/metadata/query-builder/factories/get-metadata-variables.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/metadata/query-builder/factories/get-metadata-variables.factory.ts @@ -1,19 +1,13 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory'; -import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory'; -import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory'; -import { type MetadataQueryVariables } from 'src/engine/api/rest/metadata/types/metadata-query-variables.type'; -import { type RequestContext } from 'src/engine/api/rest/types/RequestContext'; +import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util'; +import { parseLimitRestRequest } from 'src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util'; +import { parseStartingAfterRestRequest } from 'src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util'; +import { MetadataQueryVariables } from 'src/engine/api/rest/metadata/types/metadata-query-variables.type'; +import { RequestContext } from 'src/engine/api/rest/types/RequestContext'; @Injectable() export class GetMetadataVariablesFactory { - constructor( - private readonly startingAfterInputFactory: StartingAfterInputFactory, - private readonly endingBeforeInputFactory: EndingBeforeInputFactory, - private readonly limitInputFactory: LimitInputFactory, - ) {} - create( id: string | undefined, requestContext: RequestContext, @@ -22,9 +16,9 @@ export class GetMetadataVariablesFactory { return { id }; } - const limit = this.limitInputFactory.create(requestContext, 1000); - const before = this.endingBeforeInputFactory.create(requestContext); - const after = this.startingAfterInputFactory.create(requestContext); + const limit = parseLimitRestRequest(requestContext, 1000); + const before = parseEndingBeforeRestRequest(requestContext); + const after = parseStartingAfterRestRequest(requestContext); if (before && after) { throw new BadRequestException( diff --git a/packages/twenty-server/test/integration/graphql/suites/composite-field-pagination.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/composite-field-pagination.integration-spec.ts index 78b99e88635..a94de023953 100644 --- a/packages/twenty-server/test/integration/graphql/suites/composite-field-pagination.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/composite-field-pagination.integration-spec.ts @@ -257,7 +257,7 @@ describe('GraphQL People Pagination with Composite Field Sorting', () => { expect(backwardPagePeople).toHaveLength(2); - expect(backwardPagePeople[0].id).toBe(allPeople.at(-2)?.id); - expect(backwardPagePeople[1].id).toBe(allPeople.at(-3)?.id); + expect(backwardPagePeople[0].id).toBe(allPeople.at(-3)?.id); + expect(backwardPagePeople[1].id).toBe(allPeople.at(-2)?.id); }); });