1import type {
2 ExecutionResult,
3 Operation,
4 OperationResult,
5 IncrementalPayload,
6} from '../types';
7import { CombinedError } from './error';
8
9/** Converts the `ExecutionResult` received for a given `Operation` to an `OperationResult`.
10 *
11 * @param operation - The {@link Operation} for which the API’s result is for.
12 * @param result - The GraphQL API’s {@link ExecutionResult}.
13 * @param response - Optionally, a raw object representing the API’s result (Typically a {@link Response}).
14 * @returns An {@link OperationResult}.
15 *
16 * @remarks
17 * This utility can be used to create {@link OperationResult | OperationResults} in the shape
18 * that `urql` expects and defines, and should be used rather than creating the results manually.
19 *
20 * @throws
21 * If no data, or errors are contained within the result, or the result is instead an incremental
22 * response containing a `path` property, a “No Content” error is thrown.
23 *
24 * @see {@link ExecutionResult} for the type definition of GraphQL API results.
25 */
26export const makeResult = (
27 operation: Operation,
28 result: ExecutionResult,
29 response?: any
30): OperationResult => {
31 if (
32 !('data' in result) &&
33 (!('errors' in result) || !Array.isArray(result.errors))
34 ) {
35 throw new Error('No Content');
36 }
37
38 const defaultHasNext = operation.kind === 'subscription';
39 return {
40 operation,
41 data: result.data,
42 error: Array.isArray(result.errors)
43 ? new CombinedError({
44 graphQLErrors: result.errors,
45 response,
46 })
47 : undefined,
48 extensions: result.extensions ? { ...result.extensions } : undefined,
49 hasNext: result.hasNext == null ? defaultHasNext : result.hasNext,
50 stale: false,
51 };
52};
53
54const deepMerge = (target: any, source: any): any => {
55 if (typeof target === 'object' && target != null) {
56 if (Array.isArray(target)) {
57 target = [...target];
58 for (let i = 0, l = source.length; i < l; i++)
59 target[i] = deepMerge(target[i], source[i]);
60
61 return target;
62 }
63 if (!target.constructor || target.constructor === Object) {
64 target = { ...target };
65 for (const key in source)
66 target[key] = deepMerge(target[key], source[key]);
67 return target;
68 }
69 }
70 return source;
71};
72
73/** Merges an incrementally delivered `ExecutionResult` into a previous `OperationResult`.
74 *
75 * @param prevResult - The {@link OperationResult} that preceded this result.
76 * @param path - The GraphQL API’s {@link ExecutionResult} that should be patching the `prevResult`.
77 * @param response - Optionally, a raw object representing the API’s result (Typically a {@link Response}).
78 * @returns A new {@link OperationResult} patched with the incremental result.
79 *
80 * @remarks
81 * This utility should be used to merge subsequent {@link ExecutionResult | ExecutionResults} of
82 * incremental responses into a prior {@link OperationResult}.
83 *
84 * When directives like `@defer`, `@stream`, and `@live` are used, GraphQL may deliver new
85 * results that modify previous results. In these cases, it'll set a `path` property to modify
86 * the result it sent last. This utility is built to handle these cases and merge these payloads
87 * into existing {@link OperationResult | OperationResults}.
88 *
89 * @see {@link ExecutionResult} for the type definition of GraphQL API results.
90 */
91export const mergeResultPatch = (
92 prevResult: OperationResult,
93 nextResult: ExecutionResult,
94 response?: any,
95 pending?: ExecutionResult['pending']
96): OperationResult => {
97 let errors = prevResult.error ? prevResult.error.graphQLErrors : [];
98 let hasExtensions =
99 !!prevResult.extensions || !!(nextResult.payload || nextResult).extensions;
100 const extensions = {
101 ...prevResult.extensions,
102 ...(nextResult.payload || nextResult).extensions,
103 };
104
105 let incremental = nextResult.incremental;
106
107 // NOTE: We handle the old version of the incremental delivery payloads as well
108 if ('path' in nextResult) {
109 incremental = [nextResult as IncrementalPayload];
110 }
111
112 const withData = { data: prevResult.data };
113 if (incremental) {
114 for (let i = 0, l = incremental.length; i < l; i++) {
115 const patch = incremental[i];
116 if (Array.isArray(patch.errors)) {
117 errors.push(...(patch.errors as any));
118 }
119
120 if (patch.extensions) {
121 Object.assign(extensions, patch.extensions);
122 hasExtensions = true;
123 }
124
125 let prop: string | number = 'data';
126 let part: Record<string, any> | Array<any> = withData;
127 let path: readonly (string | number)[] = [];
128 if (patch.path) {
129 path = patch.path;
130 } else if (pending) {
131 const res = pending.find(pendingRes => pendingRes.id === patch.id);
132 if (patch.subPath) {
133 path = [...res!.path, ...patch.subPath];
134 } else {
135 path = res!.path;
136 }
137 }
138
139 for (let i = 0, l = path.length; i < l; prop = path[i++]) {
140 part = part[prop] = Array.isArray(part[prop])
141 ? [...part[prop]]
142 : { ...part[prop] };
143 }
144
145 if (patch.items) {
146 const startIndex = +prop >= 0 ? (prop as number) : 0;
147 for (let i = 0, l = patch.items.length; i < l; i++)
148 part[startIndex + i] = deepMerge(
149 part[startIndex + i],
150 patch.items[i]
151 );
152 } else if (patch.data !== undefined) {
153 part[prop] = deepMerge(part[prop], patch.data);
154 }
155 }
156 } else {
157 withData.data = (nextResult.payload || nextResult).data || prevResult.data;
158 errors =
159 (nextResult.errors as any[]) ||
160 (nextResult.payload && nextResult.payload.errors) ||
161 errors;
162 }
163
164 return {
165 operation: prevResult.operation,
166 data: withData.data,
167 error: errors.length
168 ? new CombinedError({ graphQLErrors: errors, response })
169 : undefined,
170 extensions: hasExtensions ? extensions : undefined,
171 hasNext:
172 nextResult.hasNext != null ? nextResult.hasNext : prevResult.hasNext,
173 stale: false,
174 };
175};
176
177/** Creates an `OperationResult` containing a network error for requests that encountered unexpected errors.
178 *
179 * @param operation - The {@link Operation} for which the API’s result is for.
180 * @param error - The network-like error that prevented an API result from being delivered.
181 * @param response - Optionally, a raw object representing the API’s result (Typically a {@link Response}).
182 * @returns An {@link OperationResult} containing only a {@link CombinedError}.
183 *
184 * @remarks
185 * This utility can be used to create {@link OperationResult | OperationResults} in the shape
186 * that `urql` expects and defines, and should be used rather than creating the results manually.
187 * This function should be used for when the {@link CombinedError.networkError} property is
188 * populated and no GraphQL execution actually occurred.
189 */
190export const makeErrorResult = (
191 operation: Operation,
192 error: Error,
193 response?: any
194): OperationResult => ({
195 operation,
196 data: undefined,
197 error: new CombinedError({
198 networkError: error,
199 response,
200 }),
201 extensions: undefined,
202 hasNext: false,
203 stale: false,
204});