graphql#FieldNode TypeScript Examples

The following examples show how to use graphql#FieldNode. 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: CacheResultProcessor.ts    From apollo-cache-policies with Apache License 2.0 6 votes vote down vote up
private getFieldsForQuery(
    options: Cache.ReadOptions<any> | Cache.WriteOptions<any, any>
  ) {
    const operationDefinition = getOperationDefinition(options.query);
    const fragmentMap = createFragmentMap(
      getFragmentDefinitions(options.query)
    );

    return operationDefinition!.selectionSet.selections.reduce<SelectionNode[]>(
      (acc, selection) => {
        if (isField(selection)) {
          acc.push(selection);
          return acc;
        }

        const selections = getFragmentFromSelection(selection, fragmentMap)
          ?.selectionSet?.selections;

        if (selections) {
          acc.push(...selections);
        }

        return acc;
      },
      []
    ) as FieldNode[];
  }
Example #2
Source File: graphcms.ts    From next-right-now-admin with MIT License 6 votes vote down vote up
fieldAliasResolver = (
  field: IntrospectionField,
  fieldName: string,
  acc: FieldNode[],
  introspectionResults: IntrospectionResult,
): string => {
  if (isLocalisedField(fieldName)) {
    return getLocalisedFieldAlias(fieldName);
  }
  return fieldName;
}
Example #3
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 #4
Source File: graphql.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
export function withTypenameFieldAddedWhereNeeded(ast: ASTNode) {
  return visit(ast, {
    enter: {
      SelectionSet(node: SelectionSetNode) {
        return {
          ...node,
          selections: node.selections.filter(
            selection => !(selection.kind === 'Field' && (selection as FieldNode).name.value === '__typename')
          ),
        };
      },
    },
    leave(node: ASTNode) {
      if (!(node.kind === 'Field' || node.kind === 'FragmentDefinition')) return undefined;
      if (!node.selectionSet) return undefined;

      if (true) {
        return {
          ...node,
          selectionSet: {
            ...node.selectionSet,
            selections: [typenameField, ...node.selectionSet.selections],
          },
        };
      } else {
        return undefined;
      }
    },
  });
}
Example #5
Source File: graphql.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
/**
 * Not exactly the same as the executor's definition of getFieldDef, in this
 * statically evaluated environment we do not always have an Object type,
 * and need to handle Interface and Union types.
 */
export function getFieldDef(
  schema: GraphQLSchema,
  parentType: GraphQLCompositeType,
  fieldAST: FieldNode
): GraphQLField<any, any> | undefined {
  const name = fieldAST.name.value;
  if (name === SchemaMetaFieldDef.name && schema.getQueryType() === parentType) {
    return SchemaMetaFieldDef;
  }
  if (name === TypeMetaFieldDef.name && schema.getQueryType() === parentType) {
    return TypeMetaFieldDef;
  }
  if (
    name === TypeNameMetaFieldDef.name &&
    (parentType instanceof GraphQLObjectType || parentType instanceof GraphQLInterfaceType || parentType instanceof GraphQLUnionType)
  ) {
    return TypeNameMetaFieldDef;
  }
  if (parentType instanceof GraphQLObjectType || parentType instanceof GraphQLInterfaceType) {
    return parentType.getFields()[name];
  }

  return undefined;
}
Example #6
Source File: validation.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
export function NoTypenameAlias(context: ValidationContext) {
  return {
    Field(node: FieldNode) {
      const aliasName = node.alias && node.alias.value;
      if (aliasName == '__typename') {
        context.reportError(
          new GraphQLError('Apollo needs to be able to insert __typename when needed, please do not use it as an alias', [node])
        );
      }
    },
  };
}
Example #7
Source File: buildGqlQuery.ts    From ra-data-prisma with MIT License 6 votes vote down vote up
filterFields = (
  fields: FieldNode[],
  fragment: WhiteListFragment | BlackListFragment,
): FieldNode[] => {
  return fields.filter((f) => {
    const match = fragment.fields.includes(f.name.value);
    if (fragment.type === "whitelist") {
      // only include these
      return match;
    } else {
      // blacklist, exclude these
      return !match;
    }
  });
}
Example #8
Source File: wasm.ts    From anchor-web-app with Apache License 2.0 5 votes vote down vote up
export function wasmQueryToFields<T>(queries: WasmQueryInput<T>): FieldNode[] {
  const keys = Object.keys(queries) as Array<keyof T>;

  return keys
    .map((key) => [
      {
        kind: 'Field',
        alias: {
          kind: 'Name',
          value: key,
        },
        name: {
          kind: 'Name',
          value: 'WasmContractsContractAddressStore',
        },
        arguments: [
          {
            kind: 'Argument',
            name: {
              kind: 'Name',
              value: 'ContractAddress',
            },
            value: {
              kind: 'StringValue',
              value: queries[key]['contractAddress'],
            },
          },
          {
            kind: 'Argument',
            name: {
              kind: 'Name',
              value: 'QueryMsg',
            },
            value: {
              kind: 'StringValue',
              value: JSON.stringify(queries[key]['query']),
            },
          },
        ],
        directives: [],
        selectionSet: {
          kind: 'SelectionSet',
          selections: [
            {
              kind: 'Field',
              name: {
                kind: 'Name',
                value: 'Result',
              },
            },
            {
              kind: 'Field',
              name: {
                kind: 'Name',
                value: 'Height',
              },
            },
          ],
        },
      } as FieldNode,
    ])
    .flat();
}
Example #9
Source File: buildGqlQuery.ts    From ra-data-prisma with MIT License 5 votes vote down vote up
buildFields =
  (introspectionResults: IntrospectionResult) =>
  (
    fields: IntrospectionField[],
    maxNestingDepth = 1,
    depth = 0,
  ): FieldNode[] => {
    return fields.reduce((acc: FieldNode[], field) => {
      const type = getFinalType(field.type);

      if (type.name.startsWith("_")) {
        return acc;
      }
      // skip if the field has mandatory args
      if (field.args.some((arg) => arg.type.kind === "NON_NULL")) {
        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 = introspectionResults.types.find(
        (t) => t.name === type.name,
      );

      // linkedtypes  (= nested objects) are currently a bit problematic
      // it is handy to have them, but they be slow to fetch
      // we therefore limit the depth
      if (linkedType && depth < maxNestingDepth) {
        return [
          ...acc,
          gqlTypes.field(gqlTypes.name(field.name), {
            selectionSet: gqlTypes.selectionSet(
              buildFields(introspectionResults)(
                (linkedType as any).fields,
                maxNestingDepth,
                depth + 1,
              ),
            ),
          }),
        ];
      }

      return acc;
    }, [] as FieldNode[]);
  }
Example #10
Source File: gqlTypes.ts    From ra-data-prisma with MIT License 5 votes vote down vote up
field = (
  name: NameNode,
  optionalValues: Partial<FieldNode> = {},
): FieldNode => ({
  kind: Kind.FIELD,
  name,
  ...optionalValues,
})
Example #11
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 #12
Source File: no-deprecated.ts    From graphql-eslint with MIT License 4 votes vote down vote up
rule: GraphQLESLintRule<[], true> = {
  meta: {
    type: 'suggestion',
    hasSuggestions: true,
    docs: {
      category: 'Operations',
      description: 'Enforce that deprecated fields or enum values are not in use by operations.',
      url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
      requiresSchema: true,
      examples: [
        {
          title: 'Incorrect (field)',
          code: /* GraphQL */ `
            # In your schema
            type User {
              id: ID!
              name: String! @deprecated(reason: "old field, please use fullName instead")
              fullName: String!
            }

            # Query
            query user {
              user {
                name # This is deprecated, so you'll get an error
              }
            }
          `,
        },
        {
          title: 'Incorrect (enum value)',
          code: /* GraphQL */ `
            # In your schema
            type Mutation {
              changeSomething(type: SomeType): Boolean!
            }

            enum SomeType {
              NEW
              OLD @deprecated(reason: "old field, please use NEW instead")
            }

            # Mutation
            mutation {
              changeSomething(
                type: OLD # This is deprecated, so you'll get an error
              ) {
                ...
              }
            }
          `,
        },
        {
          title: 'Correct',
          code: /* GraphQL */ `
            # In your schema
            type User {
              id: ID!
              name: String! @deprecated(reason: "old field, please use fullName instead")
              fullName: String!
            }

            # Query
            query user {
              user {
                id
                fullName
              }
            }
          `,
        },
      ],
      recommended: true,
    },
    messages: {
      [RULE_ID]: 'This {{ type }} is marked as deprecated in your GraphQL schema (reason: {{ reason }})',
    },
    schema: [],
  },
  create(context) {
    requireGraphQLSchemaFromContext(RULE_ID, context);

    function report(node: GraphQLESTreeNode<EnumValueNode | FieldNode, true>, reason: string): void {
      const nodeName = node.kind === Kind.ENUM ? node.value : node.name.value;
      const nodeType = node.kind === Kind.ENUM ? 'enum value' : 'field';
      context.report({
        node,
        messageId: RULE_ID,
        data: {
          type: nodeType,
          reason,
        },
        suggest: [
          {
            desc: `Remove \`${nodeName}\` ${nodeType}`,
            fix: fixer => fixer.remove(node as any),
          },
        ],
      });
    }

    return {
      EnumValue(node) {
        const typeInfo = node.typeInfo();
        const reason = typeInfo.enumValue?.deprecationReason;

        if (reason) {
          report(node, reason);
        }
      },
      Field(node) {
        const typeInfo = node.typeInfo();
        const reason = typeInfo.fieldDef?.deprecationReason;

        if (reason) {
          report(node, reason);
        }
      },
    };
  },
}
Example #13
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);
      });
    });
  });
});