feat: merge records (#13436)

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
neo773
2025-07-31 10:06:36 +00:00
committed by GitHub
co-authored by Félix Malfait
parent 00d12e854a
commit 06e0814ce8
45 changed files with 2047 additions and 29 deletions
@@ -13,6 +13,7 @@ import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/gr
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
import { GraphqlQueryMergeManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service';
import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service';
import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service';
import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service';
@@ -33,6 +34,7 @@ const graphqlQueryResolvers = [
GraphqlQueryFindDuplicatesResolverService,
GraphqlQueryFindManyResolverService,
GraphqlQueryFindOneResolverService,
GraphqlQueryMergeManyResolverService,
GraphqlQueryRestoreManyResolverService,
GraphqlQueryRestoreOneResolverService,
GraphqlQueryUpdateManyResolverService,
@@ -0,0 +1,437 @@
import { Injectable, Logger } from '@nestjs/common';
import {
MUTATION_MAX_MERGE_RECORDS,
QUERY_MAX_RECORDS,
} from 'twenty-shared/constants';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import {
GraphqlQueryBaseResolverService,
GraphqlQueryResolverExecutionArgs,
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { MergeManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { FieldMetadataRelationSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { 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 GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolverService<
MergeManyResolverArgs,
ObjectRecord
> {
private readonly logger = new Logger(
GraphqlQueryMergeManyResolverService.name,
);
async resolve(
executionArgs: GraphqlQueryResolverExecutionArgs<MergeManyResolverArgs>,
): Promise<ObjectRecord> {
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
executionArgs.options;
const { ids, conflictPriorityIndex, dryRun } = executionArgs.args;
const { roleId } = executionArgs;
const recordsToMerge = await this.fetchRecordsToMerge(executionArgs, ids);
const priorityRecord = this.validateAndGetPriorityRecord(
recordsToMerge,
ids,
conflictPriorityIndex,
);
const mergedData = this.performDeepMerge(
recordsToMerge,
priorityRecord.id,
objectMetadataItemWithFieldMaps,
);
if (dryRun) {
return this.createDryRunResponse(
priorityRecord,
mergedData,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
}
const idsToDelete = ids.filter((id) => id !== priorityRecord.id);
await this.migrateRelatedRecords(
executionArgs,
idsToDelete,
priorityRecord.id,
);
const queryBuilder = executionArgs.repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
);
await queryBuilder
.softDelete()
.whereInIds(idsToDelete)
.returning('*')
.execute();
const updatedRecord = await this.updatePriorityRecord(
executionArgs,
priorityRecord.id,
mergedData,
);
if (roleId) {
await this.processNestedRelations(
executionArgs,
[updatedRecord],
authContext,
roleId,
);
}
return this.formatResponse(
updatedRecord,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
}
private async fetchRecordsToMerge(
executionArgs: GraphqlQueryResolverExecutionArgs<MergeManyResolverArgs>,
ids: string[],
): Promise<ObjectRecord[]> {
const recordsToMerge = await executionArgs.repository.find({
where: { id: In(ids) },
});
if (recordsToMerge.length !== ids.length) {
throw new GraphqlQueryRunnerException(
'One or more records not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
return recordsToMerge as ObjectRecord[];
}
private validateAndGetPriorityRecord(
recordsToMerge: ObjectRecord[],
ids: string[],
conflictPriorityIndex: number,
): ObjectRecord {
const priorityRecordId = ids[conflictPriorityIndex];
const priorityRecord = recordsToMerge.find(
(record) => record.id === priorityRecordId,
);
if (!priorityRecord) {
throw new GraphqlQueryRunnerException(
'Priority record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
return priorityRecord;
}
private performDeepMerge(
recordsToMerge: ObjectRecord[],
priorityRecordId: string,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
): Partial<ObjectRecord> {
const mergedResult: Partial<ObjectRecord> = {};
const allFieldNames = new Set<string>();
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 priorityValue = recordsWithValues.find(
(item) => item.recordId === priorityRecordId,
);
if (priorityValue) {
mergedResult[fieldName] = priorityValue.value;
} else {
mergedResult[fieldName] = recordsWithValues[0].value;
}
}
});
return mergedResult;
}
private shouldExcludeFieldFromMerge(
fieldName: string,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
): boolean {
const fieldMetadata = Object.values(
objectMetadataItemWithFieldMaps.fieldsById,
).find((field) => field?.name === fieldName);
if (fieldMetadata?.isSystem) {
return true;
}
return false;
}
private createDryRunResponse(
priorityRecord: ObjectRecord,
mergedData: Partial<ObjectRecord>,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
): ObjectRecord {
const dryRunRecord = {
...priorityRecord,
...mergedData,
id: uuidv4(),
deletedAt: new Date().toISOString(),
} as ObjectRecord;
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return typeORMObjectRecordsParser.processRecord({
objectRecord: dryRunRecord,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
});
}
private async updatePriorityRecord(
executionArgs: GraphqlQueryResolverExecutionArgs<MergeManyResolverArgs>,
priorityRecordId: string,
mergedData: Partial<ObjectRecord>,
): Promise<ObjectRecord> {
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
const queryBuilder = executionArgs.repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
);
const columnsToReturn = buildColumnsToReturn({
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
});
const updatedObjectRecords = await queryBuilder
.update()
.set(mergedData)
.where({ id: priorityRecordId })
.returning(columnsToReturn)
.execute();
if (!updatedObjectRecords.generatedMaps.length) {
throw new GraphqlQueryRunnerException(
'Failed to update record',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const updatedRecord = updatedObjectRecords.generatedMaps[0] as ObjectRecord;
return updatedRecord;
}
private async migrateRelatedRecords(
executionArgs: GraphqlQueryResolverExecutionArgs<MergeManyResolverArgs>,
fromIds: string[],
toId: string,
): Promise<void> {
const { objectMetadataMaps, objectMetadataItemWithFieldMaps } =
executionArgs.options;
const relationFieldsPointingToCurrentObject = Object.values(
objectMetadataMaps.byId,
)
.filter(isDefined)
.flatMap((metadata) => {
const relationFields = Object.values(metadata.fieldsById)
.filter(isDefined)
.filter((field) =>
isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION),
)
.filter(
(field) =>
field.relationTargetObjectMetadataId ===
objectMetadataItemWithFieldMaps.id && field.isActive,
);
return relationFields
.filter((field) => {
const relationSettings =
field.settings as FieldMetadataRelationSettings;
return (
relationSettings?.relationType === RelationType.MANY_TO_ONE &&
relationSettings?.joinColumnName
);
})
.map((field) => {
const relationSettings =
field.settings as FieldMetadataRelationSettings;
return {
objectMetadata: metadata,
fieldName: field.name,
fieldId: field.id,
joinColumnName: relationSettings.joinColumnName,
};
});
});
for (const relationField of relationFieldsPointingToCurrentObject) {
if (!relationField.joinColumnName) {
continue;
}
try {
const repository = executionArgs.workspaceDataSource.getRepository(
relationField.objectMetadata.nameSingular,
executionArgs.isExecutedByApiKey,
executionArgs.roleId,
);
const whereCondition = { [relationField.joinColumnName]: In(fromIds) };
const existingRecords = await repository.find({
where: whereCondition,
});
if (existingRecords.length > 0) {
await repository.update(whereCondition, {
[relationField.joinColumnName]: toId,
});
}
} catch (error) {
this.logger.warn(
`Failed to migrate relation field "${relationField.fieldName}" (${relationField.joinColumnName}) in object "${relationField.objectMetadata.nameSingular}":`,
error.message,
);
}
}
}
private async processNestedRelations(
executionArgs: GraphqlQueryResolverExecutionArgs<MergeManyResolverArgs>,
updatedRecords: ObjectRecord[],
authContext: AuthContext,
roleId: string,
): Promise<void> {
const { objectMetadataMaps, objectMetadataItemWithFieldMaps } =
executionArgs.options;
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: updatedRecords,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource: executionArgs.workspaceDataSource,
roleId,
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
});
}
}
private formatResponse(
updatedRecord: ObjectRecord,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
): ObjectRecord {
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return typeORMObjectRecordsParser.processRecord({
objectRecord: updatedRecord,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
});
}
async validate(
args: MergeManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<void> {
assertMutationNotOnRemoteObject(options.objectMetadataItemWithFieldMaps);
if (!isDefined(options.objectMetadataItemWithFieldMaps.duplicateCriteria)) {
throw new GraphqlQueryRunnerException(
`Merge is only available for objects with duplicate criteria. Object '${options.objectMetadataItemWithFieldMaps.nameSingular}' does not have duplicate criteria defined.`,
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
const { ids, conflictPriorityIndex } = args;
if (!ids || ids.length < 2) {
throw new GraphqlQueryRunnerException(
'At least 2 record IDs are required for merge',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
if (ids.length > MUTATION_MAX_MERGE_RECORDS) {
throw new GraphqlQueryRunnerException(
`Maximum ${MUTATION_MAX_MERGE_RECORDS} records can be merged at once`,
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
if (conflictPriorityIndex < 0 || conflictPriorityIndex >= ids.length) {
throw new GraphqlQueryRunnerException(
`Invalid conflict priority '${conflictPriorityIndex}'. Valid options for ${ids.length} records: 0-${ids.length - 1}`,
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
}
}
@@ -0,0 +1,29 @@
export const hasRecordFieldValue = (value: unknown): boolean => {
if (value === null || value === undefined) {
return false;
}
if (typeof value === 'string') {
return value.trim() !== '';
}
if (typeof value === 'number') {
return !isNaN(value);
}
if (typeof value === 'boolean') {
return true;
}
if (Array.isArray(value)) {
return value.length > 0;
}
if (typeof value === 'object') {
const objectValue = value as Record<string, unknown>;
return Object.values(objectValue).some((val) => hasRecordFieldValue(val));
}
return true;
};
@@ -14,6 +14,7 @@ import {
FindDuplicatesResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
MergeManyResolverArgs,
ResolverArgs,
ResolverArgsType,
UpdateManyResolverArgs,
@@ -145,6 +146,22 @@ export class QueryRunnerArgsFactory {
) ?? [],
),
} satisfies FindDuplicatesResolverArgs;
case ResolverArgsType.MergeMany:
return {
...args,
ids: (await Promise.all(
(args as MergeManyResolverArgs).ids?.map((id) =>
this.overrideValueByFieldMetadata(
'id',
id,
fieldMetadataMapByNameByName,
),
) ?? [],
)) as string[],
conflictPriorityIndex: (args as MergeManyResolverArgs)
.conflictPriorityIndex,
dryRun: (args as MergeManyResolverArgs).dryRun,
} satisfies MergeManyResolverArgs;
default:
return args;
}
@@ -8,6 +8,7 @@ import {
FindDuplicatesResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
MergeManyResolverArgs,
RestoreManyResolverArgs,
UpdateManyResolverArgs,
UpdateOneResolverArgs,
@@ -42,4 +43,6 @@ export type WorkspacePreQueryHookPayload<T> = T extends 'createMany'
? DestroyManyResolverArgs
: T extends 'destroyOne'
? DestroyOneResolverArgs
: never;
: T extends 'mergeMany'
? MergeManyResolverArgs
: never;
@@ -12,4 +12,5 @@ export const RESOLVER_METHOD_NAMES = {
RESTORE_ONE: 'restoreOne',
DESTROY_MANY: 'destroyMany',
DESTROY_ONE: 'destroyOne',
MERGE_MANY: 'mergeMany',
} as const;
@@ -11,6 +11,7 @@ import { DeleteOneResolverFactory } from './delete-one-resolver.factory';
import { FindDuplicatesResolverFactory } from './find-duplicates-resolver.factory';
import { FindManyResolverFactory } from './find-many-resolver.factory';
import { FindOneResolverFactory } from './find-one-resolver.factory';
import { MergeManyResolverFactory } from './merge-many-resolver.factory';
import { UpdateOneResolverFactory } from './update-one-resolver.factory';
export const workspaceResolverBuilderFactories = [
@@ -27,6 +28,7 @@ export const workspaceResolverBuilderFactories = [
DestroyManyResolverFactory,
RestoreOneResolverFactory,
RestoreManyResolverFactory,
MergeManyResolverFactory,
];
export const workspaceResolverBuilderMethodNames = {
@@ -46,5 +48,6 @@ export const workspaceResolverBuilderMethodNames = {
DestroyManyResolverFactory.methodName,
RestoreOneResolverFactory.methodName,
RestoreManyResolverFactory.methodName,
MergeManyResolverFactory.methodName,
],
} as const;
@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
MergeManyResolverArgs,
Resolver,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryMergeManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service';
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
@Injectable()
export class MergeManyResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = RESOLVER_METHOD_NAMES.MERGE_MANY;
constructor(
private readonly graphqlQueryRunnerService: GraphqlQueryMergeManyResolverService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<MergeManyResolverArgs> {
const internalContext = context;
return async (_source, args, _context, info) => {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
MergeManyResolverFactory.methodName,
);
};
}
}
@@ -26,6 +26,7 @@ export enum ResolverArgsType {
RestoreMany = 'RestoreMany',
DestroyMany = 'DestroyMany',
DestroyOne = 'DestroyOne',
MergeMany = 'MergeMany',
}
export interface FindManyResolverArgs<
@@ -99,6 +100,12 @@ export interface RestoreManyResolverArgs<Filter = any> {
filter: Filter;
}
export interface MergeManyResolverArgs {
ids: string[];
conflictPriorityIndex: number;
dryRun?: boolean;
}
export interface DestroyOneResolverArgs {
id: string;
}
@@ -132,6 +139,7 @@ export type ResolverArgs =
| FindDuplicatesResolverArgs
| FindManyResolverArgs
| FindOneResolverArgs
| MergeManyResolverArgs
| RestoreManyResolverArgs
| RestoreOneResolverArgs
| UpdateManyResolverArgs
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { FindDuplicatesResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory';
import { MergeManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@Injectable()
@@ -18,6 +19,8 @@ export class WorkspaceResolverBuilderService {
switch (methodName) {
case FindDuplicatesResolverFactory.methodName:
return isDefined(objectMetadata.duplicateCriteria);
case MergeManyResolverFactory.methodName:
return isDefined(objectMetadata.duplicateCriteria);
default:
return true;
}
@@ -6,6 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
import { MergeManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory';
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
import { RestoreOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory';
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
@@ -52,6 +53,7 @@ export class WorkspaceResolverFactory {
private readonly restoreOneResolverFactory: RestoreOneResolverFactory,
private readonly restoreManyResolverFactory: RestoreManyResolverFactory,
private readonly destroyManyResolverFactory: DestroyManyResolverFactory,
private readonly mergeManyResolverFactory: MergeManyResolverFactory,
private readonly workspaceResolverBuilderService: WorkspaceResolverBuilderService,
private readonly featureFlagService: FeatureFlagService,
) {}
@@ -74,6 +76,7 @@ export class WorkspaceResolverFactory {
['findDuplicates', this.findDuplicatesResolverFactory],
['findMany', this.findManyResolverFactory],
['findOne', this.findOneResolverFactory],
['mergeMany', this.mergeManyResolverFactory],
['restoreMany', this.restoreManyResolverFactory],
['restoreOne', this.restoreOneResolverFactory],
['updateMany', this.updateManyResolverFactory],
@@ -154,12 +157,19 @@ export class WorkspaceResolverFactory {
throw new Error(`Unknown mutation resolver type: ${methodName}`);
}
// @ts-expect-error legacy noImplicitAny
resolvers.Mutation[resolverName] = resolverFactory.create({
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps: objectMetadata,
});
if (
this.workspaceResolverBuilderService.shouldBuildResolver(
objectMetadata,
methodName,
)
) {
// @ts-expect-error legacy noImplicitAny
resolvers.Mutation[resolverName] = resolverFactory.create({
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps: objectMetadata,
});
}
}
}
@@ -145,6 +145,22 @@ export const getResolverArgs = (
isNullable: false,
},
};
case 'mergeMany':
return {
ids: {
type: UUIDScalarType,
isNullable: false,
isArray: true,
},
conflictPriorityIndex: {
type: GraphQLInt,
isNullable: false,
},
dryRun: {
type: GraphQLBoolean,
isNullable: true,
},
};
default:
throw new Error(`Unknown resolver type: ${type}`);
}