feat: Dynamic hook registration for WorkspaceQueryHooks (#6008)

#### Overview

This PR introduces a new API for dynamically registering and executing
pre and post query hooks in the Workspace Query Hook system using the
`@WorkspaceQueryHook` decorator. This approach eliminates the need for
manual provider registration, and fix the issue of `undefined` or `null`
repository using `@InjectWorkspaceRepository`.

#### New API

**Define a Hook**

Use the `@WorkspaceQueryHook` decorator to define pre or post hooks:

```typescript
@WorkspaceQueryHook({
  key: `calendarEvent.findMany`,
  scope: Scope.REQUEST,
})
export class CalendarEventFindManyPreQueryHook implements WorkspaceQueryHookInstance {
  async execute(userId: string, workspaceId: string, payload: FindManyResolverArgs): Promise<void> {
    if (!payload?.filter?.id?.eq) {
      throw new BadRequestException('id filter is required');
    }

    // Implement hook logic here
  }
}
```

This API simplifies the registration and execution of query hooks,
providing a more flexible and maintainable approach.

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Jérémy M
2024-06-25 12:41:46 +02:00
committed by GitHub
parent 4dfca45fd3
commit 7c2e745b45
32 changed files with 472 additions and 235 deletions

View File

@ -141,8 +141,10 @@ export class GraphQLConfigService
// Create a new contextId for each request
const contextId = ContextIdFactory.create();
// Register the request in the contextId
this.moduleRef.registerRequestByContextId(context.req, contextId);
if (this.moduleRef.registerRequestByContextId) {
// Register the request in the contextId
this.moduleRef.registerRequestByContextId(context.req, contextId);
}
// Resolve the WorkspaceSchemaFactory for the contextId
const workspaceFactory = await this.moduleRef.resolve(

View File

@ -1,31 +0,0 @@
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type';
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';
import { BlocklistCreateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook';
import { BlocklistUpdateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook';
import { BlocklistUpdateOnePreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-update-one.pre-query.hook';
import { WorkspaceMemberDeleteOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook';
import { WorkspaceMemberDeleteManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook';
import { MessageFindManyPreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook';
import { MessageFindOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook';
// TODO: move to a decorator
export const workspacePreQueryHooks: WorkspaceQueryHook = {
message: {
findOne: [MessageFindOnePreQueryHook.name],
findMany: [MessageFindManyPreQueryHook.name],
},
calendarEvent: {
findOne: [CalendarEventFindOnePreQueryHook.name],
findMany: [CalendarEventFindManyPreQueryHook.name],
},
blocklist: {
createMany: [BlocklistCreateManyPreQueryHook.name],
updateMany: [BlocklistUpdateManyPreQueryHook.name],
updateOne: [BlocklistUpdateOnePreQueryHook.name],
},
workspaceMember: {
deleteOne: [WorkspaceMemberDeleteOnePreQueryHook.name],
deleteMany: [WorkspaceMemberDeleteManyPreQueryHook.name],
},
};

View File

@ -1,19 +0,0 @@
import { Module } from '@nestjs/common';
import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service';
import { CalendarQueryHookModule } from 'src/modules/calendar/query-hooks/calendar-query-hook.module';
import { ConnectedAccountQueryHookModule } from 'src/modules/connected-account/query-hooks/connected-account-query-hook.module';
import { MessagingQueryHookModule } from 'src/modules/messaging/common/query-hooks/messaging-query-hook.module';
import { WorkspaceMemberQueryHookModule } from 'src/modules/workspace-member/query-hooks/workspace-member-query-hook.module';
@Module({
imports: [
MessagingQueryHookModule,
CalendarQueryHookModule,
ConnectedAccountQueryHookModule,
WorkspaceMemberQueryHookModule,
],
providers: [WorkspacePreQueryHookService],
exports: [WorkspacePreQueryHookService],
})
export class WorkspacePreQueryHookModule {}

View File

@ -1,34 +0,0 @@
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import {
ExecutePreHookMethod,
WorkspacePreQueryHookPayload,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type';
import { workspacePreQueryHooks } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config';
@Injectable()
export class WorkspacePreQueryHookService {
constructor(private readonly workspaceQueryHookModuleRef: ModuleRef) {}
public async executePreHooks<T extends ExecutePreHookMethod>(
userId: string | undefined,
workspaceId: string,
objectName: string,
method: T,
payload: WorkspacePreQueryHookPayload<T>,
): Promise<void> {
const hooks = workspacePreQueryHooks[objectName] || [];
for (const hookName of Object.values(hooks[method] ?? [])) {
const hook: WorkspacePreQueryHook =
await this.workspaceQueryHookModuleRef.get(hookName, {
strict: false,
});
await hook.execute(userId, workspaceId, payload);
}
}
}

View File

@ -0,0 +1,40 @@
import { Scope, SetMetadata } from '@nestjs/common';
import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/constants';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WORKSPACE_QUERY_HOOK_METADATA } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.constants';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
export type WorkspaceQueryHookKey =
`${string}.${WorkspaceResolverBuilderMethodNames}`;
export interface WorkspaceQueryHookOptions {
key: WorkspaceQueryHookKey;
type?: WorkspaceQueryHookType;
scope?: Scope;
}
export function WorkspaceQueryHook(key: WorkspaceQueryHookKey): ClassDecorator;
export function WorkspaceQueryHook(
options: WorkspaceQueryHookOptions,
): ClassDecorator;
export function WorkspaceQueryHook(
keyOrOptions: WorkspaceQueryHookKey | WorkspaceQueryHookOptions,
): ClassDecorator {
const options: WorkspaceQueryHookOptions =
keyOrOptions && typeof keyOrOptions === 'object'
? keyOrOptions
: { key: keyOrOptions };
// Default to PreHook
if (!options.type) {
options.type = WorkspaceQueryHookType.PreHook;
}
// eslint-disable-next-line @typescript-eslint/ban-types
return (target: Function) => {
SetMetadata(SCOPE_OPTIONS_METADATA, options)(target);
SetMetadata(WORKSPACE_QUERY_HOOK_METADATA, options)(target);
};
}

View File

@ -1,6 +1,6 @@
import { ResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
export interface WorkspacePreQueryHook {
export interface WorkspaceQueryHookInstance {
execute(
userId: string | undefined,
workspaceId: string,

View File

@ -0,0 +1,59 @@
// hook-registry.service.ts
import { Injectable } from '@nestjs/common';
import { Module } from '@nestjs/core/injector/module';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
interface WorkspaceQueryHookData<T> {
instance: T;
host: Module;
isRequestScoped: boolean;
}
@Injectable()
export class WorkspaceQueryHookStorage {
private preHookInstances = new Map<
WorkspaceQueryHookKey,
WorkspaceQueryHookData<WorkspaceQueryHookInstance>[]
>();
private postHookInstances = new Map<
WorkspaceQueryHookKey,
WorkspaceQueryHookData<WorkspaceQueryHookInstance>[]
>();
registerWorkspaceQueryPreHookInstance(
key: WorkspaceQueryHookKey,
data: WorkspaceQueryHookData<WorkspaceQueryHookInstance>,
) {
if (!this.preHookInstances.has(key)) {
this.preHookInstances.set(key, []);
}
this.preHookInstances.get(key)?.push(data);
}
getWorkspaceQueryPreHookInstances(
key: WorkspaceQueryHookKey,
): WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] | undefined {
return this.preHookInstances.get(key);
}
registerWorkspaceQueryPostHookInstance(
key: WorkspaceQueryHookKey,
data: WorkspaceQueryHookData<WorkspaceQueryHookInstance>,
) {
if (!this.postHookInstances.has(key)) {
this.postHookInstances.set(key, []);
}
this.postHookInstances.get(key)?.push(data);
}
getWorkspaceQueryPostHookInstances(
key: WorkspaceQueryHookKey,
): WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] | undefined {
return this.postHookInstances.get(key);
}
}

View File

@ -10,26 +10,10 @@ import {
UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
export type ExecutePreHookMethod =
| 'createMany'
| 'createOne'
| 'deleteMany'
| 'deleteOne'
| 'findMany'
| 'findOne'
| 'findDuplicates'
| 'updateMany'
| 'updateOne';
export type ObjectName = string;
export type HookName = string;
export type WorkspaceQueryHook = {
[key in ObjectName]: {
[key in ExecutePreHookMethod]?: HookName[];
};
};
export enum WorkspaceQueryHookType {
PreHook = 'PreHook',
PostHook = 'PostHook',
}
export type WorkspacePreQueryHookPayload<T> = T extends 'createMany'
? CreateManyResolverArgs

View File

@ -0,0 +1,25 @@
/* eslint-disable @typescript-eslint/ban-types */
import { Injectable, Type } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { WORKSPACE_QUERY_HOOK_METADATA } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.constants';
import { WorkspaceQueryHookOptions } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
@Injectable()
export class WorkspaceQueryHookMetadataAccessor {
constructor(private readonly reflector: Reflector) {}
isWorkspaceQueryHook(target: Type<any> | Function): boolean {
if (!target) {
return false;
}
return !!this.reflector.get(WORKSPACE_QUERY_HOOK_METADATA, target);
}
getWorkspaceQueryHookMetadata(
target: Type<any> | Function,
): WorkspaceQueryHookOptions | undefined {
return this.reflector.get(WORKSPACE_QUERY_HOOK_METADATA, target);
}
}

View File

@ -0,0 +1,3 @@
export const WORKSPACE_QUERY_HOOK_METADATA = Symbol(
'workspace-query-hook:query-hook-metadata',
);

View File

@ -0,0 +1,139 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { DiscoveryService, ModuleRef, createContextId } from '@nestjs/core';
import { Module } from '@nestjs/core/injector/module';
import { Injector } from '@nestjs/core/injector/injector';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHookMetadataAccessor } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor';
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
@Injectable()
export class WorkspaceQueryHookExplorer implements OnModuleInit {
private readonly logger = new Logger('WorkspaceQueryHookModule');
private readonly injector = new Injector();
constructor(
private readonly moduleRef: ModuleRef,
private readonly discoveryService: DiscoveryService,
private readonly metadataAccessor: WorkspaceQueryHookMetadataAccessor,
private readonly workspaceQueryHookStorage: WorkspaceQueryHookStorage,
) {}
onModuleInit() {
this.explore();
}
async explore() {
const hooks = this.discoveryService
.getProviders()
.filter((wrapper) =>
this.metadataAccessor.isWorkspaceQueryHook(
!wrapper.metatype || wrapper.inject
? wrapper.instance?.constructor
: wrapper.metatype,
),
);
for (const hook of hooks) {
const { instance, metatype } = hook;
const { key, type } =
this.metadataAccessor.getWorkspaceQueryHookMetadata(
instance.constructor || metatype,
) ?? {};
if (!key || !type) {
this.logger.error(
`PreHook ${hook.name} is missing key or type metadata`,
);
continue;
}
if (!hook.host) {
this.logger.error(`PreHook ${hook.name} is missing host metadata`);
continue;
}
this.registerWorkspaceQueryHook(
key,
type,
instance,
hook.host,
!hook.isDependencyTreeStatic(),
);
}
}
async handleHook(
payload: Parameters<WorkspaceQueryHookInstance['execute']>,
instance: object,
host: Module,
isRequestScoped: boolean,
) {
const methodName = 'execute';
if (isRequestScoped) {
const contextId = createContextId();
if (this.moduleRef.registerRequestByContextId) {
this.moduleRef.registerRequestByContextId(
{
req: {
workspaceId: payload?.[1],
},
},
contextId,
);
}
const contextInstance = await this.injector.loadPerContext(
instance,
host,
host.providers,
contextId,
);
await contextInstance[methodName].call(contextInstance, ...payload);
} else {
await instance[methodName].call(instance, ...payload);
}
}
private registerWorkspaceQueryHook(
key: WorkspaceQueryHookKey,
type: WorkspaceQueryHookType,
instance: object,
host: Module,
isRequestScoped: boolean,
) {
switch (type) {
case WorkspaceQueryHookType.PreHook:
this.workspaceQueryHookStorage.registerWorkspaceQueryPreHookInstance(
key,
{
instance: instance as WorkspaceQueryHookInstance,
host,
isRequestScoped,
},
);
break;
case WorkspaceQueryHookType.PostHook:
this.workspaceQueryHookStorage.registerWorkspaceQueryPostHookInstance(
key,
{
instance: instance as WorkspaceQueryHookInstance,
host,
isRequestScoped,
},
);
break;
default:
this.logger.error(`Unknown WorkspaceQueryHookType: ${type}`);
break;
}
}
}

View File

@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { DiscoveryModule } from '@nestjs/core';
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
import { WorkspaceQueryHookMetadataAccessor } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor';
import { WorkspaceQueryHookExplorer } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
import { CalendarQueryHookModule } from 'src/modules/calendar/query-hooks/calendar-query-hook.module';
import { ConnectedAccountQueryHookModule } from 'src/modules/connected-account/query-hooks/connected-account-query-hook.module';
import { MessagingQueryHookModule } from 'src/modules/messaging/common/query-hooks/messaging-query-hook.module';
import { WorkspaceMemberQueryHookModule } from 'src/modules/workspace-member/query-hooks/workspace-member-query-hook.module';
@Module({
imports: [
MessagingQueryHookModule,
CalendarQueryHookModule,
ConnectedAccountQueryHookModule,
WorkspaceMemberQueryHookModule,
DiscoveryModule,
],
providers: [
WorkspaceQueryHookService,
WorkspaceQueryHookExplorer,
WorkspaceQueryHookMetadataAccessor,
WorkspaceQueryHookStorage,
],
exports: [WorkspaceQueryHookService],
})
export class WorkspaceQueryHookModule {}

View File

@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookExplorer } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer';
import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
@Injectable()
export class WorkspaceQueryHookService {
constructor(
private readonly workspaceQueryHookStorage: WorkspaceQueryHookStorage,
private readonly workspaceQueryHookExplorer: WorkspaceQueryHookExplorer,
) {}
public async executePreQueryHooks<
T extends WorkspaceResolverBuilderMethodNames,
>(
userId: string | undefined,
workspaceId: string,
objectName: string,
methodName: T,
payload: WorkspacePreQueryHookPayload<T>,
): Promise<void> {
const key: WorkspaceQueryHookKey = `${objectName}.${methodName}`;
const preHookInstances =
this.workspaceQueryHookStorage.getWorkspaceQueryPreHookInstances(key);
if (!preHookInstances) {
return;
}
for (const preHookInstance of preHookInstances) {
await this.workspaceQueryHookExplorer.handleHook(
[userId, workspaceId, payload],
preHookInstance.instance,
preHookInstance.host,
preHookInstance.isRequestScoped,
);
}
}
}

View File

@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspacePreQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module';
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@ -20,7 +20,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
AuthModule,
WorkspaceQueryBuilderModule,
WorkspaceDataSourceModule,
WorkspacePreQueryHookModule,
WorkspaceQueryHookModule,
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
AnalyticsModule,
],

View File

@ -42,7 +42,7 @@ import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event';
import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { NotFoundError } from 'src/engine/utils/graphql-errors.util';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
@ -75,7 +75,7 @@ export class WorkspaceQueryRunnerService {
@InjectMessageQueue(MessageQueue.webhookQueue)
private readonly messageQueueService: MessageQueueService,
private readonly eventEmitter: EventEmitter2,
private readonly workspacePreQueryHookService: WorkspacePreQueryHookService,
private readonly workspaceQueryHookService: WorkspaceQueryHookService,
private readonly environmentService: EnvironmentService,
) {}
@ -101,7 +101,7 @@ export class WorkspaceQueryRunnerService {
options,
);
await this.workspacePreQueryHookService.executePreHooks(
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
@ -148,7 +148,7 @@ export class WorkspaceQueryRunnerService {
options,
);
await this.workspacePreQueryHookService.executePreHooks(
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
@ -223,7 +223,7 @@ export class WorkspaceQueryRunnerService {
existingRecord,
);
await this.workspacePreQueryHookService.executePreHooks(
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
@ -260,7 +260,7 @@ export class WorkspaceQueryRunnerService {
ResolverArgsType.CreateMany,
)) as CreateManyResolverArgs<Record>;
await this.workspacePreQueryHookService.executePreHooks(
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
@ -333,7 +333,7 @@ export class WorkspaceQueryRunnerService {
options,
);
await this.workspacePreQueryHookService.executePreHooks(
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
@ -389,7 +389,7 @@ export class WorkspaceQueryRunnerService {
atMost: maximumRecordAffected,
});
await this.workspacePreQueryHookService.executePreHooks(
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
@ -441,7 +441,7 @@ export class WorkspaceQueryRunnerService {
atMost: maximumRecordAffected,
});
await this.workspacePreQueryHookService.executePreHooks(
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,
@ -509,7 +509,7 @@ export class WorkspaceQueryRunnerService {
);
// TODO END
await this.workspacePreQueryHookService.executePreHooks(
await this.workspaceQueryHookService.executePreQueryHooks(
userId,
workspaceId,
objectMetadataItem.nameSingular,