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 30f6191518..6528d70da7 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 @@ -72,10 +72,11 @@ export abstract class CommonBaseQueryRunnerService< @Inject() protected readonly commonResultGettersService: CommonResultGettersService; + protected abstract readonly operationName: CommonQueryNames; + public async execute( args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, - operationName: CommonQueryNames, ): Promise { const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = queryRunnerContext; @@ -104,7 +105,7 @@ export abstract class CommonBaseQueryRunnerService< const processedArgs = await this.processArgs( args, queryRunnerContext, - operationName, + this.operationName, commonQueryParser, ); @@ -121,7 +122,7 @@ export abstract class CommonBaseQueryRunnerService< return this.enrichResultsWithGettersAndHooks({ results, - operationName, + operationName: this.operationName, authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps, 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 1c643203ad..38e2715080 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 @@ -21,6 +21,7 @@ import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/co import { CommonExtendedInput, CommonInput, + CommonQueryNames, CreateManyQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type'; @@ -34,13 +35,14 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -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 CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerService< CreateManyQueryArgs, ObjectRecord[] > { + protected readonly operationName = CommonQueryNames.CREATE_MANY; async run( args: CommonExtendedInput, queryRunnerContext: CommonExtendedQueryRunnerContext, 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 3d0612d364..b978188493 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 @@ -11,6 +11,7 @@ import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/co import { CommonExtendedInput, CommonInput, + CommonQueryNames, CreateManyQueryArgs, CreateOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; @@ -29,6 +30,8 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ super(); } + protected readonly operationName = CommonQueryNames.CREATE_ONE; + async run( args: CommonExtendedInput, queryRunnerContext: CommonExtendedQueryRunnerContext, 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 index fc5cd85c6f..924b0902cd 100644 --- 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 @@ -17,6 +17,7 @@ import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/co import { CommonExtendedInput, CommonInput, + CommonQueryNames, 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'; @@ -30,6 +31,8 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer DeleteManyQueryArgs, ObjectRecord[] > { + protected readonly operationName = CommonQueryNames.DELETE_MANY; + async run( args: CommonExtendedInput, queryRunnerContext: CommonExtendedQueryRunnerContext, 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 index 3879990bde..7949b8224a 100644 --- 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 @@ -16,6 +16,7 @@ import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/co import { CommonExtendedInput, CommonInput, + CommonQueryNames, 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'; @@ -33,6 +34,8 @@ export class CommonDeleteOneQueryRunnerService extends CommonBaseQueryRunnerServ super(); } + protected readonly operationName = CommonQueryNames.DELETE_ONE; + async run( args: CommonExtendedInput, queryRunnerContext: CommonExtendedQueryRunnerContext, 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 index aa210077b6..9cebffc580 100644 --- 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 @@ -17,6 +17,7 @@ import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/co import { CommonExtendedInput, CommonInput, + CommonQueryNames, 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'; @@ -27,6 +28,8 @@ export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerSe DestroyManyQueryArgs, ObjectRecord[] > { + protected readonly operationName = CommonQueryNames.DESTROY_MANY; + async run( args: CommonExtendedInput, queryRunnerContext: CommonExtendedQueryRunnerContext, 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 index 9b8b7b1a85..7eb424bacf 100644 --- 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 @@ -16,6 +16,7 @@ import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/co import { CommonExtendedInput, CommonInput, + CommonQueryNames, DestroyOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @@ -31,6 +32,8 @@ export class CommonDestroyOneQueryRunnerService extends CommonBaseQueryRunnerSer super(); } + protected readonly operationName = CommonQueryNames.DESTROY_ONE; + async run( args: CommonExtendedInput, queryRunnerContext: CommonExtendedQueryRunnerContext, diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts new file mode 100644 index 0000000000..26e95a163f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts @@ -0,0 +1,208 @@ +import { Injectable } from '@nestjs/common'; + +import isEmpty from 'lodash.isempty'; +import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; +import { ObjectRecord, OrderByDirection } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { In } 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 { CommonFindDuplicatesOutputItem } from 'src/engine/api/common/types/common-find-duplicates-output-item.type'; +import { + CommonExtendedInput, + CommonInput, + CommonQueryNames, + FindDuplicatesQueryArgs, +} from 'src/engine/api/common/types/common-query-args.type'; +import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util'; +import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; +import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; + +@Injectable() +export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunnerService< + FindDuplicatesQueryArgs, + CommonFindDuplicatesOutputItem[] +> { + protected readonly operationName = CommonQueryNames.FIND_DUPLICATES; + + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const { + repository, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + commonQueryParser, + } = queryRunnerContext; + + const existingRecordsQueryBuilder = repository.createQueryBuilder( + objectMetadataItemWithFieldMaps.nameSingular, + ); + + let objectRecords: Partial[] = []; + + const columnsToSelect = buildColumnsToSelect({ + select: args.selectedFieldsResult.select, + relations: args.selectedFieldsResult.relations, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + }); + + if (isDefined(args.ids)) { + objectRecords = (await existingRecordsQueryBuilder + .where({ id: In(args.ids) }) + .setFindOptions({ + select: columnsToSelect, + }) + .getMany()) as ObjectRecord[]; + } else if (args.data && !isEmpty(args.data)) { + objectRecords = args.data; + } + + const findDuplicatesOutput: CommonFindDuplicatesOutputItem[] = + await Promise.all( + objectRecords.map(async (record) => { + const duplicateConditions = buildDuplicateConditions( + objectMetadataItemWithFieldMaps, + [record], + record.id, + ); + + if (isEmpty(duplicateConditions)) { + return { + records: [], + totalCount: 0, + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }; + } + + const duplicateRecordsQueryBuilder = repository.createQueryBuilder( + objectMetadataItemWithFieldMaps.nameSingular, + ); + + commonQueryParser.applyFilterToBuilder( + duplicateRecordsQueryBuilder, + objectMetadataItemWithFieldMaps.nameSingular, + duplicateConditions, + ); + + const duplicates = (await duplicateRecordsQueryBuilder + .setFindOptions({ + select: columnsToSelect, + }) + .take(QUERY_MAX_RECORDS) + .getMany()) as ObjectRecord[]; + + const aggregateQueryBuilder = duplicateRecordsQueryBuilder.clone(); + const totalCount = await aggregateQueryBuilder.getCount(); + + const { startCursor, endCursor } = getPageInfo( + duplicates, + [{ id: OrderByDirection.AscNullsFirst }], + QUERY_MAX_RECORDS, + true, + ); + + return { + records: duplicates, + totalCount, + hasNextPage: false, + hasPreviousPage: false, + startCursor, + endCursor, + }; + }), + ); + + return findDuplicatesOutput; + } + + async computeArgs( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + + return { + ...args, + ids: await Promise.all( + args.ids?.map((id) => + this.queryRunnerArgsFactory.overrideValueByFieldMetadata( + 'id', + id, + objectMetadataItemWithFieldMaps.fieldsById, + objectMetadataItemWithFieldMaps, + ), + ) ?? [], + ), + data: await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({ + partialRecordInputs: args.data, + authContext, + objectMetadataItemWithFieldMaps, + shouldBackfillPositionIfUndefined: false, + }), + }; + } + + async processQueryResult( + queryResult: CommonFindDuplicatesOutputItem[], + objectMetadataItemId: string, + objectMetadataMaps: ObjectMetadataMaps, + authContext: WorkspaceAuthContext, + ): Promise { + const processedResults = await Promise.all( + queryResult.map(async (result) => { + return { + ...result, + records: await this.commonResultGettersService.processRecordArray( + result.records, + objectMetadataItemId, + objectMetadataMaps, + authContext.workspace.id, + ), + }; + }), + ); + + return processedResults; + } + + async validate( + args: CommonInput, + _queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise { + if (!args.data && !args.ids) { + throw new CommonQueryRunnerException( + 'You have to provide either "data" or "ids" argument', + CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + + if (args.data && args.ids) { + throw new CommonQueryRunnerException( + 'You cannot provide both "data" and "ids" arguments', + CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + + if (!args.ids && isEmpty(args.data)) { + throw new CommonQueryRunnerException( + 'The "data" condition can not be empty when "ids" input not provided', + 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 e2c03af601..e4326ddc09 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 @@ -22,6 +22,7 @@ import { CommonFindManyOutput } from 'src/engine/api/common/types/common-find-ma import { CommonExtendedInput, CommonInput, + CommonQueryNames, FindManyQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util'; @@ -36,6 +37,8 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi FindManyQueryArgs, CommonFindManyOutput > { + protected readonly operationName = CommonQueryNames.FIND_MANY; + async run( args: CommonExtendedInput, queryRunnerContext: CommonExtendedQueryRunnerContext, 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 380a0e7d66..3cbd1100f7 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 @@ -18,6 +18,7 @@ import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/co import { CommonExtendedInput, CommonInput, + CommonQueryNames, FindOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; @@ -28,6 +29,8 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic FindOneQueryArgs, ObjectRecord > { + protected readonly operationName = CommonQueryNames.FIND_ONE; + async run( args: CommonExtendedInput, queryRunnerContext: CommonExtendedQueryRunnerContext, 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 fed2d2c9b4..e3cea1ad65 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 @@ -25,6 +25,7 @@ import { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-grou import { CommonExtendedInput, CommonInput, + CommonQueryNames, GroupByQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util'; @@ -54,6 +55,8 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic super(); } + protected readonly operationName = CommonQueryNames.GROUP_BY; + async run( args: CommonExtendedInput, queryRunnerContext: CommonExtendedQueryRunnerContext, diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts new file mode 100644 index 0000000000..869c9ce882 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts @@ -0,0 +1,471 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { + MUTATION_MAX_MERGE_RECORDS, + QUERY_MAX_RECORDS, +} from 'twenty-shared/constants'; +import { + FieldMetadataType, + ObjectRecord, + RelationType, +} from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { FindOptionsRelations, In, ObjectLiteral } from 'typeorm'; +import { v4 as uuidv4 } from 'uuid'; + +import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; +import { FieldMetadataRelationSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.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, + CommonQueryNames, + MergeManyQueryArgs, +} from 'src/engine/api/common/types/common-query-args.type'; +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; +import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; +import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; +import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util'; +import { mergeFieldValues } from 'src/engine/api/graphql/graphql-query-runner/utils/merge-field-values.util'; +import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; +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 { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; + +@Injectable() +export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerService< + MergeManyQueryArgs, + ObjectRecord +> { + protected readonly operationName = CommonQueryNames.MERGE_MANY; + + private readonly logger = new Logger(CommonMergeManyQueryRunnerService.name); + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const { objectMetadataMaps, objectMetadataItemWithFieldMaps } = + queryRunnerContext; + + const recordsToMerge = await this.fetchRecordsToMerge( + queryRunnerContext, + args, + ); + + const priorityRecord = this.validateAndGetPriorityRecord( + recordsToMerge, + args.ids, + args.conflictPriorityIndex, + ); + + const mergedData = this.performDeepMerge( + recordsToMerge, + priorityRecord.id, + objectMetadataItemWithFieldMaps, + ); + + if (args.dryRun) { + return this.createDryRunResponse( + priorityRecord, + mergedData, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + ); + } + + const idsToDelete = args.ids.filter((id) => id !== priorityRecord.id); + + await this.migrateRelatedRecords( + queryRunnerContext, + idsToDelete, + priorityRecord.id, + ); + + const queryBuilder = queryRunnerContext.repository.createQueryBuilder( + objectMetadataItemWithFieldMaps.nameSingular, + ); + + const columnsToReturn = buildColumnsToReturn({ + select: args.selectedFieldsResult.select, + relations: args.selectedFieldsResult.relations, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + }); + + await queryBuilder + .softDelete() + .whereInIds(idsToDelete) + .returning(columnsToReturn) + .execute(); + + const updatedRecord = await this.updatePriorityRecord( + args, + queryRunnerContext, + priorityRecord.id, + mergedData, + ); + + await this.processNestedRelations({ + args, + queryRunnerContext, + updatedRecords: [updatedRecord], + }); + + return updatedRecord; + } + + private async fetchRecordsToMerge( + context: CommonExtendedQueryRunnerContext, + args: CommonExtendedInput, + ): Promise { + const columnsToSelect = buildColumnsToSelect({ + select: args.selectedFieldsResult.select, + relations: args.selectedFieldsResult.relations, + objectMetadataItemWithFieldMaps: context.objectMetadataItemWithFieldMaps, + objectMetadataMaps: context.objectMetadataMaps, + }); + + const recordsToMerge = await context.repository.find({ + where: { id: In(args.ids) }, + select: columnsToSelect, + }); + + if (recordsToMerge.length !== args.ids.length) { + throw new CommonQueryRunnerException( + 'One or more records not found', + CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + ); + } + + return recordsToMerge as ObjectRecord[]; + } + + private validateAndGetPriorityRecord( + recordsToMerge: ObjectRecord[], + ids: string[], + conflictPriorityIndex: number, + ): ObjectRecord { + const priorityRecordId = ids[conflictPriorityIndex]; + const priorityRecord = recordsToMerge.find( + (record) => record.id === priorityRecordId, + ); + + if (!priorityRecord) { + throw new GraphqlQueryRunnerException( + 'Priority record not found', + GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, + ); + } + + return priorityRecord; + } + + private performDeepMerge( + recordsToMerge: ObjectRecord[], + priorityRecordId: string, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + ): Partial { + const mergedResult: Partial = {}; + + const allFieldNames = new Set(); + + recordsToMerge.forEach((record) => { + Object.keys(record).forEach((fieldName) => { + if ( + !this.shouldExcludeFieldFromMerge( + fieldName, + objectMetadataItemWithFieldMaps, + ) + ) { + allFieldNames.add(fieldName); + } + }); + }); + + allFieldNames.forEach((fieldName) => { + const recordsWithValues: { value: unknown; recordId: string }[] = []; + + recordsToMerge.forEach((record) => { + const fieldValue = record[fieldName]; + + if (hasRecordFieldValue(fieldValue)) { + recordsWithValues.push({ value: fieldValue, recordId: record.id }); + } + }); + + if (recordsWithValues.length === 0) { + return; + } else if (recordsWithValues.length === 1) { + mergedResult[fieldName] = recordsWithValues[0].value; + } else { + const fieldMetadata = Object.values( + objectMetadataItemWithFieldMaps.fieldsById, + ).find((field) => field?.name === fieldName); + + if (!fieldMetadata) { + return; + } + + mergedResult[fieldName] = mergeFieldValues( + fieldMetadata.type, + recordsWithValues, + priorityRecordId, + ); + } + }); + + return mergedResult; + } + + private shouldExcludeFieldFromMerge( + fieldName: string, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + ): boolean { + const fieldMetadata = Object.values( + objectMetadataItemWithFieldMaps.fieldsById, + ).find((field) => field?.name === fieldName); + + return fieldMetadata?.isSystem ?? false; + } + + private createDryRunResponse( + priorityRecord: ObjectRecord, + mergedData: Partial, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + objectMetadataMaps: ObjectMetadataMaps, + ): ObjectRecord { + const dryRunRecord = { + ...priorityRecord, + ...mergedData, + id: uuidv4(), + deletedAt: new Date().toISOString(), + } as ObjectRecord; + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + + return typeORMObjectRecordsParser.processRecord({ + objectRecord: dryRunRecord, + objectName: objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } + + private async updatePriorityRecord( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + priorityRecordId: string, + mergedData: Partial, + ): Promise { + const { objectMetadataItemWithFieldMaps, objectMetadataMaps, repository } = + queryRunnerContext; + + const queryBuilder = repository.createQueryBuilder( + objectMetadataItemWithFieldMaps.nameSingular, + ); + + const columnsToReturn = buildColumnsToReturn({ + select: args.selectedFieldsResult.select, + relations: args.selectedFieldsResult.relations, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + }); + + const updatedObjectRecords = await queryBuilder + .update() + .set(mergedData) + .where({ id: priorityRecordId }) + .returning(columnsToReturn) + .execute(); + + if (!updatedObjectRecords.generatedMaps.length) { + throw new CommonQueryRunnerException( + 'Failed to update record', + CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + ); + } + + const updatedRecord = updatedObjectRecords.generatedMaps[0] as ObjectRecord; + + return updatedRecord; + } + + private async migrateRelatedRecords( + context: CommonExtendedQueryRunnerContext, + fromIds: string[], + toId: string, + ): Promise { + const { objectMetadataMaps, objectMetadataItemWithFieldMaps } = context; + + const relationFieldsPointingToCurrentObject = Object.values( + objectMetadataMaps.byId, + ) + .filter(isDefined) + .flatMap((metadata) => { + const relationFields = Object.values(metadata.fieldsById) + .filter(isDefined) + .filter((field) => + isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION), + ) + .filter( + (field) => + field.relationTargetObjectMetadataId === + objectMetadataItemWithFieldMaps.id && field.isActive, + ); + + return relationFields + .filter((field) => { + const relationSettings = + field.settings as FieldMetadataRelationSettings; + + return ( + relationSettings?.relationType === RelationType.MANY_TO_ONE && + relationSettings?.joinColumnName + ); + }) + .map((field) => { + const relationSettings = + field.settings as FieldMetadataRelationSettings; + + return { + objectMetadata: metadata, + fieldName: field.name, + fieldId: field.id, + joinColumnName: relationSettings.joinColumnName, + }; + }); + }); + + for (const relationField of relationFieldsPointingToCurrentObject) { + if (!relationField.joinColumnName) { + continue; + } + + try { + const repository = context.workspaceDataSource.getRepository( + relationField.objectMetadata.nameSingular, + context.rolePermissionConfig, + ); + + const whereCondition = { [relationField.joinColumnName]: In(fromIds) }; + + const existingRecords = await repository.find({ + where: whereCondition, + }); + + if (existingRecords.length > 0) { + await repository.update(whereCondition, { + [relationField.joinColumnName]: toId, + }); + } + } catch (error) { + this.logger.warn( + `Failed to migrate relation field "${relationField.fieldName}" (${relationField.joinColumnName}) in object "${relationField.objectMetadata.nameSingular}":`, + error.message, + ); + } + } + } + + private async processNestedRelations({ + args, + queryRunnerContext, + updatedRecords, + }: { + args: CommonExtendedInput; + queryRunnerContext: CommonExtendedQueryRunnerContext; + updatedRecords: ObjectRecord[]; + }): Promise { + const { + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + authContext, + workspaceDataSource, + rolePermissionConfig, + } = queryRunnerContext; + + if (args.selectedFieldsResult.relations) { + await this.processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: updatedRecords, + 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> { + return args; + } + + async processQueryResult( + queryResult: ObjectRecord, + _objectMetadataItemId: string, + _objectMetadataMaps: ObjectMetadataMaps, + _authContext: WorkspaceAuthContext, + ): Promise { + return queryResult; + } + + async validate( + args: CommonInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + + assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + + if (!isDefined(objectMetadataItemWithFieldMaps.duplicateCriteria)) { + throw new CommonQueryRunnerException( + `Merge is only available for objects with duplicate criteria. Object '${objectMetadataItemWithFieldMaps.nameSingular}' does not have duplicate criteria defined.`, + CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + + const { ids, conflictPriorityIndex } = args; + + if (!ids || ids.length < 2) { + throw new CommonQueryRunnerException( + 'At least 2 record IDs are required for merge', + CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + + if (ids.length > MUTATION_MAX_MERGE_RECORDS) { + throw new CommonQueryRunnerException( + `Maximum ${MUTATION_MAX_MERGE_RECORDS} records can be merged at once`, + CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + + if (conflictPriorityIndex < 0 || conflictPriorityIndex >= ids.length) { + throw new CommonQueryRunnerException( + `Invalid conflict priority '${conflictPriorityIndex}'. Valid options for ${ids.length} records: 0-${ids.length - 1}`, + CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + } +} 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 f3157a45f6..46ccff7404 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 @@ -4,18 +4,30 @@ import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common 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 { CommonFindDuplicatesQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-duplicates-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'; +import { CommonMergeManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-merge-many-query-runner.service'; +import { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service'; +import { CommonRestoreOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-one-query-runner.service'; +import { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-many-query-runner.service'; +import { CommonUpdateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-one-query-runner.service'; export const CommonQueryRunners = [ - CommonFindOneQueryRunnerService, - CommonFindManyQueryRunnerService, CommonCreateOneQueryRunnerService, CommonCreateManyQueryRunnerService, + CommonFindManyQueryRunnerService, + CommonFindOneQueryRunnerService, CommonGroupByQueryRunnerService, - CommonDestroyOneQueryRunnerService, + CommonUpdateOneQueryRunnerService, + CommonUpdateManyQueryRunnerService, CommonDestroyManyQueryRunnerService, - CommonDeleteOneQueryRunnerService, + CommonDestroyOneQueryRunnerService, CommonDeleteManyQueryRunnerService, + CommonDeleteOneQueryRunnerService, + CommonFindDuplicatesQueryRunnerService, + CommonRestoreManyQueryRunnerService, + CommonRestoreOneQueryRunnerService, + CommonMergeManyQueryRunnerService, ]; diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts new file mode 100644 index 0000000000..cc8722a00a --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts @@ -0,0 +1,146 @@ +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, + CommonQueryNames, + RestoreManyQueryArgs, +} 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 CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerService< + RestoreManyQueryArgs, + ObjectRecord[] +> { + protected readonly operationName = CommonQueryNames.RESTORE_MANY; + + 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 restoredObjectRecords = await queryBuilder + .restore() + .returning(columnsToReturn) + .execute(); + + const restoredRecords = + restoredObjectRecords.generatedMaps as ObjectRecord[]; + + if (isDefined(args.selectedFieldsResult.relations)) { + await this.processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: restoredRecords, + //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 restoredRecords; + } + + 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: RestoreManyQueryArgs, + queryRunnerContext: CommonBaseQueryRunnerContext, + ) { + const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + + assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + if (!isDefined(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-restore-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts new file mode 100644 index 0000000000..40a1398376 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts @@ -0,0 +1,86 @@ +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 { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-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, + CommonQueryNames, + RestoreOneQueryArgs, +} 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 CommonRestoreOneQueryRunnerService extends CommonBaseQueryRunnerService< + RestoreOneQueryArgs, + ObjectRecord +> { + constructor( + private readonly commonRestoreManyQueryRunnerService: CommonRestoreManyQueryRunnerService, + ) { + super(); + } + + protected readonly operationName = CommonQueryNames.RESTORE_ONE; + + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const result = await this.commonRestoreManyQueryRunnerService.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-update-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts new file mode 100644 index 0000000000..4599fd232a --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts @@ -0,0 +1,155 @@ +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, + CommonQueryNames, + UpdateManyQueryArgs, +} 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 CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerService< + UpdateManyQueryArgs, + ObjectRecord[] +> { + protected readonly operationName = CommonQueryNames.UPDATE_MANY; + + 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 updatedObjectRecords = await queryBuilder + .update() + .set(args.data) + .returning(columnsToReturn) + .execute(); + + const updatedRecords = updatedObjectRecords.generatedMaps as ObjectRecord[]; + + if (isDefined(args.selectedFieldsResult.relations)) { + await this.processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: updatedRecords, + //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 updatedRecords; + } + + async computeArgs( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise> { + const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + + return { + ...args, + filter: + this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( + args.filter, + objectMetadataItemWithFieldMaps, + ) || {}, + data: ( + await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({ + partialRecordInputs: [args.data], + authContext, + objectMetadataItemWithFieldMaps, + shouldBackfillPositionIfUndefined: false, + }) + )[0], + }; + } + + async validate( + args: CommonInput, + queryRunnerContext: CommonBaseQueryRunnerContext, + ): Promise { + 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)); + } + + async processQueryResult( + queryResult: ObjectRecord[], + objectMetadataItemId: string, + objectMetadataMaps: ObjectMetadataMaps, + authContext: WorkspaceAuthContext, + ): Promise { + return await this.commonResultGettersService.processRecordArray( + queryResult, + objectMetadataItemId, + objectMetadataMaps, + authContext.workspace.id, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts new file mode 100644 index 0000000000..61988d1ec5 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts @@ -0,0 +1,84 @@ +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 { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-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, + CommonQueryNames, + UpdateOneQueryArgs, +} 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 CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerService< + UpdateOneQueryArgs, + ObjectRecord +> { + constructor( + private readonly commonUpdateManyQueryRunnerService: CommonUpdateManyQueryRunnerService, + ) { + super(); + } + protected readonly operationName = CommonQueryNames.UPDATE_ONE; + + async run( + args: CommonExtendedInput, + queryRunnerContext: CommonExtendedQueryRunnerContext, + ): Promise { + const result = await this.commonUpdateManyQueryRunnerService.run( + { + ...args, + filter: { id: { eq: args.id } }, + }, + queryRunnerContext, + ); + + if (!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/types/common-find-duplicates-output-item.type.ts b/packages/twenty-server/src/engine/api/common/types/common-find-duplicates-output-item.type.ts new file mode 100644 index 0000000000..0c3ebc0f23 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/types/common-find-duplicates-output-item.type.ts @@ -0,0 +1,10 @@ +import { type ObjectRecord } from 'twenty-shared/types'; + +export type CommonFindDuplicatesOutputItem = { + records: ObjectRecord[]; + totalCount: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string | null; + endCursor: string | null; +}; diff --git a/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts b/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts index 11fe35d2db..2e4d8bfb24 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 @@ -24,6 +24,12 @@ export enum CommonQueryNames { DESTROY_ONE = 'destroyOne', DELETE_MANY = 'deleteMany', DELETE_ONE = 'deleteOne', + UPDATE_ONE = 'updateOne', + UPDATE_MANY = 'updateMany', + FIND_DUPLICATES = 'findDuplicates', + RESTORE_MANY = 'restoreMany', + RESTORE_ONE = 'restoreOne', + MERGE_MANY = 'mergeMany', } interface BaseQueryArgs { @@ -77,6 +83,35 @@ export interface DeleteManyQueryArgs { filter: ObjectRecordFilter; } +export interface UpdateOneQueryArgs { + id: string; + data: Partial; +} + +export interface UpdateManyQueryArgs { + filter: ObjectRecordFilter; + data: Partial; +} + +export interface FindDuplicatesQueryArgs { + data?: Partial[]; + ids?: string[]; +} + +export interface RestoreManyQueryArgs { + filter: ObjectRecordFilter; +} + +export interface RestoreOneQueryArgs { + id: string; +} + +export interface MergeManyQueryArgs { + ids: string[]; + conflictPriorityIndex: number; + dryRun?: boolean; +} + export type CommonQueryArgs = | FindOneQueryArgs | FindManyQueryArgs @@ -86,7 +121,12 @@ export type CommonQueryArgs = | DestroyOneQueryArgs | DestroyManyQueryArgs | DeleteOneQueryArgs - | DeleteManyQueryArgs; + | DeleteManyQueryArgs + | UpdateOneQueryArgs + | UpdateManyQueryArgs + | FindDuplicatesQueryArgs + | RestoreManyQueryArgs + | RestoreOneQueryArgs; export type CommonInput = T & BaseQueryArgs; 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 index f875ae64b2..310add8f9b 100644 --- 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 @@ -1,5 +1,6 @@ import { type ObjectRecord } from 'twenty-shared/types'; +import { type CommonFindDuplicatesOutputItem } from 'src/engine/api/common/types/common-find-duplicates-output-item.type'; 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'; @@ -7,4 +8,5 @@ export type CommonQueryResult = | ObjectRecord[] | ObjectRecord | CommonGroupByOutputItem[] - | CommonFindManyOutput; + | CommonFindManyOutput + | CommonFindDuplicatesOutputItem[]; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts index 94a6ea1b30..a3ceef4155 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import isEmpty from 'lodash.isempty'; +import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; import { OrderByDirection, type ObjectRecord } from 'twenty-shared/types'; import { In } from 'typeorm'; @@ -114,6 +115,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR .setFindOptions({ select: columnsToSelect, }) + .take(QUERY_MAX_RECORDS) .getMany()) as ObjectRecord[]; return typeORMObjectRecordsParser.createConnection({ 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 001b7dca2a..303827eba8 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 @@ -302,7 +302,7 @@ export class QueryRunnerArgsFactory { } } - private async overrideValueByFieldMetadata( + async overrideValueByFieldMetadata( key: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, 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 c6e3a27499..0e7fb8fa2b 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,7 +12,6 @@ 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'; @@ -52,7 +51,6 @@ export class CreateManyResolverFactory const records = await this.commonCreateManyQueryRunnerService.execute( { ...args, selectedFields }, internalContext, - CommonQueryNames.CREATE_MANY, ); const typeORMObjectRecordsParser = @@ -70,7 +68,7 @@ export class CreateManyResolverFactory }), ); } catch (error) { - return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } } 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 d80a015080..08951c3d23 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,7 +11,6 @@ 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'; @@ -49,7 +48,6 @@ export class CreateOneResolverFactory const record = await this.commonCreateOneQueryRunnerService.execute( { ...args, selectedFields }, internalContext, - CommonQueryNames.CREATE_ONE, ); const typeORMObjectRecordsParser = @@ -65,7 +63,7 @@ export class CreateOneResolverFactory totalCount: 1, }); } catch (error) { - return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } } 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 af46785953..3673ecd955 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 @@ -12,7 +12,6 @@ import { 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'; @@ -52,7 +51,6 @@ export class DeleteManyResolverFactory const records = await this.commonDeleteManyQueryRunnerService.execute( { ...args, selectedFields }, internalContext, - CommonQueryNames.DELETE_MANY, ); const typeORMObjectRecordsParser = @@ -70,7 +68,7 @@ export class DeleteManyResolverFactory }), ); } catch (error) { - return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } } 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 4a65947396..ae6287f1c5 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 @@ -11,7 +11,6 @@ import { 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'; @@ -51,7 +50,6 @@ export class DeleteOneResolverFactory const record = await this.commonDeleteOneQueryRunnerService.execute( { ...args, selectedFields }, internalContext, - CommonQueryNames.DELETE_ONE, ); const typeORMObjectRecordsParser = @@ -67,7 +65,7 @@ export class DeleteOneResolverFactory totalCount: 1, }); } catch (error) { - return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } } 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 b3438ccb3c..2d07795f5f 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 @@ -12,7 +12,6 @@ import { 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'; @@ -53,7 +52,6 @@ export class DestroyManyResolverFactory await this.commonDestroyManyQueryRunnerService.execute( { ...args, selectedFields }, internalContext, - CommonQueryNames.DESTROY_MANY, ); const typeORMObjectRecordsParser = @@ -71,7 +69,7 @@ export class DestroyManyResolverFactory }), ); } catch (error) { - return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } } 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 19b3c22ce3..9aa5f6e913 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 @@ -11,7 +11,6 @@ import { 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'; @@ -51,7 +50,6 @@ export class DestroyOneResolverFactory const record = await this.commonDestroyOneQueryRunnerService.execute( { ...args, selectedFields }, internalContext, - CommonQueryNames.DESTROY_ONE, ); const typeORMObjectRecordsParser = @@ -67,7 +65,7 @@ export class DestroyOneResolverFactory totalCount: 1, }); } catch (error) { - return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts index 1c2f1fd01a..e73ad82269 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts @@ -1,5 +1,8 @@ import { Injectable } from '@nestjs/common'; +import graphqlFields from 'graphql-fields'; +import { OrderByDirection } from 'twenty-shared/types'; + import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { @@ -8,8 +11,13 @@ 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 { CommonFindDuplicatesQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service'; +import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class FindDuplicatesResolverFactory @@ -19,6 +27,8 @@ export class FindDuplicatesResolverFactory constructor( private readonly graphqlQueryRunnerService: GraphqlQueryFindDuplicatesResolverService, + private readonly commonFindDuplicatesQueryRunnerService: CommonFindDuplicatesQueryRunnerService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -27,6 +37,45 @@ export class FindDuplicatesResolverFactory const internalContext = context; return async (_source, args, _context, info) => { + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId: internalContext.authContext.workspace?.id as string, + }); + + const featureFlagsMap = workspaceDataSource.featureFlagMap; + + if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { + const selectedFields = graphqlFields(info); + + try { + const paginatedDuplicates = + await this.commonFindDuplicatesQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, + ); + + return paginatedDuplicates.map((duplicate) => + typeORMObjectRecordsParser.createConnection({ + objectRecords: duplicate.records, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: duplicate.records.length, + totalCount: duplicate.totalCount, + order: [{ id: OrderByDirection.AscNullsFirst }], + hasNextPage: duplicate.hasNextPage, + hasPreviousPage: duplicate.hasPreviousPage, + }), + ); + } catch (error) { + 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 2b5a158773..9bec7c395e 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,7 +11,6 @@ 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'; @@ -55,7 +54,6 @@ export class FindManyResolverFactory } = await this.commonFindManyQueryRunnerService.execute( { ...args, selectedFields }, internalContext, - CommonQueryNames.FIND_MANY, ); const typeORMObjectRecordsParser = @@ -76,7 +74,7 @@ export class FindManyResolverFactory hasPreviousPage: pageInfo.hasPreviousPage, }); } catch (error) { - return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } } 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 1ac63d67ed..5ec044697a 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,7 +10,6 @@ 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'; @@ -48,7 +47,6 @@ export class FindOneResolverFactory const record = await this.commonFindOneQueryRunnerService.execute( { ...args, selectedFields }, internalContext, - CommonQueryNames.FIND_ONE, ); const typeORMObjectRecordsParser = @@ -64,7 +62,7 @@ export class FindOneResolverFactory totalCount: 1, }); } catch (error) { - return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } } 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 548f6b7c8e..fd1718e85f 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,7 +11,6 @@ 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'; @@ -48,10 +47,9 @@ export class GroupByResolverFactory return await this.commonGroupByQueryRunnerService.execute( { ...args, selectedFields }, internalContext, - CommonQueryNames.GROUP_BY, ); } catch (error) { - return workspaceQueryRunnerGraphqlApiExceptionHandler(error); + workspaceQueryRunnerGraphqlApiExceptionHandler(error); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts index a4ae45b8f0..b9f11a3edf 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts @@ -1,15 +1,22 @@ import { Injectable } from '@nestjs/common'; +import graphqlFields from 'graphql-fields'; + import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type MergeManyResolverArgs, type Resolver, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { CommonMergeManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-merge-many-query-runner.service'; +import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { GraphqlQueryMergeManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @Injectable() export class MergeManyResolverFactory @@ -19,6 +26,8 @@ export class MergeManyResolverFactory constructor( private readonly graphqlQueryRunnerService: GraphqlQueryMergeManyResolverService, + private readonly commonMergeManyQueryRunnerService: CommonMergeManyQueryRunnerService, + private readonly featureFlagService: FeatureFlagService, ) {} create( @@ -27,6 +36,37 @@ export class MergeManyResolverFactory const internalContext = context; return async (_source, args, _context, info) => { + const isCommonApiEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_COMMON_API_ENABLED, + internalContext.authContext.workspace?.id as string, + ); + + if (isCommonApiEnabled) { + const selectedFields = graphqlFields(info); + + try { + const record = await this.commonMergeManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, + ); + + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + } + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, info, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts index 1f84741195..a4a2fa379b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts @@ -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,13 @@ 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 { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service'; +import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class RestoreManyResolverFactory @@ -19,6 +27,8 @@ export class RestoreManyResolverFactory constructor( private readonly graphqlQueryRunnerService: GraphqlQueryRestoreManyResolverService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly commonRestoreManyQueryRunnerService: CommonRestoreManyQueryRunnerService, ) {} create( @@ -27,6 +37,41 @@ export class RestoreManyResolverFactory const internalContext = context; return async (_source, args, _context, info) => { + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId: internalContext.authContext.workspace?.id as string, + }); + + const featureFlagsMap = workspaceDataSource.featureFlagMap; + + if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { + const selectedFields = graphqlFields(info); + + try { + const records = + await this.commonRestoreManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, + ); + + return records.map((record: ObjectRecord) => + typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }), + ); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + } const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, info, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts index 3ee00f9238..384e525f6a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts @@ -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,13 @@ 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 { CommonRestoreOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-one-query-runner.service'; +import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class RestoreOneResolverFactory @@ -19,6 +26,8 @@ export class RestoreOneResolverFactory constructor( private readonly graphqlQueryRunnerService: GraphqlQueryRestoreOneResolverService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly commonRestoreOneQueryRunnerService: CommonRestoreOneQueryRunnerService, ) {} create( @@ -27,6 +36,39 @@ export class RestoreOneResolverFactory const internalContext = context; return async (_source, args, _context, info) => { + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId: internalContext.authContext.workspace?.id as string, + }); + + const featureFlagsMap = workspaceDataSource.featureFlagMap; + + if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { + const selectedFields = graphqlFields(info); + + try { + const record = await this.commonRestoreOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, + ); + + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + } + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, info, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts index c13edb3d7f..c28f5cbff5 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts @@ -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,15 +11,24 @@ 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 { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-many-query-runner.service'; +import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; + @Injectable() export class UpdateManyResolverFactory implements WorkspaceResolverBuilderFactoryInterface { public static methodName = RESOLVER_METHOD_NAMES.UPDATE_MANY; + constructor( private readonly graphqlQueryRunnerService: GraphqlQueryUpdateManyResolverService, + private readonly commonUpdateManyQueryRunnerService: CommonUpdateManyQueryRunnerService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -25,6 +37,41 @@ export class UpdateManyResolverFactory const internalContext = context; return async (_source, args, _context, info) => { + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId: internalContext.authContext.workspace?.id as string, + }); + + const featureFlagsMap = workspaceDataSource.featureFlagMap; + + if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { + const selectedFields = graphqlFields(info); + + try { + const records = await this.commonUpdateManyQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, + ); + + return records.map((record: ObjectRecord) => + typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }), + ); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + } + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, info, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts index 7686941006..cf74028db3 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts @@ -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,13 @@ 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 { CommonUpdateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-one-query-runner.service'; +import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class UpdateOneResolverFactory @@ -19,6 +26,8 @@ export class UpdateOneResolverFactory constructor( private readonly graphqlQueryRunnerService: GraphqlQueryUpdateOneResolverService, + private readonly commonUpdateOneQueryRunnerService: CommonUpdateOneQueryRunnerService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} create( @@ -27,6 +36,39 @@ export class UpdateOneResolverFactory const internalContext = context; return async (_source, args, _context, info) => { + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId: internalContext.authContext.workspace?.id as string, + }); + + const featureFlagsMap = workspaceDataSource.featureFlagMap; + + if (featureFlagsMap[FeatureFlagKey.IS_COMMON_API_ENABLED]) { + const selectedFields = graphqlFields(info); + + try { + const record = await this.commonUpdateOneQueryRunnerService.execute( + { ...args, selectedFields }, + internalContext, + ); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper( + internalContext.objectMetadataMaps, + ); + + return typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: + internalContext.objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + } + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, info, diff --git a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts index f9d5501d56..2a916e2450 100644 --- a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts +++ b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts @@ -106,6 +106,32 @@ export class RestApiCoreController { res.status(200).send(result); } + @Patch('restore/*') + async handleApiRestore( + @Req() request: AuthenticatedRequest, + @Res() res: Response, + ) { + this.logger.log( + `[REST API] Processing RESTORE request to ${request.path} on workspace ${request.workspaceId}`, + ); + const result = await this.restApiCoreService.restore(request); + + res.status(200).send(result); + } + + @Patch('*/merge') + async handleApiMerge( + @Req() request: AuthenticatedRequest, + @Res() res: Response, + ) { + this.logger.log( + `[REST API] Processing MERGE request to ${request.path} on workspace ${request.workspaceId}`, + ); + const result = await this.restApiCoreService.mergeMany(request); + + res.status(200).send(result); + } + @Patch('*') async handleApiPatch( @Req() request: AuthenticatedRequest, 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 797a223a99..89c1c7d607 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 @@ -11,7 +11,6 @@ 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'; @@ -49,7 +48,6 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { objectMetadataMaps, objectMetadataItemWithFieldMaps, }, - CommonQueryNames.CREATE_MANY, ); return this.formatRestResponse( 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 315fd5d6af..64a6ec081f 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 @@ -11,7 +11,6 @@ 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'; @@ -50,7 +49,6 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { objectMetadataMaps, objectMetadataItemWithFieldMaps, }, - CommonQueryNames.CREATE_ONE, ); return this.formatRestResponse( diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts index df52b529cd..03df516d3f 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts @@ -6,7 +6,6 @@ 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'; @@ -36,7 +35,6 @@ export class RestApiDeleteManyHandler extends RestApiBaseHandler { objectMetadataMaps, objectMetadataItemWithFieldMaps, }, - CommonQueryNames.DELETE_MANY, ); return this.formatRestResponse( 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 4b03e2db1a..1a552beb53 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 @@ -6,9 +6,7 @@ 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 { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; @@ -37,7 +35,6 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler { objectMetadataMaps, objectMetadataItemWithFieldMaps, }, - CommonQueryNames.DELETE_ONE, ); return this.formatRestResponse( @@ -64,7 +61,6 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler { 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 index 411a8905c3..0ca338d315 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts @@ -6,7 +6,6 @@ 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'; @@ -36,7 +35,6 @@ export class RestApiDestroyManyHandler extends RestApiBaseHandler { objectMetadataMaps, objectMetadataItemWithFieldMaps, }, - CommonQueryNames.DESTROY_MANY, ); return this.formatRestResponse( diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts index 3e98b5ed17..808c1f55bd 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts @@ -6,7 +6,6 @@ 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'; @@ -37,7 +36,6 @@ export class RestApiDestroyOneHandler extends RestApiBaseHandler { objectMetadataMaps, objectMetadataItemWithFieldMaps, }, - CommonQueryNames.DESTROY_ONE, ); return this.formatRestResponse( diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts index 7b92eb05d8..08183ba79b 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts @@ -10,11 +10,93 @@ import { RestApiBaseHandler, } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { CommonFindDuplicatesQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; +import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; @Injectable() export class RestApiFindDuplicatesHandler extends RestApiBaseHandler { + constructor( + private readonly commonFindDuplicatesQueryRunnerService: CommonFindDuplicatesQueryRunnerService, + ) { + super(); + } + + async commonHandle(request: AuthenticatedRequest) { + try { + this.validate(request); + + const { data, ids, depth } = this.parseRequestArgs(request); + + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + } = await this.buildCommonOptions(request); + + const selectedFields = await this.computeSelectedFields({ + depth, + objectMetadataMapItem: objectMetadataItemWithFieldMaps, + objectMetadataMaps, + authContext, + }); + + const duplicateConnections = + await this.commonFindDuplicatesQueryRunnerService.execute( + { data, ids, selectedFields }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + ); + + return this.formatRestResponse( + duplicateConnections, + objectMetadataItemWithFieldMaps.nameSingular, + ); + } catch (error) { + workspaceQueryRunnerRestApiExceptionHandler(error); + } + } + + private formatRestResponse( + duplicateConnections: Array<{ + records: ObjectRecord[]; + totalCount: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string | null; + endCursor: string | null; + }>, + objectNameSingular: string, + ) { + return { + data: duplicateConnections.map((connection) => ({ + data: { + [`${objectNameSingular}Duplicates`]: connection.records, + }, + totalCount: connection.totalCount, + pageInfo: { + hasNextPage: connection.hasNextPage, + hasPreviousPage: connection.hasPreviousPage, + startCursor: connection.startCursor, + endCursor: connection.endCursor, + }, + })), + }; + } + + private parseRequestArgs(request: AuthenticatedRequest) { + return { + data: request.body.data, + ids: request.body.ids, + depth: parseDepthRestRequest(request), + }; + } + async handle(request: AuthenticatedRequest) { this.validate(request); 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 2973fd3e20..51aba0d884 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,7 +8,6 @@ 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'; @@ -118,7 +117,6 @@ export class RestApiFindManyHandler extends RestApiBaseHandler { objectMetadataMaps, objectMetadataItemWithFieldMaps, }, - CommonQueryNames.FIND_MANY, ); return this.formatRestResponse( 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 912177c72a..b68140a20a 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,7 +6,6 @@ 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'; @@ -81,7 +80,6 @@ export class RestApiFindOneHandler extends RestApiBaseHandler { objectMetadataMaps, objectMetadataItemWithFieldMaps, }, - CommonQueryNames.FIND_ONE, ); return this.formatRestResponse( 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 17b49a1593..bb29b2b532 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,7 +3,6 @@ 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'; @@ -44,10 +43,9 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { objectMetadataMaps, objectMetadataItemWithFieldMaps, }, - CommonQueryNames.GROUP_BY, ); } catch (error) { - throw workspaceQueryRunnerRestApiExceptionHandler(error); + return workspaceQueryRunnerRestApiExceptionHandler(error); } } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts new file mode 100644 index 0000000000..7129e48e31 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-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 { CommonMergeManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-merge-many-query-runner.service'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; +import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; +import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; + +@Injectable() +export class RestApiMergeManyHandler extends RestApiBaseHandler { + constructor( + private readonly commonMergeManyQueryRunnerService: CommonMergeManyQueryRunnerService, + ) { + super(); + } + + async handle(request: AuthenticatedRequest) { + try { + const { depth, ...restArgs } = await this.parseRequestArgs(request); + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + } = await this.buildCommonOptions(request); + + const selectedFields = await this.computeSelectedFields({ + depth, + objectMetadataMapItem: objectMetadataItemWithFieldMaps, + objectMetadataMaps, + authContext, + }); + + const record = await this.commonMergeManyQueryRunnerService.execute( + { ...restArgs, selectedFields }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + ); + + return this.formatRestResponse( + record, + objectMetadataItemWithFieldMaps.nameSingular, + ); + } catch (error) { + return workspaceQueryRunnerRestApiExceptionHandler(error); + } + } + + private formatRestResponse(record: ObjectRecord, objectNamePlural: string) { + return { data: { [`merge${capitalize(objectNamePlural)}`]: record } }; + } + + private async parseRequestArgs(request: AuthenticatedRequest) { + const depth = parseDepthRestRequest(request); + + return { + conflictPriorityIndex: request.body.conflictPriorityIndex, + dryRun: request.body.dryRun, + ids: request.body.ids, + depth, + }; + } +} diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts new file mode 100644 index 0000000000..53e23a7aa5 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts @@ -0,0 +1,80 @@ +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 { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; +import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; +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 RestApiRestoreManyHandler extends RestApiBaseHandler { + constructor( + private readonly commonRestoreManyQueryRunnerService: CommonRestoreManyQueryRunnerService, + ) { + super(); + } + + async handle(request: AuthenticatedRequest): Promise<{ + data: { + [x: string]: ObjectRecord[]; + }; + }> { + try { + const { filter, depth } = this.parseRequestArgs(request); + + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + } = await this.buildCommonOptions(request); + + const selectedFields = await this.computeSelectedFields({ + depth, + objectMetadataMapItem: objectMetadataItemWithFieldMaps, + objectMetadataMaps, + authContext, + }); + + const records = await this.commonRestoreManyQueryRunnerService.execute( + { filter, selectedFields }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + ); + + return this.formatRestResponse( + records, + objectMetadataItemWithFieldMaps.namePlural, + ); + } catch (error) { + return workspaceQueryRunnerRestApiExceptionHandler(error); + } + } + + private formatRestResponse( + records: ObjectRecord[], + objectNamePlural: string, + ) { + return { + data: { + [`restore${capitalize(objectNamePlural)}`]: records, + }, + }; + } + + private parseRequestArgs(request: AuthenticatedRequest) { + const filter = parseFilterRestRequest(request); + + return { + filter, + depth: parseDepthRestRequest(request), + }; + } +} diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts new file mode 100644 index 0000000000..104f5a9646 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts @@ -0,0 +1,75 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; + +import { ObjectRecord } from 'twenty-shared/types'; +import { capitalize, isDefined } from 'twenty-shared/utils'; + +import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; + +import { CommonRestoreOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-one-query-runner.service'; +import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; +import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; +import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; + +@Injectable() +export class RestApiRestoreOneHandler extends RestApiBaseHandler { + constructor( + private readonly commonRestoreOneQueryRunnerService: CommonRestoreOneQueryRunnerService, + ) { + super(); + } + + async handle(request: AuthenticatedRequest) { + try { + const { id, depth } = this.parseRequestArgs(request); + + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + } = await this.buildCommonOptions(request); + + const selectedFields = await this.computeSelectedFields({ + depth, + objectMetadataMapItem: objectMetadataItemWithFieldMaps, + objectMetadataMaps, + authContext, + }); + + const record = await this.commonRestoreOneQueryRunnerService.execute( + { id, selectedFields }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + ); + + return this.formatRestResponse( + record, + objectMetadataItemWithFieldMaps.nameSingular, + ); + } catch (error) { + return workspaceQueryRunnerRestApiExceptionHandler(error); + } + } + + private formatRestResponse(record: ObjectRecord, objectNameSingular: string) { + return { + data: { [`restore${capitalize(objectNameSingular)}`]: record }, + }; + } + + private parseRequestArgs(request: AuthenticatedRequest) { + const { id } = parseCorePath(request); + + if (!isDefined(id)) { + throw new BadRequestException('Record ID not found'); + } + + return { + id, + depth: parseDepthRestRequest(request), + }; + } +} diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts new file mode 100644 index 0000000000..beeef56867 --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts @@ -0,0 +1,75 @@ +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 { CommonUpdateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-many-query-runner.service'; +import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; +import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; +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 RestApiUpdateManyHandler extends RestApiBaseHandler { + constructor( + private readonly commonUpdateManyQueryRunnerService: CommonUpdateManyQueryRunnerService, + ) { + super(); + } + + async handle(request: AuthenticatedRequest) { + try { + const { data, depth, filter } = this.parseRequestArgs(request); + + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + } = await this.buildCommonOptions(request); + + const selectedFields = await this.computeSelectedFields({ + depth, + objectMetadataMapItem: objectMetadataItemWithFieldMaps, + objectMetadataMaps, + authContext, + }); + + const records = await this.commonUpdateManyQueryRunnerService.execute( + { data, filter, selectedFields }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + ); + + return this.formatRestResponse( + records, + objectMetadataItemWithFieldMaps.namePlural, + ); + } catch (error) { + return workspaceQueryRunnerRestApiExceptionHandler(error); + } + } + + private formatRestResponse( + records: ObjectRecord[], + objectNamePlural: string, + ) { + return { + data: { + [`update${capitalize(objectNamePlural)}`]: records, + }, + }; + } + + private parseRequestArgs(request: AuthenticatedRequest) { + return { + data: request.body, + depth: parseDepthRestRequest(request), + filter: parseFilterRestRequest(request), + }; + } +} diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts index 2fa3f5858d..10b948b721 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts @@ -5,17 +5,79 @@ import { } from '@nestjs/common'; import isEmpty from 'lodash.isempty'; -import { isDefined } from 'twenty-shared/utils'; +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 { CommonUpdateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-one-query-runner.service'; import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; +import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; @Injectable() export class RestApiUpdateOneHandler extends RestApiBaseHandler { + constructor( + private readonly commonUpdateOneQueryRunnerService: CommonUpdateOneQueryRunnerService, + ) { + super(); + } + + async commonHandle(request: AuthenticatedRequest) { + try { + const { id, data, depth } = this.parseRequestArgs(request); + + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + } = await this.buildCommonOptions(request); + + const selectedFields = await this.computeSelectedFields({ + depth, + objectMetadataMapItem: objectMetadataItemWithFieldMaps, + objectMetadataMaps, + authContext, + }); + + const record = await this.commonUpdateOneQueryRunnerService.execute( + { id, data, selectedFields }, + { + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }, + ); + + return this.formatRestResponse( + record, + objectMetadataItemWithFieldMaps.nameSingular, + ); + } catch (error) { + workspaceQueryRunnerRestApiExceptionHandler(error); + } + } + + private formatRestResponse(record: ObjectRecord, objectNameSingular: string) { + return { data: { [`update${capitalize(objectNameSingular)}`]: record } }; + } + + private parseRequestArgs(request: AuthenticatedRequest) { + const { id } = parseCorePath(request); + + if (!id) { + throw new BadRequestException('Record ID not found'); + } + + return { + id, + data: request.body, + depth: parseDepthRestRequest(request), + }; + } + async handle(request: AuthenticatedRequest) { const { id: recordId } = parseCorePath(request); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils.ts index 247d0c7529..f4bb56fe06 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { type Request } from 'express'; -import { isValidUuid } from 'twenty-shared/utils'; +import { isDefined, isValidUuid } from 'twenty-shared/utils'; export const parseCorePath = ( request: Request, @@ -12,7 +12,10 @@ export const parseCorePath = ( .split('/') .filter(Boolean); - if (queryAction.length > 2) { + if ( + queryAction.length > 2 || + (queryAction.length > 3 && queryAction[0] === 'restore') + ) { throw new BadRequestException( `Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies or /rest/batch/companies`, ); @@ -32,10 +35,27 @@ export const parseCorePath = ( return { object: queryAction[1] }; } - if (queryAction[1] === 'duplicates' || queryAction[1] === 'group') { + if ( + queryAction[1] === 'duplicates' || + queryAction[1] === 'group' || + queryAction[1] === 'merge' + ) { return { object: queryAction[0] }; } + if (queryAction[0] === 'restore') { + const recordId = queryAction[2]; + + if (isDefined(recordId) && !isValidUuid(recordId)) { + throw new BadRequestException(`'${recordId}' is not a valid UUID`); + } + + return { + object: queryAction[1], + id: recordId, + }; + } + const recordId = queryAction[1]; if (!isValidUuid(recordId)) { 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 ad3fe7b6ed..cedde75502 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 @@ -13,6 +13,10 @@ import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/ 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 { RestApiMergeManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-merge-many.handler'; +import { RestApiRestoreManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-many.handler'; +import { RestApiRestoreOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-one.handler'; +import { RestApiUpdateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-many.handler'; import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler'; import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module'; import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories'; @@ -33,6 +37,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ const restApiCoreResolvers = [ RestApiCreateOneHandler, RestApiCreateManyHandler, + RestApiUpdateOneHandler, + RestApiUpdateManyHandler, RestApiFindOneHandler, RestApiFindManyHandler, RestApiFindDuplicatesHandler, @@ -42,6 +48,9 @@ const restApiCoreResolvers = [ RestApiDestroyManyHandler, RestApiDeleteOneHandler, RestApiDeleteManyHandler, + RestApiRestoreOneHandler, + RestApiRestoreManyHandler, + RestApiMergeManyHandler, ]; @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 a65d70d030..e8a617dc54 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 @@ -12,6 +12,10 @@ import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/ 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 { RestApiMergeManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-merge-many.handler'; +import { RestApiRestoreManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-many.handler'; +import { RestApiRestoreOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-restore-one.handler'; +import { RestApiUpdateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-many.handler'; import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler'; import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; import { parseSoftDeleteRestRequest } from 'src/engine/api/rest/input-request-parsers/soft-delete-parser-utils/parse-soft-delete-rest-request.util'; @@ -24,15 +28,19 @@ export class RestApiCoreService { constructor( private readonly restApiCreateOneHandler: RestApiCreateOneHandler, private readonly restApiCreateManyHandler: RestApiCreateManyHandler, + private readonly restApiUpdateOneHandler: RestApiUpdateOneHandler, + private readonly restApiUpdateManyHandler: RestApiUpdateManyHandler, 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 restApiRestoreOneHandler: RestApiRestoreOneHandler, + private readonly restApiRestoreManyHandler: RestApiRestoreManyHandler, + private readonly restApiMergeManyHandler: RestApiMergeManyHandler, private readonly featureFlagService: FeatureFlagService, ) {} @@ -63,6 +71,37 @@ export class RestApiCoreService { } } + async findDuplicates(request: AuthenticatedRequest) { + const isCommonApiEnabled = await this.isCommonApiEnabled(request); + + if (isCommonApiEnabled) { + return await this.restApiFindDuplicatesHandler.commonHandle(request); + } else { + return await this.restApiFindDuplicatesHandler.handle(request); + } + } + + async update(request: AuthenticatedRequest) { + const { id: recordId } = parseCorePath(request); + const isCommonApiEnabled = await this.isCommonApiEnabled(request); + + if (isCommonApiEnabled) { + if (isDefined(recordId)) { + return await this.restApiUpdateOneHandler.commonHandle(request); + } else { + return await this.restApiUpdateManyHandler.handle(request); + } + } else { + if (isDefined(recordId)) { + return await this.restApiUpdateOneHandler.handle(request); + } else { + throw new BadRequestException( + 'Activate feature flag to use UpdateMany in the REST API', + ); + } + } + } + async get(request: AuthenticatedRequest) { const { id: recordId } = parseCorePath(request); const isCommonApiEnabled = await this.isCommonApiEnabled(request); @@ -94,14 +133,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 delete(request: AuthenticatedRequest) { const { id: recordId } = parseCorePath(request); @@ -125,4 +156,34 @@ export class RestApiCoreService { 'Activate feature flag IS_COMMON_API_ENABLED to use Delete in the REST API', ); } + + async restore(request: AuthenticatedRequest) { + const { id: recordId } = parseCorePath(request); + + const isCommonApiEnabled = await this.isCommonApiEnabled(request); + + if (isCommonApiEnabled) { + if (isDefined(recordId)) { + return await this.restApiRestoreOneHandler.handle(request); + } else { + return await this.restApiRestoreManyHandler.handle(request); + } + } else { + throw new BadRequestException( + 'Activate feature flag to use Restore in the REST API', + ); + } + } + + async mergeMany(request: AuthenticatedRequest) { + const isCommonApiEnabled = await this.isCommonApiEnabled(request); + + if (isCommonApiEnabled) { + return await this.restApiMergeManyHandler.handle(request); + } else { + throw new BadRequestException( + 'Activate feature flag to use Merge in the REST API', + ); + } + } } diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts index 7f511bfb61..218c3489a0 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts @@ -28,6 +28,9 @@ import { computeBatchPath, computeDuplicatesResultPath, computeManyResultPath, + computeMergeManyResultPath, + computeRestoreManyResultPath, + computeRestoreOneResultPath, computeSingleResultPath, } from 'src/engine/core-modules/open-api/utils/path.utils'; import { @@ -43,12 +46,12 @@ import { } from 'src/engine/core-modules/open-api/utils/responses.utils'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; import { getServerUrl } from 'src/utils/get-server-url'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; @Injectable() export class OpenApiService { @@ -125,6 +128,10 @@ export class OpenApiService { paths[`/${item.namePlural}/{id}`] = computeSingleResultPath(item); paths[`/${item.namePlural}/duplicates`] = computeDuplicatesResultPath(item); + paths[`/restore/${item.namePlural}/{id}`] = + computeRestoreOneResultPath(item); + paths[`/restore/${item.namePlural}`] = computeRestoreManyResultPath(item); + paths[`/${item.namePlural}/merge`] = computeMergeManyResultPath(item); return paths; }, schema.paths as OpenAPIV3_1.PathsObject); 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 3cdf36fe4a..17d3de5acb 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 @@ -4,6 +4,7 @@ import { capitalize } from 'twenty-shared/utils'; import { getArrayRequestBody, getFindDuplicatesRequestBody, + getMergeManyRequestBody, getRequestBody, getUpdateRequestBody, } from 'src/engine/core-modules/open-api/utils/request-body.utils'; @@ -16,6 +17,10 @@ import { getFindManyResponse200, getFindOneResponse200, getJsonResponse, + getMergeManyResponse200, + getRestoreManyResponse200, + getRestoreOneResponse200, + getUpdateManyResponse200, getUpdateOneResponse200, } from 'src/engine/core-modules/open-api/utils/responses.utils'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @@ -94,6 +99,21 @@ export const computeManyResultPath = ( '401': { $ref: '#/components/responses/401' }, }, }, + patch: { + tags: [item.namePlural], + summary: `Update Many ${item.namePlural}`, + operationId: `updateMany${capitalize(item.namePlural)}`, + parameters: [ + { $ref: '#/components/parameters/depth' }, + { $ref: '#/components/parameters/filter' }, + ], + requestBody: getUpdateRequestBody(capitalize(item.nameSingular)), + responses: { + '200': getUpdateManyResponse200(item), + '400': { $ref: '#/components/responses/400' }, + '401': { $ref: '#/components/responses/401' }, + }, + }, } as OpenAPIV3_1.PathItemObject; }; @@ -187,3 +207,64 @@ export const computeDuplicatesResultPath = ( }, } as OpenAPIV3_1.PathItemObject; }; + +export const computeRestoreOneResultPath = ( + item: ObjectMetadataEntity, +): OpenAPIV3_1.PathItemObject => { + return { + patch: { + tags: [item.namePlural], + summary: `Restore One ${item.nameSingular}`, + operationId: `restoreOne${capitalize(item.nameSingular)}`, + parameters: [ + { $ref: '#/components/parameters/idPath' }, + { $ref: '#/components/parameters/depth' }, + ], + responses: { + '200': getRestoreOneResponse200(item), + '400': { $ref: '#/components/responses/400' }, + '401': { $ref: '#/components/responses/401' }, + }, + }, + } as OpenAPIV3_1.PathItemObject; +}; + +export const computeRestoreManyResultPath = ( + item: ObjectMetadataEntity, +): OpenAPIV3_1.PathItemObject => { + return { + patch: { + tags: [item.namePlural], + summary: `Restore Many ${item.namePlural}`, + operationId: `restoreMany${capitalize(item.namePlural)}`, + parameters: [ + { $ref: '#/components/parameters/filter' }, + { $ref: '#/components/parameters/depth' }, + ], + responses: { + '200': getRestoreManyResponse200(item), + '400': { $ref: '#/components/responses/400' }, + '401': { $ref: '#/components/responses/401' }, + }, + }, + } as OpenAPIV3_1.PathItemObject; +}; + +export const computeMergeManyResultPath = ( + item: ObjectMetadataEntity, +): OpenAPIV3_1.PathItemObject => { + return { + patch: { + tags: [item.namePlural], + summary: `Merge Many ${item.namePlural}`, + operationId: `mergeMany${capitalize(item.namePlural)}`, + parameters: [{ $ref: '#/components/parameters/depth' }], + requestBody: getMergeManyRequestBody(), + responses: { + '200': getMergeManyResponse200(item), + '400': { $ref: '#/components/responses/400' }, + '401': { $ref: '#/components/responses/401' }, + }, + }, + } as OpenAPIV3_1.PathItemObject; +}; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/request-body.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/request-body.utils.ts index 93055f0854..8606962391 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/request-body.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/request-body.utils.ts @@ -73,3 +73,40 @@ export const getFindDuplicatesRequestBody = (name: string) => { }, }; }; + +export const getMergeManyRequestBody = () => { + return { + description: 'body', + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + ids: { + type: 'array', + description: 'The IDs of the records to merge', + items: { + type: 'string', + format: 'uuid', + }, + }, + conflictPriorityIndex: { + type: 'number', + description: + 'The index of the record to use when conflicts occur', + }, + dryRun: { + description: + 'If true, the merge will not be performed and a preview of the merge will be returned.', + type: 'boolean', + default: false, + }, + }, + example: { ids: [v4()], conflictPriorityIndex: 0, dryRun: false }, + required: ['ids', 'conflictPriorityIndex'], + }, + }, + }, + }; +}; 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 19f580ba95..88aba47abe 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 @@ -81,6 +81,65 @@ export const getFindOneResponse200 = ( }; }; +export const getRestoreOneResponse200 = ( + item: Pick, +) => { + const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)}ForResponse`; + + return { + description: 'Successful operation', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + data: { + type: 'object', + properties: { + [`restore${capitalize(item.nameSingular)}`]: { + $ref: schemaRef, + }, + }, + }, + }, + }, + }, + }, + }; +}; + +export const getRestoreManyResponse200 = ( + item: Pick, +) => { + const schemaRef = `#/components/schemas/${capitalize( + item.nameSingular, + )}ForResponse`; + + return { + description: 'Successful operation', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + data: { + type: 'object', + properties: { + [`restore${capitalize(item.namePlural)}`]: { + type: 'array', + items: { + $ref: schemaRef, + }, + }, + }, + }, + }, + }, + }, + }, + }; +}; + export const getCreateOneResponse201 = ( item: Pick, fromMetadata = false, @@ -208,6 +267,36 @@ export const getDeleteManyResponse200 = ( }; }; +export const getUpdateManyResponse200 = ( + item: Pick, +) => { + const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)}ForResponse`; + + return { + description: 'Successful operation', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + data: { + type: 'object', + properties: { + [`update${capitalize(item.namePlural)}`]: { + type: 'array', + items: { + $ref: schemaRef, + }, + }, + }, + }, + }, + }, + }, + }, + }; +}; + export const getDeleteResponse200 = ( item: Pick, fromMetadata = false, @@ -350,3 +439,32 @@ export const getFindDuplicatesResponse200 = ( }, }; }; + +export const getMergeManyResponse200 = ( + item: Pick, +) => { + const schemaRef = `#/components/schemas/${capitalize( + item.nameSingular, + )}ForResponse`; + + return { + description: 'Successful operation', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + data: { + type: 'object', + properties: { + [`merge${capitalize(item.namePlural)}`]: { + $ref: schemaRef, + }, + }, + }, + }, + }, + }, + }, + }; +};