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

@ -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 {}