UNPKG

7.46 kBPlain TextView Raw
1import express from 'express';
2import corsMiddleware from 'cors';
3import { json, OptionsJson } from 'body-parser';
4import {
5 renderPlaygroundPage,
6 RenderPageOptions as PlaygroundRenderPageOptions,
7} from '@apollographql/graphql-playground-html';
8import {
9 GraphQLOptions,
10 FileUploadOptions,
11 ApolloServerBase,
12 formatApolloErrors,
13 processFileUploads,
14 ContextFunction,
15 Context,
16 Config,
17} from 'apollo-server-core';
18import { ExecutionParams } from 'subscriptions-transport-ws';
19import accepts from 'accepts';
20import typeis from 'type-is';
21import { graphqlExpress } from './expressApollo';
22
23export { GraphQLOptions, GraphQLExtension } from 'apollo-server-core';
24
25export interface GetMiddlewareOptions {
26 path?: string;
27 cors?: corsMiddleware.CorsOptions | corsMiddleware.CorsOptionsDelegate | boolean;
28 bodyParserConfig?: OptionsJson | boolean;
29 onHealthCheck?: (req: express.Request) => Promise<any>;
30 disableHealthCheck?: boolean;
31}
32
33export interface ServerRegistration extends GetMiddlewareOptions {
34 // Note: You can also pass a connect.Server here. If we changed this field to
35 // `express.Application | connect.Server`, it would be very hard to get the
36 // app.use calls to typecheck even though they do work properly. Our
37 // assumption is that very few people use connect with TypeScript (and in fact
38 // we suspect the only connect users left writing GraphQL apps are Meteor
39 // users).
40 app: express.Application;
41}
42
43const fileUploadMiddleware = (
44 uploadsConfig: FileUploadOptions,
45 server: ApolloServerBase,
46) => (
47 req: express.Request,
48 res: express.Response,
49 next: express.NextFunction,
50) => {
51 // Note: we use typeis directly instead of via req.is for connect support.
52 if (
53 typeof processFileUploads === 'function' &&
54 typeis(req, ['multipart/form-data'])
55 ) {
56 processFileUploads(req, res, uploadsConfig)
57 .then(body => {
58 req.body = body;
59 next();
60 })
61 .catch(error => {
62 if (error.status && error.expose) res.status(error.status);
63
64 next(
65 formatApolloErrors([error], {
66 formatter: server.requestOptions.formatError,
67 debug: server.requestOptions.debug,
68 }),
69 );
70 });
71 } else {
72 next();
73 }
74};
75
76export interface ExpressContext {
77 req: express.Request;
78 res: express.Response;
79 connection?: ExecutionParams;
80}
81
82export interface ApolloServerExpressConfig extends Config {
83 context?: ContextFunction<ExpressContext, Context> | Context;
84}
85
86export class ApolloServer extends ApolloServerBase {
87 constructor(config: ApolloServerExpressConfig) {
88 super(config);
89 }
90
91 // This translates the arguments from the middleware into graphQL options It
92 // provides typings for the integration specific behavior, ideally this would
93 // be propagated with a generic to the super class
94 async createGraphQLServerOptions(
95 req: express.Request,
96 res: express.Response,
97 ): Promise<GraphQLOptions> {
98 return super.graphQLServerOptions({ req, res });
99 }
100
101 protected supportsSubscriptions(): boolean {
102 return true;
103 }
104
105 protected supportsUploads(): boolean {
106 return true;
107 }
108
109 public applyMiddleware({ app, ...rest }: ServerRegistration) {
110 app.use(this.getMiddleware(rest));
111 }
112
113 // TODO: While `express` is not Promise-aware, this should become `async` in
114 // a major release in order to align the API with other integrations (e.g.
115 // Hapi) which must be `async`.
116 public getMiddleware({
117 path,
118 cors,
119 bodyParserConfig,
120 disableHealthCheck,
121 onHealthCheck,
122 }: GetMiddlewareOptions = {}): express.Router {
123 if (!path) path = '/graphql';
124
125 const router = express.Router();
126
127 // Despite the fact that this `applyMiddleware` function is `async` in
128 // other integrations (e.g. Hapi), currently it is not for Express (@here).
129 // That should change in a future version, but that would be a breaking
130 // change right now (see comment above this method's declaration above).
131 //
132 // That said, we do need to await the `willStart` lifecycle event which
133 // can perform work prior to serving a request. Since Express doesn't
134 // natively support Promises yet, we'll do this via a middleware that
135 // calls `next` when the `willStart` finishes. We'll kick off the
136 // `willStart` right away, so hopefully it'll finish before the first
137 // request comes in, but we won't call `next` on this middleware until it
138 // does. (And we'll take care to surface any errors via the `.catch`-able.)
139 const promiseWillStart = this.willStart();
140
141 router.use(path, (_req, _res, next) => {
142 promiseWillStart.then(() => next()).catch(next);
143 });
144
145 if (!disableHealthCheck) {
146 router.use('/.well-known/apollo/server-health', (req, res) => {
147 // Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
148 res.type('application/health+json');
149
150 if (onHealthCheck) {
151 onHealthCheck(req)
152 .then(() => {
153 res.json({ status: 'pass' });
154 })
155 .catch(() => {
156 res.status(503).json({ status: 'fail' });
157 });
158 } else {
159 res.json({ status: 'pass' });
160 }
161 });
162 }
163
164 let uploadsMiddleware;
165 if (this.uploadsConfig && typeof processFileUploads === 'function') {
166 uploadsMiddleware = fileUploadMiddleware(this.uploadsConfig, this);
167 }
168
169 // XXX multiple paths?
170 this.graphqlPath = path;
171
172 // Note that we don't just pass all of these handlers to a single app.use call
173 // for 'connect' compatibility.
174 if (cors === true) {
175 router.use(path, corsMiddleware());
176 } else if (cors !== false) {
177 router.use(path, corsMiddleware(cors));
178 }
179
180 if (bodyParserConfig === true) {
181 router.use(path, json());
182 } else if (bodyParserConfig !== false) {
183 router.use(path, json(bodyParserConfig));
184 }
185
186 if (uploadsMiddleware) {
187 router.use(path, uploadsMiddleware);
188 }
189
190 // Note: if you enable playground in production and expect to be able to see your
191 // schema, you'll need to manually specify `introspection: true` in the
192 // ApolloServer constructor; by default, the introspection query is only
193 // enabled in dev.
194 router.use(path, (req, res, next) => {
195 if (this.playgroundOptions && req.method === 'GET') {
196 // perform more expensive content-type check only if necessary
197 // XXX We could potentially move this logic into the GuiOptions lambda,
198 // but I don't think it needs any overriding
199 const accept = accepts(req);
200 const types = accept.types() as string[];
201 const prefersHTML =
202 types.find(
203 (x: string) => x === 'text/html' || x === 'application/json',
204 ) === 'text/html';
205
206 if (prefersHTML) {
207 const playgroundRenderPageOptions: PlaygroundRenderPageOptions = {
208 endpoint: req.originalUrl,
209 subscriptionEndpoint: this.subscriptionsPath,
210 ...this.playgroundOptions,
211 };
212 res.setHeader('Content-Type', 'text/html');
213 const playground = renderPlaygroundPage(playgroundRenderPageOptions);
214 res.write(playground);
215 res.end();
216 return;
217 }
218 }
219
220 return graphqlExpress(() => this.createGraphQLServerOptions(req, res))(
221 req,
222 res,
223 next,
224 );
225 });
226
227 return router;
228 }
229}