From 0849483d2eff4c2d15131c47e4be91b2ed2b9b56 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:01:52 +0100 Subject: [PATCH] Common - Remove feature flag (#15371) To be done in an other PR, move graphql-query-runner handlers in common-api-query-runner folder (+ renaming + typing improvments) Fixes : - totalCount on findMany (Rest) - getAllSelectedFields did not handle composite field (Rest) - issue with endCursor (Rest) - args processing for updateOne and createOne (Rest + Gql) - fix findDuplicates (Gql) --- .../src/generated-metadata/graphql.ts | 1 - .../twenty-front/src/generated/graphql.ts | 1 - .../api/common/types/common-page-info.type.ts | 2 +- .../graphql-query-runner.module.ts | 33 - .../interfaces/base-resolver-service.ts | 248 ------- ...phql-query-create-many-resolver.service.ts | 542 --------------- ...aphql-query-create-one-resolver.service.ts | 51 -- ...phql-query-delete-many-resolver.service.ts | 90 --- ...aphql-query-delete-one-resolver.service.ts | 93 --- ...hql-query-destroy-many-resolver.service.ts | 85 --- ...phql-query-destroy-one-resolver.service.ts | 92 --- ...-query-find-duplicates-resolver.service.ts | 161 ----- ...raphql-query-find-many-resolver.service.ts | 229 ------- ...graphql-query-find-one-resolver.service.ts | 110 --- ...aphql-query-merge-many-resolver.service.ts | 438 ------------ ...hql-query-restore-many-resolver.service.ts | 90 --- ...phql-query-restore-one-resolver.service.ts | 92 --- ...phql-query-update-many-resolver.service.ts | 91 --- ...aphql-query-update-one-resolver.service.ts | 92 --- .../factories/create-many-resolver.factory.ts | 69 +- .../factories/create-one-resolver.factory.ts | 63 +- .../factories/delete-many-resolver.factory.ts | 69 +- .../factories/delete-one-resolver.factory.ts | 65 +- .../destroy-many-resolver.factory.ts | 72 +- .../factories/destroy-one-resolver.factory.ts | 65 +- .../find-duplicates-resolver.factory.ts | 81 +-- .../factories/find-many-resolver.factory.ts | 82 +-- .../factories/find-one-resolver.factory.ts | 60 +- .../factories/merge-many-resolver.factory.ts | 63 +- .../restore-many-resolver.factory.ts | 71 +- .../factories/restore-one-resolver.factory.ts | 65 +- .../factories/update-many-resolver.factory.ts | 69 +- .../factories/update-one-resolver.factory.ts | 65 +- .../core/handlers/rest-api-base.handler.ts | 250 +++++++ .../handlers/rest-api-create-many.handler.ts | 105 +-- .../handlers/rest-api-create-one.handler.ts | 86 +-- .../handlers/rest-api-delete-many.handler.ts | 3 +- .../handlers/rest-api-delete-one.handler.ts | 5 +- .../handlers/rest-api-destroy-many.handler.ts | 3 +- .../handlers/rest-api-destroy-one.handler.ts | 43 +- .../rest-api-find-duplicates.handler.ts | 144 +--- .../handlers/rest-api-find-many.handler.ts | 73 +- .../handlers/rest-api-find-one.handler.ts | 48 +- .../handlers/rest-api-group-by.handler.ts | 3 +- .../handlers/rest-api-merge-many.handler.ts | 3 +- .../handlers/rest-api-restore-many.handler.ts | 3 +- .../handlers/rest-api-restore-one.handler.ts | 5 +- .../handlers/rest-api-update-many.handler.ts | 3 +- .../handlers/rest-api-update-one.handler.ts | 83 +-- .../core/interfaces/rest-api-base.handler.ts | 629 ------------------ .../core-query-builder.factory.ts | 146 ---- .../core-query-builder.module.ts | 26 - .../factories/create-many-query.factory.ts | 42 -- .../factories/create-variables.factory.ts | 14 - .../core/query-builder/factories/factories.ts | 15 - .../find-duplicates-query.factory.ts | 49 -- .../find-duplicates-variables.factory.ts | 12 - .../factories/get-variables.factory.ts | 49 -- .../check-fields.utils.spec.ts.snap | 7 - .../__tests__/check-fields.utils.spec.ts | 63 -- .../__tests__/compute-depth.utils.spec.ts | 28 - .../__tests__/get-field-type.utils.spec.ts | 45 -- ...ld-metadata-to-graphql-query.utils.spec.ts | 144 ---- .../mocks/opportunity-field-maps.mock.ts | 464 ------------- .../query-builder/utils/check-fields.utils.ts | 55 -- .../utils/compute-depth.utils.ts | 25 - .../__tests__/parse-filter.utils.spec.ts | 145 ---- .../utils/filter-utils/parse-filter.utils.ts | 69 -- .../utils/get-field-type.utils.ts | 13 - ...p-field-metadata-to-graphql-query.utils.ts | 188 ------ .../parse-core-batch-path.utils.spec.ts | 11 - .../parse-core-batch-path.utils.ts | 5 - .../api/rest/core/rest-api-core.module.ts | 6 +- .../core/services/rest-api-core.service.ts | 112 +--- .../__tests__/filter-input.factory.spec.ts | 202 ------ .../__tests__/order-by-input.factory.spec.ts | 138 ---- .../ending-before-input.factory.ts | 16 - .../api/rest/input-factories/factories.ts | 4 - .../input-factories/filter-input.factory.ts | 33 - .../input-factories/order-by-input.factory.ts | 98 --- ...util.spec.ts => parse-filter.util.spec.ts} | 20 +- .../add-default-conjunction.util.ts | 2 +- .../parse-filter-rest-request.util.ts | 4 +- ...alidation.util.ts => parse-filter.util.ts} | 12 +- .../parse-order-by-rest-request.util.ts | 3 +- .../__tests__/parse-core-path.utils.spec.ts | 2 +- .../parse-core-path.utils.ts | 0 .../factories/metadata-factories.ts | 10 +- .../enums/feature-flag-key.enum.ts | 1 - .../workspace-entity-manager.spec.ts | 2 - .../core/utils/seed-feature-flags.util.ts | 5 - ...r-input-by-field-metadata-type.constant.ts | 27 +- ...t-api-core-create-many.integration-spec.ts | 4 +- ...st-api-core-create-one.integration-spec.ts | 4 +- .../rest-api-core-delete.integration-spec.ts | 6 +- ...rest-api-core-find-one.integration-spec.ts | 17 +- .../rest-api-core-update.integration-spec.ts | 6 +- 97 files changed, 655 insertions(+), 6683 deletions(-) delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts create mode 100644 packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-many-query.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-variables.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/factories/factories.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/factories/get-variables.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/__snapshots__/check-fields.utils.spec.ts.snap delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/compute-depth.utils.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-fields.utils.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/compute-depth.utils.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/__tests__/parse-core-batch-path.utils.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-batch-path.utils.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/ending-before-input.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/factories.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/filter-input.factory.ts delete mode 100644 packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts rename packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/{parse-filter-without-metdata-validation.util.spec.ts => parse-filter.util.spec.ts} (77%) rename packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/{parse-filter-without-metadata-validation.util.ts => parse-filter.util.ts} (87%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/path-parsers => input-request-parsers/path-parser-utils}/__tests__/parse-core-path.utils.spec.ts (95%) rename packages/twenty-server/src/engine/api/rest/{core/query-builder/utils/path-parsers => input-request-parsers/path-parser-utils}/parse-core-path.utils.ts (100%) diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 04c0d6de9c..8f2e721d0d 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1244,7 +1244,6 @@ export enum FeatureFlagKey { IS_AI_ENABLED = 'IS_AI_ENABLED', IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED', - IS_COMMON_API_ENABLED = 'IS_COMMON_API_ENABLED', IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED', IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED', IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED', diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index e08d1b78c6..c7d3219d66 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1208,7 +1208,6 @@ export enum FeatureFlagKey { IS_AI_ENABLED = 'IS_AI_ENABLED', IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED', - IS_COMMON_API_ENABLED = 'IS_COMMON_API_ENABLED', IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED', IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED', IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED', 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 index 82bb2395c3..440617fd10 100644 --- 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 @@ -1,4 +1,4 @@ -import { type PageInfo } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { type PageInfo } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; export type CommonPageInfo = { hasNextPage: NonNullable; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts index ed6540e41d..e4be271cc5 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts @@ -4,20 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm'; 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 { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service'; -import { GraphqlQueryCreateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service'; -import { GraphqlQueryDeleteManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service'; -import { GraphqlQueryDeleteOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service'; -import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service'; -import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service'; -import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service'; -import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service'; -import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service'; -import { GraphqlQueryMergeManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service'; -import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service'; -import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service'; -import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service'; -import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service'; 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'; @@ -28,23 +14,6 @@ import { ViewFilterGroupModule } from 'src/engine/metadata-modules/view-filter-g import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module'; import { ViewModule } from 'src/engine/metadata-modules/view/view.module'; -const graphqlQueryResolvers = [ - GraphqlQueryCreateManyResolverService, - GraphqlQueryCreateOneResolverService, - GraphqlQueryDeleteManyResolverService, - GraphqlQueryDeleteOneResolverService, - GraphqlQueryDestroyManyResolverService, - GraphqlQueryDestroyOneResolverService, - GraphqlQueryFindDuplicatesResolverService, - GraphqlQueryFindManyResolverService, - GraphqlQueryFindOneResolverService, - GraphqlQueryMergeManyResolverService, - GraphqlQueryRestoreManyResolverService, - GraphqlQueryRestoreOneResolverService, - GraphqlQueryUpdateManyResolverService, - GraphqlQueryUpdateOneResolverService, -]; - @Module({ imports: [ WorkspaceQueryHookModule, @@ -61,8 +30,6 @@ const graphqlQueryResolvers = [ ProcessNestedRelationsHelper, ProcessNestedRelationsV2Helper, ProcessAggregateHelper, - ...graphqlQueryResolvers, ], - exports: [...graphqlQueryResolvers], }) export class GraphqlQueryRunnerModule {} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts deleted file mode 100644 index 2e40695c63..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; - -import graphqlFields from 'graphql-fields'; -import { type ObjectRecord } from 'twenty-shared/types'; -import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; -import { type ObjectLiteral } from 'typeorm'; - -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 { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { - type ResolverArgs, - type WorkspaceResolverBuilderMethodNames, -} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements'; -import { type GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; -import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; -import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory'; -import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; -import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; -import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; -import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; -import { - PermissionsException, - PermissionsExceptionCode, - PermissionsExceptionMessage, -} from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; -import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; -import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; - -export type GraphqlQueryResolverExecutionArgs = { - args: Input; - options: WorkspaceQueryRunnerOptions; - workspaceDataSource: WorkspaceDataSource; - repository: WorkspaceRepository; - graphqlQueryParser: GraphqlQueryParser; - graphqlQuerySelectedFieldsResult: GraphqlQuerySelectedFieldsResult; - isExecutedByApiKey: boolean; - rolePermissionConfig?: RolePermissionConfig; -}; - -@Injectable() -export abstract class GraphqlQueryBaseResolverService< - Input extends ResolverArgs, - Response extends - | ObjectRecord - | ObjectRecord[] - | IConnection> - | IConnection>[], -> { - @Inject() - protected readonly workspaceQueryHookService: WorkspaceQueryHookService; - @Inject() - protected readonly queryRunnerArgsFactory: QueryRunnerArgsFactory; - @Inject() - protected readonly queryResultGettersFactory: QueryResultGettersFactory; - @Inject() - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager; - @Inject() - protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper; - @Inject() - protected readonly permissionsService: PermissionsService; - @Inject() - protected readonly userRoleService: UserRoleService; - @Inject() - protected readonly apiKeyRoleService: ApiKeyRoleService; - - public async execute( - args: Input, - options: WorkspaceQueryRunnerOptions, - operationName: WorkspaceResolverBuilderMethodNames, - ): Promise { - try { - const { authContext, objectMetadataItemWithFieldMaps } = options; - - const workspace = authContext.workspace; - - assertIsDefinedOrThrow(workspace); - - await this.validate(args, options); - - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: workspace.id, - }); - - const featureFlagsMap = workspaceDataSource.featureFlagMap; - - if (objectMetadataItemWithFieldMaps.isSystem === true) { - await this.validateSettingsPermissionsOnObjectOrThrow(options); - } - - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItemWithFieldMaps.nameSingular, - operationName, - args, - ); - - const computedArgs = (await this.queryRunnerArgsFactory.create( - hookedArgs, - options, - operationName, - )) as Input; - - let roleId: string | undefined; - - if (isDefined(authContext.apiKey)) { - roleId = await this.apiKeyRoleService.getRoleIdForApiKey( - authContext.apiKey.id, - workspace.id, - ); - } - - if (isDefined(authContext.userWorkspaceId)) { - roleId = await this.userRoleService.getRoleIdForUserWorkspace({ - userWorkspaceId: authContext.userWorkspaceId, - workspaceId: workspace.id, - }); - - if (!roleId) { - throw new PermissionsException( - PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE, - PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE, - ); - } - } - - if ( - !isDefined(authContext.apiKey) && - !isDefined(authContext.userWorkspaceId) - ) { - throw new PermissionsException( - PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT, - PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT, - ); - } - const rolePermissionConfig: RolePermissionConfig | undefined = roleId - ? { unionOf: [roleId] } - : undefined; - const repository = workspaceDataSource.getRepository( - objectMetadataItemWithFieldMaps.nameSingular, - rolePermissionConfig, - authContext, - ); - - const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldMaps, - options.objectMetadataMaps, - ); - - const selectedFields = graphqlFields(options.info); - - const graphqlQuerySelectedFieldsResult = - graphqlQueryParser.parseSelectedFields(selectedFields); - - const graphqlQueryResolverExecutionArgs = { - args: computedArgs, - options, - workspaceDataSource, - repository, - graphqlQueryParser, - graphqlQuerySelectedFieldsResult, - isExecutedByApiKey: isDefined(authContext.apiKey), - rolePermissionConfig, - }; - const results = await this.resolve( - graphqlQueryResolverExecutionArgs, - featureFlagsMap, - ); - - const resultWithGetters = await this.queryResultGettersFactory.create( - results, - objectMetadataItemWithFieldMaps, - workspace.id, - options.objectMetadataMaps, - ); - - await this.workspaceQueryHookService.executePostQueryHooks( - authContext, - objectMetadataItemWithFieldMaps.nameSingular, - operationName, - resultWithGetters, - ); - - return resultWithGetters; - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } - } - - private async validateSettingsPermissionsOnObjectOrThrow( - options: WorkspaceQueryRunnerOptions, - ) { - const { authContext, objectMetadataItemWithFieldMaps } = options; - - const workspace = authContext.workspace; - - assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); - - if ( - Object.keys(OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS).includes( - objectMetadataItemWithFieldMaps.nameSingular, - ) - ) { - const permissionRequired: PermissionFlagType = - // @ts-expect-error legacy noImplicitAny - OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS[ - objectMetadataItemWithFieldMaps.nameSingular - ]; - - const userHasPermission = - await this.permissionsService.userHasWorkspaceSettingPermission({ - userWorkspaceId: authContext.userWorkspaceId, - setting: permissionRequired, - workspaceId: workspace.id, - apiKeyId: authContext.apiKey?.id, - }); - - if (!userHasPermission) { - throw new PermissionsException( - PermissionsExceptionMessage.PERMISSION_DENIED, - PermissionsExceptionCode.PERMISSION_DENIED, - ); - } - } - } - - protected abstract resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - featureFlagsMap: Record, - ): Promise; - - protected abstract validate( - args: Input, - options: WorkspaceQueryRunnerOptions, - ): Promise; -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts deleted file mode 100644 index 814f199b0c..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts +++ /dev/null @@ -1,542 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { msg } from '@lingui/core/macro'; -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { type ObjectRecord } from 'twenty-shared/types'; -import { capitalize, isDefined } from 'twenty-shared/utils'; -import { - In, - type FindOperator, - type InsertResult, - type ObjectLiteral, -} from 'typeorm'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; -import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; -import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; -import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; -import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.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'; -import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; - -type PartialObjectRecordWithId = Partial & { id: string }; -@Injectable() -export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResolverService< - CreateManyResolverArgs, - ObjectRecord[] -> { - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - if (executionArgs.args.data.length > QUERY_MAX_RECORDS) { - throw new GraphqlQueryRunnerException( - `Maximum number of records to upsert is ${QUERY_MAX_RECORDS}.`, - GraphqlQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED, - { - userFriendlyMessage: msg`Maximum number of records to upsert is ${QUERY_MAX_RECORDS}.`, - }, - ); - } - - const { objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const objectRecords = await this.insertOrUpsertRecords(executionArgs); - - const upsertedRecords = await this.fetchUpsertedRecords( - executionArgs, - objectRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - ); - - await this.processNestedRelationsIfNeeded({ - executionArgs, - records: upsertedRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - return this.formatRecordsForResponse( - upsertedRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - ); - } - - private async insertOrUpsertRecords( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - if (!executionArgs.args.upsert) { - const { objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const selectedColumns = buildColumnsToReturn({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - return await executionArgs.repository.insert( - executionArgs.args.data, - undefined, - selectedColumns, - ); - } - - return this.performUpsertOperation(executionArgs); - } - - private async performUpsertOperation( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const { objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const conflictingFields = this.getConflictingFields( - objectMetadataItemWithFieldMaps, - ); - const existingRecords = await this.findExistingRecords( - executionArgs, - conflictingFields, - ); - - const { recordsToUpdate, recordsToInsert } = this.categorizeRecords( - executionArgs.args.data, - conflictingFields, - existingRecords, - ); - - const result: InsertResult = { - identifiers: [], - generatedMaps: [], - raw: [], - }; - - const columnsToReturn = buildColumnsToReturn({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - if (recordsToUpdate.length > 0) { - await this.processRecordsToUpdate({ - partialRecordsToUpdate: recordsToUpdate, - repository: executionArgs.repository, - objectMetadataItemWithFieldMaps, - result, - columnsToReturn, - }); - } - - await this.processRecordsToInsert({ - recordsToInsert, - repository: executionArgs.repository, - result, - columnsToReturn, - }); - - return result; - } - - //TODO : Improve conflicting fields logic - unicity can be define on combination of fields - should be based on unique index not on field metadata - //TODO : https://github.com/twentyhq/core-team-issues/issues/1115 - private getConflictingFields( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - ): { - baseField: string; - fullPath: string; - column: string; - }[] { - return Object.values(objectMetadataItemWithFieldMaps.fieldsById) - .filter((field) => field.isUnique || field.name === 'id') - .flatMap((field) => { - const compositeType = compositeTypeDefinitions.get(field.type); - - if (!compositeType) { - return [ - { - baseField: field.name, - fullPath: field.name, - column: field.name, - }, - ]; - } - - const property = compositeType.properties.find( - (prop) => prop.isIncludedInUniqueConstraint, - ); - - return property - ? [ - { - baseField: field.name, - fullPath: `${field.name}.${property.name}`, - column: `${field.name}${capitalize(property.name)}`, - }, - ] - : []; - }); - } - - private async findExistingRecords( - executionArgs: GraphqlQueryResolverExecutionArgs, - conflictingFields: { - baseField: string; - fullPath: string; - column: string; - }[], - ): Promise { - const { objectMetadataItemWithFieldMaps } = executionArgs.options; - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - const whereConditions = this.buildWhereConditions( - executionArgs.args.data, - conflictingFields, - ); - - whereConditions.forEach((condition) => { - queryBuilder.orWhere(condition); - }); - - const restrictedFields = - executionArgs.repository.objectRecordsPermissions?.[ - objectMetadataItemWithFieldMaps.id - ]?.restrictedFields; - - const selectOptions = getAllSelectableColumnNames({ - restrictedFields: restrictedFields ?? {}, - objectMetadata: { - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - }, - }); - - return (await queryBuilder - .withDeleted() - .setFindOptions({ - select: selectOptions, - }) - .getMany()) as PartialObjectRecordWithId[]; - } - - private getValueFromPath( - record: Partial, - path: string, - ): unknown { - const pathParts = path.split('.'); - - if (pathParts.length === 1) { - return record[path]; - } - - const [parentField, childField] = pathParts; - - return record[parentField]?.[childField]; - } - - private buildWhereConditions( - records: Partial[], - conflictingFields: { - baseField: string; - fullPath: string; - column: string; - }[], - ): Record>[] { - const whereConditions = []; - - for (const field of conflictingFields) { - const fieldValues = records - .map((record) => this.getValueFromPath(record, field.fullPath)) - .filter(Boolean); - - //TODO : Adapt to composite constraint - https://github.com/twentyhq/core-team-issues/issues/1115 - if (fieldValues.length > 0) { - whereConditions.push({ [field.column]: In(fieldValues) }); - } - } - - return whereConditions; - } - - private categorizeRecords( - records: Partial[], - conflictingFields: { - baseField: string; - fullPath: string; - column: string; - }[], - existingRecords: PartialObjectRecordWithId[], - ): { - recordsToUpdate: PartialObjectRecordWithId[]; - recordsToInsert: Partial[]; - } { - const recordsToUpdate: PartialObjectRecordWithId[] = []; - const recordsToInsert: Partial[] = []; - - for (const record of records) { - const matchingRecordId = this.getMatchingRecordId( - record, - conflictingFields, - existingRecords, - ); - - if (isDefined(matchingRecordId)) { - recordsToUpdate.push({ ...record, id: matchingRecordId }); - } else { - recordsToInsert.push(record); - } - } - - return { recordsToUpdate, recordsToInsert }; - } - - private getMatchingRecordId( - record: Partial, - conflictingFields: { - baseField: string; - fullPath: string; - column: string; - }[], - existingRecords: PartialObjectRecordWithId[], - ): string | undefined { - const matchingRecordIds = conflictingFields.reduce( - (acc, field) => { - const requestFieldValue = this.getValueFromPath(record, field.fullPath); - - const matchingRecord = existingRecords.find((existingRecord) => { - const existingFieldValue = this.getValueFromPath( - existingRecord, - field.fullPath, - ); - - return ( - isDefined(existingFieldValue) && - existingFieldValue === requestFieldValue - ); - }); - - if (isDefined(matchingRecord)) { - acc.push(matchingRecord.id); - } - - return acc; - }, - [], - ); - - if ([...new Set(matchingRecordIds)].length > 1) { - const conflictingFieldsValues = conflictingFields - .map((field) => { - const value = this.getValueFromPath(record, field.fullPath); - - return isDefined(value) ? `${field.fullPath}: ${value}` : undefined; - }) - .filter(isDefined) - .join(', '); - - throw new GraphqlQueryRunnerException( - `Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`, - GraphqlQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT, - { - userFriendlyMessage: msg`Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`, - }, - ); - } - - return matchingRecordIds[0]; - } - - private async processRecordsToUpdate({ - partialRecordsToUpdate, - repository, - objectMetadataItemWithFieldMaps, - result, - columnsToReturn, - }: { - partialRecordsToUpdate: PartialObjectRecordWithId[]; - repository: WorkspaceRepository; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - result: InsertResult; - columnsToReturn: string[]; - }): Promise { - const partialRecordsToUpdateWithoutCreatedByUpdate = - partialRecordsToUpdate.map((record) => - this.getRecordWithoutCreatedBy(record, objectMetadataItemWithFieldMaps), - ); - - const savedRecords = await repository.updateMany( - partialRecordsToUpdateWithoutCreatedByUpdate.map((record) => ({ - criteria: record.id, - partialEntity: { ...record, deletedAt: null }, - })), - undefined, - columnsToReturn, - ); - - result.identifiers.push( - ...savedRecords.generatedMaps.map((record) => ({ id: record.id })), - ); - result.generatedMaps.push( - ...savedRecords.generatedMaps.map((record) => ({ id: record.id })), - ); - } - - private async processRecordsToInsert({ - recordsToInsert, - repository, - result, - columnsToReturn, - }: { - recordsToInsert: Partial[]; - repository: WorkspaceRepository; - result: InsertResult; - columnsToReturn: string[]; - }): Promise { - if (recordsToInsert.length > 0) { - const insertResult = await repository.insert( - recordsToInsert, - undefined, - columnsToReturn, - ); - - result.identifiers.push(...insertResult.identifiers); - result.generatedMaps.push(...insertResult.generatedMaps); - result.raw.push(...insertResult.raw); - } - } - - private async fetchUpsertedRecords( - executionArgs: GraphqlQueryResolverExecutionArgs, - objectRecords: InsertResult, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, - ): Promise { - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - const columnsToSelect = buildColumnsToSelect({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - const upsertedRecords = await queryBuilder - .setFindOptions({ - select: columnsToSelect, - }) - .where({ - id: In(objectRecords.generatedMaps.map((record) => record.id)), - }) - .withDeleted() - .take(QUERY_MAX_RECORDS) - .getMany(); - - return upsertedRecords as ObjectRecord[]; - } - - private async processNestedRelationsIfNeeded({ - executionArgs, - records, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }: { - executionArgs: GraphqlQueryResolverExecutionArgs; - records: ObjectRecord[]; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; - }): Promise { - if (!executionArgs.graphqlQuerySelectedFieldsResult.relations) { - return; - } - - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: records, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - limit: QUERY_MAX_RECORDS, - authContext: executionArgs.options.authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - - private formatRecordsForResponse( - upsertedRecords: ObjectRecord[], - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, - ): ObjectRecord[] { - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return upsertedRecords.map((record: ObjectRecord) => - typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }), - ); - } - - private getRecordWithoutCreatedBy( - record: PartialObjectRecordWithId, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - ): Omit { - let recordWithoutCreatedByUpdate = record; - - const createdByFieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName['createdBy']; - const createdByFieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[createdByFieldMetadataId]; - - if (!isDefined(createdByFieldMetadata)) { - throw new GraphqlQueryRunnerException( - `Missing createdBy field metadata for object ${objectMetadataItemWithFieldMaps.nameSingular}`, - GraphqlQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD, - ); - } - - if ('createdBy' in record && createdByFieldMetadata.isCustom === false) { - const { createdBy: _createdBy, ...recordWithoutCreatedBy } = record; - - recordWithoutCreatedByUpdate = recordWithoutCreatedBy; - } - - return recordWithoutCreatedByUpdate; - } - - async validate( - args: CreateManyResolverArgs>, - options: WorkspaceQueryRunnerOptions, - ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); - - args.data.forEach((record) => { - if (record?.id) { - assertIsValidUuid(record.id); - } - }); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts deleted file mode 100644 index 361a6c7ded..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type ObjectRecord } from 'twenty-shared/types'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service'; -import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; - -@Injectable() -export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolverService< - CreateOneResolverArgs, - ObjectRecord -> { - constructor( - private readonly createManyResolverService: GraphqlQueryCreateManyResolverService, - ) { - super(); - } - - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const result = await this.createManyResolverService.resolve({ - ...executionArgs, - args: { - ...executionArgs.args, - data: [executionArgs.args.data], - }, - }); - - return result[0]; - } - - async validate( - args: CreateOneResolverArgs>, - options: WorkspaceQueryRunnerOptions, - ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); - - if (args.data?.id) { - assertIsValidUuid(args.data.id); - } - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts deleted file mode 100644 index 8f76c2e286..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { type ObjectRecord } from 'twenty-shared/types'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; -import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; - -@Injectable() -export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResolverService< - DeleteManyResolverArgs, - ObjectRecord[] -> { - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - executionArgs.graphqlQueryParser.applyFilterToBuilder( - queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, - executionArgs.args.filter, - ); - - const columnsToReturn = buildColumnsToReturn({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - const deletedObjectRecords = await queryBuilder - .softDelete() - .returning(columnsToReturn) - .execute(); - - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: - deletedObjectRecords.generatedMaps as ObjectRecord[], - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return deletedObjectRecords.generatedMaps.map((record: ObjectRecord) => - typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }), - ); - } - - async validate( - args: DeleteManyResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); - if (!args.filter) { - throw new Error('Filter is required'); - } - - args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id)); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts deleted file mode 100644 index 1ae7925e2e..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { type ObjectRecord } from 'twenty-shared/types'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; -import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; - -@Injectable() -export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolverService< - DeleteOneResolverArgs, - ObjectRecord -> { - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - const columnsToReturn = buildColumnsToReturn({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - const deletedObjectRecords = await queryBuilder - .softDelete() - .where({ id: executionArgs.args.id }) - .returning(columnsToReturn) - .execute(); - - if (deletedObjectRecords.generatedMaps.length === 0) { - throw new GraphqlQueryRunnerException( - 'Record not found', - GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, - ); - } - - const deletedRecord = deletedObjectRecords.generatedMaps[0] as ObjectRecord; - - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: [deletedRecord], - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - const result = typeORMObjectRecordsParser.processRecord({ - objectRecord: deletedRecord, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - - return result; - } - - async validate( - args: DeleteOneResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); - assertIsValidUuid(args.id); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts deleted file mode 100644 index 043791d74f..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { type ObjectRecord } from 'twenty-shared/types'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; - -@Injectable() -export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseResolverService< - DestroyManyResolverArgs, - ObjectRecord[] -> { - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - executionArgs.graphqlQueryParser.applyFilterToBuilder( - queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, - executionArgs.args.filter, - ); - - const columnsToReturn = buildColumnsToReturn({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - const deletedObjectRecords = await queryBuilder - .delete() - .returning(columnsToReturn) - .execute(); - - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: - deletedObjectRecords.generatedMaps as ObjectRecord[], - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return deletedObjectRecords.generatedMaps.map((record: ObjectRecord) => - typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }), - ); - } - - async validate( - args: DestroyManyResolverArgs, - _options: WorkspaceQueryRunnerOptions, - ): Promise { - if (!args.filter) { - throw new Error('Filter is required'); - } - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts deleted file mode 100644 index c25fd7f548..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { type ObjectRecord } from 'twenty-shared/types'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type DestroyOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; - -@Injectable() -export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResolverService< - DestroyOneResolverArgs, - ObjectRecord -> { - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - const columnsToReturn = buildColumnsToReturn({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - const deletedObjectRecords = await queryBuilder - .delete() - .where({ id: executionArgs.args.id }) - .returning(columnsToReturn) - .execute(); - - if (!deletedObjectRecords.affected) { - throw new GraphqlQueryRunnerException( - 'Record not found', - GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, - ); - } - - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: - deletedObjectRecords.generatedMaps as ObjectRecord[], - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: deletedObjectRecords.generatedMaps[0] as ObjectRecord, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } - - async validate( - args: DestroyOneResolverArgs, - _options: WorkspaceQueryRunnerOptions, - ): Promise { - if (!args.id) { - throw new GraphqlQueryRunnerException( - 'Missing id', - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts deleted file mode 100644 index a3ceef4155..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import isEmpty from 'lodash.isempty'; -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { OrderByDirection, type ObjectRecord } from 'twenty-shared/types'; -import { In } from 'typeorm'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type FindDuplicatesResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -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 { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; - -@Injectable() -export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService< - FindDuplicatesResolverArgs, - IConnection[] -> { - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise[]> { - const { objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const existingRecordsQueryBuilder = - executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - const objectMetadataItemWithFieldsMaps = - getObjectMetadataMapItemByNameSingular( - objectMetadataMaps, - objectMetadataItemWithFieldMaps.nameSingular, - ); - - if (!objectMetadataItemWithFieldsMaps) { - throw new GraphqlQueryRunnerException( - `Object ${objectMetadataItemWithFieldMaps.nameSingular} not found`, - GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - ); - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - let objectRecords: Partial[] = []; - - const columnsToSelect = buildColumnsToSelect({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - if (executionArgs.args.ids) { - objectRecords = (await existingRecordsQueryBuilder - .where({ id: In(executionArgs.args.ids) }) - .setFindOptions({ - select: columnsToSelect, - }) - .getMany()) as ObjectRecord[]; - } else if (executionArgs.args.data && !isEmpty(executionArgs.args.data)) { - objectRecords = executionArgs.args.data; - } - - const duplicateConnections: IConnection[] = await Promise.all( - objectRecords.map(async (record) => { - const duplicateConditions = buildDuplicateConditions( - objectMetadataItemWithFieldMaps, - [record], - record.id, - ); - - if (isEmpty(duplicateConditions)) { - return typeORMObjectRecordsParser.createConnection({ - objectRecords: [], - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 0, - totalCount: 0, - order: [{ id: OrderByDirection.AscNullsFirst }], - hasNextPage: false, - hasPreviousPage: false, - }); - } - - const duplicateRecordsQueryBuilder = - executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - graphqlQueryParser.applyFilterToBuilder( - duplicateRecordsQueryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, - duplicateConditions, - ); - - const duplicates = (await duplicateRecordsQueryBuilder - .setFindOptions({ - select: columnsToSelect, - }) - .take(QUERY_MAX_RECORDS) - .getMany()) as ObjectRecord[]; - - return typeORMObjectRecordsParser.createConnection({ - objectRecords: duplicates, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: duplicates.length, - totalCount: duplicates.length, - order: [{ id: OrderByDirection.AscNullsFirst }], - hasNextPage: false, - hasPreviousPage: false, - }); - }), - ); - - return duplicateConnections; - } - - async validate( - args: FindDuplicatesResolverArgs, - _options: WorkspaceQueryRunnerOptions, - ): Promise { - if (!args.data && !args.ids) { - throw new GraphqlQueryRunnerException( - 'You have to provide either "data" or "ids" argument', - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - - if (args.data && args.ids) { - throw new GraphqlQueryRunnerException( - 'You cannot provide both "data" and "ids" arguments', - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - - if (!args.ids && isEmpty(args.data)) { - throw new GraphqlQueryRunnerException( - 'The "data" condition can not be empty when "ids" input not provided', - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - } -} 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 deleted file mode 100644 index 91b26d433f..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { ObjectRecord, OrderByDirection } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { - type ObjectRecordFilter, - type ObjectRecordOrderBy, -} 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 WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -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, - getPaginationInfo, -} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; -import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; - -@Injectable() -export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolverService< - FindManyResolverArgs, - IConnection -> { - constructor() { - super(); - } - - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise> { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const objectMetadataNameSingular = - objectMetadataItemWithFieldMaps.nameSingular; - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataNameSingular, - ); - - const aggregateQueryBuilder = queryBuilder.clone(); - - let appliedFilters = - executionArgs.args.filter ?? ({} as ObjectRecordFilter); - - executionArgs.graphqlQueryParser.applyFilterToBuilder( - aggregateQueryBuilder, - objectMetadataNameSingular, - appliedFilters, - ); - - executionArgs.graphqlQueryParser.applyDeletedAtToBuilder( - aggregateQueryBuilder, - appliedFilters, - ); - - const orderByWithIdCondition = [ - ...(executionArgs.args.orderBy ?? []), - { id: OrderByDirection.AscNullsFirst }, - ] as ObjectRecordOrderBy; - - const isForwardPagination = !isDefined(executionArgs.args.before); - - const cursor = getCursor(executionArgs.args); - - if (cursor) { - const cursorArgFilter = computeCursorArgFilter( - cursor, - orderByWithIdCondition, - objectMetadataItemWithFieldMaps, - isForwardPagination, - ); - - appliedFilters = (executionArgs.args.filter - ? { - and: [executionArgs.args.filter, { or: cursorArgFilter }], - } - : { or: cursorArgFilter }) as unknown as ObjectRecordFilter; - } - - executionArgs.graphqlQueryParser.applyFilterToBuilder( - queryBuilder, - objectMetadataNameSingular, - appliedFilters, - ); - - executionArgs.graphqlQueryParser.applyOrderToBuilder( - queryBuilder, - orderByWithIdCondition, - objectMetadataNameSingular, - isForwardPagination, - ); - - executionArgs.graphqlQueryParser.applyDeletedAtToBuilder( - queryBuilder, - appliedFilters, - ); - - ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({ - selectedAggregatedFields: - executionArgs.graphqlQuerySelectedFieldsResult.aggregate, - queryBuilder: aggregateQueryBuilder, - objectMetadataNameSingular, - }); - - const limit = - executionArgs.args.first ?? executionArgs.args.last ?? QUERY_MAX_RECORDS; - - const columnsToSelect = buildColumnsToSelect({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - if (isDefined(executionArgs.args.offset)) { - queryBuilder.skip(executionArgs.args.offset); - } - - const objectRecords = (await queryBuilder - .setFindOptions({ - select: columnsToSelect, - }) - .take(limit + 1) - .getMany()) as ObjectRecord[]; - - const { hasNextPage, hasPreviousPage } = getPaginationInfo( - objectRecords, - limit, - isForwardPagination, - ); - - if (objectRecords.length > limit) { - objectRecords.pop(); - } - - const parentObjectRecordsAggregatedValues = - await aggregateQueryBuilder.getRawOne(); - - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: objectRecords, - parentObjectRecordsAggregatedValues, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - aggregate: executionArgs.graphqlQuerySelectedFieldsResult.aggregate, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return typeORMObjectRecordsParser.createConnection({ - objectRecords: isForwardPagination - ? objectRecords - : objectRecords.reverse(), - objectRecordsAggregatedValues: parentObjectRecordsAggregatedValues, - selectedAggregatedFields: - executionArgs.graphqlQuerySelectedFieldsResult.aggregate, - objectName: objectMetadataNameSingular, - take: limit, - totalCount: parentObjectRecordsAggregatedValues?.totalCount, - order: orderByWithIdCondition, - hasNextPage, - hasPreviousPage, - }); - } - - async validate( - args: FindManyResolverArgs, - _options: WorkspaceQueryRunnerOptions, - ): Promise { - if (args.first && args.last) { - throw new GraphqlQueryRunnerException( - 'Cannot provide both first and last', - GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT, - ); - } - if (args.before && args.after) { - throw new GraphqlQueryRunnerException( - 'Cannot provide both before and after', - GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT, - ); - } - if (args.before && args.first) { - throw new GraphqlQueryRunnerException( - 'Cannot provide both before and first', - GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT, - ); - } - if (args.after && args.last) { - throw new GraphqlQueryRunnerException( - 'Cannot provide both after and last', - GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT, - ); - } - if (args.first !== undefined && args.first < 0) { - throw new GraphqlQueryRunnerException( - 'First argument must be non-negative', - GraphqlQueryRunnerExceptionCode.INVALID_ARGS_FIRST, - ); - } - if (args.last !== undefined && args.last < 0) { - throw new GraphqlQueryRunnerException( - 'Last argument must be non-negative', - GraphqlQueryRunnerExceptionCode.INVALID_ARGS_LAST, - ); - } - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts deleted file mode 100644 index 296ecf27e4..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts +++ /dev/null @@ -1,110 +0,0 @@ -//TODO : Refacto-common - To delete -import { Injectable } from '@nestjs/common'; - -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { ObjectRecord } from 'twenty-shared/types'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -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 { - WorkspaceQueryRunnerException, - WorkspaceQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; - -@Injectable() -export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolverService< - FindOneResolverArgs, - ObjectRecord -> { - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - executionArgs.graphqlQueryParser.applyFilterToBuilder( - queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, - executionArgs.args.filter ?? ({} as ObjectRecordFilter), - ); - - executionArgs.graphqlQueryParser.applyDeletedAtToBuilder( - queryBuilder, - executionArgs.args.filter ?? ({} as ObjectRecordFilter), - ); - - const columnsToSelect = buildColumnsToSelect({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - const objectRecord = await queryBuilder - .setFindOptions({ - select: columnsToSelect, - }) - .getOne(); - - if (!objectRecord) { - throw new GraphqlQueryRunnerException( - 'Record not found', - GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, - ); - } - - const objectRecords = [objectRecord] as ObjectRecord[]; - - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: objectRecords, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: objectRecords[0], - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }) as ObjectRecord; - } - - async validate( - args: FindOneResolverArgs, - _options: WorkspaceQueryRunnerOptions, - ): Promise { - if (!args.filter || Object.keys(args.filter).length === 0) { - throw new WorkspaceQueryRunnerException( - 'Missing filter argument', - WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts deleted file mode 100644 index 37a9e95a9c..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts +++ /dev/null @@ -1,438 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { - MUTATION_MAX_MERGE_RECORDS, - QUERY_MAX_RECORDS, -} from 'twenty-shared/constants'; -import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { In } from 'typeorm'; -import { v4 as uuidv4 } from 'uuid'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type MergeManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { type FieldMetadataRelationSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; -import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util'; -import { mergeFieldValues } from 'src/engine/api/graphql/graphql-query-runner/utils/merge-field-values.util'; -import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.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'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; - -@Injectable() -export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolverService< - MergeManyResolverArgs, - ObjectRecord -> { - private readonly logger = new Logger( - GraphqlQueryMergeManyResolverService.name, - ); - - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - const { ids, conflictPriorityIndex, dryRun } = executionArgs.args; - - const recordsToMerge = await this.fetchRecordsToMerge(executionArgs, ids); - - const priorityRecord = this.validateAndGetPriorityRecord( - recordsToMerge, - ids, - conflictPriorityIndex, - ); - - const mergedData = this.performDeepMerge( - recordsToMerge, - priorityRecord.id, - objectMetadataItemWithFieldMaps, - ); - - if (dryRun) { - return this.createDryRunResponse( - priorityRecord, - mergedData, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - ); - } - - const idsToDelete = ids.filter((id) => id !== priorityRecord.id); - - await this.migrateRelatedRecords( - executionArgs, - idsToDelete, - priorityRecord.id, - ); - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - await queryBuilder - .softDelete() - .whereInIds(idsToDelete) - .returning('*') - .execute(); - - const updatedRecord = await this.updatePriorityRecord( - executionArgs, - priorityRecord.id, - mergedData, - ); - - if (executionArgs.rolePermissionConfig) { - await this.processNestedRelations({ - executionArgs, - updatedRecords: [updatedRecord], - authContext, - }); - } - - return this.formatResponse( - updatedRecord, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - ); - } - - private async fetchRecordsToMerge( - executionArgs: GraphqlQueryResolverExecutionArgs, - ids: string[], - ): Promise { - const recordsToMerge = await executionArgs.repository.find({ - where: { id: In(ids) }, - }); - - if (recordsToMerge.length !== ids.length) { - throw new GraphqlQueryRunnerException( - 'One or more records not found', - GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, - ); - } - - return recordsToMerge as ObjectRecord[]; - } - - private validateAndGetPriorityRecord( - recordsToMerge: ObjectRecord[], - ids: string[], - conflictPriorityIndex: number, - ): ObjectRecord { - const priorityRecordId = ids[conflictPriorityIndex]; - const priorityRecord = recordsToMerge.find( - (record) => record.id === priorityRecordId, - ); - - if (!priorityRecord) { - throw new GraphqlQueryRunnerException( - 'Priority record not found', - GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, - ); - } - - return priorityRecord; - } - - private performDeepMerge( - recordsToMerge: ObjectRecord[], - priorityRecordId: string, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - ): Partial { - const mergedResult: Partial = {}; - - const allFieldNames = new Set(); - - recordsToMerge.forEach((record) => { - Object.keys(record).forEach((fieldName) => { - if ( - !this.shouldExcludeFieldFromMerge( - fieldName, - objectMetadataItemWithFieldMaps, - ) - ) { - allFieldNames.add(fieldName); - } - }); - }); - - allFieldNames.forEach((fieldName) => { - const recordsWithValues: { value: unknown; recordId: string }[] = []; - - recordsToMerge.forEach((record) => { - const fieldValue = record[fieldName]; - - if (hasRecordFieldValue(fieldValue)) { - recordsWithValues.push({ value: fieldValue, recordId: record.id }); - } - }); - - if (recordsWithValues.length === 0) { - return; - } else if (recordsWithValues.length === 1) { - mergedResult[fieldName] = recordsWithValues[0].value; - } else { - const fieldMetadata = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, - ).find((field) => field?.name === fieldName); - - if (!fieldMetadata) { - return; - } - - mergedResult[fieldName] = mergeFieldValues( - fieldMetadata.type, - recordsWithValues, - priorityRecordId, - ); - } - }); - - return mergedResult; - } - - private shouldExcludeFieldFromMerge( - fieldName: string, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - ): boolean { - const fieldMetadata = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, - ).find((field) => field?.name === fieldName); - - return fieldMetadata?.isSystem ?? false; - } - - private createDryRunResponse( - priorityRecord: ObjectRecord, - mergedData: Partial, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, - ): ObjectRecord { - const dryRunRecord = { - ...priorityRecord, - ...mergedData, - id: uuidv4(), - deletedAt: new Date().toISOString(), - } as ObjectRecord; - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: dryRunRecord, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } - - private async updatePriorityRecord( - executionArgs: GraphqlQueryResolverExecutionArgs, - priorityRecordId: string, - mergedData: Partial, - ): Promise { - const { objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - const columnsToReturn = buildColumnsToReturn({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - const updatedObjectRecords = await queryBuilder - .update() - .set(mergedData) - .where({ id: priorityRecordId }) - .returning(columnsToReturn) - .execute(); - - if (!updatedObjectRecords.generatedMaps.length) { - throw new GraphqlQueryRunnerException( - 'Failed to update record', - GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, - ); - } - - const updatedRecord = updatedObjectRecords.generatedMaps[0] as ObjectRecord; - - return updatedRecord; - } - - private async migrateRelatedRecords( - executionArgs: GraphqlQueryResolverExecutionArgs, - fromIds: string[], - toId: string, - ): Promise { - const { objectMetadataMaps, objectMetadataItemWithFieldMaps } = - executionArgs.options; - - const relationFieldsPointingToCurrentObject = Object.values( - objectMetadataMaps.byId, - ) - .filter(isDefined) - .flatMap((metadata) => { - const relationFields = Object.values(metadata.fieldsById) - .filter(isDefined) - .filter((field) => - isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION), - ) - .filter( - (field) => - field.relationTargetObjectMetadataId === - objectMetadataItemWithFieldMaps.id && field.isActive, - ); - - return relationFields - .filter((field) => { - const relationSettings = - field.settings as FieldMetadataRelationSettings; - - return ( - relationSettings?.relationType === RelationType.MANY_TO_ONE && - relationSettings?.joinColumnName - ); - }) - .map((field) => { - const relationSettings = - field.settings as FieldMetadataRelationSettings; - - return { - objectMetadata: metadata, - fieldName: field.name, - fieldId: field.id, - joinColumnName: relationSettings.joinColumnName, - }; - }); - }); - - for (const relationField of relationFieldsPointingToCurrentObject) { - if (!relationField.joinColumnName) { - continue; - } - - try { - const repository = executionArgs.workspaceDataSource.getRepository( - relationField.objectMetadata.nameSingular, - executionArgs.rolePermissionConfig, - ); - - const whereCondition = { [relationField.joinColumnName]: In(fromIds) }; - - const existingRecords = await repository.find({ - where: whereCondition, - }); - - if (existingRecords.length > 0) { - await repository.update(whereCondition, { - [relationField.joinColumnName]: toId, - }); - } - } catch (error) { - this.logger.warn( - `Failed to migrate relation field "${relationField.fieldName}" (${relationField.joinColumnName}) in object "${relationField.objectMetadata.nameSingular}":`, - error.message, - ); - } - } - } - - private async processNestedRelations({ - executionArgs, - updatedRecords, - authContext, - }: { - executionArgs: GraphqlQueryResolverExecutionArgs; - updatedRecords: ObjectRecord[]; - authContext: AuthContext; - }): Promise { - const { objectMetadataMaps, objectMetadataItemWithFieldMaps } = - executionArgs.options; - - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: updatedRecords, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - } - - private formatResponse( - updatedRecord: ObjectRecord, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, - ): ObjectRecord { - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: updatedRecord, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } - - async validate( - args: MergeManyResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); - - if (!isDefined(options.objectMetadataItemWithFieldMaps.duplicateCriteria)) { - throw new GraphqlQueryRunnerException( - `Merge is only available for objects with duplicate criteria. Object '${options.objectMetadataItemWithFieldMaps.nameSingular}' does not have duplicate criteria defined.`, - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - - const { ids, conflictPriorityIndex } = args; - - if (!ids || ids.length < 2) { - throw new GraphqlQueryRunnerException( - 'At least 2 record IDs are required for merge', - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - - if (ids.length > MUTATION_MAX_MERGE_RECORDS) { - throw new GraphqlQueryRunnerException( - `Maximum ${MUTATION_MAX_MERGE_RECORDS} records can be merged at once`, - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - - if (conflictPriorityIndex < 0 || conflictPriorityIndex >= ids.length) { - throw new GraphqlQueryRunnerException( - `Invalid conflict priority '${conflictPriorityIndex}'. Valid options for ${ids.length} records: 0-${ids.length - 1}`, - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts deleted file mode 100644 index 38c88ee284..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { type ObjectRecord } from 'twenty-shared/types'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type RestoreManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; -import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; - -@Injectable() -export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseResolverService< - RestoreManyResolverArgs, - ObjectRecord[] -> { - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - executionArgs.graphqlQueryParser.applyFilterToBuilder( - queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, - executionArgs.args.filter, - ); - - const columnsToReturn = buildColumnsToReturn({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - const restoredObjectRecords = await queryBuilder - .restore() - .returning(columnsToReturn) - .execute(); - - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: - restoredObjectRecords.generatedMaps as ObjectRecord[], - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return restoredObjectRecords.generatedMaps.map((record: ObjectRecord) => - typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }), - ); - } - - async validate( - args: RestoreManyResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); - if (!args.filter) { - throw new Error('Filter is required'); - } - - args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id)); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts deleted file mode 100644 index abeb78b26d..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { type ObjectRecord } from 'twenty-shared/types'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type RestoreOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; -import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; - -@Injectable() -export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResolverService< - RestoreOneResolverArgs, - ObjectRecord -> { - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - const columnsToReturn = buildColumnsToReturn({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - const restoredObjectRecords = await queryBuilder - .restore() - .where({ id: executionArgs.args.id }) - .returning(columnsToReturn) - .execute(); - - if (restoredObjectRecords.generatedMaps.length === 0) { - throw new GraphqlQueryRunnerException( - 'Record not found', - GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, - ); - } - - const restoredRecord = restoredObjectRecords - .generatedMaps[0] as ObjectRecord; - - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: [restoredRecord], - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: restoredRecord, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } - - async validate( - args: RestoreOneResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); - assertIsValidUuid(args.id); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts deleted file mode 100644 index 111eeb3f7b..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { type ObjectRecord } from 'twenty-shared/types'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; -import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; - -@Injectable() -export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResolverService< - UpdateManyResolverArgs, - ObjectRecord[] -> { - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - executionArgs.graphqlQueryParser.applyFilterToBuilder( - queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, - executionArgs.args.filter, - ); - - const columnsToReturn = buildColumnsToReturn({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - const updatedObjectRecords = await queryBuilder - .update() - .set(executionArgs.args.data) - .returning(columnsToReturn) - .execute(); - - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: - updatedObjectRecords.generatedMaps as ObjectRecord[], - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return updatedObjectRecords.generatedMaps.map((record: ObjectRecord) => - typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }), - ); - } - - async validate( - args: UpdateManyResolverArgs>, - options: WorkspaceQueryRunnerOptions, - ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); - if (!args.filter) { - throw new Error('Filter is required'); - } - - args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id)); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts deleted file mode 100644 index 911966ea63..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; -import { type ObjectRecord } from 'twenty-shared/types'; - -import { - GraphqlQueryBaseResolverService, - type GraphqlQueryResolverExecutionArgs, -} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; -import { type UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; -import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; - -@Injectable() -export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolverService< - UpdateOneResolverArgs, - ObjectRecord -> { - async resolve( - executionArgs: GraphqlQueryResolverExecutionArgs, - ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - executionArgs.options; - - const queryBuilder = executionArgs.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, - ); - - const columnsToReturn = buildColumnsToReturn({ - select: executionArgs.graphqlQuerySelectedFieldsResult.select, - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - - const updatedObjectRecords = await queryBuilder - .update() - .set(executionArgs.args.data) - .where({ id: executionArgs.args.id }) - .returning(columnsToReturn) - .execute(); - - const updatedRecord = updatedObjectRecords.generatedMaps[0] as ObjectRecord; - - if (!updatedRecord) { - throw new GraphqlQueryRunnerException( - 'Record not found', - GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, - ); - } - - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: [updatedRecord], - relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource: executionArgs.workspaceDataSource, - rolePermissionConfig: executionArgs.rolePermissionConfig, - selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select, - }); - } - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: updatedRecord, - objectName: objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } - - async validate( - args: UpdateOneResolverArgs>, - options: WorkspaceQueryRunnerOptions, - ): Promise { - assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps); - assertIsValidUuid(args.id); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts index 0e7fb8fa2b..d02a8d8d09 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; import { ObjectRecord } from 'twenty-shared/types'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type CreateManyResolverArgs, @@ -13,11 +12,8 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class CreateManyResolverFactory @@ -26,9 +22,7 @@ export class CreateManyResolverFactory public static methodName = RESOLVER_METHOD_NAMES.CREATE_MANY; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryCreateManyResolverService, private readonly commonCreateManyQueryRunnerService: CommonCreateManyQueryRunnerService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -37,54 +31,31 @@ export class CreateManyResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: internalContext.authContext.workspace?.id as string, - }); + const selectedFields = graphqlFields(info); - const featureFlagsMap = workspaceDataSource.featureFlagMap; + try { + const records = await this.commonCreateManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { - const selectedFields = graphqlFields(info); - - try { - const records = await this.commonCreateManyQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return records.map((record: ObjectRecord) => - typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }), - ); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + return records.map((record: ObjectRecord) => + typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }), + ); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphqlQueryRunnerService.execute( - args, - options, - CreateManyResolverFactory.methodName, - ); }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts index 08951c3d23..ecee4e26ab 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type CreateOneResolverArgs, @@ -12,11 +11,8 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryCreateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-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'; -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 CreateOneResolverFactory @@ -25,8 +21,6 @@ export class CreateOneResolverFactory public static methodName = RESOLVER_METHOD_NAMES.CREATE_ONE; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryCreateOneResolverService, - private readonly featureFlagService: FeatureFlagService, private readonly commonCreateOneQueryRunnerService: CommonCreateOneQueryRunnerService, ) {} @@ -36,50 +30,29 @@ export class CreateOneResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_COMMON_API_ENABLED, - internalContext.authContext.workspace?.id as string, - ); + const selectedFields = graphqlFields(info); - if (isCommonApiEnabled) { - const selectedFields = graphqlFields(info); + try { + const record = await this.commonCreateOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - try { - const record = await this.commonCreateOneQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphqlQueryRunnerService.execute( - args, - options, - CreateOneResolverFactory.methodName, - ); }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts index 3673ecd955..febdd06de2 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; import { ObjectRecord } from 'twenty-shared/types'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type DeleteManyResolverArgs, @@ -13,11 +12,8 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryDeleteManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class DeleteManyResolverFactory @@ -26,9 +22,7 @@ export class DeleteManyResolverFactory public static methodName = RESOLVER_METHOD_NAMES.DELETE_MANY; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryDeleteManyResolverService, private readonly commonDeleteManyQueryRunnerService: CommonDeleteManyQueryRunnerService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -37,54 +31,31 @@ export class DeleteManyResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: internalContext.authContext.workspace?.id as string, - }); + const selectedFields = graphqlFields(info); - const featureFlagsMap = workspaceDataSource.featureFlagMap; + try { + const records = await this.commonDeleteManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { - const selectedFields = graphqlFields(info); - - try { - const records = await this.commonDeleteManyQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return records.map((record: ObjectRecord) => - typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }), - ); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + return records.map((record: ObjectRecord) => + typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }), + ); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphqlQueryRunnerService.execute( - args, - options, - DeleteManyResolverFactory.methodName, - ); }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts index ae6287f1c5..6d82982847 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type DeleteOneResolverArgs, @@ -12,11 +11,8 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works import { CommonDeleteOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-one-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryDeleteOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-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'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class DeleteOneResolverFactory @@ -25,9 +21,7 @@ export class DeleteOneResolverFactory public static methodName = RESOLVER_METHOD_NAMES.DELETE_ONE; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryDeleteOneResolverService, private readonly commonDeleteOneQueryRunnerService: CommonDeleteOneQueryRunnerService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -36,52 +30,29 @@ export class DeleteOneResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: internalContext.authContext.workspace?.id as string, - }); + const selectedFields = graphqlFields(info); - const featureFlagsMap = workspaceDataSource.featureFlagMap; + try { + const record = await this.commonDeleteOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { - const selectedFields = graphqlFields(info); - - try { - const record = await this.commonDeleteOneQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphqlQueryRunnerService.execute( - args, - options, - DeleteOneResolverFactory.methodName, - ); }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts index 2d07795f5f..1a48865f82 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; import { ObjectRecord } from 'twenty-shared/types'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type DestroyManyResolverArgs, @@ -13,11 +12,8 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class DestroyManyResolverFactory @@ -26,9 +22,7 @@ export class DestroyManyResolverFactory public static methodName = RESOLVER_METHOD_NAMES.DESTROY_MANY; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryDestroyManyResolverService, private readonly commonDestroyManyQueryRunnerService: CommonDestroyManyQueryRunnerService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -37,55 +31,31 @@ export class DestroyManyResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: internalContext.authContext.workspace?.id as string, - }); + const selectedFields = graphqlFields(info); - const featureFlagsMap = workspaceDataSource.featureFlagMap; + try { + const records = await this.commonDestroyManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { - const selectedFields = graphqlFields(info); - - try { - const records = - await this.commonDestroyManyQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, - ); - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return records.map((record: ObjectRecord) => - typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }), + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + + return records.map((record: ObjectRecord) => + typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }), + ); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphqlQueryRunnerService.execute( - args, - options, - DestroyManyResolverFactory.methodName, - ); }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts index 9aa5f6e913..86a33573cc 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type DestroyOneResolverArgs, @@ -12,11 +11,8 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works import { CommonDestroyOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-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'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class DestroyOneResolverFactory @@ -25,9 +21,7 @@ export class DestroyOneResolverFactory public static methodName = RESOLVER_METHOD_NAMES.DESTROY_ONE; constructor( - private readonly graphQLQueryRunnerService: GraphqlQueryDestroyOneResolverService, private readonly commonDestroyOneQueryRunnerService: CommonDestroyOneQueryRunnerService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -36,52 +30,29 @@ export class DestroyOneResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: internalContext.authContext.workspace?.id as string, - }); + const selectedFields = graphqlFields(info); - const featureFlagsMap = workspaceDataSource.featureFlagMap; + try { + const record = await this.commonDestroyOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { - const selectedFields = graphqlFields(info); - - try { - const record = await this.commonDestroyOneQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphQLQueryRunnerService.execute( - args, - options, - DestroyOneResolverFactory.methodName, - ); }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts index e73ad82269..b964ae4ba4 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; import { OrderByDirection } from 'twenty-shared/types'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type FindDuplicatesResolverArgs, @@ -13,11 +12,8 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works import { CommonFindDuplicatesQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class FindDuplicatesResolverFactory @@ -26,9 +22,7 @@ export class FindDuplicatesResolverFactory public static methodName = RESOLVER_METHOD_NAMES.FIND_DUPLICATES; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryFindDuplicatesResolverService, private readonly commonFindDuplicatesQueryRunnerService: CommonFindDuplicatesQueryRunnerService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -37,58 +31,35 @@ export class FindDuplicatesResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: internalContext.authContext.workspace?.id as string, - }); + const selectedFields = graphqlFields(info); - const featureFlagsMap = workspaceDataSource.featureFlagMap; - - if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { - const selectedFields = graphqlFields(info); - - try { - const paginatedDuplicates = - await this.commonFindDuplicatesQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, - ); - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return paginatedDuplicates.map((duplicate) => - typeORMObjectRecordsParser.createConnection({ - objectRecords: duplicate.records, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: duplicate.records.length, - totalCount: duplicate.totalCount, - order: [{ id: OrderByDirection.AscNullsFirst }], - hasNextPage: duplicate.hasNextPage, - hasPreviousPage: duplicate.hasPreviousPage, - }), + try { + const paginatedDuplicates = + await this.commonFindDuplicatesQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, ); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, + ); + + return paginatedDuplicates.map((duplicate) => + typeORMObjectRecordsParser.createConnection({ + objectRecords: duplicate.records, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: duplicate.records.length, + totalCount: duplicate.totalCount, + order: [{ id: OrderByDirection.AscNullsFirst }], + hasNextPage: duplicate.hasNextPage, + hasPreviousPage: duplicate.hasPreviousPage, + }), + ); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphqlQueryRunnerService.execute( - args, - options, - FindDuplicatesResolverFactory.methodName, - ); }; } } 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 9bec7c395e..3c71ed3efb 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 @@ -12,11 +12,8 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace- import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service'; 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 @@ -26,8 +23,6 @@ export class FindManyResolverFactory constructor( private readonly commonFindManyQueryRunnerService: CommonFindManyQueryRunnerService, - private readonly graphqlQueryRunnerService: GraphqlQueryFindManyResolverService, - private readonly featureFlagService: FeatureFlagService, ) {} create( @@ -36,59 +31,40 @@ export class FindManyResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_COMMON_API_ENABLED, - internalContext.authContext.workspace?.id as string, - ); + const selectedFields = graphqlFields(info); - if (isCommonApiEnabled) { - const selectedFields = graphqlFields(info); + try { + const { + records, + aggregatedValues, + totalCount, + pageInfo, + selectedFieldsResult, + } = await this.commonFindManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - try { - const { - records, - aggregatedValues, - totalCount, - pageInfo, - selectedFieldsResult, - } = await this.commonFindManyQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - 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) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + 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) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - return await this.graphqlQueryRunnerService.execute( - args, - { - 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 5ec044697a..6e82e7389e 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,11 +11,8 @@ 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 { 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'; -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 FindOneResolverFactory @@ -25,8 +22,6 @@ export class FindOneResolverFactory constructor( private readonly commonFindOneQueryRunnerService: CommonFindOneQueryRunnerService, - private readonly featureFlagService: FeatureFlagService, - private readonly graphqlQueryRunnerService: GraphqlQueryFindOneResolverService, ) {} create( @@ -35,48 +30,29 @@ export class FindOneResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_COMMON_API_ENABLED, - internalContext.authContext.workspace?.id as string, - ); + try { + const selectedFields = graphqlFields(info); - if (isCommonApiEnabled) { - try { - const selectedFields = graphqlFields(info); + const record = await this.commonFindOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - const record = await this.commonFindOneQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - return await this.graphqlQueryRunnerService.execute( - args, - { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }, - FindOneResolverFactory.methodName, - ); }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts index b9f11a3edf..dd2adec181 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type MergeManyResolverArgs, @@ -12,11 +11,8 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace- import { CommonMergeManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-merge-many-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryMergeManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-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 MergeManyResolverFactory @@ -25,9 +21,7 @@ export class MergeManyResolverFactory public static methodName = RESOLVER_METHOD_NAMES.MERGE_MANY; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryMergeManyResolverService, private readonly commonMergeManyQueryRunnerService: CommonMergeManyQueryRunnerService, - private readonly featureFlagService: FeatureFlagService, ) {} create( @@ -36,50 +30,29 @@ export class MergeManyResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_COMMON_API_ENABLED, - internalContext.authContext.workspace?.id as string, - ); + const selectedFields = graphqlFields(info); - if (isCommonApiEnabled) { - const selectedFields = graphqlFields(info); + try { + const record = await this.commonMergeManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - try { - const record = await this.commonMergeManyQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphqlQueryRunnerService.execute( - args, - options, - MergeManyResolverFactory.methodName, - ); }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts index a4a2fa379b..6c5b732674 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; import { ObjectRecord } from 'twenty-shared/types'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type Resolver, @@ -13,11 +12,8 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works import { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class RestoreManyResolverFactory @@ -26,8 +22,6 @@ export class RestoreManyResolverFactory public static methodName = RESOLVER_METHOD_NAMES.RESTORE_MANY; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryRestoreManyResolverService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly commonRestoreManyQueryRunnerService: CommonRestoreManyQueryRunnerService, ) {} @@ -37,54 +31,31 @@ export class RestoreManyResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: internalContext.authContext.workspace?.id as string, - }); + const selectedFields = graphqlFields(info); - const featureFlagsMap = workspaceDataSource.featureFlagMap; + try { + const records = await this.commonRestoreManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { - const selectedFields = graphqlFields(info); - - try { - const records = - await this.commonRestoreManyQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, - ); - - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return records.map((record: ObjectRecord) => - typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }), + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } - } - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - return await this.graphqlQueryRunnerService.execute( - args, - options, - RestoreManyResolverFactory.methodName, - ); + return records.map((record: ObjectRecord) => + typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }), + ); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts index 384e525f6a..ef59ff6d51 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type Resolver, @@ -12,11 +11,8 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works import { CommonRestoreOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-one-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-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'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class RestoreOneResolverFactory @@ -25,8 +21,6 @@ export class RestoreOneResolverFactory public static methodName = RESOLVER_METHOD_NAMES.RESTORE_ONE; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryRestoreOneResolverService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly commonRestoreOneQueryRunnerService: CommonRestoreOneQueryRunnerService, ) {} @@ -36,52 +30,29 @@ export class RestoreOneResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: internalContext.authContext.workspace?.id as string, - }); + const selectedFields = graphqlFields(info); - const featureFlagsMap = workspaceDataSource.featureFlagMap; + try { + const record = await this.commonRestoreOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { - const selectedFields = graphqlFields(info); - - try { - const record = await this.commonRestoreOneQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphqlQueryRunnerService.execute( - args, - options, - RestoreOneResolverFactory.methodName, - ); }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts index c28f5cbff5..8a0c477244 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; import { ObjectRecord } from 'twenty-shared/types'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type Resolver, @@ -13,11 +12,8 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works import { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-many-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class UpdateManyResolverFactory @@ -26,9 +22,7 @@ export class UpdateManyResolverFactory public static methodName = RESOLVER_METHOD_NAMES.UPDATE_MANY; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryUpdateManyResolverService, private readonly commonUpdateManyQueryRunnerService: CommonUpdateManyQueryRunnerService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -37,54 +31,31 @@ export class UpdateManyResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: internalContext.authContext.workspace?.id as string, - }); + const selectedFields = graphqlFields(info); - const featureFlagsMap = workspaceDataSource.featureFlagMap; + try { + const records = await this.commonUpdateManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { - const selectedFields = graphqlFields(info); - - try { - const records = await this.commonUpdateManyQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return records.map((record: ObjectRecord) => - typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }), - ); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + return records.map((record: ObjectRecord) => + typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }), + ); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphqlQueryRunnerService.execute( - args, - options, - UpdateManyResolverFactory.methodName, - ); }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts index cf74028db3..fd75c558ec 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type Resolver, @@ -12,11 +11,8 @@ import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/works import { CommonUpdateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-one-query-runner.service'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-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'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class UpdateOneResolverFactory @@ -25,9 +21,7 @@ export class UpdateOneResolverFactory public static methodName = RESOLVER_METHOD_NAMES.UPDATE_ONE; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryUpdateOneResolverService, private readonly commonUpdateOneQueryRunnerService: CommonUpdateOneQueryRunnerService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -36,52 +30,29 @@ export class UpdateOneResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: internalContext.authContext.workspace?.id as string, - }); + const selectedFields = graphqlFields(info); - const featureFlagsMap = workspaceDataSource.featureFlagMap; + try { + const record = await this.commonUpdateOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); - if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { - const selectedFields = graphqlFields(info); - - try { - const record = await this.commonUpdateOneQueryRunnerService.execute( - { ...args, selectedFields }, - internalContext, + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, ); - const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, - ); - - return typeORMObjectRecordsParser.processRecord({ - objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, - take: 1, - totalCount: 1, - }); - } catch (error) { - workspaceQueryRunnerGraphqlApiExceptionHandler(error); - } + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } - - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphqlQueryRunnerService.execute( - args, - options, - UpdateOneResolverFactory.methodName, - ); }; } } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts new file mode 100644 index 0000000000..55538a9f46 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts @@ -0,0 +1,250 @@ +import { BadRequestException, Inject } from '@nestjs/common'; + +import { SettingsPath } from 'twenty-shared/types'; +import { + assertIsDefinedOrThrow, + getSettingsPath, + isDefined, +} from 'twenty-shared/utils'; + +import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; + +import { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type'; +import { CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type'; +import { RestToCommonSelectedFieldsHandler } from 'src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler'; +import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils'; +import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type'; +import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; +import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service'; +import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service'; +import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; +import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; +import { + PermissionsException, + PermissionsExceptionCode, + PermissionsExceptionMessage, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; +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'; +import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; +import { getObjectMetadataMapItemByNamePlural } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util'; +import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; +import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; +import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; + +export interface PageInfo { + hasNextPage?: boolean; + hasPreviousPage?: boolean; + startCursor: string | null; + endCursor: string | null; +} + +export interface FormatResult { + data?: { + [operation: string]: object; + }; + pageInfo?: PageInfo; + totalCount?: number; +} + +export abstract class RestApiBaseHandler { + @Inject() + protected readonly recordInputTransformerService: RecordInputTransformerService; + @Inject() + protected readonly twentyORMManager: TwentyORMManager; + @Inject() + protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService; + @Inject() + protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService; + @Inject() + protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService; + @Inject() + protected readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService; + @Inject() + protected readonly apiKeyRoleService: ApiKeyRoleService; + @Inject() + protected readonly restToCommonSelectedFieldsHandler: RestToCommonSelectedFieldsHandler; + @Inject() + protected readonly userRoleService: UserRoleService; + @Inject() + protected readonly accessTokenService: AccessTokenService; + @Inject() + protected readonly workspaceDomainsService: WorkspaceDomainsService; + @Inject() + protected readonly featureFlagService: FeatureFlagService; + + protected abstract handle( + request: AuthenticatedRequest, + ): Promise< + FormatResult | { data: FormatResult[] } | CommonGroupByOutputItem[] + >; + + public getAuthContextFromRequest( + request: AuthenticatedRequest, + ): WorkspaceAuthContext { + return request; + } + + private getObjectsPermissions = async (authContext: WorkspaceAuthContext) => { + let roleId: string; + + if (isDefined(authContext.apiKey)) { + roleId = await this.apiKeyRoleService.getRoleIdForApiKey( + authContext.apiKey.id, + authContext.workspace.id, + ); + } else { + const userWorkspaceRoleId = + await this.userRoleService.getRoleIdForUserWorkspace({ + userWorkspaceId: authContext.userWorkspaceId, + workspaceId: authContext.workspace.id, + }); + + if (!isDefined(userWorkspaceRoleId)) { + throw new PermissionsException( + PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE, + PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE, + ); + } + + roleId = userWorkspaceRoleId; + } + + const objectMetadataPermissions = + await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles( + { + workspaceId: authContext.workspace.id, + roleIds: [roleId], + }, + ); + + return { objectsPermissions: objectMetadataPermissions[roleId] }; + }; + + async computeSelectedFields({ + authContext, + depth, + objectMetadataMapItem, + objectMetadataMaps, + }: { + authContext: WorkspaceAuthContext; + depth?: Depth | undefined; + objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + objectMetadataMaps: ObjectMetadataMaps; + }): Promise { + const { objectsPermissions } = + await this.getObjectsPermissions(authContext); + + return this.restToCommonSelectedFieldsHandler.computeFromDepth({ + objectsPermissions, + objectMetadataMaps, + objectMetadataMapItem, + depth, + }); + } + + async buildCommonOptions(request: AuthenticatedRequest) { + const { object: parsedObject } = parseCorePath(request); + + const { objectMetadataMaps, objectMetadataMapItem } = + await this.getObjectMetadata(request, parsedObject); + + const authContext = this.getAuthContextFromRequest(request); + + return { + authContext, + objectMetadataItemWithFieldMaps: objectMetadataMapItem, + objectMetadataMaps: objectMetadataMaps, + }; + } + + private async getObjectMetadata( + request: AuthenticatedRequest, + parsedObject: string, + ): Promise<{ + objectMetadataMaps: ObjectMetadataMaps; + objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + }> { + const { workspace } = + await this.accessTokenService.validateTokenByRequest(request); + + assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); + + const currentCacheVersion = + await this.workspaceCacheStorageService.getMetadataVersion(workspace.id); + + if (currentCacheVersion === undefined) { + await this.workspaceMetadataCacheService.recomputeMetadataCache({ + workspaceId: workspace.id, + }); + + throw new BadRequestException('Metadata cache version not found'); + } + const objectMetadataMaps = + await this.workspaceCacheStorageService.getObjectMetadataMaps( + workspace.id, + currentCacheVersion, + ); + + if (!objectMetadataMaps) { + throw new BadRequestException( + `No object was found for the workspace associated with this API key. You may generate a new one here ${this.workspaceDomainsService + .buildWorkspaceURL({ + workspace, + pathname: getSettingsPath(SettingsPath.ApiWebhooks), + }) + .toString()}`, + ); + } + + const objectMetadataItem = getObjectMetadataMapItemByNamePlural( + objectMetadataMaps, + parsedObject, + ); + + if (!objectMetadataItem) { + const wrongObjectMetadataItem = getObjectMetadataMapItemByNameSingular( + objectMetadataMaps, + parsedObject, + ); + + let hint = 'eg: companies'; + + if (wrongObjectMetadataItem) { + hint = `Did you mean '${wrongObjectMetadataItem.namePlural}'?`; + } + + throw new BadRequestException( + `object '${parsedObject}' not found. ${hint}`, + ); + } + + const workspaceFeatureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspace.id); + + // Check if this entity is workspace-gated and should be blocked from workspace API + if ( + shouldExcludeFromWorkspaceApi( + objectMetadataItem, + standardObjectMetadataDefinitions, + workspaceFeatureFlagsMap, + ) + ) { + throw new BadRequestException( + `object '${parsedObject}' not found. ${parsedObject} is not available via REST API.`, + ); + } + + return { + objectMetadataMaps, + objectMetadataMapItem: objectMetadataItem, + }; + } +} 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 1da509ba5f..0cb0b8a0ec 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 @@ -1,21 +1,14 @@ -import { - BadRequestException, - Injectable, - InternalServerErrorException, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; -import isEmpty from 'lodash.isempty'; import { type ObjectRecord } from 'twenty-shared/types'; -import { capitalize, isDefined } from 'twenty-shared/utils'; - -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { capitalize } from 'twenty-shared/utils'; import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { parseUpsertRestRequest } from 'src/engine/api/rest/input-request-parsers/upsert-parser-utils/parse-upsert-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'; -import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; @Injectable() export class RestApiCreateManyHandler extends RestApiBaseHandler { constructor( @@ -24,7 +17,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { super(); } - async commonHandle(request: AuthenticatedRequest) { + async handle(request: AuthenticatedRequest) { try { const { data, depth, upsert } = this.parseRequestArgs(request); @@ -55,7 +48,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { objectMetadataItemWithFieldMaps.namePlural, ); } catch (error) { - workspaceQueryRunnerRestApiExceptionHandler(error); + return workspaceQueryRunnerRestApiExceptionHandler(error); } } @@ -73,92 +66,4 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { upsert: parseUpsertRestRequest(request), }; } - - async handle(request: AuthenticatedRequest) { - const { objectMetadata, repository, restrictedFields } = - await this.getRepositoryAndMetadataOrFail(request); - - const body = request.body; - - if (!Array.isArray(body)) { - throw new BadRequestException('Body must be an array'); - } - - if (body.length === 0) { - throw new BadRequestException('Input must not be empty'); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const overriddenRecordsToCreate: Record[] = []; - - for (const recordToCreate of body) { - const overriddenBody = await this.recordInputTransformerService.process({ - recordInput: recordToCreate, - objectMetadataMapItem: objectMetadata.objectMetadataMapItem, - }); - - const recordExists = - isDefined(overriddenBody.id) && - (await repository.exists({ - where: { - id: overriddenBody.id, - }, - })); - - if (recordExists) { - throw new BadRequestException('Record already exists'); - } - - overriddenRecordsToCreate.push(overriddenBody); - } - - const recordsToCreate = - await this.createdByFromAuthContextService.injectCreatedBy( - overriddenRecordsToCreate, - objectMetadata.objectMetadataMapItem.nameSingular, - this.getAuthContextFromRequest(request), - ); - - let selectedColumns = undefined; - - if (!isEmpty(restrictedFields)) { - const selectableFields = getAllSelectableColumnNames({ - restrictedFields, - objectMetadata, - }); - - selectedColumns = Object.keys(selectableFields).filter( - (key) => selectableFields[key], - ); - } - - const createdRecords = await repository.insert( - recordsToCreate, - undefined, - selectedColumns, - ); - const createdRecordsIds = createdRecords.identifiers.map( - (record) => record.id, - ); - - const records = await this.getRecord({ - recordIds: createdRecordsIds, - repository, - objectMetadata, - depth: parseDepthRestRequest(request), - restrictedFields, - }); - - if (records.length !== body.length) { - throw new InternalServerErrorException( - `Error when creating records. ${body.length - records.length} records are missing after creation.`, - ); - } - - return this.formatResult({ - operation: 'create', - objectNamePlural: objectMetadata.objectMetadataMapItem.namePlural, - data: records, - }); - } } 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 5c61125f65..5694046a47 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 @@ -1,21 +1,14 @@ -import { - BadRequestException, - Injectable, - InternalServerErrorException, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; -import isEmpty from 'lodash.isempty'; import { ObjectRecord } from 'twenty-shared/types'; -import { capitalize, isDefined } from 'twenty-shared/utils'; - -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { capitalize } from 'twenty-shared/utils'; import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { parseUpsertRestRequest } from 'src/engine/api/rest/input-request-parsers/upsert-parser-utils/parse-upsert-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'; -import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; @Injectable() export class RestApiCreateOneHandler extends RestApiBaseHandler { @@ -25,7 +18,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { super(); } - async commonHandle(request: AuthenticatedRequest) { + async handle(request: AuthenticatedRequest) { try { const { data, depth, upsert } = this.parseRequestArgs(request); @@ -56,7 +49,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { objectMetadataItemWithFieldMaps.nameSingular, ); } catch (error) { - workspaceQueryRunnerRestApiExceptionHandler(error); + return workspaceQueryRunnerRestApiExceptionHandler(error); } } @@ -71,73 +64,4 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { upsert: parseUpsertRestRequest(request), }; } - - async handle(request: AuthenticatedRequest) { - const { objectMetadata, repository, restrictedFields } = - await this.getRepositoryAndMetadataOrFail(request); - - const overriddenBody = await this.recordInputTransformerService.process({ - recordInput: request.body, - objectMetadataMapItem: objectMetadata.objectMetadataMapItem, - }); - - const recordExists = - isDefined(overriddenBody.id) && - (await repository.exists({ - where: { - id: overriddenBody.id, - }, - })); - - if (recordExists) { - throw new BadRequestException('Record already exists'); - } - - const [recordToCreate] = - await this.createdByFromAuthContextService.injectCreatedBy( - [overriddenBody], - objectMetadata.objectMetadataMapItem.nameSingular, - this.getAuthContextFromRequest(request), - ); - - let selectedColumns = undefined; - - if (!isEmpty(restrictedFields)) { - const selectableFields = getAllSelectableColumnNames({ - restrictedFields, - objectMetadata, - }); - - selectedColumns = Object.keys(selectableFields).filter( - (key) => selectableFields[key], - ); - } - - const createdRecordResult = await repository.insert( - recordToCreate, - undefined, - selectedColumns, - ); - const createdRecord = createdRecordResult.identifiers[0]; - - const records = await this.getRecord({ - recordIds: [createdRecord.id], - repository, - objectMetadata, - depth: parseDepthRestRequest(request), - restrictedFields, - }); - - const record = records[0]; - - if (!isDefined(record)) { - throw new InternalServerErrorException('Created record not found'); - } - - return this.formatResult({ - operation: 'create', - objectNameSingular: objectMetadata.objectMetadataMapItem.nameSingular, - data: record, - }); - } } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts index 03df516d3f..d14b3d9ef8 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts @@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; - +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service'; import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts index 1a552beb53..93e95b030c 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts @@ -3,10 +3,9 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize, isDefined } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; - import { CommonDeleteOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-one-query-runner.service'; -import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; +import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils'; 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'; diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts index 0ca338d315..709d2b940e 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts @@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; - +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service'; import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts index 8651df70ce..b5feb4be1c 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts @@ -3,13 +3,11 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize, isDefined } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; - import { CommonDestroyOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service'; -import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; +import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils'; 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'; -import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; @Injectable() export class RestApiDestroyOneHandler extends RestApiBaseHandler { @@ -19,7 +17,7 @@ export class RestApiDestroyOneHandler extends RestApiBaseHandler { super(); } - async commonHandle(request: AuthenticatedRequest) { + async handle(request: AuthenticatedRequest) { try { const { id } = this.parseRequestArgs(request); @@ -43,7 +41,7 @@ export class RestApiDestroyOneHandler extends RestApiBaseHandler { objectMetadataItemWithFieldMaps.nameSingular, ); } catch (error) { - workspaceQueryRunnerRestApiExceptionHandler(error); + return workspaceQueryRunnerRestApiExceptionHandler(error); } } @@ -62,37 +60,4 @@ export class RestApiDestroyOneHandler extends RestApiBaseHandler { id, }; } - - async handle(request: AuthenticatedRequest) { - const { id: recordId } = parseCorePath(request); - - if (!recordId) { - throw new BadRequestException('Record ID not found'); - } - - const { objectMetadata, repository, restrictedFields } = - await this.getRepositoryAndMetadataOrFail(request); - - const selectOptions = getAllSelectableColumnNames({ - restrictedFields, - objectMetadata, - }); - - const recordToDelete = await repository.findOneOrFail({ - where: { id: recordId }, - select: selectOptions, - }); - - const columnsToReturnForDelete: string[] = []; - - await repository.delete(recordId, undefined, columnsToReturnForDelete); - - return this.formatResult({ - operation: 'delete', - objectNameSingular: objectMetadata.objectMetadataMapItem.nameSingular, - data: { - id: recordToDelete.id, - }, - }); - } } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts index 76ea2c252e..66b0125baa 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts @@ -1,20 +1,12 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; -import { type Request } from 'express'; -import isEmpty from 'lodash.isempty'; import { type ObjectRecord } from 'twenty-shared/types'; -import { In } from 'typeorm'; - -import { - type FormatResult, - RestApiBaseHandler, -} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; import { CommonFindDuplicatesQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/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 { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; -import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; @Injectable() export class RestApiFindDuplicatesHandler extends RestApiBaseHandler { @@ -24,10 +16,8 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler { super(); } - async commonHandle(request: AuthenticatedRequest) { + async handle(request: AuthenticatedRequest) { try { - this.validate(request); - const { data, ids, depth } = this.parseRequestArgs(request); const { @@ -58,7 +48,7 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler { objectMetadataItemWithFieldMaps.nameSingular, ); } catch (error) { - workspaceQueryRunnerRestApiExceptionHandler(error); + return workspaceQueryRunnerRestApiExceptionHandler(error); } } @@ -94,130 +84,4 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler { depth: parseDepthRestRequest(request), }; } - - async handle(request: AuthenticatedRequest) { - this.validate(request); - - const { - repository, - objectMetadata, - objectMetadataItemWithFieldsMaps, - restrictedFields, - } = await this.getRepositoryAndMetadataOrFail(request); - - const existingRecordsQueryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldsMaps.nameSingular, - ); - - let objectRecords: Partial[] = []; - - if (request.body.ids) { - objectRecords = (await existingRecordsQueryBuilder - .where({ id: In(request.body.ids) }) - .getMany()) as ObjectRecord[]; - } else if (request.body.data && !isEmpty(request.body.data)) { - objectRecords = request.body.data; - } - - const duplicateConditions = objectRecords.map((record) => - buildDuplicateConditions( - objectMetadataItemWithFieldsMaps, - [record], - record.id, - ), - ); - - const result: { data: FormatResult[] } = { - data: [], - }; - - for (const duplicateCondition of duplicateConditions) { - const { - records, - isForwardPagination, - hasMoreRecords, - totalCount, - startCursor, - endCursor, - } = await this.findRecords({ - request, - repository, - objectMetadata, - objectMetadataItemWithFieldsMaps, - extraFilters: duplicateCondition, - restrictedFields, - }); - - const paginatedResult = this.formatPaginatedDuplicatesResult({ - finalRecords: records, - objectMetadataNameSingular: - objectMetadata.objectMetadataMapItem.nameSingular, - isForwardPagination, - hasMoreRecords, - totalCount, - startCursor, - endCursor, - }); - - result.data.push(paginatedResult); - } - - return result; - } - - private validate(request: Request) { - const { data, ids } = request.body; - - if (!data && !ids) { - throw new BadRequestException( - 'You have to provide either "data" or "ids" argument', - ); - } - - if (data && ids) { - throw new BadRequestException( - 'You cannot provide both "data" and "ids" arguments', - ); - } - - if (!ids && isEmpty(data)) { - throw new BadRequestException( - 'The "data" condition can not be empty when "ids" input not provided', - ); - } - } - - formatPaginatedDuplicatesResult({ - finalRecords, - objectMetadataNameSingular, - isForwardPagination, - hasMoreRecords, - totalCount, - startCursor, - endCursor, - }: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - finalRecords: any[]; - objectMetadataNameSingular: string; - isForwardPagination: boolean; - hasMoreRecords: boolean; - totalCount: number; - startCursor: string | null; - endCursor: string | null; - }) { - const hasPreviousPage = !isForwardPagination && hasMoreRecords; - - return this.formatResult({ - operation: 'findDuplicates', - objectNameSingular: objectMetadataNameSingular, - data: isForwardPagination ? finalRecords : finalRecords.reverse(), - pageInfo: { - hasNextPage: isForwardPagination && hasMoreRecords, - ...(hasPreviousPage ? { hasPreviousPage } : {}), - startCursor, - endCursor, - }, - totalCount, - }); - } } 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 489958bfde..7ffda7a581 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 @@ -5,8 +5,7 @@ import { ObjectRecord } from 'twenty-shared/types'; import { PageInfo, RestApiBaseHandler, -} from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; - +} from 'src/engine/api/rest/core/handlers/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'; @@ -26,74 +25,6 @@ export class RestApiFindManyHandler extends RestApiBaseHandler { } async handle(request: AuthenticatedRequest) { - const { - repository, - objectMetadata, - objectMetadataItemWithFieldsMaps, - restrictedFields, - } = await this.getRepositoryAndMetadataOrFail(request); - - const { - records, - isForwardPagination, - hasMoreRecords, - totalCount, - startCursor, - endCursor, - } = await this.findRecords({ - request, - repository, - objectMetadata, - objectMetadataItemWithFieldsMaps, - restrictedFields, - }); - - return this.formatPaginatedResult({ - finalRecords: records, - objectMetadataNamePlural: objectMetadata.objectMetadataMapItem.namePlural, - isForwardPagination, - hasMoreRecords, - totalCount, - startCursor, - endCursor, - }); - } - - formatPaginatedResult({ - finalRecords, - objectMetadataNamePlural, - isForwardPagination, - hasMoreRecords, - totalCount, - startCursor, - endCursor, - }: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - finalRecords: any[]; - objectMetadataNamePlural: string; - isForwardPagination: boolean; - hasMoreRecords: boolean; - totalCount: number; - startCursor: string | null; - endCursor: string | null; - }) { - const hasPreviousPage = !isForwardPagination && hasMoreRecords; - - return this.formatResult({ - operation: 'findMany', - objectNamePlural: objectMetadataNamePlural, - data: isForwardPagination ? finalRecords : finalRecords.reverse(), - pageInfo: { - hasNextPage: isForwardPagination && hasMoreRecords, - ...(hasPreviousPage ? { hasPreviousPage } : {}), - startCursor, - endCursor, - }, - totalCount, - }); - } - - async commonHandle(request: AuthenticatedRequest) { try { const parsedArgs = this.parseRequestArgs(request); const { @@ -129,7 +60,7 @@ export class RestApiFindManyHandler extends RestApiBaseHandler { pageInfo, ); } catch (error) { - workspaceQueryRunnerRestApiExceptionHandler(error); + return workspaceQueryRunnerRestApiExceptionHandler(error); } } 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 b68140a20a..a018ed6660 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 @@ -1,13 +1,11 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; 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 { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; +import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils'; 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'; @@ -20,44 +18,6 @@ export class RestApiFindOneHandler extends RestApiBaseHandler { } async handle(request: AuthenticatedRequest) { - const { id: recordId } = parseCorePath(request); - - if (!isDefined(recordId)) { - throw new BadRequestException( - 'No recordId provided in rest api get one query', - ); - } - - const { - repository, - objectMetadata, - objectMetadataItemWithFieldsMaps, - restrictedFields, - } = await this.getRepositoryAndMetadataOrFail(request); - - const { records } = await this.findRecords({ - request, - recordId, - repository, - objectMetadata, - objectMetadataItemWithFieldsMaps, - restrictedFields, - }); - - const record = records?.[0]; - - if (!isDefined(record)) { - throw new BadRequestException('Record not found'); - } - - return this.formatResult({ - operation: 'findOne', - objectNameSingular: objectMetadata.objectMetadataMapItem.nameSingular, - data: record, - }); - } - - async commonHandle(request: AuthenticatedRequest) { try { const { filter, depth } = await this.parseRequestArgs(request); const { @@ -87,7 +47,7 @@ export class RestApiFindOneHandler extends RestApiBaseHandler { objectMetadataItemWithFieldMaps.nameSingular, ); } catch (error) { - workspaceQueryRunnerRestApiExceptionHandler(error); + return workspaceQueryRunnerRestApiExceptionHandler(error); } } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts index 6ecb88997d..377db2581e 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; - +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service'; import { parseAggregateFieldsRestRequest } from 'src/engine/api/rest/input-request-parsers/aggregate-fields-parser-utils/parse-aggregate-fields-rest-request.util'; import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts index 7129e48e31..f624570ce9 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts @@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; - +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonMergeManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-merge-many-query-runner.service'; 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'; diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts index 53e23a7aa5..1151988149 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts @@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; - +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts index 104f5a9646..b248987e93 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts @@ -3,11 +3,10 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize, isDefined } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; - import { CommonRestoreOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-one-query-runner.service'; -import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; +import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils'; 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'; diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts index beeef56867..2746f10a55 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts @@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; - +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-many-query-runner.service'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; 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 dd3904e113..b881ba8098 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 @@ -1,21 +1,14 @@ -import { - BadRequestException, - Injectable, - InternalServerErrorException, -} from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; -import isEmpty from 'lodash.isempty'; import { ObjectRecord } from 'twenty-shared/types'; -import { capitalize, isDefined } from 'twenty-shared/utils'; - -import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { capitalize } from 'twenty-shared/utils'; import { CommonUpdateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-one-query-runner.service'; -import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; +import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils'; 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'; -import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; @Injectable() export class RestApiUpdateOneHandler extends RestApiBaseHandler { @@ -25,7 +18,7 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler { super(); } - async commonHandle(request: AuthenticatedRequest) { + async handle(request: AuthenticatedRequest) { try { const { id, data, depth } = this.parseRequestArgs(request); @@ -56,7 +49,7 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler { objectMetadataItemWithFieldMaps.nameSingular, ); } catch (error) { - workspaceQueryRunnerRestApiExceptionHandler(error); + return workspaceQueryRunnerRestApiExceptionHandler(error); } } @@ -77,68 +70,4 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler { depth: parseDepthRestRequest(request), }; } - - async handle(request: AuthenticatedRequest) { - const { id: recordId } = parseCorePath(request); - - if (!recordId) { - throw new BadRequestException('Record ID not found'); - } - - const { objectMetadata, repository, restrictedFields } = - await this.getRepositoryAndMetadataOrFail(request); - - // assert the record exists - await repository.findOneOrFail({ - select: { id: true }, - where: { id: recordId }, - }); - - const overriddenBody = await this.recordInputTransformerService.process({ - recordInput: request.body, - objectMetadataMapItem: objectMetadata.objectMetadataMapItem, - }); - - let selectedColumns = undefined; - - if (!isEmpty(restrictedFields)) { - const selectableFields = getAllSelectableColumnNames({ - restrictedFields, - objectMetadata, - }); - - selectedColumns = Object.keys(selectableFields).filter( - (key) => selectableFields[key], - ); - } - - const updatedRecord = await repository.update( - recordId, - overriddenBody, - undefined, - selectedColumns, - ); - - const updatedRecordId = updatedRecord.generatedMaps[0].id; - - const records = await this.getRecord({ - recordIds: [updatedRecordId], - repository, - objectMetadata, - depth: parseDepthRestRequest(request), - restrictedFields, - }); - - const record = records[0]; - - if (!isDefined(record)) { - throw new InternalServerErrorException('Updated record not found'); - } - - return this.formatResult({ - operation: 'update', - objectNameSingular: objectMetadata.objectMetadataMapItem.nameSingular, - data: record, - }); - } } 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 deleted file mode 100644 index 20700abff0..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts +++ /dev/null @@ -1,629 +0,0 @@ -import { BadRequestException, Inject } from '@nestjs/common'; - -import chunk from 'lodash.chunk'; -import isEmpty from 'lodash.isempty'; -import { - FieldMetadataType, - ObjectRecord, - type RestrictedFieldsPermissions, -} from 'twenty-shared/types'; -import { capitalize, isDefined } from 'twenty-shared/utils'; -import { In, type ObjectLiteral } from 'typeorm'; - -import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; -import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; - -import { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type'; -import { CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type'; -import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; -import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; -import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory'; -import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory'; -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 { 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 { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; -import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service'; -import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service'; -import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; -import { - PermissionsException, - PermissionsExceptionCode, - PermissionsExceptionMessage, -} from 'src/engine/metadata-modules/permissions/permissions.exception'; -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'; -import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; -import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; -import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; -import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; - -export interface PageInfo { - hasNextPage?: boolean; - hasPreviousPage?: boolean; - startCursor: string | null; - endCursor: string | null; -} - -interface FormatResultParams { - operation: - | 'delete' - | 'create' - | 'update' - | 'findOne' - | 'findMany' - | 'findDuplicates'; - objectNameSingular?: string; - objectNamePlural?: string; - data: T; - pageInfo?: PageInfo; - totalCount?: number; -} - -export interface FormatResult { - data?: { - [operation: string]: object; - }; - pageInfo?: PageInfo; - totalCount?: number; -} - -export abstract class RestApiBaseHandler { - @Inject() - protected readonly recordInputTransformerService: RecordInputTransformerService; - @Inject() - protected readonly coreQueryBuilderFactory: CoreQueryBuilderFactory; - @Inject() - protected readonly twentyORMManager: TwentyORMManager; - @Inject() - protected readonly getVariablesFactory: GetVariablesFactory; - @Inject() - protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService; - @Inject() - protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService; - @Inject() - protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService; - @Inject() - protected readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService; - @Inject() - protected readonly apiKeyRoleService: ApiKeyRoleService; - @Inject() - protected readonly restToCommonSelectedFieldsHandler: RestToCommonSelectedFieldsHandler; - @Inject() - protected readonly userRoleService: UserRoleService; - - protected abstract handle( - request: AuthenticatedRequest, - ): Promise< - FormatResult | { data: FormatResult[] } | CommonGroupByOutputItem[] - >; - - public async getRepositoryAndMetadataOrFail(request: AuthenticatedRequest) { - const { workspace, apiKey, userWorkspaceId } = request; - const { object: parsedObject } = parseCorePath(request); - - const objectMetadata = await this.coreQueryBuilderFactory.getObjectMetadata( - request, - parsedObject, - ); - - if (!workspace?.id) { - throw new BadRequestException('Workspace not found'); - } - - if (!objectMetadata) { - throw new BadRequestException('Object metadata not found'); - } - - const workspaceDataSource = await this.twentyORMManager.getDatasource(); - - const objectMetadataNameSingular = - objectMetadata.objectMetadataMapItem.nameSingular; - - const objectMetadataItemWithFieldsMaps = - getObjectMetadataMapItemByNameSingular( - objectMetadata.objectMetadataMaps, - objectMetadataNameSingular, - ); - - if (!isDefined(objectMetadataItemWithFieldsMaps)) { - throw new BadRequestException( - `Object metadata item with name singular ${objectMetadataNameSingular} not found`, - ); - } - - let roleId: string; - - if (isDefined(apiKey)) { - roleId = await this.apiKeyRoleService.getRoleIdForApiKey( - apiKey.id, - workspace.id, - ); - - if (!isDefined(roleId)) { - throw new PermissionsException( - PermissionsExceptionMessage.API_KEY_ROLE_NOT_FOUND, - PermissionsExceptionCode.API_KEY_ROLE_NOT_FOUND, - ); - } - } else { - const userWorkspaceRoleId = - await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId( - { - workspaceId: workspace.id, - userWorkspaceId, - }, - ); - - if (!isDefined(userWorkspaceRoleId)) { - throw new PermissionsException( - PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE, - PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE, - ); - } - - roleId = userWorkspaceRoleId; - } - - const repository = workspaceDataSource.getRepository( - objectMetadataNameSingular, - { - unionOf: [roleId], - }, - ); - - const objectMetadataPermissions = - await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles( - { - workspaceId: workspace.id, - roleIds: roleId ? [roleId] : undefined, - }, - ); - - if ( - !isDefined( - objectMetadataPermissions?.[roleId]?.[ - objectMetadata.objectMetadataMapItem.id - ]?.restrictedFields, - ) - ) { - throw new InternalServerError('Fields permissions not found for role'); - } - - const restrictedFields = - objectMetadataPermissions[roleId][objectMetadata.objectMetadataMapItem.id] - .restrictedFields; - - return { - objectMetadata, - repository, - workspaceDataSource, - objectMetadataItemWithFieldsMaps, - restrictedFields, - isExecutedByApiKey: isDefined(apiKey), - authContext: this.getAuthContextFromRequest(request), - objectsPermissions: objectMetadataPermissions[roleId], - }; - } - - getRelations({ - objectMetadata, - depth, - }: { - objectMetadata: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - }; - depth: Depth | undefined; - }) { - if (!isDefined(depth) || depth === 0) { - return []; - } - - const relations: string[] = []; - - Object.values(objectMetadata.objectMetadataMapItem.fieldsById).forEach( - (field) => { - if (isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) { - if ( - depth === MAX_DEPTH && - isDefined(field.relationTargetObjectMetadataId) - ) { - const relationTargetObjectMetadata = - objectMetadata.objectMetadataMaps.byId[ - field.relationTargetObjectMetadataId - ]; - - if (!isDefined(relationTargetObjectMetadata)) { - throw new BadRequestException( - `Object metadata relation target not found for relation creation payload`, - ); - } - const depth2Relations = this.getRelations({ - objectMetadata: { - objectMetadataMaps: objectMetadata.objectMetadataMaps, - objectMetadataMapItem: relationTargetObjectMetadata, - }, - depth: 1, - }); - - depth2Relations.forEach((depth2Relation) => { - relations.push(`${field.name}.${depth2Relation}`); - }); - } else { - relations.push(`${field.name}`); - } - } - }, - ); - - return relations; - } - - async getRecord({ - recordIds, - repository, - objectMetadata, - depth, - restrictedFields, - }: { - recordIds: string[]; - repository: WorkspaceRepository; - objectMetadata: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - }; - depth: Depth | undefined; - restrictedFields: RestrictedFieldsPermissions; - }) { - const relations = this.getRelations({ - objectMetadata, - depth: depth, - }); - - const relationsChunk = chunk(relations, 50); - - let selectOptions = undefined; - - if (!isEmpty(restrictedFields)) { - selectOptions = getAllSelectableColumnNames({ - restrictedFields, - objectMetadata, - }); - } - - const recordsWithoutRelations = await repository.find({ - ...(selectOptions && { select: selectOptions }), - where: { id: In(recordIds) }, - }); - - const recordsMap = new Map( - recordsWithoutRelations.map((record) => [record.id, record]), - ); - - for (const relationChunk of relationsChunk) { - const records = await repository.find({ - ...(selectOptions && { select: selectOptions }), - where: { id: In(recordIds) }, - relations: relationChunk, - }); - - records.map((record) => { - recordsMap.set(record.id, { - ...recordsMap.get(record.id), - ...record, - }); - }); - } - - const orderedRecords = recordIds.map((id) => recordsMap.get(id)); - - return orderedRecords; - } - - public getAuthContextFromRequest( - request: AuthenticatedRequest, - ): WorkspaceAuthContext { - return request; - } - - public formatResult({ - operation, - objectNameSingular, - objectNamePlural, - data, - pageInfo, - totalCount, - }: FormatResultParams) { - let prefix: string; - - if (isDefined(objectNameSingular) && isDefined(objectNamePlural)) { - throw new Error( - 'Cannot define both objectNameSingular and objectNamePlural', - ); - } - - if (operation === 'findOne') { - prefix = objectNameSingular || ''; - } else if (operation === 'findMany') { - prefix = objectNamePlural || ''; - } else if (operation === 'findDuplicates') { - prefix = `${objectNameSingular}Duplicates`; - } else { - prefix = - operation + capitalize(objectNameSingular || objectNamePlural || ''); - } - - return { - ...(operation === 'findDuplicates' - ? { - [prefix]: data, - } - : { - data: { - [prefix]: data, - }, - }), - ...(isDefined(pageInfo) ? { pageInfo } : {}), - ...(isDefined(totalCount) ? { totalCount } : {}), - }; - } - - async findRecords({ - request, - recordId, - repository, - objectMetadata, - objectMetadataItemWithFieldsMaps, - extraFilters, - restrictedFields, - }: { - request: AuthenticatedRequest; - recordId?: string; - repository: WorkspaceRepository; - objectMetadata: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - }; - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; - extraFilters?: Partial; - restrictedFields: RestrictedFieldsPermissions; - }) { - const objectMetadataNameSingular = - objectMetadata.objectMetadataMapItem.nameSingular; - - const qb = repository - .createQueryBuilder(objectMetadataNameSingular) - .select('id'); - - const inputs = this.getVariablesFactory.create( - recordId, - request, - objectMetadata, - ); - - const isForwardPagination = !inputs.endingBefore; - - const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldsMaps, - objectMetadata.objectMetadataMaps, - ); - - const filters = this.computeFilters({ - inputs, - objectMetadata, - isForwardPagination, - extraFilters, - }); - - let selectQueryBuilder = isDefined(filters) - ? graphqlQueryParser.applyFilterToBuilder( - qb, - objectMetadataNameSingular, - filters, - ) - : qb; - - const totalCount = await this.getTotalCount(selectQueryBuilder); - - selectQueryBuilder = graphqlQueryParser.applyOrderToBuilder( - selectQueryBuilder, - inputs.orderBy || [], - objectMetadataNameSingular, - isForwardPagination, - ); - - if (inputs.first) { - selectQueryBuilder = selectQueryBuilder.limit(inputs.first); - } - - if (inputs.last) { - selectQueryBuilder = selectQueryBuilder.limit(inputs.last); - } - - const recordIds = await selectQueryBuilder - .select(`${objectMetadataNameSingular}.id`) - .getMany(); - - const records = await this.getRecord({ - recordIds: recordIds.map((record) => record.id), - repository, - objectMetadata, - depth: parseDepthRestRequest(request), - restrictedFields, - }); - - const hasMoreRecords = records.length < totalCount; - - const finalRecords = formatGetManyData( - records, - objectMetadataItemWithFieldsMaps, - objectMetadata.objectMetadataMaps, - ); - - const startCursor = - finalRecords.length > 0 - ? encodeCursor(finalRecords[0], inputs.orderBy) - : null; - - const endCursor = - finalRecords.length > 0 - ? encodeCursor(finalRecords[finalRecords.length - 1], inputs.orderBy) - : null; - - return { - records: finalRecords, - totalCount, - hasMoreRecords, - isForwardPagination, - startCursor, - endCursor, - }; - } - - async getTotalCount( - query: WorkspaceSelectQueryBuilder, - ): Promise { - const countQuery = query.clone(); - - return await countQuery.getCount(); - } - - computeFilters({ - inputs, - objectMetadata, - isForwardPagination, - extraFilters, - }: { - inputs: QueryVariables; - objectMetadata: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - }; - isForwardPagination: boolean; - extraFilters?: Partial; - }) { - let appliedFilters = inputs.filter; - - if (extraFilters) { - appliedFilters = (appliedFilters - ? { and: [appliedFilters, extraFilters] } - : extraFilters) as unknown as ObjectRecordFilter; - } - - const cursor = inputs.startingAfter || inputs.endingBefore; - - if (cursor) { - const cursorArgFilter = computeCursorArgFilter( - this.parseCursor(cursor), - inputs.orderBy || [], - objectMetadata.objectMetadataMapItem, - isForwardPagination, - ); - - appliedFilters = (appliedFilters - ? { - and: [appliedFilters, { or: cursorArgFilter }], - } - : { or: cursorArgFilter }) as unknown as ObjectRecordFilter; - } - - return appliedFilters; - } - - private parseCursor = (cursor: string) => { - try { - return JSON.parse(Buffer.from(cursor ?? '', 'base64').toString()); - } catch { - throw new BadRequestException(`Invalid cursor: ${cursor}`); - } - }; - - private getObjectsPermissions = async (authContext: WorkspaceAuthContext) => { - let roleId: string; - - if (isDefined(authContext.apiKey)) { - roleId = await this.apiKeyRoleService.getRoleIdForApiKey( - authContext.apiKey.id, - authContext.workspace.id, - ); - } else { - const userWorkspaceRoleId = - await this.userRoleService.getRoleIdForUserWorkspace({ - userWorkspaceId: authContext.userWorkspaceId, - workspaceId: authContext.workspace.id, - }); - - if (!isDefined(userWorkspaceRoleId)) { - throw new PermissionsException( - PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE, - PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE, - ); - } - - roleId = userWorkspaceRoleId; - } - - const objectMetadataPermissions = - await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles( - { - workspaceId: authContext.workspace.id, - roleIds: [roleId], - }, - ); - - return { objectsPermissions: objectMetadataPermissions[roleId] }; - }; - - async computeSelectedFields({ - authContext, - depth, - objectMetadataMapItem, - objectMetadataMaps, - }: { - authContext: WorkspaceAuthContext; - depth?: Depth | undefined; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; - }): Promise { - const { objectsPermissions } = - await this.getObjectsPermissions(authContext); - - return this.restToCommonSelectedFieldsHandler.computeFromDepth({ - objectsPermissions, - objectMetadataMaps, - objectMetadataMapItem, - 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/core-query-builder.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts deleted file mode 100644 index 071b25f393..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; - -import { type Request } from 'express'; -import { SettingsPath } from 'twenty-shared/types'; -import { assertIsDefinedOrThrow, getSettingsPath } from 'twenty-shared/utils'; - -import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory'; -import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory'; -import { FindDuplicatesQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory'; -import { FindDuplicatesVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory'; -import { computeDepth } from 'src/engine/api/rest/core/query-builder/utils/compute-depth.utils'; -import { parseCoreBatchPath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-batch-path.utils'; -import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; -import { type Query } from 'src/engine/api/rest/core/types/query.type'; -import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; -import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -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'; -import { getObjectMetadataMapItemByNamePlural } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; -import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; -import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; - -@Injectable() -export class CoreQueryBuilderFactory { - constructor( - private readonly createManyQueryFactory: CreateManyQueryFactory, - private readonly createVariablesFactory: CreateVariablesFactory, - private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory, - private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory, - private readonly accessTokenService: AccessTokenService, - private readonly workspaceDomainsService: WorkspaceDomainsService, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, - private readonly featureFlagService: FeatureFlagService, - ) {} - - async getObjectMetadata( - request: Request, - parsedObject: string, - ): Promise<{ - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - }> { - const { workspace } = - await this.accessTokenService.validateTokenByRequest(request); - - assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); - - const currentCacheVersion = - await this.workspaceCacheStorageService.getMetadataVersion(workspace.id); - - if (currentCacheVersion === undefined) { - await this.workspaceMetadataCacheService.recomputeMetadataCache({ - workspaceId: workspace.id, - }); - - throw new BadRequestException('Metadata cache version not found'); - } - const objectMetadataMaps = - await this.workspaceCacheStorageService.getObjectMetadataMaps( - workspace.id, - currentCacheVersion, - ); - - if (!objectMetadataMaps) { - throw new BadRequestException( - `No object was found for the workspace associated with this API key. You may generate a new one here ${this.workspaceDomainsService - .buildWorkspaceURL({ - workspace, - pathname: getSettingsPath(SettingsPath.ApiWebhooks), - }) - .toString()}`, - ); - } - - const objectMetadataItem = getObjectMetadataMapItemByNamePlural( - objectMetadataMaps, - parsedObject, - ); - - if (!objectMetadataItem) { - const wrongObjectMetadataItem = getObjectMetadataMapItemByNameSingular( - objectMetadataMaps, - parsedObject, - ); - - let hint = 'eg: companies'; - - if (wrongObjectMetadataItem) { - hint = `Did you mean '${wrongObjectMetadataItem.namePlural}'?`; - } - - throw new BadRequestException( - `object '${parsedObject}' not found. ${hint}`, - ); - } - - const workspaceFeatureFlagsMap = - await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspace.id); - - // Check if this entity is workspace-gated and should be blocked from workspace API - if ( - shouldExcludeFromWorkspaceApi( - objectMetadataItem, - standardObjectMetadataDefinitions, - workspaceFeatureFlagsMap, - ) - ) { - throw new BadRequestException( - `object '${parsedObject}' not found. ${parsedObject} is not available via REST API.`, - ); - } - - return { - objectMetadataMaps, - objectMetadataMapItem: objectMetadataItem, - }; - } - - async createMany(request: Request): Promise { - const { object: parsedObject } = parseCoreBatchPath(request); - const objectMetadata = await this.getObjectMetadata(request, parsedObject); - const depth = computeDepth(request); - - return { - query: this.createManyQueryFactory.create(objectMetadata, depth), - variables: this.createVariablesFactory.create(request), - }; - } - - async findDuplicates(request: Request): Promise { - const { object: parsedObject } = parseCorePath(request); - const objectMetadata = await this.getObjectMetadata(request, parsedObject); - const depth = computeDepth(request); - - return { - query: this.findDuplicatesQueryFactory.create(objectMetadata, depth), - variables: this.findDuplicatesVariablesFactory.create(request), - }; - } -} diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts deleted file mode 100644 index 0af8f2b3b6..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory'; -import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories'; -import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; -import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; -import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; -import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; -import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; -import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; - -@Module({ - imports: [ - AuthModule, - ApiKeyModule, - WorkspaceDomainsModule, - FeatureFlagModule, - WorkspaceCacheStorageModule, - WorkspaceMetadataCacheModule, - WorkspacePermissionsCacheModule, - ], - providers: [...coreQueryBuilderFactories, CoreQueryBuilderFactory], - exports: [CoreQueryBuilderFactory], -}) -export class CoreQueryBuilderModule {} diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-many-query.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-many-query.factory.ts deleted file mode 100644 index 4a35b380a7..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-many-query.factory.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { capitalize } from 'twenty-shared/utils'; - -import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils'; -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 CreateManyQueryFactory { - create( - objectMetadata: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - }, - depth?: number, - ): string { - const objectNamePlural = capitalize( - objectMetadata.objectMetadataMapItem.namePlural, - ); - const objectNameSingular = capitalize( - objectMetadata.objectMetadataMapItem.nameSingular, - ); - - return ` - mutation Create${objectNamePlural}($data: [${objectNameSingular}CreateInput!]) { - create${objectNamePlural}(data: $data) { - id - ${Object.values(objectMetadata.objectMetadataMapItem.fieldsById) - .map((field) => - mapFieldMetadataToGraphqlQuery( - objectMetadata.objectMetadataMaps, - field, - depth, - ), - ) - .join('\n')} - } - } - `; - } -} diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-variables.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-variables.factory.ts deleted file mode 100644 index db8d7833c7..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/create-variables.factory.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type Request } from 'express'; - -import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type'; - -@Injectable() -export class CreateVariablesFactory { - create(request: Request): QueryVariables { - return { - data: request.body, - }; - } -} diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/factories.ts deleted file mode 100644 index f0405a5315..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/factories.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory'; -import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory'; -import { FindDuplicatesQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory'; -import { FindDuplicatesVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory'; -import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory'; -import { inputFactories } from 'src/engine/api/rest/input-factories/factories'; - -export const coreQueryBuilderFactories = [ - CreateManyQueryFactory, - FindDuplicatesQueryFactory, - CreateVariablesFactory, - GetVariablesFactory, - FindDuplicatesVariablesFactory, - ...inputFactories, -]; diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory.ts deleted file mode 100644 index aa90b7eaac..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { capitalize } from 'twenty-shared/utils'; - -import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils'; -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 FindDuplicatesQueryFactory { - create( - objectMetadata: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - }, - depth?: number, - ): string { - const objectNameSingular = - objectMetadata.objectMetadataMapItem.nameSingular; - - return ` - query FindDuplicate${capitalize( - objectNameSingular, - )}($ids: [UUID], $data: [${capitalize(objectNameSingular)}CreateInput]) { - ${objectNameSingular}Duplicates(ids: $ids, data: $data) { - totalCount - pageInfo { - hasNextPage - startCursor - endCursor - } - edges{ - node { - ${Object.values(objectMetadata.objectMetadataMapItem.fieldsById) - .map((field) => - mapFieldMetadataToGraphqlQuery( - objectMetadata.objectMetadataMaps, - field, - depth, - ), - ) - .join('\n')} - } - } - } - } - `; - } -} diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory.ts deleted file mode 100644 index 883e89d68d..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type Request } from 'express'; - -import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type'; - -@Injectable() -export class FindDuplicatesVariablesFactory { - create(request: Request): QueryVariables { - return request.body; - } -} 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 deleted file mode 100644 index 5617c6ed50..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/get-variables.factory.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { isDefined } from 'twenty-shared/utils'; - -import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type'; -import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory'; -import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-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 orderByInputFactory: OrderByInputFactory, - private readonly filterInputFactory: FilterInputFactory, - ) {} - - create( - id: string | undefined, - request: AuthenticatedRequest, - objectMetadata: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - }, - ): QueryVariables { - if (isDefined(id)) { - return { filter: { id: { eq: id } } }; - } - - const filter = this.filterInputFactory.create(request, objectMetadata); - const limit = parseLimitRestRequest(request); - const orderBy = this.orderByInputFactory.create(request, objectMetadata); - const endingBefore = parseEndingBeforeRestRequest(request); - const startingAfter = parseStartingAfterRestRequest(request); - - return { - filter, - orderBy, - first: !endingBefore ? limit : undefined, - last: endingBefore ? limit : undefined, - startingAfter, - endingBefore, - }; - } -} diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/__snapshots__/check-fields.utils.spec.ts.snap b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/__snapshots__/check-fields.utils.spec.ts.snap deleted file mode 100644 index f83fe67628..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/__snapshots__/check-fields.utils.spec.ts.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`checkFields should accept valid relation field name 1`] = `"field 'pointOfContact' does not exist in 'opportunity' object"`; - -exports[`checkFields should reject invalid field name 1`] = `"field 'wrongField' does not exist in 'opportunity' object"`; - -exports[`checkFields should reject mix of valid and invalid field names 1`] = `"field 'wrongField' does not exist in 'opportunity' object"`; diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts deleted file mode 100644 index 9843edd129..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/check-fields.utils.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { type EachTestingContext } from 'twenty-shared/testing'; - -import { OPPORTUNITY_WITH_FIELDS_MAPS } from 'src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock'; -import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils'; - -describe('checkFields', () => { - const testCases: EachTestingContext<{ - fields: string[]; - shouldThrow?: boolean; - }>[] = [ - { - title: 'should accept valid join column id', - context: { - fields: ['pointOfContactId'], - }, - }, - { - title: 'should accept valid relation field name', - context: { - shouldThrow: true, - fields: ['pointOfContact'], - }, - }, - { - title: 'should accept valid field name', - context: { - fields: ['position'], - }, - }, - { - title: 'should reject invalid field name', - context: { - fields: ['wrongField'], - shouldThrow: true, - }, - }, - { - title: 'should reject mix of valid and invalid field names', - context: { - fields: ['position', 'wrongField'], - shouldThrow: true, - }, - }, - { - title: 'should accept composite field', - context: { - fields: ['source'], - }, - }, - ]; - - it.each(testCases)('$title', ({ context }) => { - if (context.shouldThrow) { - expect(() => - checkFields(OPPORTUNITY_WITH_FIELDS_MAPS, context.fields), - ).toThrowErrorMatchingSnapshot(); - } else { - expect(() => - checkFields(OPPORTUNITY_WITH_FIELDS_MAPS, context.fields), - ).not.toThrow(); - } - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/compute-depth.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/compute-depth.utils.spec.ts deleted file mode 100644 index b0b655f036..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/compute-depth.utils.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { computeDepth } from 'src/engine/api/rest/core/query-builder/utils/compute-depth.utils'; - -describe('computeDepth', () => { - [0, 1].forEach((depth) => { - it('should compute depth from query', () => { - const request: any = { - query: { depth: `${depth}` }, - }; - - expect(computeDepth(request)).toEqual(depth); - }); - }); - - it('should return default depth if missing', () => { - const request: any = { query: {} }; - - expect(computeDepth(request)).toEqual(undefined); - }); - it('should raise if wrong depth', () => { - const request: any = { query: { depth: '100' } }; - - expect(() => computeDepth(request)).toThrow(); - - request.query.depth = '-1'; - - expect(() => computeDepth(request)).toThrow(); - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts deleted file mode 100644 index 6b84e7c2d8..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; - -import { - fieldNumberMock, - objectMetadataItemMock, -} from 'src/engine/api/__mocks__/object-metadata-item.mock'; -import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; - -describe('getFieldType', () => { - const completeFieldNumberMock = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', - objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: 'field-number-id', - type: fieldNumberMock.type, - name: fieldNumberMock.name, - label: 'Field Number', - isNullable: fieldNumberMock.isNullable, - defaultValue: fieldNumberMock.defaultValue, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const fieldsById: FieldMetadataMap = { - 'field-number-id': completeFieldNumberMock, - }; - - const mockObjectMetadataWithFieldMaps = { - ...objectMetadataItemMock, - fieldsById, - fieldIdByName: { - [completeFieldNumberMock.name]: completeFieldNumberMock.id, - }, - fieldIdByJoinColumnName: {}, - indexMetadatas: [], - }; - - it('should get field type', () => { - expect( - getFieldType(mockObjectMetadataWithFieldMaps, 'fieldNumber'), - ).toEqual(FieldMetadataType.NUMBER); - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts deleted file mode 100644 index 9cd5f81d75..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; - -import { type FieldMetadataRelationSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { - fieldCurrencyMock, - fieldNumberMock, - fieldTextMock, - objectMetadataItemMock, -} from 'src/engine/api/__mocks__/object-metadata-item.mock'; -import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -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'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; - -describe('mapFieldMetadataToGraphqlQuery', () => { - const typedFieldNumberMock = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', - objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000002', - name: fieldNumberMock.name, - type: fieldNumberMock.type, - label: 'Field Number', - isNullable: fieldNumberMock.isNullable, - defaultValue: fieldNumberMock.defaultValue, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const typedFieldTextMock = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', - objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000003', - name: fieldTextMock.name, - type: fieldTextMock.type, - label: 'Field Text', - isNullable: fieldTextMock.isNullable, - defaultValue: fieldTextMock.defaultValue, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const typedFieldCurrencyMock = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', - objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000004', - name: fieldCurrencyMock.name, - type: fieldCurrencyMock.type, - label: 'Field Currency', - isNullable: fieldCurrencyMock.isNullable, - defaultValue: fieldCurrencyMock.defaultValue, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const fieldsById: FieldMetadataMap = { - 'field-number-id': typedFieldNumberMock, - 'field-text-id': typedFieldTextMock, - 'field-currency-id': typedFieldCurrencyMock, - }; - - const typedObjectMetadataItem: ObjectMetadataItemWithFieldMaps = { - ...objectMetadataItemMock, - fieldsById, - fieldIdByName: { - [typedFieldNumberMock.name]: typedFieldNumberMock.id, - [typedFieldTextMock.name]: typedFieldTextMock.id, - [typedFieldCurrencyMock.name]: typedFieldCurrencyMock.id, - }, - fieldIdByJoinColumnName: {}, - indexMetadatas: [], - }; - - const objectMetadataMapsMock: ObjectMetadataMaps = { - byId: { - [objectMetadataItemMock.id]: typedObjectMetadataItem, - }, - idByNameSingular: { - [objectMetadataItemMock.nameSingular]: objectMetadataItemMock.id, - }, - }; - - it('should map properly', () => { - expect( - mapFieldMetadataToGraphqlQuery( - objectMetadataMapsMock, - typedFieldNumberMock, - ), - ).toEqual('fieldNumber'); - expect( - mapFieldMetadataToGraphqlQuery( - objectMetadataMapsMock, - typedFieldTextMock, - ), - ).toEqual('fieldText'); - expect( - mapFieldMetadataToGraphqlQuery( - objectMetadataMapsMock, - typedFieldCurrencyMock, - ), - ).toEqual(` - fieldCurrency - { - amountMicros - currencyCode - } - `); - }); - - describe('should handle all field metadata types', () => { - Object.values(FieldMetadataType).forEach((fieldMetadataType) => { - it(`with field type ${fieldMetadataType}`, () => { - const field = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', - objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000005', - type: fieldMetadataType, - name: 'toObjectMetadataName', - label: 'Test Field', - isNullable: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - settings: - fieldMetadataType === FieldMetadataType.RELATION || - fieldMetadataType === FieldMetadataType.MORPH_RELATION - ? ({ - relationType: RelationType.MANY_TO_ONE, - } as FieldMetadataRelationSettings) - : null, - }); - - expect( - mapFieldMetadataToGraphqlQuery(objectMetadataMapsMock, field), - ).toBeDefined(); - }); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock.ts deleted file mode 100644 index 9d70f7476d..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock.ts +++ /dev/null @@ -1,464 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; - -import { DateDisplayFormat } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; - -const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419'; -const objectMetadataId = '20202020-6e2c-42f6-a83c-cc58d776af88'; - -export const OPPORTUNITY_WITH_FIELDS_MAPS = - getMockObjectMetadataItemWithFieldsMaps({ - id: objectMetadataId, - nameSingular: 'opportunity', - namePlural: 'opportunities', - labelSingular: 'Opportunity', - labelPlural: 'Opportunities', - description: 'An opportunity', - icon: 'IconTargetArrow', - targetTableName: 'DEPRECATED', - isCustom: false, - isRemote: false, - isActive: true, - isSystem: false, - isAuditLogged: true, - isSearchable: true, - labelIdentifierFieldMetadataId: '20202020-c2f1-4435-adca-22931f8b41b6', - imageIdentifierFieldMetadataId: null, - workspaceId, - indexMetadatas: [], - fieldsById: { - '20202020-c2f1-4435-adca-22931f8b41b6': getMockFieldMetadataEntity({ - id: '20202020-c2f1-4435-adca-22931f8b41b6', - workspaceId, - objectMetadataId, - type: FieldMetadataType.TEXT, - name: 'name', - label: 'Name', - defaultValue: "''", - description: 'The opportunity name', - icon: 'IconTargetArrow', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: false, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-5eef-417a-b517-ebeedaa8e10b': getMockFieldMetadataEntity({ - id: '20202020-5eef-417a-b517-ebeedaa8e10b', - workspaceId, - objectMetadataId, - type: FieldMetadataType.CURRENCY, - name: 'amount', - label: 'Amount', - defaultValue: { amountMicros: null, currencyCode: "''" }, - description: 'Opportunity amount', - icon: 'IconCurrencyDollar', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-597c-44d3-98ec-ea71aea5256b': getMockFieldMetadataEntity({ - id: '20202020-597c-44d3-98ec-ea71aea5256b', - workspaceId, - objectMetadataId, - type: FieldMetadataType.DATE_TIME, - name: 'closeDate', - label: 'Close date', - defaultValue: null, - description: 'Opportunity close date', - icon: 'IconCalendarEvent', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-9b94-454a-94ca-8afb09c68faf': getMockFieldMetadataEntity({ - id: '20202020-9b94-454a-94ca-8afb09c68faf', - workspaceId, - objectMetadataId, - type: FieldMetadataType.SELECT, - name: 'stage', - label: 'Stage', - defaultValue: "'NEW'", - description: 'Opportunity stage', - icon: 'IconProgressCheck', - options: [ - { - id: '20202020-dba8-4975-81bd-b29a41c0a387', - color: 'red', - label: 'New', - value: 'NEW', - position: 0, - }, - { - id: '20202020-1c9d-490c-940c-bf47addcd6a1', - color: 'purple', - label: 'Screening', - value: 'SCREENING', - position: 1, - }, - { - id: '20202020-1368-438b-8702-6d5e5727a888', - color: 'sky', - label: 'Meeting', - value: 'MEETING', - position: 2, - }, - { - id: '20202020-41e4-4b4e-a038-f02645f55767', - color: 'turquoise', - label: 'Proposal', - value: 'PROPOSAL', - position: 3, - }, - { - id: '20202020-8acf-4934-8519-42f7c9133cd5', - color: 'yellow', - label: 'Customer', - value: 'CUSTOMER', - position: 4, - }, - ], - isCustom: false, - isActive: true, - isSystem: false, - isNullable: false, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-30a5-4d8e-9b93-12d31ece0aaa': getMockFieldMetadataEntity({ - id: '20202020-30a5-4d8e-9b93-12d31ece0aaa', - workspaceId, - objectMetadataId, - type: FieldMetadataType.POSITION, - name: 'position', - label: 'Position', - defaultValue: 0, - description: 'Opportunity record position', - icon: 'IconHierarchy2', - isCustom: false, - isActive: true, - isSystem: true, - isNullable: false, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-f95f-424f-ab32-65961e8e9635': getMockFieldMetadataEntity({ - id: '20202020-f95f-424f-ab32-65961e8e9635', - workspaceId, - objectMetadataId, - type: FieldMetadataType.ACTOR, - name: 'createdBy', - label: 'Created by', - defaultValue: { name: "'System'", source: "'MANUAL'" }, - description: 'The creator of the record', - icon: 'IconCreativeCommonsSa', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: false, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-5e10-4780-babb-38a465ac546c': getMockFieldMetadataEntity({ - id: '20202020-5e10-4780-babb-38a465ac546c', - workspaceId, - objectMetadataId, - type: FieldMetadataType.TEXT, - name: 'searchVector', - label: 'Search vector', - defaultValue: null, - description: 'Field used for full-text search', - icon: 'IconUser', - isCustom: false, - isActive: true, - isSystem: true, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-8f4a-4f8d-822e-90fe72f75b79': getMockFieldMetadataEntity({ - id: '20202020-8f4a-4f8d-822e-90fe72f75b79', - workspaceId, - objectMetadataId, - type: FieldMetadataType.UUID, - name: 'id', - label: 'Id', - defaultValue: 'uuid', - description: 'Id', - icon: 'Icon123', - isCustom: false, - isActive: true, - isSystem: true, - isNullable: false, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-f120-4b59-b239-f7f1d8eb243e': getMockFieldMetadataEntity({ - id: '20202020-f120-4b59-b239-f7f1d8eb243e', - workspaceId, - objectMetadataId, - type: FieldMetadataType.DATE_TIME, - name: 'createdAt', - label: 'Creation date', - defaultValue: 'now', - description: 'Creation date', - icon: 'IconCalendar', - settings: { displayFormat: DateDisplayFormat.RELATIVE }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: false, - isUnique: false, - isLabelSyncedWithName: false, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-dcc8-4318-9756-b87377692561': getMockFieldMetadataEntity({ - id: '20202020-dcc8-4318-9756-b87377692561', - workspaceId, - objectMetadataId, - type: FieldMetadataType.DATE_TIME, - name: 'updatedAt', - label: 'Last update', - defaultValue: 'now', - description: 'Last time the record was changed', - icon: 'IconCalendarClock', - settings: { displayFormat: DateDisplayFormat.RELATIVE }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: false, - isUnique: false, - isLabelSyncedWithName: false, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-1694-4f8b-8760-61a5ff330022': getMockFieldMetadataEntity({ - id: '20202020-1694-4f8b-8760-61a5ff330022', - workspaceId, - objectMetadataId, - type: FieldMetadataType.DATE_TIME, - name: 'deletedAt', - label: 'Deletion date', - defaultValue: null, - description: 'Record deletion date', - icon: 'IconCalendarOff', - settings: { displayFormat: DateDisplayFormat.RELATIVE }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: false, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-4f52-4dea-a116-723f9bf7f082': getMockFieldMetadataEntity({ - id: '20202020-4f52-4dea-a116-723f9bf7f082', - workspaceId, - objectMetadataId, - type: FieldMetadataType.RELATION, - name: 'pointOfContact', - label: 'Point of Contact', - description: 'The point of contact for this opportunity', - icon: 'IconUser', - settings: { - relationType: RelationType.MANY_TO_ONE, - joinColumnName: 'pointOfContactId', - onDelete: RelationOnDeleteAction.CASCADE, - }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-fc02-4be2-be1a-e121daf5400d': getMockFieldMetadataEntity({ - id: '20202020-fc02-4be2-be1a-e121daf5400d', - workspaceId, - objectMetadataId, - type: FieldMetadataType.RELATION, - name: 'company', - label: 'Company', - description: 'The company this opportunity is associated with', - icon: 'IconBuildingSkyscraper', - settings: { - relationType: RelationType.MANY_TO_ONE, - joinColumnName: 'companyId', - onDelete: RelationOnDeleteAction.CASCADE, - }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-fd9f-48f0-bd5f-5b0fec6a5de4': getMockFieldMetadataEntity({ - id: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4', - workspaceId, - objectMetadataId, - type: FieldMetadataType.RELATION, - name: 'favorites', - label: 'Favorites', - description: 'Users who favorited this opportunity', - icon: 'IconStar', - settings: { - relationType: RelationType.ONE_TO_MANY, - onDelete: RelationOnDeleteAction.CASCADE, - }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-88ab-4138-98ce-80533bb423e3': getMockFieldMetadataEntity({ - id: '20202020-88ab-4138-98ce-80533bb423e3', - workspaceId, - objectMetadataId, - type: FieldMetadataType.RELATION, - name: 'taskTargets', - label: 'Task Targets', - description: 'Tasks targeting this opportunity', - icon: 'IconCheckbox', - settings: { - relationType: RelationType.ONE_TO_MANY, - onDelete: RelationOnDeleteAction.CASCADE, - }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-4258-422b-b35b-db3f090af8da': getMockFieldMetadataEntity({ - id: '20202020-4258-422b-b35b-db3f090af8da', - workspaceId, - objectMetadataId, - type: FieldMetadataType.RELATION, - name: 'noteTargets', - label: 'Note Targets', - description: 'Notes targeting this opportunity', - icon: 'IconNotes', - settings: { - relationType: RelationType.ONE_TO_MANY, - onDelete: RelationOnDeleteAction.CASCADE, - }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-16ca-40a7-a1ba-712975c916cd': getMockFieldMetadataEntity({ - id: '20202020-16ca-40a7-a1ba-712975c916cd', - workspaceId, - objectMetadataId, - type: FieldMetadataType.RELATION, - name: 'attachments', - label: 'Attachments', - description: 'Attachments for this opportunity', - icon: 'IconPaperclip', - settings: { - relationType: RelationType.ONE_TO_MANY, - onDelete: RelationOnDeleteAction.CASCADE, - }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - '20202020-92a5-47bf-a38d-c1c72b2c3e4d': getMockFieldMetadataEntity({ - id: '20202020-92a5-47bf-a38d-c1c72b2c3e4d', - workspaceId, - objectMetadataId, - type: FieldMetadataType.RELATION, - name: 'timelineActivities', - label: 'Timeline Activities', - description: 'Timeline activities for this opportunity', - icon: 'IconTimeline', - settings: { - relationType: RelationType.ONE_TO_MANY, - onDelete: RelationOnDeleteAction.CASCADE, - }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date('2025-06-27T12:55:13.271Z'), - updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }), - }, - fieldIdByName: { - name: '20202020-c2f1-4435-adca-22931f8b41b6', - amount: '20202020-5eef-417a-b517-ebeedaa8e10b', - closeDate: '20202020-597c-44d3-98ec-ea71aea5256b', - stage: '20202020-9b94-454a-94ca-8afb09c68faf', - position: '20202020-30a5-4d8e-9b93-12d31ece0aaa', - createdBy: '20202020-f95f-424f-ab32-65961e8e9635', - searchVector: '20202020-5e10-4780-babb-38a465ac546c', - id: '20202020-8f4a-4f8d-822e-90fe72f75b79', - createdAt: '20202020-f120-4b59-b239-f7f1d8eb243e', - updatedAt: '20202020-dcc8-4318-9756-b87377692561', - deletedAt: '20202020-1694-4f8b-8760-61a5ff330022', - pointOfContact: '20202020-4f52-4dea-a116-723f9bf7f082', - company: '20202020-fc02-4be2-be1a-e121daf5400d', - favorites: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4', - taskTargets: '20202020-88ab-4138-98ce-80533bb423e3', - noteTargets: '20202020-4258-422b-b35b-db3f090af8da', - attachments: '20202020-16ca-40a7-a1ba-712975c916cd', - timelineActivities: '20202020-92a5-47bf-a38d-c1c72b2c3e4d', - }, - fieldIdByJoinColumnName: { - pointOfContactId: '20202020-4f52-4dea-a116-723f9bf7f082', - companyId: '20202020-fc02-4be2-be1a-e121daf5400d', - }, - }); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-fields.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-fields.utils.ts deleted file mode 100644 index 4d2ca665e4..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/check-fields.utils.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; - -import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; - -export const checkFields = ( - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps, - fieldNames: string[], -): void => { - const fieldMetadataNames: string[] = Object.values( - objectMetadataItemWithFieldsMaps.fieldsById, - ) - .flatMap((field) => { - if (isCompositeFieldMetadataType(field.type)) { - const compositeType = compositeTypeDefinitions.get(field.type); - - if (!compositeType) { - throw new BadRequestException( - `Composite type '${field.type}' not found`, - ); - } - - // TODO: Don't really know why we need to put fieldName and compositeType name here - return [ - field.name, - compositeType.properties.map( - (compositeProperty) => compositeProperty.name, - ), - ].flat(); - } - - if (isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) { - return field.settings?.joinColumnName; - } - - return field.name; - }) - .filter(isDefined); - - for (const fieldName of fieldNames) { - if (!fieldMetadataNames.includes(fieldName)) { - throw new BadRequestException( - `field '${fieldName}' does not exist in '${computeObjectTargetTable( - objectMetadataItemWithFieldsMaps, - )}' object`, - ); - } - } -}; diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/compute-depth.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/compute-depth.utils.ts deleted file mode 100644 index 50d537a22f..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/compute-depth.utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; - -import { type Request } from 'express'; - -const ALLOWED_DEPTH_VALUES = [0, 1]; - -export const computeDepth = (request: Request): number | undefined => { - if (!request.query.depth) { - return undefined; - } - - const depth = +request.query.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/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 deleted file mode 100644 index 7deeb4a2fe..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { - fieldNumberMock, - fieldTextMock, - objectMetadataItemMock, -} from 'src/engine/api/__mocks__/object-metadata-item.mock'; -import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; - -describe('parseFilter', () => { - const completeFieldNumberMock = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', - objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000002', - type: fieldNumberMock.type, - name: fieldNumberMock.name, - label: 'Field Number', - isNullable: fieldNumberMock.isNullable, - defaultValue: fieldNumberMock.defaultValue, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const completeFieldTextMock = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', - objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000003', - type: fieldTextMock.type, - name: fieldTextMock.name, - label: 'Field Text', - isNullable: fieldTextMock.isNullable, - defaultValue: fieldTextMock.defaultValue, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const fieldsById: FieldMetadataMap = { - 'field-number-id': completeFieldNumberMock, - 'field-text-id': completeFieldTextMock, - }; - - const mockObjectMetadataWithFieldMaps: ObjectMetadataItemWithFieldMaps = { - ...objectMetadataItemMock, - fieldsById, - fieldIdByName: { - [completeFieldNumberMock.name]: completeFieldNumberMock.id, - [completeFieldTextMock.name]: completeFieldTextMock.id, - }, - fieldIdByJoinColumnName: {}, - indexMetadatas: [], - }; - - it('should parse string filter test 1', () => { - expect( - parseFilter( - 'and(fieldNumber[eq]:1,fieldNumber[eq]:2)', - mockObjectMetadataWithFieldMaps, - ), - ).toEqual({ - and: [{ fieldNumber: { eq: '1' } }, { fieldNumber: { eq: '2' } }], - }); - }); - - it('should parse string filter test 2', () => { - expect( - parseFilter( - 'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3))', - mockObjectMetadataWithFieldMaps, - ), - ).toEqual({ - and: [ - { fieldNumber: { eq: '1' } }, - { or: [{ fieldNumber: { eq: '2' } }, { fieldNumber: { eq: '3' } }] }, - ], - }); - }); - - it('should parse string filter test 3', () => { - expect( - parseFilter( - '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))', - mockObjectMetadataWithFieldMaps, - ), - ).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( - parseFilter( - '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))', - mockObjectMetadataWithFieldMaps, - ), - ).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( - parseFilter( - 'and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))', - mockObjectMetadataWithFieldMaps, - ), - ).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/parse-filter.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils.ts deleted file mode 100644 index e581163cf1..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; - -import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.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 { - or = 'or', - and = 'and', - not = 'not', -} - -export const parseFilter = ( - filterQuery: string, - objectMetadataItem: ObjectMetadataItemWithFieldMaps, -): Record => { - const result: Record = {}; - 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) => - parseFilter(elem, objectMetadataItem), - ); - - 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)`, - ); - } - result[conjunction] = subResult[0]; - } else { - result[conjunction] = subResult; - } - - return result; - } - const { fields, comparator, value } = parseBaseFilter(filterQuery); - - const fieldName = fields[0]; - - checkFields(objectMetadataItem, fields); - const fieldType = getFieldType(objectMetadataItem, fieldName); - - checkFilterEnumValues(fieldType, fieldName, value, objectMetadataItem); - - const formattedValue = formatFieldValue(value, fieldType, comparator); - - return fields.reverse().reduce( - (acc, currentValue) => { - return { [currentValue]: acc }; - }, - { [comparator]: formattedValue }, - ); -}; diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts deleted file mode 100644 index 993c5458f9..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type FieldMetadataType } from 'twenty-shared/types'; - -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export const getFieldType = ( - objectMetadataItem: ObjectMetadataItemWithFieldMaps, - fieldName: string, -): FieldMetadataType | undefined => { - const fieldMetadataId = objectMetadataItem.fieldIdByName[fieldName]; - const field = objectMetadataItem.fieldsById[fieldMetadataId]; - - return field?.type; -}; diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts deleted file mode 100644 index a5c023e14c..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; - -const DEFAULT_DEPTH_VALUE = 1; - -// TODO: Should be properly type and based on composite type definitions -export const mapFieldMetadataToGraphqlQuery = ( - objectMetadataMaps: ObjectMetadataMaps, - field: FieldMetadataEntity, - maxDepthForRelations = DEFAULT_DEPTH_VALUE, -): string | undefined => { - if (maxDepthForRelations < 0) { - return ''; - } - - const fieldType = field.type; - - const fieldIsSimpleValue = [ - FieldMetadataType.UUID, - FieldMetadataType.TEXT, - FieldMetadataType.DATE_TIME, - FieldMetadataType.DATE, - FieldMetadataType.BOOLEAN, - FieldMetadataType.NUMBER, - FieldMetadataType.NUMERIC, - FieldMetadataType.RATING, - FieldMetadataType.SELECT, - FieldMetadataType.MULTI_SELECT, - FieldMetadataType.POSITION, - FieldMetadataType.RAW_JSON, - FieldMetadataType.RICH_TEXT, - FieldMetadataType.ARRAY, - FieldMetadataType.TS_VECTOR, - ].includes(fieldType); - - const isRelation = - isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) || - isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION); - - if (fieldIsSimpleValue) { - return field.name; - } else if ( - maxDepthForRelations > 0 && - isRelation && - field.settings?.relationType === RelationType.MANY_TO_ONE - ) { - const targetObjectMetadataId = field.relationTargetObjectMetadataId; - - if (!targetObjectMetadataId) { - return ''; - } - - const relationMetadataItem = - objectMetadataMaps.byId[targetObjectMetadataId]; - - if (!isDefined(relationMetadataItem)) { - return ''; - } - - return `${field.name} - { - id - ${Object.values(relationMetadataItem.fieldsById) - .map((field) => - mapFieldMetadataToGraphqlQuery( - objectMetadataMaps, - field, - maxDepthForRelations - 1, - ), - ) - .join('\n')} - }`; - } else if ( - maxDepthForRelations > 0 && - isRelation && - field.settings?.relationType === RelationType.ONE_TO_MANY - ) { - const targetObjectMetadataId = field.relationTargetObjectMetadataId; - - if (!targetObjectMetadataId) { - return ''; - } - const relationMetadataItem = - objectMetadataMaps.byId[targetObjectMetadataId]; - - if (!relationMetadataItem) { - return ''; - } - - return `${field.name} - { - edges { - node { - id - ${Object.values(relationMetadataItem.fieldsById) - .map((field) => - mapFieldMetadataToGraphqlQuery( - objectMetadataMaps, - field, - maxDepthForRelations - 1, - ), - ) - .join('\n')} - } - } - }`; - } else if (fieldType === FieldMetadataType.LINKS) { - return ` - ${field.name} - { - primaryLinkLabel - primaryLinkUrl - secondaryLinks - } - `; - } else if (fieldType === FieldMetadataType.CURRENCY) { - return ` - ${field.name} - { - amountMicros - currencyCode - } - `; - } else if (fieldType === FieldMetadataType.FULL_NAME) { - return ` - ${field.name} - { - firstName - lastName - } - `; - } else if (fieldType === FieldMetadataType.ADDRESS) { - return ` - ${field.name} - { - addressStreet1 - addressStreet2 - addressCity - addressPostcode - addressState - addressCountry - addressLat - addressLng - } - `; - } else if (fieldType === FieldMetadataType.ACTOR) { - return ` - ${field.name} - { - source - workspaceMemberId - name - } - `; - } else if (fieldType === FieldMetadataType.EMAILS) { - return ` - ${field.name} - { - primaryEmail - additionalEmails - } - `; - } else if (fieldType === FieldMetadataType.PHONES) { - return ` - ${field.name} - { - primaryPhoneNumber - primaryPhoneCountryCode - primaryPhoneCallingCode - additionalPhones - } - `; - } else if (fieldType === FieldMetadataType.RICH_TEXT_V2) { - return ` - ${field.name} - { - blocknote - markdown - } - `; - } -}; diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/__tests__/parse-core-batch-path.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/__tests__/parse-core-batch-path.utils.spec.ts deleted file mode 100644 index a66400eecb..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/__tests__/parse-core-batch-path.utils.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { parseCoreBatchPath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-batch-path.utils'; - -describe('parseCoreBatchPath', () => { - it('should parse object from request path', () => { - const request: any = { path: '/rest/batch/companies' }; - - expect(parseCoreBatchPath(request)).toEqual({ - object: 'companies', - }); - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-batch-path.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-batch-path.utils.ts deleted file mode 100644 index 15150f6252..0000000000 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-batch-path.utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { type Request } from 'express'; - -export const parseCoreBatchPath = (request: Request): { object: string } => { - return { object: request.path.replace('/rest/batch/', '') }; -}; diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts b/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts index cedde75502..e0d0e7e96c 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts @@ -18,14 +18,13 @@ import { RestApiRestoreManyHandler } from 'src/engine/api/rest/core/handlers/res import { RestApiRestoreOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-one.handler'; import { RestApiUpdateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-many.handler'; import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler'; -import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module'; -import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories'; import { restToCommonArgsHandlers } from 'src/engine/api/rest/core/rest-to-common-args-handlers/rest-to-common-args-handlers'; import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service'; import { RestApiService } from 'src/engine/api/rest/rest-api.service'; import { ActorModule } from 'src/engine/core-modules/actor/actor.module'; import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @@ -55,7 +54,6 @@ const restApiCoreResolvers = [ @Module({ imports: [ - CoreQueryBuilderModule, WorkspaceCacheStorageModule, AuthModule, ApiKeyModule, @@ -68,12 +66,12 @@ const restApiCoreResolvers = [ ActorModule, FeatureFlagModule, CoreCommonApiModule, + WorkspaceDomainsModule, ], controllers: [RestApiCoreController], providers: [ RestApiService, RestApiCoreService, - ...coreQueryBuilderFactories, ...restApiCoreResolvers, ...restToCommonArgsHandlers, ], 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 3fa8056d2c..f32795d50e 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 @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; @@ -17,11 +17,9 @@ import { RestApiRestoreManyHandler } from 'src/engine/api/rest/core/handlers/res import { RestApiRestoreOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-one.handler'; import { RestApiUpdateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-many.handler'; import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler'; -import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils'; import { parseSoftDeleteRestRequest } from 'src/engine/api/rest/input-request-parsers/soft-delete-parser-utils/parse-soft-delete-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; -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 RestApiCoreService { @@ -41,83 +39,37 @@ export class RestApiCoreService { private readonly restApiRestoreOneHandler: RestApiRestoreOneHandler, private readonly restApiRestoreManyHandler: RestApiRestoreManyHandler, private readonly restApiMergeManyHandler: RestApiMergeManyHandler, - private readonly featureFlagService: FeatureFlagService, ) {} - private async isCommonApiEnabled(request: AuthenticatedRequest) { - return await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_COMMON_API_ENABLED, - request.workspace.id, - ); - } - async createOne(request: AuthenticatedRequest) { - const isCommonApiEnabled = await this.isCommonApiEnabled(request); - - if (isCommonApiEnabled) { - return await this.restApiCreateOneHandler.commonHandle(request); - } else { - return await this.restApiCreateOneHandler.handle(request); - } + return await this.restApiCreateOneHandler.handle(request); } async createMany(request: AuthenticatedRequest) { - const isCommonApiEnabled = await this.isCommonApiEnabled(request); - - if (isCommonApiEnabled) { - return await this.restApiCreateManyHandler.commonHandle(request); - } else { - return await this.restApiCreateManyHandler.handle(request); - } + return await this.restApiCreateManyHandler.handle(request); } async findDuplicates(request: AuthenticatedRequest) { - const isCommonApiEnabled = await this.isCommonApiEnabled(request); - - if (isCommonApiEnabled) { - return await this.restApiFindDuplicatesHandler.commonHandle(request); - } else { - return await this.restApiFindDuplicatesHandler.handle(request); - } + return await this.restApiFindDuplicatesHandler.handle(request); } async update(request: AuthenticatedRequest) { const { id: recordId } = parseCorePath(request); - const isCommonApiEnabled = await this.isCommonApiEnabled(request); - if (isCommonApiEnabled) { - if (isDefined(recordId)) { - return await this.restApiUpdateOneHandler.commonHandle(request); - } else { - return await this.restApiUpdateManyHandler.handle(request); - } + if (isDefined(recordId)) { + return await this.restApiUpdateOneHandler.handle(request); } else { - if (isDefined(recordId)) { - return await this.restApiUpdateOneHandler.handle(request); - } else { - throw new BadRequestException( - 'Activate feature flag to use UpdateMany in the REST API', - ); - } + return await this.restApiUpdateManyHandler.handle(request); } } async get(request: AuthenticatedRequest) { const { id: recordId } = parseCorePath(request); - const isCommonApiEnabled = await this.isCommonApiEnabled(request); - if (isCommonApiEnabled) { - if (isDefined(recordId)) { - return await this.restApiFindOneHandler.commonHandle(request); - } else { - return await this.restApiFindManyHandler.commonHandle(request); - } + if (isDefined(recordId)) { + return await this.restApiFindOneHandler.handle(request); } else { - if (isDefined(recordId)) { - return await this.restApiFindOneHandler.handle(request); - } else { - return await this.restApiFindManyHandler.handle(request); - } + return await this.restApiFindManyHandler.handle(request); } } @@ -128,54 +80,30 @@ export class RestApiCoreService { async delete(request: AuthenticatedRequest) { const { id: recordId } = parseCorePath(request); - const isCommonApiEnabled = await this.isCommonApiEnabled(request); const isSoftDelete = parseSoftDeleteRestRequest(request); - if (isCommonApiEnabled && !isSoftDelete && isDefined(recordId)) - return await this.restApiDestroyOneHandler.commonHandle(request); - if (isCommonApiEnabled && !isSoftDelete && !isDefined(recordId)) + if (!isSoftDelete && isDefined(recordId)) + return await this.restApiDestroyOneHandler.handle(request); + if (!isSoftDelete && !isDefined(recordId)) return await this.restApiDestroyManyHandler.handle(request); - if (isCommonApiEnabled && isSoftDelete && isDefined(recordId)) + if (isSoftDelete && isDefined(recordId)) return await this.restApiDeleteOneHandler.handle(request); - if (isCommonApiEnabled && isSoftDelete && !isDefined(recordId)) + if (isSoftDelete && !isDefined(recordId)) return await this.restApiDeleteManyHandler.handle(request); - - if (!isCommonApiEnabled && !isSoftDelete && isDefined(recordId)) - return await this.restApiDestroyOneHandler.handle(request); - - throw new BadRequestException( - 'Activate feature flag IS_COMMON_API_ENABLED to use Delete in the REST API', - ); } async restore(request: AuthenticatedRequest) { const { id: recordId } = parseCorePath(request); - const isCommonApiEnabled = await this.isCommonApiEnabled(request); - - if (isCommonApiEnabled) { - if (isDefined(recordId)) { - return await this.restApiRestoreOneHandler.handle(request); - } else { - return await this.restApiRestoreManyHandler.handle(request); - } + if (isDefined(recordId)) { + return await this.restApiRestoreOneHandler.handle(request); } else { - throw new BadRequestException( - 'Activate feature flag to use Restore in the REST API', - ); + return await this.restApiRestoreManyHandler.handle(request); } } async mergeMany(request: AuthenticatedRequest) { - const isCommonApiEnabled = await this.isCommonApiEnabled(request); - - if (isCommonApiEnabled) { - return await this.restApiMergeManyHandler.handle(request); - } else { - throw new BadRequestException( - 'Activate feature flag to use Merge in the REST API', - ); - } + return await this.restApiMergeManyHandler.handle(request); } } diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts deleted file mode 100644 index 5e5c26eed6..0000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { Test, type TestingModule } from '@nestjs/testing'; - -import { - fieldCurrencyMock, - fieldNumberMock, - fieldTextMock, - objectMetadataMapItemMock, -} from 'src/engine/api/__mocks__/object-metadata-item.mock'; -import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; - -describe('FilterInputFactory', () => { - const workspaceId = '20202020-cc80-4306-ad69-da9e11997292'; - - const completeFieldNumberMock = getMockFieldMetadataEntity({ - workspaceId, - id: 'field-number-id', - type: fieldNumberMock.type, - name: fieldNumberMock.name, - label: 'Field Number', - objectMetadataId: 'object-metadata-id', - isNullable: fieldNumberMock.isNullable, - defaultValue: fieldNumberMock.defaultValue, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const completeFieldTextMock = getMockFieldMetadataEntity({ - workspaceId, - id: 'field-text-id', - type: fieldTextMock.type, - name: fieldTextMock.name, - label: 'Field Text', - objectMetadataId: 'object-metadata-id', - isNullable: fieldTextMock.isNullable, - defaultValue: fieldTextMock.defaultValue, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const completeFieldCurrencyMock = getMockFieldMetadataEntity({ - workspaceId, - id: 'field-currency-id', - type: fieldCurrencyMock.type, - name: fieldCurrencyMock.name, - label: 'Field Currency', - objectMetadataId: 'object-metadata-id', - isNullable: fieldCurrencyMock.isNullable, - defaultValue: fieldCurrencyMock.defaultValue, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const fieldsById: FieldMetadataMap = { - 'field-number-id': completeFieldNumberMock, - 'field-text-id': completeFieldTextMock, - 'field-currency-id': completeFieldCurrencyMock, - }; - - const objectMetadataMapItem: ObjectMetadataItemWithFieldMaps = { - ...objectMetadataMapItemMock, - fieldsById, - fieldIdByName: { - [completeFieldNumberMock.name]: completeFieldNumberMock.id, - [completeFieldTextMock.name]: completeFieldTextMock.id, - [completeFieldCurrencyMock.name]: completeFieldCurrencyMock.id, - }, - fieldIdByJoinColumnName: {}, - }; - - const objectMetadataMaps = { - byId: { - [objectMetadataMapItemMock.id || 'mock-id']: objectMetadataMapItem, - }, - idByNameSingular: { - [objectMetadataMapItemMock.nameSingular]: - objectMetadataMapItemMock.id || 'mock-id', - }, - }; - - const objectMetadata = { - objectMetadataMaps, - objectMetadataMapItem, - }; - - let service: FilterInputFactory; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [FilterInputFactory], - }).compile(); - - service = module.get(FilterInputFactory); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('create', () => { - it('should return default if filter missing', () => { - const request: any = { query: {} }; - - expect(service.create(request, objectMetadata)).toEqual({}); - }); - - it('should throw when wrong field provided', () => { - const request: any = { - query: { - filter: 'wrongField[eq]:1', - }, - }; - - expect(() => service.create(request, objectMetadata)).toThrow( - "field 'wrongField' does not exist in 'objectName' object", - ); - }); - - it('should throw when wrong comparator provided', () => { - const request: any = { - query: { - filter: 'fieldNumber[wrongComparator]:1', - }, - }; - - expect(() => service.create(request, objectMetadata)).toThrow( - "'filter' invalid for 'fieldNumber[wrongComparator]:1', comparator wrongComparator not in eq, neq, in, containsAny, is, gt, gte, lt, lte, startsWith, like, ilike", - ); - }); - - it('should throw when wrong filter provided', () => { - const request: any = { - query: { - filter: 'fieldNumber[wrongComparator:1', - }, - }; - - expect(() => service.create(request, objectMetadata)).toThrow( - "'filter' invalid for 'fieldNumber[wrongComparator:1'. eg: price[gte]:10", - ); - }); - - it('should throw when parenthesis are not closed', () => { - const request: any = { - query: { - filter: 'and(fieldNumber[eq]:1,not(fieldNumber[neq]:1)', - }, - }; - - expect(() => service.create(request, objectMetadata)).toThrow( - "'filter' invalid. 1 close bracket is missing in the query", - ); - }); - - it('should create filter parser properly', () => { - const request: any = { - query: { - filter: 'fieldNumber[eq]:1,fieldText[eq]:"Test"', - }, - }; - - expect(service.create(request, objectMetadata)).toEqual({ - and: [{ fieldNumber: { eq: 1 } }, { fieldText: { eq: 'Test' } }], - }); - }); - - it('should create complex filter parser properly', () => { - const request: any = { - query: { - filter: - 'and(fieldNumber[eq]:1,fieldText[gte]:"Test",not(fieldText[ilike]:"%val%"),or(not(and(fieldText[startsWith]:"test",fieldNumber[in]:[2,4,5])),fieldCurrency.amountMicros[gt]:1))', - }, - }; - - expect(service.create(request, objectMetadata)).toEqual({ - and: [ - { fieldNumber: { eq: 1 } }, - { fieldText: { gte: 'Test' } }, - { not: { fieldText: { ilike: '%val%' } } }, - { - or: [ - { - not: { - and: [ - { fieldText: { startsWith: 'test' } }, - { fieldNumber: { in: [2, 4, 5] } }, - ], - }, - }, - { fieldCurrency: { amountMicros: { gt: '1' } } }, - ], - }, - ], - }); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts deleted file mode 100644 index 72f425c923..0000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/order-by-input.factory.spec.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Test, type TestingModule } from '@nestjs/testing'; - -import { OrderByDirection } from 'twenty-shared/types'; - -import { - objectMetadataMapItemMock, - objectMetadataMapsMock, -} from 'src/engine/api/__mocks__/object-metadata-item.mock'; -import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory'; -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'; - -describe('OrderByInputFactory', () => { - const objectMetadata: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - } = { - objectMetadataMaps: objectMetadataMapsMock, - objectMetadataMapItem: objectMetadataMapItemMock, - }; - - let service: OrderByInputFactory; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [OrderByInputFactory], - }).compile(); - - service = module.get(OrderByInputFactory); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('create', () => { - it('should return default if order by missing', () => { - const request: any = { query: {} }; - - expect(service.create(request, objectMetadata)).toEqual([ - {}, - { id: OrderByDirection.AscNullsFirst }, - ]); - }); - - it('should create order by parser properly', () => { - const request: any = { - query: { - order_by: 'fieldNumber[AscNullsFirst],fieldText[DescNullsLast]', - }, - }; - - expect(service.create(request, objectMetadata)).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(service.create(request, objectMetadata)).toEqual([ - { fieldNumber: OrderByDirection.AscNullsFirst }, - { id: OrderByDirection.AscNullsFirst }, - ]); - }); - - it('should handle complex fields', () => { - const request: any = { - query: { - order_by: 'fieldCurrency.amountMicros', - }, - }; - - expect(service.create(request, objectMetadata)).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(service.create(request, objectMetadata)).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(service.create(request, objectMetadata)).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(() => service.create(request, objectMetadata)).toThrow( - "'order_by' direction 'invalid' invalid. Allowed values are 'AscNullsFirst', 'AscNullsLast', 'DescNullsFirst', 'DescNullsLast'. eg: ?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3", - ); - }); - - it('should throw if field invalid', () => { - const request: any = { - query: { - order_by: 'wrongField[DescNullsLast]', - }, - }; - - expect(() => service.create(request, objectMetadata)).toThrow( - "field 'wrongField' does not exist in 'objectName' object", - ); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/ending-before-input.factory.ts b/packages/twenty-server/src/engine/api/rest/input-factories/ending-before-input.factory.ts deleted file mode 100644 index e7e7550896..0000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/ending-before-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 EndingBeforeInputFactory { - create(request: 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/input-factories/factories.ts b/packages/twenty-server/src/engine/api/rest/input-factories/factories.ts deleted file mode 100644 index 335ff460d3..0000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/factories.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory'; -import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory'; - -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 deleted file mode 100644 index 07a7c47c60..0000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/filter-input.factory.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type Request } from 'express'; - -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'; - -@Injectable() -export class FilterInputFactory { - create( - request: Request, - objectMetadata: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - }, - ): Record { - let filterQuery = request.query.filter; - - if (typeof filterQuery !== 'string') { - return {}; - } - - checkFilterQuery(filterQuery); - - filterQuery = addDefaultConjunctionIfMissing(filterQuery); - - return parseFilter(filterQuery, objectMetadata.objectMetadataMapItem); - } -} diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts b/packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts deleted file mode 100644 index c9836e69c8..0000000000 --- a/packages/twenty-server/src/engine/api/rest/input-factories/order-by-input.factory.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; - -import { type Request } from 'express'; -import { OrderByDirection } from 'twenty-shared/types'; - -import { ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; - -import { checkFields } from 'src/engine/api/rest/core/query-builder/utils/check-fields.utils'; -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'; - -export const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst; - -@Injectable() -export class OrderByInputFactory { - create( - request: Request, - objectMetadata: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - }, - ): ObjectRecordOrderBy { - const orderByQuery = request.query.order_by; - - if (typeof orderByQuery !== 'string') { - return this.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 [fieldsString, direction] = orderByItem - .replace(']', '') - .split('['); - - // 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 = fieldsString; - } 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]; - } - - checkFields( - objectMetadata.objectMetadataMapItem, - result.flatMap((fields) => Object.keys(fields)), - ); - - return this.addDefaultOrderById(result); - } - - 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/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.util.spec.ts similarity index 77% rename from packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-filter-without-metdata-validation.util.spec.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/parse-filter.util.spec.ts index f1944c9dfd..4f0355f19d 100644 --- 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.util.spec.ts @@ -1,19 +1,15 @@ -import { parseFilterWithoutMetadataValidation } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util'; +import { parseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter.util'; -describe('parseFilterWithoutMetadataValidation', () => { +describe('parseFilter', () => { it('should parse string filter test 1', () => { - expect( - parseFilterWithoutMetadataValidation( - 'and(fieldNumber[eq]:1,fieldNumber[eq]:2)', - ), - ).toEqual({ + expect(parseFilter('and(fieldNumber[eq]:1,fieldNumber[eq]:2)')).toEqual({ and: [{ fieldNumber: { eq: '1' } }, { fieldNumber: { eq: '2' } }], }); }); it('should parse string filter test 2', () => { expect( - parseFilterWithoutMetadataValidation( + parseFilter( 'and(fieldNumber[eq]:1,or(fieldNumber[eq]:2,fieldNumber[eq]:3))', ), ).toEqual({ @@ -26,7 +22,7 @@ describe('parseFilterWithoutMetadataValidation', () => { it('should parse string filter test 3', () => { expect( - parseFilterWithoutMetadataValidation( + parseFilter( '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({ @@ -48,7 +44,7 @@ describe('parseFilterWithoutMetadataValidation', () => { it('should parse string filter test 4', () => { expect( - parseFilterWithoutMetadataValidation( + parseFilter( '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({ @@ -73,9 +69,7 @@ describe('parseFilterWithoutMetadataValidation', () => { it('should handle not', () => { expect( - parseFilterWithoutMetadataValidation( - 'and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))', - ), + parseFilter('and(fieldNumber[eq]:1,not(fieldNumber[eq]:2))'), ).toEqual({ and: [ { fieldNumber: { eq: '1' } }, diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util.ts index f30bf375d2..8fde997cd9 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util.ts @@ -1,4 +1,4 @@ -import { Conjunctions } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils'; +import { Conjunctions } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter.util'; export const DEFAULT_CONJUNCTION = Conjunctions.and; 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 index e96da03b4e..e75eb68afc 100644 --- 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 @@ -2,7 +2,7 @@ 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 { parseFilter } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter.util'; import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; export const parseFilterRestRequest = ( @@ -18,5 +18,5 @@ export const parseFilterRestRequest = ( filterQuery = addDefaultConjunctionIfMissing(filterQuery); - return parseFilterWithoutMetadataValidation(filterQuery); + return parseFilter(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.util.ts similarity index 87% rename from packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-without-metadata-validation.util.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter.util.ts index 1c1ca18cb0..ed855861b7 100644 --- 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.util.ts @@ -1,6 +1,5 @@ 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'; @@ -10,8 +9,13 @@ import { RestInputRequestParserExceptionCode, } from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception'; -//TODO : Refacto-common - Rename after deleting parseFilter -export const parseFilterWithoutMetadataValidation = ( +export enum Conjunctions { + or = 'or', + and = 'and', + not = 'not', +} + +export const parseFilter = ( filterQuery: string, ): Record => { const result = {}; @@ -28,7 +32,7 @@ export const parseFilterWithoutMetadataValidation = ( ); } const subResult = parseFilterContent(filterQuery).map((elem) => - parseFilterWithoutMetadataValidation(elem), + parseFilter(elem), ); if (conjunction === Conjunctions.not) { 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 index 9cc632ab0b..deaa0a2be3 100644 --- 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 @@ -4,7 +4,6 @@ import { OrderByDirection } from 'twenty-shared/types'; import { type ObjectRecordOrderBy } 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 { RestInputRequestParserException, @@ -12,6 +11,8 @@ import { } from 'src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception'; import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; +export const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst; + export const parseOrderByRestRequest = ( request: AuthenticatedRequest, ): ObjectRecordOrderBy => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/__tests__/parse-core-path.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/path-parser-utils/__tests__/parse-core-path.utils.spec.ts similarity index 95% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/__tests__/parse-core-path.utils.spec.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/path-parser-utils/__tests__/parse-core-path.utils.spec.ts index e62e278f50..cbb1d24023 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/__tests__/parse-core-path.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/path-parser-utils/__tests__/parse-core-path.utils.spec.ts @@ -1,4 +1,4 @@ -import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils'; const testUUID = '20202020-ef5a-4822-9e08-cf6e4a4dcd6b'; diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils.ts similarity index 100% rename from packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils.ts rename to packages/twenty-server/src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils.ts diff --git a/packages/twenty-server/src/engine/api/rest/metadata/query-builder/factories/metadata-factories.ts b/packages/twenty-server/src/engine/api/rest/metadata/query-builder/factories/metadata-factories.ts index f8a7f48556..f17be6afd6 100644 --- a/packages/twenty-server/src/engine/api/rest/metadata/query-builder/factories/metadata-factories.ts +++ b/packages/twenty-server/src/engine/api/rest/metadata/query-builder/factories/metadata-factories.ts @@ -1,10 +1,9 @@ -import { FindOneMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/find-one-metadata-query.factory'; -import { FindManyMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/find-many-metadata-query.factory'; -import { GetMetadataVariablesFactory } from 'src/engine/api/rest/metadata/query-builder/factories/get-metadata-variables.factory'; -import { inputFactories } from 'src/engine/api/rest/input-factories/factories'; import { CreateMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/create-metadata-query.factory'; -import { UpdateMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/update-metadata-query.factory'; import { DeleteMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/delete-metadata-query.factory'; +import { FindManyMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/find-many-metadata-query.factory'; +import { FindOneMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/find-one-metadata-query.factory'; +import { GetMetadataVariablesFactory } from 'src/engine/api/rest/metadata/query-builder/factories/get-metadata-variables.factory'; +import { UpdateMetadataQueryFactory } from 'src/engine/api/rest/metadata/query-builder/factories/update-metadata-query.factory'; export const metadataQueryBuilderFactories = [ FindOneMetadataQueryFactory, @@ -13,5 +12,4 @@ export const metadataQueryBuilderFactories = [ DeleteMetadataQueryFactory, UpdateMetadataQueryFactory, GetMetadataVariablesFactory, - ...inputFactories, ]; diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index e3bbfd7a02..69e0f91ef6 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -19,6 +19,5 @@ export enum FeatureFlagKey { IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED', - IS_COMMON_API_ENABLED = 'IS_COMMON_API_ENABLED', IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED', } diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 7b8ee9e0af..cbbfb46f1b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -142,7 +142,6 @@ describe('WorkspaceEntityManager', () => { IS_PUBLIC_DOMAIN_ENABLED: false, IS_EMAILING_DOMAIN_ENABLED: false, IS_DYNAMIC_SEARCH_FIELDS_ENABLED: false, - IS_COMMON_API_ENABLED: false, IS_WORKFLOW_RUN_STOPPAGE_ENABLED: false, }, eventEmitterService: { @@ -174,7 +173,6 @@ describe('WorkspaceEntityManager', () => { IS_PUBLIC_DOMAIN_ENABLED: false, IS_EMAILING_DOMAIN_ENABLED: false, IS_DYNAMIC_SEARCH_FIELDS_ENABLED: false, - IS_COMMON_API_ENABLED: false, }, permissionsPerRoleId: {}, } as WorkspaceDataSource; diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index e0a3c34195..2fb5172bdf 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -61,11 +61,6 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: workspaceId === SEED_APPLE_WORKSPACE_ID, }, - { - key: FeatureFlagKey.IS_COMMON_API_ENABLED, - workspaceId: workspaceId, - value: false, - }, { key: FeatureFlagKey.IS_PAGE_LAYOUT_ENABLED, workspaceId: workspaceId, diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts index 765b4cbb15..39634ee9cc 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/filter-validation/constants/failing-filter-input-by-field-metadata-type.constant.ts @@ -24,7 +24,8 @@ export const failingFilterInputByFieldMetadataType: { gqlErrorMessage: 'is not defined by type', restFilterInput: 'manyToOneRelationField[eq]:"6dd71a46-68fe-4420-82b3-0d5b00ad2642"', - restErrorMessage: "'manyToOneRelationField' does not exist", + restErrorMessage: + 'column apiInputValidationTestObject.manyToOneRelationField does not exist', }, { gqlFilterInput: { @@ -45,7 +46,8 @@ export const failingFilterInputByFieldMetadataType: { gqlErrorMessage: 'is not defined by type', restFilterInput: 'oneToManyRelationFieldId[eq]:"6dd71a46-68fe-4420-82b3-0d5b00ad2642"', - restErrorMessage: "'oneToManyRelationFieldId' does not exist", + restErrorMessage: + 'Field metadata not found for field: oneToManyRelationFieldId', }, // { // gqlFilterInput: { @@ -314,31 +316,32 @@ export const failingFilterInputByFieldMetadataType: { gqlFilterInput: { multiSelectField: { eq: 'not-a-multi-select' } }, gqlErrorMessage: 'Value "not-a-multi-select" does not exist ', restFilterInput: 'multiSelectField[eq]:"not-a-multi-select"', - restErrorMessage: "not available in 'multiSelectField'", - }, - { - gqlFilterInput: { multiSelectField: { eq: {} } }, - gqlErrorMessage: 'cannot represent non-string value: {}.', - restFilterInput: 'multiSelectField[eq]:"{}"', - restErrorMessage: "not available in 'multiSelectField'", + restErrorMessage: 'malformed array literal', }, + // TODO - fix this, should throw + // { + // gqlFilterInput: { multiSelectField: { eq: {} } }, + // gqlErrorMessage: 'cannot represent non-string value: {}.', + // restFilterInput: 'multiSelectField[eq]:"{}"', + // restErrorMessage: "not available in 'multiSelectField'", + // }, { gqlFilterInput: { multiSelectField: { eq: [] } }, gqlErrorMessage: 'cannot represent non-string value: [].', restFilterInput: 'multiSelectField[eq]:"[]"', - restErrorMessage: "not available in 'multiSelectField'", + restErrorMessage: 'malformed array literal', }, { gqlFilterInput: { multiSelectField: { eq: true } }, gqlErrorMessage: 'cannot represent non-string value: true.', restFilterInput: 'multiSelectField[eq]:"true"', - restErrorMessage: "not available in 'multiSelectField'", + restErrorMessage: 'malformed array literal', }, { gqlFilterInput: { multiSelectField: { eq: 2 } }, gqlErrorMessage: 'cannot represent non-string value: 2.', restFilterInput: 'multiSelectField[eq]:2', - restErrorMessage: "enum value '2' not available in 'multiSelectField' ", + restErrorMessage: 'malformed array literal', }, // TODO - ensure it should throw // { diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-many.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-many.integration-spec.ts index eb9eb16986..cdc809494b 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-many.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-many.integration-spec.ts @@ -220,7 +220,9 @@ describe('Core REST API Create Many endpoint', () => { }) .expect(400) .expect((res) => { - expect(res.body.messages[0]).toContain(`Record already exists`); + expect(res.body.messages[0]).toContain( + `A duplicate entry was detected`, + ); expect(res.body.error).toBe('BadRequestException'); }); }); diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts index 9f6b39fb5f..c9644e750b 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts @@ -188,7 +188,9 @@ describe('Core REST API Create One endpoint', () => { }) .expect(400) .expect((res) => { - expect(res.body.messages[0]).toContain(`Record already exists`); + expect(res.body.messages[0]).toContain( + `A duplicate entry was detected`, + ); expect(res.body.error).toBe('BadRequestException'); }); }); diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts index a928c4a094..7d64c27cd7 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts @@ -56,8 +56,8 @@ describe('Core REST API Delete One endpoint', () => { path: `/people/${NOT_EXISTING_TEST_PERSON_ID}`, }); - expect(response.status).toBe(400); - expect(response.body.error).toBe('EntityNotFoundError'); - // expect(response.body.messages[0]).toBe('Record not found'); + expect(response.status).toBe(404); + expect(response.body.error).toBe('NotFoundException'); + expect(response.body.messages[0]).toBe('Record not found'); }); }); diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-one.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-one.integration-spec.ts index 63309f045c..90f51beb50 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-one.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-find-one.integration-spec.ts @@ -57,18 +57,15 @@ describe('Core REST API Find One endpoint', () => { }); }); - it('should return 400 error when trying to retrieve a non-existing person', async () => { - await makeRestAPIRequest({ + it('should return 404 error when trying to retrieve a non-existing person', async () => { + const response = await makeRestAPIRequest({ method: 'get', path: `/people/${NOT_EXISTING_TEST_PERSON_ID}`, - }) - //TODO : Refacto-common - This should be a 404 - .expect(400) - .expect((res) => { - expect(res.body.messages[0]).toContain('Record not found'); - //TODO : Refacto-common - This should be a NotFoundException - expect(res.body.error).toBe('BadRequestException'); - }); + }); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('NotFoundException'); + expect(response.body.messages[0]).toBe('Record not found'); }); it('should return 400 error when trying to retrieve with malformed uuid', async () => { diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-update.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-update.integration-spec.ts index 5cc32c563b..c07743c77b 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-update.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-update.integration-spec.ts @@ -112,8 +112,8 @@ describe('Core REST API Update One endpoint', () => { path: `/people/${NOT_EXISTING_TEST_PERSON_ID}`, }); - expect(response.status).toBe(400); - expect(response.body.error).toBe('EntityNotFoundError'); - // expect(response.body.message).toBe('Record not found'); + expect(response.status).toBe(404); + expect(response.body.error).toBe('NotFoundException'); + expect(response.body.messages[0]).toBe('Record not found'); }); });