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