diff --git a/package.json b/package.json index c131fa489..171529213 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/runtime/server/server.ts b/src/runtime/server/server.ts index cd430dbcd..32c698b40 100644 --- a/src/runtime/server/server.ts +++ b/src/runtime/server/server.ts @@ -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' @@ -47,6 +48,7 @@ interface State { httpServer: HTTP.Server createContext: null | (() => ContextAdder) apolloServer: null | ApolloServerExpress + enableSubscriptionsServer: boolean } export const defaultState = { @@ -54,6 +56,7 @@ export const defaultState = { httpServer: HTTP.createServer(), createContext: null, apolloServer: null, + enableSubscriptionsServer: false, } export function create(appState: AppState) { @@ -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, @@ -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() { @@ -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() }, @@ -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. */ @@ -217,3 +240,7 @@ function createContextCreator( return createContext } + +function hasSubscriptionFields(schema: GraphQLSchema): boolean { + return !isEmpty(schema.getSubscriptionType()?.getFields()) +} diff --git a/src/runtime/server/settings.ts b/src/runtime/server/settings.ts index 31d306c6b..7a2230b60 100644 --- a/src/runtime/server/settings.ts +++ b/src/runtime/server/settings.ts @@ -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' @@ -37,7 +38,38 @@ export type PlaygroundLonghandInput = { settings?: Omit>, 'general.betaUpdates'> } +type SubscriptionsLonghandInput = Omit & { + /** + * 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. * @@ -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 */ @@ -199,9 +231,25 @@ export type SettingsInput = { } } -export type SettingsData = Setset.InferDataFromInput> & { +type ServerStartInfo = { + port: number + host: string + ip: string + paths: { + graphql: string + graphqlSubscrtipions: null | string + } +} + +export type SettingsData = Setset.InferDataFromInput< + Omit +> & { host?: string cors: ResolvedOptional + subscriptions: Omit & { + enabled: boolean + path: string + } apollo: { engine: ApolloConfigEngine & { enabled: boolean @@ -212,6 +260,28 @@ export type SettingsData = Setset.InferDataFromInput Setset.create({ 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: { @@ -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, }) } }, diff --git a/yarn.lock b/yarn.lock index 6316d3078..23f92304a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"