diff --git a/packages/mercurius/lib/drivers/mercurius-federation.driver.ts b/packages/mercurius/lib/drivers/mercurius-federation.driver.ts index 1a84e3322..ade72c67a 100644 --- a/packages/mercurius/lib/drivers/mercurius-federation.driver.ts +++ b/packages/mercurius/lib/drivers/mercurius-federation.driver.ts @@ -9,6 +9,7 @@ import { IncomingMessage, Server, ServerResponse } from 'http'; import mercurius from 'mercurius'; import { MercuriusDriverConfig } from '../interfaces/mercurius-driver-config.interface'; import { buildMercuriusFederatedSchema } from '../utils/build-mercurius-federated-schema.util'; +import { registerMercuriusHooks } from '../utils/register-mercurius-hooks.util'; import { registerMercuriusPlugin } from '../utils/register-mercurius-plugin.util'; @Injectable() @@ -29,7 +30,7 @@ export class MercuriusFederationDriver extends AbstractGraphQLDriver { @@ -23,12 +24,13 @@ export class MercuriusGatewayDriver extends AbstractGraphQLDriver(); await app.register(mercurius, { ...mercuriusOptions, }); await registerMercuriusPlugin(app, plugins); + await registerMercuriusHooks(app, hooks); } public async stop(): Promise {} diff --git a/packages/mercurius/lib/drivers/mercurius.driver.ts b/packages/mercurius/lib/drivers/mercurius.driver.ts index 712d28695..1159302c8 100644 --- a/packages/mercurius/lib/drivers/mercurius.driver.ts +++ b/packages/mercurius/lib/drivers/mercurius.driver.ts @@ -5,6 +5,7 @@ import { printSchema } from 'graphql'; import { IncomingMessage, Server, ServerResponse } from 'http'; import mercurius from 'mercurius'; import { MercuriusDriverConfig } from '../interfaces/mercurius-driver-config.interface'; +import { registerMercuriusHooks } from '../utils/register-mercurius-hooks.util'; import { registerMercuriusPlugin } from '../utils/register-mercurius-plugin.util'; export class MercuriusDriver extends AbstractGraphQLDriver { @@ -18,7 +19,7 @@ export class MercuriusDriver extends AbstractGraphQLDriver( mercuriusOptions, ); @@ -41,6 +42,7 @@ export class MercuriusDriver extends AbstractGraphQLDriver {} diff --git a/packages/mercurius/lib/interfaces/mercurius-driver-config.interface.ts b/packages/mercurius/lib/interfaces/mercurius-driver-config.interface.ts index 6539b432b..f17bf1fe7 100644 --- a/packages/mercurius/lib/interfaces/mercurius-driver-config.interface.ts +++ b/packages/mercurius/lib/interfaces/mercurius-driver-config.interface.ts @@ -4,11 +4,13 @@ import { GqlOptionsFactory, } from '@nestjs/graphql'; import { MercuriusOptions } from 'mercurius'; +import { MercuriusHooks } from './mercurius-hook.interface'; import { MercuriusPlugins } from './mercurius-plugin.interface'; export type MercuriusDriverConfig = GqlModuleOptions & MercuriusOptions & - MercuriusPlugins; + MercuriusPlugins & + MercuriusHooks; export type MercuriusDriverConfigFactory = GqlOptionsFactory; diff --git a/packages/mercurius/lib/interfaces/mercurius-gateway-driver-config.interface.ts b/packages/mercurius/lib/interfaces/mercurius-gateway-driver-config.interface.ts index 9a8a2f768..527df4c77 100644 --- a/packages/mercurius/lib/interfaces/mercurius-gateway-driver-config.interface.ts +++ b/packages/mercurius/lib/interfaces/mercurius-gateway-driver-config.interface.ts @@ -4,12 +4,14 @@ import { GqlOptionsFactory, } from '@nestjs/graphql'; import { MercuriusCommonOptions, MercuriusGatewayOptions } from 'mercurius'; +import { MercuriusHooks } from './mercurius-hook.interface'; import { MercuriusPlugin } from './mercurius-plugin.interface'; export type MercuriusGatewayDriverConfig = GqlModuleOptions & MercuriusCommonOptions & MercuriusGatewayOptions & - MercuriusPlugin; + MercuriusPlugin & + MercuriusHooks; export type MercuriusGatewayDriverConfigFactory = GqlOptionsFactory; diff --git a/packages/mercurius/lib/interfaces/mercurius-hook.interface.ts b/packages/mercurius/lib/interfaces/mercurius-hook.interface.ts new file mode 100644 index 000000000..91ee338df --- /dev/null +++ b/packages/mercurius/lib/interfaces/mercurius-hook.interface.ts @@ -0,0 +1,69 @@ +import { + MercuriusContext, + onGatewayReplaceSchemaHookHandler, + onResolutionHookHandler, + onSubscriptionEndHookHandler, + onSubscriptionResolutionHookHandler, + preExecutionHookHandler, + preGatewayExecutionHookHandler, + preGatewaySubscriptionExecutionHookHandler, + preParsingHookHandler, + preSubscriptionExecutionHookHandler, + preSubscriptionParsingHookHandler, + preValidationHookHandler, +} from 'mercurius'; + +export interface MercuriusHooksObject< + Context extends MercuriusContext = MercuriusContext, +> { + preParsing?: + | preParsingHookHandler + | preParsingHookHandler[]; + preValidation?: + | preValidationHookHandler + | preValidationHookHandler[]; + preExecution?: + | preExecutionHookHandler + | preExecutionHookHandler[]; + onResolution?: + | onResolutionHookHandler + | onResolutionHookHandler[]; + preSubscriptionParsing?: + | preSubscriptionParsingHookHandler + | preSubscriptionParsingHookHandler[]; + preSubscriptionExecution?: + | preSubscriptionExecutionHookHandler + | preSubscriptionExecutionHookHandler[]; + onSubscriptionResolution?: + | onSubscriptionResolutionHookHandler + | onSubscriptionResolutionHookHandler[]; + onSubscriptionEnd?: + | onSubscriptionEndHookHandler + | onSubscriptionEndHookHandler[]; +} + +export interface MercuriusHooks< + Context extends MercuriusContext = MercuriusContext, +> { + hooks?: MercuriusHooksObject; +} + +export interface MercuriusGatewayHooksObject< + Context extends MercuriusContext = MercuriusContext, +> extends MercuriusHooksObject { + preGatewayExecution?: + | preGatewayExecutionHookHandler + | preGatewayExecutionHookHandler[]; + preGatewaySubscriptionExecution?: + | preGatewaySubscriptionExecutionHookHandler + | preGatewaySubscriptionExecutionHookHandler[]; + onGatewayReplaceSchema?: + | onGatewayReplaceSchemaHookHandler + | onGatewayReplaceSchemaHookHandler[]; +} + +export interface MercuriusGatewayHooks< + Context extends MercuriusContext = MercuriusContext, +> { + hooks?: MercuriusGatewayHooksObject; +} diff --git a/packages/mercurius/lib/utils/register-mercurius-hooks.util.ts b/packages/mercurius/lib/utils/register-mercurius-hooks.util.ts new file mode 100644 index 000000000..704201aca --- /dev/null +++ b/packages/mercurius/lib/utils/register-mercurius-hooks.util.ts @@ -0,0 +1,25 @@ +import { FastifyInstance } from 'fastify'; +import { MercuriusGatewayHooksObject } from '../interfaces/mercurius-hook.interface'; +import { isArray, isNull, isUndefined } from './validation.util'; + +export function registerMercuriusHooks( + app: FastifyInstance, + hooks?: MercuriusGatewayHooksObject | null, +): void { + if (isUndefined(hooks) || isNull(hooks)) { + return; + } + + Object.entries(hooks).forEach(([hookName, hookFn]: [any, any]) => { + if (isUndefined(hookFn) || isNull(hookFn)) { + return; + } + + if (isArray(hookFn)) { + hookFn.forEach((fn) => app.graphql.addHook(hookName, fn)); + return; + } + + app.graphql.addHook(hookName, hookFn); + }); +} diff --git a/packages/mercurius/tests/e2e/base-hooks-array.spec.ts b/packages/mercurius/tests/e2e/base-hooks-array.spec.ts new file mode 100644 index 000000000..17c653372 --- /dev/null +++ b/packages/mercurius/tests/e2e/base-hooks-array.spec.ts @@ -0,0 +1,80 @@ +import { INestApplication } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { FastifyAdapter } from '@nestjs/platform-fastify'; +import * as request from 'supertest'; +import { ApplicationModule } from '../hooks/base-array/hooks.module'; +import { MockLogger } from '../hooks/mocks/logger.mock'; + +describe('Base hooks in array format', () => { + let app: INestApplication; + let logger: MockLogger; + + beforeEach(async () => { + logger = new MockLogger(); + app = await NestFactory.create(ApplicationModule, new FastifyAdapter(), { + logger, + }); + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + it('hooks should be triggered', async () => { + await request(app.getHttpServer()) + .post('/graphql') + .send({ + operationName: null, + variables: {}, + query: '{ getAnimalName }', + }) + .expect(200, { + data: { + getAnimalName: 'cat', + }, + }); + expect(logger.warn).toHaveBeenCalledTimes(8); + expect(logger.warn).toHaveBeenNthCalledWith( + 1, + 'preParsing1', + 'GqlConfigService', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 2, + 'preParsing2', + 'GqlConfigService', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 3, + 'preValidation1', + 'GqlConfigService', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 4, + 'preValidation2', + 'GqlConfigService', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 5, + 'preExecution1', + 'GqlConfigService', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 6, + 'preExecution2', + 'GqlConfigService', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 7, + 'onResolution1', + 'GqlConfigService', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 8, + 'onResolution2', + 'GqlConfigService', + ); + }); + + afterEach(async () => { + await app.close(); + }); +}); diff --git a/packages/mercurius/tests/e2e/base-hooks.spec.ts b/packages/mercurius/tests/e2e/base-hooks.spec.ts new file mode 100644 index 000000000..32f69075c --- /dev/null +++ b/packages/mercurius/tests/e2e/base-hooks.spec.ts @@ -0,0 +1,60 @@ +import { INestApplication } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { FastifyAdapter } from '@nestjs/platform-fastify'; +import * as request from 'supertest'; +import { ApplicationModule } from '../hooks/base/hooks.module'; +import { MockLogger } from '../hooks/mocks/logger.mock'; + +describe('Base hooks', () => { + let app: INestApplication; + let logger: MockLogger; + + beforeEach(async () => { + logger = new MockLogger(); + app = await NestFactory.create(ApplicationModule, new FastifyAdapter(), { + logger, + }); + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + it('hooks should be triggered', async () => { + await request(app.getHttpServer()) + .post('/graphql') + .send({ + operationName: null, + variables: {}, + query: '{ getAnimalName }', + }) + .expect(200, { + data: { + getAnimalName: 'cat', + }, + }); + expect(logger.warn).toHaveBeenCalledTimes(4); + expect(logger.warn).toHaveBeenNthCalledWith( + 1, + 'preParsing', + 'GqlConfigService', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 2, + 'preValidation', + 'GqlConfigService', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 3, + 'preExecution', + 'GqlConfigService', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 4, + 'onResolution', + 'GqlConfigService', + ); + }); + + afterEach(async () => { + await app.close(); + }); +}); diff --git a/packages/mercurius/tests/e2e/gateway-hooks.spec.ts b/packages/mercurius/tests/e2e/gateway-hooks.spec.ts new file mode 100644 index 000000000..3b75fd50b --- /dev/null +++ b/packages/mercurius/tests/e2e/gateway-hooks.spec.ts @@ -0,0 +1,134 @@ +import { INestApplication } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { + FastifyAdapter, + NestFastifyApplication, +} from '@nestjs/platform-fastify'; +import { Test } from '@nestjs/testing'; +import * as request from 'supertest'; +import { AppModule as PostsModule } from '../graphql-federation/posts-service/federation-posts.module'; +import { AppModule as UsersModule } from '../graphql-federation/users-service/federation-users.module'; +import { ApplicationModule } from '../hooks/gateway/hooks.module'; +import { MockLogger } from '../hooks/mocks/logger.mock'; + +function timeout(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +describe('GraphQL Gateway Hooks', () => { + let postsApp: INestApplication; + let usersApp: INestApplication; + let gatewayApp: INestApplication; + let logger: MockLogger; + + beforeEach(async () => { + const usersModule = await Test.createTestingModule({ + imports: [UsersModule], + }).compile(); + + usersApp = usersModule.createNestApplication(new FastifyAdapter()); + await usersApp.listen(3011); + + const postsModule = await Test.createTestingModule({ + imports: [PostsModule], + }).compile(); + + postsApp = postsModule.createNestApplication(new FastifyAdapter()); + await postsApp.listen(3012); + + logger = new MockLogger(); + gatewayApp = await NestFactory.create( + ApplicationModule, + new FastifyAdapter(), + { + logger, + }, + ); + + await gatewayApp.init(); + }); + + it('should trigger preGatewayExecution', async () => { + await gatewayApp.getHttpAdapter().getInstance().ready(); + await request(gatewayApp.getHttpServer()) + .post('/graphql') + .send({ + operationName: null, + variables: {}, + query: ` + { + getPosts { + id, + title, + body, + user { + id, + name, + } + } + }`, + }) + .expect(200, { + data: { + getPosts: [ + { + id: '1', + title: 'HELLO WORLD', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + user: { + id: '5', + name: 'GraphQL', + }, + }, + ], + }, + }); + expect(logger.warn).toHaveBeenCalledTimes(2); + expect(logger.warn).toHaveBeenNthCalledWith( + 1, + 'preGatewayExecution', + 'GqlConfigService', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 2, + 'preGatewayExecution', + 'GqlConfigService', + ); + }); + + it('should trigger onGatewayReplaceSchema', async () => { + const instance = await gatewayApp.getHttpAdapter().getInstance(); + await instance.ready(); + + setTimeout(async () => { + instance.graphql.gateway.serviceMap.users.setSchema( + ` + type User @key(fields: "id") { + id: ID! + name: String! + public: Boolean + } + + extend type Query { + getUser(id: ID!): User + } + `, + ); + }, 200); + setTimeout(() => { + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn).toHaveBeenNthCalledWith( + 1, + 'onGatewayReplaceSchema', + 'GqlConfigService', + ); + }, 1000); + await timeout(1001); + }); + + afterEach(async () => { + await postsApp.close(); + await usersApp.close(); + await gatewayApp.close(); + }); +}); diff --git a/packages/mercurius/tests/hooks/base-array/graphql.config.ts b/packages/mercurius/tests/hooks/base-array/graphql.config.ts new file mode 100644 index 000000000..bd68a2443 --- /dev/null +++ b/packages/mercurius/tests/hooks/base-array/graphql.config.ts @@ -0,0 +1,58 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { GqlOptionsFactory } from '@nestjs/graphql'; +import { MercuriusDriverConfig } from '../../../lib/interfaces/mercurius-driver-config.interface'; + +@Injectable() +export class GqlConfigService + implements GqlOptionsFactory +{ + private readonly logger = new Logger(GqlConfigService.name); + + public createGqlOptions(): MercuriusDriverConfig { + return { + autoSchemaFile: true, + hooks: { + preParsing: [ + (schema, document, context) => { + this.logger.warn('preParsing1'); + return { schema, document, context }; + }, + (schema, document, context) => { + this.logger.warn('preParsing2'); + return { schema, document, context }; + }, + ], + preValidation: [ + (schema, document, context) => { + this.logger.warn('preValidation1'); + return { schema, document, context }; + }, + (schema, document, context) => { + this.logger.warn('preValidation2'); + return { schema, document, context }; + }, + ], + preExecution: [ + (schema, document, context) => { + this.logger.warn('preExecution1'); + return { schema, document, context }; + }, + (schema, document, context) => { + this.logger.warn('preExecution2'); + return { schema, document, context }; + }, + ], + onResolution: [ + (execution, context) => { + this.logger.warn('onResolution1'); + return execution; + }, + (execution, context) => { + this.logger.warn('onResolution2'); + return execution; + }, + ], + }, + }; + } +} diff --git a/packages/mercurius/tests/hooks/base-array/hooks.module.ts b/packages/mercurius/tests/hooks/base-array/hooks.module.ts new file mode 100644 index 000000000..64fdc4fa1 --- /dev/null +++ b/packages/mercurius/tests/hooks/base-array/hooks.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { GraphQLModule } from '@nestjs/graphql'; +import { MercuriusDriver } from '../../../lib/drivers'; +import { MercuriusDriverConfig } from '../../../lib/interfaces/mercurius-driver-config.interface'; +import { CatsModule } from '../cats/cats.module'; +import { GqlConfigService } from './graphql.config'; + +@Module({ + imports: [ + CatsModule, + GraphQLModule.forRootAsync({ + driver: MercuriusDriver, + useClass: GqlConfigService, + }), + ], +}) +export class ApplicationModule {} diff --git a/packages/mercurius/tests/hooks/base/graphql.config.ts b/packages/mercurius/tests/hooks/base/graphql.config.ts new file mode 100644 index 000000000..1286613b8 --- /dev/null +++ b/packages/mercurius/tests/hooks/base/graphql.config.ts @@ -0,0 +1,34 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { GqlOptionsFactory } from '@nestjs/graphql'; +import { MercuriusDriverConfig } from '../../../lib/interfaces/mercurius-driver-config.interface'; + +@Injectable() +export class GqlConfigService + implements GqlOptionsFactory +{ + private readonly logger = new Logger(GqlConfigService.name); + + public createGqlOptions(): MercuriusDriverConfig { + return { + autoSchemaFile: true, + hooks: { + preParsing: (schema, document, context) => { + this.logger.warn('preParsing'); + return { schema, document, context }; + }, + preValidation: (schema, document, context) => { + this.logger.warn('preValidation'); + return { schema, document, context }; + }, + preExecution: (schema, document, context) => { + this.logger.warn('preExecution'); + return { schema, document, context }; + }, + onResolution: (execution, context) => { + this.logger.warn('onResolution'); + return execution; + }, + }, + }; + } +} diff --git a/packages/mercurius/tests/hooks/base/hooks.module.ts b/packages/mercurius/tests/hooks/base/hooks.module.ts new file mode 100644 index 000000000..64fdc4fa1 --- /dev/null +++ b/packages/mercurius/tests/hooks/base/hooks.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { GraphQLModule } from '@nestjs/graphql'; +import { MercuriusDriver } from '../../../lib/drivers'; +import { MercuriusDriverConfig } from '../../../lib/interfaces/mercurius-driver-config.interface'; +import { CatsModule } from '../cats/cats.module'; +import { GqlConfigService } from './graphql.config'; + +@Module({ + imports: [ + CatsModule, + GraphQLModule.forRootAsync({ + driver: MercuriusDriver, + useClass: GqlConfigService, + }), + ], +}) +export class ApplicationModule {} diff --git a/packages/mercurius/tests/hooks/cats/cats.module.ts b/packages/mercurius/tests/hooks/cats/cats.module.ts new file mode 100644 index 000000000..6e36eb45d --- /dev/null +++ b/packages/mercurius/tests/hooks/cats/cats.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { CatsResolver } from './cats.resolver'; + +@Module({ + providers: [CatsResolver], +}) +export class CatsModule {} diff --git a/packages/mercurius/tests/hooks/cats/cats.resolver.ts b/packages/mercurius/tests/hooks/cats/cats.resolver.ts new file mode 100644 index 000000000..1f02ebeac --- /dev/null +++ b/packages/mercurius/tests/hooks/cats/cats.resolver.ts @@ -0,0 +1,9 @@ +import { Query, Resolver } from '@nestjs/graphql'; + +@Resolver() +export class CatsResolver { + @Query((returns) => String) + public getAnimalName(): string { + return 'cat'; + } +} diff --git a/packages/mercurius/tests/hooks/gateway/graphql.config.ts b/packages/mercurius/tests/hooks/gateway/graphql.config.ts new file mode 100644 index 000000000..9ca5da926 --- /dev/null +++ b/packages/mercurius/tests/hooks/gateway/graphql.config.ts @@ -0,0 +1,45 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { GqlOptionsFactory } from '@nestjs/graphql'; +import { MercuriusGatewayDriverConfig } from '../../../lib/interfaces/mercurius-gateway-driver-config.interface'; + +@Injectable() +export class GqlConfigService + implements GqlOptionsFactory +{ + private readonly logger = new Logger(GqlConfigService.name); + + public createGqlOptions(): MercuriusGatewayDriverConfig { + return { + gateway: { + services: [ + { + name: 'users', + url: 'http://localhost:3011/graphql', + schema: ` + type User @key(fields: "id") { + id: ID! + name: String! + } + + extend type Query { + getUser(id: ID!): User + } + `, + }, + { name: 'posts', url: 'http://localhost:3012/graphql' }, + ], + pollingInterval: 500, + }, + hooks: { + preGatewayExecution: (schema, document, context) => { + this.logger.warn('preGatewayExecution'); + return { schema, document, context }; + }, + onGatewayReplaceSchema: (instance, schema) => { + this.logger.warn('onGatewayReplaceSchema'); + return schema; + }, + }, + }; + } +} diff --git a/packages/mercurius/tests/hooks/gateway/hooks.module.ts b/packages/mercurius/tests/hooks/gateway/hooks.module.ts new file mode 100644 index 000000000..0419654f1 --- /dev/null +++ b/packages/mercurius/tests/hooks/gateway/hooks.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { GraphQLModule } from '@nestjs/graphql'; +import { MercuriusGatewayDriver } from '../../../lib/drivers'; +import { MercuriusGatewayDriverConfig } from '../../../lib/interfaces/mercurius-gateway-driver-config.interface'; +import { GqlConfigService } from './graphql.config'; + +@Module({ + imports: [ + GraphQLModule.forRootAsync({ + driver: MercuriusGatewayDriver, + useClass: GqlConfigService, + }), + ], +}) +export class ApplicationModule {} diff --git a/packages/mercurius/tests/hooks/mocks/logger.mock.ts b/packages/mercurius/tests/hooks/mocks/logger.mock.ts new file mode 100644 index 000000000..28600696b --- /dev/null +++ b/packages/mercurius/tests/hooks/mocks/logger.mock.ts @@ -0,0 +1,9 @@ +import { LoggerService } from '@nestjs/common'; + +export class MockLogger implements LoggerService { + public log = jest.fn(); + public error = jest.fn(); + public warn = jest.fn(); + public debug = jest.fn(); + public verbose = jest.fn(); +} diff --git a/packages/mercurius/tests/subscriptions/app/app.module.ts b/packages/mercurius/tests/subscriptions/app/app.module.ts index c9d55663f..5ce0635fd 100644 --- a/packages/mercurius/tests/subscriptions/app/app.module.ts +++ b/packages/mercurius/tests/subscriptions/app/app.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { LoggerService, Module } from '@nestjs/common'; import { DynamicModule } from '@nestjs/common/interfaces'; import { GraphQLModule } from '@nestjs/graphql'; import { MercuriusDriverConfig } from '../../../lib'; @@ -8,6 +8,7 @@ import { NotificationModule } from './notification.module'; export type AppModuleConfig = { context?: MercuriusDriverConfig['context']; subscription?: MercuriusDriverConfig['subscription']; + logger?: LoggerService; }; @Module({}) @@ -26,4 +27,33 @@ export class AppModule { ], }; } + + static forRootWithHooks(options?: AppModuleConfig): DynamicModule { + return { + module: AppModule, + imports: [ + NotificationModule, + GraphQLModule.forRoot({ + driver: MercuriusDriver, + context: options?.context, + autoSchemaFile: true, + subscription: options?.subscription, + hooks: { + preSubscriptionParsing: (schema, document, context) => { + options?.logger.warn('preSubscriptionParsing'); + return { schema, document, context }; + }, + preSubscriptionExecution: async (schema, document, context) => { + options?.logger.warn('preSubscriptionExecution'); + return { schema, document, context }; + }, + onSubscriptionResolution: async (execution, context) => { + options?.logger.warn('onSubscriptionResolution'); + return { execution, context }; + }, + }, + }), + ], + }; + } } diff --git a/packages/mercurius/tests/subscriptions/subscriptions.spec.ts b/packages/mercurius/tests/subscriptions/subscriptions.spec.ts index 6495b3e4e..48639bfa7 100644 --- a/packages/mercurius/tests/subscriptions/subscriptions.spec.ts +++ b/packages/mercurius/tests/subscriptions/subscriptions.spec.ts @@ -4,10 +4,12 @@ import { Test } from '@nestjs/testing'; import { gql } from 'graphql-tag'; import { createMercuriusTestClient } from 'mercurius-integration-testing'; import { EventEmitter } from 'stream'; +import { MockLogger } from '../hooks/mocks/logger.mock'; import { AppModule } from './app/app.module'; class CustomPubSub { emitter: EventEmitter; + constructor() { this.emitter = new EventEmitter(); } @@ -164,3 +166,93 @@ describe('Subscriptions', () => { await app.close(); }); }); + +describe('Subscriptions with hooks', () => { + let app: INestApplication; + let logger: MockLogger; + + beforeEach(async () => { + logger = new MockLogger(); + const module = await Test.createTestingModule({ + imports: [ + AppModule.forRootWithHooks({ + subscription: { + pubsub, + context: (conn, request: any) => { + const { authorization } = request.raw?.headers ?? {}; + if (authorization) { + return { user: authorization.split('Bearer ')[1] }; + } else { + return {}; + } + }, + }, + logger, + }), + ], + }).compile(); + app = module.createNestApplication(new FastifyAdapter(), { logger }); + await app.listen(3077); + }); + + it('hooks should be triggered', (done) => { + const testClient = createMercuriusTestClient( + app.getHttpAdapter().getInstance(), + ); + const examplePayload = { + newNotification: { + id: '1', + recipient: 'test', + message: 'Hello ws', + }, + }; + testClient + .subscribe({ + query: subscriptionQuery, + variables: { + id: '1', + }, + headers: { + authorization: 'Bearer test', + }, + onData(response) { + expect(response.data).toEqual({ + newNotification: { + id: examplePayload.newNotification.id, + message: examplePayload.newNotification.message, + }, + }); + done(); + + expect(logger.warn).toHaveBeenCalledTimes(3); + expect(logger.warn).toHaveBeenNthCalledWith( + 1, + 'preSubscriptionParsing', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 2, + 'preSubscriptionExecution', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 3, + 'onSubscriptionResolution', + ); + }, + }) + .then(() => { + // timeout needed to allow the subscription to be established + setTimeout( + () => + pubsub.publish({ + topic: 'newNotification', + payload: examplePayload, + }), + 1000, + ); + }); + }); + + afterEach(async () => { + await app.close(); + }); +});