graphql#OperationDefinitionNode TypeScript Examples

The following examples show how to use graphql#OperationDefinitionNode. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: gql.ts    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
export function findSelectionSet(document: DocumentNode): SelectionSetNode {
  const query: DefinitionNode | undefined = document.definitions.find(
    (definition) =>
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'query',
  );

  if (!query) {
    throw new Error(`Can't find "query" operation from query`);
  }

  return (query as OperationDefinitionNode).selectionSet;
}
Example #2
Source File: operation.ts    From apollo-link-sentry with MIT License 6 votes vote down vote up
export function extractDefinition(
  operation: Operation,
): OperationDefinitionNode {
  // We know we always have a single definition, because Apollo validates this before we get here.
  // With more then one query defined, an error like this is thrown and the query is never sent:
  // "react-apollo only supports a query, subscription, or a mutation per HOC. [object Object] had 2 queries, 0 subscriptions and 0 mutations. You can use 'compose' to join multiple operation types to a component"
  return operation.query.definitions.find(
    (q) => q.kind === 'OperationDefinition',
  ) as OperationDefinitionNode;
}
Example #3
Source File: generate-query.ts    From graphql-query-generator with MIT License 6 votes vote down vote up
function getQueryOperationDefinition(
  schema: GraphQLSchema,
  config: InternalConfiguration
): {
  queryDocument: OperationDefinitionNode
  variableValues: { [varName: string]: any }
} {
  const node = schema.getQueryType().astNode
  const {
    selectionSet,
    variableDefinitionsMap,
    variableValues
  } = getSelectionSetAndVars(schema, node, config)

  // Throw error if query would be empty
  if (selectionSet.selections.length === 0) {
    throw new Error(
      `Could not create query - no selection was possible at the root level`
    )
  }

  return {
    queryDocument: {
      kind: Kind.OPERATION_DEFINITION,
      operation: 'query',
      selectionSet,
      variableDefinitions: Object.values(variableDefinitionsMap),
      loc,
      name: getName('RandomQuery')
    },
    variableValues
  }
}
Example #4
Source File: generate-query.ts    From graphql-query-generator with MIT License 6 votes vote down vote up
function getMutationOperationDefinition(
  schema: GraphQLSchema,
  config: InternalConfiguration
): {
  mutationDocument: OperationDefinitionNode
  variableValues: { [varName: string]: any }
} {
  const node = schema.getMutationType().astNode
  const {
    selectionSet,
    variableDefinitionsMap,
    variableValues
  } = getSelectionSetAndVars(schema, node, config)

  // Throw error if mutation would be empty
  if (selectionSet.selections.length === 0) {
    throw new Error(
      `Could not create mutation - no selection was possible at the root level`
    )
  }

  return {
    mutationDocument: {
      kind: Kind.OPERATION_DEFINITION,
      operation: 'mutation',
      selectionSet,
      variableDefinitions: Object.values(variableDefinitionsMap),
      loc,
      name: getName('RandomMutation')
    },
    variableValues
  }
}
Example #5
Source File: index.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
compileOperation(operationDefinition: OperationDefinitionNode): Operation {
    if (!operationDefinition.name) {
      throw new Error('Operations should be named');
    }

    const filePath = filePathForNode(operationDefinition);
    const operationName = operationDefinition.name.value;
    const operationType = operationDefinition.operation;

    const variables = (operationDefinition.variableDefinitions || []).map(node => {
      const name = node.variable.name.value;
      const type = typeFromAST(this.schema, node.type as NonNullTypeNode);
      this.addTypeUsed(getNamedType(type as GraphQLType));
      return { name, type: type as GraphQLNonNull<any> };
    });

    const source = print(operationDefinition);
    const rootType = getOperationRootType(this.schema, operationDefinition) as GraphQLObjectType;
    const selectionSet = this.compileSelectionSet(operationDefinition.selectionSet, rootType);

    this.addTypeUsed(getNamedType((selectionSet.selections[0] as Field).type)); // store result type

    return {
      filePath,
      operationName,
      operationType,
      variables,
      source,
      rootType,
      selectionSet,
    };
  }
Example #6
Source File: graphql.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
export function removeClientDirectives(ast: ASTNode) {
  return visit(ast, {
    Field(node: FieldNode): FieldNode | null {
      if (node.directives && node.directives.find(directive => directive.name.value === 'client')) return null;
      return node;
    },
    OperationDefinition: {
      leave(node: OperationDefinitionNode): OperationDefinitionNode | null {
        if (!node.selectionSet.selections.length) return null;
        return node;
      },
    },
  });
}
Example #7
Source File: graphql.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
// Utility functions extracted from graphql-js

/**
 * Extracts the root type of the operation from the schema.
 */
export function getOperationRootType(schema: GraphQLSchema, operation: OperationDefinitionNode) {
  switch (operation.operation) {
    case 'query':
      return schema.getQueryType();
    case 'mutation':
      const mutationType = schema.getMutationType();
      if (!mutationType) {
        throw new GraphQLError('Schema is not configured for mutations', [operation]);
      }
      return mutationType;
    case 'subscription':
      const subscriptionType = schema.getSubscriptionType();
      if (!subscriptionType) {
        throw new GraphQLError('Schema is not configured for subscriptions', [operation]);
      }
      return subscriptionType;
    default:
      throw new GraphQLError('Can only compile queries, mutations and subscriptions', [operation]);
  }
}
Example #8
Source File: validation.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
export function NoAnonymousQueries(context: ValidationContext) {
  return {
    OperationDefinition(node: OperationDefinitionNode) {
      if (!node.name) {
        context.reportError(new GraphQLError('Apollo does not support anonymous operations', [node]));
      }
      return false;
    },
  };
}
Example #9
Source File: panel.tsx    From storybook-addon-apollo-client with MIT License 6 votes vote down vote up
getOperationName = (mockedResponse: MockedResponse): string => {
  if (mockedResponse.request.operationName) {
    return mockedResponse.request.operationName;
  }

  const operationDefinition = mockedResponse.request.query.definitions.find(
    (definition): definition is OperationDefinitionNode =>
      definition.kind === 'OperationDefinition'
  );

  if (operationDefinition && operationDefinition.name) {
    return operationDefinition.name.value;
  }

  return 'Unnamed';
}
Example #10
Source File: gqlTypes.ts    From ra-data-prisma with MIT License 6 votes vote down vote up
operationDefinition = (
  operation: OperationTypeNode,
  selectionSet: SelectionSetNode,
  name: NameNode,
  variableDefinitions: VariableDefinitionNode[],
): OperationDefinitionNode => ({
  kind: Kind.OPERATION_DEFINITION,
  operation,
  selectionSet,
  name,
  variableDefinitions,
})
Example #11
Source File: no-anonymous-operations.ts    From graphql-eslint with MIT License 5 votes vote down vote up
rule: GraphQLESLintRule = {
  meta: {
    type: 'suggestion',
    hasSuggestions: true,
    docs: {
      category: 'Operations',
      description:
        'Require name for your GraphQL operations. This is useful since most GraphQL client libraries are using the operation name for caching purposes.',
      recommended: true,
      url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
      examples: [
        {
          title: 'Incorrect',
          code: /* GraphQL */ `
            query {
              # ...
            }
          `,
        },
        {
          title: 'Correct',
          code: /* GraphQL */ `
            query user {
              # ...
            }
          `,
        },
      ],
    },
    messages: {
      [RULE_ID]: 'Anonymous GraphQL operations are forbidden. Make sure to name your {{ operation }}!',
    },
    schema: [],
  },
  create(context) {
    return {
      'OperationDefinition[name=undefined]'(node: GraphQLESTreeNode<OperationDefinitionNode>) {
        const [firstSelection] = node.selectionSet.selections;
        const suggestedName =
          firstSelection.kind === Kind.FIELD ? (firstSelection.alias || firstSelection.name).value : node.operation;

        context.report({
          loc: getLocation(node.loc.start, node.operation),
          messageId: RULE_ID,
          data: {
            operation: node.operation,
          },
          suggest: [
            {
              desc: `Rename to \`${suggestedName}\``,
              fix(fixer) {
                const sourceCode = context.getSourceCode();
                const hasQueryKeyword =
                  sourceCode.getText({ range: [node.range[0], node.range[0] + 1] } as any) !== '{';

                return fixer.insertTextAfterRange(
                  [node.range[0], node.range[0] + (hasQueryKeyword ? node.operation.length : 0)],
                  `${hasQueryKeyword ? '' : 'query'} ${suggestedName}${hasQueryKeyword ? '' : ' '}`
                );
              },
            },
          ],
        });
      },
    };
  },
}
Example #12
Source File: generate-query.test.ts    From graphql-query-generator with MIT License 5 votes vote down vote up
function getOperationDefinition(doc: DocumentNode): OperationDefinitionNode {
  const opDefs: DefinitionNode[] = doc.definitions.filter((def) => {
    return def.kind === 'OperationDefinition'
  })
  return opDefs[0] as OperationDefinitionNode
}
Example #13
Source File: alphabetize.ts    From graphql-eslint with MIT License 4 votes vote down vote up
rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
  meta: {
    type: 'suggestion',
    fixable: 'code',
    docs: {
      category: ['Schema', 'Operations'],
      description:
        'Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.',
      url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
      examples: [
        {
          title: 'Incorrect',
          usage: [{ fields: [Kind.OBJECT_TYPE_DEFINITION] }],
          code: /* GraphQL */ `
            type User {
              password: String
              firstName: String! # should be before "password"
              age: Int # should be before "firstName"
              lastName: String!
            }
          `,
        },
        {
          title: 'Correct',
          usage: [{ fields: [Kind.OBJECT_TYPE_DEFINITION] }],
          code: /* GraphQL */ `
            type User {
              age: Int
              firstName: String!
              lastName: String!
              password: String
            }
          `,
        },
        {
          title: 'Incorrect',
          usage: [{ values: [Kind.ENUM_TYPE_DEFINITION] }],
          code: /* GraphQL */ `
            enum Role {
              SUPER_ADMIN
              ADMIN # should be before "SUPER_ADMIN"
              USER
              GOD # should be before "USER"
            }
          `,
        },
        {
          title: 'Correct',
          usage: [{ values: [Kind.ENUM_TYPE_DEFINITION] }],
          code: /* GraphQL */ `
            enum Role {
              ADMIN
              GOD
              SUPER_ADMIN
              USER
            }
          `,
        },
        {
          title: 'Incorrect',
          usage: [{ selections: [Kind.OPERATION_DEFINITION] }],
          code: /* GraphQL */ `
            query {
              me {
                firstName
                lastName
                email # should be before "lastName"
              }
            }
          `,
        },
        {
          title: 'Correct',
          usage: [{ selections: [Kind.OPERATION_DEFINITION] }],
          code: /* GraphQL */ `
            query {
              me {
                email
                firstName
                lastName
              }
            }
          `,
        },
      ],
      configOptions: {
        schema: [
          {
            fields: fieldsEnum,
            values: valuesEnum,
            arguments: argumentsEnum,
            // TODO: add in graphql-eslint v4
            // definitions: true,
          },
        ],
        operations: [
          {
            selections: selectionsEnum,
            variables: variablesEnum,
            arguments: [Kind.FIELD, Kind.DIRECTIVE],
          },
        ],
      },
    },
    messages: {
      [RULE_ID]: '`{{ currName }}` should be before {{ prevName }}.',
    },
    schema: {
      type: 'array',
      minItems: 1,
      maxItems: 1,
      items: {
        type: 'object',
        additionalProperties: false,
        minProperties: 1,
        properties: {
          fields: {
            ...ARRAY_DEFAULT_OPTIONS,
            items: {
              enum: fieldsEnum,
            },
            description: 'Fields of `type`, `interface`, and `input`.',
          },
          values: {
            ...ARRAY_DEFAULT_OPTIONS,
            items: {
              enum: valuesEnum,
            },
            description: 'Values of `enum`.',
          },
          selections: {
            ...ARRAY_DEFAULT_OPTIONS,
            items: {
              enum: selectionsEnum,
            },
            description: 'Selections of `fragment` and operations `query`, `mutation` and `subscription`.',
          },
          variables: {
            ...ARRAY_DEFAULT_OPTIONS,
            items: {
              enum: variablesEnum,
            },
            description: 'Variables of operations `query`, `mutation` and `subscription`.',
          },
          arguments: {
            ...ARRAY_DEFAULT_OPTIONS,
            items: {
              enum: argumentsEnum,
            },
            description: 'Arguments of fields and directives.',
          },
          definitions: {
            type: 'boolean',
            description: 'Definitions – `type`, `interface`, `enum`, `scalar`, `input`, `union` and `directive`.',
            default: false,
          },
        },
      },
    },
  },
  create(context) {
    const sourceCode = context.getSourceCode();

    function isNodeAndCommentOnSameLine(node: { loc: SourceLocation }, comment: Comment): boolean {
      return node.loc.end.line === comment.loc.start.line;
    }

    function getBeforeComments(node): Comment[] {
      const commentsBefore = sourceCode.getCommentsBefore(node);
      if (commentsBefore.length === 0) {
        return [];
      }
      const tokenBefore = sourceCode.getTokenBefore(node);
      if (tokenBefore) {
        return commentsBefore.filter(comment => !isNodeAndCommentOnSameLine(tokenBefore, comment));
      }
      const filteredComments = [];
      const nodeLine = node.loc.start.line;
      // Break on comment that not attached to node
      for (let i = commentsBefore.length - 1; i >= 0; i -= 1) {
        const comment = commentsBefore[i];
        if (nodeLine - comment.loc.start.line - filteredComments.length > 1) {
          break;
        }
        filteredComments.unshift(comment);
      }
      return filteredComments;
    }

    function getRangeWithComments(node): AST.Range {
      if (node.kind === Kind.VARIABLE) {
        node = node.parent;
      }
      const [firstBeforeComment] = getBeforeComments(node);
      const [firstAfterComment] = sourceCode.getCommentsAfter(node);
      const from = firstBeforeComment || node;
      const to = firstAfterComment && isNodeAndCommentOnSameLine(node, firstAfterComment) ? firstAfterComment : node;
      return [from.range[0], to.range[1]];
    }

    function checkNodes(nodes: GraphQLESTreeNode<ASTNode>[]) {
      // Starts from 1, ignore nodes.length <= 1
      for (let i = 1; i < nodes.length; i += 1) {
        const currNode = nodes[i];
        const currName = 'name' in currNode && currNode.name?.value;
        if (!currName) {
          // we don't move unnamed current nodes
          continue;
        }

        const prevNode = nodes[i - 1];
        const prevName = 'name' in prevNode && prevNode.name?.value;
        if (prevName) {
          // Compare with lexicographic order
          const compareResult = prevName.localeCompare(currName);
          const shouldSort = compareResult === 1;
          if (!shouldSort) {
            const isSameName = compareResult === 0;
            if (!isSameName || !prevNode.kind.endsWith('Extension') || currNode.kind.endsWith('Extension')) {
              continue;
            }
          }
        }

        context.report({
          node: currNode.name,
          messageId: RULE_ID,
          data: {
            currName,
            prevName: prevName ? `\`${prevName}\`` : lowerCase(prevNode.kind),
          },
          *fix(fixer) {
            const prevRange = getRangeWithComments(prevNode);
            const currRange = getRangeWithComments(currNode);
            yield fixer.replaceTextRange(prevRange, sourceCode.getText({ range: currRange } as any));
            yield fixer.replaceTextRange(currRange, sourceCode.getText({ range: prevRange } as any));
          },
        });
      }
    }

    const opts = context.options[0];
    const fields = new Set(opts.fields ?? []);
    const listeners: GraphQLESLintRuleListener = {};

    const kinds = [
      fields.has(Kind.OBJECT_TYPE_DEFINITION) && [Kind.OBJECT_TYPE_DEFINITION, Kind.OBJECT_TYPE_EXTENSION],
      fields.has(Kind.INTERFACE_TYPE_DEFINITION) && [Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION],
      fields.has(Kind.INPUT_OBJECT_TYPE_DEFINITION) && [
        Kind.INPUT_OBJECT_TYPE_DEFINITION,
        Kind.INPUT_OBJECT_TYPE_EXTENSION,
      ],
    ]
      .filter(Boolean)
      .flat();

    const fieldsSelector = kinds.join(',');

    const hasEnumValues = opts.values?.[0] === Kind.ENUM_TYPE_DEFINITION;
    const selectionsSelector = opts.selections?.join(',');
    const hasVariables = opts.variables?.[0] === Kind.OPERATION_DEFINITION;
    const argumentsSelector = opts.arguments?.join(',');

    if (fieldsSelector) {
      listeners[fieldsSelector] = (
        node: GraphQLESTreeNode<
          | ObjectTypeDefinitionNode
          | ObjectTypeExtensionNode
          | InterfaceTypeDefinitionNode
          | InterfaceTypeExtensionNode
          | InputObjectTypeDefinitionNode
          | InputObjectTypeExtensionNode
        >
      ) => {
        checkNodes(node.fields);
      };
    }

    if (hasEnumValues) {
      const enumValuesSelector = [Kind.ENUM_TYPE_DEFINITION, Kind.ENUM_TYPE_EXTENSION].join(',');
      listeners[enumValuesSelector] = (node: GraphQLESTreeNode<EnumTypeDefinitionNode | EnumTypeExtensionNode>) => {
        checkNodes(node.values);
      };
    }

    if (selectionsSelector) {
      listeners[`:matches(${selectionsSelector}) SelectionSet`] = (node: GraphQLESTreeNode<SelectionSetNode>) => {
        checkNodes(
          node.selections.map(selection =>
            // sort by alias is field is renamed
            'alias' in selection && selection.alias ? ({ name: selection.alias } as any) : selection
          )
        );
      };
    }

    if (hasVariables) {
      listeners.OperationDefinition = (node: GraphQLESTreeNode<OperationDefinitionNode>) => {
        checkNodes(node.variableDefinitions.map(varDef => varDef.variable));
      };
    }

    if (argumentsSelector) {
      listeners[argumentsSelector] = (
        node: GraphQLESTreeNode<FieldDefinitionNode | FieldNode | DirectiveDefinitionNode | DirectiveNode>
      ) => {
        checkNodes(node.arguments);
      };
    }

    if (opts.definitions) {
      listeners.Document = node => {
        checkNodes(node.definitions);
      };
    }

    return listeners;
  },
}
Example #14
Source File: match-document-filename.ts    From graphql-eslint with MIT License 4 votes vote down vote up
rule: GraphQLESLintRule<[MatchDocumentFilenameRuleConfig]> = {
  meta: {
    type: 'suggestion',
    docs: {
      category: 'Operations',
      description: 'This rule allows you to enforce that the file name should match the operation name.',
      url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/match-document-filename.md',
      examples: [
        {
          title: 'Correct',
          usage: [{ fileExtension: '.gql' }],
          code: /* GraphQL */ `
            # user.gql
            type User {
              id: ID!
            }
          `,
        },
        {
          title: 'Correct',
          usage: [{ query: 'snake_case' }],
          code: /* GraphQL */ `
            # user_by_id.gql
            query UserById {
              userById(id: 5) {
                id
                name
                fullName
              }
            }
          `,
        },
        {
          title: 'Correct',
          usage: [{ fragment: { style: 'kebab-case', suffix: '.fragment' } }],
          code: /* GraphQL */ `
            # user-fields.fragment.gql
            fragment user_fields on User {
              id
              email
            }
          `,
        },
        {
          title: 'Correct',
          usage: [{ mutation: { style: 'PascalCase', suffix: 'Mutation' } }],
          code: /* GraphQL */ `
            # DeleteUserMutation.gql
            mutation DELETE_USER {
              deleteUser(id: 5)
            }
          `,
        },
        {
          title: 'Incorrect',
          usage: [{ fileExtension: '.graphql' }],
          code: /* GraphQL */ `
            # post.gql
            type Post {
              id: ID!
            }
          `,
        },
        {
          title: 'Incorrect',
          usage: [{ query: 'PascalCase' }],
          code: /* GraphQL */ `
            # user-by-id.gql
            query UserById {
              userById(id: 5) {
                id
                name
                fullName
              }
            }
          `,
        },
      ],
      configOptions: [
        {
          query: 'kebab-case',
          mutation: 'kebab-case',
          subscription: 'kebab-case',
          fragment: 'kebab-case',
        },
      ],
    },
    messages: {
      [MATCH_EXTENSION]: 'File extension "{{ fileExtension }}" don\'t match extension "{{ expectedFileExtension }}"',
      [MATCH_STYLE]: 'Unexpected filename "{{ filename }}". Rename it to "{{ expectedFilename }}"',
    },
    schema: {
      definitions: {
        asString: {
          enum: CASE_STYLES,
          description: `One of: ${CASE_STYLES.map(t => `\`${t}\``).join(', ')}`,
        },
        asObject: {
          type: 'object',
          additionalProperties: false,
          minProperties: 1,
          properties: {
            style: { enum: CASE_STYLES },
            suffix: { type: 'string' },
          },
        },
      },
      type: 'array',
      minItems: 1,
      maxItems: 1,
      items: {
        type: 'object',
        additionalProperties: false,
        minProperties: 1,
        properties: {
          fileExtension: { enum: ACCEPTED_EXTENSIONS },
          query: schemaOption,
          mutation: schemaOption,
          subscription: schemaOption,
          fragment: schemaOption,
        },
      },
    },
  },
  create(context) {
    const options: MatchDocumentFilenameRuleConfig = context.options[0] || {
      fileExtension: null,
    };
    const filePath = context.getFilename();
    const isVirtualFile = !existsSync(filePath);

    if (process.env.NODE_ENV !== 'test' && isVirtualFile) {
      // Skip validation for code files
      return {};
    }

    const fileExtension = extname(filePath);
    const filename = basename(filePath, fileExtension);

    return {
      Document(documentNode) {
        if (options.fileExtension && options.fileExtension !== fileExtension) {
          context.report({
            loc: REPORT_ON_FIRST_CHARACTER,
            messageId: MATCH_EXTENSION,
            data: {
              fileExtension,
              expectedFileExtension: options.fileExtension,
            },
          });
        }

        const firstOperation = documentNode.definitions.find(
          n => n.kind === Kind.OPERATION_DEFINITION
        ) as GraphQLESTreeNode<OperationDefinitionNode>;
        const firstFragment = documentNode.definitions.find(
          n => n.kind === Kind.FRAGMENT_DEFINITION
        ) as GraphQLESTreeNode<FragmentDefinitionNode>;

        const node = firstOperation || firstFragment;

        if (!node) {
          return;
        }
        const docName = node.name?.value;

        if (!docName) {
          return;
        }
        const docType = 'operation' in node ? node.operation : 'fragment';

        let option = options[docType];
        if (!option) {
          // Config not provided
          return;
        }

        if (typeof option === 'string') {
          option = { style: option } as PropertySchema;
        }
        const expectedExtension = options.fileExtension || fileExtension;
        let expectedFilename: string;
        if (option.style) {
          expectedFilename = option.style === 'matchDocumentStyle' ? docName : convertCase(option.style, docName);
        } else {
          expectedFilename = filename;
        }
        expectedFilename += (option.suffix || '') + expectedExtension;
        const filenameWithExtension = filename + expectedExtension;

        if (expectedFilename !== filenameWithExtension) {
          context.report({
            loc: REPORT_ON_FIRST_CHARACTER,
            messageId: MATCH_STYLE,
            data: {
              expectedFilename,
              filename: filenameWithExtension,
            },
          });
        }
      },
    };
  },
}
Example #15
Source File: sibling-operations.ts    From graphql-eslint with MIT License 4 votes vote down vote up
export function getSiblingOperations(projectForFile: GraphQLProjectConfig): SiblingOperations {
  const siblings = getSiblings(projectForFile);

  if (siblings.length === 0) {
    let printed = false;

    const noopWarn = () => {
      if (!printed) {
        logger.warn(
          'getSiblingOperations was called without any operations. Make sure to set "parserOptions.operations" to make this feature available!'
        );
        printed = true;
      }
      return [];
    };

    return {
      available: false,
      getFragment: noopWarn,
      getFragments: noopWarn,
      getFragmentByType: noopWarn,
      getFragmentsInUse: noopWarn,
      getOperation: noopWarn,
      getOperations: noopWarn,
      getOperationByType: noopWarn,
    };
  }

  // Since the siblings array is cached, we can use it as cache key.
  // We should get the same array reference each time we get
  // to this point for the same graphql project
  if (siblingOperationsCache.has(siblings)) {
    return siblingOperationsCache.get(siblings);
  }

  let fragmentsCache: FragmentSource[] | null = null;

  const getFragments = (): FragmentSource[] => {
    if (fragmentsCache === null) {
      const result: FragmentSource[] = [];

      for (const source of siblings) {
        for (const definition of source.document.definitions) {
          if (definition.kind === Kind.FRAGMENT_DEFINITION) {
            result.push({
              filePath: source.location,
              document: definition,
            });
          }
        }
      }
      fragmentsCache = result;
    }
    return fragmentsCache;
  };

  let cachedOperations: OperationSource[] | null = null;

  const getOperations = (): OperationSource[] => {
    if (cachedOperations === null) {
      const result: OperationSource[] = [];

      for (const source of siblings) {
        for (const definition of source.document.definitions) {
          if (definition.kind === Kind.OPERATION_DEFINITION) {
            result.push({
              filePath: source.location,
              document: definition,
            });
          }
        }
      }
      cachedOperations = result;
    }
    return cachedOperations;
  };

  const getFragment = (name: string) => getFragments().filter(f => f.document.name?.value === name);

  const collectFragments = (
    selectable: SelectionSetNode | OperationDefinitionNode | FragmentDefinitionNode,
    recursive,
    collected = new Map<string, FragmentDefinitionNode>()
  ) => {
    visit(selectable, {
      FragmentSpread(spread) {
        const fragmentName = spread.name.value;
        const [fragment] = getFragment(fragmentName);

        if (!fragment) {
          logger.warn(
            `Unable to locate fragment named "${fragmentName}", please make sure it's loaded using "parserOptions.operations"`
          );
          return;
        }
        if (!collected.has(fragmentName)) {
          collected.set(fragmentName, fragment.document);
          if (recursive) {
            collectFragments(fragment.document, recursive, collected);
          }
        }
      },
    });
    return collected;
  };

  const siblingOperations: SiblingOperations = {
    available: true,
    getFragment,
    getFragments,
    getFragmentByType: typeName => getFragments().filter(f => f.document.typeCondition?.name?.value === typeName),
    getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
    getOperation: name => getOperations().filter(o => o.document.name?.value === name),
    getOperations,
    getOperationByType: type => getOperations().filter(o => o.document.operation === type),
  };

  siblingOperationsCache.set(siblings, siblingOperations);
  return siblingOperations;
}
Example #16
Source File: cache.spec.ts    From graphql-mesh with MIT License 4 votes vote down vote up
describe('cache', () => {
  let schema: GraphQLSchema;
  let cache: KeyValueCache;
  let pubsub: MeshPubSub;
  const baseDir: string = undefined;

  beforeEach(() => {
    const baseSchema = buildSchema(/* GraphQL */ `
      type Query {
        user(id: ID!): User
        users(filter: SearchUsersInput): [User!]!
      }

      type Mutation {
        updateUser(userId: ID!, name: String!): User
        deleteUser(userIdToDelete: ID!): Boolean
      }

      input SearchUsersInput {
        username: String
        email: String
        name: String
        age: String
      }

      type User {
        id: ID!
        username: String!
        email: String!
        profile: Profile!
        friend(id: ID!): User
      }

      type Profile {
        name: String!
        age: Int!
      }
    `);

    schema = addResolversToSchema({
      schema: baseSchema,
      resolvers: spies,
    });

    cache = new LocalforageCache();
    pubsub = new PubSub();

    spies.Query.user.mockClear();
    spies.Query.users.mockClear();
  });

  describe('Resolvers Composition', () => {
    it('should replace resolvers correctly with a specific type and field', async () => {
      expect(schema.getQueryType()?.getFields().user.resolve.name).toBe(spies.Query.user.bind(null).name);

      const transform = new CacheTransform({
        apiName: 'test',
        importFn,
        cache,
        config: [
          {
            field: 'Query.user',
          },
        ],
        pubsub,
        baseDir,
      });
      const modifiedSchema = transform.transformSchema(schema);

      expect(modifiedSchema!.getQueryType()?.getFields().user.resolve.name).not.toBe(spies.Query.user.bind(null).name);
      expect(modifiedSchema!.getQueryType()?.getFields().users.resolve.name).toBe(spies.Query.users.bind(null).name);
    });

    it('should replace resolvers correctly with a wildcard', async () => {
      expect(schema.getQueryType()?.getFields().user.resolve.name).toBe(spies.Query.user.bind(null).name);
      expect(schema.getQueryType()?.getFields().users.resolve.name).toBe(spies.Query.users.bind(null).name);

      const transform = new CacheTransform({
        apiName: 'test',
        importFn,
        cache,
        config: [
          {
            field: 'Query.*',
          },
        ],
        pubsub,
        baseDir,
      });

      const modifiedSchema = transform.transformSchema(schema);

      expect(modifiedSchema!.getQueryType()?.getFields().user.resolve.name).not.toBe(spies.Query.user.bind(null).name);
      expect(modifiedSchema!.getQueryType()?.getFields().users.resolve.name).not.toBe(
        spies.Query.users.bind(null).name
      );
    });
  });

  describe('Cache Wrapper', () => {
    const checkCache = async (config: YamlConfig.CacheTransformConfig[], cacheKeyToCheck?: string) => {
      const transform = new CacheTransform({
        apiName: 'test',
        importFn,
        cache,
        config,
        pubsub,
        baseDir,
      });

      const modifiedSchema = transform.transformSchema(schema);

      const executeOptions = {
        schema: modifiedSchema!,
        document: parse(/* GraphQL */ `
          query user {
            user(id: 1) {
              id
              username
            }
          }
        `),
        contextValue: {},
      };

      const queryType = schema.getType('Query') as GraphQLObjectType;
      const queryFields = queryType.getFields();
      const operation = executeOptions.document.definitions[0] as OperationDefinitionNode;
      const cacheKey =
        cacheKeyToCheck ||
        computeCacheKey({
          keyStr: undefined,
          args: { id: '1' },
          info: {
            fieldName: 'user',
            parentType: queryType,
            returnType: queryFields.user.type,
            schema,
            fieldNodes: operation.selectionSet.selections as FieldNode[],
            fragments: {},
            rootValue: {},
            operation,
            variableValues: {},
            path: {
              prev: null,
              key: 'user',
            },
          } as any,
        });

      // No data in cache before calling it
      expect(await cache.get(cacheKey)).not.toBeDefined();
      // Run it for the first time
      await execute(executeOptions);
      // Original resolver should now be called
      expect(spies.Query.user.mock.calls.length).toBe(1);
      // Data should be stored in cache
      const data: any = await cache.get(cacheKey);
      const mockData = MOCK_DATA[0];
      expect(data.id).toBe(mockData.id);
      expect(data.username).toBe(mockData.username);
      // Running it again
      await execute(executeOptions);
      // No new calls to the original resolver
      expect(spies.Query.user.mock.calls.length).toBe(1);

      return {
        cache,
        executeAgain: () => execute(executeOptions),
        executeDocument: (operation: DocumentNode, variables: Record<string, any> = {}) =>
          execute({
            schema: modifiedSchema,
            document: operation,
            variableValues: variables,
          }),
      };
    };

    it('Should wrap resolver correctly with caching - without cacheKey', async () => {
      await checkCache([
        {
          field: 'Query.user',
        },
      ]);
    });

    it('Should wrap resolver correctly with caching with custom key', async () => {
      const cacheKey = `customUser`;

      await checkCache(
        [
          {
            field: 'Query.user',
            cacheKey,
          },
        ],
        cacheKey
      );
    });

    it('Should wrap resolver correctly with caching with custom key', async () => {
      const cacheKey = `customUser`;

      await checkCache(
        [
          {
            field: 'Query.user',
            cacheKey,
          },
        ],
        cacheKey
      );
    });

    it('Should clear cache correctly when TTL is set', async () => {
      const key = 'user-1';
      const { cache, executeAgain } = await checkCache(
        [
          {
            field: 'Query.user',
            cacheKey: key,
            invalidate: {
              ttl: 1,
            },
          },
        ],
        key
      );

      expect(await cache.get(key)).toBeDefined();
      await wait(1.1);
      expect(await cache.get(key)).not.toBeDefined();
      await executeAgain();
      expect(await cache.get(key)).toBeDefined();
    });

    it('Should wrap resolver correctly with caching with custom calculated key - and ensure calling resovler again when key is different', async () => {
      const { cache, executeDocument } = await checkCache(
        [
          {
            field: 'Query.user',
            cacheKey: `query-user-{args.id}`,
          },
        ],
        'query-user-1'
      );

      const otherIdQuery = parse(/* GraphQL */ `
        query user {
          user(id: 2) {
            id
          }
        }
      `);

      expect(await cache.get('query-user-2')).not.toBeDefined();
      await executeDocument(otherIdQuery);
      const cachedObj: any = await cache.get('query-user-2');
      const mockObj = MOCK_DATA[1];
      expect(cachedObj.id).toBe(mockObj.id);
      expect(spies.Query.user.mock.calls.length).toBe(2);
      await executeDocument(otherIdQuery);
      expect(spies.Query.user.mock.calls.length).toBe(2);
    });

    it('Should work correctly with argsHash', async () => {
      const expectedHash = `query-user-${hashObject({ id: '1' })}`;

      await checkCache(
        [
          {
            field: 'Query.user',
            cacheKey: `query-user-{argsHash}`,
          },
        ],
        expectedHash
      );
    });

    it('Should work correctly with hash helper', async () => {
      const expectedHash = hashObject('1');

      await checkCache(
        [
          {
            field: 'Query.user',
            cacheKey: `{args.id|hash}`,
          },
        ],
        expectedHash
      );
    });

    it('Should work correctly with date helper', async () => {
      const expectedHash = `1-${dayjs(new Date()).format(`yyyy-MM-dd`)}`;

      await checkCache(
        [
          {
            field: 'Query.user',
            cacheKey: `{args.id}-{yyyy-MM-dd|date}`,
          },
        ],
        expectedHash
      );
    });
  });

  describe('Opration-based invalidation', () => {
    it('Should invalidate cache when mutation is done based on key', async () => {
      const transform = new CacheTransform({
        apiName: 'test',
        importFn,
        config: [
          {
            field: 'Query.user',
            cacheKey: 'query-user-{args.id}',
            invalidate: {
              effectingOperations: [
                {
                  operation: 'Mutation.updateUser',
                  matchKey: 'query-user-{args.userId}',
                },
                {
                  operation: 'Mutation.deleteUser',
                  matchKey: 'query-user-{args.userIdToDelete}',
                },
              ],
            },
          },
        ],
        cache,
        pubsub,
        baseDir,
      });

      const schemaWithCache = transform.transformSchema(schema);

      const expectedCacheKey = `query-user-1`;

      const executeOptions = {
        schema: schemaWithCache!,
        document: parse(/* GraphQL */ `
          query user {
            user(id: 1) {
              id
              name
            }
          }
        `),
      };

      // Make sure cache works as needed, runs resolvers logic only once
      expect(await cache.get(expectedCacheKey)).not.toBeDefined();
      expect(spies.Query.user.mock.calls.length).toBe(0);
      await execute(executeOptions);
      expect(await cache.get(expectedCacheKey)).toBeDefined();
      expect(spies.Query.user.mock.calls.length).toBe(1);
      await execute(executeOptions);
      expect(spies.Query.user.mock.calls.length).toBe(1);

      // Run effecting mutation
      await execute({
        schema: schemaWithCache!,
        document: parse(/* GraphQL */ `
          mutation updateUser {
            updateUser(userId: 1, name: "test new") {
              id
              name
            }
          }
        `),
      });

      // Cache should be empty now, no calls for resolvers since then
      expect(await cache.get(expectedCacheKey)).not.toBeDefined();
      expect(spies.Query.user.mock.calls.length).toBe(1);

      // Running again query, cache should be filled and resolver should get called again
      await execute(executeOptions);
      expect(await cache.get(expectedCacheKey)).toBeDefined();
      expect(spies.Query.user.mock.calls.length).toBe(2);
    });

    describe('Subfields', () => {
      it('Should cache queries including subfield arguments', async () => {
        const transform = new CacheTransform({
          apiName: 'test',
          importFn,
          config: [{ field: 'Query.user' }],
          cache,
          pubsub,
          baseDir,
        });
        const schemaWithCache = transform.transformSchema(schema);

        // First query should call resolver and fill cache
        const executeOptions1 = {
          schema: schemaWithCache,
          document: parse(/* GraphQL */ `
            query {
              user(id: 1) {
                friend(id: 2) {
                  id
                }
              }
            }
          `),
        };
        const { data: actual1 }: any = await execute(executeOptions1);
        expect(spies.Query.user.mock.calls.length).toBe(1);
        expect(actual1.user.friend.id).toBe('2');

        // Second query should call resolver and also fill cache
        const executeOptions2 = {
          schema: schemaWithCache,
          document: parse(/* GraphQL */ `
            query {
              user(id: 1) {
                friend(id: 3) {
                  id
                }
              }
            }
          `),
        };
        const { data: actual2 }: any = await execute(executeOptions2);
        expect(spies.Query.user.mock.calls.length).toBe(2);
        expect(actual2.user.friend.id).toBe('3');

        // Repeat both queries, no new calls for resolver
        const { data: repeat1 }: any = await execute(executeOptions1);
        const { data: repeat2 }: any = await execute(executeOptions2);
        expect(spies.Query.user.mock.calls.length).toBe(2);
        expect(repeat1.user.friend.id).toBe('2');
        expect(repeat2.user.friend.id).toBe('3');
      });
    });

    describe('Race condition', () => {
      it('should wait for local cache transform to finish writing the entry', async () => {
        const options: MeshTransformOptions<YamlConfig.CacheTransformConfig[]> = {
          apiName: 'test',
          importFn,
          config: [
            {
              field: 'Query.foo',
              cacheKey: 'random',
            },
          ],
          cache,
          pubsub,
          baseDir,
        };

        let callCount = 0;
        const schema = makeExecutableSchema({
          typeDefs: /* GraphQL */ `
            type Query {
              foo: String
            }
          `,
          resolvers: {
            Query: {
              foo: () => new Promise(resolve => setTimeout(() => resolve((callCount++).toString()), 300)),
            },
          },
        });
        const transform = new CacheTransform(options);
        const transformedSchema = transform.transformSchema(schema);
        const query = /* GraphQL */ `
          {
            foo1: foo
            foo2: foo
          }
        `;
        const result = await execute({
          schema: transformedSchema,
          document: parse(query),
        });

        expect(result.data.foo2).toBe(result.data.foo1);
      });

      it('should wait for other cache transform to finish writing the entry when delay >= safe threshold)', async () => {
        let callCount = 0;
        const options: MeshTransformOptions<YamlConfig.CacheTransformConfig[]> = {
          apiName: 'test',
          importFn,
          config: [
            {
              field: 'Query.foo',
              cacheKey: 'random',
            },
          ],
          cache,
          pubsub,
          baseDir,
        };
        function getNewSchema() {
          return makeExecutableSchema({
            typeDefs: /* GraphQL */ `
              type Query {
                foo: String
              }
            `,
            resolvers: {
              Query: {
                foo: async () => {
                  callCount++;
                  await new Promise(resolve => setTimeout(resolve, 300));
                  return 'FOO';
                },
              },
            },
          });
        }
        const transform1 = new CacheTransform(options);
        const transformedSchema1 = transform1.transformSchema(getNewSchema());
        const transform2 = new CacheTransform(options);
        const transformedSchema2 = transform2.transformSchema(getNewSchema());
        const query = /* GraphQL */ `
          {
            foo
          }
        `;

        await execute({
          schema: transformedSchema1,
          document: parse(query),
        });
        await wait(0);
        await execute({
          schema: transformedSchema2,
          document: parse(query),
        });

        expect(callCount).toBe(1);
      });

      it('should fail to wait for other cache transform to finish writing the entry when delay < safe threshold', async () => {
        let callCount = 0;
        const options: MeshTransformOptions<YamlConfig.CacheTransformConfig[]> = {
          apiName: 'test',
          importFn,
          config: [
            {
              field: 'Query.foo',
              cacheKey: 'random',
            },
          ],
          cache,
          pubsub,
          baseDir,
        };
        function getNewSchema() {
          return makeExecutableSchema({
            typeDefs: /* GraphQL */ `
              type Query {
                foo: String
              }
            `,
            resolvers: {
              Query: {
                foo: async () => {
                  callCount++;
                  await new Promise(resolve => setTimeout(resolve, 300));
                  return 'FOO';
                },
              },
            },
          });
        }
        const transform1 = new CacheTransform(options);
        const transformedSchema1 = transform1.transformSchema(getNewSchema());
        const transform2 = new CacheTransform(options);
        const transformedSchema2 = transform2.transformSchema(getNewSchema());
        const query = /* GraphQL */ `
          {
            foo
          }
        `;
        await Promise.all([
          execute({
            schema: transformedSchema1,
            document: parse(query),
          }),
          execute({
            schema: transformedSchema2,
            document: parse(query),
          }),
        ]);
        expect(callCount).toBe(2);
      });
    });
  });
});