Refactor graphql query runner and add mutation resolvers (#7418)
Fixes https://github.com/twentyhq/twenty/issues/6859 This PR adds all the remaining resolvers for - updateOne/updateMany - createOne/createMany - deleteOne/deleteMany - destroyOne - restoreMany Also - refactored the graphql-query-runner to be able to add other resolvers without too much boilerplate. - add missing events that were not sent anymore as well as webhooks - make resolver injectable so they can inject other services as well - use objectMetadataMap from cache instead of computing it multiple time - various fixes (mutation not correctly parsing JSON, relationHelper fetching data with empty ids set, ...) Next steps: - Wrapping query builder to handle DB events properly - Move webhook emitters to db event listener - Add pagination where it's missing (findDuplicates, nested relations, etc...)
This commit is contained in:
@ -6,180 +6,90 @@ import {
|
||||
RecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import {
|
||||
CreateManyResolverArgs,
|
||||
CreateOneResolverArgs,
|
||||
DeleteManyResolverArgs,
|
||||
DeleteOneResolverArgs,
|
||||
DestroyOneResolverArgs,
|
||||
FindDuplicatesResolverArgs,
|
||||
FindManyResolverArgs,
|
||||
FindOneResolverArgs,
|
||||
ResolverArgs,
|
||||
ResolverArgsType,
|
||||
RestoreManyResolverArgs,
|
||||
SearchResolverArgs,
|
||||
UpdateManyResolverArgs,
|
||||
UpdateOneResolverArgs,
|
||||
WorkspaceResolverBuilderMethodNames,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
|
||||
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-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 { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service';
|
||||
import { GraphqlQueryResolverFactory } from 'src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory';
|
||||
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||
import {
|
||||
CallWebhookJobsJob,
|
||||
CallWebhookJobsJobData,
|
||||
CallWebhookJobsJobOperation,
|
||||
} 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 { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
|
||||
import {
|
||||
WorkspaceQueryRunnerException,
|
||||
WorkspaceQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryRunnerService {
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly workspaceQueryHookService: WorkspaceQueryHookService,
|
||||
private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
@InjectMessageQueue(MessageQueue.webhookQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly graphqlQueryResolverFactory: GraphqlQueryResolverFactory,
|
||||
private readonly apiEventEmitterService: ApiEventEmitterService,
|
||||
) {}
|
||||
|
||||
/** QUERIES */
|
||||
|
||||
@LogExecutionTime()
|
||||
async findOne<
|
||||
ObjectRecord extends IRecord = IRecord,
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
>(
|
||||
async findOne<ObjectRecord extends IRecord, Filter extends RecordFilter>(
|
||||
args: FindOneResolverArgs<Filter>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord | undefined> {
|
||||
const graphqlQueryFindOneResolverService =
|
||||
new GraphqlQueryFindOneResolverService(this.twentyORMGlobalManager);
|
||||
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
if (!args.filter || Object.keys(args.filter).length === 0) {
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
'Missing filter argument',
|
||||
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findOne',
|
||||
args,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
hookedArgs,
|
||||
): Promise<ObjectRecord> {
|
||||
return this.executeQuery<FindOneResolverArgs<Filter>, ObjectRecord>(
|
||||
'findOne',
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.FindOne,
|
||||
)) as FindOneResolverArgs<Filter>;
|
||||
|
||||
return graphqlQueryFindOneResolverService.findOne(computedArgs, options);
|
||||
);
|
||||
}
|
||||
|
||||
@LogExecutionTime()
|
||||
async findMany<
|
||||
ObjectRecord extends IRecord = IRecord,
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
||||
ObjectRecord extends IRecord,
|
||||
Filter extends RecordFilter,
|
||||
OrderBy extends RecordOrderBy,
|
||||
>(
|
||||
args: FindManyResolverArgs<Filter, OrderBy>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<IConnection<ObjectRecord>> {
|
||||
const graphqlQueryFindManyResolverService =
|
||||
new GraphqlQueryFindManyResolverService(this.twentyORMGlobalManager);
|
||||
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
hookedArgs,
|
||||
options,
|
||||
ResolverArgsType.FindMany,
|
||||
)) as FindManyResolverArgs<Filter, OrderBy>;
|
||||
|
||||
return graphqlQueryFindManyResolverService.findMany(computedArgs, options);
|
||||
): Promise<IConnection<ObjectRecord, IEdge<ObjectRecord>>> {
|
||||
return this.executeQuery<
|
||||
FindManyResolverArgs<Filter, OrderBy>,
|
||||
IConnection<ObjectRecord, IEdge<ObjectRecord>>
|
||||
>('findMany', args, options);
|
||||
}
|
||||
|
||||
@LogExecutionTime()
|
||||
async createOne<ObjectRecord extends IRecord = IRecord>(
|
||||
args: CreateOneResolverArgs<Partial<ObjectRecord>>,
|
||||
async findDuplicates<ObjectRecord extends IRecord>(
|
||||
args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord | undefined> {
|
||||
const graphqlQueryCreateManyResolverService =
|
||||
new GraphqlQueryCreateManyResolverService(this.twentyORMGlobalManager);
|
||||
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
if (args.data.id) {
|
||||
assertIsValidUuid(args.data.id);
|
||||
}
|
||||
|
||||
const createManyArgs = {
|
||||
data: [args.data],
|
||||
upsert: args.upsert,
|
||||
} as CreateManyResolverArgs<ObjectRecord>;
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'createMany',
|
||||
createManyArgs,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
hookedArgs,
|
||||
options,
|
||||
ResolverArgsType.CreateMany,
|
||||
)) as CreateManyResolverArgs<ObjectRecord>;
|
||||
|
||||
const results = (await graphqlQueryCreateManyResolverService.createMany(
|
||||
computedArgs,
|
||||
options,
|
||||
)) as ObjectRecord[];
|
||||
|
||||
await this.triggerWebhooks<ObjectRecord>(
|
||||
results,
|
||||
CallWebhookJobsJobOperation.create,
|
||||
options,
|
||||
);
|
||||
|
||||
this.emitCreateEvents<ObjectRecord>(
|
||||
results,
|
||||
authContext,
|
||||
objectMetadataItem,
|
||||
);
|
||||
|
||||
return results?.[0] as ObjectRecord;
|
||||
): Promise<IConnection<ObjectRecord>[]> {
|
||||
return this.executeQuery<
|
||||
FindDuplicatesResolverArgs<Partial<ObjectRecord>>,
|
||||
IConnection<ObjectRecord>[]
|
||||
>('findDuplicates', args, options);
|
||||
}
|
||||
|
||||
@LogExecutionTime()
|
||||
@ -187,104 +97,286 @@ export class GraphqlQueryRunnerService {
|
||||
args: SearchResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<IConnection<ObjectRecord>> {
|
||||
const graphqlQuerySearchResolverService =
|
||||
new GraphqlQuerySearchResolverService(
|
||||
this.twentyORMGlobalManager,
|
||||
this.featureFlagService,
|
||||
);
|
||||
return this.executeQuery<SearchResolverArgs, IConnection<ObjectRecord>>(
|
||||
'search',
|
||||
args,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
return graphqlQuerySearchResolverService.search(args, options);
|
||||
/** MUTATIONS */
|
||||
|
||||
@LogExecutionTime()
|
||||
async createOne<ObjectRecord extends IRecord>(
|
||||
args: CreateOneResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord> {
|
||||
const results = await this.executeQuery<
|
||||
CreateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
ObjectRecord[]
|
||||
>('createMany', { data: [args.data], upsert: args.upsert }, options);
|
||||
|
||||
// TODO: emitCreateEvents should be moved to the ORM layer
|
||||
if (results) {
|
||||
this.apiEventEmitterService.emitCreateEvents(
|
||||
results,
|
||||
options.authContext,
|
||||
options.objectMetadataItem,
|
||||
);
|
||||
}
|
||||
|
||||
return results[0];
|
||||
}
|
||||
|
||||
@LogExecutionTime()
|
||||
async createMany<ObjectRecord extends IRecord = IRecord>(
|
||||
async createMany<ObjectRecord extends IRecord>(
|
||||
args: CreateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord[] | undefined> {
|
||||
const graphqlQueryCreateManyResolverService =
|
||||
new GraphqlQueryCreateManyResolverService(this.twentyORMGlobalManager);
|
||||
): Promise<ObjectRecord[]> {
|
||||
const results = await this.executeQuery<
|
||||
CreateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
ObjectRecord[]
|
||||
>('createMany', args, options);
|
||||
|
||||
if (results) {
|
||||
this.apiEventEmitterService.emitCreateEvents(
|
||||
results,
|
||||
options.authContext,
|
||||
options.objectMetadataItem,
|
||||
);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@LogExecutionTime()
|
||||
public async updateOne<ObjectRecord extends IRecord>(
|
||||
args: UpdateOneResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord> {
|
||||
const existingRecord = await this.executeQuery<
|
||||
FindOneResolverArgs,
|
||||
ObjectRecord
|
||||
>(
|
||||
'findOne',
|
||||
{
|
||||
filter: { id: { eq: args.id } },
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
const result = await this.executeQuery<
|
||||
UpdateOneResolverArgs<Partial<ObjectRecord>>,
|
||||
ObjectRecord
|
||||
>('updateOne', args, options);
|
||||
|
||||
this.apiEventEmitterService.emitUpdateEvents(
|
||||
[existingRecord],
|
||||
[result],
|
||||
Object.keys(args.data),
|
||||
options.authContext,
|
||||
options.objectMetadataItem,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@LogExecutionTime()
|
||||
public async updateMany<ObjectRecord extends IRecord>(
|
||||
args: UpdateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord[]> {
|
||||
const existingRecords = await this.executeQuery<
|
||||
FindManyResolverArgs,
|
||||
IConnection<ObjectRecord, IEdge<ObjectRecord>>
|
||||
>(
|
||||
'findMany',
|
||||
{
|
||||
filter: args.filter,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
const result = await this.executeQuery<
|
||||
UpdateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
ObjectRecord[]
|
||||
>('updateMany', args, options);
|
||||
|
||||
this.apiEventEmitterService.emitUpdateEvents(
|
||||
existingRecords.edges.map((edge) => edge.node),
|
||||
result,
|
||||
Object.keys(args.data),
|
||||
options.authContext,
|
||||
options.objectMetadataItem,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@LogExecutionTime()
|
||||
public async deleteOne<ObjectRecord extends IRecord & { deletedAt?: Date }>(
|
||||
args: DeleteOneResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord> {
|
||||
const result = await this.executeQuery<
|
||||
UpdateOneResolverArgs<Partial<ObjectRecord>>,
|
||||
ObjectRecord
|
||||
>(
|
||||
'deleteOne',
|
||||
{
|
||||
id: args.id,
|
||||
data: { deletedAt: new Date() } as Partial<ObjectRecord>,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
this.apiEventEmitterService.emitDeletedEvents(
|
||||
[result],
|
||||
options.authContext,
|
||||
options.objectMetadataItem,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@LogExecutionTime()
|
||||
public async deleteMany<ObjectRecord extends IRecord & { deletedAt?: Date }>(
|
||||
args: DeleteManyResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord[]> {
|
||||
const result = await this.executeQuery<
|
||||
UpdateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
ObjectRecord[]
|
||||
>(
|
||||
'deleteMany',
|
||||
{
|
||||
filter: args.filter,
|
||||
|
||||
data: { deletedAt: new Date() } as Partial<ObjectRecord>,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
this.apiEventEmitterService.emitDeletedEvents(
|
||||
result,
|
||||
options.authContext,
|
||||
options.objectMetadataItem,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@LogExecutionTime()
|
||||
async destroyOne<ObjectRecord extends IRecord>(
|
||||
args: DestroyOneResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord> {
|
||||
const result = await this.executeQuery<
|
||||
DestroyOneResolverArgs,
|
||||
ObjectRecord
|
||||
>('destroyOne', args, options);
|
||||
|
||||
this.apiEventEmitterService.emitDestroyEvents(
|
||||
[result],
|
||||
options.authContext,
|
||||
options.objectMetadataItem,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@LogExecutionTime()
|
||||
public async restoreMany<ObjectRecord extends IRecord>(
|
||||
args: RestoreManyResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord> {
|
||||
const result = await this.executeQuery<
|
||||
UpdateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
ObjectRecord
|
||||
>(
|
||||
'restoreMany',
|
||||
{
|
||||
filter: args.filter,
|
||||
data: { deletedAt: null } as Partial<ObjectRecord>,
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async executeQuery<Input extends ResolverArgs, Response>(
|
||||
operationName: WorkspaceResolverBuilderMethodNames,
|
||||
args: Input,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Response> {
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
const resolver =
|
||||
this.graphqlQueryResolverFactory.getResolver(operationName);
|
||||
|
||||
args.data.forEach((record) => {
|
||||
if (record?.id) {
|
||||
assertIsValidUuid(record.id);
|
||||
}
|
||||
});
|
||||
await resolver.validate(args, options);
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'createMany',
|
||||
operationName,
|
||||
args,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
const computedArgs = await this.queryRunnerArgsFactory.create(
|
||||
hookedArgs,
|
||||
options,
|
||||
ResolverArgsType.CreateMany,
|
||||
)) as CreateManyResolverArgs<ObjectRecord>;
|
||||
ResolverArgsType[capitalize(operationName)],
|
||||
);
|
||||
|
||||
const results = (await graphqlQueryCreateManyResolverService.createMany(
|
||||
computedArgs,
|
||||
options,
|
||||
)) as ObjectRecord[];
|
||||
const results = await resolver.resolve(computedArgs as Input, options);
|
||||
|
||||
await this.workspaceQueryHookService.executePostQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'createMany',
|
||||
results,
|
||||
operationName,
|
||||
Array.isArray(results) ? results : [results],
|
||||
);
|
||||
|
||||
await this.triggerWebhooks<ObjectRecord>(
|
||||
results,
|
||||
CallWebhookJobsJobOperation.create,
|
||||
options,
|
||||
);
|
||||
const jobOperation = this.operationNameToJobOperation(operationName);
|
||||
|
||||
this.emitCreateEvents<ObjectRecord>(
|
||||
results,
|
||||
authContext,
|
||||
objectMetadataItem,
|
||||
);
|
||||
if (jobOperation) {
|
||||
await this.triggerWebhooks(results, jobOperation, options);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private emitCreateEvents<BaseRecord extends IRecord = IRecord>(
|
||||
records: BaseRecord[],
|
||||
authContext: AuthContext,
|
||||
objectMetadataItem: ObjectMetadataInterface,
|
||||
) {
|
||||
this.workspaceEventEmitter.emit(
|
||||
`${objectMetadataItem.nameSingular}.created`,
|
||||
records.map(
|
||||
(record) =>
|
||||
({
|
||||
userId: authContext.user?.id,
|
||||
recordId: record.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
after: record,
|
||||
},
|
||||
}) satisfies ObjectRecordCreateEvent<any>,
|
||||
),
|
||||
authContext.workspace.id,
|
||||
);
|
||||
private operationNameToJobOperation(
|
||||
operationName: WorkspaceResolverBuilderMethodNames,
|
||||
): CallWebhookJobsJobOperation | undefined {
|
||||
switch (operationName) {
|
||||
case 'createOne':
|
||||
case 'createMany':
|
||||
return CallWebhookJobsJobOperation.create;
|
||||
case 'updateOne':
|
||||
case 'updateMany':
|
||||
case 'restoreMany':
|
||||
return CallWebhookJobsJobOperation.update;
|
||||
case 'deleteOne':
|
||||
case 'deleteMany':
|
||||
return CallWebhookJobsJobOperation.delete;
|
||||
case 'destroyOne':
|
||||
return CallWebhookJobsJobOperation.destroy;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async triggerWebhooks<Record>(
|
||||
jobsData: Record[] | undefined,
|
||||
private async triggerWebhooks<T>(
|
||||
jobsData: T[] | undefined,
|
||||
operation: CallWebhookJobsJobOperation,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
) {
|
||||
if (!Array.isArray(jobsData)) {
|
||||
return;
|
||||
}
|
||||
): Promise<void> {
|
||||
if (!jobsData || !Array.isArray(jobsData)) return;
|
||||
|
||||
jobsData.forEach((jobData) => {
|
||||
this.messageQueueService.add<CallWebhookJobsJobData>(
|
||||
CallWebhookJobsJob.name,
|
||||
@ -298,99 +390,4 @@ export class GraphqlQueryRunnerService {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@LogExecutionTime()
|
||||
async destroyOne<ObjectRecord extends IRecord = IRecord>(
|
||||
args: DestroyOneResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord | undefined> {
|
||||
const graphqlQueryDestroyOneResolverService =
|
||||
new GraphqlQueryDestroyOneResolverService(this.twentyORMGlobalManager);
|
||||
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
assertIsValidUuid(args.id);
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'destroyOne',
|
||||
args,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
hookedArgs,
|
||||
options,
|
||||
ResolverArgsType.DestroyOne,
|
||||
)) as DestroyOneResolverArgs;
|
||||
|
||||
const result = (await graphqlQueryDestroyOneResolverService.destroyOne(
|
||||
computedArgs,
|
||||
options,
|
||||
)) as ObjectRecord;
|
||||
|
||||
await this.workspaceQueryHookService.executePostQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'destroyOne',
|
||||
[result],
|
||||
);
|
||||
|
||||
await this.triggerWebhooks<IRecord>(
|
||||
[result],
|
||||
CallWebhookJobsJobOperation.destroy,
|
||||
options,
|
||||
);
|
||||
|
||||
this.emitDestroyEvents<IRecord>([result], authContext, objectMetadataItem);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private emitDestroyEvents<BaseRecord extends IRecord = IRecord>(
|
||||
records: BaseRecord[],
|
||||
authContext: AuthContext,
|
||||
objectMetadataItem: ObjectMetadataInterface,
|
||||
) {
|
||||
this.workspaceEventEmitter.emit(
|
||||
`${objectMetadataItem.nameSingular}.destroyed`,
|
||||
records.map((record) => {
|
||||
return {
|
||||
userId: authContext.user?.id,
|
||||
recordId: record.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
before: this.removeNestedProperties(record),
|
||||
},
|
||||
} satisfies ObjectRecordDeleteEvent<any>;
|
||||
}),
|
||||
authContext.workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
private removeNestedProperties<Record extends IRecord = IRecord>(
|
||||
record: Record,
|
||||
) {
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedRecord = {};
|
||||
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
if (value && typeof value === 'object' && value['edges']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === '__typename') {
|
||||
continue;
|
||||
}
|
||||
|
||||
sanitizedRecord[key] = value;
|
||||
}
|
||||
|
||||
return sanitizedRecord;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user