From c39bf78d518e05f7317462d7d299d23d7f28fdf9 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Thu, 23 Jul 2020 19:14:12 +0200 Subject: [PATCH 01/11] feat: add cors support for express --- src/runtime/app.ts | 2 +- src/runtime/server/server.ts | 10 +++- src/runtime/server/settings.ts | 87 ++++++++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/runtime/app.ts b/src/runtime/app.ts index 00bfd46b6..4dd8fd7b3 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -46,7 +46,7 @@ export interface App { use(plugin: Plugin.Plugin): void /** * Run this to gather the final state of all Nexus api interactions. This method - * is experimental. It provides experimental support for Nextjs integration. + * is experimental. It provides experimental support for NextJS integration. * * In a regular Nexus app, you should not need to use this method. * diff --git a/src/runtime/server/server.ts b/src/runtime/server/server.ts index f918c457c..e4b67a011 100644 --- a/src/runtime/server/server.ts +++ b/src/runtime/server/server.ts @@ -71,6 +71,10 @@ export function create(appState: AppState) { get graphql() { return ( assembledGuard(appState, 'app.server.handlers.graphql', () => { + if (Boolean(settings.data.cors)) { + log.warn('CORS does not work for serverless handlers. Settings will be ignored.') + } + return createRequestHandlerGraphQL( appState.assembled!.schema, appState.assembled!.createContext, @@ -115,7 +119,11 @@ export function create(appState: AppState) { : false, }) - state.apolloServer.applyMiddleware({ app: express, path: settings.data.path }) + state.apolloServer.applyMiddleware({ + app: express, + path: settings.data.path, + cors: settings.data.cors, + }) return { createContext } }, diff --git a/src/runtime/server/settings.ts b/src/runtime/server/settings.ts index 5809a04f4..9b914d08e 100644 --- a/src/runtime/server/settings.ts +++ b/src/runtime/server/settings.ts @@ -1,6 +1,7 @@ +import { PlaygroundRenderPageOptions } from 'apollo-server-express' +import { CorsOptions as OriginalCorsOption } from 'cors' import * as Process from '../../lib/process' import * as Utils from '../../lib/utils' -import { PlaygroundRenderPageOptions } from 'apollo-server-express' import { log as serverLogger } from './logger' const log = serverLogger.child('settings') @@ -9,6 +10,60 @@ export type PlaygroundSettings = { settings?: Omit>, 'general.betaUpdates'> } +export type CorsSettings = { + /** + * Configures the Access-Control-Allow-Origin CORS header. Possible values: + * + * Boolean - set origin to true to reflect the request origin, as defined by req.header('Origin'), or set it to false to disable CORS. + * String - set origin to a specific origin. For example if you set it to "http://example.com" only requests from "http://example.com" will be allowed. + * RegExp - set origin to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern /example\.com$/ will reflect any request that is coming from an origin ending with "example.com". + * Array - set origin to an array of valid origins. Each origin can be a String or a RegExp. For example ["http://example1.com", /\.example2\.com$/] will accept any request from "http://example1.com" or from a subdomain of "example2.com". + * Function - set origin to a function implementing some custom logic. The function takes the request origin as the first parameter and a callback (called as callback(err, origin), where origin is a non-function value of the origin option) as the second. + * + */ + origin?: OriginalCorsOption['origin'] + /** + * Configures the Access-Control-Allow-Methods CORS header. + * + * Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: ['GET', 'PUT', 'POST']). + */ + methods?: string | string[] + /** + * Configures the Access-Control-Allow-Headers CORS header. + * + * Expects a comma-delimited string (ex: 'Content-Type,Authorization') or an array (ex: ['Content-Type', 'Authorization']). + * If not specified, defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header. + */ + allowedHeaders?: string | string[] + /** + * Configures the Access-Control-Expose-Headers CORS header. + * + * Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') or an array (ex: ['Content-Range', 'X-Content-Range']). + * If not specified, no custom headers are exposed. + */ + exposedHeaders?: string | string[] + /** + * Configures the Access-Control-Allow-Credentials CORS header. + * + * Set to true to pass the header, otherwise it is omitted. + */ + credentials?: boolean + /** + * Configures the Access-Control-Max-Age CORS header. + * + * Set to an integer to pass the header, otherwise it is omitted. + */ + maxAge?: number + /** + * Pass the CORS preflight response to the next handler. + */ + preflightContinue?: boolean + /** + * Provides a status code to use for successful OPTIONS requests, since some legacy browsers (IE11, various SmartTVs) choke on 204. + */ + optionsSuccessStatus?: number +} + export type GraphqlSettings = { introspection?: boolean } @@ -33,15 +88,31 @@ export type SettingsInput = { * production, without some kind of security/access control, you will almost * certainly want this disabled. * - * To learn more about GraphQL Playgorund see + * To learn more about GraphQL Playground see * https://github.com/prisma-labs/graphql-playground */ playground?: boolean | PlaygroundSettings + /** + * Enable CORS for your server + * + * When true is passed, the default config is the following: + * + * ``` + * { + * "origin": "*", + * "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + * "preflightContinue": false, + * "optionsSuccessStatus": 204 + * } + * ``` + * + * @default false + */ + cors?: boolean | CorsSettings /** * The path on which the GraphQL API should be served. * - * @default - * /graphql + * @default /graphql */ path?: string /** @@ -55,10 +126,14 @@ export type SettingsInput = { graphql?: GraphqlSettings } -export type SettingsData = Omit, 'host' | 'playground' | 'graphql'> & { +export type SettingsData = Omit< + Utils.DeepRequired, + 'host' | 'playground' | 'graphql' | 'cors' +> & { host: string | undefined playground: false | Required graphql: Required + cors: boolean | CorsSettings } export const defaultPlaygroundPath = '/graphql' @@ -104,6 +179,7 @@ export const defaultSettings: () => Readonly = () => { }, playground: process.env.NODE_ENV === 'production' ? false : defaultPlaygroundSettings(), path: '/graphql', + cors: false, graphql: defaultGraphqlSettings(), } } @@ -159,6 +235,7 @@ export function changeSettings(state: SettingsData, newSettings: SettingsInput): state.port = updatedSettings.port state.startMessage = updatedSettings.startMessage state.graphql = graphqlSettings(updatedSettings.graphql) + state.cors = updatedSettings.cors } export function createServerSettingsManager() { From 053c41c4577e001420a2807589aafd88f41b7c2b Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Fri, 24 Jul 2020 11:26:45 +0200 Subject: [PATCH 02/11] fix review --- src/runtime/server/settings.ts | 41 ++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/runtime/server/settings.ts b/src/runtime/server/settings.ts index 9b914d08e..45cf58d9c 100644 --- a/src/runtime/server/settings.ts +++ b/src/runtime/server/settings.ts @@ -3,6 +3,7 @@ import { CorsOptions as OriginalCorsOption } from 'cors' import * as Process from '../../lib/process' import * as Utils from '../../lib/utils' import { log as serverLogger } from './logger' +import { LiteralUnion } from 'type-fest' const log = serverLogger.child('settings') @@ -10,36 +11,50 @@ export type PlaygroundSettings = { settings?: Omit>, 'general.betaUpdates'> } +export type HTTPMethods = LiteralUnion<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD', string> + export type CorsSettings = { + /** + * Enable or disable CORS. + * + * @default true + */ + enabled?: boolean /** * Configures the Access-Control-Allow-Origin CORS header. Possible values: * * Boolean - set origin to true to reflect the request origin, as defined by req.header('Origin'), or set it to false to disable CORS. + * * String - set origin to a specific origin. For example if you set it to "http://example.com" only requests from "http://example.com" will be allowed. + * * RegExp - set origin to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern /example\.com$/ will reflect any request that is coming from an origin ending with "example.com". + * * Array - set origin to an array of valid origins. Each origin can be a String or a RegExp. For example ["http://example1.com", /\.example2\.com$/] will accept any request from "http://example1.com" or from a subdomain of "example2.com". + * * Function - set origin to a function implementing some custom logic. The function takes the request origin as the first parameter and a callback (called as callback(err, origin), where origin is a non-function value of the origin option) as the second. * */ - origin?: OriginalCorsOption['origin'] + origin?: OriginalCorsOption['origin'] // TODO: Improve function interface with promise-based callback /** * Configures the Access-Control-Allow-Methods CORS header. - * - * Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: ['GET', 'PUT', 'POST']). + * + * @example ['GET', 'PUT', 'POST'] */ - methods?: string | string[] + methods?: string | HTTPMethods[] /** * Configures the Access-Control-Allow-Headers CORS header. * - * Expects a comma-delimited string (ex: 'Content-Type,Authorization') or an array (ex: ['Content-Type', 'Authorization']). * If not specified, defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header. + * + * @example ['Content-Type', 'Authorization'] */ allowedHeaders?: string | string[] /** * Configures the Access-Control-Expose-Headers CORS header. * - * Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') or an array (ex: ['Content-Range', 'X-Content-Range']). * If not specified, no custom headers are exposed. + * + * @example ['Content-Range', 'X-Content-Range'] */ exposedHeaders?: string | string[] /** @@ -224,6 +239,18 @@ function validateGraphQLPath(path: string): string { return outputPath } +export function corsSettings(newSettings: SettingsInput['cors']): SettingsData['cors'] { + if (typeof newSettings === 'boolean') { + return newSettings + } + + if (!newSettings || newSettings.enabled === false) { + return false + } + + return newSettings +} + /** * Mutate the settings data */ @@ -235,7 +262,7 @@ export function changeSettings(state: SettingsData, newSettings: SettingsInput): state.port = updatedSettings.port state.startMessage = updatedSettings.startMessage state.graphql = graphqlSettings(updatedSettings.graphql) - state.cors = updatedSettings.cors + state.cors = corsSettings(updatedSettings.cors) } export function createServerSettingsManager() { From 593129f7beb21ac10de0c89805c74f6df6a70e82 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Fri, 24 Jul 2020 11:34:22 +0200 Subject: [PATCH 03/11] add cors api docs --- .../content/040-api/01-nexus/04-settings.mdx | 108 ++++++++++++++++-- 1 file changed, 98 insertions(+), 10 deletions(-) diff --git a/website/content/040-api/01-nexus/04-settings.mdx b/website/content/040-api/01-nexus/04-settings.mdx index 5395b79f8..a09d19fca 100644 --- a/website/content/040-api/01-nexus/04-settings.mdx +++ b/website/content/040-api/01-nexus/04-settings.mdx @@ -17,6 +17,7 @@ Use the settings to centrally configure various aspects of the various component host?: string path?: string playground?: boolean | PlaygroundSettings + cors?: boolean | CorsSettings } schema?: { nullable?: { @@ -50,7 +51,7 @@ Change your app's current settings. This is an additive API meaning it can be ca #### `server.playground` -``` +```ts boolean | PlaygroundSettings ``` @@ -62,7 +63,7 @@ _Default_ #### `server.playground.settings` -``` +```ts { 'request.credentials': string; 'tracing.hideTracingResponse': boolean; @@ -79,7 +80,7 @@ Configure the Playground settings _Default_ -``` +```ts { 'request.credentials': 'omit', 'tracing.hideTracingResponse': true, @@ -94,7 +95,7 @@ _Default_ #### `server.port` -``` +```ts number ``` @@ -109,7 +110,7 @@ _Default_ #### `server.host` -``` +```ts string ``` @@ -122,7 +123,7 @@ _Default_ #### `server.path` -``` +```ts string ``` @@ -132,9 +133,96 @@ _Default_ `/graphql` -#### `schema.nullable.inputs` +#### `server.cors` +```ts +boolean | { + /** + * Enable or disable CORS. + * + * @default true + */ + enabled?: boolean + /** + * Configures the Access-Control-Allow-Origin CORS header. Possible values: + * + * Boolean - set origin to true to reflect the request origin, as defined by req.header('Origin'), or set it to false to disable CORS. + * + * String - set origin to a specific origin. For example if you set it to "http://example.com" only requests from "http://example.com" will be allowed. + * + * RegExp - set origin to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern /example\.com$/ will reflect any request that is coming from an origin ending with "example.com". + * + * Array - set origin to an array of valid origins. Each origin can be a String or a RegExp. For example ["http://example1.com", /\.example2\.com$/] will accept any request from "http://example1.com" or from a subdomain of "example2.com". + * + * Function - set origin to a function implementing some custom logic. The function takes the request origin as the first parameter and a callback (called as callback(err, origin), where origin is a non-function value of the origin option) as the second. + * + */ + origin?: OriginalCorsOption['origin'] // TODO: Improve function interface with promise-based callback + /** + * Configures the Access-Control-Allow-Methods CORS header. + * + * @example ['GET', 'PUT', 'POST'] + */ + methods?: string | HTTPMethods[] + /** + * Configures the Access-Control-Allow-Headers CORS header. + * + * If not specified, defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header. + * + * @example ['Content-Type', 'Authorization'] + */ + allowedHeaders?: string | string[] + /** + * Configures the Access-Control-Expose-Headers CORS header. + * + * If not specified, no custom headers are exposed. + * + * @example ['Content-Range', 'X-Content-Range'] + */ + exposedHeaders?: string | string[] + /** + * Configures the Access-Control-Allow-Credentials CORS header. + * + * Set to true to pass the header, otherwise it is omitted. + */ + credentials?: boolean + /** + * Configures the Access-Control-Max-Age CORS header. + * + * Set to an integer to pass the header, otherwise it is omitted. + */ + maxAge?: number + /** + * Pass the CORS preflight response to the next handler. + */ + preflightContinue?: boolean + /** + * Provides a status code to use for successful OPTIONS requests, since some legacy browsers (IE11, various SmartTVs) choke on 204. + */ + optionsSuccessStatus?: number +} ``` + +Enable CORS for your server. + +When true is passed, the default config is the following: + +```ts +{ + "origin": "*", + "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "preflightContinue": false, + "optionsSuccessStatus": 204 +} +``` + +_Default_ + +false + +#### `schema.nullable.inputs` + +```ts boolean ``` @@ -146,7 +234,7 @@ _Default_ #### `schema.nullable.outputs` -``` +```ts boolean ``` @@ -158,7 +246,7 @@ _Default_ #### `schema.generateGraphQLSDLFile` -``` +```ts false | string ``` @@ -172,7 +260,7 @@ _Default_ #### `schema.rootTypingsGlobPattern` -``` +```ts string ``` From 6851eae6543f1cf059b1be0c31f2703aaa5df127 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 27 Jul 2020 11:18:18 +0200 Subject: [PATCH 04/11] fix: context params and pass req and res in an object BREAKING CHANGE: the incoming request and server response are now passed as a parameter object with `req` and `res` as key. --- src/runtime/app.ts | 3 +-- src/runtime/schema/index.ts | 3 ++- src/runtime/schema/schema.ts | 7 ++++++- src/runtime/server/handler-graphql.ts | 7 ++++--- src/runtime/server/server.ts | 23 +++++++---------------- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/runtime/app.ts b/src/runtime/app.ts index 4dd8fd7b3..7e1372d37 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -9,7 +9,6 @@ import { Index } from '../lib/utils' import * as Lifecycle from './lifecycle' import * as Schema from './schema' import * as Server from './server' -import { ContextCreator } from './server/server' import * as Settings from './settings' import { assertAppNotAssembled } from './utils' @@ -88,7 +87,7 @@ export type AppState = { schema: NexusSchema.core.NexusGraphQLSchema missingTypes: Index loadedPlugins: RuntimeContributions[] - createContext: ContextCreator + createContext: Schema.ContextContributor } running: boolean components: { diff --git a/src/runtime/schema/index.ts b/src/runtime/schema/index.ts index b05b75afd..8eefaacf2 100644 --- a/src/runtime/schema/index.ts +++ b/src/runtime/schema/index.ts @@ -1,2 +1,3 @@ -export { create, LazyState, Schema } from './schema' +export { ContextContributor, create, LazyState, Schema } from './schema' export { SettingsData, SettingsInput } from './settings' + diff --git a/src/runtime/schema/schema.ts b/src/runtime/schema/schema.ts index 1964a8a27..8752f0d8d 100644 --- a/src/runtime/schema/schema.ts +++ b/src/runtime/schema/schema.ts @@ -41,7 +41,12 @@ export interface Request extends HTTP.IncomingMessage { log: NexusLogger.Logger } -export type ContextContributor = (req: Request) => MaybePromise> +export interface Response extends HTTP.ServerResponse {} + +export type ContextContributor = (params: { + req: Request + res: Response +}) => MaybePromise> type MiddlewareFn = ( source: any, diff --git a/src/runtime/server/handler-graphql.ts b/src/runtime/server/handler-graphql.ts index cbfbb5f87..90459b1ba 100644 --- a/src/runtime/server/handler-graphql.ts +++ b/src/runtime/server/handler-graphql.ts @@ -1,7 +1,8 @@ import { GraphQLError, GraphQLFormattedError, GraphQLSchema } from 'graphql' +import { ContextContributor } from '../schema' import { ApolloServerless } from './apollo-server' import { log } from './logger' -import { ContextCreator, NexusRequestHandler } from './server' +import { NexusRequestHandler } from './server' import { PlaygroundSettings } from './settings' type Settings = { @@ -13,7 +14,7 @@ type Settings = { type CreateHandler = ( schema: GraphQLSchema, - createContext: ContextCreator, + createContext: ContextContributor, settings: Settings ) => NexusRequestHandler @@ -30,5 +31,5 @@ export const createRequestHandlerGraphQL: CreateHandler = (schema, createContext playground: settings.playground, }) - return server.createHandler({ path: settings.path }) + return server.createHandler({ path: settings.path }) } diff --git a/src/runtime/server/server.ts b/src/runtime/server/server.ts index e4b67a011..36b482bf1 100644 --- a/src/runtime/server/server.ts +++ b/src/runtime/server/server.ts @@ -4,10 +4,10 @@ import * as HTTP from 'http' import { HttpError } from 'http-errors' import * as Net from 'net' import * as Plugin from '../../lib/plugin' -import { httpClose, httpListen, MaybePromise, noop } from '../../lib/utils' +import { httpClose, httpListen, noop } from '../../lib/utils' import { AppState } from '../app' import * as DevMode from '../dev-mode' -import { ContextContributor } from '../schema/schema' +import { ContextContributor } from '../schema' import { assembledGuard } from '../utils' import { ApolloServerExpress } from './apollo-server' import { errorFormatter } from './error-formatter' @@ -45,7 +45,7 @@ export interface Server { interface State { running: boolean httpServer: HTTP.Server - createContext: null | (() => ContextCreator, Record>) + createContext: null | (() => ContextContributor) apolloServer: null | ApolloServerExpress } @@ -182,29 +182,20 @@ const wrapHandlerWithErrorHandling = (handler: NexusRequestHandler): NexusReques } } -type AnonymousRequest = Record - -type AnonymousContext = Record - -export type ContextCreator< - Req extends AnonymousRequest = AnonymousRequest, - Context extends AnonymousContext = AnonymousContext -> = (req: Req) => MaybePromise - /** * Combine all the context contributions defined in the app and in plugins. */ function createContextCreator( contextContributors: ContextContributor[], plugins: Plugin.RuntimeContributions[] -): ContextCreator { - const createContext: ContextCreator = async (req) => { +): ContextContributor { + const createContext: ContextContributor = async (params) => { let context: Record = {} // Integrate context from plugins for (const plugin of plugins) { if (!plugin.context) continue - const contextContribution = await plugin.context.create(req) + const contextContribution = await plugin.context.create(params.req) Object.assign(context, contextContribution) } @@ -212,7 +203,7 @@ function createContextCreator( // Integrate context from app context api // TODO good runtime feedback to user if something goes wrong for (const contextContributor of contextContributors) { - const contextContribution = await contextContributor(req as any) + const contextContribution = await contextContributor(params) Object.assign(context, contextContribution) } From dd08276789a7176fa587918063e3462f20803e67 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 27 Jul 2020 11:52:45 +0200 Subject: [PATCH 05/11] add context test --- src/runtime/server/context.spec.ts | 71 ++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/runtime/server/context.spec.ts diff --git a/src/runtime/server/context.spec.ts b/src/runtime/server/context.spec.ts new file mode 100644 index 000000000..dfca4e3b2 --- /dev/null +++ b/src/runtime/server/context.spec.ts @@ -0,0 +1,71 @@ +import { makeSchema, queryType } from '@nexus/schema' +import { IncomingMessage, ServerResponse } from 'http' +import { Socket } from 'net' +import { createRequestHandlerGraphQL } from './handler-graphql' +import { NexusRequestHandler } from './server' +import { errorFormatter } from './error-formatter' + +let handler: NexusRequestHandler +let socket: Socket +let req: IncomingMessage +let res: ServerResponse +let contextInput: any + +beforeEach(() => { + // todo actually use req body etc. + contextInput = null + socket = new Socket() + req = new IncomingMessage(socket) + res = new ServerResponse(req) + createHandler( + queryType({ + definition(t) { + t.boolean('foo', () => false) + }, + }) + ) +}) + +it('passes the request and response to the schema context', async () => { + reqPOST(`{ foo }`) + + await handler(req, res) + + expect(contextInput.req).toBeInstanceOf(IncomingMessage) + expect(contextInput.res).toBeInstanceOf(ServerResponse) +}) + +/** + * helpers + */ + +function createHandler(...types: any) { + handler = createRequestHandlerGraphQL( + makeSchema({ + outputs: false, + types, + }), + (params) => { + contextInput = params + + return params + }, + { + introspection: true, + errorFormatterFn: errorFormatter, + path: '/graphql', + playground: false, + } + ) +} + +function reqPOST(params: string | { query?: string; variables?: string }): void { + req.method = 'POST' + if (typeof params === 'string') { + ;(req as any).body = { + query: params, + } + } else { + ;(req as any).body = params + } +} From bcb8f3328033aee3ccbe26302247647e1013c115 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 27 Jul 2020 11:52:53 +0200 Subject: [PATCH 06/11] improve types --- src/runtime/schema/schema.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/runtime/schema/schema.ts b/src/runtime/schema/schema.ts index 8752f0d8d..c007326fa 100644 --- a/src/runtime/schema/schema.ts +++ b/src/runtime/schema/schema.ts @@ -40,13 +40,13 @@ export function createLazyState(): LazyState { export interface Request extends HTTP.IncomingMessage { log: NexusLogger.Logger } - export interface Response extends HTTP.ServerResponse {} -export type ContextContributor = (params: { +export type ContextParams = { req: Request res: Response -}) => MaybePromise> +} +export type ContextContributor = (params: ContextParams) => MaybePromise> type MiddlewareFn = ( source: any, From 7b1764088adcb27d4b4df075106c7808444a619d Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 27 Jul 2020 12:01:51 +0200 Subject: [PATCH 07/11] update docs --- .../010-nexus-schema-users.mdx | 2 +- .../011-adoption-guides/020-prisma-users.mdx | 2 +- .../content/040-api/01-nexus/01-schema.mdx | 30 ++++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/website/content/011-adoption-guides/010-nexus-schema-users.mdx b/website/content/011-adoption-guides/010-nexus-schema-users.mdx index cc5c4c91b..444db6961 100644 --- a/website/content/011-adoption-guides/010-nexus-schema-users.mdx +++ b/website/content/011-adoption-guides/010-nexus-schema-users.mdx @@ -94,7 +94,7 @@ Nexus has an API for adding to context. +++ app.ts + import { schema } from 'nexus' -+ schema.addToContext(req => { ++ schema.addToContext(({ req, res }) => { + return { ... } + }) diff --git a/website/content/011-adoption-guides/020-prisma-users.mdx b/website/content/011-adoption-guides/020-prisma-users.mdx index 2545df769..b798309a1 100644 --- a/website/content/011-adoption-guides/020-prisma-users.mdx +++ b/website/content/011-adoption-guides/020-prisma-users.mdx @@ -22,7 +22,7 @@ import { schema } from 'nexus' const db = new PrismaClient() -schema.addToContext(req => ({ db })) // exopse Prisma Client to all resolvers +schema.addToContext(({ req, res }) => ({ db })) // expose Prisma Client to all resolvers schema.queryType({ definition(t) { diff --git a/website/content/040-api/01-nexus/01-schema.mdx b/website/content/040-api/01-nexus/01-schema.mdx index 2cc10b1de..815ab4011 100644 --- a/website/content/040-api/01-nexus/01-schema.mdx +++ b/website/content/040-api/01-nexus/01-schema.mdx @@ -922,12 +922,16 @@ Sugar for creating arguments of type `Int` `String` `Float` `ID` `Boolean`. Add context to your graphql resolver functions. The objects returned by your context contributor callbacks will be shallow-merged into `ctx`. The `ctx` type will also accurately reflect the types you return from callbacks passed to `addToContext`. +The incoming request and server response are passed to the callback in the following shape: `{ req: IncomingMessage, res: ServerResponse }`. See below how to use them. + ### Example +Defining arbitrary values to your GraphQL context + ```ts import { schema } from 'nexus' -schema.addToContext(_req => { +schema.addToContext(({ req, res }) => { return { greeting: 'Howdy!', } @@ -944,6 +948,30 @@ schema.queryType({ }) ``` +Forwarding the incoming request to your GraphQL Context + +```ts +import { schema } from 'nexus' + +schema.addToContext(({ req, res }) => { + return { + req + } +}) + +schema.queryType({ + definition(t) { + t.string('hello', { + resolve(_root, _args, ctx) { + if (ctx.req.headers['authorization']) { + /* ... */ + } + }, + }) + }, +}) +``` + ## `use` Add schema plugins to your app. These plugins represent a subset of what framework plugins ([`app.use`](../../api/nexus/use) can do. This is useful when, for example, a schema plugin you would like to use has not integrated into any framework plugin. You can find a list of schema plugins [here](../../components-standalone/schema/plugins). From 5a1c96b039c888404211e7b60f9ebd701e087546 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 27 Jul 2020 12:03:20 +0200 Subject: [PATCH 08/11] improve js docs --- src/runtime/schema/schema.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/runtime/schema/schema.ts b/src/runtime/schema/schema.ts index c007326fa..215b75b5f 100644 --- a/src/runtime/schema/schema.ts +++ b/src/runtime/schema/schema.ts @@ -43,7 +43,13 @@ export interface Request extends HTTP.IncomingMessage { export interface Response extends HTTP.ServerResponse {} export type ContextParams = { + /** + * Incoming express request + */ req: Request + /** + * Server response + */ res: Response } export type ContextContributor = (params: ContextParams) => MaybePromise> From c0ed9a6e57cb8fb47afe2a88c971238ff526383c Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Mon, 27 Jul 2020 12:03:39 +0200 Subject: [PATCH 09/11] js docs --- src/runtime/schema/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/schema/schema.ts b/src/runtime/schema/schema.ts index 215b75b5f..b47a15e20 100644 --- a/src/runtime/schema/schema.ts +++ b/src/runtime/schema/schema.ts @@ -44,7 +44,7 @@ export interface Response extends HTTP.ServerResponse {} export type ContextParams = { /** - * Incoming express request + * Incoming HTTP request */ req: Request /** From 96d39c5b84c5d18729a137fdde6e5cba76bd280f Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Tue, 28 Jul 2020 10:52:42 +0200 Subject: [PATCH 10/11] rename context types --- src/runtime/app.ts | 2 +- src/runtime/schema/index.ts | 2 +- src/runtime/schema/schema.ts | 12 ++++++------ src/runtime/server/handler-graphql.ts | 4 ++-- src/runtime/server/server.ts | 12 ++++++------ 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/runtime/app.ts b/src/runtime/app.ts index 7e1372d37..64de009e2 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -87,7 +87,7 @@ export type AppState = { schema: NexusSchema.core.NexusGraphQLSchema missingTypes: Index loadedPlugins: RuntimeContributions[] - createContext: Schema.ContextContributor + createContext: Schema.ContextAdder } running: boolean components: { diff --git a/src/runtime/schema/index.ts b/src/runtime/schema/index.ts index 8eefaacf2..5a67e8fa1 100644 --- a/src/runtime/schema/index.ts +++ b/src/runtime/schema/index.ts @@ -1,3 +1,3 @@ -export { ContextContributor, create, LazyState, Schema } from './schema' +export { ContextAdder, create, LazyState, Schema } from './schema' export { SettingsData, SettingsInput } from './settings' diff --git a/src/runtime/schema/schema.ts b/src/runtime/schema/schema.ts index b47a15e20..b3ca19c07 100644 --- a/src/runtime/schema/schema.ts +++ b/src/runtime/schema/schema.ts @@ -17,7 +17,7 @@ import { createSchemaSettingsManager, SchemaSettingsManager } from './settings' import { mapSettingsAndPluginsToNexusSchemaConfig } from './settings-mapper' export type LazyState = { - contextContributors: ContextContributor[] + contextContributors: ContextAdder[] plugins: NexusSchema.core.NexusPlugin[] scalars: Scalars.Scalars } @@ -42,7 +42,7 @@ export interface Request extends HTTP.IncomingMessage { } export interface Response extends HTTP.ServerResponse {} -export type ContextParams = { +export type ContextAdderLens = { /** * Incoming HTTP request */ @@ -52,7 +52,7 @@ export type ContextParams = { */ res: Response } -export type ContextContributor = (params: ContextParams) => MaybePromise> +export type ContextAdder = (params: ContextAdderLens) => MaybePromise> type MiddlewareFn = ( source: any, @@ -77,7 +77,7 @@ export interface Schema extends NexusSchemaStatefulBuilders { /** * todo link to website docs */ - addToContext(contextContributor: ContextContributor): void + addToContext(contextAdder: ContextAdder): void } /** @@ -107,8 +107,8 @@ export function create(state: AppState): SchemaInternal { assertAppNotAssembled(state, 'app.schema.use', 'The Nexus Schema plugin you used will be ignored.') state.components.schema.plugins.push(plugin) }, - addToContext(contextContributor) { - state.components.schema.contextContributors.push(contextContributor) + addToContext(contextAdder) { + state.components.schema.contextContributors.push(contextAdder) }, middleware(fn) { api.use( diff --git a/src/runtime/server/handler-graphql.ts b/src/runtime/server/handler-graphql.ts index 9ef5273a9..86826635d 100644 --- a/src/runtime/server/handler-graphql.ts +++ b/src/runtime/server/handler-graphql.ts @@ -1,5 +1,5 @@ import { GraphQLError, GraphQLFormattedError, GraphQLSchema } from 'graphql' -import { ContextContributor } from '../schema' +import { ContextAdder } from '../schema' import { ApolloServerless } from './apollo-server' import { log } from './logger' import { NexusRequestHandler } from './server' @@ -14,7 +14,7 @@ type Settings = { type CreateHandler = ( schema: GraphQLSchema, - createContext: ContextContributor, + createContext: ContextAdder, settings: Settings ) => NexusRequestHandler diff --git a/src/runtime/server/server.ts b/src/runtime/server/server.ts index 0e13f70ee..40134d94c 100644 --- a/src/runtime/server/server.ts +++ b/src/runtime/server/server.ts @@ -7,7 +7,7 @@ import * as Plugin from '../../lib/plugin' import { httpClose, httpListen, noop } from '../../lib/utils' import { AppState } from '../app' import * as DevMode from '../dev-mode' -import { ContextContributor } from '../schema' +import { ContextAdder } from '../schema' import { assembledGuard } from '../utils' import { ApolloServerExpress } from './apollo-server' import { errorFormatter } from './error-formatter' @@ -45,7 +45,7 @@ export interface Server { interface State { running: boolean httpServer: HTTP.Server - createContext: null | (() => ContextContributor) + createContext: null | (() => ContextAdder) apolloServer: null | ApolloServerExpress } @@ -187,16 +187,16 @@ const wrapHandlerWithErrorHandling = (handler: NexusRequestHandler): NexusReques * Combine all the context contributions defined in the app and in plugins. */ function createContextCreator( - contextContributors: ContextContributor[], + contextContributors: ContextAdder[], plugins: Plugin.RuntimeContributions[] -): ContextContributor { - const createContext: ContextContributor = async (params) => { +): ContextAdder { + const createContext: ContextAdder = async (params) => { let context: Record = {} // Integrate context from plugins for (const plugin of plugins) { if (!plugin.context) continue - const contextContribution = await plugin.context.create(params.req) + const contextContribution = plugin.context.create(params.req) Object.assign(context, contextContribution) } From 1e10b07cf6168e759399546230f3826fc6d623a8 Mon Sep 17 00:00:00 2001 From: Flavian DESVERNE Date: Tue, 28 Jul 2020 18:09:07 +0200 Subject: [PATCH 11/11] add req and res by default --- .../add-to-context-extractor/extractor.spec.ts | 16 ++++++++++++++++ src/lib/add-to-context-extractor/extractor.ts | 10 +++++----- src/lib/add-to-context-extractor/typegen.ts | 14 +++++++++++++- src/runtime/app.ts | 5 +++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/lib/add-to-context-extractor/extractor.spec.ts b/src/lib/add-to-context-extractor/extractor.spec.ts index e59a7bf22..6a7f1bdc4 100644 --- a/src/lib/add-to-context-extractor/extractor.spec.ts +++ b/src/lib/add-to-context-extractor/extractor.spec.ts @@ -2,6 +2,7 @@ import { isLeft } from 'fp-ts/lib/Either' import * as tsm from 'ts-morph' import { normalizePathsInData } from '../../lib/utils' import { extractContextTypes } from './extractor' +import { DEFAULT_CONTEXT_TYPES } from './typegen' describe('syntax cases', () => { it('will extract from import name of nexus default export', () => { @@ -141,6 +142,21 @@ it('extracts from returned object of referenced primitive value', () => { `) }) +it('can access all default context types', () => { + const allTypesExported = DEFAULT_CONTEXT_TYPES.typeImports.every(i => { + const project = new tsm.Project({ + addFilesFromTsConfig: false, + skipFileDependencyResolution: true + }) + + const sourceFile = project.addSourceFileAtPath(i.modulePath + '.d.ts') + + return sourceFile.getExportedDeclarations().has(i.name) + }) + + expect(allTypesExported).toEqual(true) +}) + it('extracts from returned object of referenced object value', () => { expect( extract(` diff --git a/src/lib/add-to-context-extractor/extractor.ts b/src/lib/add-to-context-extractor/extractor.ts index 21585ae5b..149ab9a36 100644 --- a/src/lib/add-to-context-extractor/extractor.ts +++ b/src/lib/add-to-context-extractor/extractor.ts @@ -42,13 +42,13 @@ function contribTypeLiteral(value: string): ContribTypeLiteral { /** * Extract types from all `addToContext` calls. */ -export function extractContextTypes(program: tsm.Project): Either { +export function extractContextTypes( + program: tsm.Project, + defaultTypes: ExtractedContextTypes = { typeImports: [], types: [] } +): Either { const typeImportsIndex: Record = {} - const contextTypeContributions: ExtractedContextTypes = { - typeImports: [], - types: [], - } + const contextTypeContributions: ExtractedContextTypes = defaultTypes const appSourceFiles = findModulesThatImportModule(program, 'nexus') diff --git a/src/lib/add-to-context-extractor/typegen.ts b/src/lib/add-to-context-extractor/typegen.ts index 5b4201a13..ecf3427aa 100644 --- a/src/lib/add-to-context-extractor/typegen.ts +++ b/src/lib/add-to-context-extractor/typegen.ts @@ -18,6 +18,18 @@ export const NEXUS_DEFAULT_RUNTIME_CONTEXT_TYPEGEN_PATH = fs.path( 'index.d.ts' ) +export const DEFAULT_CONTEXT_TYPES: ExtractedContextTypes = { + typeImports: [ + { + name: 'ContextAdderLens', + modulePath: require.resolve('../../../dist/runtime/schema/schema').split('.')[0], + isExported: true, + isNode: false, + }, + ], + types: [{ kind: 'ref', name: 'ContextAdderLens' }], +} + /** * Run the pure extractor and then write results to a typegen module. */ @@ -28,7 +40,7 @@ export async function generateContextExtractionArtifacts( const errProject = createTSProject(layout, { withCache: true }) if (isLeft(errProject)) return errProject const tsProject = errProject.right - const contextTypes = extractContextTypes(tsProject) + const contextTypes = extractContextTypes(tsProject, DEFAULT_CONTEXT_TYPES) if (isLeft(contextTypes)) { return contextTypes diff --git a/src/runtime/app.ts b/src/runtime/app.ts index 64de009e2..3ebb5117a 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -222,6 +222,11 @@ export function create(): App { app.schema.importType(builtinScalars.DateTime, 'date') app.schema.importType(builtinScalars.Json, 'json') + /** + * Add `req` and `res` to the context by default + */ + app.schema.addToContext(params => params) + return { ...app, private: {