Skip to content

Commit

Permalink
feat: Dynamic hook registration for WorkspaceQueryHooks (#6008)
Browse files Browse the repository at this point in the history
#### 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 <[email protected]>
  • Loading branch information
magrinj and Weiko authored Jun 25, 2024
1 parent 4dfca45 commit 7c2e745
Show file tree
Hide file tree
Showing 32 changed files with 472 additions and 235 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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);
};
}
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const WORKSPACE_QUERY_HOOK_METADATA = Symbol(
'workspace-query-hook:query-hook-metadata',
);
Loading

0 comments on commit 7c2e745

Please sign in to comment.