Skip to content

Commit 476af07

Browse files
author
Jason Kuhrt
authored
feat(server): add a subscriptions server (#1397)
1 parent 3225a40 commit 476af07

File tree

4 files changed

+135
-33
lines changed

4 files changed

+135
-33
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"prismjs": "^1.20.0",
6262
"prompts": "^2.3.0",
6363
"rxjs": "^6.5.4",
64-
"setset": "^0.0.3",
64+
"setset": "^0.0.4",
6565
"simple-git": "^2.0.0",
6666
"slash": "^3.0.0",
6767
"source-map-support": "^0.5.19",

src/runtime/server/server.ts

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import chalk from 'chalk'
12
import createExpress, { Express } from 'express'
2-
import { GraphQLError, GraphQLSchema } from 'graphql'
3+
import { GraphQLSchema } from 'graphql'
34
import * as HTTP from 'http'
4-
import { HttpError } from 'http-errors'
5+
import { isEmpty } from 'lodash'
56
import * as Net from 'net'
67
import * as Plugin from '../../lib/plugin'
78
import { httpClose, httpListen, noop } from '../../lib/utils'
@@ -47,13 +48,15 @@ interface State {
4748
httpServer: HTTP.Server
4849
createContext: null | (() => ContextAdder)
4950
apolloServer: null | ApolloServerExpress
51+
enableSubscriptionsServer: boolean
5052
}
5153

5254
export const defaultState = {
5355
running: false,
5456
httpServer: HTTP.createServer(),
5557
createContext: null,
5658
apolloServer: null,
59+
enableSubscriptionsServer: false,
5760
}
5861

5962
export function create(appState: AppState) {
@@ -106,9 +109,43 @@ export function create(appState: AppState) {
106109
loadedRuntimePlugins
107110
)
108111

112+
/**
113+
* Resolve if subscriptions are enabled or not
114+
*/
115+
116+
if (settings.metadata.fields.subscriptions.fields.enabled.from === 'change') {
117+
state.enableSubscriptionsServer = settings.data.subscriptions.enabled
118+
/**
119+
* Validate the integration of server subscription settings and the schema subscription type definitions.
120+
*/
121+
if (hasSubscriptionFields(schema)) {
122+
if (!settings.data.subscriptions.enabled) {
123+
log.error(
124+
`You have disabled server subscriptions but your schema has a ${chalk.yellowBright(
125+
'Subscription'
126+
)} type with fields present. When your API clients send subscription operations at runtime they will fail.`
127+
)
128+
}
129+
} else if (settings.data.subscriptions.enabled) {
130+
log.warn(
131+
`You have enabled server subscriptions but your schema has no ${chalk.yellowBright(
132+
'Subscription'
133+
)} type with fields.`
134+
)
135+
}
136+
} else if (hasSubscriptionFields(schema)) {
137+
state.enableSubscriptionsServer = true
138+
}
139+
140+
/**
141+
* Setup Apollo Server
142+
*/
143+
109144
state.apolloServer = new ApolloServerExpress({
110145
schema,
111146
engine: settings.data.apollo.engine.enabled ? settings.data.apollo.engine : false,
147+
// todo expose options
148+
subscriptions: settings.data.subscriptions,
112149
context: createContext,
113150
introspection: settings.data.graphql.introspection,
114151
formatError: errorFormatter,
@@ -127,6 +164,10 @@ export function create(appState: AppState) {
127164
cors: settings.data.cors,
128165
})
129166

167+
if (state.enableSubscriptionsServer) {
168+
state.apolloServer.installSubscriptionHandlers(state.httpServer)
169+
}
170+
130171
return { createContext }
131172
},
132173
async start() {
@@ -143,7 +184,10 @@ export function create(appState: AppState) {
143184
port: address.port,
144185
host: address.address,
145186
ip: address.address,
146-
path: settings.data.path,
187+
paths: {
188+
graphql: settings.data.path,
189+
graphqlSubscrtipions: state.enableSubscriptionsServer ? settings.data.subscriptions.path : null,
190+
},
147191
})
148192
DevMode.sendServerReadySignalToDevModeMaster()
149193
},
@@ -163,27 +207,6 @@ export function create(appState: AppState) {
163207
return internalServer
164208
}
165209

166-
/**
167-
* Log http errors during development.
168-
*/
169-
const wrapHandlerWithErrorHandling = (handler: NexusRequestHandler): NexusRequestHandler => {
170-
return async (req, res) => {
171-
await handler(req, res)
172-
if (res.statusCode !== 200 && (res as any).error) {
173-
const error: HttpError = (res as any).error
174-
const graphqlErrors: GraphQLError[] = error.graphqlErrors
175-
176-
if (graphqlErrors.length > 0) {
177-
graphqlErrors.forEach(errorFormatter)
178-
} else {
179-
log.error(error.message, {
180-
error,
181-
})
182-
}
183-
}
184-
}
185-
}
186-
187210
/**
188211
* Combine all the context contributions defined in the app and in plugins.
189212
*/
@@ -217,3 +240,7 @@ function createContextCreator(
217240

218241
return createContext
219242
}
243+
244+
function hasSubscriptionFields(schema: GraphQLSchema): boolean {
245+
return !isEmpty(schema.getSubscriptionType()?.getFields())
246+
}

src/runtime/server/settings.ts

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SubscriptionServerOptions } from 'apollo-server-core'
12
import { PlaygroundRenderPageOptions } from 'apollo-server-express'
23
import { CorsOptions as OriginalCorsOption } from 'cors'
34
import * as Setset from 'setset'
@@ -37,7 +38,38 @@ export type PlaygroundLonghandInput = {
3738
settings?: Omit<Partial<Exclude<PlaygroundRenderPageOptions['settings'], undefined>>, 'general.betaUpdates'>
3839
}
3940

41+
type SubscriptionsLonghandInput = Omit<SubscriptionServerOptions, 'path'> & {
42+
/**
43+
* The path for clients to send subscriptions to.
44+
*
45+
* @default "/graphql"
46+
*/
47+
path?: string
48+
/**
49+
* Disable or enable the subscriptions server.
50+
*
51+
* @dynamicDefault
52+
*
53+
* - true if there is a Subscription type in your schema
54+
* - false otherwise
55+
*/
56+
enabled?: boolean
57+
}
58+
4059
export type SettingsInput = {
60+
/**
61+
* Configure the subscriptions server.
62+
*
63+
* - Pass true to force enable with setting defaults
64+
* - Pass false to force disable
65+
* - Pass settings to customize config. Note does not imply enabled. Set "enabled: true" for that or rely on default.
66+
*
67+
* @dynamicDefault
68+
*
69+
* - true if there is a Subscription type in your schema
70+
* - false otherwise
71+
*/
72+
subscriptions?: boolean | SubscriptionsLonghandInput
4173
/**
4274
* Port the server should be listening on.
4375
*
@@ -190,7 +222,7 @@ export type SettingsInput = {
190222
* Create a message suitable for printing to the terminal about the server
191223
* having been booted.
192224
*/
193-
startMessage?: (address: { port: number; host: string; ip: string; path: string }) => void
225+
startMessage?: (startInfo: ServerStartInfo) => void
194226
/**
195227
* todo
196228
*/
@@ -199,9 +231,25 @@ export type SettingsInput = {
199231
}
200232
}
201233

202-
export type SettingsData = Setset.InferDataFromInput<Omit<SettingsInput, 'host' | 'cors' | 'apollo'>> & {
234+
type ServerStartInfo = {
235+
port: number
236+
host: string
237+
ip: string
238+
paths: {
239+
graphql: string
240+
graphqlSubscrtipions: null | string
241+
}
242+
}
243+
244+
export type SettingsData = Setset.InferDataFromInput<
245+
Omit<SettingsInput, 'host' | 'cors' | 'apollo' | 'subscriptions'>
246+
> & {
203247
host?: string
204248
cors: ResolvedOptional<SettingsInput['cors']>
249+
subscriptions: Omit<SubscriptionsLonghandInput, 'enabled' | 'path'> & {
250+
enabled: boolean
251+
path: string
252+
}
205253
apollo: {
206254
engine: ApolloConfigEngine & {
207255
enabled: boolean
@@ -212,6 +260,28 @@ export type SettingsData = Setset.InferDataFromInput<Omit<SettingsInput, 'host'
212260
export const createServerSettingsManager = () =>
213261
Setset.create<SettingsInput, SettingsData>({
214262
fields: {
263+
subscriptions: {
264+
shorthand(enabled) {
265+
return { enabled }
266+
},
267+
fields: {
268+
path: {
269+
initial() {
270+
return '/graphql'
271+
},
272+
},
273+
keepAlive: {},
274+
onConnect: {},
275+
onDisconnect: {},
276+
enabled: {
277+
initial() {
278+
// This is not accurate. The default is actually dynamic depending
279+
// on if the user has defined any subscription type or not.
280+
return true
281+
},
282+
},
283+
},
284+
},
215285
apollo: {
216286
fields: {
217287
engine: {
@@ -350,9 +420,14 @@ export const createServerSettingsManager = () =>
350420
},
351421
startMessage: {
352422
initial() {
353-
return ({ port, host, path }): void => {
423+
return ({ port, host, paths }): void => {
424+
const url = `http://${Utils.prettifyHost(host)}:${port}${paths.graphql}`
425+
const subscrtipionsURL = paths.graphqlSubscrtipions
426+
? `http://${Utils.prettifyHost(host)}:${port}${paths.graphqlSubscrtipions}`
427+
: null
354428
serverLogger.info('listening', {
355-
url: `http://${Utils.prettifyHost(host)}:${port}${path}`,
429+
url,
430+
subscrtipionsURL,
356431
})
357432
}
358433
},

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5682,10 +5682,10 @@ [email protected]:
56825682
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
56835683
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
56845684

5685-
setset@^0.0.3:
5686-
version "0.0.3"
5687-
resolved "https://registry.yarnpkg.com/setset/-/setset-0.0.3.tgz#b071ddfeaf257ecb6148aa8a1187eb1ef5360826"
5688-
integrity sha512-rty4d5o1LVjA5Ct4fUAH0MeHfKNZTLxM409j68KLg8zd25Fb9tBAHjB5xeP9mK/qUtwCSH/ScYdpB0vGMo7dhg==
5685+
setset@^0.0.4:
5686+
version "0.0.4"
5687+
resolved "https://registry.yarnpkg.com/setset/-/setset-0.0.4.tgz#93ebc4e091d6435151deea5b200ea238e71b6466"
5688+
integrity sha512-4y8ju0HCfyZybvaLvFzuwF8GWhetQWNQOyx/sclP/bHa0m2zahpXsnszmJSGAS6l/xVfnCD3VJO/eToAGfmAOQ==
56895689
dependencies:
56905690
"@jsdevtools/ono" "^7.1.3"
56915691
"@nexus/logger" "^0.2.0"

0 commit comments

Comments
 (0)