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.

(graphcache) - Fix uncached GraphQLError fields making it impossible to get first API result (#1367)

authored by

Phil Pluckthun and committed by
GitHub
832d9fd5 6a687091

+102 -22
+64
exchanges/graphcache/src/cacheExchange.test.ts
··· 4 4 ExchangeIO, 5 5 Operation, 6 6 OperationResult, 7 + CombinedError, 7 8 } from '@urql/core'; 8 9 9 10 import { ··· 609 610 610 611 jest.runAllTimers(); 611 612 expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(2); 613 + }); 614 + 615 + it('marks errored null fields as uncached but delivers them as expected', () => { 616 + const client = createClient({ url: 'http://0.0.0.0' }); 617 + const { source: ops$, next } = makeSubject<Operation>(); 618 + 619 + const query = gql` 620 + { 621 + field 622 + author { 623 + id 624 + } 625 + } 626 + `; 627 + 628 + const operation = client.createRequestOperation('query', { 629 + key: 1, 630 + query, 631 + }); 632 + 633 + const queryResult: OperationResult = { 634 + operation, 635 + data: { 636 + __typename: 'Query', 637 + field: 'test', 638 + author: null, 639 + }, 640 + error: new CombinedError({ 641 + graphQLErrors: [ 642 + { 643 + message: 'Test', 644 + path: ['author'], 645 + }, 646 + ], 647 + }), 648 + }; 649 + 650 + const reexecuteOperation = jest 651 + .spyOn(client, 'reexecuteOperation') 652 + .mockImplementation(next); 653 + 654 + const response = jest.fn( 655 + (forwardOp: Operation): OperationResult => { 656 + if (forwardOp.key === 1) return queryResult; 657 + return undefined as any; 658 + } 659 + ); 660 + 661 + const result = jest.fn(); 662 + const forward: ExchangeIO = ops$ => pipe(ops$, map(response)); 663 + 664 + pipe( 665 + cacheExchange({})({ forward, client, dispatchDebug })(ops$), 666 + tap(result), 667 + publish 668 + ); 669 + 670 + next(operation); 671 + 672 + expect(response).toHaveBeenCalledTimes(1); 673 + expect(result).toHaveBeenCalledTimes(1); 674 + expect(reexecuteOperation).toHaveBeenCalledTimes(0); 675 + expect(result.mock.calls[0][0]).toHaveProperty('data.author', null); 612 676 }); 613 677 }); 614 678
+7 -1
exchanges/graphcache/src/cacheExchange.ts
··· 234 234 ).dependencies; 235 235 collectPendingOperations(pendingOperations, writeDependencies); 236 236 237 - const queryResult = query(store, operation, result.data, key); 237 + const queryResult = query( 238 + store, 239 + operation, 240 + result.data, 241 + result.error, 242 + key 243 + ); 238 244 result.data = queryResult.data; 239 245 if (operation.kind === 'query') { 240 246 // Collect the query's dependencies for future pending operation updates
+31 -21
exchanges/graphcache/src/operations/query.ts
··· 1 1 import { FieldNode, DocumentNode, FragmentDefinitionNode } from 'graphql'; 2 + import { CombinedError } from '@urql/core'; 2 3 3 4 import { 4 5 getSelectionSet, ··· 6 7 SelectionSet, 7 8 getFragmentTypeName, 8 9 getFieldAlias, 9 - } from '../ast'; 10 - 11 - import { 12 10 getFragments, 13 11 getMainOperation, 14 12 normalizeVariables, ··· 44 42 ensureData, 45 43 makeContext, 46 44 updateContext, 45 + getFieldError, 47 46 } from './shared'; 48 47 49 48 import { ··· 62 61 store: Store, 63 62 request: OperationRequest, 64 63 data?: Data, 64 + error?: CombinedError | undefined, 65 65 key?: number 66 66 ): QueryResult => { 67 67 initDataState('read', store.data, (data && key) || null); 68 - const result = read(store, request, data); 68 + const result = read(store, request, data, error); 69 69 clearDataState(); 70 70 return result; 71 71 }; ··· 73 73 export const read = ( 74 74 store: Store, 75 75 request: OperationRequest, 76 - input?: Data 76 + input?: Data, 77 + error?: CombinedError | undefined 77 78 ): QueryResult => { 78 79 const operation = getMainOperation(request.query); 79 80 const rootKey = store.rootFields[operation.operation]; ··· 84 85 normalizeVariables(operation, request.variables), 85 86 getFragments(request.query), 86 87 rootKey, 87 - rootKey 88 + rootKey, 89 + false, 90 + error 88 91 ); 89 92 90 93 if (process.env.NODE_ENV !== 'production') { ··· 117 120 select: SelectionSet, 118 121 originalData: Data 119 122 ): Data => { 120 - if (typeof originalData.__typename !== 'string') { 123 + const typename = ctx.store.rootNames[entityKey] 124 + ? entityKey 125 + : originalData.__typename; 126 + if (typeof typename !== 'string') { 121 127 return originalData; 122 128 } 123 129 124 130 const iterate = makeSelectionIterator(entityKey, entityKey, select, ctx); 125 - const data = {} as Data; 126 - data.__typename = originalData.__typename; 131 + const data = { __typename: typename }; 127 132 128 133 let node: FieldNode | void; 129 134 while ((node = iterate())) { 130 135 const fieldAlias = getFieldAlias(node); 131 136 const fieldValue = originalData[fieldAlias]; 132 137 // Add the current alias to the walked path before processing the field's value 133 - if (process.env.NODE_ENV !== 'production') 134 - ctx.__internal.path.push(fieldAlias); 138 + ctx.__internal.path.push(fieldAlias); 135 139 // Process the root field's value 136 140 if (node.selectionSet && fieldValue !== null) { 137 141 const fieldData = ensureData(fieldValue); ··· 140 144 data[fieldAlias] = fieldValue; 141 145 } 142 146 // After processing the field, remove the current alias from the path again 143 - if (process.env.NODE_ENV !== 'production') ctx.__internal.path.pop(); 147 + ctx.__internal.path.pop(); 144 148 } 145 149 146 150 return data; ··· 155 159 const newData = new Array(originalData.length); 156 160 for (let i = 0, l = originalData.length; i < l; i++) { 157 161 // Add the current index to the walked path before reading the field's value 158 - if (process.env.NODE_ENV !== 'production') ctx.__internal.path.push(i); 162 + ctx.__internal.path.push(i); 159 163 // Recursively read the root field's value 160 164 newData[i] = readRootField(ctx, select, originalData[i]); 161 165 // After processing the field, remove the current index from the path 162 - if (process.env.NODE_ENV !== 'production') ctx.__internal.path.pop(); 166 + ctx.__internal.path.pop(); 163 167 } 164 168 165 169 return newData; ··· 312 316 // means that the value is missing from the cache 313 317 let dataFieldValue: void | DataField; 314 318 // Add the current alias to the walked path before processing the field's value 315 - if (process.env.NODE_ENV !== 'production') 316 - ctx.__internal.path.push(fieldAlias); 319 + ctx.__internal.path.push(fieldAlias); 317 320 318 321 if (resultValue !== undefined && node.selectionSet === undefined) { 319 322 // The field is a scalar and can be retrieved directly from the result ··· 396 399 } 397 400 } 398 401 402 + // If we have an error registered for the current field change undefined values to null 403 + if (dataFieldValue === undefined && !!getFieldError(ctx)) { 404 + hasPartials = true; 405 + dataFieldValue = null; 406 + } 407 + 399 408 // After processing the field, remove the current alias from the path again 400 - if (process.env.NODE_ENV !== 'production') ctx.__internal.path.pop(); 409 + ctx.__internal.path.pop(); 410 + 401 411 // Now that dataFieldValue has been retrieved it'll be set on data 402 412 // If it's uncached (undefined) but nullable we can continue assembling 403 413 // a partial query result ··· 442 452 const data = new Array(result.length); 443 453 for (let i = 0, l = result.length; i < l; i++) { 444 454 // Add the current index to the walked path before reading the field's value 445 - if (process.env.NODE_ENV !== 'production') ctx.__internal.path.push(i); 455 + ctx.__internal.path.push(i); 446 456 // Recursively read resolver result 447 457 const childResult = resolveResolverResult( 448 458 ctx, ··· 455 465 result[i] 456 466 ); 457 467 // After processing the field, remove the current index from the path 458 - if (process.env.NODE_ENV !== 'production') ctx.__internal.path.pop(); 468 + ctx.__internal.path.pop(); 459 469 // Check the result for cache-missed values 460 470 if (childResult === undefined && !_isListNullable) { 461 471 return undefined; ··· 504 514 const newLink = new Array(link.length); 505 515 for (let i = 0, l = link.length; i < l; i++) { 506 516 // Add the current index to the walked path before reading the field's value 507 - if (process.env.NODE_ENV !== 'production') ctx.__internal.path.push(i); 517 + ctx.__internal.path.push(i); 508 518 // Recursively read the link 509 519 const childLink = resolveLink( 510 520 ctx, ··· 515 525 prevData != null ? prevData[i] : undefined 516 526 ); 517 527 // After processing the field, remove the current index from the path 518 - if (process.env.NODE_ENV !== 'production') ctx.__internal.path.pop(); 528 + ctx.__internal.path.pop(); 519 529 // Check the result for cache-missed values 520 530 if (childLink === undefined && !_isListNullable) { 521 531 return undefined;