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:
@ -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,
|
||||
|
||||
@ -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`;
|
||||
|
||||
@ -36,6 +36,7 @@ export class FindManyQueryFactory {
|
||||
const argsString = this.argsStringFactory.create(
|
||||
args,
|
||||
options.fieldMetadataCollection,
|
||||
!options.withSoftDeleted && !!options.objectMetadataItem.isSoftDeletable,
|
||||
);
|
||||
|
||||
return `
|
||||
|
||||
@ -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 `
|
||||
|
||||
@ -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
|
||||
|
||||
@ -3,6 +3,7 @@ export interface Record {
|
||||
[key: string]: any;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
}
|
||||
|
||||
export type RecordFilter = {
|
||||
|
||||
@ -8,4 +8,5 @@ export interface WorkspaceQueryBuilderOptions {
|
||||
info: GraphQLResolveInfo;
|
||||
fieldMetadataCollection: FieldMetadataInterface[];
|
||||
objectMetadataCollection: ObjectMetadataInterface[];
|
||||
withSoftDeleted?: boolean;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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: {},
|
||||
|
||||
@ -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] = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user