// @flow
// @module GQLExpressMiddleware
import { SyntaxTree } from './SyntaxTree'
import graphqlHTTP from 'express-graphql'
import { parse, print, buildSchema, GraphQLInterfaceType } from 'graphql'
import { GQLBase } from './GQLBase'
import { GQLInterface } from './GQLInterface'
import { typeOf } from './types'
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
{
/**
* 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;
}
/**
* An asynchronous function used to parse the supplied classes for each
* ones resolvers and mutators. These are all combined into a single root
* object passed to express-graphql.
*
* @instance
* @memberof GQLExpressMiddleware
* @method ⌾⠀makeRoot
*
* @param {Request} req an Express 4.x request object
* @param {Response} res an Express 4.x response object
* @param {Object} gql an object containing information about the graphql
* request. It has the format of `{ query, variables, operationName, raw }`
* as described here: https://github.com/graphql/express-graphql
* @return {Promise<Object>} a Promise resolving to an Object containing all
* the functions described in both Query and Mutation types.
*/
async makeRoot(req: Object, res: Object, gql: Object): Promise<Object> {
this.root = {};
for (let object of this.handlers) {
Object.assign(
this.root,
await object.RESOLVERS({req, res, gql}),
await object.MUTATORS({req, res, gql})
);
}
return this.root;
}
/**
* A function that combines the IDL schemas of all the supplied classes and
* returns that value to the middleware getter.
*
* @instance
* @memberof GQLExpressMiddleware
* @method ⌾⠀makeSchema
*
* @return {string} a dynamically generated GraphQL IDL schema string
*/
makeSchema(): string {
let schema = SyntaxTree.EmptyDocument();
for (let Class of this.handlers) {
let classSchema = Class.SCHEMA;
if (typeOf(classSchema) === 'Symbol') {
let handler = Class.handler;
let filename = path.basename(Class.handler.path)
classSchema = handler.getSchema();
console.log(
`\nRead schema (%s)\n%s\n%s\n`,
filename,
'-'.repeat(14 + filename.length),
classSchema.replace(/^/gm, ' ')
)
}
schema.appendDefinitions(classSchema);
}
console.log('\nGenerated GraphQL Schema\n----------------\n%s', schema);
return schema.toString();
}
/**
* 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});
}
/**
* 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} patchFinalOpts see above
* @return {Function} a middleware function compatible with Express
*/
customMiddleware(
graphqlHttpOptions: Object = {graphiql: true},
patchFinalOpts: Function = null
): Function {
const schema = buildSchema(this.makeSchema());
// TODO handle scalars, unions and the rest
this.injectInterfaceResolvers(schema);
this.injectComments(schema);
// See if there is a way abstract the passing req, res, gql to each
// makeRoot resolver without invoking makeRoot again every time.
return graphqlHTTP(async (req, res, gql) => {
let opts = {
schema,
rootValue: await this.makeRoot(req, res, gql),
formatError: error => ({
message: error.message,
locations: error.locations,
stack: error.stack,
path: error.path
})
};
Object.assign(opts, graphqlHttpOptions);
if (patchFinalOpts) {
Object.assign(opts, patchFinalOpts.bind(this)(opts) || opts);
}
return opts;
});
}
/**
* Until such time as I can get the reference Facebook GraphQL AST parser to
* read and apply descriptions or until such time as I employ the Apollo
* AST parser, providing a `static get apiDocs()` getter is the way to get
* your descriptions into the proper fields, post schema creation.
*
* This method walks the types in the registered handlers and the supplied
* schema type. It then injects the written comments such that they can
* be exposed in graphiql and to applications or code that read the meta
* fields of a built schema
*
* TODO handle argument comments and other outliers
*
* @memberof GQLExpressMiddleware
* @method ⌾⠀injectComments
* @instance
*
* @param {Object} schema a built GraphQLSchema object created via buildSchema
* or some other alternative but compatible manner
*/
injectComments(schema: Object) {
const {
DOC_CLASS, DOC_FIELDS, DOC_QUERIES, DOC_MUTATORS, DOC_SUBSCRIPTIONS
} = GQLBase;
for (let handler of this.handlers) {
console.log('handler: %s', handler.name)
const docs = handler.apiDocs();
const query = schema._typeMap.Query;
const mutation = schema._typeMap.Mutation;
const subscription = schema._typeMap.Subscription;
let type;
if ((type = schema._typeMap[handler.name])) {
let fields = type._fields;
if (docs[DOC_CLASS]) { type.description = docs[DOC_CLASS] }
for (let field of Object.keys(docs[DOC_FIELDS] || {})) {
if (field in fields) {
fields[field].description = docs[DOC_FIELDS][field];
}
}
}
for (let [_type, _CONST] of [
[query, DOC_QUERIES],
[mutation, DOC_MUTATORS],
[subscription, DOC_SUBSCRIPTIONS]
]) {
if (_type && Object.keys(docs[_CONST] || {}).length) {
let fields = _type._fields;
if (docs[_CONST][DOC_CLASS]) {
_type.description = docs[_CONST][DOC_CLASS]
}
for (let field of Object.keys(docs[_CONST])) {
if (field in fields) {
fields[field].description = docs[_CONST][field];
}
}
}
}
}
}
/**
* Somewhat like `injectComments` and other similar methods, the
* `injectInterfaceResolvers` method walks the registered handlers and
* finds `GQLInterface` types and applies their `resolveType()`
* implementations.
*
* @memberof GQLExpressMiddleware
* @method ⌾⠀injectInterfaceResolvers
* @instance
*
* @param {Object} schema a built GraphQLSchema object created via buildSchema
* or some other alternative but compatible manner
*/
injectInterfaceResolvers(schema: Object) {
for (let handler of this.handlers) {
if (handler.GQL_TYPE === GraphQLInterfaceType) {
console.log(`Applying ${handler.name}'s resolveType() method`);
schema._typeMap[handler.name].resolveType =
schema._typeMap[handler.name]._typeConfig.resolveType =
handler.resolveType;
}
}
}
}
export default GQLExpressMiddleware;