GQLEnum.js

/**
 @namespace GQLInterface
 @flow
 */

import { GQLBase } from './GQLBase'
import { GraphQLEnumType, parse } from 'graphql'
import { Getters } from './decorators/ModelProperties'
import { LatticeLogs as ll } from './utils'

/* 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();

/**
 * GraphQL Enum types can be a bit picky when it comes to how scalar types
 * equate to enum values. Lattice makes this easier by allowing you to specify
 * a value or the key when your enum has a value other than the key; GraphQL
 * does not allow this by default.
 *
 * Further more, when instantiating a GQLEnum type, you can pass a string or
 * value matching the enum key or value or you can pass an object with key of
 * value and the value being either the enum key or value. If any of those
 * things match, then your `instance.value` will equate to the enum's key. If,
 * on the other hand, your supplied values do not match then `instance.value`
 * will be `null`.
 *
 * @class GQLEnum
 */
@Getters('symbol')
export class GQLEnum extends GQLBase {
  constructor(enumValueOrKey: ?Object, requestData: ?Object) {
    super({}, requestData)

    const Class = this.constructor
    const enums = Class.enums;
    let symbol;
    let enumVK: (Object | string | null) = enumValueOrKey || null

    // @ComputedType
    symbol = enums[enumVK] || enumVK && enums[enumVK.value] || null

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

  /**
   * Retrieves the actual symbol stored name property from the internal
   * model object for this enum instance. That is a mouthfull, but it
   * basically means that if your enum is something like:
   *
   * ```
   * enum Person { TALL, SHORT }
   * ```
   *
   * and you create an instance using any of the following
   *
   * ```
   * p = new Person('TALL')
   * p = new Person(valueFor('TALL'))
   * p = new Person({value: 'TALL'})
   * ```
   *
   * that your response to `p.name` will equate to `TALL`.
   *
   * @method ⬇︎⠀name
   * @memberof GQLEnum
   * @return {mixed} typically a String but any valid type supplied
   */
  get name(): mixed {
    const name = this.getModel().name

    return (
      name !== undefined &&
      name !== null &&
      name !== NaN
    ) ? name : null;
  }

  /**
   * Much like the `.name` getter, the `.value` getter will typically
   * retreive the name of the enum key you are requesting. In rare cases
   * where you have defined values that differ from the name, the `.value`
   * getter will retrieve that custom value from the `.value` property on
   * the symbol in question.
   *
   * This should do the right thing even if you instantiated the instance
   * using the name.
   *
   * @memberof GQLEnum
   * @method ⬇︎⠀value
   * @return {mixed} the value of the enum type; this in all likihood should
   * be a String or potentially an object
   */
  get value(): mixed {
    const value = this.getModel().value

    return (
      value !== undefined &&
      value !== null &&
      value !== NaN
    ) ? value : null;
  }

  /**
   * 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: Object = { 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> {
    // @ComputedType
    if (!this[ENUMS]) {
      const map: Map<*,*> = new Map();
      const ast = parse((this.SCHEMA: any));
      const array = new Proxy([], GQLEnum.GenerateEnumsProxyHandler(map));
      const values = this.values || {};
      let astValues: Array<any>;

      try {
        // TODO: $FlowFixMe
        astValues = ast.definitions[0].values;
      }
      catch (error) {
        ll.error('Unable to discern the values from your enums SCHEMA')
        ll.error(error)
        throw error;
      }

      // 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 astValues) {
        let defKey = enumDef.name.value;
        let symObj: Object = Object(Symbol.for(defKey));

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

        map.set(symObj.name, symObj)
        map.set(symObj.value, symObj)

        // 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)
      }

      // @ComputedType
      this[ENUMS] = array;
    }

    // @ComputedType
    return this[ENUMS];
  }

  /**
   * Due to the complexity of being able to access both the keys and values
   * properly for an enum type, a Map is used as the backing store. The handler
   * returned by this method is to be passed to a Proxy.
   *
   * @method GQLEnum#GenerateEnumsProxyHandler
   * @static
   *
   * @param {Map} map the map containing the key<->value and
   * value<->key mappings; the true storage backing the array in question.
   * @return {Object}
   */
  static GenerateEnumsProxyHandler(map: Map<*, *>) {
    return {
      /**
       * Get handler for the Map backed Array Proxy
       *
       * @memberof! GQLEnum
       * @method get
       *
       * @param {mixed} obj the object targeted by the Proxy
       * @param {string} key `key` of the value being requested
       * @return {mixed} the `value` being requested
       */
      get(obj, key) {
        if (map.has(key)) {
          return map.get(key)
        }

        return obj[key]
      },

      /**
       * Set handler for the Map backed Array Proxy.
       *
       * @memberof! GQLEnum
       * @method set
       *
       * @param {mixed} obj the object the Proxy is targeting
       * @param {string} key a string `key` being set
       * @param {mixed} value the `value` being assigned to `key`
       */
      set(obj, key, value) {
        if (isFinite(key) && value instanceof Symbol) {
          map.set(value.name, value)
          map.set(value.value, value)
        }

        // Some accessor on the receiving array
        obj[key] = value;

        // Arrays return length when pushing. Assume value as return
        // otherwise. ¯\_(ツ)_/¯
        return isFinite(key) ? obj.length : obj[key];
      }
    }
  }

  /** @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.
      `
    }
  }
}