SchemaUtils.js

// @flow

import path from 'path'
import { SyntaxTree } from './SyntaxTree'
import { GQLBase, META_KEY } from './GQLBase'
import { GQLEnum } from './GQLEnum'
import { GQLInterface } from './GQLInterface'
import { GQLScalar } from './GQLScalar'
import { typeOf } from 'ne-types'
import { LatticeLogs as ll } from './utils'
import { merge } from 'lodash'
import EventEmitter from 'events'
import {
  parse,
  print,
  buildSchema,
  GraphQLInterfaceType,
  GraphQLEnumType,
  GraphQLScalarType,
  GraphQLSchema
} from 'graphql'

/**
 * The SchemaUtils is used by tools such as GQLExpressMiddleware in order to
 * apply GraphQL Lattice specifics to the build schema.
 *
 * @class SchemaUtils
 */
export class SchemaUtils extends EventEmitter {
  /**
   * Calls all the Lattice post-schema creation routines on a given Schema
   * using data from a supplied array of classes.
   *
   * @param {GraphQLSchema} schema the schema to post-process
   * @param {Array<GQLBase>} Classes the Classes from which to drive post
   * processing data from
   */
  static injectAll(schema: GraphQLSchema, Classes: Array<GQLBase>) {
    SchemaUtils.injectInterfaceResolvers(schema, Classes);
    SchemaUtils.injectEnums(schema, Classes);
    SchemaUtils.injectScalars(schema, Classes);
    SchemaUtils.injectComments(schema, Classes);
  }

  /**
   * 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 classes 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
   *
   * @memberof SchemaUtils
   * @method ⌾⠀injectComments
   * @static
   * @since 2.7.0
   *
   * @param {Object} schema a built GraphQLSchema object created via buildSchema
   * or some other alternative but compatible manner
   * @param {Function[]} Classes these are GQLBase extended classes used to
   * manipulate the schema with.
   */
  static injectComments(schema: Object, Classes: Array<GQLBase>) {
    const {
      DOC_CLASS, DOC_FIELDS, DOC_QUERIES, DOC_MUTATORS, DOC_SUBSCRIPTIONS,
      DOC_QUERY, DOC_MUTATION, DOC_SUBSCRIPTION
    } = GQLBase;

    for (let Class of Classes) {
      const docs = Class.apiDocs();
      const query = schema._typeMap.Query;
      const mutation = schema._typeMap.Mutation;
      const subscription = schema._typeMap.Subscription;
      let type;

      if ((type = schema._typeMap[Class.name])) {
        let fields = type._fields;
        let values = type._values;

        if (docs[DOC_CLASS]) { type.description = docs[DOC_CLASS] }

        for (let field of Object.keys(docs[DOC_FIELDS] || {})) {
          if (fields && field in fields) {
            fields[field].description = docs[DOC_FIELDS][field];
          }
          if (values) {
            for (let value of values) {
              if (value.name === field) {
                value.description = docs[DOC_FIELDS][field]
              }
            }
          }
        }
      }

      for (let [_type, _CONST, _topCONST] of [
        [query, DOC_QUERIES, DOC_QUERY],
        [mutation, DOC_MUTATORS, DOC_MUTATION],
        [subscription, DOC_SUBSCRIPTIONS, DOC_SUBSCRIPTION]
      ]) {
        if (
          _type
          && (
            (Object.keys(docs[_CONST] || {}).length)
            || (docs[_topCONST] && docs[_topCONST].length)
          )
        ) {
          let fields = _type._fields;

          if (docs[_topCONST]) {
            _type.description = docs[_topCONST]
          }

          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 classes and
   * finds `GQLInterface` types and applies their `resolveType()`
   * implementations.
   *
   * @memberof SchemaUtils
   * @method ⌾⠀injectInterfaceResolvers
   * @static
   *
   * @param {Object} schema a built GraphQLSchema object created via buildSchema
   * or some other alternative but compatible manner
   * @param {Function[]} Classes these are GQLBase extended classes used to
   * manipulate the schema with.
   */
  static injectInterfaceResolvers(schema: Object, Classes: Array<GQLBase>) {
    for (let Class of Classes) {
      if (Class.GQL_TYPE === GraphQLInterfaceType) {
        schema._typeMap[Class.name].resolveType =
        schema._typeMap[Class.name]._typeConfig.resolveType =
          Class.resolveType;
      }
    }
  }

  /**
   * Somewhat like `injectComments` and other similar methods, the
   * `injectInterfaceResolvers` method walks the registered classes and
   * finds `GQLInterface` types and applies their `resolveType()`
   * implementations.
   *
   * @memberof SchemaUtils
   * @method ⌾⠀injectEnums
   * @static
   *
   * @param {Object} schema a built GraphQLSchema object created via buildSchema
   * or some other alternative but compatible manner
   * @param {Function[]} Classes these are GQLBase extended classes used to
   * manipulate the schema with.
   */
  static injectEnums(schema: Object, Classes: Array<GQLBase>) {
    for (let Class of Classes) {
      if (Class.GQL_TYPE === GraphQLEnumType) {
        const __enum = schema._typeMap[Class.name];
        const values = Class.values;

        for (let value of __enum._values) {
          if (value.name in values) {
            merge(value, values[value.name])
          }
        }
      }
    }
  }

  /**
   * GQLScalar types must define three methods to have a valid implementation.
   * They are serialize, parseValue and parseLiteral. See their docs for more
   * info on how to do so.
   *
   * This code finds each scalar and adds their implementation details to the
   * generated schema type config.
   *
   * @memberof SchemaUtils
   * @method ⌾⠀injectScalars
   * @static
   *
   * @param {Object} schema a built GraphQLSchema object created via buildSchema
   * or some other alternative but compatible manner
   * @param {Function[]} Classes these are GQLBase extended classes used to
   * manipulate the schema with.
   */
  static injectScalars(schema: Object, Classes: Array<GQLBase>) {
    for (let Class of Classes) {
      if (Class.GQL_TYPE === GraphQLScalarType) {
        // @ComputedType
        const type = schema._typeMap[Class.name];

        // @ComputedType
        const { serialize, parseValue, parseLiteral } = Class;

        if (!serialize || !parseValue || !parseLiteral) {
          // @ComputedType
          ll.error(`Scalar type ${Class.name} has invaild impl.`);
          continue;
        }

        merge(type._scalarConfig, {
          serialize,
          parseValue,
          parseLiteral
        });
      }
    }
  }

  /**
   * A function that combines the IDL schemas of all the supplied classes and
   * returns that value to the middleware getter.
   *
   * @static
   * @memberof GQLExpressMiddleware
   * @method ⌾⠀generateSchemaSDL
   *
   * @return {string} a dynamically generated GraphQL IDL schema string
   */
  static generateSchemaSDL(
    Classes: Array<GQLBase>,
    logOutput: boolean = true
  ): string {
    let schema = SyntaxTree.EmptyDocument();
    let log = (...args) => {
      if (logOutput) {
        console.log(...args);
      }
    }

    for (let Class of Classes) {
      let classSchema = Class.SCHEMA;

      if (typeOf(classSchema) === 'Symbol') {
        let handler = Class.handler;
        let filename = path.basename(Class.handler.path)

        classSchema = handler.getSchema();
        log(
          `\nRead schema (%s)\n%s\n%s\n`,
          filename,
          '-'.repeat(14 + filename.length),
          classSchema.replace(/^/gm, '  ')
        )
      }

      schema.appendDefinitions(classSchema);
    }

    log('\nGenerated GraphQL Schema\n----------------\n%s', schema);

    return schema.toString();
  }

  /**
   * 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.
   *
   * @static
   * @memberof SchemaUtils
   * @method ⌾⠀createMergedRoot
   *
   * @param {Array<GQLBase>} Classes the GQLBase extended class objects or
   * functions from which to merge the RESOLVERS and MUTATORS functions.
   * @param {Object} requestData for Express apss, this will be an object
   * containing { req, res, gql } where those are the Express request and
   * response object as well as the GraphQL parameters for the request.
   * @return {Promise<Object>} a Promise resolving to an Object containing all
   * the functions described in both Query and Mutation types.
   */
  static async createMergedRoot(
    Classes: Array<GQLBase>,
    requestData: Object,
    separateByType: boolean = false
  ): Promise<Object> {
    const root = {};

    for (let Class of Classes) {
      merge(
        root,
        // $FlowFixMe
        await Class.getMergedRoot(requestData, separateByType)
      );
    }

    return root;
  }
}

export default SchemaUtils