1import type {
2 FieldNode,
3 SelectionNode,
4 DefinitionNode,
5 DirectiveNode,
6} from '@0no-co/graphql.web';
7import { Kind } from '@0no-co/graphql.web';
8import type { KeyedDocumentNode } from './request';
9import { keyDocument } from './request';
10import type { FormattedNode, TypedDocumentNode } from '../types';
11
12const formatNode = <
13 T extends SelectionNode | DefinitionNode | TypedDocumentNode<any, any>,
14>(
15 node: T
16): FormattedNode<T> => {
17 if ('definitions' in node) {
18 const definitions: FormattedNode<DefinitionNode>[] = [];
19 for (let i = 0, l = node.definitions.length; i < l; i++) {
20 const newDefinition = formatNode(node.definitions[i]);
21 definitions.push(newDefinition);
22 }
23
24 return { ...node, definitions } as FormattedNode<T>;
25 }
26
27 if ('directives' in node && node.directives && node.directives.length) {
28 const directives: DirectiveNode[] = [];
29 const _directives = {};
30 for (let i = 0, l = node.directives.length; i < l; i++) {
31 const directive = node.directives[i];
32 let name = directive.name.value;
33 if (name[0] !== '_') {
34 directives.push(directive);
35 } else {
36 name = name.slice(1);
37 }
38 _directives[name] = directive;
39 }
40 node = { ...node, directives, _directives };
41 }
42
43 if ('selectionSet' in node) {
44 const selections: FormattedNode<SelectionNode>[] = [];
45 let hasTypename = node.kind === Kind.OPERATION_DEFINITION;
46 if (node.selectionSet) {
47 for (let i = 0, l = node.selectionSet.selections.length; i < l; i++) {
48 const selection = node.selectionSet.selections[i];
49 hasTypename =
50 hasTypename ||
51 (selection.kind === Kind.FIELD &&
52 selection.name.value === '__typename' &&
53 !selection.alias);
54 const newSelection = formatNode(selection);
55 selections.push(newSelection);
56 }
57
58 if (!hasTypename) {
59 selections.push({
60 kind: Kind.FIELD,
61 name: {
62 kind: Kind.NAME,
63 value: '__typename',
64 },
65 _generated: true,
66 } as FormattedNode<FieldNode>);
67 }
68
69 return {
70 ...node,
71 selectionSet: { ...node.selectionSet, selections },
72 } as FormattedNode<T>;
73 }
74 }
75
76 return node as FormattedNode<T>;
77};
78
79const formattedDocs: Map<number, KeyedDocumentNode> = new Map<
80 number,
81 KeyedDocumentNode
82>();
83
84/** Formats a GraphQL document to add `__typename` fields and process client-side directives.
85 *
86 * @param node - a {@link DocumentNode}.
87 * @returns a {@link FormattedDocument}
88 *
89 * @remarks
90 * Cache {@link Exchange | Exchanges} will require typename introspection to
91 * recognize types in a GraphQL response. To retrieve these typenames,
92 * this function is used to add the `__typename` fields to non-root
93 * selection sets of a GraphQL document.
94 *
95 * Additionally, this utility will process directives, filter out client-side
96 * directives starting with an `_` underscore, and place a `_directives` dictionary
97 * on selection nodes.
98 *
99 * This utility also preserves the internally computed key of the
100 * document as created by {@link createRequest} to avoid any
101 * formatting from being duplicated.
102 *
103 * @see {@link https://spec.graphql.org/October2021/#sec-Type-Name-Introspection} for more information
104 * on typename introspection via the `__typename` field.
105 */
106export const formatDocument = <T extends TypedDocumentNode<any, any>>(
107 node: T
108): FormattedNode<T> => {
109 const query = keyDocument(node);
110
111 let result = formattedDocs.get(query.__key);
112 if (!result) {
113 formattedDocs.set(
114 query.__key,
115 (result = formatNode(query) as KeyedDocumentNode)
116 );
117 // Ensure that the hash of the resulting document won't suddenly change
118 // we are marking __key as non-enumerable so when external exchanges use visit
119 // to manipulate a document we won't restore the previous query due to the __key
120 // property.
121 Object.defineProperty(result, '__key', {
122 value: query.__key,
123 enumerable: false,
124 });
125 }
126
127 return result as FormattedNode<T>;
128};