Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(core): Fix Incremental Delivery payloads deep merging and root patches (#3124)

Co-authored-by: jdecroock <decroockjovi@gmail.com>

authored by

Phil Pluckthun
jdecroock
and committed by
GitHub
5c783941 02e1fd72

+222 -31
+6
.changeset/hungry-coins-lie.md
··· 1 + --- 2 + '@urql/core': patch 3 + --- 4 + 5 + Fix incremental delivery payloads not merging data correctly, or not handling patches on root 6 + results.
+120
packages/core/src/internal/fetchSource.test.ts
··· 459 459 }, 460 460 }); 461 461 }); 462 + 463 + it('merges deferred results on the root-type', async () => { 464 + fetch.mockResolvedValue({ 465 + status: 200, 466 + headers: { 467 + get() { 468 + return 'text/event-stream'; 469 + }, 470 + }, 471 + body: { 472 + getReader: function () { 473 + let cancelled = false; 474 + const results = [ 475 + { 476 + done: false, 477 + value: Buffer.from( 478 + wrap({ 479 + hasNext: true, 480 + data: { 481 + author: { 482 + id: '1', 483 + __typename: 'Author', 484 + }, 485 + }, 486 + }) 487 + ), 488 + }, 489 + { 490 + done: false, 491 + value: Buffer.from( 492 + wrap({ 493 + incremental: [ 494 + { 495 + path: [], 496 + data: { author: { name: 'Steve' } }, 497 + }, 498 + ], 499 + hasNext: true, 500 + }) 501 + ), 502 + }, 503 + { 504 + done: false, 505 + value: Buffer.from(wrap({ hasNext: false })), 506 + }, 507 + { done: true }, 508 + ]; 509 + let count = 0; 510 + return { 511 + cancel: function () { 512 + cancelled = true; 513 + }, 514 + read: function () { 515 + if (cancelled) throw new Error('No'); 516 + 517 + return Promise.resolve(results[count++]); 518 + }, 519 + }; 520 + }, 521 + }, 522 + }); 523 + 524 + const AuthorFragment = gql` 525 + fragment authorFields on Query { 526 + author { 527 + name 528 + } 529 + } 530 + `; 531 + 532 + const streamedQueryOperation: Operation = makeOperation( 533 + 'query', 534 + { 535 + query: gql` 536 + query { 537 + author { 538 + id 539 + ...authorFields @defer 540 + } 541 + } 542 + 543 + ${AuthorFragment} 544 + `, 545 + variables: {}, 546 + key: 1, 547 + }, 548 + context 549 + ); 550 + 551 + const chunks: OperationResult[] = await pipe( 552 + makeFetchSource(streamedQueryOperation, 'https://test.com/graphql', {}), 553 + scan((prev: OperationResult[], item) => [...prev, item], []), 554 + toPromise 555 + ); 556 + 557 + expect(chunks.length).toEqual(3); 558 + 559 + expect(chunks[0].data).toEqual({ 560 + author: { 561 + id: '1', 562 + __typename: 'Author', 563 + }, 564 + }); 565 + 566 + expect(chunks[1].data).toEqual({ 567 + author: { 568 + id: '1', 569 + name: 'Steve', 570 + __typename: 'Author', 571 + }, 572 + }); 573 + 574 + expect(chunks[2].data).toEqual({ 575 + author: { 576 + id: '1', 577 + name: 'Steve', 578 + __typename: 'Author', 579 + }, 580 + }); 581 + }); 462 582 });
+68
packages/core/src/utils/result.test.ts
··· 163 163 }); 164 164 }); 165 165 166 + it('should apply incremental stream patches deeply', () => { 167 + const prevResult: OperationResult = { 168 + operation: queryOperation, 169 + data: { 170 + __typename: 'Query', 171 + test: [ 172 + { 173 + __typename: 'Test', 174 + }, 175 + ], 176 + }, 177 + stale: false, 178 + hasNext: true, 179 + }; 180 + 181 + const patch = { name: 'Test' }; 182 + 183 + const merged = mergeResultPatch(prevResult, { 184 + incremental: [ 185 + { 186 + items: [patch], 187 + path: ['test', 0], 188 + }, 189 + ], 190 + }); 191 + 192 + expect(merged.data).toStrictEqual({ 193 + __typename: 'Query', 194 + test: [ 195 + { 196 + __typename: 'Test', 197 + name: 'Test', 198 + }, 199 + ], 200 + }); 201 + }); 202 + 166 203 it('should handle null incremental stream patches', () => { 167 204 const prevResult: OperationResult = { 168 205 operation: queryOperation, ··· 188 225 expect(merged.data).toStrictEqual({ 189 226 __typename: 'Query', 190 227 items: [{ __typename: 'Item' }], 228 + }); 229 + }); 230 + 231 + it('should handle root incremental stream patches', () => { 232 + const prevResult: OperationResult = { 233 + operation: queryOperation, 234 + data: { 235 + __typename: 'Query', 236 + item: { 237 + test: true, 238 + }, 239 + }, 240 + stale: false, 241 + hasNext: true, 242 + }; 243 + 244 + const merged = mergeResultPatch(prevResult, { 245 + incremental: [ 246 + { 247 + data: { item: { test2: false } }, 248 + path: [], 249 + }, 250 + ], 251 + }); 252 + 253 + expect(merged.data).toStrictEqual({ 254 + __typename: 'Query', 255 + item: { 256 + test: true, 257 + test2: false, 258 + }, 191 259 }); 192 260 }); 193 261
+28 -13
packages/core/src/utils/result.ts
··· 48 48 }; 49 49 }; 50 50 51 + const deepMerge = (target: any, source: any) => { 52 + if (typeof target === 'object' && target != null) { 53 + if ( 54 + !target.constructor || 55 + target.constructor === Object || 56 + Array.isArray(target) 57 + ) { 58 + target = Array.isArray(target) ? [...target] : { ...target }; 59 + for (const key of Object.keys(source)) 60 + target[key] = deepMerge(target[key], source[key]); 61 + return target; 62 + } 63 + } 64 + return source; 65 + }; 66 + 51 67 /** Merges an incrementally delivered `ExecutionResult` into a previous `OperationResult`. 52 68 * 53 69 * @param prevResult - The {@link OperationResult} that preceded this result. ··· 71 87 nextResult: ExecutionResult, 72 88 response?: any 73 89 ): OperationResult => { 74 - let data: ExecutionResult['data']; 75 90 let errors = prevResult.error ? prevResult.error.graphQLErrors : []; 76 91 let hasExtensions = !!prevResult.extensions || !!nextResult.extensions; 77 92 const extensions = { ...prevResult.extensions, ...nextResult.extensions }; ··· 83 98 incremental = [nextResult as IncrementalPayload]; 84 99 } 85 100 101 + const withData = { data: prevResult.data }; 86 102 if (incremental) { 87 - data = { ...prevResult.data }; 88 103 for (const patch of incremental) { 89 104 if (Array.isArray(patch.errors)) { 90 105 errors.push(...(patch.errors as any)); ··· 95 110 hasExtensions = true; 96 111 } 97 112 98 - let prop: string | number = patch.path[0]; 99 - let part: Record<string, any> | Array<any> = data as object; 100 - for (let i = 1, l = patch.path.length; i < l; prop = patch.path[i++]) { 113 + let prop: string | number = 'data'; 114 + let part: Record<string, any> | Array<any> = withData; 115 + for (let i = 0, l = patch.path.length; i < l; prop = patch.path[i++]) { 101 116 part = part[prop] = Array.isArray(part[prop]) 102 117 ? [...part[prop]] 103 118 : { ...part[prop] }; 104 119 } 105 120 106 - if (Array.isArray(patch.items)) { 121 + if (patch.items) { 107 122 const startIndex = +prop >= 0 ? (prop as number) : 0; 108 123 for (let i = 0, l = patch.items.length; i < l; i++) 109 - part[startIndex + i] = patch.items[i]; 124 + part[startIndex + i] = deepMerge( 125 + part[startIndex + i], 126 + patch.items[i] 127 + ); 110 128 } else if (patch.data !== undefined) { 111 - part[prop] = 112 - part[prop] && patch.data 113 - ? { ...part[prop], ...patch.data } 114 - : patch.data; 129 + part[prop] = deepMerge(part[prop], patch.data); 115 130 } 116 131 } 117 132 } else { 118 - data = nextResult.data || prevResult.data; 133 + withData.data = nextResult.data || prevResult.data; 119 134 errors = (nextResult.errors as any[]) || errors; 120 135 } 121 136 122 137 return { 123 138 operation: prevResult.operation, 124 - data, 139 + data: withData.data, 125 140 error: errors.length 126 141 ? new CombinedError({ graphQLErrors: errors, response }) 127 142 : undefined,
-18
pnpm-lock.yaml
··· 185 185 react-dom: 17.0.2_react@17.0.2 186 186 urql: link:../../packages/react-urql 187 187 188 - exchanges/multipart-fetch: 189 - specifiers: 190 - '@urql/core': '>=4.0.0' 191 - extract-files: ^11.0.0 192 - graphql: ^16.6.0 193 - wonka: ^6.3.0 194 - dependencies: 195 - '@urql/core': link:../../packages/core 196 - extract-files: 11.0.0 197 - wonka: 6.3.0 198 - devDependencies: 199 - graphql: 16.6.0 200 - 201 188 exchanges/persisted: 202 189 specifiers: 203 190 '@urql/core': '>=4.0.0' ··· 7110 7097 schema-utils: 1.0.0 7111 7098 webpack: 4.46.0 7112 7099 webpack-sources: 1.4.3 7113 - 7114 - /extract-files/11.0.0: 7115 - resolution: {integrity: sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==} 7116 - engines: {node: ^12.20 || >= 14.13} 7117 - dev: false 7118 7100 7119 7101 /extract-zip/2.0.1_supports-color@8.1.1: 7120 7102 resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}