Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 7.1 kB view raw
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});