1import type {
2 SelectionNode,
3 DocumentNode,
4 OperationDefinitionNode,
5 FragmentSpreadNode,
6 InlineFragmentNode,
7} from '@0no-co/graphql.web';
8import { valueFromASTUntyped, Kind } from '@0no-co/graphql.web';
9
10import type { FormattedNode } from '@urql/core';
11import { getName, getDirectives } from './node';
12import { invariant } from '../helpers/help';
13import type { Fragments, Variables } from '../types';
14
15function getMainOperation(
16 doc: FormattedNode<DocumentNode>
17): FormattedNode<OperationDefinitionNode>;
18function getMainOperation(doc: DocumentNode): OperationDefinitionNode;
19
20/** Returns the main operation's definition */
21function getMainOperation(doc: DocumentNode): OperationDefinitionNode {
22 for (let i = 0; i < doc.definitions.length; i++) {
23 if (doc.definitions[i].kind === Kind.OPERATION_DEFINITION) {
24 return doc.definitions[i] as FormattedNode<OperationDefinitionNode>;
25 }
26 }
27
28 invariant(
29 false,
30 'Invalid GraphQL document: All GraphQL documents must contain an OperationDefinition' +
31 'node for a query, subscription, or mutation.',
32 1
33 );
34}
35
36export { getMainOperation };
37
38/** Returns a mapping from fragment names to their selections */
39export const getFragments = (doc: FormattedNode<DocumentNode>): Fragments => {
40 const fragments: Fragments = {};
41 for (let i = 0; i < doc.definitions.length; i++) {
42 const node = doc.definitions[i];
43 if (node.kind === Kind.FRAGMENT_DEFINITION) {
44 fragments[getName(node)] = node;
45 }
46 }
47
48 return fragments;
49};
50
51/** Resolves @include and @skip directives to determine whether field is included. */
52export const shouldInclude = (
53 node: FormattedNode<SelectionNode>,
54 vars: Variables
55): boolean => {
56 const directives = getDirectives(node);
57 if (directives.include || directives.skip) {
58 // Finds any @include or @skip directive that forces the node to be skipped
59 for (const name in directives) {
60 const directive = directives[name];
61 if (
62 directive &&
63 (name === 'include' || name === 'skip') &&
64 directive.arguments &&
65 directive.arguments[0] &&
66 getName(directive.arguments[0]) === 'if'
67 ) {
68 // Return whether this directive forces us to skip
69 // `@include(if: false)` or `@skip(if: true)`
70 const value = valueFromASTUntyped(directive.arguments[0].value, vars);
71 return name === 'include' ? !!value : !value;
72 }
73 }
74 }
75 return true;
76};
77
78/** Resolves @defer directive to determine whether a fragment is potentially skipped. */
79export const isDeferred = (
80 node: FormattedNode<FragmentSpreadNode | InlineFragmentNode>,
81 vars: Variables
82): boolean => {
83 const { defer } = getDirectives(node);
84 if (defer) {
85 for (const argument of defer.arguments || []) {
86 if (getName(argument) === 'if') {
87 // Return whether `@defer(if: )` is enabled
88 return !!valueFromASTUntyped(argument.value, vars);
89 }
90 }
91 return true;
92 }
93
94 return false;
95};
96
97/** Resolves @_optional and @_required directive to determine whether the fields in a fragment are conaidered optional. */
98export const isOptional = (
99 node: FormattedNode<FragmentSpreadNode | InlineFragmentNode>
100): boolean | undefined => {
101 const { optional, required } = getDirectives(node);
102 if (required) {
103 return false;
104 }
105
106 if (optional) {
107 return true;
108 }
109
110 return undefined;
111};