Skip to content

Commit 7cfd27c

Browse files
author
Jason Kuhrt
authored
feat(server): add serverless handlers (#788)
progresses #782 closes #528 #### Implementation Notes Several deps have been added in this PR. The goal is to move quickly and iterate on our server components. None of the deps are large, even [fp-ts](https://bundlephobia.com/[email protected]). fp-ts is exciting. It begins our journey into throw-free code! #### User Notes This feature will remain undocumented while we make progress on more parts around it. This feature exposes request handlers on the server component. ```ts import { server } from 'nexus' server.handlers.graphql server.handlers.playground ``` Users can use these to response to requests in a serverless environment. ```ts import { server } from 'nexus' export default (req, res) => { server.handlers.graphql(req, res) } ```
1 parent eedc779 commit 7cfd27c

20 files changed

+773
-488
lines changed

docs/_sidebar.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
- [Concepts](guides/concepts)
1111
- [Schema](guides/schema)
12+
- [Serverless](guides/serverless)
1213
- [Testing](guides/testing)
1314
- [Project Layout](guides/project-layout)
1415
- [Error Handling](guides/error-handling)

docs/guides/serverless.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
- Nexus has experimental support for serverless deployments.
2+
- Support for serverless is being tracked in [#782](https://github.com./graphql-nexus/nexus/issues/782).
3+
- Serverless features are not yet documented in the API docs.
4+
- The server component of Nexus exposes HTTP request handlers.
5+
6+
```ts
7+
import { server } from 'nexus'
8+
9+
server.handlers.graphql // call with (req, res)
10+
server.handlers.playground // call with (req, res)
11+
```
12+
13+
- Use these to handle to requests in your serverless environment.
14+
15+
```ts
16+
import { server } from 'nexus'
17+
18+
export default (req, res) => {
19+
server.handlers.graphql(req, res)
20+
}
21+
```
22+
23+
- See the [Next.JS example](https://github.com./graphql-nexus/examples/tree/master/integration-nextjs) for a functioning serverless reference.

package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@
3737
"chokidar": "^3.3.0",
3838
"codename": "^0.0.6",
3939
"common-tags": "^1.8.0",
40+
"content-type": "^1.0.4",
4041
"dotenv": "^8.2.0",
4142
"express": "^4.17.1",
42-
"express-graphql": "^0.9.0",
43+
"fp-ts": "^2.5.4",
4344
"fs-jetpack": "^2.2.3",
4445
"get-port": "^5.1.0",
4546
"graphql": "^14.5.8",
47+
"http-errors": "^1.7.3",
4648
"lodash": "^4.17.15",
4749
"node-fetch": "^2.6.0",
4850
"pino": "^5.15.0",
@@ -62,6 +64,8 @@
6264
"@prisma-labs/prettier-config": "0.1.0",
6365
"@types/anymatch": "1.3.1",
6466
"@types/common-tags": "1.8.0",
67+
"@types/content-type": "^1.1.3",
68+
"@types/http-errors": "^1.6.3",
6569
"@types/jest": "25.2.1",
6670
"@types/lodash": "4.14.150",
6771
"@types/node": "13.13.4",

src/lib/cli/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { HelpError, unknownCommand } from './help'
2-
export { Command, CommandNode, Index as Dictionary } from './types'
3-
export { arg, format, isError, generateHelpForCommandIndex, generateHelpForCommand } from './helpers'
41
export { CLI } from './cli'
2+
export { HelpError, unknownCommand } from './help'
3+
export { arg, format, generateHelpForCommand, generateHelpForCommandIndex, isError } from './helpers'
4+
export { Command, CommandNode } from './types'

src/lib/cli/types.ts

-4
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,3 @@ export type ConcreteCommand = {
7070
type: 'concrete_command'
7171
value: Command
7272
}
73-
74-
export type Index<T> = {
75-
[key: string]: T
76-
}

src/lib/reflection/fork-script.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ async function run() {
3838
if (isReflectionStage('typegen')) {
3939
try {
4040
await writeArtifacts({
41-
graphqlSchema: app.private.state.schemaComponent.schema!,
41+
graphqlSchema: app.private.state.assembled!.schema,
4242
layout,
4343
schemaSettings: app.settings.current.schema,
4444
plugins: Plugin.importAndLoadRuntimePlugins(app.private.state.plugins),

src/lib/utils/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,7 @@ export function simpleDebounce<T extends (...args: any[]) => Promise<any>>(fn: T
293293
executing = false
294294
}) as any
295295
}
296+
297+
export type Index<T> = {
298+
[key: string]: T
299+
}

src/runtime/app.ts

+36-21
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import * as Logger from '@nexus/logger'
2-
import { stripIndent } from 'common-tags'
2+
import { MissingType, NexusGraphQLSchema } from '@nexus/schema/dist/core'
33
import * as Plugin from '../lib/plugin'
4+
import { RuntimeContributions } from '../lib/plugin'
45
import * as Reflection from '../lib/reflection'
6+
import { Index } from '../lib/utils'
57
import * as Schema from './schema'
68
import * as Server from './server'
9+
import { ContextCreator } from './server/server'
710
import * as Settings from './settings'
11+
import { assertAppNotAssembled } from './utils'
812

913
const log = Logger.create({ name: 'app' })
1014

@@ -54,15 +58,22 @@ export interface App {
5458
*/
5559
}
5660

61+
type Mutable<T> = { -readonly [P in keyof T]: T[P] extends ReadonlyArray<infer U> ? U[] : T[P] }
62+
5763
export type AppState = {
5864
plugins: Plugin.Plugin[]
59-
// schema: () => NexusSchema.core.NexusGraphQLSchema
6065
/**
6166
* Once the app is started incremental component APIs can no longer be used. This
6267
* flag let's those APIs detect that they are being used after app start. Then
6368
* they can do something useful like tell the user about their mistake.
6469
*/
65-
assembled: boolean
70+
assembled: null | {
71+
settings: Settings.SettingsData
72+
schema: NexusGraphQLSchema
73+
missingTypes: Index<MissingType>
74+
loadedPlugins: RuntimeContributions<any>[]
75+
createContext: ContextCreator
76+
}
6677
running: boolean
6778
schemaComponent: Schema.LazyState
6879
}
@@ -80,7 +91,7 @@ export type PrivateApp = App & {
8091
*/
8192
export function createAppState(): AppState {
8293
const appState = {
83-
assembled: false,
94+
assembled: null,
8495
running: false,
8596
plugins: [],
8697
} as Omit<AppState, 'schemaComponent'>
@@ -95,33 +106,40 @@ export function create(): App {
95106
const appState = createAppState()
96107
const serverComponent = Server.create(appState)
97108
const schemaComponent = Schema.create(appState)
98-
const settingsComponent = Settings.create({
109+
const settingsComponent = Settings.create(appState, {
99110
serverSettings: serverComponent.private.settings,
100111
schemaSettings: schemaComponent.private.settings,
101112
log,
102113
})
103-
const api: PrivateApp = {
114+
const api: App = {
104115
log: log,
105116
settings: settingsComponent.public,
106117
schema: schemaComponent.public,
107118
server: serverComponent.public,
108-
// todo call this in the start module
109119
assemble() {
110120
if (appState.assembled) return
111121

112-
appState.assembled = true
122+
// todo https://github.com./graphql-nexus/nexus/pull/788#discussion_r420645846
123+
appState.assembled = {} as Partial<AppState['assembled']>
113124

114125
if (Reflection.isReflectionStage('plugin')) return
115126

116-
const loadedRuntimePlugins = Plugin.importAndLoadRuntimePlugins(appState.plugins)
127+
const loadedPlugins = Plugin.importAndLoadRuntimePlugins(appState.plugins)
128+
appState.assembled!.loadedPlugins = loadedPlugins
117129

118-
schemaComponent.private.assemble(loadedRuntimePlugins)
130+
const { schema, missingTypes } = schemaComponent.private.assemble(loadedPlugins)
131+
appState.assembled!.schema = schema
132+
appState.assembled!.missingTypes = missingTypes
119133

120134
if (Reflection.isReflectionStage('typegen')) return
121135

122-
schemaComponent.private.checks()
136+
const { createContext } = serverComponent.private.assemble(loadedPlugins, schema)
137+
appState.assembled!.createContext = createContext
123138

124-
serverComponent.private.assemble(loadedRuntimePlugins, appState.schemaComponent.schema!)
139+
const { settings } = settingsComponent.private.assemble()
140+
appState.assembled!.settings = settings
141+
142+
schemaComponent.private.checks()
125143
},
126144
async start() {
127145
if (Reflection.isReflection()) return
@@ -136,18 +154,15 @@ export function create(): App {
136154
appState.running = false
137155
},
138156
use(plugin) {
139-
if (appState.assembled === true) {
140-
log.warn(stripIndent`
141-
A plugin was ignored because it was loaded after the server was started
142-
Make sure to call \`use\` before you call \`server.start\`
143-
`)
144-
}
157+
assertAppNotAssembled(appState, 'app.use', 'The plugin you attempted to use will be ignored')
145158
appState.plugins.push(plugin)
146159
},
160+
}
161+
162+
return {
163+
...api,
147164
private: {
148165
state: appState,
149166
},
150-
}
151-
152-
return api
167+
} as App
153168
}

src/runtime/schema/schema.ts

+40-65
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import * as Logger from '@nexus/logger'
22
import * as NexusSchema from '@nexus/schema'
3-
import { CreateFieldResolverInfo, makeSchemaInternal } from '@nexus/schema/dist/core'
4-
import { stripIndent } from 'common-tags'
3+
import {
4+
CreateFieldResolverInfo,
5+
makeSchemaInternal,
6+
MissingType,
7+
NexusGraphQLSchema,
8+
} from '@nexus/schema/dist/core'
59
import { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql'
610
import * as HTTP from 'http'
711
import * as Layout from '../../lib/layout'
812
import { createNexusSchemaStateful, NexusSchemaStatefulBuilders } from '../../lib/nexus-schema-stateful'
913
import { RuntimeContributions } from '../../lib/plugin'
10-
import { MaybePromise } from '../../lib/utils'
14+
import { Index, MaybePromise } from '../../lib/utils'
1115
import { AppState } from '../app'
16+
import { assertAppNotAssembled } from '../utils'
1217
import { log } from './logger'
1318
import {
1419
createSchemaSettingsManager,
@@ -19,28 +24,12 @@ import {
1924
export type LazyState = {
2025
contextContributors: ContextContributor[]
2126
plugins: NexusSchema.core.NexusPlugin[]
22-
/**
23-
* GraphQL schema built by @nexus/schema
24-
*
25-
* @remarks
26-
*
27-
* Only available after assembly
28-
*/
29-
schema: null | NexusSchema.core.NexusGraphQLSchema
30-
/**
31-
* @remarks
32-
*
33-
* Only available after assembly
34-
*/
35-
missingTypes: null | Record<string, NexusSchema.core.MissingType>
3627
}
3728

3829
function createLazyState(): LazyState {
3930
return {
4031
contextContributors: [],
4132
plugins: [],
42-
schema: null,
43-
missingTypes: null,
4433
}
4534
}
4635

@@ -84,7 +73,9 @@ export interface SchemaInternal {
8473
private: {
8574
settings: SchemaSettingsManager
8675
checks(): void
87-
assemble(plugins: RuntimeContributions[]): void
76+
assemble(
77+
plugins: RuntimeContributions[]
78+
): { schema: NexusGraphQLSchema; missingTypes: Index<MissingType> }
8879
}
8980
public: Schema
9081
}
@@ -94,61 +85,45 @@ export function create(appState: AppState): SchemaInternal {
9485
const statefulNexusSchema = createNexusSchemaStateful()
9586
const settings = createSchemaSettingsManager()
9687

97-
const middleware: SchemaInternal['public']['middleware'] = (fn) => {
98-
api.public.use(
99-
NexusSchema.plugin({
100-
// TODO: Do we need to expose the name property?
101-
name: 'local-middleware',
102-
onCreateFieldResolver(config) {
103-
return fn(config)
104-
},
105-
})
106-
)
88+
const api: Schema = {
89+
...statefulNexusSchema.builders,
90+
use(plugin) {
91+
assertAppNotAssembled(appState, 'app.schema.use', 'The Nexus Schema plugin you used will be ignored.')
92+
appState.schemaComponent.plugins.push(plugin)
93+
},
94+
addToContext(contextContributor) {
95+
appState.schemaComponent.contextContributors.push(contextContributor)
96+
},
97+
middleware(fn) {
98+
api.use(
99+
NexusSchema.plugin({
100+
// TODO: Do we need to expose the name property?
101+
name: 'local-middleware',
102+
onCreateFieldResolver(config) {
103+
return fn(config)
104+
},
105+
})
106+
)
107+
},
107108
}
108109

109-
const api: SchemaInternal = {
110-
public: {
111-
...statefulNexusSchema.builders,
112-
use(plugin) {
113-
if (appState.assembled === true) {
114-
log.warn(stripIndent`
115-
A Nexus Schema plugin was ignored because it was loaded after the server was started
116-
Make sure to call \`schema.use\` before you call \`server.start\`
117-
`)
118-
}
119-
120-
appState.schemaComponent.plugins.push(plugin)
121-
},
122-
addToContext(contextContributor) {
123-
appState.schemaComponent.contextContributors.push(contextContributor)
124-
},
125-
middleware,
126-
},
110+
return {
111+
public: api,
127112
private: {
128113
settings: settings,
129-
checks() {
130-
NexusSchema.core.assertNoMissingTypes(
131-
appState.schemaComponent.schema!,
132-
appState.schemaComponent.missingTypes!
133-
)
134-
135-
if (statefulNexusSchema.state.types.length === 0) {
136-
log.warn(Layout.schema.emptyExceptionMessage())
137-
}
138-
},
139114
assemble: (plugins) => {
140115
const nexusSchemaConfig = mapSettingsToNexusSchemaConfig(plugins, settings.data)
141-
142116
nexusSchemaConfig.types.push(...statefulNexusSchema.state.types)
143117
nexusSchemaConfig.plugins!.push(...appState.schemaComponent.plugins)
144-
145118
const { schema, missingTypes } = makeSchemaInternal(nexusSchemaConfig)
146-
147-
appState.schemaComponent.schema = schema
148-
appState.schemaComponent.missingTypes = missingTypes
119+
return { schema, missingTypes }
120+
},
121+
checks() {
122+
NexusSchema.core.assertNoMissingTypes(appState.assembled!.schema, appState.assembled!.missingTypes)
123+
if (statefulNexusSchema.state.types.length === 0) {
124+
log.warn(Layout.schema.emptyExceptionMessage())
125+
}
149126
},
150127
},
151128
}
152-
153-
return api
154129
}

0 commit comments

Comments
 (0)