Home Reference Source

src/schema/enhanceSchemaWithQueryArguments.js

// @flow
import cloneDeep from 'lodash.clonedeep';

import {
  getBaseType,
  buildTypeDefinition,
  buildField,
  buildValue,
  buildArgument,
  isScalarField
} from '../util/graphql';

import {
  DOCUMENT,
  OBJECT_TYPE_DEFINITION,
  INPUT_OBJECT_TYPE_DEFINITION,
  ENUM_TYPE_DEFINITION,
  TYPE_EXTENSION_DEFINITION,
  LIST_TYPE,
  FIELD_DEFINITION,
  NAME
} from 'graphql/language/kinds';

import {
  INT,
  FLOAT,
  STRING,
  ENUM,
  BOOLEAN,
  BSON_TYPE,
  REGEX,
  QUERY
} from '../constants';

import { lcFirst } from '../util/capitalization';

/**
 * prepares the schema for the additional query arguments types
 * @public
 * @param {object} inputSchema - the input's schema with all fields
 * @return {object} enhancedSchema - the enhanced output Schema
 */

export function enhanceSchemaWithQueryArguments(inputSchema: any): any {
  const enhancedSchema = cloneDeep(inputSchema);
  const queryArguments = {};

  if (enhancedSchema.kind === DOCUMENT) {
    enhancedSchema.definitions
      .filter(def => def.kind === OBJECT_TYPE_DEFINITION)
      .forEach(({ fields, name }) => {
        const TypeName = name.value;

        // get all field definition types
        const fieldTypes = getFieldTypes(fields);

        // prepare "filter" query argument fields for scalar types
        const filterInputFields = getFilterInputFields(fieldTypes, TypeName);

        // prepare an enum to contain all sortable fields for the orderBy
        const sortEnumValues = getEnumValues(fieldTypes);

        // prepare "orderBy" query argument fields
        const orderByInputFields = getOrderByInputFields(TypeName);

        if (filterInputFields.length > 0) {
          const queryFieldName = `${lcFirst(TypeName)}s`;
          const FilterInputTypeName = `${TypeName}Filter`;
          const SortEnumTypeName = `${TypeName}Sort`;
          const OrderByInputTypeName = `${TypeName}OrderBy`;

          addFilterInputType(
            FilterInputTypeName,
            filterInputFields,
            enhancedSchema
          );
          addSortEnumType(SortEnumTypeName, sortEnumValues, enhancedSchema);
          addOrderByInputType(
            OrderByInputTypeName,
            orderByInputFields,
            enhancedSchema
          );

          queryArguments[queryFieldName] = {
            filterName: FilterInputTypeName,
            orderByName: `[${OrderByInputTypeName}!]`
          };
        }
      });

    // add the newly created types to the Query itself,
    // to make them accessable by users
    addQueryArguments(enhancedSchema, queryArguments);

    // add query arguments to other paginated fields
    addPaginatedFieldsArguments(enhancedSchema);
  }

  return enhancedSchema;
}

/**
 * prepares the orderBy input type
 * @private
 * @param {string} TypeName - the name of the type
 * @return {object} fieldAST - the AST of an input type field
 */

function getOrderByInputFields(TypeName) {
  // prepare "orderBy" query argument fields
  return [
    buildField('sort', [], `${TypeName}Sort!`),
    buildField('direction', [], 'Direction = ASC')
  ];
}

/**
 * prepare enum values out of field types
 * @private
 * @param {array} fieldTypes - AST array of field definitions
 * @return {object} fieldAST - the AST of enum values
 */

function getEnumValues(fieldTypes) {
  // prepare an enum to contain all sortable fields for the orderBy
  return fieldTypes
    .filter(field => field.scalar)
    .map(field => buildValue(field.name, [], STRING));
}

/**
 * prepare enum values out of field types
 * @private
 * @param {array} fieldTypes - AST array of field definitions
 * @param {string} TypeName - the name of the type
 * @return {object} fieldAST - the AST of enum values
 */

function getFilterInputFields(fieldTypes, TypeName) {
  // prepare "filter" query argument fields for scalar types
  return fieldTypes
    .filter(field => field.scalar)
    .map(field => prepareArgument(field, TypeName))
    .reduce((array, field) => array.concat(field), [
      { [`AND`]: `[${TypeName}Filter!]` },
      { [`NOR`]: `[${TypeName}Filter!]` },
      { [`OR`]: `[${TypeName}Filter!]` }
    ])
    .map(field => {
      const name = Object.keys(field)[0];
      const type = field[name];
      return buildField(name, [], type);
    });
}

/**
 * analyse and identify field information for further processing
 * @private
 * @param {array} fields - AST array of field definitions
 * @return {array} fieldAST - list of field descriptions
 */

function getFieldTypes(fields) {
  return fields.map(field => ({
    name: field.name.value,
    type: getBaseType(field.type).name.value,
    scalar: isScalarField(field)
  }));
}

/**
 * registers the "filter" input type into the AST
 * @private
 * @param {string} FilterInputTypeName - name of the input field
 * @param {array} filterInputFields - AST array of field definitions
 * @param {object} enhancedSchema - AST of the document
 */

function addFilterInputType(
  FilterInputTypeName,
  filterInputFields,
  enhancedSchema
) {
  // add "input <TypeName>Filter" type to the schema
  enhancedSchema.definitions.push(
    buildTypeDefinition(
      FilterInputTypeName,
      filterInputFields,
      INPUT_OBJECT_TYPE_DEFINITION
    )
  );
}

/**
 * registers the "enum <Type>Sort" enum type into the AST
 * @private
 * @param {string} SortEnumTypeName - name of the enum field
 * @param {array} sortEnumValues - AST array of field definitions
 * @param {object} enhancedSchema - AST of the document
 */

function addSortEnumType(SortEnumTypeName, sortEnumValues, enhancedSchema) {
  // add "enum <TypeName>Sort" type
  enhancedSchema.definitions.push(
    buildTypeDefinition(
      SortEnumTypeName,
      [],
      ENUM_TYPE_DEFINITION,
      sortEnumValues
    )
  );
}

/**
 * registers the "input <Type>OrderBy" input type into the AST
 * @private
 * @param {string} OrderByInputTypeName - name of the input field
 * @param {array} orderByInputFields - AST array of field definitions
 * @param {object} enhancedSchema - AST of the document
 */

function addOrderByInputType(
  OrderByInputTypeName,
  orderByInputFields,
  enhancedSchema
) {
  // add "input <TypeName>OrderBy" type
  enhancedSchema.definitions.push(
    buildTypeDefinition(
      OrderByInputTypeName,
      orderByInputFields,
      INPUT_OBJECT_TYPE_DEFINITION
    )
  );
}

/**
 * adds the query arguments "filter" and "orderBy" to the "extend Query" type
 * @private
 * @param {object} enhancedSchema - AST document definition, to be enhanced
 * @param {array} queryArguments - dictionary of queryArguments to be added
 */

function addQueryArguments(enhancedSchema, queryArguments) {
  // add the arguments to the Query:
  // e.g.: tweets(filter: TweetFilter, orderBy: [TweetOrderBy!])
  enhancedSchema.definitions
    .filter(
      def =>
        def.kind === TYPE_EXTENSION_DEFINITION &&
        def.definition.kind === OBJECT_TYPE_DEFINITION &&
        def.definition.name.value === QUERY
    )
    .forEach(({ definition }) => {
      const { fields, name } = definition;

      if (fields && name) {
        // find the right fields
        fields
          // only in field definition, which are a list, such as 'users'
          .filter(
            field =>
              field.type.kind === LIST_TYPE && field.kind === FIELD_DEFINITION
          )
          .forEach(field => {
            // as arguments is a reserved field, we need to rename to args
            const args = field.arguments;

            const queryArgument = queryArguments[field.name.value];
            // if we have arguments to add for this type e.g. 'users'
            if (queryArgument && field.name.kind === NAME && args) {
              // add "skip" query argument
              args.push(buildArgument('skip', 'Int'));

              // add "filter" query argument
              args.push(buildArgument('filter', queryArgument.filterName));

              // add "orderBy" query argument
              args.push(buildArgument('orderBy', queryArgument.orderByName));
            }
          });
      }
    });
}

/**
 * adds the query arguments "filter" and "orderBy" to the type paginated fields
 * @private
 * @param {object} enhancedSchema - AST document definition, to be enhanced
 * @param {array} queryArguments - dictionary of queryArguments to be added
 */

function addPaginatedFieldsArguments(enhancedSchema) {
  // add the arguments to other paginated fields
  // e.g.: tweets(filter: TweetFilter, orderBy: [TweetOrderBy!])
  enhancedSchema.definitions
    .filter(def => def.kind === OBJECT_TYPE_DEFINITION)
    .forEach(definition => {
      const { fields, name } = definition;

      if (fields && name) {
        // find the right fields
        fields
          // only in field definition, which are a list, such as 'users'
          .filter(
            field =>
              field.type.kind === LIST_TYPE && field.kind === FIELD_DEFINITION
          )
          .forEach(field => {
            // get its target type
            const targetType = field.type.type.name.value;
            const filterName = `${targetType}Filter`;
            const orderByName = `[${targetType}OrderBy!]`;

            // as arguments is a reserved field, we need to rename to args
            const args = field.arguments;

            // add "skip" query argument
            args.push(buildArgument('skip', 'Int'));

            // add "filter" query argument
            args.push(buildArgument('filter', filterName));

            // add "orderBy" query argument
            args.push(buildArgument('orderBy', orderByName));
          });
      }
    });
}

/**
 * define the combinations of input and output types
 * depending on the later expected values for the functions
 * @private
 * @param {object} field - field of the type
 * @property {string} name - name of the field
 * @property {string} type - type of the field
 * @param {string} TypeName - name of the type
 * @return {array} argumentFields - map of arguments and their types
 */

function prepareArgument({ name, type }, TypeName) {
  switch (type) {
    case STRING:
    case ENUM:
      return [
        { [`${name}`]: type },
        { [`${name}_lt`]: type },
        { [`${name}_lte`]: type },
        { [`${name}_gt`]: type },
        { [`${name}_gte`]: type },
        { [`${name}_eq`]: type },
        { [`${name}_ne`]: type },
        { [`${name}_not`]: `${TypeName}Filter` },
        { [`${name}_all`]: `[${type}!]` },
        { [`${name}_in`]: `[${type}!]` },
        { [`${name}_nin`]: `[${type}!]` },
        { [`${name}_not_in`]: `[${type}!]` },
        { [`${name}_exists`]: BOOLEAN },
        { [`${name}_type`]: BSON_TYPE },
        { [`${name}_regex`]: REGEX },
        { [`${name}_contains`]: STRING },
        { [`${name}_starts_with`]: STRING },
        { [`${name}_ends_with`]: STRING },
        { [`${name}_not_contains`]: STRING },
        { [`${name}_not_starts_with`]: STRING },
        { [`${name}_not_ends_with`]: STRING },
        { [`${name}_contains_ci`]: STRING },
        { [`${name}_starts_with_ci`]: STRING },
        { [`${name}_ends_with_ci`]: STRING },
        { [`${name}_not_contains_ci`]: STRING },
        { [`${name}_not_starts_with_ci`]: STRING },
        { [`${name}_not_ends_with_ci`]: STRING }
      ];

    case INT:
    case FLOAT:
      return [
        { [`${name}`]: type },
        { [`${name}_lt`]: type },
        { [`${name}_lte`]: type },
        { [`${name}_gt`]: type },
        { [`${name}_gte`]: type },
        { [`${name}_eq`]: type },
        { [`${name}_ne`]: type },
        { [`${name}_all`]: `[${type}!]` },
        { [`${name}_in`]: `[${type}!]` },
        { [`${name}_nin`]: `[${type}!]` },
        { [`${name}_not_in`]: `[${type}!]` },
        { [`${name}_exists`]: BOOLEAN },
        { [`${name}_type`]: BSON_TYPE }
      ];

    case BOOLEAN:
    default:
      return [
        { [`${name}`]: type },
        { [`${name}_eq`]: type },
        { [`${name}_ne`]: type },
        { [`${name}_all`]: `[${type}!]` },
        { [`${name}_in`]: `[${type}!]` },
        { [`${name}_nin`]: `[${type}!]` },
        { [`${name}_not_in`]: `[${type}!]` },
        { [`${name}_exists`]: BOOLEAN },
        { [`${name}_type`]: BSON_TYPE }
      ];
  }
}