Skip to content

Commit 1df6d23

Browse files
authored
fix: context params and pass req and res in an object (#1295)
`req` and `res` are now also set by default in the GraphQL context. BREAKING CHANGES: - The function passed to `schema.addToContext` (known as a "Context Adder") no longer has its first parameter being the request object. Instead it now receives an object (known as the "Context Adder Lens") _containing_ the request object. In addition, on this lens, the response object can also be found. The lens exposes the request and response objects on properties `req` and `res` respectively. If you feel strongly that they should be `request` and `response` or any other thoughts you have please let us know on the issue tracker! before: ```ts schema.addToContext((req) => { // do something }) ``` after: ```ts schema.addToContext(({ req, res }}) => { // do something }) ```
1 parent d7b67a0 commit 1df6d23

File tree

12 files changed

+171
-36
lines changed

12 files changed

+171
-36
lines changed

src/lib/add-to-context-extractor/extractor.spec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { isLeft } from 'fp-ts/lib/Either'
22
import * as tsm from 'ts-morph'
33
import { normalizePathsInData } from '../../lib/utils'
44
import { extractContextTypes } from './extractor'
5+
import { DEFAULT_CONTEXT_TYPES } from './typegen'
56

67
describe('syntax cases', () => {
78
it('will extract from import name of nexus default export', () => {
@@ -141,6 +142,21 @@ it('extracts from returned object of referenced primitive value', () => {
141142
`)
142143
})
143144

145+
it('can access all default context types', () => {
146+
const allTypesExported = DEFAULT_CONTEXT_TYPES.typeImports.every(i => {
147+
const project = new tsm.Project({
148+
addFilesFromTsConfig: false,
149+
skipFileDependencyResolution: true
150+
})
151+
152+
const sourceFile = project.addSourceFileAtPath(i.modulePath + '.d.ts')
153+
154+
return sourceFile.getExportedDeclarations().has(i.name)
155+
})
156+
157+
expect(allTypesExported).toEqual(true)
158+
})
159+
144160
it('extracts from returned object of referenced object value', () => {
145161
expect(
146162
extract(`

src/lib/add-to-context-extractor/extractor.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ function contribTypeLiteral(value: string): ContribTypeLiteral {
4242
/**
4343
* Extract types from all `addToContext` calls.
4444
*/
45-
export function extractContextTypes(program: tsm.Project): Either<Exception, ExtractedContextTypes> {
45+
export function extractContextTypes(
46+
program: tsm.Project,
47+
defaultTypes: ExtractedContextTypes = { typeImports: [], types: [] }
48+
): Either<Exception, ExtractedContextTypes> {
4649
const typeImportsIndex: Record<string, TypeImportInfo> = {}
4750

48-
const contextTypeContributions: ExtractedContextTypes = {
49-
typeImports: [],
50-
types: [],
51-
}
51+
const contextTypeContributions: ExtractedContextTypes = defaultTypes
5252

5353
const appSourceFiles = findModulesThatImportModule(program, 'nexus')
5454

src/lib/add-to-context-extractor/typegen.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ export const NEXUS_DEFAULT_RUNTIME_CONTEXT_TYPEGEN_PATH = fs.path(
1818
'index.d.ts'
1919
)
2020

21+
export const DEFAULT_CONTEXT_TYPES: ExtractedContextTypes = {
22+
typeImports: [
23+
{
24+
name: 'ContextAdderLens',
25+
modulePath: require.resolve('../../../dist/runtime/schema/schema').split('.')[0],
26+
isExported: true,
27+
isNode: false,
28+
},
29+
],
30+
types: [{ kind: 'ref', name: 'ContextAdderLens' }],
31+
}
32+
2133
/**
2234
* Run the pure extractor and then write results to a typegen module.
2335
*/
@@ -28,7 +40,7 @@ export async function generateContextExtractionArtifacts(
2840
const errProject = createTSProject(layout, { withCache: true })
2941
if (isLeft(errProject)) return errProject
3042
const tsProject = errProject.right
31-
const contextTypes = extractContextTypes(tsProject)
43+
const contextTypes = extractContextTypes(tsProject, DEFAULT_CONTEXT_TYPES)
3244

3345
if (isLeft(contextTypes)) {
3446
return contextTypes

src/runtime/app.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { Index } from '../lib/utils'
99
import * as Lifecycle from './lifecycle'
1010
import * as Schema from './schema'
1111
import * as Server from './server'
12-
import { ContextCreator } from './server/server'
1312
import * as Settings from './settings'
1413
import { assertAppNotAssembled } from './utils'
1514

@@ -88,7 +87,7 @@ export type AppState = {
8887
schema: NexusSchema.core.NexusGraphQLSchema
8988
missingTypes: Index<NexusSchema.core.MissingType>
9089
loadedPlugins: RuntimeContributions<any>[]
91-
createContext: ContextCreator
90+
createContext: Schema.ContextAdder
9291
}
9392
running: boolean
9493
components: {
@@ -223,6 +222,11 @@ export function create(): App {
223222
app.schema.importType(builtinScalars.DateTime, 'date')
224223
app.schema.importType(builtinScalars.Json, 'json')
225224

225+
/**
226+
* Add `req` and `res` to the context by default
227+
*/
228+
app.schema.addToContext(params => params)
229+
226230
return {
227231
...app,
228232
private: {

src/runtime/schema/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
export { create, LazyState, Schema } from './schema'
1+
export { ContextAdder, create, LazyState, Schema } from './schema'
22
export { SettingsData, SettingsInput } from './settings'
3+

src/runtime/schema/schema.ts

+16-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { createSchemaSettingsManager, SchemaSettingsManager } from './settings'
1717
import { mapSettingsAndPluginsToNexusSchemaConfig } from './settings-mapper'
1818

1919
export type LazyState = {
20-
contextContributors: ContextContributor[]
20+
contextContributors: ContextAdder[]
2121
plugins: NexusSchema.core.NexusPlugin[]
2222
scalars: Scalars.Scalars
2323
}
@@ -40,8 +40,19 @@ export function createLazyState(): LazyState {
4040
export interface Request extends HTTP.IncomingMessage {
4141
log: NexusLogger.Logger
4242
}
43+
export interface Response extends HTTP.ServerResponse {}
4344

44-
export type ContextContributor = (req: Request) => MaybePromise<Record<string, unknown>>
45+
export type ContextAdderLens = {
46+
/**
47+
* Incoming HTTP request
48+
*/
49+
req: Request
50+
/**
51+
* Server response
52+
*/
53+
res: Response
54+
}
55+
export type ContextAdder = (params: ContextAdderLens) => MaybePromise<Record<string, unknown>>
4556

4657
type MiddlewareFn = (
4758
source: any,
@@ -66,7 +77,7 @@ export interface Schema extends NexusSchemaStatefulBuilders {
6677
/**
6778
* todo link to website docs
6879
*/
69-
addToContext(contextContributor: ContextContributor): void
80+
addToContext(contextAdder: ContextAdder): void
7081
}
7182

7283
/**
@@ -96,8 +107,8 @@ export function create(state: AppState): SchemaInternal {
96107
assertAppNotAssembled(state, 'app.schema.use', 'The Nexus Schema plugin you used will be ignored.')
97108
state.components.schema.plugins.push(plugin)
98109
},
99-
addToContext(contextContributor) {
100-
state.components.schema.contextContributors.push(contextContributor)
110+
addToContext(contextAdder) {
111+
state.components.schema.contextContributors.push(contextAdder)
101112
},
102113
middleware(fn) {
103114
api.use(

src/runtime/server/context.spec.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { makeSchema, queryType } from '@nexus/schema'
2+
import { IncomingMessage, ServerResponse } from 'http'
3+
import { Socket } from 'net'
4+
import { createRequestHandlerGraphQL } from './handler-graphql'
5+
import { NexusRequestHandler } from './server'
6+
import { errorFormatter } from './error-formatter'
7+
8+
let handler: NexusRequestHandler
9+
let socket: Socket
10+
let req: IncomingMessage
11+
let res: ServerResponse
12+
let contextInput: any
13+
14+
beforeEach(() => {
15+
// todo actually use req body etc.
16+
contextInput = null
17+
socket = new Socket()
18+
req = new IncomingMessage(socket)
19+
res = new ServerResponse(req)
20+
createHandler(
21+
queryType({
22+
definition(t) {
23+
t.boolean('foo', () => false)
24+
},
25+
})
26+
)
27+
})
28+
29+
it('passes the request and response to the schema context', async () => {
30+
reqPOST(`{ foo }`)
31+
32+
await handler(req, res)
33+
34+
expect(contextInput.req).toBeInstanceOf(IncomingMessage)
35+
expect(contextInput.res).toBeInstanceOf(ServerResponse)
36+
})
37+
38+
/**
39+
* helpers
40+
*/
41+
42+
function createHandler(...types: any) {
43+
handler = createRequestHandlerGraphQL(
44+
makeSchema({
45+
outputs: false,
46+
types,
47+
}),
48+
(params) => {
49+
contextInput = params
50+
51+
return params
52+
},
53+
{
54+
introspection: true,
55+
errorFormatterFn: errorFormatter,
56+
path: '/graphql',
57+
playground: false,
58+
}
59+
)
60+
}
61+
62+
function reqPOST(params: string | { query?: string; variables?: string }): void {
63+
req.method = 'POST'
64+
if (typeof params === 'string') {
65+
;(req as any).body = {
66+
query: params,
67+
}
68+
} else {
69+
;(req as any).body = params
70+
}
71+
}

src/runtime/server/handler-graphql.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { GraphQLError, GraphQLFormattedError, GraphQLSchema } from 'graphql'
2+
import { ContextAdder } from '../schema'
23
import { ApolloServerless } from './apollo-server'
34
import { log } from './logger'
4-
import { ContextCreator, NexusRequestHandler } from './server'
5+
import { NexusRequestHandler } from './server'
56
import { PlaygroundInput } from './settings'
67

78
type Settings = {
@@ -13,7 +14,7 @@ type Settings = {
1314

1415
type CreateHandler = (
1516
schema: GraphQLSchema,
16-
createContext: ContextCreator,
17+
createContext: ContextAdder,
1718
settings: Settings
1819
) => NexusRequestHandler
1920

src/runtime/server/server.ts

+8-17
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import * as HTTP from 'http'
44
import { HttpError } from 'http-errors'
55
import * as Net from 'net'
66
import * as Plugin from '../../lib/plugin'
7-
import { httpClose, httpListen, MaybePromise, noop } from '../../lib/utils'
7+
import { httpClose, httpListen, noop } from '../../lib/utils'
88
import { AppState } from '../app'
99
import * as DevMode from '../dev-mode'
10-
import { ContextContributor } from '../schema/schema'
10+
import { ContextAdder } from '../schema'
1111
import { assembledGuard } from '../utils'
1212
import { ApolloServerExpress } from './apollo-server'
1313
import { errorFormatter } from './error-formatter'
@@ -45,7 +45,7 @@ export interface Server {
4545
interface State {
4646
running: boolean
4747
httpServer: HTTP.Server
48-
createContext: null | (() => ContextCreator<Record<string, any>, Record<string, any>>)
48+
createContext: null | (() => ContextAdder)
4949
apolloServer: null | ApolloServerExpress
5050
}
5151

@@ -183,37 +183,28 @@ const wrapHandlerWithErrorHandling = (handler: NexusRequestHandler): NexusReques
183183
}
184184
}
185185

186-
type AnonymousRequest = Record<string, any>
187-
188-
type AnonymousContext = Record<string, any>
189-
190-
export type ContextCreator<
191-
Req extends AnonymousRequest = AnonymousRequest,
192-
Context extends AnonymousContext = AnonymousContext
193-
> = (req: Req) => MaybePromise<Context>
194-
195186
/**
196187
* Combine all the context contributions defined in the app and in plugins.
197188
*/
198189
function createContextCreator(
199-
contextContributors: ContextContributor[],
190+
contextContributors: ContextAdder[],
200191
plugins: Plugin.RuntimeContributions[]
201-
): ContextCreator {
202-
const createContext: ContextCreator = async (req) => {
192+
): ContextAdder {
193+
const createContext: ContextAdder = async (params) => {
203194
let context: Record<string, any> = {}
204195

205196
// Integrate context from plugins
206197
for (const plugin of plugins) {
207198
if (!plugin.context) continue
208-
const contextContribution = await plugin.context.create(req)
199+
const contextContribution = plugin.context.create(params.req)
209200

210201
Object.assign(context, contextContribution)
211202
}
212203

213204
// Integrate context from app context api
214205
// TODO good runtime feedback to user if something goes wrong
215206
for (const contextContributor of contextContributors) {
216-
const contextContribution = await contextContributor(req as any)
207+
const contextContribution = await contextContributor(params)
217208

218209
Object.assign(context, contextContribution)
219210
}

website/content/011-adoption-guides/010-nexus-schema-users.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ Nexus has an API for adding to context.
9494
+++ app.ts
9595
+ import { schema } from 'nexus'
9696

97-
+ schema.addToContext(req => {
97+
+ schema.addToContext(({ req, res }) => {
9898
+ return { ... }
9999
+ })
100100

website/content/011-adoption-guides/020-prisma-users.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { schema } from 'nexus'
2222

2323
const db = new PrismaClient()
2424

25-
schema.addToContext(req => ({ db })) // exopse Prisma Client to all resolvers
25+
schema.addToContext(({ req, res }) => ({ db })) // expose Prisma Client to all resolvers
2626

2727
schema.queryType({
2828
definition(t) {

website/content/040-api/01-nexus/01-schema.mdx

+29-1
Original file line numberDiff line numberDiff line change
@@ -922,12 +922,16 @@ Sugar for creating arguments of type `Int` `String` `Float` `ID` `Boolean`.
922922
923923
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`.
924924
925+
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.
926+
925927
### Example
926928
929+
Defining arbitrary values to your GraphQL context
930+
927931
```ts
928932
import { schema } from 'nexus'
929933

930-
schema.addToContext(_req => {
934+
schema.addToContext(({ req, res }) => {
931935
return {
932936
greeting: 'Howdy!',
933937
}
@@ -944,6 +948,30 @@ schema.queryType({
944948
})
945949
```
946950
951+
Forwarding the incoming request to your GraphQL Context
952+
953+
```ts
954+
import { schema } from 'nexus'
955+
956+
schema.addToContext(({ req, res }) => {
957+
return {
958+
req
959+
}
960+
})
961+
962+
schema.queryType({
963+
definition(t) {
964+
t.string('hello', {
965+
resolve(_root, _args, ctx) {
966+
if (ctx.req.headers['authorization']) {
967+
/* ... */
968+
}
969+
},
970+
})
971+
},
972+
})
973+
```
974+
947975
## `use`
948976
949977
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).

0 commit comments

Comments
 (0)