UNPKG

14.3 kBJavaScriptView Raw
1// @flow
2// @module GQLExpressMiddleware
3
4import { SyntaxTree } from './SyntaxTree'
5import { GQLBase } from './GQLBase'
6import { GQLInterface } from './GQLInterface'
7import { GQLScalar } from './GQLScalar'
8import { typeOf } from 'ne-types'
9import { SchemaUtils } from './SchemaUtils'
10import _, { merge } from 'lodash'
11import { makeExecutableSchema } from 'graphql-tools'
12
13import {
14 parse,
15 print,
16 buildSchema,
17 GraphQLSchema,
18 GraphQLInterfaceType,
19 GraphQLEnumType,
20 GraphQLScalarType
21} from 'graphql'
22
23import bodyParser from 'body-parser'
24import graphqlHTTP from 'express-graphql'
25import EventEmitter from 'events'
26import path from 'path'
27
28/**
29 * A handler that exposes an express middleware function that mounts a
30 * GraphQL I/O endpoint. Typical usage follows:
31 *
32 * ```js
33 * const app = express();
34 * app.use(/.../, new GQLExpressMiddleware([...classes]).middleware);
35 * ```
36 *
37 * @class GQLExpressMiddleware
38 */
39export class GQLExpressMiddleware extends EventEmitter
40{
41 handlers: Array<GQLBase>;
42
43 schema: string;
44
45 cache: Map<any, any> = new Map()
46
47 /**
48 * For now, takes an Array of classes extending from GQLBase. These are
49 * parsed and a combined schema of all their individual schemas is generated
50 * via the use of ASTs. This is passed off to express-graphql.
51 *
52 * @memberof GQLExpressMiddleware
53 * @method ⎆⠀constructor
54 * @constructor
55 *
56 * @param {Array<GQLBase>} handlers an array of GQLBase extended classes
57 */
58 constructor(handlers: Array<GQLBase>) {
59 super()
60
61 this.handlers = handlers
62
63 // Generate and cache the schema SDL/IDL string and ast obj (GraphQLSchema)
64 this.ast
65 }
66
67 /**
68 * The Schema String and Schema AST/GraphQLSchema JavaScript objects are
69 * cached after being processed once. If there is a runtime need to rebuild
70 * these objects, calling `clearCache()` will allow their next usage to
71 * rebuild them dynamically.
72 *
73 * @method clearCache
74 * @memberof GQLExpressMiddleware
75 *
76 * @return {GQLExpressMiddleware} returns this so that it can be inlined; ala
77 * `gqlExpressMiddleware.clearCache().ast`, for example
78 */
79 clearCache(): GQLExpressMiddleware {
80 this.cache.clear()
81 return this
82 }
83
84 /**
85 * The schema property returns the textual Schema as it is generated based
86 * on the various Lattice types, interfaces and enums defined in your
87 * project. The ast property returns the JavaScript AST represenatation of
88 * that schema with all injected modificiations detailed in your classes.
89 */
90 get ast(): GraphQLSchema {
91 let cached: ?GraphQLSchema = this.cache.get('ast')
92
93 if (cached) {
94 return cached
95 }
96
97 let ast: GraphQLSchema = buildSchema(this.schema)
98
99 SchemaUtils.injectAll(ast, this.handlers);
100
101 this.cache.set('ast', ast)
102
103 return ast;
104 }
105
106 /**
107 * Generates the textual schema based on the registered `GQLBase` handlers
108 * this instance represents.
109 *
110 * @method GQLExpressMiddleware#⬇︎⠀schema
111 * @since 2.7.0
112 *
113 * @return {string} a generated schema string based on the handlers that
114 * are registered with this `GQLExpressMiddleware` instance.
115 */
116 get schema(): string {
117 let cached = this.cache.get('schema')
118 let schema
119
120 if (cached) return cached
121
122 schema = SchemaUtils.generateSchemaSDL(this.handlers);
123 this.cache.set('schema', schema)
124
125 return schema
126 }
127
128 async rootValue(
129 requestData: Object,
130 separateByType: boolean = false
131 ): Object {
132 let root = await SchemaUtils.createMergedRoot(
133 this.handlers, requestData, separateByType
134 )
135
136 return root;
137 }
138
139 /**
140 * Using the express-graphql module, it returns an Express 4.x middleware
141 * function.
142 *
143 * @instance
144 * @memberof GQLExpressMiddleware
145 * @method ⬇︎⠀middleware
146 *
147 * @return {Function} a function that expects request, response and next
148 * parameters as all Express middleware functions.
149 */
150 get middleware(): Function {
151 return this.customMiddleware();
152 }
153
154 /**
155 * Using the express-graphql module, it returns an Express 4.x middleware
156 * function. This version however, has graphiql disabled. Otherwise it is
157 * identical to the `middleware` property
158 *
159 * @instance
160 * @memberof GQLExpressMiddleware
161 * @method ⬇︎⠀middlewareWithoutGraphiQL
162 *
163 * @return {Function} a function that expects request, response and next
164 * parameters as all Express middleware functions.
165 */
166 get middlewareWithoutGraphiQL(): Function {
167 return this.customMiddleware({graphiql: false});
168 }
169
170 /**
171 * In order to ensure that Lattice functions receive the request data,
172 * it is important to use the options function feature of both
173 * `express-graphql` and `apollo-server-express`. This function will create
174 * an options function that reflects that schema and Lattice types defined
175 * in your project.
176 *
177 * Should you need to tailor the response before it is sent out, you may
178 * supply a function as a second parameter that takes two parameters and
179 * returns an options object. The patchFn callback signature looks like this
180 *
181 * ```patchFn(options, {req, res, next|gql})```
182 *
183 * When using the reference implementation, additional graphql request info
184 * can be obtained in lieu of the `next()` function so typically found in
185 * Express middleware. Apollo Server simply provides the next function in
186 * this location.
187 *
188 * @param {Object} options any options, to either engine, that make the most
189 * sense
190 * @param {Function} patchFn see above
191 */
192 generateOptions(
193 options: Object = { graphiql: true },
194 patchFn: ?Function = null
195 ): Function {
196 const optsFn = async (req: mixed, res: mixed, gql: mixed) => {
197 let schema = this.ast;
198 let opts = {
199 schema,
200 rootValue: await this.rootValue({req, res, gql}),
201 formatError: error => ({
202 message: error.message,
203 locations: error.locations,
204 stack: error.stack,
205 path: error.path
206 })
207 }
208
209 merge(opts, options);
210 if (patchFn && typeof patchFn === 'function') {
211 merge(
212 opts,
213 (patchFn.bind(this)(opts, {req, res, gql})) || opts
214 );
215 }
216
217 return opts;
218 }
219
220 return optsFn
221 }
222
223 /**
224 * In order to ensure that Lattice functions receive the request data,
225 * it is important to use the options function feature of both
226 * `express-graphql` and `apollo-server-express`. This function will create
227 * an options function that reflects that schema and Lattice types defined
228 * in your project.
229 *
230 * Should you need to tailor the response before it is sent out, you may
231 * supply a function as a second parameter that takes two parameters and
232 * returns an options object. The patchFn callback signature looks like this
233 *
234 * ```patchFn(options, {req, res, next|gql})```
235 *
236 * When using the reference implementation, additional graphql request info
237 * can be obtained in lieu of the `next()` function so typically found in
238 * Express middleware. Apollo Server simply provides the next function in
239 * this location.
240 *
241 * @param {Object} options any options, to either engine, that make the most
242 * sense
243 * @param {Function} patchFn see above
244 */
245 generateApolloOptions(
246 options: Object = {
247 formatError: error => ({
248 message: error.message,
249 locations: error.locations,
250 stack: error.stack,
251 path: error.path
252 }),
253 debug: true
254 },
255 patchFn: ?Function = null
256 ): Function {
257 const optsFn = async (req: mixed, res: mixed) => {
258 let opts = {
259 schema: this.ast,
260 resolvers: await this.rootValue({req, res}, true)
261 }
262
263 opts.schema = makeExecutableSchema({
264 typeDefs: [this.schema],
265 resolvers: opts.resolvers
266 })
267
268 SchemaUtils.injectAll(opts.schema, this.handlers);
269
270 merge(opts, options);
271 if (patchFn && typeof patchFn === 'function') {
272 merge(
273 opts,
274 (patchFn.bind(this)(opts, {req, res})) || opts
275 );
276 }
277
278 return opts;
279 }
280
281 return optsFn
282 }
283
284 apolloMiddleware(
285 apolloFn: Function,
286 apolloOpts: Object = {},
287 patchFn: ?Function = null
288 ): Array<Function> {
289 let opts = this.generateApolloOptions(apolloOpts, patchFn)
290
291 return [
292 bodyParser.json(),
293 bodyParser.text({ type: 'application/graphql' }),
294 (req, res, next) => {
295 if (req.is('application/graphql')) {
296 req.body = { query: req.body };
297 }
298 next();
299 },
300 apolloFn(opts)
301 ]
302 }
303
304 /**
305 * If your needs require you to specify different values to `graphqlHTTP`,
306 * part of the `express-graphql` package, you can use the `customMiddleware`
307 * function to do so.
308 *
309 * The first parameter is an object that should contain valid `graphqlHTTP`
310 * options. See https://github.com/graphql/express-graphql#options for more
311 * details. Validation is NOT performed.
312 *
313 * The second parameter is a function that will be called after any options
314 * have been applied from the first parameter and the rest of the middleware
315 * has been performed. This, if not modified, will be the final options
316 * passed into `graphqlHTTP`. In your callback, it is expected that the
317 * supplied object is to be modified and THEN RETURNED. Whatever is returned
318 * will be used or passed on. If nothing is returned, the options supplied
319 * to the function will be used instead.
320 *
321 * @method ⌾⠀customMiddleware
322 * @memberof GQLExpressMiddleware
323 * @instance
324 *
325 * @param {Object} [graphqlHttpOptions={graphiql: true}] standard set of
326 * `express-graphql` options. See above.
327 * @param {Function} patchFn see above
328
329 * @return {Function} a middleware function compatible with Express
330 */
331 customMiddleware(
332 graphqlHttpOptions: Object = {graphiql: true},
333 patchFn?: Function
334 ): Function {
335 const optsFn = this.generateOptions(graphqlHttpOptions, patchFn)
336 return graphqlHTTP(optsFn)
337 }
338
339 /**
340 * An optional express middleware function that can be mounted to return
341 * a copy of the generated schema string being used by GQLExpressMiddleware.
342 *
343 * @memberof GQLExpressMiddleware
344 * @method schemaMiddleware
345 * @instance
346 *
347 * @type {Function}
348 */
349 get schemaMiddleware(): Function {
350 return (req: Object, res: Object, next: ?Function) => {
351 res.status(200).send(this.schema);
352 }
353 }
354
355 /**
356 * An optional express middleware function that can be mounted to return
357 * the JSON AST representation of the schema string being used by
358 * GQLExpressMiddleware.
359 *
360 * @memberof GQLExpressMiddleware
361 * @method astMiddleware
362 * @instance
363 *
364 * @type {Function}
365 */
366 get astMiddleware(): Function {
367 return (req: Object, res: Object, next: ?Function) => {
368 res.status(200).send('Temporarily disabled in this version')
369
370 // let cachedOutput = this.cache.get('astMiddlewareOutput')
371 // if (cachedOutput) {
372 // res
373 // .status(302)
374 // .set('Content-Type', 'application/json')
375 // .send(cachedOutput)
376 // }
377 // else {
378 // this.rootValue({req, res, next}, true)
379 // .then(resolvers => {
380 // let schema: GraphQLSchema = buildSchema(this.schema)
381
382 // SchemaUtils.injectInterfaceResolvers(schema, this.handlers);
383 // SchemaUtils.injectEnums(schema, this.handlers);
384 // SchemaUtils.injectScalars(schema, this.handlers);
385 // SchemaUtils.injectComments(schema, this.handlers);
386
387 // function killToJSON(obj: any, path = 'obj.') {
388 // for (let key in obj) {
389 // try {
390 // if (key == 'prev' || key == 'next' || key == 'ofType') continue;
391
392 // if (key == 'toJSON') {
393 // let success = delete obj.toJSON
394 // //console.log(`Killing ${path}toJSON...${success ? 'success' : 'failure'}`)
395 // continue
396 // }
397
398 // if (key == 'inspect') {
399 // let success = delete obj.inspect
400 // //console.log(`Killing ${path}inspect...${success ? 'success' : 'failure'}`)
401 // continue
402 // }
403
404 // if (key == 'toString') {
405 // obj.toString = Object.prototype.toString
406 // //console.log(`Replacing ${path}toString with default`)
407 // continue
408 // }
409
410 // if (typeof obj[key] == 'function') {
411 // obj[key] = `[Function ${obj[key].name}]`
412 // continue
413 // }
414
415 // if (typeof obj[key] == 'object') {
416 // obj[key] = killToJSON(obj[key], `${path}${key}.`)
417 // continue
418 // }
419 // }
420 // catch (error) {
421 // continue
422 // }
423 // }
424
425 // return obj
426 // }
427
428 // // $FlowFixMe
429 // schema = killToJSON(schema)
430
431 // // Still do not know why/how they are preventing JSONifying the
432 // // _typeMap keys. So aggravting
433 // for (let typeKey of Object.keys(schema._typeMap)) {
434 // let object = {}
435
436 // // $FlowFixMe
437 // for (let valKey of Object.keys(schema._typeMap[typeKey])) {
438 // // $FlowFixMe
439 // object[valKey] = schema._typeMap[typeKey][valKey]
440 // }
441
442 // // $FlowFixMe
443 // schema._typeMap[typeKey] = object
444 // }
445
446 // let output = JSON.stringify(schema)
447 // this.cache.delete('ast')
448 // this.cache.set('astMiddlewareOutput', output)
449
450 // res
451 // .status(200)
452 // .set('Content-Type', 'application/json')
453 // .send(output)
454 // })
455 // .catch(error => {
456 // console.error(error)
457
458 // res
459 // .status(500)
460 // .json(error)
461 // })
462 // }
463 }
464 }
465}
466
467export default GQLExpressMiddleware;