UnlyEd/ra-data-graphql-prisma

View on GitHub
src/buildGqlQuery.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import {
  ArgumentNode,
  DocumentNode,
  FieldNode,
  IntrospectionField,
  IntrospectionNamedTypeRef,
  IntrospectionObjectType,
  IntrospectionType, ListTypeNode, NamedTypeNode, NonNullTypeNode,
  parse,
  SelectionNode,
  TypeKind,
  VariableDefinitionNode,
} from 'graphql';
import { DELETE, GET_LIST, GET_MANY, GET_MANY_REFERENCE } from 'ra-core';
import { QUERY_TYPES } from 'ra-data-graphql';
import { IntrospectionResult, Resource } from './constants/interfaces';
import getFinalType from './utils/getFinalType';

import * as gqlTypes from './utils/gqlTypes';
import isList from './utils/isList';
import isRequired from './utils/isRequired';

export interface Query {
  name?: string;
  args: IntrospectionField[];
}

export const buildFields = (introspectionResults: IntrospectionResult) => (
  fields: IntrospectionField[],
): FieldNode[] => {
  return fields.reduce(
    (acc: FieldNode[], field: IntrospectionField) => {
      const type: IntrospectionNamedTypeRef = getFinalType(field.type);

      if (type.name.startsWith('_')) {
        return acc;
      }

      if (type.kind !== TypeKind.OBJECT) {
        return [...acc, gqlTypes.field(gqlTypes.name(field.name))];
      }

      const linkedResource = introspectionResults.resources.find(
        (r) => r.type.name === type.name,
      );

      if (linkedResource) {
        return [
          ...acc,
          gqlTypes.field(gqlTypes.name(field.name), {
            selectionSet: gqlTypes.selectionSet([
              gqlTypes.field(gqlTypes.name('id')),
            ]),
          }),
        ];
      }

      const linkedType: IntrospectionType | undefined = introspectionResults.types.find(
        (t) => t.name === type.name,
      );

      if (linkedType) {
        return [
          ...acc,
          gqlTypes.field(gqlTypes.name(field.name), {
            selectionSet: gqlTypes.selectionSet(
              buildFields(introspectionResults)(
                // @ts-ignore
                (linkedType as IntrospectionObjectType).fields,
              ),
            ),
          }),
        ];
      }

      // NOTE: We might have to handle linked types which are not resources but will have to be careful about
      // ending with endless circular dependencies
      return acc;
    },
    [] as FieldNode[],
  );
};

export const getArgType = (arg: IntrospectionField): ListTypeNode | NonNullTypeNode | NamedTypeNode => {
  const type = getFinalType(arg.type);
  const required = isRequired(arg.type);
  const list = isList(arg.type);

  if (list) {
    if (required) {
      return gqlTypes.listType(
        gqlTypes.nonNullType(gqlTypes.namedType(gqlTypes.name(type.name))),
      );
    }
    return gqlTypes.listType(gqlTypes.namedType(gqlTypes.name(type.name)));
  }

  if (required) {
    return gqlTypes.nonNullType(gqlTypes.namedType(gqlTypes.name(type.name)));
  }

  return gqlTypes.namedType(gqlTypes.name(type.name));
};

export const buildArgs = (
  query: Query,
  variables: { [key: string]: any } = {},
) => {
  if (query.args.length === 0) {
    return [];
  }

  const validVariables = Object.keys(variables).filter(
    (k) => typeof variables[k] !== 'undefined',
  );
  return query.args
    .filter((arg) => validVariables.includes(arg.name))
    .reduce(
      (acc: ArgumentNode[], arg) => [
        ...acc,
        gqlTypes.argument(
          gqlTypes.name(arg.name),
          gqlTypes.variable(gqlTypes.name(arg.name)),
        ),
      ],
      [] as ArgumentNode[],
    );
};

export const buildApolloArgs = (
  query: Query,
  variables: { [key: string]: any } = {},
) => {
  if (query.args.length === 0) {
    return [];
  }

  const validVariables = Object.keys(variables).filter(
    (k) => typeof variables[k] !== 'undefined',
  );

  return query.args
    .filter((arg) => validVariables.includes(arg.name))
    .reduce(
      (acc: VariableDefinitionNode[], arg) => [
        ...acc,
        gqlTypes.variableDefinition(
          gqlTypes.variable(gqlTypes.name(arg.name)),
          getArgType(arg),
        ),
      ],
      [] as VariableDefinitionNode[],
    );
};

//TODO: validate fragment against the schema
const buildFieldsFromFragment = (
  fragment: DocumentNode | string,
  resourceName: string,
  fetchType: string,
): SelectionNode[] => {
  let parsedFragment = {};

  if (
    typeof fragment === 'object' &&
    fragment.kind &&
    fragment.kind === 'Document'
  ) {
    parsedFragment = fragment;
  }

  if (typeof fragment === 'string') {
    if (!fragment.startsWith('fragment')) {
      fragment = `fragment tmp on ${resourceName} ${fragment}`;
    }

    try {
      parsedFragment = parse(fragment);
    } catch (e) {
      throw new Error(
        `Invalid fragment given for resource '${resourceName}' and fetchType '${fetchType}' (${
          e.message
        }).`,
      );
    }
  }

  return (parsedFragment as any).definitions[0].selectionSet.selections;
};

export default (introspectionResults: IntrospectionResult) => (
  resource: Resource,
  aorFetchType: string,
  queryType: Query,
  variables: { [key: string]: any },
  fragment: DocumentNode,
): DocumentNode => {
  const { orderBy, skip, first, ...countVariables } = variables;
  const apolloArgs = buildApolloArgs(queryType, variables);
  const args = buildArgs(queryType, variables);
  const countArgs = buildArgs(queryType, countVariables);
  const fields = !!fragment
    ? buildFieldsFromFragment(fragment, resource.type.name, aorFetchType)
    : buildFields(introspectionResults)(
      // @ts-ignore
      (resource.type as IntrospectionObjectType).fields,
    );

  if (
    aorFetchType === GET_LIST ||
    aorFetchType === GET_MANY ||
    aorFetchType === GET_MANY_REFERENCE
  ) {
    return gqlTypes.document([
      gqlTypes.operationDefinition(
        'query',
        gqlTypes.selectionSet([
          gqlTypes.field(gqlTypes.name(queryType.name!), {
            alias: gqlTypes.name('items'),
            arguments: args,
            selectionSet: gqlTypes.selectionSet(fields),
          }),
          gqlTypes.field(gqlTypes.name(`${queryType.name}Connection`), {
            alias: gqlTypes.name('total'),
            arguments: countArgs,
            selectionSet: gqlTypes.selectionSet([
              gqlTypes.field(gqlTypes.name('aggregate'), {
                selectionSet: gqlTypes.selectionSet([
                  gqlTypes.field(gqlTypes.name('count')),
                ]),
              }),
            ]),
          }),
        ]),
        gqlTypes.name(queryType.name!),
        apolloArgs,
      ),
    ]);
  }

  if (aorFetchType === DELETE) {
    return gqlTypes.document([
      gqlTypes.operationDefinition(
        'mutation',
        gqlTypes.selectionSet([
          gqlTypes.field(gqlTypes.name(queryType.name!), {
            alias: gqlTypes.name('data'),
            arguments: args,
            selectionSet: gqlTypes.selectionSet([
              gqlTypes.field(gqlTypes.name('id')),
            ]),
          }),
        ]),
        gqlTypes.name(queryType.name!),
        apolloArgs,
      ),
    ]);
  }

  return gqlTypes.document([
    gqlTypes.operationDefinition(
      QUERY_TYPES.includes(aorFetchType) ? 'query' : 'mutation',
      gqlTypes.selectionSet([
        gqlTypes.field(gqlTypes.name(queryType.name!), {
          alias: gqlTypes.name('data'),
          arguments: args,
          selectionSet: gqlTypes.selectionSet(fields),
        }),
      ]),
      gqlTypes.name(queryType.name!),
      apolloArgs,
    ),
  ]);
};