UNPKG

@graphql-yoga/nestjs

Version:

GraphQL Yoga driver for NestJS GraphQL.

249 lines (248 loc) • 11.3 kB
import { __decorate } from "tslib"; import { printSchema } from 'graphql'; import { createYoga, filter, mergeSchemas, pipe, } from 'graphql-yoga'; import { Injectable, Logger } from '@nestjs/common'; import { AbstractGraphQLDriver, GqlSubscriptionService, } from '@nestjs/graphql'; export class AbstractYogaDriver extends AbstractGraphQLDriver { yoga; async start(options) { const platformName = this.httpAdapterHost.httpAdapter.getType(); options = { ...options, // disable error masking by default maskedErrors: options.maskedErrors == null ? false : options.maskedErrors, // disable graphiql in production graphiql: options.graphiql == null ? process.env['NODE_ENV'] !== 'production' : options.graphiql, }; if (platformName === 'express') { return this.registerExpress(options); } if (platformName === 'fastify') { return this.registerFastify(options); } throw new Error(`Provided HttpAdapter "${platformName}" not supported`); } async stop() { // noop } registerExpress({ conditionalSchema, ...options }, { preStartHook } = {}) { const app = this.httpAdapterHost.httpAdapter.getInstance(); preStartHook?.(app); // nest's logger doesnt have the info method class LoggerWithInfo extends Logger { constructor(context) { super(context); } // eslint-disable-next-line @typescript-eslint/no-explicit-any info(message, ...args) { this.log(message, ...args); } } const schema = this.mergeConditionalSchema(conditionalSchema, options.schema); const yoga = createYoga({ ...options, schema, graphqlEndpoint: options.path, // disable logging by default // however, if `true` use nest logger logging: options.logging == null ? false : options.logging ? new LoggerWithInfo('YogaDriver') : options.logging, }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - TODO: fix types this.yoga = yoga; app.use(yoga.graphqlEndpoint, (req, res) => yoga(req, res, { req, res })); } registerFastify({ conditionalSchema, ...options }, { preStartHook } = {}) { const app = this.httpAdapterHost.httpAdapter.getInstance(); preStartHook?.(app); const schema = this.mergeConditionalSchema(conditionalSchema, options.schema); const yoga = createYoga({ ...options, schema, graphqlEndpoint: options.path, // disable logging by default // however, if `true` use fastify logger logging: options.logging == null ? false : options.logging ? app.log : options.logging, }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - TODO: fix types this.yoga = yoga; app.all(yoga.graphqlEndpoint, async (req, reply) => { const response = await yoga.handleNodeRequestAndResponse(req, reply, { req, reply, }); for (const [key, value] of response.headers.entries()) reply.header(key, value); reply.status(response.status); reply.send(response.body); return reply; }); } mergeConditionalSchema(conditionalSchema, schema) { let mergedSchema = schema; if (conditionalSchema) { mergedSchema = async (request) => { const schemas = []; if (schema) { schemas.push(schema); } const conditionalSchemaResult = typeof conditionalSchema === 'function' ? await conditionalSchema(request) : await conditionalSchema; if (conditionalSchemaResult) { schemas.push(conditionalSchemaResult); } return mergeSchemas({ schemas, }); }; } return mergedSchema; } subscriptionWithFilter(instanceRef, filterFn, // disable next error, the original function in @nestjs/graphql is also untyped // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type createSubscribeContext) { return async (...args) => { return pipe(await createSubscribeContext()(...args), filter((payload) => // typecast the spread sliced args to avoid error TS 2556, see https://github.com/microsoft/TypeScript/issues/49802 filterFn.call(instanceRef, payload, ...args.slice(1)))); }; } } let YogaDriver = class YogaDriver extends AbstractYogaDriver { subscriptionService; async start(options) { if (options.definitions?.path) { if (!options.schema) { throw new Error('Schema is required when generating definitions'); } await this.graphQlFactory.generateDefinitions(printSchema(options.schema), options); } await super.start(options); if (options.subscriptions) { if (!options.schema) { throw new Error('Schema is required when using subscriptions'); } const config = options.subscriptions === true ? { 'graphql-ws': true, } : options.subscriptions; if (config['graphql-ws']) { config['graphql-ws'] = typeof config['graphql-ws'] === 'object' ? config['graphql-ws'] : {}; if (options.conditionalSchema) { throw new Error(` Conditional schema is not supported with graphql-ws. `); } config['graphql-ws'].onSubscribe = async (ctx, _id, params) => { const { schema, execute, subscribe, contextFactory, parse, validate } = this.yoga.getEnveloped({ ...ctx, // @ts-expect-error context extra is from graphql-ws/lib/use/ws req: ctx.extra.request, // @ts-expect-error context extra is from graphql-ws/lib/use/ws socket: ctx.extra.socket, params, }); const args = { schema, operationName: params.operationName, document: parse(params.query), variableValues: params.variables, contextValue: await contextFactory({ execute, subscribe }), }; const errors = validate(args.schema, args.document); if (errors.length) return errors; return args; }; } if (config['subscriptions-transport-ws']) { config['subscriptions-transport-ws'] = typeof config['subscriptions-transport-ws'] === 'object' ? config['subscriptions-transport-ws'] : {}; if (options.conditionalSchema) { throw new Error(` Conditional schema is not supported with subscriptions-transport-ws. `); } config['subscriptions-transport-ws'].onOperation = async (_msg, params, ws) => { const { schema, execute, subscribe, contextFactory, parse, validate } = this.yoga.getEnveloped({ ...params.context, req: // @ts-expect-error upgradeReq does exist but is untyped ws.upgradeReq, socket: ws, params, }); const args = { schema, operationName: params.operationName, document: typeof params.query === 'string' ? parse(params.query) : params.query, variables: params.variables, context: await contextFactory({ execute, subscribe }), }; const errors = validate(args.schema, args.document); if (errors.length) return errors; return args; }; } this.subscriptionService = new GqlSubscriptionService({ schema: options.schema, path: options.path, // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- because we test both graphql v15 and v16 // @ts-ignore execute: (...args) => { const contextValue = // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- because we test both graphql v15 and v16 // @ts-ignore args[0].contextValue || // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- because we test both graphql v15 and v16 // @ts-ignore args[3]; if (!contextValue) { throw new Error('Execution arguments are missing the context value'); } return (contextValue // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- because we test both graphql v15 and v16 // @ts-ignore .execute(...args)); }, // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- because we test both graphql v15 and v16 // @ts-ignore subscribe: (...args) => { const contextValue = // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- because we test both graphql v15 and v16 // @ts-ignore args[0].contextValue || // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- because we test both graphql v15 and v16 // @ts-ignore args?.[3]; if (!contextValue) { throw new Error('Subscribe arguments are missing the context value'); } return (contextValue // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- because we test both graphql v15 and v16 // @ts-ignore .subscribe(...args)); }, ...config, }, this.httpAdapterHost.httpAdapter.getHttpServer()); } } async stop() { await this.subscriptionService?.stop(); } }; YogaDriver = __decorate([ Injectable() ], YogaDriver); export { YogaDriver };