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 |
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 |
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 |
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 |
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 |
/**
* 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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);
});
});
});
});