From 3e8406a51e9d534a547fe21e075f973e2f5528ab Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Tue, 21 Apr 2020 16:57:37 +0200 Subject: [PATCH 1/6] feat(schema): add schema.middleware helper --- docs/api/modules/main/exports/schema.md | 103 ++++++++++++++++++++++++ docs/index.html | 4 - src/runtime/app.ts | 12 +-- src/runtime/schema/schema.ts | 42 ++++++++-- 4 files changed, 147 insertions(+), 14 deletions(-) diff --git a/docs/api/modules/main/exports/schema.md b/docs/api/modules/main/exports/schema.md index 5aecd29e4..816ea0c7d 100644 --- a/docs/api/modules/main/exports/schema.md +++ b/docs/api/modules/main/exports/schema.md @@ -988,6 +988,109 @@ schema.use({ }) ``` +### `middleware` + +Add a global middleware to your schema. This middleware will be executed before/after every resolver of your schema. + +##### Signature + +```ts +schema.middleware((config: CreateFieldResolverInfo) => MiddlewareFn | undefined)): void +``` + +`schema.middleware` expect a function with the signature `(root, args, ctx, info, next)` to be returned. + +If the current middleware function does not end the request-response cycle, it must call `next` to pass control to the next middleware function, until the actual GraphQL resolver gets called. + +> Note: You can skip the creation of a middleware by returning `undefined` instead of a middleware. + +##### Examples + +**Simple middlewares** + +```ts +// graphql.ts +import { schema } from 'nexus' + +schema.middleware((_config) => { + return async (root, args, ctx, info, next) => { + console.log('before - middleware 1') + const result = await next(root, args, ctx, info) + console.log('after - middleware 1') + return result + } +}) + +schema.middleware((_config) => { + return async (root, args, ctx, info, next) => { + console.log('before - middleware 2') + const result = await next(root, args, ctx, info) + console.log('after - middleware 2') + return result + } +}) + +schema.queryType({ + definition(t) { + t.string('hello', () => { + console.log('executing resolver') + return Promise.resolve('world') + }) + }, +}) + +/** + * Output + * before - middleware 1 + * before - middleware 2 + * executing resolver + * after - middleware 2 + * after - middleware 1 + */ +``` + +**Trace resolvers completion time of the `Query` type only** + +```ts +import { schema } from 'nexus' + +schema.middleware((config) => { + if (config.parentTypeConfig.name !== 'Query') { + return + } + + return async (root, args, ctx, info, next) => { + const startTimeMs = new Date().valueOf() + const value = await next(root, args, ctx, info) + const endTimeMs = new Date().valueOf() + const resolver = `Query.${config.fieldConfig.name}` + const completionTime = endTimeMs - startTimeMs + + ctx.log.info(`Resolver '${resolver}' took ${completionTime} ms`, { + resolver, + completionTime, + }) + + return value + } +}) + +schema.queryType({ + definition(t) { + t.string('hello', async () => { + // Wait two seconds + await new Promise((res) => setTimeout(res, 2000)) + return 'world' + }) + }, +}) + +/** + * Output: + * ● server:request Resolver 'Query.hello' took 2001 ms -- resolver: 'Query.hello' time: 2001 + */ +``` + ### Type Glossary #### `I` `FieldConfig` diff --git a/docs/index.html b/docs/index.html index 8a93f0391..46e44f3cc 100644 --- a/docs/index.html +++ b/docs/index.html @@ -426,10 +426,6 @@ padding-top: 1rem !important; padding-bottom: 1rem !important; } - .markdown-section pre { - max-height: 40rem; - overflow-y: scroll; - } .markdown-section pre > code { font-size: 0.8rem; diff --git a/src/runtime/app.ts b/src/runtime/app.ts index 278dda8f5..86229563b 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -66,11 +66,13 @@ export type Settings = { change(newSetting: SettingsInput): void } +export type AppState = { + plugins: Plugin.Plugin[] + isWasServerStartCalled: boolean +} + export type InternalApp = App & { - __state: { - plugins: Plugin.Plugin[] - isWasServerStartCalled: boolean - } + __state: AppState } /** @@ -83,7 +85,7 @@ export function create(): App { } const server = Server.create() - const schemaComponent = Schema.create() + const schemaComponent = Schema.create(__state) const settings: Settings = { change(newSettings) { diff --git a/src/runtime/schema/schema.ts b/src/runtime/schema/schema.ts index 64d25e758..3f5461bc2 100644 --- a/src/runtime/schema/schema.ts +++ b/src/runtime/schema/schema.ts @@ -1,5 +1,7 @@ import * as NexusSchema from '@nexus/schema' -import { makeSchemaInternal } from '@nexus/schema/dist/core' +import { CreateFieldResolverInfo, makeSchemaInternal } from '@nexus/schema/dist/core' +import { stripIndent } from 'common-tags' +import { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql' import * as HTTP from 'http' import { runAddToContextExtractorAsWorkerIfPossible } from '../../lib/add-to-context-extractor/add-to-context-extractor' import * as Layout from '../../lib/layout' @@ -10,6 +12,7 @@ import { writeTypegen, } from '../../lib/nexus-schema-stateful' import { RuntimeContributions } from '../../lib/plugin' +import { AppState } from '../app' import { log } from './logger' import { changeSettings, mapSettingsToNexusSchemaConfig, SettingsData, SettingsInput } from './settings' @@ -26,11 +29,23 @@ export interface Request extends HTTP.IncomingMessage { export type ContextContributor = (req: Req) => Record +type MiddlewareFn = ( + source: any, + args: any, + context: NexusSchema.core.GetGen<'context'>, + info: GraphQLResolveInfo, + next: GraphQLFieldResolver +) => any + export interface Schema extends NexusSchemaStatefulBuilders { /** * todo link to website docs */ use: (schemaPlugin: NexusSchema.core.NexusPlugin) => void + /** + * todo link to website docs + */ + middleware: (fn: (config: CreateFieldResolverInfo) => MiddlewareFn | undefined) => void /** * todo link to website docs */ @@ -57,7 +72,7 @@ interface SchemaInternal { public: Schema } -export function create(): SchemaInternal { +export function create(appState: AppState): SchemaInternal { const statefulNexusSchema = createNexusSchemaStateful() const state: SchemaInternal['private']['state'] = { @@ -66,18 +81,35 @@ export function create(): SchemaInternal { contextContributors: [], } + const middleware: SchemaInternal['public']['middleware'] = (fn) => { + api.public.use( + NexusSchema.plugin({ + // TODO: Do we need to expose the name property? + name: 'local-middleware', + onCreateFieldResolver(config) { + return fn(config) + }, + }) + ) + } + const api: SchemaInternal = { public: { ...statefulNexusSchema.builders, use(plugin) { - // todo if caleld after app start, raise a warning (dev), error (prod) - // this will require the component having access to some of the - // framework state. + if (appState.isWasServerStartCalled === true) { + log.warn(stripIndent` + A Nexus Schema plugin was ignored because it was loaded after the server was started + Make sure to call \`schema.use\` before you call \`server.start\` + `) + } + state.schemaPlugins.push(plugin) }, addToContext(contextContributor) { state.contextContributors.push(contextContributor) }, + middleware, }, private: { state: state, From e4c883e6665751845ebcb09bfea7c62736c09893 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Tue, 21 Apr 2020 17:07:35 +0200 Subject: [PATCH 2/6] fix tests --- src/runtime/schema/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/schema/index.spec.ts b/src/runtime/schema/index.spec.ts index 92ff9cd70..9b1f45bdc 100644 --- a/src/runtime/schema/index.spec.ts +++ b/src/runtime/schema/index.spec.ts @@ -5,7 +5,7 @@ import { mapSettingsToNexusSchemaConfig } from './settings' let schema: ReturnType beforeEach(() => { - schema = create() + schema = create({ isWasServerStartCalled: false, plugins: [] }) }) it('defaults to outputs being nullable by default', () => { From fc9844a2e74bc7037ba2a224ae3d3887f5c4577c Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Tue, 21 Apr 2020 17:12:55 +0200 Subject: [PATCH 3/6] fix tests --- src/lib/watcher/index.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/watcher/index.spec.ts b/src/lib/watcher/index.spec.ts index e39c5442c..1b7154d88 100644 --- a/src/lib/watcher/index.spec.ts +++ b/src/lib/watcher/index.spec.ts @@ -177,14 +177,14 @@ it('restarts when a file is deleted', async () => { it('restarts when a file has an error', async () => { const { bufferedEvents } = await testSimpleCase({ - entrypoint: `throw new Error('there is an error')`, + entrypoint: `throw new Error('This is an expected test error')`, fsUpdate: () => { ctx.write({ 'entrypoint.ts': `process.stdout.write('error fixed')` }) }, }) expect(bufferedEvents[0].type).toBe('runner_stdio') - expect((bufferedEvents[0] as any).data).toContain('Error: there is an error') + expect((bufferedEvents[0] as any).data).toContain('Error: This is an expected test error') bufferedEvents.shift() From ba2c605c1395a4a4a63fd96d4dc0a56f1cdf7a80 Mon Sep 17 00:00:00 2001 From: Flavian Desverne Date: Tue, 21 Apr 2020 19:54:12 +0200 Subject: [PATCH 4/6] Update doc Co-Authored-By: Jason Kuhrt --- docs/api/modules/main/exports/schema.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/modules/main/exports/schema.md b/docs/api/modules/main/exports/schema.md index 816ea0c7d..df723658f 100644 --- a/docs/api/modules/main/exports/schema.md +++ b/docs/api/modules/main/exports/schema.md @@ -990,7 +990,7 @@ schema.use({ ### `middleware` -Add a global middleware to your schema. This middleware will be executed before/after every resolver of your schema. +Run arbitrary logic before and/or after GraphQL resolvers in your schema. Middleware is run as a first-in-first-out (FIFO) stack. The order of your middleware definitions determines their order in the stack. ##### Signature From 690875934bb6435eedfa2125222f60eecd2d5299 Mon Sep 17 00:00:00 2001 From: Flavian Desverne Date: Tue, 21 Apr 2020 20:38:58 +0200 Subject: [PATCH 5/6] Update docs Co-Authored-By: Jason Kuhrt --- docs/api/modules/main/exports/schema.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/modules/main/exports/schema.md b/docs/api/modules/main/exports/schema.md index df723658f..7fd2a2f1f 100644 --- a/docs/api/modules/main/exports/schema.md +++ b/docs/api/modules/main/exports/schema.md @@ -1006,7 +1006,7 @@ If the current middleware function does not end the request-response cycle, it m ##### Examples -**Simple middlewares** +##### Example: Simple middlewares ```ts // graphql.ts From ef72586c94fd096849d452e9a5683483f7842854 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Tue, 21 Apr 2020 20:39:51 +0200 Subject: [PATCH 6/6] update docs --- docs/api/modules/main/exports/schema.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/api/modules/main/exports/schema.md b/docs/api/modules/main/exports/schema.md index 7fd2a2f1f..2683343f8 100644 --- a/docs/api/modules/main/exports/schema.md +++ b/docs/api/modules/main/exports/schema.md @@ -1004,8 +1004,6 @@ If the current middleware function does not end the request-response cycle, it m > Note: You can skip the creation of a middleware by returning `undefined` instead of a middleware. -##### Examples - ##### Example: Simple middlewares ```ts @@ -1049,7 +1047,7 @@ schema.queryType({ */ ``` -**Trace resolvers completion time of the `Query` type only** +##### Example: Trace resolvers completion time of the `Query` type only ```ts import { schema } from 'nexus'