diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index 8b955f573b..30f6191518 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -1,17 +1,33 @@ import { Inject, Injectable } from '@nestjs/common'; -import { type ObjectRecord } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { Omit } from 'zod/v4/core/util.cjs'; import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; +import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value'; import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service'; -import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; +import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; +import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; +import { + CommonExtendedInput, + CommonInput, + CommonQueryArgs, + CommonQueryNames, +} from 'src/engine/api/common/types/common-query-args.type'; +import { CommonQueryResult } from 'src/engine/api/common/types/common-query-result.type'; +import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util'; import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements'; +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 { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type'; 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 { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; @@ -29,7 +45,10 @@ import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/wo import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() -export abstract class CommonBaseQueryRunnerService { +export abstract class CommonBaseQueryRunnerService< + Args extends CommonQueryArgs, + Output extends CommonQueryResult, +> { @Inject() protected readonly workspaceQueryHookService: WorkspaceQueryHookService; @Inject() @@ -53,82 +72,147 @@ export abstract class CommonBaseQueryRunnerService { @Inject() protected readonly commonResultGettersService: CommonResultGettersService; - public async prepareQueryRunnerContext({ - authContext, - objectMetadataItemWithFieldMaps, - }: { - authContext: WorkspaceAuthContext; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - }) { - if (objectMetadataItemWithFieldMaps.isSystem === true) { - await this.validateSettingsPermissionsOnObjectOrThrow( - authContext, - objectMetadataItemWithFieldMaps, + public async execute( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + operationName: CommonQueryNames, + ): Promise { + const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = + queryRunnerContext; + + if (!isWorkspaceAuthContext(authContext)) { + throw new CommonQueryRunnerException( + 'Invalid auth context', + CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, ); } - const workspace = authContext.workspace; + await this.validate(args, queryRunnerContext); - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: workspace.id, - }); + if (objectMetadataItemWithFieldMaps.isSystem === true) { + await this.validateSettingsPermissionsOnObjectOrThrow( + authContext, + queryRunnerContext, + ); + } - const { roleId } = await this.getRoleIdAndObjectsPermissions( - authContext, - workspace.id, + const commonQueryParser = new GraphqlQueryParser( + objectMetadataItemWithFieldMaps, + objectMetadataMaps, ); - const rolePermissionConfig = { unionOf: [roleId] }; - - const repository = workspaceDataSource.getRepository( - objectMetadataItemWithFieldMaps.nameSingular, - rolePermissionConfig, - authContext, + const processedArgs = await this.processArgs( + args, + queryRunnerContext, + operationName, + commonQueryParser, ); + const extendedQueryRunnerContext = + await this.prepareExtendedQueryRunnerContext( + authContext, + queryRunnerContext, + ); + + const results = await this.run(processedArgs, { + ...extendedQueryRunnerContext, + commonQueryParser, + }); + + return this.enrichResultsWithGettersAndHooks({ + results, + operationName, + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + }); + } + + protected abstract run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise; + + protected abstract validate( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise; + + protected abstract computeArgs( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise>; + + protected abstract processQueryResult( + queryResult: Output, + objectMetadataItemId: string, + objectMetadataMaps: ObjectMetadataMaps, + authContext: WorkspaceAuthContext, + ): Promise; + + private async processArgs( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + operationName: CommonQueryNames, + commonQueryParser: GraphqlQueryParser, + ): Promise> { + const selectedFieldsResult = commonQueryParser.parseSelectedFields( + args.selectedFields, + ); + + const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const hookedArgs = + (await this.workspaceQueryHookService.executePreQueryHooks( + authContext, + objectMetadataItemWithFieldMaps.nameSingular, + operationName, + args as WorkspacePreQueryHookPayload, + )) as CommonInput; + + const computedArgs = await this.computeArgs(hookedArgs, queryRunnerContext); + return { - workspaceDataSource, - repository, - rolePermissionConfig, + ...computedArgs, + selectedFieldsResult, }; } - public async enrichResultsWithGettersAndHooks({ + private async enrichResultsWithGettersAndHooks({ results, operationName, authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps, }: { - results: ObjectRecord[]; + results: Output; operationName: CommonQueryNames; authContext: WorkspaceAuthContext; objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; objectMetadataMaps: ObjectMetadataMaps; - }): Promise { - const resultWithGetters = - await this.commonResultGettersService.processQueryResult( - results, - objectMetadataItemWithFieldMaps.id, - objectMetadataMaps, - authContext.workspace.id, - ); + }): Promise { + const resultWithGetters = await this.processQueryResult( + results, + objectMetadataItemWithFieldMaps.id, + objectMetadataMaps, + authContext, + ); await this.workspaceQueryHookService.executePostQueryHooks( authContext, objectMetadataItemWithFieldMaps.nameSingular, operationName, - resultWithGetters, + resultWithGetters as QueryResultFieldValue, ); - return resultWithGetters; + return resultWithGetters as Output; } private async validateSettingsPermissionsOnObjectOrThrow( authContext: WorkspaceAuthContext, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + queryRunnerContext: CommonBaseQueryRunnerContext, ) { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const workspace = authContext.workspace; if ( @@ -206,4 +290,35 @@ export abstract class CommonBaseQueryRunnerService { return { roleId, objectsPermissions: objectMetadataPermissions[roleId] }; } + + private async prepareExtendedQueryRunnerContext( + authContext: WorkspaceAuthContext, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId: authContext.workspace.id, + }); + + const { roleId } = await this.getRoleIdAndObjectsPermissions( + authContext, + authContext.workspace.id, + ); + + const rolePermissionConfig = { unionOf: [roleId] }; + + const repository = workspaceDataSource.getRepository( + queryRunnerContext.objectMetadataItemWithFieldMaps.nameSingular, + rolePermissionConfig, + authContext, + ); + + return { + ...queryRunnerContext, + authContext, + workspaceDataSource, + rolePermissionConfig, + repository, + }; + } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts index cb8d5f5073..1c643203ad 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts @@ -16,13 +16,14 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; +import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { - CommonQueryNames, + CommonExtendedInput, + CommonInput, CreateManyQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type'; -import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util'; -import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { 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'; @@ -36,63 +37,28 @@ import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace. import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @Injectable() -export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerService { - async run({ - args, - authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, - }: { - args: CreateManyQueryArgs; - authContext: AuthContext; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - }): Promise { - if (!isWorkspaceAuthContext(authContext)) { - throw new CommonQueryRunnerException( - 'Invalid auth context', - CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, - ); - } - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); - - // TODO : Refacto-common - Remove this validation once https://github.com/twentyhq/core-team-issues/issues/1622 done - args.data.forEach((record) => { - if (record?.id) { - assertIsValidUuid(record.id); - } - }); - - //TODO : Refacto-common - QueryParser should be common branded service - const commonQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - ); - - const selectedFieldsResult = commonQueryParser.parseSelectedFields( - objectMetadataItemWithFieldMaps, - args.selectedFields, - objectMetadataMaps, - ); - - const { workspaceDataSource, repository, rolePermissionConfig } = - await this.prepareQueryRunnerContext({ - authContext, - objectMetadataItemWithFieldMaps, - }); - - const processedArgs = await this.processQueryArgs({ +export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerService< + CreateManyQueryArgs, + ObjectRecord[] +> { + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const { + repository, authContext, + rolePermissionConfig, objectMetadataItemWithFieldMaps, - args, - }); + objectMetadataMaps, + workspaceDataSource, + } = queryRunnerContext; const objectRecords = await this.insertOrUpsertRecords({ repository, objectMetadataItemWithFieldMaps, objectMetadataMaps, - args: processedArgs, - selectedFieldsResult, + args, }); const upsertedRecords = await this.fetchUpsertedRecords({ @@ -100,63 +66,103 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer objectMetadataItemWithFieldMaps, objectMetadataMaps, repository, - selectedFieldsResult, + selectedFieldsResult: args.selectedFieldsResult, }); await this.processNestedRelationsIfNeeded({ + args, records: upsertedRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, - rolePermissionConfig, authContext, workspaceDataSource, - selectedFieldsResult, + rolePermissionConfig, }); return upsertedRecords; } - async processQueryArgs({ - authContext, - objectMetadataItemWithFieldMaps, + private async processNestedRelationsIfNeeded({ args, + records, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + authContext, + workspaceDataSource, + rolePermissionConfig, }: { - authContext: WorkspaceAuthContext; + args: CommonExtendedInput; + records: ObjectRecord[]; objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - args: CreateManyQueryArgs; - }): Promise { - const hookedArgs = - (await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItemWithFieldMaps.nameSingular, - CommonQueryNames.CREATE_MANY, - args, - //TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency - )) as CreateManyQueryArgs; + objectMetadataMaps: ObjectMetadataMaps; + authContext: AuthContext; + workspaceDataSource: WorkspaceDataSource; + rolePermissionConfig?: RolePermissionConfig; + }): Promise { + if (!args.selectedFieldsResult.relations) { + return; + } + + await this.processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: records, + relations: args.selectedFieldsResult.relations as Record< + string, + FindOptionsRelations + >, + limit: QUERY_MAX_RECORDS, + authContext, + workspaceDataSource, + rolePermissionConfig, + selectedFields: args.selectedFieldsResult.select, + }); + } + + async computeArgs( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; return { - ...hookedArgs, + ...args, data: await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({ - partialRecordInputs: hookedArgs.data, + partialRecordInputs: args.data, authContext, objectMetadataItemWithFieldMaps, }), }; } + async validate( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + + assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + + args.data.forEach((record) => { + if (record?.id) { + assertIsValidUuid(record.id); + } + }); + } + private async insertOrUpsertRecords({ repository, objectMetadataItemWithFieldMaps, objectMetadataMaps, args, - selectedFieldsResult, }: { repository: WorkspaceRepository; objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; objectMetadataMaps: ObjectMetadataMaps; - args: CreateManyQueryArgs; - selectedFieldsResult: CommonSelectedFieldsResult; + args: CommonExtendedInput; }): Promise { + const { selectedFieldsResult } = args; + if (!args.upsert) { const selectedColumns = buildColumnsToReturn({ select: selectedFieldsResult.select, @@ -380,42 +386,18 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer return upsertedRecords as ObjectRecord[]; } - private async processNestedRelationsIfNeeded({ - records, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - rolePermissionConfig, - authContext, - workspaceDataSource, - selectedFieldsResult, - }: { - records: ObjectRecord[]; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; - authContext: AuthContext; - workspaceDataSource: WorkspaceDataSource; - rolePermissionConfig?: RolePermissionConfig; - selectedFieldsResult: CommonSelectedFieldsResult; - }): Promise { - if (!selectedFieldsResult.relations) { - return; - } - - await this.processNestedRelationsHelper.processNestedRelations({ + async processQueryResult( + queryResult: ObjectRecord[], + objectMetadataItemId: string, + objectMetadataMaps: ObjectMetadataMaps, + authContext: WorkspaceAuthContext, + ): Promise { + return await this.commonResultGettersService.processRecordArray( + queryResult, + objectMetadataItemId, objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: records, - //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common - relations: selectedFieldsResult.relations as Record< - string, - FindOptionsRelations - >, - limit: QUERY_MAX_RECORDS, - authContext, - workspaceDataSource, - rolePermissionConfig, - selectedFields: selectedFieldsResult.select, - }); + authContext.workspace.id, + ); } private getRecordWithoutCreatedBy( diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts index 643b4ccf0b..3d0612d364 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts @@ -2,42 +2,74 @@ import { Injectable } from '@nestjs/common'; import { type ObjectRecord } from 'twenty-shared/types'; +import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; + import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service'; import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service'; -import { CreateOneQueryArgs } from 'src/engine/api/common/types/common-query-args.type'; -import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; +import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; +import { + CommonExtendedInput, + CommonInput, + CreateManyQueryArgs, + CreateOneQueryArgs, +} from 'src/engine/api/common/types/common-query-args.type'; +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'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() -export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerService { +export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerService< + CreateOneQueryArgs, + ObjectRecord +> { constructor( private readonly commonCreateManyQueryRunnerService: CommonCreateManyQueryRunnerService, ) { super(); } - async run({ - args, - authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, - }: { - args: CreateOneQueryArgs; - authContext: AuthContext; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - }): Promise { - const result = await this.commonCreateManyQueryRunnerService.run({ - args: { + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const result = await this.commonCreateManyQueryRunnerService.run( + { ...args, data: [args.data], }, - authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, - }); + queryRunnerContext, + ); return result[0]; } + + async computeArgs( + args: CommonInput, + _queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + return args; + } + + async processQueryResult( + queryResult: ObjectRecord, + _objectMetadataItemId: string, + _objectMetadataMaps: ObjectMetadataMaps, + _authContext: WorkspaceAuthContext, + ): Promise { + return queryResult; + } + + async validate( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + + assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + + if (args.data?.id) { + assertIsValidUuid(args.data.id); + } + } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts new file mode 100644 index 0000000000..fc5cd85c6f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts @@ -0,0 +1,142 @@ +import { Injectable } from '@nestjs/common'; + +import { isDefined } from 'class-validator'; +import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; +import { ObjectRecord } from 'twenty-shared/types'; +import { FindOptionsRelations, ObjectLiteral } from 'typeorm'; + +import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; + +import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; +import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; +import { + CommonExtendedInput, + CommonInput, + DeleteManyQueryArgs, +} from 'src/engine/api/common/types/common-query-args.type'; +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'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { computeTableName } from 'src/engine/utils/compute-table-name.util'; + +@Injectable() +export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerService< + DeleteManyQueryArgs, + ObjectRecord[] +> { + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const { + repository, + authContext, + rolePermissionConfig, + workspaceDataSource, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + commonQueryParser, + } = queryRunnerContext; + + const queryBuilder = repository.createQueryBuilder( + objectMetadataItemWithFieldMaps.nameSingular, + ); + + const tableName = computeTableName( + objectMetadataItemWithFieldMaps.nameSingular, + objectMetadataItemWithFieldMaps.isCustom, + ); + + commonQueryParser.applyFilterToBuilder( + queryBuilder, + tableName, + args.filter, + ); + + const columnsToReturn = buildColumnsToReturn({ + select: args.selectedFieldsResult.select, + relations: args.selectedFieldsResult.relations, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + }); + + const deletedObjectRecords = await queryBuilder + .softDelete() + .returning(columnsToReturn) + .execute(); + + const deletedRecords = deletedObjectRecords.generatedMaps as ObjectRecord[]; + + if (isDefined(args.selectedFieldsResult.relations)) { + await this.processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: deletedRecords, + //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common + relations: args.selectedFieldsResult.relations as Record< + string, + FindOptionsRelations + >, + limit: QUERY_MAX_RECORDS, + authContext, + workspaceDataSource, + rolePermissionConfig, + selectedFields: args.selectedFieldsResult.select, + }); + } + + return deletedRecords; + } + + async computeArgs( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + + return { + ...args, + filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( + args.filter, + objectMetadataItemWithFieldMaps, + ), + }; + } + + async processQueryResult( + queryResult: ObjectRecord[], + objectMetadataItemId: string, + objectMetadataMaps: ObjectMetadataMaps, + authContext: WorkspaceAuthContext, + ): Promise { + return this.commonResultGettersService.processRecordArray( + queryResult, + objectMetadataItemId, + objectMetadataMaps, + authContext.workspace.id, + ); + } + + async validate( + args: DeleteManyQueryArgs, + queryRunnerContext: CommonBaseQueryRunnerContext, + ) { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + + assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + if (!args.filter) { + throw new CommonQueryRunnerException( + 'Filter is required', + CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + + args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id)); + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts new file mode 100644 index 0000000000..3879990bde --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts @@ -0,0 +1,83 @@ +import { Injectable } from '@nestjs/common'; + +import { type ObjectRecord } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; + +import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service'; +import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; +import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; +import { + CommonExtendedInput, + CommonInput, + DeleteOneQueryArgs, +} from 'src/engine/api/common/types/common-query-args.type'; +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'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; + +@Injectable() +export class CommonDeleteOneQueryRunnerService extends CommonBaseQueryRunnerService< + DeleteOneQueryArgs, + ObjectRecord +> { + constructor( + private readonly commonDeleteManyQueryRunnerService: CommonDeleteManyQueryRunnerService, + ) { + super(); + } + + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const result = await this.commonDeleteManyQueryRunnerService.run( + { + ...args, + filter: { id: { eq: args.id } }, + }, + queryRunnerContext, + ); + + if (!isDefined(result) || result.length === 0) { + throw new CommonQueryRunnerException( + 'Record not found', + CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + ); + } + + return result[0]; + } + + async computeArgs( + args: CommonInput, + _queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + return args; + } + + async processQueryResult( + queryResult: ObjectRecord, + _objectMetadataItemId: string, + _objectMetadataMaps: ObjectMetadataMaps, + _authContext: WorkspaceAuthContext, + ): Promise { + return queryResult; + } + + async validate( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + + assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertIsValidUuid(args.id); + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts new file mode 100644 index 0000000000..aa210077b6 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts @@ -0,0 +1,129 @@ +import { Injectable } from '@nestjs/common'; + +import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; +import { ObjectRecord } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { FindOptionsRelations, ObjectLiteral } from 'typeorm'; + +import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; + +import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; +import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; +import { + CommonExtendedInput, + CommonInput, + DestroyManyQueryArgs, +} from 'src/engine/api/common/types/common-query-args.type'; +import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; + +@Injectable() +export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerService< + DestroyManyQueryArgs, + ObjectRecord[] +> { + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const { + repository, + authContext, + rolePermissionConfig, + workspaceDataSource, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + commonQueryParser, + } = queryRunnerContext; + + const queryBuilder = repository.createQueryBuilder( + objectMetadataItemWithFieldMaps.nameSingular, + ); + + commonQueryParser.applyFilterToBuilder( + queryBuilder, + objectMetadataItemWithFieldMaps.nameSingular, + args.filter, + ); + + const columnsToReturn = buildColumnsToReturn({ + select: args.selectedFieldsResult.select, + relations: args.selectedFieldsResult.relations, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + }); + + const deletedObjectRecords = await queryBuilder + .delete() + .returning(columnsToReturn) + .execute(); + + const deletedRecords = deletedObjectRecords.generatedMaps as ObjectRecord[]; + + if (isDefined(args.selectedFieldsResult.relations)) { + await this.processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: deletedRecords, + //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common + relations: args.selectedFieldsResult.relations as Record< + string, + FindOptionsRelations + >, + limit: QUERY_MAX_RECORDS, + authContext, + workspaceDataSource, + rolePermissionConfig, + selectedFields: args.selectedFieldsResult.select, + }); + } + + return deletedRecords; + } + + async computeArgs( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + + return { + ...args, + filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( + args.filter, + objectMetadataItemWithFieldMaps, + ), + }; + } + + async processQueryResult( + queryResult: ObjectRecord[], + objectMetadataItemId: string, + objectMetadataMaps: ObjectMetadataMaps, + authContext: WorkspaceAuthContext, + ): Promise { + return this.commonResultGettersService.processRecordArray( + queryResult, + objectMetadataItemId, + objectMetadataMaps, + authContext.workspace.id, + ); + } + + async validate( + args: CommonInput, + _queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise { + if (!isDefined(args.filter)) { + throw new CommonQueryRunnerException( + 'Filter is required', + CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts new file mode 100644 index 0000000000..9b8b7b1a85 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts @@ -0,0 +1,83 @@ +import { Injectable } from '@nestjs/common'; + +import { type ObjectRecord } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; + +import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service'; +import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; +import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; +import { + CommonExtendedInput, + CommonInput, + DestroyOneQueryArgs, +} from 'src/engine/api/common/types/common-query-args.type'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; + +@Injectable() +export class CommonDestroyOneQueryRunnerService extends CommonBaseQueryRunnerService< + DestroyOneQueryArgs, + ObjectRecord +> { + constructor( + private readonly commonDestroyManyQueryRunnerService: CommonDestroyManyQueryRunnerService, + ) { + super(); + } + + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const result = await this.commonDestroyManyQueryRunnerService.run( + { + ...args, + filter: { id: { eq: args.id } }, + }, + queryRunnerContext, + ); + + if (!isDefined(result) || result.length === 0) { + throw new CommonQueryRunnerException( + 'Record not found', + CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + ); + } + + return result[0]; + } + + async computeArgs( + args: CommonInput, + _queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + return args; + } + + async processQueryResult( + queryResult: ObjectRecord, + _objectMetadataItemId: string, + _objectMetadataMaps: ObjectMetadataMaps, + _authContext: WorkspaceAuthContext, + ): Promise { + return queryResult; + } + + async validate( + args: CommonInput, + _queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise { + if (!isDefined(args.id)) { + throw new CommonQueryRunnerException( + 'Missing id', + CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts index fbed5f6490..e2c03af601 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts @@ -16,73 +16,39 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; -import { CommonPageInfo } from 'src/engine/api/common/types/common-page-info.type'; +import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; +import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; +import { CommonFindManyOutput } from 'src/engine/api/common/types/common-find-many-output.type'; import { - CommonQueryNames, + CommonExtendedInput, + CommonInput, FindManyQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; -import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type'; import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util'; -import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util'; -import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; import { getCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; -import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() -export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService { - async run({ - args, - authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, - }: { - args: FindManyQueryArgs; - authContext: AuthContext; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - }): Promise<{ - records: ObjectRecord[]; - aggregatedValues: Record; - totalCount: number; - pageInfo: CommonPageInfo; - selectedFieldsResult: CommonSelectedFieldsResult; - }> { - this.validate(args); - - if (!isWorkspaceAuthContext(authContext)) { - throw new CommonQueryRunnerException( - 'Invalid auth context', - CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, - ); - } - - const { workspaceDataSource, repository, rolePermissionConfig } = - await this.prepareQueryRunnerContext({ - authContext, - objectMetadataItemWithFieldMaps, - }); - - const commonQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - ); - - const selectedFieldsResult = commonQueryParser.parseSelectedFields( - objectMetadataItemWithFieldMaps, - args.selectedFields, - objectMetadataMaps, - ); - - const processedArgs = await this.processQueryArgs({ +export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService< + FindManyQueryArgs, + CommonFindManyOutput +> { + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const { + repository, authContext, + rolePermissionConfig, objectMetadataItemWithFieldMaps, - args, - }); + objectMetadataMaps, + workspaceDataSource, + commonQueryParser, + } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, @@ -90,7 +56,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi const aggregateQueryBuilder = queryBuilder.clone(); - let appliedFilters = processedArgs.filter ?? ({} as ObjectRecordFilter); + let appliedFilters = args.filter ?? ({} as ObjectRecordFilter); commonQueryParser.applyFilterToBuilder( aggregateQueryBuilder, @@ -104,13 +70,13 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi ); const orderByWithIdCondition = [ - ...(processedArgs.orderBy ?? []), + ...(args.orderBy ?? []), { id: OrderByDirection.AscNullsFirst }, ] as ObjectRecordOrderBy; - const isForwardPagination = !isDefined(processedArgs.before); + const isForwardPagination = !isDefined(args.before); - const cursor = getCursor(processedArgs); + const cursor = getCursor(args); if (cursor) { const cursorArgFilter = computeCursorArgFilter( @@ -120,9 +86,9 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi isForwardPagination, ); - appliedFilters = (processedArgs.filter + appliedFilters = (args.filter ? { - and: [processedArgs.filter, { or: cursorArgFilter }], + and: [args.filter, { or: cursorArgFilter }], } : { or: cursorArgFilter }) as unknown as ObjectRecordFilter; } @@ -143,17 +109,16 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters); ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({ - selectedAggregatedFields: selectedFieldsResult.aggregate, + selectedAggregatedFields: args.selectedFieldsResult.aggregate, queryBuilder: aggregateQueryBuilder, objectMetadataNameSingular: objectMetadataItemWithFieldMaps.nameSingular, }); - const limit = - processedArgs.first ?? processedArgs.last ?? QUERY_MAX_RECORDS; + const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS; const columnsToSelect = buildColumnsToSelect({ - select: selectedFieldsResult.select, - relations: selectedFieldsResult.relations, + select: args.selectedFieldsResult.select, + relations: args.selectedFieldsResult.relations, objectMetadataItemWithFieldMaps, objectMetadataMaps, }); @@ -183,44 +148,74 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi const parentObjectRecordsAggregatedValues = await aggregateQueryBuilder.getRawOne(); - if (selectedFieldsResult.relations) { + if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: objectRecords, parentObjectRecordsAggregatedValues, //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common - relations: selectedFieldsResult.relations as Record< + relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations >, - aggregate: selectedFieldsResult.aggregate, + aggregate: args.selectedFieldsResult.aggregate, limit: QUERY_MAX_RECORDS, authContext, workspaceDataSource, rolePermissionConfig, - selectedFields: selectedFieldsResult.select, + selectedFields: args.selectedFieldsResult.select, }); } - const enrichedRecords = await this.enrichResultsWithGettersAndHooks({ - results: objectRecords, - operationName: CommonQueryNames.FIND_MANY, - authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - }); - return { - records: enrichedRecords, + records: objectRecords, aggregatedValues: parentObjectRecordsAggregatedValues, totalCount: parentObjectRecordsAggregatedValues?.totalCount, pageInfo, - selectedFieldsResult, + selectedFieldsResult: args.selectedFieldsResult, }; } - validate(args: FindManyQueryArgs) { + async computeArgs( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + + return { + ...args, + filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( + args.filter, + objectMetadataItemWithFieldMaps, + ), + }; + } + + async processQueryResult( + queryResult: CommonFindManyOutput, + objectMetadataItemId: string, + objectMetadataMaps: ObjectMetadataMaps, + authContext: WorkspaceAuthContext, + ): Promise { + const processedRecords = + await this.commonResultGettersService.processRecordArray( + queryResult.records, + objectMetadataItemId, + objectMetadataMaps, + authContext.workspace.id, + ); + + return { + ...queryResult, + records: processedRecords, + }; + } + + async validate( + args: CommonInput, + _queryRunnerContext: CommonBaseQueryRunnerContext, + ) { if (args.first && args.last) { throw new CommonQueryRunnerException( 'Cannot provide both first and last', @@ -258,31 +253,4 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi ); } } - - async processQueryArgs({ - authContext, - objectMetadataItemWithFieldMaps, - args, - }: { - authContext: WorkspaceAuthContext; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - args: FindManyQueryArgs; - }): Promise { - const hookedArgs = - (await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItemWithFieldMaps.nameSingular, - CommonQueryNames.FIND_MANY, - args, - //TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency - )) as FindManyQueryArgs; - - return { - ...hookedArgs, - filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - hookedArgs.filter, - objectMetadataItemWithFieldMaps, - ), - }; - } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts index 9768e3eddf..380a0e7d66 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts @@ -13,70 +13,34 @@ import { CommonQueryRunnerException, CommonQueryRunnerExceptionCode, } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; +import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { - CommonQueryNames, + CommonExtendedInput, + CommonInput, FindOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; -import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util'; -import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; -import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() -export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService { - async run({ - args, - authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, - }: { - args: FindOneQueryArgs; - authContext: AuthContext; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - }): Promise { - if (!isWorkspaceAuthContext(authContext)) { - throw new CommonQueryRunnerException( - 'Invalid auth context', - CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, - ); - } - - const { workspaceDataSource, repository, rolePermissionConfig } = - await this.prepareQueryRunnerContext({ - authContext, - objectMetadataItemWithFieldMaps, - }); - - //TODO : Refacto-common - QueryParser should be common branded service - const commonQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - ); - - const selectedFieldsResult = commonQueryParser.parseSelectedFields( - objectMetadataItemWithFieldMaps, - args.selectedFields, - objectMetadataMaps, - ); - - const processedArgs = await this.processQueryArgs({ +export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService< + FindOneQueryArgs, + ObjectRecord +> { + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const { + repository, authContext, + rolePermissionConfig, + workspaceDataSource, + objectMetadataMaps, objectMetadataItemWithFieldMaps, - args, - }); - - if ( - !processedArgs.filter || - Object.keys(processedArgs.filter).length === 0 - ) { - throw new CommonQueryRunnerException( - 'Missing filter argument', - CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } + commonQueryParser, + } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, @@ -85,17 +49,17 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic commonQueryParser.applyFilterToBuilder( queryBuilder, objectMetadataItemWithFieldMaps.nameSingular, - processedArgs.filter ?? ({} as ObjectRecordFilter), + args.filter ?? ({} as ObjectRecordFilter), ); commonQueryParser.applyDeletedAtToBuilder( queryBuilder, - processedArgs.filter ?? ({} as ObjectRecordFilter), + args.filter ?? ({} as ObjectRecordFilter), ); const columnsToSelect = buildColumnsToSelect({ - select: selectedFieldsResult.select, - relations: selectedFieldsResult.relations, + select: args.selectedFieldsResult.select, + relations: args.selectedFieldsResult.relations, objectMetadataItemWithFieldMaps, objectMetadataMaps, }); @@ -115,13 +79,13 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic const objectRecords = [objectRecord] as ObjectRecord[]; - if (isDefined(selectedFieldsResult.relations)) { + if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: objectRecords, //TODO : Refacto-common - To fix when switching processNestedRelationsHelper to Common - relations: selectedFieldsResult.relations as Record< + relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations >, @@ -129,45 +93,51 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic authContext, workspaceDataSource, rolePermissionConfig, - selectedFields: selectedFieldsResult.select, + selectedFields: args.selectedFieldsResult.select, }); } - const enrichedResults = await this.enrichResultsWithGettersAndHooks({ - results: objectRecords, - authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - operationName: CommonQueryNames.FIND_ONE, - }); - - return enrichedResults[0]; + return objectRecords[0]; } - async processQueryArgs({ - authContext, - objectMetadataItemWithFieldMaps, - args, - }: { - authContext: WorkspaceAuthContext; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - args: FindOneQueryArgs; - }): Promise { - const hookedArgs = - (await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItemWithFieldMaps.nameSingular, - CommonQueryNames.FIND_ONE, - args, - //TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency - )) as FindOneQueryArgs; + async computeArgs( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; return { - ...hookedArgs, + ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - hookedArgs.filter, + args.filter, objectMetadataItemWithFieldMaps, ), }; } + + async processQueryResult( + queryResult: ObjectRecord, + objectMetadataItemId: string, + objectMetadataMaps: ObjectMetadataMaps, + authContext: WorkspaceAuthContext, + ): Promise { + return this.commonResultGettersService.processRecord( + queryResult, + objectMetadataItemId, + objectMetadataMaps, + authContext.workspace.id, + ); + } + + async validate( + args: CommonInput, + _queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise { + if (!args.filter || Object.keys(args.filter).length === 0) { + throw new CommonQueryRunnerException( + 'Missing filter argument', + CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts index 0152a279a3..fed2d2c9b4 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts @@ -11,6 +11,7 @@ import { computeRecordGqlOperationFilter, convertViewFilterValueToString, getFilterTypeFromFieldType, + isDefined, turnAnyFieldFilterIntoRecordGqlFilter, } from 'twenty-shared/utils'; @@ -18,24 +19,20 @@ import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service'; -import { - CommonQueryRunnerException, - CommonQueryRunnerExceptionCode, -} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; +import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type'; import { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type'; import { - CommonQueryNames, + CommonExtendedInput, + CommonInput, GroupByQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; -import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util'; -import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util'; import { getGroupByExpression } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util'; import { isGroupByDateField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field.util'; import { parseGroupByArgs } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util'; import { removeQuotes } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/remove-quote.util'; import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; -import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ViewFilterGroupService } from 'src/engine/metadata-modules/view-filter-group/services/view-filter-group.service'; @@ -45,7 +42,10 @@ import { ViewService } from 'src/engine/metadata-modules/view/services/view.serv import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util'; @Injectable() -export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerService { +export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerService< + GroupByQueryArgs, + CommonGroupByOutputItem[] +> { constructor( private readonly viewFilterService: ViewFilterService, private readonly viewFilterGroupService: ViewFilterGroupService, @@ -54,46 +54,16 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic super(); } - async run({ - args, - authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, - }: { - args: GroupByQueryArgs; - authContext: AuthContext; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - }): Promise { - if (!isWorkspaceAuthContext(authContext)) { - throw new CommonQueryRunnerException( - 'Invalid auth context', - CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, - ); - } - - const { repository } = await this.prepareQueryRunnerContext({ + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const { + repository, + commonQueryParser, + objectMetadataItemWithFieldMaps, authContext, - objectMetadataItemWithFieldMaps, - }); - - //TODO : Refacto-common - QueryParser should be common branded service - const commonQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldMaps, - objectMetadataMaps, - ); - - const selectedFieldsResult = commonQueryParser.parseSelectedFields( - objectMetadataItemWithFieldMaps, - args.selectedFields, - objectMetadataMaps, - ); - - const processedArgs = await this.processQueryArgs({ - authContext, - objectMetadataItemWithFieldMaps, - args, - }); + } = queryRunnerContext; const objectMetadataNameSingular = objectMetadataItemWithFieldMaps.nameSingular; @@ -104,9 +74,9 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic let appliedFilters = args.filter ?? ({} as ObjectRecordFilter); - if (args.viewId) { + if (isDefined(args.viewId)) { appliedFilters = await this.addFiltersFromView({ - args: processedArgs, + args, authContext, objectMetadataItemWithFieldMaps, appliedFilters, @@ -122,13 +92,13 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters); ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({ - selectedAggregatedFields: selectedFieldsResult.aggregate, + selectedAggregatedFields: args.selectedFieldsResult.aggregate, queryBuilder, objectMetadataNameSingular, }); const groupByFields = parseGroupByArgs( - processedArgs, + args, objectMetadataItemWithFieldMaps, ); @@ -170,7 +140,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic commonQueryParser.applyGroupByOrderToBuilder( queryBuilder, - processedArgs.orderBy ?? [], + args.orderBy ?? [], groupByFields, ); @@ -179,10 +149,19 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic return formatResultWithGroupByDimensionValues( result, groupByDefinitions, - Object.keys(selectedFieldsResult.aggregate), + Object.keys(args.selectedFieldsResult.aggregate), ); } + async processQueryResult( + queryResult: CommonGroupByOutputItem[], + _objectMetadataItemId: string, + _objectMetadataMaps: ObjectMetadataMaps, + _authContext: WorkspaceAuthContext, + ): Promise { + return queryResult; + } + private async addFiltersFromView({ args, objectMetadataItemWithFieldMaps, @@ -271,28 +250,21 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic return appliedFilters; } - async processQueryArgs({ - authContext, - objectMetadataItemWithFieldMaps, - args, - }: { - authContext: WorkspaceAuthContext; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - args: GroupByQueryArgs; - }): Promise { - const hookedArgs = - (await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItemWithFieldMaps.nameSingular, - CommonQueryNames.GROUP_BY, - args, - //TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency - )) as GroupByQueryArgs; + async validate( + _args: CommonInput, + _queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise {} + + async computeArgs( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; return { - ...hookedArgs, + ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - hookedArgs.filter, + args.filter, objectMetadataItemWithFieldMaps, ), }; diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-query-runners.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-query-runners.ts index 684394a833..f3157a45f6 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-query-runners.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-query-runners.ts @@ -1,5 +1,9 @@ import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service'; import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service'; +import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service'; +import { CommonDeleteOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-one-query-runner.service'; +import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service'; +import { CommonDestroyOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service'; import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service'; import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service'; import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service'; @@ -10,4 +14,8 @@ export const CommonQueryRunners = [ CommonCreateOneQueryRunnerService, CommonCreateManyQueryRunnerService, CommonGroupByQueryRunnerService, + CommonDestroyOneQueryRunnerService, + CommonDestroyManyQueryRunnerService, + CommonDeleteOneQueryRunnerService, + CommonDeleteManyQueryRunnerService, ]; diff --git a/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts index 48fd166d0a..1cb419b042 100644 --- a/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts @@ -43,7 +43,7 @@ export class CommonResultGettersService { ]); } - private async processRecordArray( + public async processRecordArray( recordArray: ObjectRecord[], objectMetadataItemId: string, objectMetadataMaps: ObjectMetadataMaps, @@ -62,7 +62,7 @@ export class CommonResultGettersService { ); } - private async processRecord( + public async processRecord( record: ObjectRecord, objectMetadataItemId: string, objectMetadataMaps: ObjectMetadataMaps, @@ -133,20 +133,6 @@ export class CommonResultGettersService { return processedRecord; } - async processQueryResult( - queryResultField: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, - workspaceId: string, - ): Promise { - return await this.processRecordArray( - queryResultField, - objectMetadataItemId, - objectMetadataMaps, - workspaceId, - ); - } - private getHandler(objectType: string): QueryResultGetterHandlerInterface { return ( this.handlers.get(objectType) || { diff --git a/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts b/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts new file mode 100644 index 0000000000..d6c5e1e5e5 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts @@ -0,0 +1,9 @@ +import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +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 type CommonBaseQueryRunnerContext = { + authContext: AuthContext; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + objectMetadataMaps: ObjectMetadataMaps; +}; diff --git a/packages/twenty-server/src/engine/api/common/types/common-extended-query-runner-context.type.ts b/packages/twenty-server/src/engine/api/common/types/common-extended-query-runner-context.type.ts new file mode 100644 index 0000000000..c8116998a3 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/types/common-extended-query-runner-context.type.ts @@ -0,0 +1,20 @@ +import { type ObjectLiteral } from 'typeorm'; + +import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; + +import { type CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; +import { type GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; +import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; + +export type CommonExtendedQueryRunnerContext = Omit< + CommonBaseQueryRunnerContext, + 'authContext' +> & { + authContext: WorkspaceAuthContext; + rolePermissionConfig: RolePermissionConfig; + repository: WorkspaceRepository; + commonQueryParser: GraphqlQueryParser; + workspaceDataSource: WorkspaceDataSource; +}; diff --git a/packages/twenty-server/src/engine/api/common/types/common-find-many-output.type.ts b/packages/twenty-server/src/engine/api/common/types/common-find-many-output.type.ts new file mode 100644 index 0000000000..285396e1da --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/types/common-find-many-output.type.ts @@ -0,0 +1,12 @@ +import { type ObjectRecord } from 'twenty-shared/types'; + +import { type CommonPageInfo } from 'src/engine/api/common/types/common-page-info.type'; +import { type CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type'; + +export type CommonFindManyOutput = { + records: ObjectRecord[]; + aggregatedValues: Record; + totalCount: number; + pageInfo: CommonPageInfo; + selectedFieldsResult: CommonSelectedFieldsResult; +}; diff --git a/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts b/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts index 9d6dcc8808..11fe35d2db 100644 --- a/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts +++ b/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts @@ -9,22 +9,35 @@ import { type ObjectRecordOrderBy, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { type CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type'; +import { + type CommonSelectedFields, + type CommonSelectedFieldsResult, +} from 'src/engine/api/common/types/common-selected-fields-result.type'; export enum CommonQueryNames { FIND_ONE = 'findOne', FIND_MANY = 'findMany', + CREATE_ONE = 'createOne', CREATE_MANY = 'createMany', GROUP_BY = 'groupBy', + DESTROY_MANY = 'destroyMany', + DESTROY_ONE = 'destroyOne', + DELETE_MANY = 'deleteMany', + DELETE_ONE = 'deleteOne', } -export interface FindOneQueryArgs { +interface BaseQueryArgs { selectedFields: CommonSelectedFields; +} + +interface ExtendedBaseQueryArgs { + selectedFieldsResult: CommonSelectedFieldsResult; +} +export interface FindOneQueryArgs extends BaseQueryArgs { filter?: ObjectRecordFilter; } export interface FindManyQueryArgs { - selectedFields: CommonSelectedFields; filter?: ObjectRecordFilter; orderBy?: ObjectRecordOrderBy; first?: number; @@ -34,25 +47,48 @@ export interface FindManyQueryArgs { } export interface CreateManyQueryArgs { - selectedFields: CommonSelectedFields; data: Partial[]; upsert?: boolean; } export interface CreateOneQueryArgs { - selectedFields: CommonSelectedFields; data: Partial; upsert?: boolean; } export interface GroupByQueryArgs { - selectedFields: CommonSelectedFields; filter?: ObjectRecordFilter; orderBy?: OrderByWithGroupBy; groupBy: ObjectRecordGroupBy; viewId?: string; } +export interface DestroyOneQueryArgs { + id: string; +} + +export interface DestroyManyQueryArgs { + filter: ObjectRecordFilter; +} + +export interface DeleteOneQueryArgs { + id: string; +} + +export interface DeleteManyQueryArgs { + filter: ObjectRecordFilter; +} export type CommonQueryArgs = | FindOneQueryArgs | FindManyQueryArgs - | GroupByQueryArgs; + | CreateManyQueryArgs + | CreateOneQueryArgs + | GroupByQueryArgs + | DestroyOneQueryArgs + | DestroyManyQueryArgs + | DeleteOneQueryArgs + | DeleteManyQueryArgs; + +export type CommonInput = T & BaseQueryArgs; + +export type CommonExtendedInput = T & + ExtendedBaseQueryArgs; diff --git a/packages/twenty-server/src/engine/api/common/types/common-query-result.type.ts b/packages/twenty-server/src/engine/api/common/types/common-query-result.type.ts new file mode 100644 index 0000000000..f875ae64b2 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/types/common-query-result.type.ts @@ -0,0 +1,10 @@ +import { type ObjectRecord } from 'twenty-shared/types'; + +import { type CommonFindManyOutput } from 'src/engine/api/common/types/common-find-many-output.type'; +import { type CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type'; + +export type CommonQueryResult = + | ObjectRecord[] + | ObjectRecord + | CommonGroupByOutputItem[] + | CommonFindManyOutput; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts index 32b99952eb..beb434714f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts @@ -139,19 +139,17 @@ export class GraphqlQueryParser { } public parseSelectedFields( - parentObjectMetadata: ObjectMetadataItemWithFieldMaps, // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - objectMetadataMaps: ObjectMetadataMaps, ): GraphqlQuerySelectedFieldsResult { const objectMetadataMapItem = getObjectMetadataMapItemByNameSingular( this.objectMetadataMaps, - parentObjectMetadata.nameSingular, + this.objectMetadataMapItem.nameSingular, ); if (!objectMetadataMapItem) { throw new GraphqlQueryRunnerException( - `Could not find object metadata for ${parentObjectMetadata.nameSingular}`, + `Could not find object metadata for ${this.objectMetadataMapItem.nameSingular}`, GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, ); } @@ -162,8 +160,8 @@ export class GraphqlQueryParser { return selectedFieldsParser.parse( graphqlSelectedFields, - objectMetadataMapItem, - objectMetadataMaps, + this.objectMetadataMapItem, + this.objectMetadataMaps, ); } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts index f7fa7bba55..bcc5c5df79 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts @@ -9,7 +9,7 @@ import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-contex 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 WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; -import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @Injectable() export class ProcessNestedRelationsHelper { 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 index f4afaf162a..2e40695c63 100644 --- 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 @@ -35,7 +35,7 @@ import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role 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 { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; export type GraphqlQueryResolverExecutionArgs = { args: Input; @@ -162,11 +162,7 @@ export abstract class GraphqlQueryBaseResolverService< const selectedFields = graphqlFields(options.info); const graphqlQuerySelectedFieldsResult = - graphqlQueryParser.parseSelectedFields( - objectMetadataItemWithFieldMaps, - selectedFields, - options.objectMetadataMaps, - ); + graphqlQueryParser.parseSelectedFields(selectedFields); const graphqlQueryResolverExecutionArgs = { args: computedArgs, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record-array.guard.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record-array.guard.ts index d4690d8634..c43f3bc974 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record-array.guard.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record-array.guard.ts @@ -2,8 +2,10 @@ import { type ObjectRecord } from 'twenty-shared/types'; import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value'; +import { type CommonQueryResult } from 'src/engine/api/common/types/common-query-result.type'; + export const isQueryResultFieldValueARecordArray = ( - result: QueryResultFieldValue, + result: QueryResultFieldValue | CommonQueryResult, ): result is ObjectRecord[] => { return Array.isArray(result); }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record.guard.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record.guard.ts index f42cb4d7ec..43107b9816 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record.guard.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record.guard.ts @@ -2,8 +2,10 @@ import { type ObjectRecord } from 'twenty-shared/types'; import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value'; +import { type CommonQueryResult } from 'src/engine/api/common/types/common-query-result.type'; + export const isQueryResultFieldValueARecord = ( - result: QueryResultFieldValue, + result: QueryResultFieldValue | CommonQueryResult, ): result is ObjectRecord => { return 'id' in result; }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts index 122886ed38..001b7dca2a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts @@ -233,12 +233,14 @@ export class QueryRunnerArgsFactory { return allOverriddenRecords; } - public overrideFilterByFieldMetadata( - filter: ObjectRecordFilter | undefined, + public overrideFilterByFieldMetadata< + T extends ObjectRecordFilter | undefined, + >( + filter: T, objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - ) { - if (!filter) { - return; + ): T { + if (!isDefined(filter)) { + return filter; } const overrideFilter = (filterObject: ObjectRecordFilter) => { @@ -264,7 +266,7 @@ export class QueryRunnerArgsFactory { }, {}); }; - return overrideFilter(filter); + return overrideFilter(filter) as T; } private transformFilterValueByType( 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 450bdea7b5..c6e3a27499 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 @@ -12,6 +12,7 @@ import { import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; 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'; @@ -48,13 +49,11 @@ export class CreateManyResolverFactory const selectedFields = graphqlFields(info); try { - const records = await this.commonCreateManyQueryRunnerService.run({ - args: { ...args, selectedFields }, - authContext: internalContext.authContext, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }); + const records = await this.commonCreateManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + CommonQueryNames.CREATE_MANY, + ); const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( 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 5c9028451c..d80a015080 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 @@ -11,6 +11,7 @@ import { import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; 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'; @@ -45,13 +46,11 @@ export class CreateOneResolverFactory const selectedFields = graphqlFields(info); try { - const record = await this.commonCreateOneQueryRunnerService.run({ - args: { ...args, selectedFields }, - authContext: internalContext.authContext, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }); + const record = await this.commonCreateOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + CommonQueryNames.CREATE_ONE, + ); const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( 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 8670a8ef52..af46785953 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 @@ -1,5 +1,8 @@ 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 { @@ -8,8 +11,14 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; +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 @@ -19,6 +28,8 @@ export class DeleteManyResolverFactory constructor( private readonly graphqlQueryRunnerService: GraphqlQueryDeleteManyResolverService, + private readonly commonDeleteManyQueryRunnerService: CommonDeleteManyQueryRunnerService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -27,6 +38,42 @@ 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 featureFlagsMap = workspaceDataSource.featureFlagMap; + + if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { + const selectedFields = graphqlFields(info); + + try { + const records = await this.commonDeleteManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + CommonQueryNames.DELETE_MANY, + ); + + 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) { + return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + } + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, info, 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 ab781f2dee..4a65947396 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 @@ -1,5 +1,7 @@ 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 { @@ -8,16 +10,25 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { CommonDeleteOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-one-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; +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 implements WorkspaceResolverBuilderFactoryInterface { public static methodName = RESOLVER_METHOD_NAMES.DELETE_ONE; + constructor( private readonly graphqlQueryRunnerService: GraphqlQueryDeleteOneResolverService, + private readonly commonDeleteOneQueryRunnerService: CommonDeleteOneQueryRunnerService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -26,6 +37,40 @@ 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 featureFlagsMap = workspaceDataSource.featureFlagMap; + + if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { + const selectedFields = graphqlFields(info); + + try { + const record = await this.commonDeleteOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + CommonQueryNames.DELETE_ONE, + ); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, + ); + + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + } + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, info, 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 386e715997..b3438ccb3c 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 @@ -1,5 +1,8 @@ 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 { @@ -8,8 +11,14 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; +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 @@ -19,6 +28,8 @@ export class DestroyManyResolverFactory constructor( private readonly graphqlQueryRunnerService: GraphqlQueryDestroyManyResolverService, + private readonly commonDestroyManyQueryRunnerService: CommonDestroyManyQueryRunnerService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -27,6 +38,43 @@ 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 featureFlagsMap = workspaceDataSource.featureFlagMap; + + if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { + const selectedFields = graphqlFields(info); + + try { + const records = + await this.commonDestroyManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + CommonQueryNames.DESTROY_MANY, + ); + + 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) { + return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + } + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, info, 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 dd30495e22..19b3c22ce3 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 @@ -1,5 +1,7 @@ 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 { @@ -8,8 +10,14 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { CommonDestroyOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; +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 @@ -19,6 +27,8 @@ export class DestroyOneResolverFactory constructor( private readonly graphQLQueryRunnerService: GraphqlQueryDestroyOneResolverService, + private readonly commonDestroyOneQueryRunnerService: CommonDestroyOneQueryRunnerService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -27,6 +37,40 @@ 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 featureFlagsMap = workspaceDataSource.featureFlagMap; + + if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { + const selectedFields = graphqlFields(info); + + try { + const record = await this.commonDestroyOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + CommonQueryNames.DESTROY_ONE, + ); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, + ); + + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + } + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, info, 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 7f1d15a319..2b5a158773 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 @@ -11,6 +11,7 @@ import { import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; 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'; @@ -51,13 +52,11 @@ export class FindManyResolverFactory totalCount, pageInfo, selectedFieldsResult, - } = await this.commonFindManyQueryRunnerService.run({ - args: { ...args, selectedFields }, - authContext: internalContext.authContext, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }); + } = await this.commonFindManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + CommonQueryNames.FIND_MANY, + ); const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( 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 e7dfe28ff2..1ac63d67ed 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 @@ -10,6 +10,7 @@ import { import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; 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'; @@ -44,13 +45,11 @@ export class FindOneResolverFactory try { const selectedFields = graphqlFields(info); - const record = await this.commonFindOneQueryRunnerService.run({ - args: { ...args, selectedFields }, - authContext: internalContext.authContext, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }); + const record = await this.commonFindOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + CommonQueryNames.FIND_ONE, + ); const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts index 8eb18ead70..548f6b7c8e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts @@ -11,6 +11,7 @@ import { import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; import { GraphqlQueryGroupByResolverService } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/graphql-query-group-by-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'; @@ -44,13 +45,11 @@ export class GroupByResolverFactory const selectedFields = graphqlFields(info); try { - return await this.commonGroupByQueryRunnerService.run({ - args: { ...args, selectedFields }, - authContext: internalContext.authContext, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }); + return await this.commonGroupByQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + CommonQueryNames.GROUP_BY, + ); } catch (error) { return workspaceQueryRunnerGraphqlApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts index a69bc05023..797a223a99 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 @@ -6,11 +6,12 @@ import { import isEmpty from 'lodash.isempty'; import { type ObjectRecord } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { capitalize, isDefined } from 'twenty-shared/utils'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; 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'; @@ -41,12 +42,15 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { authContext, }); - const records = await this.commonCreateManyQueryRunnerService.run({ - args: { data, selectedFields, upsert }, - authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, - }); + const records = await this.commonCreateManyQueryRunnerService.execute( + { data, selectedFields, upsert }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + CommonQueryNames.CREATE_MANY, + ); return this.formatRestResponse( records, @@ -61,7 +65,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { records: ObjectRecord[], objectNamePlural: string, ) { - return { data: { [objectNamePlural]: records } }; + return { data: { [`create${capitalize(objectNamePlural)}`]: records } }; } private parseRequestArgs(request: AuthenticatedRequest) { 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 bed04b5743..315fd5d6af 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 @@ -6,11 +6,12 @@ import { import isEmpty from 'lodash.isempty'; import { ObjectRecord } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { capitalize, isDefined } from 'twenty-shared/utils'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; 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'; @@ -42,12 +43,15 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { authContext, }); - const record = await this.commonCreateOneQueryRunnerService.run({ - args: { data, selectedFields, upsert }, - authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, - }); + const record = await this.commonCreateOneQueryRunnerService.execute( + { data, selectedFields, upsert }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + CommonQueryNames.CREATE_ONE, + ); return this.formatRestResponse( record, @@ -59,7 +63,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { } private formatRestResponse(record: ObjectRecord, objectNameSingular: string) { - return { data: { [objectNameSingular]: record } }; + return { data: { [`create${capitalize(objectNameSingular)}`]: record } }; } private parseRequestArgs(request: AuthenticatedRequest) { 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 new file mode 100644 index 0000000000..df52b529cd --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts @@ -0,0 +1,69 @@ +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 { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; +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'; +import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; + +@Injectable() +export class RestApiDeleteManyHandler extends RestApiBaseHandler { + constructor( + private readonly commonDeleteManyQueryRunnerService: CommonDeleteManyQueryRunnerService, + ) { + super(); + } + + async handle(request: AuthenticatedRequest): Promise<{ + data: { + [x: string]: ObjectRecord[]; + }; + }> { + const { filter } = this.parseRequestArgs(request); + const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = + await this.buildCommonOptions(request); + + try { + const records = await this.commonDeleteManyQueryRunnerService.execute( + { filter, selectedFields: { id: true } }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + CommonQueryNames.DELETE_MANY, + ); + + return this.formatRestResponse( + records, + objectMetadataItemWithFieldMaps.namePlural, + ); + } catch (error) { + return workspaceQueryRunnerRestApiExceptionHandler(error); + } + } + + private formatRestResponse( + records: ObjectRecord[], + objectNamePlural: string, + ) { + return { + data: { + [`delete${capitalize(objectNamePlural)}`]: records, + }, + }; + } + + private parseRequestArgs(request: AuthenticatedRequest) { + const filter = parseFilterRestRequest(request); + + return { + filter, + }; + } +} 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 ccb48eb9c3..4b03e2db1a 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 @@ -1,43 +1,70 @@ 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 { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; -import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; +import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; @Injectable() export class RestApiDeleteOneHandler extends RestApiBaseHandler { - async handle(request: AuthenticatedRequest) { - const { id: recordId } = parseCorePath(request); + constructor( + private readonly commonDeleteOneQueryRunnerService: CommonDeleteOneQueryRunnerService, + ) { + super(); + } - if (!recordId) { + async handle(request: AuthenticatedRequest) { + try { + const { id } = this.parseRequestArgs(request); + + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + } = await this.buildCommonOptions(request); + + const record = await this.commonDeleteOneQueryRunnerService.execute( + { id, selectedFields: { id: true } }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + CommonQueryNames.DELETE_ONE, + ); + + return this.formatRestResponse( + record, + objectMetadataItemWithFieldMaps.nameSingular, + ); + } catch (error) { + return workspaceQueryRunnerRestApiExceptionHandler(error); + } + } + + private formatRestResponse(record: ObjectRecord, objectNameSingular: string) { + return { + data: { [`delete${capitalize(objectNameSingular)}`]: record }, + }; + } + + private parseRequestArgs(request: AuthenticatedRequest) { + const { id } = parseCorePath(request); + + if (!isDefined(id)) { throw new BadRequestException('Record ID not found'); } - const { objectMetadata, repository, restrictedFields } = - await this.getRepositoryAndMetadataOrFail(request); - - const selectOptions = getAllSelectableFields({ - 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, - }, - }); + return { + id, + depth: parseDepthRestRequest(request), + }; } } 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 new file mode 100644 index 0000000000..411a8905c3 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts @@ -0,0 +1,69 @@ +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 { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; +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'; +import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; + +@Injectable() +export class RestApiDestroyManyHandler extends RestApiBaseHandler { + constructor( + private readonly commonDestroyManyQueryRunnerService: CommonDestroyManyQueryRunnerService, + ) { + super(); + } + + async handle(request: AuthenticatedRequest): Promise<{ + data: { + [x: string]: ObjectRecord[]; + }; + }> { + const { filter } = this.parseRequestArgs(request); + const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = + await this.buildCommonOptions(request); + + try { + const records = await this.commonDestroyManyQueryRunnerService.execute( + { filter, selectedFields: { id: true } }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + CommonQueryNames.DESTROY_MANY, + ); + + return this.formatRestResponse( + records, + objectMetadataItemWithFieldMaps.namePlural, + ); + } catch (error) { + return workspaceQueryRunnerRestApiExceptionHandler(error); + } + } + + private formatRestResponse( + records: ObjectRecord[], + objectNamePlural: string, + ) { + return { + data: { + [`delete${capitalize(objectNamePlural)}`]: records, + }, + }; + } + + private parseRequestArgs(request: AuthenticatedRequest) { + const filter = parseFilterRestRequest(request); + + return { + filter, + }; + } +} 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 new file mode 100644 index 0000000000..3e98b5ed17 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts @@ -0,0 +1,100 @@ +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 { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; +import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/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 { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; + +@Injectable() +export class RestApiDestroyOneHandler extends RestApiBaseHandler { + constructor( + private readonly commonDestroyOneQueryRunnerService: CommonDestroyOneQueryRunnerService, + ) { + super(); + } + + async commonHandle(request: AuthenticatedRequest) { + try { + const { id } = this.parseRequestArgs(request); + + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + } = await this.buildCommonOptions(request); + + const record = await this.commonDestroyOneQueryRunnerService.execute( + { id, selectedFields: { id: true } }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + CommonQueryNames.DESTROY_ONE, + ); + + return this.formatRestResponse( + record, + objectMetadataItemWithFieldMaps.nameSingular, + ); + } catch (error) { + workspaceQueryRunnerRestApiExceptionHandler(error); + } + } + + private formatRestResponse(record: ObjectRecord, objectNameSingular: string) { + return { data: { [`delete${capitalize(objectNameSingular)}`]: record } }; + } + + private parseRequestArgs(request: AuthenticatedRequest) { + const { id } = parseCorePath(request); + + if (!isDefined(id)) { + throw new BadRequestException('Record ID not found'); + } + + return { + 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 = getAllSelectableFields({ + 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-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts index da7daf1595..2973fd3e20 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 @@ -8,6 +8,7 @@ import { } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util'; import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; @@ -110,12 +111,15 @@ export class RestApiFindManyHandler extends RestApiBaseHandler { }); const { records, aggregatedValues, pageInfo } = - await this.commonFindManyQueryRunnerService.run({ - args: { ...parsedArgs, selectedFields }, - authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, - }); + await this.commonFindManyQueryRunnerService.execute( + { ...parsedArgs, selectedFields }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + CommonQueryNames.FIND_MANY, + ); return this.formatRestResponse( records, 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 09451a9918..912177c72a 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 @@ -6,6 +6,7 @@ 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 { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; @@ -73,12 +74,15 @@ export class RestApiFindOneHandler extends RestApiBaseHandler { authContext, }); - const record = await this.commonFindOneQueryRunnerService.run({ - args: { filter, selectedFields }, - authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, - }); + const record = await this.commonFindOneQueryRunnerService.execute( + { filter, selectedFields }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + CommonQueryNames.FIND_ONE, + ); return this.formatRestResponse( record, 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 2b1d125fdf..17b49a1593 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 @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; 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'; import { parseGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/group-by-parser-utils/parse-group-by-rest-request.util'; @@ -30,18 +31,21 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { const { filter, orderBy, viewId, groupBy, selectedFields } = this.parseRequestArgs(request); - return await this.commonGroupByQueryRunnerService.run({ - args: { + return await this.commonGroupByQueryRunnerService.execute( + { filter, orderBy, viewId, groupBy, selectedFields, }, - authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, - }); + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + CommonQueryNames.GROUP_BY, + ); } catch (error) { throw workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts index a04a3a838a..2ffd9547d5 100644 --- a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts @@ -593,7 +593,7 @@ export abstract class RestApiBaseHandler { objectMetadataMaps, }: { authContext: WorkspaceAuthContext; - depth: Depth; + depth?: Depth | undefined; objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; objectMetadataMaps: ObjectMetadataMaps; }) { 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 f5a6791b5d..ad3fe7b6ed 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 @@ -5,7 +5,10 @@ import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.modul import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller'; import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler'; import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler'; +import { RestApiDeleteManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-many.handler'; import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler'; +import { RestApiDestroyManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-destroy-many.handler'; +import { RestApiDestroyOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-destroy-one.handler'; import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler'; import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler'; import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler'; @@ -28,14 +31,17 @@ import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; const restApiCoreResolvers = [ - RestApiDeleteOneHandler, RestApiCreateOneHandler, RestApiCreateManyHandler, - RestApiUpdateOneHandler, RestApiFindOneHandler, RestApiFindManyHandler, RestApiFindDuplicatesHandler, RestApiGroupByHandler, + RestApiUpdateOneHandler, + RestApiDestroyOneHandler, + RestApiDestroyManyHandler, + RestApiDeleteOneHandler, + RestApiDeleteManyHandler, ]; @Module({ 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 6621e55e58..a65d70d030 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 @@ -4,13 +4,17 @@ import { isDefined } from 'twenty-shared/utils'; import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler'; import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler'; +import { RestApiDeleteManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-many.handler'; import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler'; +import { RestApiDestroyManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-destroy-many.handler'; +import { RestApiDestroyOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-destroy-one.handler'; import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler'; import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler'; import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler'; import { RestApiGroupByHandler } from 'src/engine/api/rest/core/handlers/rest-api-group-by.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 { 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'; @@ -18,14 +22,17 @@ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/service @Injectable() export class RestApiCoreService { constructor( - private readonly restApiDeleteOneHandler: RestApiDeleteOneHandler, private readonly restApiCreateOneHandler: RestApiCreateOneHandler, private readonly restApiCreateManyHandler: RestApiCreateManyHandler, - private readonly restApiUpdateOneHandler: RestApiUpdateOneHandler, private readonly restApiFindOneHandler: RestApiFindOneHandler, private readonly restApiFindManyHandler: RestApiFindManyHandler, private readonly restApiFindDuplicatesHandler: RestApiFindDuplicatesHandler, private readonly restApiGroupByHandler: RestApiGroupByHandler, + private readonly restApiUpdateOneHandler: RestApiUpdateOneHandler, + private readonly restApiDestroyOneHandler: RestApiDestroyOneHandler, + private readonly restApiDestroyManyHandler: RestApiDestroyManyHandler, + private readonly restApiDeleteOneHandler: RestApiDeleteOneHandler, + private readonly restApiDeleteManyHandler: RestApiDeleteManyHandler, private readonly featureFlagService: FeatureFlagService, ) {} @@ -36,10 +43,6 @@ export class RestApiCoreService { ); } - async delete(request: AuthenticatedRequest) { - return await this.restApiDeleteOneHandler.handle(request); - } - async createOne(request: AuthenticatedRequest) { const isCommonApiEnabled = await this.isCommonApiEnabled(request); @@ -60,14 +63,6 @@ export class RestApiCoreService { } } - async findDuplicates(request: AuthenticatedRequest) { - return await this.restApiFindDuplicatesHandler.handle(request); - } - - async update(request: AuthenticatedRequest) { - return await this.restApiUpdateOneHandler.handle(request); - } - async get(request: AuthenticatedRequest) { const { id: recordId } = parseCorePath(request); const isCommonApiEnabled = await this.isCommonApiEnabled(request); @@ -98,4 +93,36 @@ export class RestApiCoreService { ); } } + + async findDuplicates(request: AuthenticatedRequest) { + return await this.restApiFindDuplicatesHandler.handle(request); + } + + async update(request: AuthenticatedRequest) { + return await this.restApiUpdateOneHandler.handle(request); + } + + 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)) + return await this.restApiDestroyManyHandler.handle(request); + + if (isCommonApiEnabled && isSoftDelete && isDefined(recordId)) + return await this.restApiDeleteOneHandler.handle(request); + if (isCommonApiEnabled && 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', + ); + } } diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/soft-delete-parser-utils/__tests__/parse-soft-delete-rest-request.util.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/soft-delete-parser-utils/__tests__/parse-soft-delete-rest-request.util.spec.ts new file mode 100644 index 0000000000..0804b8b457 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/soft-delete-parser-utils/__tests__/parse-soft-delete-rest-request.util.spec.ts @@ -0,0 +1,61 @@ +import { parseSoftDeleteRestRequest } from 'src/engine/api/rest/input-request-parsers/soft-delete-parser-utils/parse-soft-delete-rest-request.util'; + +describe('parseSoftDeleteRestRequest', () => { + it('should return false when soft_delete query parameter is not defined', () => { + const request = { + query: {}, + } as any; + + const result = parseSoftDeleteRestRequest(request); + + expect(result).toBe(false); + }); + + it('should return true when soft_delete query parameter is "true"', () => { + const request = { + query: { + soft_delete: 'true', + }, + } as any; + + const result = parseSoftDeleteRestRequest(request); + + expect(result).toBe(true); + }); + + it('should return false when soft_delete query parameter is "false"', () => { + const request = { + query: { + soft_delete: 'false', + }, + } as any; + + const result = parseSoftDeleteRestRequest(request); + + expect(result).toBe(false); + }); + + it('should return false when soft_delete query parameter is empty string', () => { + const request = { + query: { + soft_delete: '', + }, + } as any; + + const result = parseSoftDeleteRestRequest(request); + + expect(result).toBe(false); + }); + + it('should return false when soft_delete query parameter is a boolean true', () => { + const request = { + query: { + soft_delete: true, + }, + } as any; + + const result = parseSoftDeleteRestRequest(request); + + expect(result).toBe(false); + }); +}); diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/soft-delete-parser-utils/parse-soft-delete-rest-request.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/soft-delete-parser-utils/parse-soft-delete-rest-request.util.ts new file mode 100644 index 0000000000..b206c73252 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/soft-delete-parser-utils/parse-soft-delete-rest-request.util.ts @@ -0,0 +1,13 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; + +export const parseSoftDeleteRestRequest = ( + request: AuthenticatedRequest, +): boolean => { + if (!isDefined(request.query.soft_delete)) { + return false; + } + + return request.query.soft_delete === 'true'; +}; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index 4460192661..55ef305319 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -13,6 +13,7 @@ import { computeIdPathParameter, computeLimitParameters, computeOrderByParameters, + computeSoftDeleteParameters, computeStartingAfterParameters, computeUpsertParameters, } from 'src/engine/core-modules/open-api/utils/parameters.utils'; @@ -210,6 +211,7 @@ export const computeParameterComponents = ( filter: computeFilterParameters(), depth: computeDepthParameters(), upsert: computeUpsertParameters(), + softDelete: computeSoftDeleteParameters(), orderBy: computeOrderByParameters(), limit: computeLimitParameters(fromMetadata), }; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts index b8e06203c3..63ac575ea7 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts @@ -75,6 +75,20 @@ export const computeUpsertParameters = (): OpenAPIV3_1.ParameterObject => { }; }; +export const computeSoftDeleteParameters = (): OpenAPIV3_1.ParameterObject => { + return { + name: 'soft_delete', + in: 'query', + description: + 'If true, soft deletes the objects. If false, objects are permanently deleted.', + required: false, + schema: { + type: 'boolean', + default: false, + }, + }; +}; + export const computeFilterParameters = (): OpenAPIV3_1.ParameterObject => { return { name: 'filter', diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts index 88d024daf1..3cdf36fe4a 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts @@ -10,6 +10,7 @@ import { import { getCreateManyResponse201, getCreateOneResponse201, + getDeleteManyResponse200, getDeleteResponse200, getFindDuplicatesResponse200, getFindManyResponse200, @@ -79,6 +80,20 @@ export const computeManyResultPath = ( '401': { $ref: '#/components/responses/401' }, }, }, + delete: { + tags: [item.namePlural], + summary: `Delete Many ${item.namePlural}`, + operationId: `deleteMany${capitalize(item.namePlural)}`, + parameters: [ + { $ref: '#/components/parameters/filter' }, + { $ref: '#/components/parameters/softDelete' }, + ], + responses: { + '200': getDeleteManyResponse200(item), + '400': { $ref: '#/components/responses/400' }, + '401': { $ref: '#/components/responses/401' }, + }, + }, } as OpenAPIV3_1.PathItemObject; }; @@ -105,7 +120,10 @@ export const computeSingleResultPath = ( tags: [item.namePlural], summary: `Delete One ${item.nameSingular}`, operationId: `deleteOne${capitalize(item.nameSingular)}`, - parameters: [{ $ref: '#/components/parameters/idPath' }], + parameters: [ + { $ref: '#/components/parameters/idPath' }, + { $ref: '#/components/parameters/softDelete' }, + ], responses: { '200': getDeleteResponse200(item), '400': { $ref: '#/components/responses/400' }, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts index 29aacf6a65..19f580ba95 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts @@ -174,6 +174,40 @@ export const getUpdateOneResponse200 = ( }; }; +export const getDeleteManyResponse200 = ( + item: Pick, +) => { + return { + description: 'Successful operation', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + data: { + type: 'object', + properties: { + [`delete${capitalize(item.namePlural)}`]: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; +}; + export const getDeleteResponse200 = ( item: Pick, fromMetadata = false,