1export type FileMap = Map<string, File | Blob>;
2
3const seen: Set<any> = new Set();
4const cache: WeakMap<any, any> = new WeakMap();
5
6const stringify = (x: any, includeFiles: boolean): string => {
7 if (x === null || seen.has(x)) {
8 return 'null';
9 } else if (typeof x !== 'object') {
10 return JSON.stringify(x) || '';
11 } else if (x.toJSON) {
12 return stringify(x.toJSON(), includeFiles);
13 } else if (Array.isArray(x)) {
14 let out = '[';
15 for (let i = 0, l = x.length; i < l; i++) {
16 if (out.length > 1) out += ',';
17 out += stringify(x[i], includeFiles) || 'null';
18 }
19 out += ']';
20 return out;
21 } else if (
22 !includeFiles &&
23 ((FileConstructor !== NoopConstructor && x instanceof FileConstructor) ||
24 (BlobConstructor !== NoopConstructor && x instanceof BlobConstructor))
25 ) {
26 return 'null';
27 }
28
29 const keys = Object.keys(x).sort();
30 if (
31 !keys.length &&
32 x.constructor &&
33 Object.getPrototypeOf(x).constructor !== Object.prototype.constructor
34 ) {
35 const key = cache.get(x) || Math.random().toString(36).slice(2);
36 cache.set(x, key);
37 return stringify({ __key: key }, includeFiles);
38 }
39
40 seen.add(x);
41 let out = '{';
42 for (let i = 0, l = keys.length; i < l; i++) {
43 const value = stringify(x[keys[i]], includeFiles);
44 if (value) {
45 if (out.length > 1) out += ',';
46 out += stringify(keys[i], includeFiles) + ':' + value;
47 }
48 }
49
50 seen.delete(x);
51 out += '}';
52 return out;
53};
54
55const extract = (map: FileMap, path: string, x: any): void => {
56 if (x == null || typeof x !== 'object' || x.toJSON || seen.has(x)) {
57 /*noop*/
58 } else if (Array.isArray(x)) {
59 for (let i = 0, l = x.length; i < l; i++)
60 extract(map, `${path}.${i}`, x[i]);
61 } else if (x instanceof FileConstructor || x instanceof BlobConstructor) {
62 map.set(path, x as File | Blob);
63 } else {
64 seen.add(x);
65 for (const key in x) extract(map, `${path}.${key}`, x[key]);
66 }
67};
68
69/** A stable stringifier for GraphQL variables objects.
70 *
71 * @param x - any JSON-like data.
72 * @return A JSON string.
73 *
74 * @remarks
75 * This utility creates a stable JSON string from any passed data,
76 * and protects itself from throwing.
77 *
78 * The JSON string is stable insofar as objects’ keys are sorted,
79 * and instances of non-plain objects are replaced with random keys
80 * replacing their values, which remain stable for the objects’
81 * instance.
82 */
83export const stringifyVariables = (x: any, includeFiles?: boolean): string => {
84 seen.clear();
85 return stringify(x, includeFiles || false);
86};
87
88class NoopConstructor {}
89const FileConstructor = typeof File !== 'undefined' ? File : NoopConstructor;
90const BlobConstructor = typeof Blob !== 'undefined' ? Blob : NoopConstructor;
91
92export const extractFiles = (x: any): FileMap => {
93 const map: FileMap = new Map();
94 if (
95 FileConstructor !== NoopConstructor ||
96 BlobConstructor !== NoopConstructor
97 ) {
98 seen.clear();
99 extract(map, 'variables', x);
100 }
101 return map;
102};