feat: soft delete (#6576)

Implement soft delete on standards and custom objects.
This is a temporary solution, when we drop `pg_graphql` we should rely
on the `softDelete` functions of TypeORM.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Jérémy M
2024-08-16 21:20:02 +02:00
committed by GitHub
parent 20d84755bb
commit db54469c8a
118 changed files with 1675 additions and 492 deletions

View File

@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
import { isDefined } from 'src/utils/is-defined';
import { ArgsAliasFactory } from './args-alias.factory';
@ -13,10 +14,18 @@ export class ArgsStringFactory {
create(
initialArgs: Record<string, any> | undefined,
fieldMetadataCollection: FieldMetadataInterface[],
softDeletable?: boolean,
): string | null {
if (!initialArgs) {
return null;
}
if (softDeletable) {
initialArgs.filter = {
and: [initialArgs.filter, { deletedAt: { is: 'NULL' } }].filter(
isDefined,
),
};
}
let argsString = '';
const computedArgs = this.argsAliasFactory.create(
initialArgs,

View File

@ -22,19 +22,23 @@ export class FieldsStringFactory {
private readonly relationFieldAliasFactory: RelationFieldAliasFactory,
) {}
create(
async create(
info: GraphQLResolveInfo,
fieldMetadataCollection: FieldMetadataInterface[],
objectMetadataCollection: ObjectMetadataInterface[],
withSoftDeleted?: boolean,
): Promise<string> {
const selectedFields: Partial<Record> = graphqlFields(info);
return this.createFieldsStringRecursive(
const res = await this.createFieldsStringRecursive(
info,
selectedFields,
fieldMetadataCollection,
objectMetadataCollection,
withSoftDeleted ?? false,
);
return res;
}
async createFieldsStringRecursive(
@ -42,6 +46,7 @@ export class FieldsStringFactory {
selectedFields: Partial<Record>,
fieldMetadataCollection: FieldMetadataInterface[],
objectMetadataCollection: ObjectMetadataInterface[],
withSoftDeleted: boolean,
accumulator = '',
): Promise<string> {
const fieldMetadataMap = new Map(
@ -65,6 +70,7 @@ export class FieldsStringFactory {
fieldMetadata,
objectMetadataCollection,
info,
withSoftDeleted,
);
fieldAlias = alias;
@ -91,6 +97,7 @@ export class FieldsStringFactory {
fieldValue,
fieldMetadataCollection,
objectMetadataCollection,
withSoftDeleted,
accumulator,
);
accumulator += `}\n`;

View File

@ -36,6 +36,7 @@ export class FindManyQueryFactory {
const argsString = this.argsStringFactory.create(
args,
options.fieldMetadataCollection,
!options.withSoftDeleted && !!options.objectMetadataItem.isSoftDeletable,
);
return `

View File

@ -26,10 +26,12 @@ export class FindOneQueryFactory {
options.info,
options.fieldMetadataCollection,
options.objectMetadataCollection,
options.withSoftDeleted,
);
const argsString = this.argsStringFactory.create(
args,
options.fieldMetadataCollection,
!options.withSoftDeleted && !!options.objectMetadataItem.isSoftDeletable,
);
return `

View File

@ -12,7 +12,6 @@ import {
RelationDirection,
} from 'src/engine/utils/deduce-relation-direction.util';
import { getFieldArgumentsByKey } from 'src/engine/api/graphql/workspace-query-builder/utils/get-field-arguments-by-key.util';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
@ -27,7 +26,6 @@ export class RelationFieldAliasFactory {
@Inject(forwardRef(() => FieldsStringFactory))
private readonly fieldsStringFactory: CircularDep<FieldsStringFactory>,
private readonly argsStringFactory: ArgsStringFactory,
private readonly objectMetadataService: ObjectMetadataService,
) {}
create(
@ -36,6 +34,7 @@ export class RelationFieldAliasFactory {
fieldMetadata: FieldMetadataInterface,
objectMetadataCollection: ObjectMetadataInterface[],
info: GraphQLResolveInfo,
withSoftDeleted?: boolean,
): Promise<string> {
if (!isRelationFieldMetadataType(fieldMetadata.type)) {
throw new Error(`Field ${fieldMetadata.name} is not a relation field`);
@ -47,6 +46,7 @@ export class RelationFieldAliasFactory {
fieldMetadata,
objectMetadataCollection,
info,
withSoftDeleted,
);
}
@ -56,6 +56,7 @@ export class RelationFieldAliasFactory {
fieldMetadata: FieldMetadataInterface,
objectMetadataCollection: ObjectMetadataInterface[],
info: GraphQLResolveInfo,
withSoftDeleted?: boolean,
): Promise<string> {
const relationMetadata =
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
@ -98,9 +99,11 @@ export class RelationFieldAliasFactory {
relationDirection === RelationDirection.FROM
) {
const args = getFieldArgumentsByKey(info, fieldKey);
const argsString = this.argsStringFactory.create(
args,
referencedObjectMetadata.fields ?? [],
!withSoftDeleted && !!referencedObjectMetadata.isSoftDeletable,
);
const fieldsString =
await this.fieldsStringFactory.createFieldsStringRecursive(
@ -108,6 +111,7 @@ export class RelationFieldAliasFactory {
fieldValue,
referencedObjectMetadata.fields ?? [],
objectMetadataCollection,
withSoftDeleted ?? false,
);
return `
@ -137,6 +141,7 @@ export class RelationFieldAliasFactory {
fieldValue,
referencedObjectMetadata.fields ?? [],
objectMetadataCollection,
withSoftDeleted ?? false,
);
// Otherwise it means it's a relation destination is of kind ONE

View File

@ -3,6 +3,7 @@ export interface Record {
[key: string]: any;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export type RecordFilter = {

View File

@ -8,4 +8,5 @@ export interface WorkspaceQueryBuilderOptions {
info: GraphQLResolveInfo;
fieldMetadataCollection: FieldMetadataInterface[];
objectMetadataCollection: ObjectMetadataInterface[];
withSoftDeleted?: boolean;
}

View File

@ -35,11 +35,10 @@ export class EntityEventsToDbListener {
return this.handle(payload);
}
// @OnEvent('*.deleted') - TODO: implement when we soft delete has been implemented
// ....
// @OnEvent('*.restored') - TODO: implement when we soft delete has been implemented
// ....
@OnEvent('*.deleted')
async handleDelete(payload: ObjectRecordUpdateEvent<any>) {
return this.handle(payload);
}
private async handle(payload: ObjectRecordBaseEvent) {
if (!payload.objectMetadata?.isAuditLogged) {

View File

@ -0,0 +1,29 @@
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { isDefined } from 'src/utils/is-defined';
export const withSoftDeleted = <T extends RecordFilter>(
filter: T | undefined | null,
): boolean => {
if (!isDefined(filter)) {
return false;
}
if (Array.isArray(filter)) {
return filter.some((item) => withSoftDeleted(item));
}
for (const [key, value] of Object.entries(filter)) {
if (key === 'deletedAt') {
return true;
}
if (typeof value === 'object' && value !== null) {
if (withSoftDeleted(value)) {
return true;
}
}
}
return false;
};

View File

@ -3,9 +3,11 @@ import {
CreateOneResolverArgs,
DeleteManyResolverArgs,
DeleteOneResolverArgs,
DestroyManyResolverArgs,
FindDuplicatesResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
RestoreManyResolverArgs,
UpdateManyResolverArgs,
UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@ -33,4 +35,8 @@ export type WorkspacePreQueryHookPayload<T> = T extends 'createMany'
? UpdateOneResolverArgs
: T extends 'findDuplicates'
? FindDuplicatesResolverArgs
: never;
: T extends 'restoreMany'
? RestoreManyResolverArgs
: T extends 'destroyMany'
? DestroyManyResolverArgs
: never;

View File

@ -15,10 +15,12 @@ import {
CreateOneResolverArgs,
DeleteManyResolverArgs,
DeleteOneResolverArgs,
DestroyManyResolverArgs,
FindDuplicatesResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
ResolverArgsType,
RestoreManyResolverArgs,
UpdateManyResolverArgs,
UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@ -34,6 +36,7 @@ import {
} from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { parseResult } from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util';
import { withSoftDeleted } from 'src/engine/api/graphql/workspace-query-runner/utils/with-soft-deleted.util';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
import {
WorkspaceQueryRunnerException,
@ -108,7 +111,10 @@ export class WorkspaceQueryRunnerService {
const query = await this.workspaceQueryBuilderFactory.findMany(
computedArgs,
options,
{
...options,
withSoftDeleted: withSoftDeleted(args.filter),
},
);
const result = await this.execute(query, authContext.workspace.id);
@ -159,7 +165,10 @@ export class WorkspaceQueryRunnerService {
const query = await this.workspaceQueryBuilderFactory.findOne(
computedArgs,
options,
{
...options,
withSoftDeleted: withSoftDeleted(args.filter),
},
);
const result = await this.execute(query, authContext.workspace.id);
@ -540,6 +549,7 @@ export class WorkspaceQueryRunnerService {
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { authContext, objectMetadataItem } = options;
let query: string;
assertMutationNotOnRemoteObject(objectMetadataItem);
@ -555,13 +565,25 @@ export class WorkspaceQueryRunnerService {
args,
);
const query = await this.workspaceQueryBuilderFactory.deleteMany(
hookedArgs,
{
if (objectMetadataItem.isSoftDeletable) {
query = await this.workspaceQueryBuilderFactory.updateMany(
{
filter: hookedArgs.filter,
data: {
deletedAt: new Date().toISOString(),
},
},
{
...options,
atMost: maximumRecordAffected,
},
);
} else {
query = await this.workspaceQueryBuilderFactory.deleteMany(hookedArgs, {
...options,
atMost: maximumRecordAffected,
},
);
});
}
const result = await this.execute(query, authContext.workspace.id);
@ -569,7 +591,7 @@ export class WorkspaceQueryRunnerService {
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
objectMetadataItem.isSoftDeletable ? 'update' : 'deleteFrom',
authContext.workspace.id,
)
)?.records;
@ -596,6 +618,148 @@ export class WorkspaceQueryRunnerService {
return parsedResults;
}
async destroyMany<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
args: DestroyManyResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { authContext, objectMetadataItem } = options;
assertMutationNotOnRemoteObject(objectMetadataItem);
if (!objectMetadataItem.isSoftDeletable) {
throw new WorkspaceQueryRunnerException(
'This method is reserved to objects that can be soft-deleted, use delete instead',
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
);
}
const maximumRecordAffected = this.environmentService.get(
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
);
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'destroyMany',
args,
);
const query = await this.workspaceQueryBuilderFactory.deleteMany(
{
filter: {
...hookedArgs.filter,
deletedAt: { is: 'NOT_NULL' },
},
},
{
...options,
atMost: maximumRecordAffected,
},
);
const result = await this.execute(query, authContext.workspace.id);
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
authContext.workspace.id,
)
)?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.delete,
options,
);
return parsedResults;
}
async restoreMany<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
args: RestoreManyResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { authContext, objectMetadataItem } = options;
assertMutationNotOnRemoteObject(objectMetadataItem);
if (!objectMetadataItem.isSoftDeletable) {
throw new WorkspaceQueryRunnerException(
'This method is reserved to objects that can be soft-deleted',
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
);
}
const maximumRecordAffected = this.environmentService.get(
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
);
const hookedArgs =
await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItem.nameSingular,
'restoreMany',
args,
);
const query = await this.workspaceQueryBuilderFactory.updateMany(
{
filter: {
...hookedArgs.filter,
deletedAt: { is: 'NOT_NULL' },
},
data: {
deletedAt: null,
},
},
{
...options,
atMost: maximumRecordAffected,
},
);
const result = await this.execute(query, authContext.workspace.id);
const parsedResults = (
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'update',
authContext.workspace.id,
)
)?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.delete,
options,
);
parsedResults.forEach((record) => {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
name: `${objectMetadataItem.nameSingular}.created`,
workspaceId: authContext.workspace.id,
userId: authContext.user?.id,
recordId: record.id,
objectMetadata: objectMetadataItem,
properties: {
after: this.removeNestedProperties(record),
},
} satisfies ObjectRecordCreateEvent<any>);
});
return parsedResults;
}
async deleteOne<Record extends IRecord = IRecord>(
args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
@ -606,6 +770,7 @@ export class WorkspaceQueryRunnerService {
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
let query: string;
assertMutationNotOnRemoteObject(objectMetadataItem);
assertIsValidUuid(args.id);
@ -618,10 +783,22 @@ export class WorkspaceQueryRunnerService {
args,
);
const query = await this.workspaceQueryBuilderFactory.deleteOne(
hookedArgs,
options,
);
if (objectMetadataItem.isSoftDeletable) {
query = await this.workspaceQueryBuilderFactory.updateOne(
{
id: hookedArgs.id,
data: {
deletedAt: new Date().toISOString(),
},
},
options,
);
} else {
query = await this.workspaceQueryBuilderFactory.deleteOne(
hookedArgs,
options,
);
}
const existingRecord = await repository.findOne({
where: { id: args.id },
@ -633,7 +810,7 @@ export class WorkspaceQueryRunnerService {
await this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
objectMetadataItem.isSoftDeletable ? 'update' : 'deleteFrom',
authContext.workspace.id,
)
)?.records;

View File

@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
DestroyManyResolverArgs,
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 { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
@Injectable()
export class DestroyManyResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = 'destroyMany' as const;
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<DestroyManyResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.destroyMany(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
};
}
}

View File

@ -1,3 +1,5 @@
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
import { CreateManyResolverFactory } from './create-many-resolver.factory';
@ -19,6 +21,8 @@ export const workspaceResolverBuilderFactories = [
DeleteOneResolverFactory,
UpdateManyResolverFactory,
DeleteManyResolverFactory,
DestroyManyResolverFactory,
RestoreManyResolverFactory,
];
export const workspaceResolverBuilderMethodNames = {
@ -34,5 +38,7 @@ export const workspaceResolverBuilderMethodNames = {
DeleteOneResolverFactory.methodName,
UpdateManyResolverFactory.methodName,
DeleteManyResolverFactory.methodName,
DestroyManyResolverFactory.methodName,
RestoreManyResolverFactory.methodName,
],
} as const;

View File

@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
Resolver,
RestoreManyResolverArgs,
} 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 { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
@Injectable()
export class RestoreManyResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = 'restoreMany' as const;
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<RestoreManyResolverArgs> {
const internalContext = context;
return async (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.restoreMany(args, {
authContext: internalContext.authContext,
objectMetadataItem: internalContext.objectMetadataItem,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
};
}
}

View File

@ -20,6 +20,8 @@ export enum ResolverArgsType {
UpdateMany = 'UpdateMany',
DeleteOne = 'DeleteOne',
DeleteMany = 'DeleteMany',
RestoreMany = 'RestoreMany',
DestroyMany = 'DestroyMany',
}
export interface FindManyResolverArgs<
@ -82,6 +84,14 @@ export interface DeleteManyResolverArgs<Filter = any> {
filter: Filter;
}
export interface RestoreManyResolverArgs<Filter = any> {
filter: Filter;
}
export interface DestroyManyResolverArgs<Filter = any> {
filter: Filter;
}
export type WorkspaceResolverBuilderQueryMethodNames =
(typeof workspaceResolverBuilderMethodNames.queries)[number];
@ -106,4 +116,6 @@ export type ResolverArgs =
| FindOneResolverArgs
| FindDuplicatesResolverArgs
| UpdateManyResolverArgs
| UpdateOneResolverArgs;
| UpdateOneResolverArgs
| DestroyManyResolverArgs
| RestoreManyResolverArgs;

View File

@ -5,6 +5,8 @@ import { IResolvers } from '@graphql-tools/utils';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
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 { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
@ -36,6 +38,8 @@ export class WorkspaceResolverFactory {
private readonly deleteOneResolverFactory: DeleteOneResolverFactory,
private readonly updateManyResolverFactory: UpdateManyResolverFactory,
private readonly deleteManyResolverFactory: DeleteManyResolverFactory,
private readonly restoreManyResolverFactory: RestoreManyResolverFactory,
private readonly destroyManyResolverFactory: DestroyManyResolverFactory,
) {}
async create(
@ -56,6 +60,8 @@ export class WorkspaceResolverFactory {
['deleteOne', this.deleteOneResolverFactory],
['updateMany', this.updateManyResolverFactory],
['deleteMany', this.deleteManyResolverFactory],
['restoreMany', this.restoreManyResolverFactory],
['destroyMany', this.destroyManyResolverFactory],
]);
const resolvers: IResolvers = {
Query: {},

View File

@ -2,14 +2,14 @@ import { Injectable, Logger } from '@nestjs/common';
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
import { ArgsFactory } from './args.factory';
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
@ -101,13 +101,17 @@ export class RootTypeFactory {
);
}
const allowedMethodNames = [
'updateMany',
'deleteMany',
'createMany',
'findDuplicates',
'restoreMany',
'destroyMany',
];
const outputType = this.typeMapperService.mapToGqlType(objectType, {
isArray: [
'updateMany',
'deleteMany',
'createMany',
'findDuplicates',
].includes(methodName),
isArray: allowedMethodNames.includes(methodName),
});
fieldConfigMap[name] = {

View File

@ -50,6 +50,12 @@ describe('getResolverArgs', () => {
deleteOne: {
id: { type: GraphQLID, isNullable: false },
},
restoreMany: {
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
},
destroyMany: {
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
},
};
// Test each resolver type

View File

@ -116,6 +116,20 @@ export const getResolverArgs = (
isNullable: false,
},
};
case 'restoreMany':
return {
filter: {
kind: InputTypeDefinitionKind.Filter,
isNullable: false,
},
};
case 'destroyMany':
return {
filter: {
kind: InputTypeDefinitionKind.Filter,
isNullable: false,
},
};
default:
throw new Error(`Unknown resolver type: ${type}`);
}