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 4dd8fd7b3..3ebb5117a 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.ContextAdder } running: boolean components: { @@ -223,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: { diff --git a/src/runtime/schema/index.ts b/src/runtime/schema/index.ts index b05b75afd..5a67e8fa1 100644 --- a/src/runtime/schema/index.ts +++ b/src/runtime/schema/index.ts @@ -1,2 +1,3 @@ -export { 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 1964a8a27..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 } @@ -40,8 +40,19 @@ export function createLazyState(): LazyState { export interface Request extends HTTP.IncomingMessage { log: NexusLogger.Logger } +export interface Response extends HTTP.ServerResponse {} -export type ContextContributor = (req: Request) => MaybePromise> +export type ContextAdderLens = { + /** + * Incoming HTTP request + */ + req: Request + /** + * Server response + */ + res: Response +} +export type ContextAdder = (params: ContextAdderLens) => MaybePromise> type MiddlewareFn = ( source: any, @@ -66,7 +77,7 @@ export interface Schema extends NexusSchemaStatefulBuilders { /** * todo link to website docs */ - addToContext(contextContributor: ContextContributor): void + addToContext(contextAdder: ContextAdder): void } /** @@ -96,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/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 + } +} diff --git a/src/runtime/server/handler-graphql.ts b/src/runtime/server/handler-graphql.ts index 5ea8c43b4..86826635d 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 { ContextAdder } from '../schema' import { ApolloServerless } from './apollo-server' import { log } from './logger' -import { ContextCreator, NexusRequestHandler } from './server' +import { NexusRequestHandler } from './server' import { PlaygroundInput } from './settings' type Settings = { @@ -13,7 +14,7 @@ type Settings = { type CreateHandler = ( schema: GraphQLSchema, - createContext: ContextCreator, + createContext: ContextAdder, settings: Settings ) => NexusRequestHandler diff --git a/src/runtime/server/server.ts b/src/runtime/server/server.ts index d2d28f075..40134d94c 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 { 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 | (() => ContextCreator, Record>) + createContext: null | (() => ContextAdder) apolloServer: null | ApolloServerExpress } @@ -183,29 +183,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[], + contextContributors: ContextAdder[], plugins: Plugin.RuntimeContributions[] -): ContextCreator { - const createContext: ContextCreator = async (req) => { +): 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(req) + const contextContribution = plugin.context.create(params.req) Object.assign(context, contextContribution) } @@ -213,7 +204,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) } 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).