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,

View File

@ -47,10 +47,17 @@ export class PgBossDriver
}
: {},
async (job) => {
// PGBoss work with wildcard job name
const jobName = job.name.split('.')?.[1];
if (!jobName) {
throw new Error('Job name could not be splited from the job.');
}
await handler({
data: job.data,
id: job.id,
name: job.name.split('.')[1],
name: jobName,
});
},
);

View File

@ -14,7 +14,7 @@ export class ScopedWorkspaceDatasourceFactory {
public async create(entities: EntitySchema[]) {
const workspaceId: string | undefined =
this.request['req']?.['workspaceId'];
this.request?.['req']?.['workspaceId'];
if (!workspaceId) {
return null;

View File

@ -1,16 +1,20 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { BadRequestException, NotFoundException, Scope } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity';
import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
@Injectable()
@WorkspaceQueryHook({
key: `calendarEvent.findMany`,
scope: Scope.REQUEST,
})
export class CalendarEventFindManyPreQueryHook
implements WorkspacePreQueryHook
implements WorkspaceQueryHookInstance
{
constructor(
@InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity)
@ -27,25 +31,24 @@ export class CalendarEventFindManyPreQueryHook
throw new BadRequestException('id filter is required');
}
// TODO: Re-implement this using twenty ORM
// const calendarChannelCalendarEventAssociations =
// await this.calendarChannelEventAssociationRepository.find({
// where: {
// calendarEvent: {
// id: payload?.filter?.id?.eq,
// },
// },
// relations: ['calendarChannel.connectedAccount'],
// });
const calendarChannelCalendarEventAssociations =
await this.calendarChannelEventAssociationRepository.find({
where: {
calendarEvent: {
id: payload?.filter?.id?.eq,
},
},
relations: ['calendarChannel.connectedAccount'],
});
// if (calendarChannelCalendarEventAssociations.length === 0) {
// throw new NotFoundException();
// }
if (calendarChannelCalendarEventAssociations.length === 0) {
throw new NotFoundException();
}
// await this.canAccessCalendarEventService.canAccessCalendarEvent(
// userId,
// workspaceId,
// calendarChannelCalendarEventAssociations,
// );
await this.canAccessCalendarEventService.canAccessCalendarEvent(
userId,
workspaceId,
calendarChannelCalendarEventAssociations,
);
}
}

View File

@ -1,15 +1,21 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { BadRequestException, NotFoundException, Scope } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity';
@Injectable()
export class CalendarEventFindOnePreQueryHook implements WorkspacePreQueryHook {
@WorkspaceQueryHook({
key: `calendarEvent.findOne`,
scope: Scope.REQUEST,
})
export class CalendarEventFindOnePreQueryHook
implements WorkspaceQueryHookInstance
{
constructor(
@InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity)
private readonly calendarChannelEventAssociationRepository: WorkspaceRepository<CalendarChannelEventAssociationWorkspaceEntity>,
@ -26,23 +32,24 @@ export class CalendarEventFindOnePreQueryHook implements WorkspacePreQueryHook {
}
// TODO: Re-implement this using twenty ORM
// const calendarChannelCalendarEventAssociations =
// await this.calendarChannelEventAssociationRepository.find({
// where: {
// calendarEvent: {
// id: payload?.filter?.id?.eq,
// },
// },
// });
const calendarChannelCalendarEventAssociations =
await this.calendarChannelEventAssociationRepository.find({
where: {
calendarEvent: {
id: payload?.filter?.id?.eq,
},
},
relations: ['calendarChannel.connectedAccount'],
});
// if (calendarChannelCalendarEventAssociations.length === 0) {
// throw new NotFoundException();
// }
if (calendarChannelCalendarEventAssociations.length === 0) {
throw new NotFoundException();
}
// await this.canAccessCalendarEventService.canAccessCalendarEvent(
// userId,
// workspaceId,
// calendarChannelCalendarEventAssociations,
// );
await this.canAccessCalendarEventService.canAccessCalendarEvent(
userId,
workspaceId,
calendarChannelCalendarEventAssociations,
);
}
}

View File

@ -60,7 +60,7 @@ export class CanAccessCalendarEventService {
const calendarChannelsConnectedAccounts =
await this.connectedAccountRepository.getByIds(
calendarChannels.map((channel) => channel.connectedAccount.id),
calendarChannels.map((channel) => channel.connectedAccountId),
workspaceId,
);

View File

@ -23,14 +23,8 @@ import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
],
providers: [
CanAccessCalendarEventService,
{
provide: CalendarEventFindOnePreQueryHook.name,
useClass: CalendarEventFindOnePreQueryHook,
},
{
provide: CalendarEventFindManyPreQueryHook.name,
useClass: CalendarEventFindManyPreQueryHook,
},
CalendarEventFindOnePreQueryHook,
CalendarEventFindManyPreQueryHook,
],
})
export class CalendarQueryHookModule {}

View File

@ -237,6 +237,8 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
})
connectedAccount: Relation<ConnectedAccountWorkspaceEntity>;
connectedAccountId: string;
@WorkspaceRelation({
standardId:
CALENDAR_CHANNEL_STANDARD_FIELD_IDS.calendarChannelEventAssociations,

View File

@ -1,15 +1,16 @@
import { Injectable } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import {
BlocklistItem,
BlocklistValidationService,
} from 'src/modules/connected-account/services/blocklist/blocklist-validation.service';
@Injectable()
export class BlocklistCreateManyPreQueryHook implements WorkspacePreQueryHook {
@WorkspaceQueryHook(`blocklist.createMany`)
export class BlocklistCreateManyPreQueryHook
implements WorkspaceQueryHookInstance
{
constructor(
private readonly blocklistValidationService: BlocklistValidationService,
) {}

View File

@ -1,9 +1,13 @@
import { Injectable, MethodNotAllowedException } from '@nestjs/common';
import { MethodNotAllowedException } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
@Injectable()
export class BlocklistUpdateManyPreQueryHook implements WorkspacePreQueryHook {
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
@WorkspaceQueryHook(`blocklist.updateMany`)
export class BlocklistUpdateManyPreQueryHook
implements WorkspaceQueryHookInstance
{
constructor() {}
async execute(): Promise<void> {

View File

@ -1,15 +1,16 @@
import { Injectable } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import {
BlocklistItem,
BlocklistValidationService,
} from 'src/modules/connected-account/services/blocklist/blocklist-validation.service';
@Injectable()
export class BlocklistUpdateOnePreQueryHook implements WorkspacePreQueryHook {
@WorkspaceQueryHook(`blocklist.updateOne`)
export class BlocklistUpdateOnePreQueryHook
implements WorkspaceQueryHookInstance
{
constructor(
private readonly blocklistValidationService: BlocklistValidationService,
) {}

View File

@ -8,18 +8,9 @@ import { BlocklistValidationModule } from 'src/modules/connected-account/service
@Module({
imports: [BlocklistValidationModule],
providers: [
{
provide: BlocklistCreateManyPreQueryHook.name,
useClass: BlocklistCreateManyPreQueryHook,
},
{
provide: BlocklistUpdateManyPreQueryHook.name,
useClass: BlocklistUpdateManyPreQueryHook,
},
{
provide: BlocklistUpdateOnePreQueryHook.name,
useClass: BlocklistUpdateOnePreQueryHook,
},
BlocklistCreateManyPreQueryHook,
BlocklistUpdateManyPreQueryHook,
BlocklistUpdateOnePreQueryHook,
],
})
export class ConnectedAccountQueryHookModule {}

View File

@ -1,19 +1,16 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
@Injectable()
export class MessageFindManyPreQueryHook implements WorkspacePreQueryHook {
@WorkspaceQueryHook(`message.findMany`)
export class MessageFindManyPreQueryHook implements WorkspaceQueryHookInstance {
constructor(
@InjectObjectMetadataRepository(
MessageChannelMessageAssociationWorkspaceEntity,

View File

@ -1,16 +1,17 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Injectable, NotFoundException } from '@nestjs/common';
import { NotFoundException } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
@Injectable()
export class MessageFindOnePreQueryHook implements WorkspacePreQueryHook {
@WorkspaceQueryHook(`message.findOne`)
export class MessageFindOnePreQueryHook implements WorkspaceQueryHookInstance {
constructor(
@InjectObjectMetadataRepository(
MessageChannelMessageAssociationWorkspaceEntity,

View File

@ -20,14 +20,8 @@ import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/stan
],
providers: [
CanAccessMessageThreadService,
{
provide: MessageFindOnePreQueryHook.name,
useClass: MessageFindOnePreQueryHook,
},
{
provide: MessageFindManyPreQueryHook.name,
useClass: MessageFindManyPreQueryHook,
},
MessageFindOnePreQueryHook,
MessageFindManyPreQueryHook,
],
})
export class MessagingQueryHookModule {}

View File

@ -1,10 +1,12 @@
import { Injectable, MethodNotAllowedException } from '@nestjs/common';
import { MethodNotAllowedException } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
@Injectable()
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
@WorkspaceQueryHook(`workspaceMember.deleteMany`)
export class WorkspaceMemberDeleteManyPreQueryHook
implements WorkspacePreQueryHook
implements WorkspaceQueryHookInstance
{
constructor() {}

View File

@ -1,16 +1,15 @@
import { Injectable } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity';
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
@Injectable()
@WorkspaceQueryHook(`workspaceMember.deleteOne`)
export class WorkspaceMemberDeleteOnePreQueryHook
implements WorkspacePreQueryHook
implements WorkspaceQueryHookInstance
{
constructor(
@InjectWorkspaceRepository(AttachmentWorkspaceEntity)

View File

@ -14,14 +14,8 @@ import { WorkspaceMemberDeleteOnePreQueryHook } from 'src/modules/workspace-memb
]),
],
providers: [
{
provide: WorkspaceMemberDeleteOnePreQueryHook.name,
useClass: WorkspaceMemberDeleteOnePreQueryHook,
},
{
provide: WorkspaceMemberDeleteManyPreQueryHook.name,
useClass: WorkspaceMemberDeleteManyPreQueryHook,
},
WorkspaceMemberDeleteOnePreQueryHook,
WorkspaceMemberDeleteManyPreQueryHook,
],
})
export class WorkspaceMemberQueryHookModule {}