GQLEnum.js

/**
 @namespace GQLInterface
 @flow
 */

import { GQLBase } from './GQLBase'
import { GraphQLEnumType, parse } from 'graphql'
import { Getters } from './decorators/ModelProperties'

/* Internal Symbol referring to real accessor to GQLBase model object */
const _MODEL_KEY = Symbol.for('data-model-contents-value');

/* Internal Symbol referring to the static object containing a proxy handler */
const _PROXY_HANDLER = Symbol.for('internal-base-proxy-handler')

/* Internal Symbol property referring to the mapping of values on the GQLEnum */
const ENUMS = Symbol();

/**
 * TODO finish comment
 *
 * @class GQLEnum
 */
@Getters(['value', String])
export class GQLEnum extends GQLBase {
  constructor(enumValueOrKey: ?mixed, requestData: ?Object) {
    super({}, requestData)

    const Class = this.constructor
    const enums = Class.enums;
    let symbol;

    for (let property of [
      enumValueOrKey,
      String(enumValueOrKey && enumValueOrKey.value),
      String(enumValueOrKey && enumValueOrKey.toString())
    ]) {
      if (property in enums) {        
        symbol = enums[property]
        break;
      }
    }

    if (symbol) {
      Object.assign(this.getModel(), {
        name: symbol.name,
        value: symbol.value,
        symbol: symbol
      })
    }
  }

  /**
   * Determines the default type targeted by this GQLBase class. Any
   * type will technically be valid but only will trigger special behavior
   *
   * @memberof GQLEnum
   * @method ⬇︎⠀GQL_TYPE
   * @static
   * @const
   *
   * @return {Function} a type, such as `GraphQLObjectType` or
   * `GraphQLInterfaceType`
   */
  static get GQL_TYPE(): Function {
    return GraphQLEnumType;
  }

  /**
   * Each instance of GQLEnum must specify a map of keys and values. If this
   * method returns null or is not defined, the value of the enum will match
   * the name of the enum as per the reference implementation.
   *
   * Example:
   * ```
   *   static get values(): ?Object {
   *     const { valueOf } = this;
   *
   *     return {
   *       NAME: valueOf(value)
   *     }
   *   }
   * ```
   *
   * @method ⬇︎⠀values
   * @memberof GQLEnum
   * @static
   *
   * @return {Object|Null} an object mapping with each key mapping to an object
   * possessing at least a value field, which in turn maps to the desired value
   */
  static get values(): Object {
    return {};
  }

  /**
   * Shorthand method to generate a GraphQLEnumValueDefinition implementation
   * object. Use this for building and customizing your `values` key/value
   * object in your child classes.
   *
   * @memberof GQLEnum
   * @method valueFor
   * @static
   *
   * @param {mixed} value any nonstandard value you wish your enum to have
   * @param {String} deprecationReason an optional reason to deprecate an enum
   * @param {String} description a non Lattice standard way to write a comment
   * @return {Object} an object that conforms to the GraphQLEnumValueDefinition
   * defined here http://graphql.org/graphql-js/type/#graphqlenumtype
   */
  static valueFor(
    value: mixed,
    deprecationReason: ?string,
    description: ?string
  ): Object {
    const result = { value }

    if (deprecationReason) { result.deprecationReason = deprecationReason }
    if (description) { result.description = description }

    return result;
  }

  /**
   * For easier use within JavaScript, the static enums method provides a
   * Symbol backed solution for each of the enums defined. Each `Symbol`
   * instance is wrapped in Object so as to allow some additional properties
   * to be written to it.
   *
   * @memberof GQLEnum
   * @method ⬇︎⠀enums
   * @static
   *
   * @return {Array<Symbol>} an array of modified Symbols for each enum
   * variation defined.
   */
  static get enums(): Array<Symbol> {
    if (!this[ENUMS]) {
      const ast = parse(this.SCHEMA);
      const array = [];
      const values = this.values || {};

      // Walk the AST for the class' schema and extract the names (same as 
      // values when specified in GraphQL SDL) and build an object the has 
      // the actual defined value and the AST generated name/value. 
      for (let enumDef of ast.definitions[0].values) {
        let defKey = enumDef.name.value;
        let symObj = Object(Symbol.for(defKey));

        symObj.value = (values[defKey] && values[defKey].value) || defKey;
        symObj.name = defKey;
        symObj.sym = symObj.valueOf()

        // This bit of logic allows us to look into the "enums" property and 
        // get the generated Object wrapped Symbol with keys and values by 
        // supplying either a key or value.
        array.push(symObj)
        array[defKey] = symObj;
        array[symObj.value] = symObj;
      }
      
      this[ENUMS] = array;
    }

    return this[ENUMS];
  }

  /** @inheritdoc */
  static apiDocs(): Object {
    const { DOC_CLASS, DOC_FIELDS, joinLines } = this;

    return {
      [DOC_CLASS]: joinLines`
        GQLEnums allow the definition of enum types with description fields
        and values other than a 1:1 mapping of their types and their type
        names. If you are reading this, the implementor likely did not
        contribute comments for their type.
      `
    }
  }
}