1 | // @flow
|
2 | // @module GQLExpressMiddleware
|
3 |
|
4 | import { SyntaxTree } from './SyntaxTree'
|
5 | import { GQLBase } from './GQLBase'
|
6 | import { GQLInterface } from './GQLInterface'
|
7 | import { GQLScalar } from './GQLScalar'
|
8 | import { typeOf } from 'ne-types'
|
9 | import { SchemaUtils } from './SchemaUtils'
|
10 | import _, { merge } from 'lodash'
|
11 | import { makeExecutableSchema } from 'graphql-tools'
|
12 |
|
13 | import {
|
14 | parse,
|
15 | print,
|
16 | buildSchema,
|
17 | GraphQLSchema,
|
18 | GraphQLInterfaceType,
|
19 | GraphQLEnumType,
|
20 | GraphQLScalarType
|
21 | } from 'graphql'
|
22 |
|
23 | import bodyParser from 'body-parser'
|
24 | import graphqlHTTP from 'express-graphql'
|
25 | import EventEmitter from 'events'
|
26 | import 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 | */
|
39 | export 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 |
|
467 | export default GQLExpressMiddleware;
|