Source: document.js

import Query from './query';
import Mutation from './mutation';
import Operation from './operation';
import join from './join';
import SelectionSet, {FragmentDefinition} from './selection-set';

function isAnonymous(operation) {
  return operation.isAnonymous;
}

function hasAnonymousOperations(operations) {
  return operations.some(isAnonymous);
}

function hasDuplicateOperationNames(operations) {
  const names = operations.map((operation) => operation.name);

  return names.reduce((hasDuplicates, name, index) => {
    return hasDuplicates || names.indexOf(name) !== index;
  }, false);
}

function extractOperation(typeBundle, operationType, ...args) {
  if (Operation.prototype.isPrototypeOf(args[0])) {
    return args[0];
  }

  if (operationType === 'query') {
    return new Query(typeBundle, ...args);
  } else {
    return new Mutation(typeBundle, ...args);
  }
}

function isInvalidOperationCombination(operations) {
  if (operations.length === 1) {
    return false;
  }

  return hasAnonymousOperations(operations) || hasDuplicateOperationNames(operations);
}

function fragmentNameIsNotUnique(existingDefinitions, name) {
  return existingDefinitions.some((definition) => (definition.name === name));
}

export default class Document {

  /**
   * This constructor should not be invoked directly.
   * Use the factory function {@link Client#document} to create a Document.
   * @param {Object} typeBundle A set of ES6 modules generated by {@link https://github.com/Shopify/graphql-js-schema|graphql-js-schema}.
   */
  constructor(typeBundle) {
    this.typeBundle = typeBundle;
    this.definitions = [];
  }

  /**
   * Returns the GraphQL query string for the Document (e.g. `query queryOne { ... } query queryTwo { ... }`).
   *
   * @return {String} The GraphQL query string for the Document.
   */
  toString() {
    return join(this.definitions);
  }

  /**
   * Adds an operation to the Document.
   *
   * @private
   * @param {String} operationType The type of the operation. Either 'query' or 'mutation'.
   * @param {(Operation|String)} [query|queryName] Either an instance of an operation
   *   object, or the name of an operation. Both are optional.
   * @param {Object[]} [variables] A list of variables in the operation. See {@link Client#variable}.
   * @param {Function} [callback] The query builder callback. If an operation
   *   instance is passed, this callback will be ignored.
   *   A {@link SelectionSet} is created using this callback.

   */
  addOperation(operationType, ...args) {
    const operation = extractOperation(this.typeBundle, operationType, ...args);

    if (isInvalidOperationCombination(this.operations.concat(operation))) {
      throw new Error('All operations must be uniquely named on a multi-operation document');
    }

    this.definitions.push(operation);
  }

  /**
   * Adds a query to the Document.
   *
   * @example
   * document.addQuery('myQuery', (root) => {
   *   root.add('cat', (cat) => {
   *    cat.add('name');
   *   });
   * });
   *
   * @param {(Query|String)} [query|queryName] Either an instance of a query
   *   object, or the name of a query. Both are optional.
   * @param {Object[]} [variables] A list of variables in the query. See {@link Client#variable}.
   * @param {Function} [callback] The query builder callback. If a query
   *   instance is passed, this callback will be ignored.
   *   A {@link SelectionSet} is created using this callback.
   */
  addQuery(...args) {
    this.addOperation('query', ...args);
  }

  /**
   * Adds a mutation to the Document.
   *
   * @example
   * const input = client.variable('input', 'CatCreateInput!');
   *
   * document.addMutation('myMutation', [input], (root) => {
   *   root.add('catCreate', {args: {input}}, (catCreate) => {
   *     catCreate.add('cat', (cat) => {
   *       cat.add('name');
   *     });
   *   });
   * });
   *
   * @param {(Mutation|String)} [mutation|mutationName] Either an instance of a mutation
   *   object, or the name of a mutation. Both are optional.
   * @param {Object[]} [variables] A list of variables in the mutation. See {@link Client#variable}.
   * @param {Function} [callback] The mutation builder callback. If a mutation
   *   instance is passed, this callback will be ignored.
   *   A {@link SelectionSet} is created using this callback.
   */
  addMutation(...args) {
    this.addOperation('mutation', ...args);
  }

  /**
   * Defines a fragment on the Document.
   *
   * @param {String} name The name of the fragment.
   * @param {String} onType The type the fragment is on.
   * @param {Function} [builderFunction] The query builder callback.
   *   A {@link SelectionSet} is created using this callback.
   * @return {FragmentSpread} A {@link FragmentSpread} to be used with {@link SelectionSetBuilder#addFragment}.
   */
  defineFragment(name, onType, builderFunction) {
    if (fragmentNameIsNotUnique(this.fragmentDefinitions, name)) {
      throw new Error('All fragments must be uniquely named on a multi-fragment document');
    }

    const selectionSet = new SelectionSet(this.typeBundle, onType, builderFunction);
    const fragment = new FragmentDefinition(name, onType, selectionSet);

    this.definitions.push(fragment);

    return fragment.spread;
  }

  /**
   * All operations ({@link Query} and {@link Mutation}) on the Document.
   */
  get operations() {
    return this.definitions.filter((definition) => Operation.prototype.isPrototypeOf(definition));
  }

  /**
   * All {@link FragmentDefinition}s on the Document.
   */
  get fragmentDefinitions() {
    return this.definitions.filter((definition) => FragmentDefinition.prototype.isPrototypeOf(definition));
  }
}