Skip to content

Commit

Permalink
feat(server): add a subscriptions server (#1397)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason Kuhrt authored Sep 6, 2020
1 parent 3225a40 commit 476af07
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 33 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"prismjs": "^1.20.0",
"prompts": "^2.3.0",
"rxjs": "^6.5.4",
"setset": "^0.0.3",
"setset": "^0.0.4",
"simple-git": "^2.0.0",
"slash": "^3.0.0",
"source-map-support": "^0.5.19",
Expand Down
75 changes: 51 additions & 24 deletions src/runtime/server/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import chalk from 'chalk'
import createExpress, { Express } from 'express'
import { GraphQLError, GraphQLSchema } from 'graphql'
import { GraphQLSchema } from 'graphql'
import * as HTTP from 'http'
import { HttpError } from 'http-errors'
import { isEmpty } from 'lodash'
import * as Net from 'net'
import * as Plugin from '../../lib/plugin'
import { httpClose, httpListen, noop } from '../../lib/utils'
Expand Down Expand Up @@ -47,13 +48,15 @@ interface State {
httpServer: HTTP.Server
createContext: null | (() => ContextAdder)
apolloServer: null | ApolloServerExpress
enableSubscriptionsServer: boolean
}

export const defaultState = {
running: false,
httpServer: HTTP.createServer(),
createContext: null,
apolloServer: null,
enableSubscriptionsServer: false,
}

export function create(appState: AppState) {
Expand Down Expand Up @@ -106,9 +109,43 @@ export function create(appState: AppState) {
loadedRuntimePlugins
)

/**
* Resolve if subscriptions are enabled or not
*/

if (settings.metadata.fields.subscriptions.fields.enabled.from === 'change') {
state.enableSubscriptionsServer = settings.data.subscriptions.enabled
/**
* Validate the integration of server subscription settings and the schema subscription type definitions.
*/
if (hasSubscriptionFields(schema)) {
if (!settings.data.subscriptions.enabled) {
log.error(
`You have disabled server subscriptions but your schema has a ${chalk.yellowBright(
'Subscription'
)} type with fields present. When your API clients send subscription operations at runtime they will fail.`
)
}
} else if (settings.data.subscriptions.enabled) {
log.warn(
`You have enabled server subscriptions but your schema has no ${chalk.yellowBright(
'Subscription'
)} type with fields.`
)
}
} else if (hasSubscriptionFields(schema)) {
state.enableSubscriptionsServer = true
}

/**
* Setup Apollo Server
*/

state.apolloServer = new ApolloServerExpress({
schema,
engine: settings.data.apollo.engine.enabled ? settings.data.apollo.engine : false,
// todo expose options
subscriptions: settings.data.subscriptions,
context: createContext,
introspection: settings.data.graphql.introspection,
formatError: errorFormatter,
Expand All @@ -127,6 +164,10 @@ export function create(appState: AppState) {
cors: settings.data.cors,
})

if (state.enableSubscriptionsServer) {
state.apolloServer.installSubscriptionHandlers(state.httpServer)
}

return { createContext }
},
async start() {
Expand All @@ -143,7 +184,10 @@ export function create(appState: AppState) {
port: address.port,
host: address.address,
ip: address.address,
path: settings.data.path,
paths: {
graphql: settings.data.path,
graphqlSubscrtipions: state.enableSubscriptionsServer ? settings.data.subscriptions.path : null,
},
})
DevMode.sendServerReadySignalToDevModeMaster()
},
Expand All @@ -163,27 +207,6 @@ export function create(appState: AppState) {
return internalServer
}

/**
* Log http errors during development.
*/
const wrapHandlerWithErrorHandling = (handler: NexusRequestHandler): NexusRequestHandler => {
return async (req, res) => {
await handler(req, res)
if (res.statusCode !== 200 && (res as any).error) {
const error: HttpError = (res as any).error
const graphqlErrors: GraphQLError[] = error.graphqlErrors

if (graphqlErrors.length > 0) {
graphqlErrors.forEach(errorFormatter)
} else {
log.error(error.message, {
error,
})
}
}
}
}

/**
* Combine all the context contributions defined in the app and in plugins.
*/
Expand Down Expand Up @@ -217,3 +240,7 @@ function createContextCreator(

return createContext
}

function hasSubscriptionFields(schema: GraphQLSchema): boolean {
return !isEmpty(schema.getSubscriptionType()?.getFields())
}
83 changes: 79 additions & 4 deletions src/runtime/server/settings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SubscriptionServerOptions } from 'apollo-server-core'
import { PlaygroundRenderPageOptions } from 'apollo-server-express'
import { CorsOptions as OriginalCorsOption } from 'cors'
import * as Setset from 'setset'
Expand Down Expand Up @@ -37,7 +38,38 @@ export type PlaygroundLonghandInput = {
settings?: Omit<Partial<Exclude<PlaygroundRenderPageOptions['settings'], undefined>>, 'general.betaUpdates'>
}

type SubscriptionsLonghandInput = Omit<SubscriptionServerOptions, 'path'> & {
/**
* The path for clients to send subscriptions to.
*
* @default "/graphql"
*/
path?: string
/**
* Disable or enable the subscriptions server.
*
* @dynamicDefault
*
* - true if there is a Subscription type in your schema
* - false otherwise
*/
enabled?: boolean
}

export type SettingsInput = {
/**
* Configure the subscriptions server.
*
* - Pass true to force enable with setting defaults
* - Pass false to force disable
* - Pass settings to customize config. Note does not imply enabled. Set "enabled: true" for that or rely on default.
*
* @dynamicDefault
*
* - true if there is a Subscription type in your schema
* - false otherwise
*/
subscriptions?: boolean | SubscriptionsLonghandInput
/**
* Port the server should be listening on.
*
Expand Down Expand Up @@ -190,7 +222,7 @@ export type SettingsInput = {
* Create a message suitable for printing to the terminal about the server
* having been booted.
*/
startMessage?: (address: { port: number; host: string; ip: string; path: string }) => void
startMessage?: (startInfo: ServerStartInfo) => void
/**
* todo
*/
Expand All @@ -199,9 +231,25 @@ export type SettingsInput = {
}
}

export type SettingsData = Setset.InferDataFromInput<Omit<SettingsInput, 'host' | 'cors' | 'apollo'>> & {
type ServerStartInfo = {
port: number
host: string
ip: string
paths: {
graphql: string
graphqlSubscrtipions: null | string
}
}

export type SettingsData = Setset.InferDataFromInput<
Omit<SettingsInput, 'host' | 'cors' | 'apollo' | 'subscriptions'>
> & {
host?: string
cors: ResolvedOptional<SettingsInput['cors']>
subscriptions: Omit<SubscriptionsLonghandInput, 'enabled' | 'path'> & {
enabled: boolean
path: string
}
apollo: {
engine: ApolloConfigEngine & {
enabled: boolean
Expand All @@ -212,6 +260,28 @@ export type SettingsData = Setset.InferDataFromInput<Omit<SettingsInput, 'host'
export const createServerSettingsManager = () =>
Setset.create<SettingsInput, SettingsData>({
fields: {
subscriptions: {
shorthand(enabled) {
return { enabled }
},
fields: {
path: {
initial() {
return '/graphql'
},
},
keepAlive: {},
onConnect: {},
onDisconnect: {},
enabled: {
initial() {
// This is not accurate. The default is actually dynamic depending
// on if the user has defined any subscription type or not.
return true
},
},
},
},
apollo: {
fields: {
engine: {
Expand Down Expand Up @@ -350,9 +420,14 @@ export const createServerSettingsManager = () =>
},
startMessage: {
initial() {
return ({ port, host, path }): void => {
return ({ port, host, paths }): void => {
const url = `http://${Utils.prettifyHost(host)}:${port}${paths.graphql}`
const subscrtipionsURL = paths.graphqlSubscrtipions
? `http://${Utils.prettifyHost(host)}:${port}${paths.graphqlSubscrtipions}`
: null
serverLogger.info('listening', {
url: `http://${Utils.prettifyHost(host)}:${port}${path}`,
url,
subscrtipionsURL,
})
}
},
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5682,10 +5682,10 @@ setprototypeof@1.1.1:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==

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

0 comments on commit 476af07

Please sign in to comment.