-
-
Notifications
You must be signed in to change notification settings - Fork 454
/
request.ts
202 lines (185 loc) · 6.86 KB
/
request.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import { Kind, parse, print } from '@0no-co/graphql.web';
import type { DocumentNode, DefinitionNode } from './graphql';
import type { HashValue } from './hash';
import { phash } from './hash';
import { stringifyVariables } from './variables';
import type {
DocumentInput,
TypedDocumentNode,
AnyVariables,
GraphQLRequest,
RequestExtensions,
} from '../types';
type PersistedDocumentNode = TypedDocumentNode & {
documentId?: string;
};
/** A `DocumentNode` annotated with its hashed key.
* @internal
*/
export type KeyedDocumentNode = TypedDocumentNode & {
__key: HashValue;
};
const SOURCE_NAME = 'gql';
const GRAPHQL_STRING_RE = /("{3}[\s\S]*"{3}|"(?:\\.|[^"])*")/g;
const REPLACE_CHAR_RE = /(?:#[^\n\r]+)?(?:[\r\n]+|$)/g;
const replaceOutsideStrings = (str: string, idx: number): string =>
idx % 2 === 0 ? str.replace(REPLACE_CHAR_RE, '\n') : str;
/** Sanitizes a GraphQL document string by replacing comments and redundant newlines in it. */
const sanitizeDocument = (node: string): string =>
node.split(GRAPHQL_STRING_RE).map(replaceOutsideStrings).join('').trim();
const prints: Map<DocumentNode | DefinitionNode, string> = new Map<
DocumentNode | DefinitionNode,
string
>();
const docs: Map<HashValue, KeyedDocumentNode> = new Map<
HashValue,
KeyedDocumentNode
>();
/** A cached printing function for GraphQL documents.
*
* @param node - A string of a document or a {@link DocumentNode}
* @returns A normalized printed string of the passed GraphQL document.
*
* @remarks
* This function accepts a GraphQL query string or {@link DocumentNode},
* then prints and sanitizes it. The sanitizer takes care of removing
* comments, which otherwise alter the key of the document although the
* document is otherwise equivalent to another.
*
* When a {@link DocumentNode} is passed to this function, it caches its
* output by modifying the `loc.source.body` property on the GraphQL node.
*/
export const stringifyDocument = (
node: string | DefinitionNode | DocumentNode
): string => {
let printed: string;
if (typeof node === 'string') {
printed = sanitizeDocument(node);
} else if (node.loc && docs.get((node as KeyedDocumentNode).__key) === node) {
printed = node.loc.source.body;
} else {
printed = prints.get(node) || sanitizeDocument(print(node));
prints.set(node, printed);
}
if (typeof node !== 'string' && !node.loc) {
(node as any).loc = {
start: 0,
end: printed.length,
source: {
body: printed,
name: SOURCE_NAME,
locationOffset: { line: 1, column: 1 },
},
};
}
return printed;
};
/** Computes the hash for a document's string using {@link stringifyDocument}'s output.
*
* @param node - A string of a document or a {@link DocumentNode}
* @returns A {@link HashValue}
*
* @privateRemarks
* This function adds the operation name of the document to the hash, since sometimes
* a merged document with multiple operations may be used. Although `urql` requires a
* `DocumentNode` to only contain a single operation, when the cached `loc.source.body`
* of a `DocumentNode` is used, this string may still contain multiple operations and
* the resulting hash should account for only one at a time.
*/
const hashDocument = (
node: string | DefinitionNode | DocumentNode
): HashValue => {
let key: HashValue;
if ((node as PersistedDocumentNode).documentId) {
key = phash((node as PersistedDocumentNode).documentId!);
} else {
key = phash(stringifyDocument(node));
// Add the operation name to the produced hash
if ((node as DocumentNode).definitions) {
const operationName = getOperationName(node as DocumentNode);
if (operationName) key = phash(`\n# ${operationName}`, key);
}
}
return key;
};
/** Returns a canonical version of the passed `DocumentNode` with an added hash key.
*
* @param node - A string of a document or a {@link DocumentNode}
* @returns A {@link KeyedDocumentNode}
*
* @remarks
* `urql` will always avoid unnecessary work, no matter whether a user passes `DocumentNode`s
* or strings of GraphQL documents to its APIs.
*
* This function will return a canonical version of a {@link KeyedDocumentNode} no matter
* which kind of input is passed, avoiding parsing or hashing of passed data as needed.
*/
export const keyDocument = (node: string | DocumentNode): KeyedDocumentNode => {
let key: HashValue;
let query: DocumentNode;
if (typeof node === 'string') {
key = hashDocument(node);
query = docs.get(key) || parse(node, { noLocation: true });
} else {
key = (node as KeyedDocumentNode).__key || hashDocument(node);
query = docs.get(key) || node;
}
// Add location information if it's missing
if (!query.loc) stringifyDocument(query);
(query as KeyedDocumentNode).__key = key;
docs.set(key, query as KeyedDocumentNode);
return query as KeyedDocumentNode;
};
/** Creates a `GraphQLRequest` from the passed parameters.
*
* @param q - A string of a document or a {@link DocumentNode}
* @param variables - A variables object for the defined GraphQL operation.
* @returns A {@link GraphQLRequest}
*
* @remarks
* `createRequest` creates a {@link GraphQLRequest} from the passed parameters,
* while replacing the document as needed with a canonical version of itself,
* to avoid parsing, printing, or hashing the same input multiple times.
*
* If no variables are passed, canonically it'll default to an empty object,
* which is removed from the resulting hash key.
*/
export const createRequest = <
Data = any,
Variables extends AnyVariables = AnyVariables,
>(
_query: DocumentInput<Data, Variables>,
_variables: Variables,
extensions?: RequestExtensions | undefined
): GraphQLRequest<Data, Variables> => {
const variables = _variables || ({} as Variables);
const query = keyDocument(_query);
const printedVars = stringifyVariables(variables, true);
let key = query.__key;
if (printedVars !== '{}') key = phash(printedVars, key);
return { key, query, variables, extensions };
};
/** Returns the name of the `DocumentNode`'s operation, if any.
* @param query - A {@link DocumentNode}
* @returns the operation's name contained within the document, or `undefined`
*/
export const getOperationName = (query: DocumentNode): string | undefined => {
for (let i = 0, l = query.definitions.length; i < l; i++) {
const node = query.definitions[i];
if (node.kind === Kind.OPERATION_DEFINITION) {
return node.name ? node.name.value : undefined;
}
}
};
/** Returns the type of the `DocumentNode`'s operation, if any.
* @param query - A {@link DocumentNode}
* @returns the operation's type contained within the document, or `undefined`
*/
export const getOperationType = (query: DocumentNode): string | undefined => {
for (let i = 0, l = query.definitions.length; i < l; i++) {
const node = query.definitions[i];
if (node.kind === Kind.OPERATION_DEFINITION) {
return node.operation;
}
}
};