UNPKG

@nyteshade/lattice-legacy

Version:

OO Underpinnings for ease of GraphQL Implementation

468 lines (410 loc) 14.3 kB
// @flow // @module GQLExpressMiddleware import { SyntaxTree } from './SyntaxTree' import { GQLBase } from './GQLBase' import { GQLInterface } from './GQLInterface' import { GQLScalar } from './GQLScalar' import { typeOf } from 'ne-types' import { SchemaUtils } from './SchemaUtils' import _, { merge } from 'lodash' import { makeExecutableSchema } from 'graphql-tools' import { parse, print, buildSchema, GraphQLSchema, GraphQLInterfaceType, GraphQLEnumType, GraphQLScalarType } from 'graphql' import bodyParser from 'body-parser' import graphqlHTTP from 'express-graphql' import EventEmitter from 'events' import path from 'path' /** * A handler that exposes an express middleware function that mounts a * GraphQL I/O endpoint. Typical usage follows: * * ```js * const app = express(); * app.use(/.../, new GQLExpressMiddleware([...classes]).middleware); * ``` * * @class GQLExpressMiddleware */ export class GQLExpressMiddleware extends EventEmitter { handlers: Array<GQLBase>; schema: string; cache: Map<any, any> = new Map() /** * For now, takes an Array of classes extending from GQLBase. These are * parsed and a combined schema of all their individual schemas is generated * via the use of ASTs. This is passed off to express-graphql. * * @memberof GQLExpressMiddleware * @method ⎆⠀constructor * @constructor * * @param {Array<GQLBase>} handlers an array of GQLBase extended classes */ constructor(handlers: Array<GQLBase>) { super() this.handlers = handlers // Generate and cache the schema SDL/IDL string and ast obj (GraphQLSchema) this.ast } /** * The Schema String and Schema AST/GraphQLSchema JavaScript objects are * cached after being processed once. If there is a runtime need to rebuild * these objects, calling `clearCache()` will allow their next usage to * rebuild them dynamically. * * @method clearCache * @memberof GQLExpressMiddleware * * @return {GQLExpressMiddleware} returns this so that it can be inlined; ala * `gqlExpressMiddleware.clearCache().ast`, for example */ clearCache(): GQLExpressMiddleware { this.cache.clear() return this } /** * The schema property returns the textual Schema as it is generated based * on the various Lattice types, interfaces and enums defined in your * project. The ast property returns the JavaScript AST represenatation of * that schema with all injected modificiations detailed in your classes. */ get ast(): GraphQLSchema { let cached: ?GraphQLSchema = this.cache.get('ast') if (cached) { return cached } let ast: GraphQLSchema = buildSchema(this.schema) SchemaUtils.injectAll(ast, this.handlers); this.cache.set('ast', ast) return ast; } /** * Generates the textual schema based on the registered `GQLBase` handlers * this instance represents. * * @method GQLExpressMiddleware#⬇︎⠀schema * @since 2.7.0 * * @return {string} a generated schema string based on the handlers that * are registered with this `GQLExpressMiddleware` instance. */ get schema(): string { let cached = this.cache.get('schema') let schema if (cached) return cached schema = SchemaUtils.generateSchemaSDL(this.handlers); this.cache.set('schema', schema) return schema } async rootValue( requestData: Object, separateByType: boolean = false ): Object { let root = await SchemaUtils.createMergedRoot( this.handlers, requestData, separateByType ) return root; } /** * Using the express-graphql module, it returns an Express 4.x middleware * function. * * @instance * @memberof GQLExpressMiddleware * @method ⬇︎⠀middleware * * @return {Function} a function that expects request, response and next * parameters as all Express middleware functions. */ get middleware(): Function { return this.customMiddleware(); } /** * Using the express-graphql module, it returns an Express 4.x middleware * function. This version however, has graphiql disabled. Otherwise it is * identical to the `middleware` property * * @instance * @memberof GQLExpressMiddleware * @method ⬇︎⠀middlewareWithoutGraphiQL * * @return {Function} a function that expects request, response and next * parameters as all Express middleware functions. */ get middlewareWithoutGraphiQL(): Function { return this.customMiddleware({graphiql: false}); } /** * In order to ensure that Lattice functions receive the request data, * it is important to use the options function feature of both * `express-graphql` and `apollo-server-express`. This function will create * an options function that reflects that schema and Lattice types defined * in your project. * * Should you need to tailor the response before it is sent out, you may * supply a function as a second parameter that takes two parameters and * returns an options object. The patchFn callback signature looks like this * * ```patchFn(options, {req, res, next|gql})``` * * When using the reference implementation, additional graphql request info * can be obtained in lieu of the `next()` function so typically found in * Express middleware. Apollo Server simply provides the next function in * this location. * * @param {Object} options any options, to either engine, that make the most * sense * @param {Function} patchFn see above */ generateOptions( options: Object = { graphiql: true }, patchFn: ?Function = null ): Function { const optsFn = async (req: mixed, res: mixed, gql: mixed) => { let schema = this.ast; let opts = { schema, rootValue: await this.rootValue({req, res, gql}), formatError: error => ({ message: error.message, locations: error.locations, stack: error.stack, path: error.path }) } merge(opts, options); if (patchFn && typeof patchFn === 'function') { merge( opts, (patchFn.bind(this)(opts, {req, res, gql})) || opts ); } return opts; } return optsFn } /** * In order to ensure that Lattice functions receive the request data, * it is important to use the options function feature of both * `express-graphql` and `apollo-server-express`. This function will create * an options function that reflects that schema and Lattice types defined * in your project. * * Should you need to tailor the response before it is sent out, you may * supply a function as a second parameter that takes two parameters and * returns an options object. The patchFn callback signature looks like this * * ```patchFn(options, {req, res, next|gql})``` * * When using the reference implementation, additional graphql request info * can be obtained in lieu of the `next()` function so typically found in * Express middleware. Apollo Server simply provides the next function in * this location. * * @param {Object} options any options, to either engine, that make the most * sense * @param {Function} patchFn see above */ generateApolloOptions( options: Object = { formatError: error => ({ message: error.message, locations: error.locations, stack: error.stack, path: error.path }), debug: true }, patchFn: ?Function = null ): Function { const optsFn = async (req: mixed, res: mixed) => { let opts = { schema: this.ast, resolvers: await this.rootValue({req, res}, true) } opts.schema = makeExecutableSchema({ typeDefs: [this.schema], resolvers: opts.resolvers }) SchemaUtils.injectAll(opts.schema, this.handlers); merge(opts, options); if (patchFn && typeof patchFn === 'function') { merge( opts, (patchFn.bind(this)(opts, {req, res})) || opts ); } return opts; } return optsFn } apolloMiddleware( apolloFn: Function, apolloOpts: Object = {}, patchFn: ?Function = null ): Array<Function> { let opts = this.generateApolloOptions(apolloOpts, patchFn) return [ bodyParser.json(), bodyParser.text({ type: 'application/graphql' }), (req, res, next) => { if (req.is('application/graphql')) { req.body = { query: req.body }; } next(); }, apolloFn(opts) ] } /** * If your needs require you to specify different values to `graphqlHTTP`, * part of the `express-graphql` package, you can use the `customMiddleware` * function to do so. * * The first parameter is an object that should contain valid `graphqlHTTP` * options. See https://github.com/graphql/express-graphql#options for more * details. Validation is NOT performed. * * The second parameter is a function that will be called after any options * have been applied from the first parameter and the rest of the middleware * has been performed. This, if not modified, will be the final options * passed into `graphqlHTTP`. In your callback, it is expected that the * supplied object is to be modified and THEN RETURNED. Whatever is returned * will be used or passed on. If nothing is returned, the options supplied * to the function will be used instead. * * @method ⌾⠀customMiddleware * @memberof GQLExpressMiddleware * @instance * * @param {Object} [graphqlHttpOptions={graphiql: true}] standard set of * `express-graphql` options. See above. * @param {Function} patchFn see above * @return {Function} a middleware function compatible with Express */ customMiddleware( graphqlHttpOptions: Object = {graphiql: true}, patchFn?: Function ): Function { const optsFn = this.generateOptions(graphqlHttpOptions, patchFn) return graphqlHTTP(optsFn) } /** * An optional express middleware function that can be mounted to return * a copy of the generated schema string being used by GQLExpressMiddleware. * * @memberof GQLExpressMiddleware * @method schemaMiddleware * @instance * * @type {Function} */ get schemaMiddleware(): Function { return (req: Object, res: Object, next: ?Function) => { res.status(200).send(this.schema); } } /** * An optional express middleware function that can be mounted to return * the JSON AST representation of the schema string being used by * GQLExpressMiddleware. * * @memberof GQLExpressMiddleware * @method astMiddleware * @instance * * @type {Function} */ get astMiddleware(): Function { return (req: Object, res: Object, next: ?Function) => { res.status(200).send('Temporarily disabled in this version') // let cachedOutput = this.cache.get('astMiddlewareOutput') // if (cachedOutput) { // res // .status(302) // .set('Content-Type', 'application/json') // .send(cachedOutput) // } // else { // this.rootValue({req, res, next}, true) // .then(resolvers => { // let schema: GraphQLSchema = buildSchema(this.schema) // SchemaUtils.injectInterfaceResolvers(schema, this.handlers); // SchemaUtils.injectEnums(schema, this.handlers); // SchemaUtils.injectScalars(schema, this.handlers); // SchemaUtils.injectComments(schema, this.handlers); // function killToJSON(obj: any, path = 'obj.') { // for (let key in obj) { // try { // if (key == 'prev' || key == 'next' || key == 'ofType') continue; // if (key == 'toJSON') { // let success = delete obj.toJSON // //console.log(`Killing ${path}toJSON...${success ? 'success' : 'failure'}`) // continue // } // if (key == 'inspect') { // let success = delete obj.inspect // //console.log(`Killing ${path}inspect...${success ? 'success' : 'failure'}`) // continue // } // if (key == 'toString') { // obj.toString = Object.prototype.toString // //console.log(`Replacing ${path}toString with default`) // continue // } // if (typeof obj[key] == 'function') { // obj[key] = `[Function ${obj[key].name}]` // continue // } // if (typeof obj[key] == 'object') { // obj[key] = killToJSON(obj[key], `${path}${key}.`) // continue // } // } // catch (error) { // continue // } // } // return obj // } // // $FlowFixMe // schema = killToJSON(schema) // // Still do not know why/how they are preventing JSONifying the // // _typeMap keys. So aggravting // for (let typeKey of Object.keys(schema._typeMap)) { // let object = {} // // $FlowFixMe // for (let valKey of Object.keys(schema._typeMap[typeKey])) { // // $FlowFixMe // object[valKey] = schema._typeMap[typeKey][valKey] // } // // $FlowFixMe // schema._typeMap[typeKey] = object // } // let output = JSON.stringify(schema) // this.cache.delete('ast') // this.cache.set('astMiddlewareOutput', output) // res // .status(200) // .set('Content-Type', 'application/json') // .send(output) // }) // .catch(error => { // console.error(error) // res // .status(500) // .json(error) // }) // } } } } export default GQLExpressMiddleware;