Skip to content

fix: context params and pass req and res in an object #1295

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/lib/add-to-context-extractor/extractor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(`
Expand Down
10 changes: 5 additions & 5 deletions src/lib/add-to-context-extractor/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ function contribTypeLiteral(value: string): ContribTypeLiteral {
/**
* Extract types from all `addToContext` calls.
*/
export function extractContextTypes(program: tsm.Project): Either<Exception, ExtractedContextTypes> {
export function extractContextTypes(
program: tsm.Project,
defaultTypes: ExtractedContextTypes = { typeImports: [], types: [] }
): Either<Exception, ExtractedContextTypes> {
const typeImportsIndex: Record<string, TypeImportInfo> = {}

const contextTypeContributions: ExtractedContextTypes = {
typeImports: [],
types: [],
}
const contextTypeContributions: ExtractedContextTypes = defaultTypes

const appSourceFiles = findModulesThatImportModule(program, 'nexus')

Expand Down
14 changes: 13 additions & 1 deletion src/lib/add-to-context-extractor/typegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/runtime/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -88,7 +87,7 @@ export type AppState = {
schema: NexusSchema.core.NexusGraphQLSchema
missingTypes: Index<NexusSchema.core.MissingType>
loadedPlugins: RuntimeContributions<any>[]
createContext: ContextCreator
createContext: Schema.ContextAdder
}
running: boolean
components: {
Expand Down Expand Up @@ -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: {
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { create, LazyState, Schema } from './schema'
export { ContextAdder, create, LazyState, Schema } from './schema'
export { SettingsData, SettingsInput } from './settings'

21 changes: 16 additions & 5 deletions src/runtime/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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<Record<string, unknown>>
export type ContextAdderLens = {
/**
* Incoming HTTP request
*/
req: Request
/**
* Server response
*/
res: Response
}
export type ContextAdder = (params: ContextAdderLens) => MaybePromise<Record<string, unknown>>

type MiddlewareFn = (
source: any,
Expand All @@ -66,7 +77,7 @@ export interface Schema extends NexusSchemaStatefulBuilders {
/**
* todo link to website docs
*/
addToContext(contextContributor: ContextContributor): void
addToContext(contextAdder: ContextAdder): void
}

/**
Expand Down Expand Up @@ -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(
Expand Down
71 changes: 71 additions & 0 deletions src/runtime/server/context.spec.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
5 changes: 3 additions & 2 deletions src/runtime/server/handler-graphql.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -13,7 +14,7 @@ type Settings = {

type CreateHandler = (
schema: GraphQLSchema,
createContext: ContextCreator,
createContext: ContextAdder,
settings: Settings
) => NexusRequestHandler

Expand Down
25 changes: 8 additions & 17 deletions src/runtime/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -45,7 +45,7 @@ export interface Server {
interface State {
running: boolean
httpServer: HTTP.Server
createContext: null | (() => ContextCreator<Record<string, any>, Record<string, any>>)
createContext: null | (() => ContextAdder)
apolloServer: null | ApolloServerExpress
}

Expand Down Expand Up @@ -183,37 +183,28 @@ const wrapHandlerWithErrorHandling = (handler: NexusRequestHandler): NexusReques
}
}

type AnonymousRequest = Record<string, any>

type AnonymousContext = Record<string, any>

export type ContextCreator<
Req extends AnonymousRequest = AnonymousRequest,
Context extends AnonymousContext = AnonymousContext
> = (req: Req) => MaybePromise<Context>

/**
* 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<string, any> = {}

// 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)
}

// 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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { ... }
+ })

Expand Down
2 changes: 1 addition & 1 deletion website/content/011-adoption-guides/020-prisma-users.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
30 changes: 29 additions & 1 deletion website/content/040-api/01-nexus/01-schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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!',
}
Expand All @@ -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).
Expand Down