Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 95 kB view raw
1import { 2 gql, 3 createClient, 4 ExchangeIO, 5 Operation, 6 OperationResult, 7 CombinedError, 8} from '@urql/core'; 9 10import { print, stripIgnoredCharacters } from 'graphql'; 11import { vi, expect, it, describe } from 'vitest'; 12 13import { 14 Source, 15 pipe, 16 share, 17 map, 18 merge, 19 mergeMap, 20 filter, 21 fromValue, 22 makeSubject, 23 tap, 24 publish, 25 delay, 26} from 'wonka'; 27 28import { minifyIntrospectionQuery } from '@urql/introspection'; 29import { queryResponse } from '../../../packages/core/src/test-utils'; 30import { cacheExchange } from './cacheExchange'; 31 32const queryOne = gql` 33 { 34 author { 35 id 36 name 37 } 38 unrelated { 39 id 40 } 41 } 42`; 43 44const queryOneData = { 45 __typename: 'Query', 46 author: { 47 __typename: 'Author', 48 id: '123', 49 name: 'Author', 50 }, 51 unrelated: { 52 __typename: 'Unrelated', 53 id: 'unrelated', 54 }, 55}; 56 57const dispatchDebug = vi.fn(); 58 59describe('data dependencies', () => { 60 it('writes queries to the cache', () => { 61 const client = createClient({ 62 url: 'http://0.0.0.0', 63 exchanges: [], 64 }); 65 const op = client.createRequestOperation('query', { 66 key: 1, 67 query: queryOne, 68 variables: undefined, 69 }); 70 71 const expected = { 72 __typename: 'Query', 73 author: { 74 id: '123', 75 name: 'Author', 76 __typename: 'Author', 77 }, 78 unrelated: { 79 id: 'unrelated', 80 __typename: 'Unrelated', 81 }, 82 }; 83 84 const response = vi.fn((forwardOp: Operation): OperationResult => { 85 expect(forwardOp.key).toBe(op.key); 86 return { ...queryResponse, operation: forwardOp, data: expected }; 87 }); 88 89 const { source: ops$, next } = makeSubject<Operation>(); 90 const result = vi.fn(); 91 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 92 93 pipe( 94 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 95 tap(result), 96 publish 97 ); 98 99 next(op); 100 next(op); 101 expect(response).toHaveBeenCalledTimes(1); 102 expect(result).toHaveBeenCalledTimes(2); 103 104 expect(expected).toMatchObject(result.mock.calls[0][0].data); 105 expect(result.mock.calls[1][0]).toHaveProperty( 106 'operation.context.meta.cacheOutcome', 107 'hit' 108 ); 109 expect(expected).toMatchObject(result.mock.calls[1][0].data); 110 expect(result.mock.calls[1][0].data).toBe(result.mock.calls[0][0].data); 111 }); 112 113 it('logs cache misses', () => { 114 const client = createClient({ 115 url: 'http://0.0.0.0', 116 exchanges: [], 117 }); 118 const op = client.createRequestOperation('query', { 119 key: 1, 120 query: queryOne, 121 variables: undefined, 122 }); 123 124 const expected = { 125 __typename: 'Query', 126 author: { 127 id: '123', 128 name: 'Author', 129 __typename: 'Author', 130 }, 131 unrelated: { 132 id: 'unrelated', 133 __typename: 'Unrelated', 134 }, 135 }; 136 137 const response = vi.fn((forwardOp: Operation): OperationResult => { 138 expect(forwardOp.key).toBe(op.key); 139 return { ...queryResponse, operation: forwardOp, data: expected }; 140 }); 141 142 const { source: ops$, next } = makeSubject<Operation>(); 143 const result = vi.fn(); 144 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 145 146 const messages: string[] = []; 147 pipe( 148 cacheExchange({ 149 logger(severity, message) { 150 if (severity === 'debug') { 151 messages.push(message); 152 } 153 }, 154 })({ forward, client, dispatchDebug })(ops$), 155 tap(result), 156 publish 157 ); 158 159 next(op); 160 next(op); 161 next({ 162 ...op, 163 query: gql` 164 query ($id: ID!) { 165 author(id: $id) { 166 id 167 name 168 } 169 } 170 `, 171 variables: { id: '123' }, 172 }); 173 expect(response).toHaveBeenCalledTimes(1); 174 expect(result).toHaveBeenCalledTimes(2); 175 176 expect(expected).toMatchObject(result.mock.calls[0][0].data); 177 expect(result.mock.calls[1][0]).toHaveProperty( 178 'operation.context.meta.cacheOutcome', 179 'hit' 180 ); 181 expect(expected).toMatchObject(result.mock.calls[1][0].data); 182 expect(result.mock.calls[1][0].data).toBe(result.mock.calls[0][0].data); 183 expect(messages).toEqual([ 184 'No value for field "author" on entity "Query"', 185 'No value for field "author" with args {"id":"123"} on entity "Query"', 186 ]); 187 }); 188 189 it('respects cache-only operations', () => { 190 const client = createClient({ 191 url: 'http://0.0.0.0', 192 exchanges: [], 193 }); 194 const op = client.createRequestOperation( 195 'query', 196 { 197 key: 1, 198 query: queryOne, 199 variables: undefined, 200 }, 201 { 202 requestPolicy: 'cache-only', 203 } 204 ); 205 206 const response = vi.fn((forwardOp: Operation): OperationResult => { 207 expect(forwardOp.key).toBe(op.key); 208 return { ...queryResponse, operation: forwardOp, data: queryOneData }; 209 }); 210 211 const { source: ops$, next } = makeSubject<Operation>(); 212 const result = vi.fn(); 213 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 214 215 pipe( 216 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 217 tap(result), 218 publish 219 ); 220 221 next(op); 222 expect(response).toHaveBeenCalledTimes(0); 223 expect(result).toHaveBeenCalledTimes(1); 224 225 expect(result.mock.calls[0][0]).toHaveProperty( 226 'operation.context.meta.cacheOutcome', 227 'miss' 228 ); 229 230 expect(result.mock.calls[0][0].data).toBe(null); 231 }); 232 233 it('updates related queries when their data changes', () => { 234 const queryMultiple = gql` 235 { 236 authors { 237 id 238 name 239 } 240 } 241 `; 242 243 const queryMultipleData = { 244 __typename: 'Query', 245 authors: [ 246 { 247 __typename: 'Author', 248 id: '123', 249 name: 'New Author Name', 250 }, 251 ], 252 }; 253 254 const client = createClient({ 255 url: 'http://0.0.0.0', 256 exchanges: [], 257 }); 258 const { source: ops$, next } = makeSubject<Operation>(); 259 260 const reexec = vi 261 .spyOn(client, 'reexecuteOperation') 262 .mockImplementation(next); 263 264 const opOne = client.createRequestOperation('query', { 265 key: 1, 266 query: queryOne, 267 variables: undefined, 268 }); 269 270 const opMultiple = client.createRequestOperation('query', { 271 key: 2, 272 query: queryMultiple, 273 variables: undefined, 274 }); 275 276 const response = vi.fn((forwardOp: Operation): OperationResult => { 277 if (forwardOp.key === 1) { 278 return { ...queryResponse, operation: opOne, data: queryOneData }; 279 } else if (forwardOp.key === 2) { 280 return { 281 ...queryResponse, 282 operation: opMultiple, 283 data: queryMultipleData, 284 }; 285 } 286 287 return undefined as any; 288 }); 289 290 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 291 const result = vi.fn(); 292 293 pipe( 294 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 295 tap(result), 296 publish 297 ); 298 299 next(opOne); 300 expect(response).toHaveBeenCalledTimes(1); 301 expect(result).toHaveBeenCalledTimes(1); 302 303 next(opMultiple); 304 expect(response).toHaveBeenCalledTimes(2); 305 expect(reexec.mock.calls[0][0]).toHaveProperty('key', opOne.key); 306 expect(result).toHaveBeenCalledTimes(3); 307 308 // test for reference reuse 309 const firstDataOne = result.mock.calls[0][0].data; 310 const firstDataTwo = result.mock.calls[1][0].data; 311 expect(firstDataOne).not.toBe(firstDataTwo); 312 expect(firstDataOne.author).not.toBe(firstDataTwo.author); 313 expect(firstDataOne.unrelated).toBe(firstDataTwo.unrelated); 314 }); 315 316 it('updates related queries when a mutation update touches query data', () => { 317 vi.useFakeTimers(); 318 319 const balanceFragment = gql` 320 fragment BalanceFragment on Author { 321 id 322 balance { 323 amount 324 } 325 } 326 `; 327 328 const queryById = gql` 329 query ($id: ID!) { 330 author(id: $id) { 331 id 332 name 333 ...BalanceFragment 334 } 335 } 336 337 ${balanceFragment} 338 `; 339 340 const queryByIdDataA = { 341 __typename: 'Query', 342 author: { 343 __typename: 'Author', 344 id: '1', 345 name: 'Author 1', 346 balance: { 347 __typename: 'Balance', 348 amount: 100, 349 }, 350 }, 351 }; 352 353 const queryByIdDataB = { 354 __typename: 'Query', 355 author: { 356 __typename: 'Author', 357 id: '2', 358 name: 'Author 2', 359 balance: { 360 __typename: 'Balance', 361 amount: 200, 362 }, 363 }, 364 }; 365 366 const mutation = gql` 367 mutation ($userId: ID!, $amount: Int!) { 368 updateBalance(userId: $userId, amount: $amount) { 369 userId 370 balance { 371 amount 372 } 373 } 374 } 375 `; 376 377 const mutationData = { 378 __typename: 'Mutation', 379 updateBalance: { 380 __typename: 'UpdateBalanceResult', 381 userId: '1', 382 balance: { 383 __typename: 'Balance', 384 amount: 1000, 385 }, 386 }, 387 }; 388 389 const client = createClient({ 390 url: 'http://0.0.0.0', 391 exchanges: [], 392 }); 393 const { source: ops$, next } = makeSubject<Operation>(); 394 395 const reexec = vi 396 .spyOn(client, 'reexecuteOperation') 397 .mockImplementation(next); 398 399 const opOne = client.createRequestOperation('query', { 400 key: 1, 401 query: queryById, 402 variables: { id: 1 }, 403 }); 404 405 const opTwo = client.createRequestOperation('query', { 406 key: 2, 407 query: queryById, 408 variables: { id: 2 }, 409 }); 410 411 const opMutation = client.createRequestOperation('mutation', { 412 key: 3, 413 query: mutation, 414 variables: { userId: '1', amount: 1000 }, 415 }); 416 417 const response = vi.fn((forwardOp: Operation): OperationResult => { 418 if (forwardOp.key === 1) { 419 return { ...queryResponse, operation: opOne, data: queryByIdDataA }; 420 } else if (forwardOp.key === 2) { 421 return { ...queryResponse, operation: opTwo, data: queryByIdDataB }; 422 } else if (forwardOp.key === 3) { 423 return { 424 ...queryResponse, 425 operation: opMutation, 426 data: mutationData, 427 }; 428 } 429 430 return undefined as any; 431 }); 432 433 const result = vi.fn(); 434 const forward: ExchangeIO = ops$ => 435 pipe(ops$, delay(1), map(response), share); 436 437 const updates = { 438 Mutation: { 439 updateBalance: vi.fn((result, _args, cache) => { 440 const { 441 updateBalance: { userId, balance }, 442 } = result; 443 cache.writeFragment(balanceFragment, { id: userId, balance }); 444 }), 445 }, 446 }; 447 448 const keys = { 449 Balance: () => null, 450 }; 451 452 pipe( 453 cacheExchange({ updates, keys })({ forward, client, dispatchDebug })( 454 ops$ 455 ), 456 tap(result), 457 publish 458 ); 459 460 next(opTwo); 461 vi.runAllTimers(); 462 expect(response).toHaveBeenCalledTimes(1); 463 464 next(opOne); 465 vi.runAllTimers(); 466 expect(response).toHaveBeenCalledTimes(2); 467 468 next(opMutation); 469 vi.runAllTimers(); 470 471 expect(response).toHaveBeenCalledTimes(3); 472 expect(updates.Mutation.updateBalance).toHaveBeenCalledTimes(1); 473 474 expect(reexec).toHaveBeenCalledTimes(1); 475 expect(reexec.mock.calls[0][0].key).toBe(1); 476 477 expect(result.mock.calls[2][0]).toHaveProperty( 478 'data.author.balance.amount', 479 1000 480 ); 481 }); 482 483 it('does not notify related queries when a mutation update does not change the data', () => { 484 vi.useFakeTimers(); 485 486 const balanceFragment = gql` 487 fragment BalanceFragment on Author { 488 id 489 balance { 490 amount 491 } 492 } 493 `; 494 495 const queryById = gql` 496 query ($id: ID!) { 497 author(id: $id) { 498 id 499 name 500 ...BalanceFragment 501 } 502 } 503 504 ${balanceFragment} 505 `; 506 507 const queryByIdDataA = { 508 __typename: 'Query', 509 author: { 510 __typename: 'Author', 511 id: '1', 512 name: 'Author 1', 513 balance: { 514 __typename: 'Balance', 515 amount: 100, 516 }, 517 }, 518 }; 519 520 const queryByIdDataB = { 521 __typename: 'Query', 522 author: { 523 __typename: 'Author', 524 id: '2', 525 name: 'Author 2', 526 balance: { 527 __typename: 'Balance', 528 amount: 200, 529 }, 530 }, 531 }; 532 533 const mutation = gql` 534 mutation ($userId: ID!, $amount: Int!) { 535 updateBalance(userId: $userId, amount: $amount) { 536 userId 537 balance { 538 amount 539 } 540 } 541 } 542 `; 543 544 const mutationData = { 545 __typename: 'Mutation', 546 updateBalance: { 547 __typename: 'UpdateBalanceResult', 548 userId: '1', 549 balance: { 550 __typename: 'Balance', 551 amount: 100, 552 }, 553 }, 554 }; 555 556 const client = createClient({ 557 url: 'http://0.0.0.0', 558 exchanges: [], 559 }); 560 const { source: ops$, next } = makeSubject<Operation>(); 561 562 const reexec = vi 563 .spyOn(client, 'reexecuteOperation') 564 .mockImplementation(next); 565 566 const opOne = client.createRequestOperation('query', { 567 key: 1, 568 query: queryById, 569 variables: { id: 1 }, 570 }); 571 572 const opTwo = client.createRequestOperation('query', { 573 key: 2, 574 query: queryById, 575 variables: { id: 2 }, 576 }); 577 578 const opMutation = client.createRequestOperation('mutation', { 579 key: 3, 580 query: mutation, 581 variables: { userId: '1', amount: 1000 }, 582 }); 583 584 const response = vi.fn((forwardOp: Operation): OperationResult => { 585 if (forwardOp.key === 1) { 586 return { ...queryResponse, operation: opOne, data: queryByIdDataA }; 587 } else if (forwardOp.key === 2) { 588 return { ...queryResponse, operation: opTwo, data: queryByIdDataB }; 589 } else if (forwardOp.key === 3) { 590 return { 591 ...queryResponse, 592 operation: opMutation, 593 data: mutationData, 594 }; 595 } 596 597 return undefined as any; 598 }); 599 600 const result = vi.fn(); 601 const forward: ExchangeIO = ops$ => 602 pipe(ops$, delay(1), map(response), share); 603 604 const updates = { 605 Mutation: { 606 updateBalance: vi.fn((result, _args, cache) => { 607 const { 608 updateBalance: { userId, balance }, 609 } = result; 610 cache.writeFragment(balanceFragment, { id: userId, balance }); 611 }), 612 }, 613 }; 614 615 const keys = { 616 Balance: () => null, 617 }; 618 619 pipe( 620 cacheExchange({ updates, keys })({ forward, client, dispatchDebug })( 621 ops$ 622 ), 623 tap(result), 624 publish 625 ); 626 627 next(opTwo); 628 vi.runAllTimers(); 629 expect(response).toHaveBeenCalledTimes(1); 630 631 next(opOne); 632 vi.runAllTimers(); 633 expect(response).toHaveBeenCalledTimes(2); 634 635 next(opMutation); 636 vi.runAllTimers(); 637 638 expect(response).toHaveBeenCalledTimes(3); 639 expect(updates.Mutation.updateBalance).toHaveBeenCalledTimes(1); 640 641 expect(reexec).toHaveBeenCalledTimes(0); 642 }); 643 644 it('does nothing when no related queries have changed', () => { 645 const queryUnrelated = gql` 646 { 647 user { 648 id 649 name 650 } 651 } 652 `; 653 654 const queryUnrelatedData = { 655 __typename: 'Query', 656 user: { 657 __typename: 'User', 658 id: 'me', 659 name: 'Me', 660 }, 661 }; 662 663 const client = createClient({ 664 url: 'http://0.0.0.0', 665 exchanges: [], 666 }); 667 const { source: ops$, next } = makeSubject<Operation>(); 668 const reexec = vi 669 .spyOn(client, 'reexecuteOperation') 670 .mockImplementation(next); 671 672 const opOne = client.createRequestOperation('query', { 673 key: 1, 674 query: queryOne, 675 variables: undefined, 676 }); 677 const opUnrelated = client.createRequestOperation('query', { 678 key: 2, 679 query: queryUnrelated, 680 variables: undefined, 681 }); 682 683 const response = vi.fn((forwardOp: Operation): OperationResult => { 684 if (forwardOp.key === 1) { 685 return { ...queryResponse, operation: opOne, data: queryOneData }; 686 } else if (forwardOp.key === 2) { 687 return { 688 ...queryResponse, 689 operation: opUnrelated, 690 data: queryUnrelatedData, 691 }; 692 } 693 694 return undefined as any; 695 }); 696 697 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 698 const result = vi.fn(); 699 700 pipe( 701 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 702 tap(result), 703 publish 704 ); 705 706 next(opOne); 707 expect(response).toHaveBeenCalledTimes(1); 708 709 next(opUnrelated); 710 expect(response).toHaveBeenCalledTimes(2); 711 712 expect(reexec).not.toHaveBeenCalled(); 713 expect(result).toHaveBeenCalledTimes(2); 714 }); 715 716 it('does not reach updater when mutation has no selectionset in optimistic phase', () => { 717 vi.useFakeTimers(); 718 719 const mutation = gql` 720 mutation { 721 concealAuthor 722 } 723 `; 724 725 const mutationData = { 726 __typename: 'Mutation', 727 concealAuthor: true, 728 }; 729 730 const client = createClient({ 731 url: 'http://0.0.0.0', 732 exchanges: [], 733 }); 734 const { source: ops$, next } = makeSubject<Operation>(); 735 736 vi.spyOn(client, 'reexecuteOperation').mockImplementation(next); 737 738 const opMutation = client.createRequestOperation('mutation', { 739 key: 1, 740 query: mutation, 741 variables: undefined, 742 }); 743 744 const response = vi.fn((forwardOp: Operation): OperationResult => { 745 if (forwardOp.key === 1) { 746 return { 747 ...queryResponse, 748 operation: opMutation, 749 data: mutationData, 750 }; 751 } 752 753 return undefined as any; 754 }); 755 756 const result = vi.fn(); 757 const forward: ExchangeIO = ops$ => 758 pipe(ops$, delay(1), map(response), share); 759 760 const updates = { 761 Mutation: { 762 concealAuthor: vi.fn(), 763 }, 764 }; 765 766 pipe( 767 cacheExchange({ updates })({ forward, client, dispatchDebug })(ops$), 768 tap(result), 769 publish 770 ); 771 772 next(opMutation); 773 expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(0); 774 775 vi.runAllTimers(); 776 expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1); 777 }); 778 779 it('does reach updater when mutation has no selectionset in optimistic phase with optimistic update', () => { 780 vi.useFakeTimers(); 781 782 const mutation = gql` 783 mutation { 784 concealAuthor 785 } 786 `; 787 788 const mutationData = { 789 __typename: 'Mutation', 790 concealAuthor: true, 791 }; 792 793 const client = createClient({ 794 url: 'http://0.0.0.0', 795 exchanges: [], 796 }); 797 const { source: ops$, next } = makeSubject<Operation>(); 798 799 vi.spyOn(client, 'reexecuteOperation').mockImplementation(next); 800 801 const opMutation = client.createRequestOperation('mutation', { 802 key: 1, 803 query: mutation, 804 variables: undefined, 805 }); 806 807 const response = vi.fn((forwardOp: Operation): OperationResult => { 808 if (forwardOp.key === 1) { 809 return { 810 ...queryResponse, 811 operation: opMutation, 812 data: mutationData, 813 }; 814 } 815 816 return undefined as any; 817 }); 818 819 const result = vi.fn(); 820 const forward: ExchangeIO = ops$ => 821 pipe(ops$, delay(1), map(response), share); 822 823 const updates = { 824 Mutation: { 825 concealAuthor: vi.fn(), 826 }, 827 }; 828 829 const optimistic = { 830 concealAuthor: vi.fn(() => true) as any, 831 }; 832 833 pipe( 834 cacheExchange({ updates, optimistic })({ 835 forward, 836 client, 837 dispatchDebug, 838 })(ops$), 839 tap(result), 840 publish 841 ); 842 843 next(opMutation); 844 expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1); 845 expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1); 846 847 vi.runAllTimers(); 848 expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(2); 849 }); 850 851 it('marks errored null fields as uncached but delivers them as expected', () => { 852 const client = createClient({ 853 url: 'http://0.0.0.0', 854 exchanges: [], 855 }); 856 const { source: ops$, next } = makeSubject<Operation>(); 857 858 const query = gql` 859 { 860 field 861 author { 862 id 863 } 864 } 865 `; 866 867 const operation = client.createRequestOperation('query', { 868 key: 1, 869 query, 870 variables: undefined, 871 }); 872 873 const queryResult: OperationResult = { 874 ...queryResponse, 875 operation, 876 data: { 877 __typename: 'Query', 878 field: 'test', 879 author: null, 880 }, 881 error: new CombinedError({ 882 graphQLErrors: [ 883 { 884 message: 'Test', 885 path: ['author'], 886 }, 887 ], 888 }), 889 }; 890 891 const reexecuteOperation = vi 892 .spyOn(client, 'reexecuteOperation') 893 .mockImplementation(next); 894 895 const response = vi.fn((forwardOp: Operation): OperationResult => { 896 if (forwardOp.key === 1) return queryResult; 897 return undefined as any; 898 }); 899 900 const result = vi.fn(); 901 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 902 903 pipe( 904 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 905 tap(result), 906 publish 907 ); 908 909 next(operation); 910 911 expect(response).toHaveBeenCalledTimes(1); 912 expect(result).toHaveBeenCalledTimes(1); 913 expect(reexecuteOperation).toHaveBeenCalledTimes(0); 914 expect(result.mock.calls[0][0]).toHaveProperty('data.author', null); 915 }); 916 917 it('mutation does not change number of reexecute request after a query', () => { 918 const client = createClient({ 919 url: 'http://0.0.0.0', 920 exchanges: [], 921 }); 922 923 const { source: ops$, next: nextOp } = makeSubject<Operation>(); 924 925 const reexec = vi 926 .spyOn(client, 'reexecuteOperation') 927 .mockImplementation(nextOp); 928 929 const mutation = gql` 930 mutation { 931 updateNode { 932 __typename 933 id 934 } 935 } 936 `; 937 938 const normalQuery = gql` 939 { 940 __typename 941 item { 942 __typename 943 id 944 } 945 } 946 `; 947 948 const extendedQuery = gql` 949 { 950 __typename 951 item { 952 __typename 953 extended: id 954 extra @_optional 955 } 956 } 957 `; 958 959 const mutationOp = client.createRequestOperation('mutation', { 960 key: 0, 961 query: mutation, 962 variables: undefined, 963 }); 964 965 const normalOp = client.createRequestOperation( 966 'query', 967 { 968 key: 1, 969 query: normalQuery, 970 variables: undefined, 971 }, 972 { 973 requestPolicy: 'cache-and-network', 974 } 975 ); 976 977 const extendedOp = client.createRequestOperation( 978 'query', 979 { 980 key: 2, 981 query: extendedQuery, 982 variables: undefined, 983 }, 984 { 985 requestPolicy: 'cache-only', 986 } 987 ); 988 989 const response = vi.fn((forwardOp: Operation): OperationResult => { 990 if (forwardOp.key === 0) { 991 return { 992 operation: mutationOp, 993 data: { 994 __typename: 'Mutation', 995 updateNode: { 996 __typename: 'Node', 997 id: 'id', 998 }, 999 }, 1000 stale: false, 1001 hasNext: false, 1002 }; 1003 } else if (forwardOp.key === 1) { 1004 return { 1005 operation: normalOp, 1006 data: { 1007 __typename: 'Query', 1008 item: { 1009 __typename: 'Node', 1010 id: 'id', 1011 }, 1012 }, 1013 stale: false, 1014 hasNext: false, 1015 }; 1016 } else if (forwardOp.key === 2) { 1017 return { 1018 operation: extendedOp, 1019 data: { 1020 __typename: 'Query', 1021 item: { 1022 __typename: 'Node', 1023 extended: 'id', 1024 extra: 'extra', 1025 }, 1026 }, 1027 stale: false, 1028 hasNext: false, 1029 }; 1030 } 1031 1032 return undefined as any; 1033 }); 1034 1035 const forward = (ops$: Source<Operation>): Source<OperationResult> => 1036 pipe(ops$, map(response), share); 1037 1038 pipe(cacheExchange()({ forward, client, dispatchDebug })(ops$), publish); 1039 1040 nextOp(normalOp); 1041 expect(reexec).toHaveBeenCalledTimes(0); 1042 1043 nextOp(extendedOp); 1044 expect(reexec).toHaveBeenCalledTimes(0); 1045 1046 // re-execute first operation 1047 reexec.mockClear(); 1048 nextOp(normalOp); 1049 expect(reexec).toHaveBeenCalledTimes(4); 1050 1051 nextOp(mutationOp); 1052 1053 // re-execute first operation after mutation 1054 reexec.mockClear(); 1055 nextOp(normalOp); 1056 expect(reexec).toHaveBeenCalledTimes(4); 1057 }); 1058}); 1059 1060describe('directives', () => { 1061 it('returns optional fields as partial', () => { 1062 const client = createClient({ 1063 url: 'http://0.0.0.0', 1064 exchanges: [], 1065 }); 1066 const { source: ops$, next } = makeSubject<Operation>(); 1067 1068 const query = gql` 1069 { 1070 todos { 1071 id 1072 text 1073 completed @_optional 1074 } 1075 } 1076 `; 1077 1078 const operation = client.createRequestOperation('query', { 1079 key: 1, 1080 query, 1081 variables: undefined, 1082 }); 1083 1084 const queryResult: OperationResult = { 1085 ...queryResponse, 1086 operation, 1087 data: { 1088 __typename: 'Query', 1089 todos: [ 1090 { 1091 id: '1', 1092 text: 'learn urql', 1093 __typename: 'Todo', 1094 }, 1095 ], 1096 }, 1097 }; 1098 1099 const reexecuteOperation = vi 1100 .spyOn(client, 'reexecuteOperation') 1101 .mockImplementation(next); 1102 1103 const response = vi.fn((forwardOp: Operation): OperationResult => { 1104 if (forwardOp.key === 1) return queryResult; 1105 return undefined as any; 1106 }); 1107 1108 const result = vi.fn(); 1109 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 1110 1111 pipe( 1112 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 1113 tap(result), 1114 publish 1115 ); 1116 1117 next(operation); 1118 1119 expect(response).toHaveBeenCalledTimes(1); 1120 expect(result).toHaveBeenCalledTimes(1); 1121 expect(reexecuteOperation).toHaveBeenCalledTimes(0); 1122 expect(result.mock.calls[0][0].data).toEqual({ 1123 todos: [ 1124 { 1125 completed: null, 1126 id: '1', 1127 text: 'learn urql', 1128 }, 1129 ], 1130 }); 1131 }); 1132 1133 it('Does not return partial data for nested selections', () => { 1134 const client = createClient({ 1135 url: 'http://0.0.0.0', 1136 exchanges: [], 1137 }); 1138 const { source: ops$, next } = makeSubject<Operation>(); 1139 1140 const query = gql` 1141 { 1142 todo { 1143 ... on Todo @_optional { 1144 id 1145 text 1146 author { 1147 id 1148 name 1149 } 1150 } 1151 } 1152 } 1153 `; 1154 1155 const operation = client.createRequestOperation('query', { 1156 key: 1, 1157 query, 1158 variables: undefined, 1159 }); 1160 1161 const queryResult: OperationResult = { 1162 ...queryResponse, 1163 operation, 1164 data: { 1165 __typename: 'Query', 1166 todo: { 1167 id: '1', 1168 text: 'learn urql', 1169 __typename: 'Todo', 1170 author: { 1171 __typename: 'Author', 1172 }, 1173 }, 1174 }, 1175 }; 1176 1177 const reexecuteOperation = vi 1178 .spyOn(client, 'reexecuteOperation') 1179 .mockImplementation(next); 1180 1181 const response = vi.fn((forwardOp: Operation): OperationResult => { 1182 if (forwardOp.key === 1) return queryResult; 1183 return undefined as any; 1184 }); 1185 1186 const result = vi.fn(); 1187 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 1188 1189 pipe( 1190 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 1191 tap(result), 1192 publish 1193 ); 1194 1195 next(operation); 1196 1197 expect(response).toHaveBeenCalledTimes(1); 1198 expect(result).toHaveBeenCalledTimes(1); 1199 expect(reexecuteOperation).toHaveBeenCalledTimes(0); 1200 expect(result.mock.calls[0][0].data).toEqual(null); 1201 }); 1202 1203 it('returns partial results when an inline-fragment is marked as optional', () => { 1204 const client = createClient({ 1205 url: 'http://0.0.0.0', 1206 exchanges: [], 1207 }); 1208 const { source: ops$, next } = makeSubject<Operation>(); 1209 1210 const query = gql` 1211 { 1212 todos { 1213 id 1214 text 1215 ... @_optional { 1216 ... on Todo { 1217 completed 1218 } 1219 } 1220 } 1221 } 1222 `; 1223 1224 const operation = client.createRequestOperation('query', { 1225 key: 1, 1226 query, 1227 variables: undefined, 1228 }); 1229 1230 const queryResult: OperationResult = { 1231 ...queryResponse, 1232 operation, 1233 data: { 1234 __typename: 'Query', 1235 todos: [ 1236 { 1237 id: '1', 1238 text: 'learn urql', 1239 __typename: 'Todo', 1240 }, 1241 ], 1242 }, 1243 }; 1244 1245 const reexecuteOperation = vi 1246 .spyOn(client, 'reexecuteOperation') 1247 .mockImplementation(next); 1248 1249 const response = vi.fn((forwardOp: Operation): OperationResult => { 1250 if (forwardOp.key === 1) return queryResult; 1251 return undefined as any; 1252 }); 1253 1254 const result = vi.fn(); 1255 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 1256 1257 pipe( 1258 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 1259 tap(result), 1260 publish 1261 ); 1262 1263 next(operation); 1264 1265 expect(response).toHaveBeenCalledTimes(1); 1266 expect(result).toHaveBeenCalledTimes(1); 1267 expect(reexecuteOperation).toHaveBeenCalledTimes(0); 1268 expect(result.mock.calls[0][0].data).toEqual({ 1269 todos: [ 1270 { 1271 completed: null, 1272 id: '1', 1273 text: 'learn urql', 1274 }, 1275 ], 1276 }); 1277 }); 1278 1279 it('does not return partial results when an inline-fragment is marked as optional with a required child fragment', () => { 1280 const client = createClient({ 1281 url: 'http://0.0.0.0', 1282 exchanges: [], 1283 }); 1284 const { source: ops$, next } = makeSubject<Operation>(); 1285 1286 const query = gql` 1287 { 1288 todos { 1289 id 1290 ... on Todo @_optional { 1291 text 1292 ... on Todo @_required { 1293 completed 1294 } 1295 } 1296 } 1297 } 1298 `; 1299 1300 const operation = client.createRequestOperation('query', { 1301 key: 1, 1302 query, 1303 variables: undefined, 1304 }); 1305 1306 const queryResult: OperationResult = { 1307 ...queryResponse, 1308 operation, 1309 data: { 1310 __typename: 'Query', 1311 todos: [ 1312 { 1313 id: '1', 1314 text: 'learn urql', 1315 __typename: 'Todo', 1316 }, 1317 ], 1318 }, 1319 }; 1320 1321 const reexecuteOperation = vi 1322 .spyOn(client, 'reexecuteOperation') 1323 .mockImplementation(next); 1324 1325 const response = vi.fn((forwardOp: Operation): OperationResult => { 1326 if (forwardOp.key === 1) return queryResult; 1327 return undefined as any; 1328 }); 1329 1330 const result = vi.fn(); 1331 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 1332 1333 pipe( 1334 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 1335 tap(result), 1336 publish 1337 ); 1338 1339 next(operation); 1340 1341 expect(response).toHaveBeenCalledTimes(1); 1342 expect(result).toHaveBeenCalledTimes(1); 1343 expect(reexecuteOperation).toHaveBeenCalledTimes(0); 1344 expect(result.mock.calls[0][0].data).toEqual(null); 1345 }); 1346 1347 it('does not return partial results when an inline-fragment is marked as optional with a required field', () => { 1348 const client = createClient({ 1349 url: 'http://0.0.0.0', 1350 exchanges: [], 1351 }); 1352 const { source: ops$, next } = makeSubject<Operation>(); 1353 1354 const query = gql` 1355 { 1356 todos { 1357 id 1358 ... on Todo @_optional { 1359 text 1360 completed @_required 1361 } 1362 } 1363 } 1364 `; 1365 1366 const operation = client.createRequestOperation('query', { 1367 key: 1, 1368 query, 1369 variables: undefined, 1370 }); 1371 1372 const queryResult: OperationResult = { 1373 ...queryResponse, 1374 operation, 1375 data: { 1376 __typename: 'Query', 1377 todos: [ 1378 { 1379 id: '1', 1380 text: 'learn urql', 1381 __typename: 'Todo', 1382 }, 1383 ], 1384 }, 1385 }; 1386 1387 const reexecuteOperation = vi 1388 .spyOn(client, 'reexecuteOperation') 1389 .mockImplementation(next); 1390 1391 const response = vi.fn((forwardOp: Operation): OperationResult => { 1392 if (forwardOp.key === 1) return queryResult; 1393 return undefined as any; 1394 }); 1395 1396 const result = vi.fn(); 1397 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 1398 1399 pipe( 1400 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 1401 tap(result), 1402 publish 1403 ); 1404 1405 next(operation); 1406 1407 expect(response).toHaveBeenCalledTimes(1); 1408 expect(result).toHaveBeenCalledTimes(1); 1409 expect(reexecuteOperation).toHaveBeenCalledTimes(0); 1410 expect(result.mock.calls[0][0].data).toEqual(null); 1411 }); 1412 1413 it('returns partial results when a fragment-definition is marked as optional', () => { 1414 const client = createClient({ 1415 url: 'http://0.0.0.0', 1416 exchanges: [], 1417 }); 1418 const { source: ops$, next } = makeSubject<Operation>(); 1419 1420 const query = gql` 1421 { 1422 todos { 1423 id 1424 text 1425 ...Fields 1426 } 1427 } 1428 1429 fragment Fields on Todo @_optional { 1430 completed 1431 } 1432 `; 1433 1434 const operation = client.createRequestOperation('query', { 1435 key: 1, 1436 query, 1437 variables: undefined, 1438 }); 1439 1440 const queryResult: OperationResult = { 1441 ...queryResponse, 1442 operation, 1443 data: { 1444 __typename: 'Query', 1445 todos: [ 1446 { 1447 id: '1', 1448 text: 'learn urql', 1449 __typename: 'Todo', 1450 }, 1451 ], 1452 }, 1453 }; 1454 1455 const reexecuteOperation = vi 1456 .spyOn(client, 'reexecuteOperation') 1457 .mockImplementation(next); 1458 1459 const response = vi.fn((forwardOp: Operation): OperationResult => { 1460 if (forwardOp.key === 1) return queryResult; 1461 return undefined as any; 1462 }); 1463 1464 const result = vi.fn(); 1465 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 1466 1467 pipe( 1468 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 1469 tap(result), 1470 publish 1471 ); 1472 1473 next(operation); 1474 1475 expect(response).toHaveBeenCalledTimes(1); 1476 expect(result).toHaveBeenCalledTimes(1); 1477 expect(reexecuteOperation).toHaveBeenCalledTimes(0); 1478 expect(result.mock.calls[0][0].data).toEqual(null); 1479 }); 1480 1481 it('does not return missing required fields', () => { 1482 const client = createClient({ 1483 url: 'http://0.0.0.0', 1484 exchanges: [], 1485 }); 1486 const { source: ops$, next } = makeSubject<Operation>(); 1487 1488 const query = gql` 1489 { 1490 todos { 1491 id 1492 text 1493 completed @_required 1494 } 1495 } 1496 `; 1497 1498 const operation = client.createRequestOperation('query', { 1499 key: 1, 1500 query, 1501 variables: undefined, 1502 }); 1503 1504 const queryResult: OperationResult = { 1505 ...queryResponse, 1506 operation, 1507 data: { 1508 __typename: 'Query', 1509 todos: [ 1510 { 1511 id: '1', 1512 text: 'learn urql', 1513 __typename: 'Todo', 1514 }, 1515 ], 1516 }, 1517 }; 1518 1519 const reexecuteOperation = vi 1520 .spyOn(client, 'reexecuteOperation') 1521 .mockImplementation(next); 1522 1523 const response = vi.fn((forwardOp: Operation): OperationResult => { 1524 if (forwardOp.key === 1) return queryResult; 1525 return undefined as any; 1526 }); 1527 1528 const result = vi.fn(); 1529 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 1530 1531 pipe( 1532 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 1533 tap(result), 1534 publish 1535 ); 1536 1537 next(operation); 1538 1539 expect(response).toHaveBeenCalledTimes(1); 1540 expect(result).toHaveBeenCalledTimes(1); 1541 expect( 1542 stripIgnoredCharacters(print(response.mock.calls[0][0].query)) 1543 ).toEqual('{todos{id text completed __typename}}'); 1544 expect(reexecuteOperation).toHaveBeenCalledTimes(0); 1545 expect(result.mock.calls[0][0].data).toEqual(null); 1546 }); 1547 1548 it('does not return missing fields when nullable fields from a defined schema are marked as required in the query', () => { 1549 const client = createClient({ 1550 url: 'http://0.0.0.0', 1551 exchanges: [], 1552 }); 1553 const { source: ops$, next } = makeSubject<Operation>(); 1554 1555 const initialQuery = gql` 1556 query { 1557 latestTodo { 1558 id 1559 } 1560 } 1561 `; 1562 1563 const query = gql` 1564 { 1565 latestTodo { 1566 id 1567 author @_required { 1568 id 1569 name 1570 } 1571 } 1572 } 1573 `; 1574 1575 const initialQueryOperation = client.createRequestOperation('query', { 1576 key: 1, 1577 query: initialQuery, 1578 variables: undefined, 1579 }); 1580 1581 const queryOperation = client.createRequestOperation('query', { 1582 key: 2, 1583 query, 1584 variables: undefined, 1585 }); 1586 1587 const initialQueryResult: OperationResult = { 1588 ...queryResponse, 1589 operation: initialQueryOperation, 1590 data: { 1591 __typename: 'Query', 1592 latestTodo: { 1593 __typename: 'Todo', 1594 id: '1', 1595 }, 1596 }, 1597 }; 1598 1599 const queryResult: OperationResult = { 1600 ...queryResponse, 1601 operation: queryOperation, 1602 data: { 1603 __typename: 'Query', 1604 latestTodo: { 1605 __typename: 'Todo', 1606 id: '1', 1607 author: null, 1608 }, 1609 }, 1610 }; 1611 1612 const response = vi.fn((forwardOp: Operation): OperationResult => { 1613 if (forwardOp.key === 1) { 1614 return initialQueryResult; 1615 } else if (forwardOp.key === 2) { 1616 return queryResult; 1617 } 1618 return undefined as any; 1619 }); 1620 1621 const result = vi.fn(); 1622 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 1623 1624 pipe( 1625 cacheExchange({ 1626 schema: minifyIntrospectionQuery( 1627 // eslint-disable-next-line 1628 require('./test-utils/simple_schema.json') 1629 ), 1630 })({ forward, client, dispatchDebug })(ops$), 1631 tap(result), 1632 publish 1633 ); 1634 1635 next(initialQueryOperation); 1636 vi.runAllTimers(); 1637 next(queryOperation); 1638 vi.runAllTimers(); 1639 1640 expect(result.mock.calls[0][0].data).toEqual({ 1641 latestTodo: { 1642 id: '1', 1643 }, 1644 }); 1645 expect(result.mock.calls[1][0].data).toEqual(null); 1646 }); 1647}); 1648 1649describe('optimistic updates', () => { 1650 it('writes optimistic mutations to the cache', () => { 1651 vi.useFakeTimers(); 1652 1653 const mutation = gql` 1654 mutation { 1655 concealAuthor { 1656 id 1657 name 1658 } 1659 } 1660 `; 1661 1662 const optimisticMutationData = { 1663 __typename: 'Mutation', 1664 concealAuthor: { 1665 __typename: 'Author', 1666 id: '123', 1667 name() { 1668 return '[REDACTED OFFLINE]'; 1669 }, 1670 }, 1671 }; 1672 1673 const mutationData = { 1674 __typename: 'Mutation', 1675 concealAuthor: { 1676 __typename: 'Author', 1677 id: '123', 1678 name: '[REDACTED ONLINE]', 1679 }, 1680 }; 1681 1682 const client = createClient({ 1683 url: 'http://0.0.0.0', 1684 exchanges: [], 1685 }); 1686 const { source: ops$, next } = makeSubject<Operation>(); 1687 1688 const reexec = vi 1689 .spyOn(client, 'reexecuteOperation') 1690 .mockImplementation(next); 1691 1692 const opOne = client.createRequestOperation('query', { 1693 key: 1, 1694 query: queryOne, 1695 variables: undefined, 1696 }); 1697 1698 const opMutation = client.createRequestOperation('mutation', { 1699 key: 2, 1700 query: mutation, 1701 variables: undefined, 1702 }); 1703 1704 const response = vi.fn((forwardOp: Operation): OperationResult => { 1705 if (forwardOp.key === 1) { 1706 return { ...queryResponse, operation: opOne, data: queryOneData }; 1707 } else if (forwardOp.key === 2) { 1708 return { 1709 ...queryResponse, 1710 operation: opMutation, 1711 data: mutationData, 1712 }; 1713 } 1714 1715 return undefined as any; 1716 }); 1717 1718 const result = vi.fn(); 1719 const forward: ExchangeIO = ops$ => 1720 pipe(ops$, delay(1), map(response), share); 1721 1722 const optimistic = { 1723 concealAuthor: vi.fn(() => optimisticMutationData.concealAuthor) as any, 1724 }; 1725 1726 pipe( 1727 cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), 1728 tap(result), 1729 publish 1730 ); 1731 1732 next(opOne); 1733 vi.runAllTimers(); 1734 expect(response).toHaveBeenCalledTimes(1); 1735 1736 next(opMutation); 1737 expect(response).toHaveBeenCalledTimes(1); 1738 expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1); 1739 expect(reexec).toHaveBeenCalledTimes(1); 1740 1741 expect(result.mock.calls[1][0]?.data).toMatchObject({ 1742 author: { name: '[REDACTED OFFLINE]' }, 1743 }); 1744 1745 vi.runAllTimers(); 1746 expect(response).toHaveBeenCalledTimes(2); 1747 expect(result).toHaveBeenCalledTimes(4); 1748 }); 1749 1750 it('batches optimistic mutation result application', () => { 1751 vi.useFakeTimers(); 1752 1753 const mutation = gql` 1754 mutation { 1755 concealAuthor { 1756 id 1757 name 1758 } 1759 } 1760 `; 1761 1762 const optimisticMutationData = { 1763 __typename: 'Mutation', 1764 concealAuthor: { 1765 __typename: 'Author', 1766 id: '123', 1767 name: '[REDACTED OFFLINE]', 1768 }, 1769 }; 1770 1771 const mutationData = { 1772 __typename: 'Mutation', 1773 concealAuthor: { 1774 __typename: 'Author', 1775 id: '123', 1776 name: '[REDACTED ONLINE]', 1777 }, 1778 }; 1779 1780 const client = createClient({ 1781 url: 'http://0.0.0.0', 1782 exchanges: [], 1783 }); 1784 const { source: ops$, next } = makeSubject<Operation>(); 1785 1786 const reexec = vi 1787 .spyOn(client, 'reexecuteOperation') 1788 .mockImplementation(next); 1789 1790 const opOne = client.createRequestOperation('query', { 1791 key: 1, 1792 query: queryOne, 1793 variables: undefined, 1794 }); 1795 1796 const opMutationOne = client.createRequestOperation('mutation', { 1797 key: 2, 1798 query: mutation, 1799 variables: undefined, 1800 }); 1801 1802 const opMutationTwo = client.createRequestOperation('mutation', { 1803 key: 3, 1804 query: mutation, 1805 variables: undefined, 1806 }); 1807 1808 const response = vi.fn((forwardOp: Operation): OperationResult => { 1809 if (forwardOp.key === 1) { 1810 return { ...queryResponse, operation: opOne, data: queryOneData }; 1811 } else if (forwardOp.key === 2) { 1812 return { 1813 ...queryResponse, 1814 operation: opMutationOne, 1815 data: mutationData, 1816 }; 1817 } else if (forwardOp.key === 3) { 1818 return { 1819 ...queryResponse, 1820 operation: opMutationTwo, 1821 data: mutationData, 1822 }; 1823 } 1824 1825 return undefined as any; 1826 }); 1827 1828 const result = vi.fn(); 1829 const forward: ExchangeIO = ops$ => 1830 pipe(ops$, delay(3), map(response), share); 1831 1832 const optimistic = { 1833 concealAuthor: vi.fn(() => optimisticMutationData.concealAuthor) as any, 1834 }; 1835 1836 pipe( 1837 cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), 1838 filter(x => x.operation.kind === 'mutation'), 1839 tap(result), 1840 publish 1841 ); 1842 1843 next(opOne); 1844 vi.runAllTimers(); 1845 expect(response).toHaveBeenCalledTimes(1); 1846 expect(result).toHaveBeenCalledTimes(0); 1847 1848 next(opMutationOne); 1849 vi.advanceTimersByTime(1); 1850 next(opMutationTwo); 1851 1852 expect(response).toHaveBeenCalledTimes(1); 1853 expect(optimistic.concealAuthor).toHaveBeenCalledTimes(2); 1854 expect(reexec).toHaveBeenCalledTimes(1); 1855 expect(result).toHaveBeenCalledTimes(0); 1856 1857 vi.advanceTimersByTime(2); 1858 expect(response).toHaveBeenCalledTimes(2); 1859 expect(reexec).toHaveBeenCalledTimes(2); 1860 expect(result).toHaveBeenCalledTimes(1); 1861 1862 vi.runAllTimers(); 1863 expect(response).toHaveBeenCalledTimes(3); 1864 expect(reexec).toHaveBeenCalledTimes(2); 1865 expect(result).toHaveBeenCalledTimes(2); 1866 }); 1867 1868 it('blocks refetches of overlapping queries', () => { 1869 vi.useFakeTimers(); 1870 1871 const mutation = gql` 1872 mutation { 1873 concealAuthor { 1874 id 1875 name 1876 } 1877 } 1878 `; 1879 1880 const optimisticMutationData = { 1881 __typename: 'Mutation', 1882 concealAuthor: { 1883 __typename: 'Author', 1884 id: '123', 1885 name: '[REDACTED OFFLINE]', 1886 }, 1887 }; 1888 1889 const client = createClient({ 1890 url: 'http://0.0.0.0', 1891 exchanges: [], 1892 }); 1893 const { source: ops$, next } = makeSubject<Operation>(); 1894 1895 const reexec = vi 1896 .spyOn(client, 'reexecuteOperation') 1897 .mockImplementation(next); 1898 1899 const opOne = client.createRequestOperation( 1900 'query', 1901 { 1902 key: 1, 1903 query: queryOne, 1904 variables: undefined, 1905 }, 1906 { 1907 requestPolicy: 'cache-and-network', 1908 } 1909 ); 1910 1911 const opMutation = client.createRequestOperation('mutation', { 1912 key: 2, 1913 query: mutation, 1914 variables: undefined, 1915 }); 1916 1917 const response = vi.fn((forwardOp: Operation): OperationResult => { 1918 if (forwardOp.key === 1) { 1919 return { ...queryResponse, operation: opOne, data: queryOneData }; 1920 } 1921 1922 return undefined as any; 1923 }); 1924 1925 const result = vi.fn(); 1926 const forward: ExchangeIO = ops$ => 1927 pipe( 1928 ops$, 1929 delay(1), 1930 filter(x => x.kind !== 'mutation'), 1931 map(response), 1932 share 1933 ); 1934 1935 const optimistic = { 1936 concealAuthor: vi.fn(() => optimisticMutationData.concealAuthor) as any, 1937 }; 1938 1939 pipe( 1940 cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), 1941 tap(result), 1942 publish 1943 ); 1944 1945 next(opOne); 1946 vi.runAllTimers(); 1947 expect(response).toHaveBeenCalledTimes(1); 1948 1949 next(opMutation); 1950 expect(response).toHaveBeenCalledTimes(1); 1951 expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1); 1952 expect(reexec).toHaveBeenCalledTimes(1); 1953 1954 expect(reexec.mock.calls[0][0]).toHaveProperty( 1955 'context.requestPolicy', 1956 'cache-first' 1957 ); 1958 1959 vi.runAllTimers(); 1960 expect(response).toHaveBeenCalledTimes(1); 1961 1962 next(opOne); 1963 expect(response).toHaveBeenCalledTimes(1); 1964 expect(reexec).toHaveBeenCalledTimes(1); 1965 }); 1966 1967 it('correctly clears on error', () => { 1968 vi.useFakeTimers(); 1969 1970 const authorsQuery = gql` 1971 query { 1972 authors { 1973 id 1974 name 1975 } 1976 } 1977 `; 1978 1979 const authorsQueryData = { 1980 __typename: 'Query', 1981 authors: [ 1982 { 1983 __typename: 'Author', 1984 id: '1', 1985 name: 'Author', 1986 }, 1987 ], 1988 }; 1989 1990 const mutation = gql` 1991 mutation { 1992 addAuthor { 1993 id 1994 name 1995 } 1996 } 1997 `; 1998 1999 const optimisticMutationData = { 2000 __typename: 'Mutation', 2001 addAuthor: { 2002 __typename: 'Author', 2003 id: '123', 2004 name: '[REDACTED OFFLINE]', 2005 }, 2006 }; 2007 2008 const client = createClient({ 2009 url: 'http://0.0.0.0', 2010 exchanges: [], 2011 }); 2012 const { source: ops$, next } = makeSubject<Operation>(); 2013 2014 const reexec = vi 2015 .spyOn(client, 'reexecuteOperation') 2016 .mockImplementation(next); 2017 2018 const opOne = client.createRequestOperation('query', { 2019 key: 1, 2020 query: authorsQuery, 2021 variables: undefined, 2022 }); 2023 2024 const opMutation = client.createRequestOperation('mutation', { 2025 key: 2, 2026 query: mutation, 2027 variables: undefined, 2028 }); 2029 2030 const response = vi.fn((forwardOp: Operation): OperationResult => { 2031 if (forwardOp.key === 1) { 2032 return { ...queryResponse, operation: opOne, data: authorsQueryData }; 2033 } else if (forwardOp.key === 2) { 2034 return { 2035 ...queryResponse, 2036 operation: opMutation, 2037 error: 'error' as any, 2038 data: { __typename: 'Mutation', addAuthor: null }, 2039 }; 2040 } 2041 2042 return undefined as any; 2043 }); 2044 2045 const result = vi.fn(); 2046 const forward: ExchangeIO = ops$ => 2047 pipe(ops$, delay(1), map(response), share); 2048 2049 const optimistic = { 2050 addAuthor: vi.fn(() => optimisticMutationData.addAuthor) as any, 2051 }; 2052 2053 const updates = { 2054 Mutation: { 2055 addAuthor: vi.fn((data, _, cache) => { 2056 cache.updateQuery({ query: authorsQuery }, (prevData: any) => ({ 2057 ...prevData, 2058 authors: [...prevData.authors, data.addAuthor], 2059 })); 2060 }), 2061 }, 2062 }; 2063 2064 pipe( 2065 cacheExchange({ optimistic, updates })({ 2066 forward, 2067 client, 2068 dispatchDebug, 2069 })(ops$), 2070 tap(result), 2071 publish 2072 ); 2073 2074 next(opOne); 2075 vi.runAllTimers(); 2076 expect(response).toHaveBeenCalledTimes(1); 2077 2078 next(opMutation); 2079 expect(response).toHaveBeenCalledTimes(1); 2080 expect(optimistic.addAuthor).toHaveBeenCalledTimes(1); 2081 expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(1); 2082 expect(reexec).toHaveBeenCalledTimes(1); 2083 2084 vi.runAllTimers(); 2085 2086 expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(2); 2087 expect(response).toHaveBeenCalledTimes(2); 2088 expect(result).toHaveBeenCalledTimes(4); 2089 expect(reexec).toHaveBeenCalledTimes(2); 2090 2091 next(opOne); 2092 vi.runAllTimers(); 2093 expect(result).toHaveBeenCalledTimes(5); 2094 }); 2095 2096 it('does not block subsequent query operations', () => { 2097 vi.useFakeTimers(); 2098 2099 const authorsQuery = gql` 2100 query { 2101 authors { 2102 id 2103 name 2104 } 2105 } 2106 `; 2107 2108 const authorsQueryData = { 2109 __typename: 'Query', 2110 authors: [ 2111 { 2112 __typename: 'Author', 2113 id: '123', 2114 name: 'Author', 2115 }, 2116 ], 2117 }; 2118 2119 const mutation = gql` 2120 mutation { 2121 deleteAuthor { 2122 id 2123 name 2124 } 2125 } 2126 `; 2127 2128 const optimisticMutationData = { 2129 __typename: 'Mutation', 2130 deleteAuthor: { 2131 __typename: 'Author', 2132 id: '123', 2133 name: '[REDACTED OFFLINE]', 2134 }, 2135 }; 2136 2137 const client = createClient({ 2138 url: 'http://0.0.0.0', 2139 exchanges: [], 2140 }); 2141 const { source: ops$, next } = makeSubject<Operation>(); 2142 2143 const reexec = vi 2144 .spyOn(client, 'reexecuteOperation') 2145 .mockImplementation(next); 2146 2147 const opOne = client.createRequestOperation('query', { 2148 key: 1, 2149 query: authorsQuery, 2150 variables: undefined, 2151 }); 2152 2153 const opMutation = client.createRequestOperation('mutation', { 2154 key: 2, 2155 query: mutation, 2156 variables: undefined, 2157 }); 2158 2159 const response = vi.fn((forwardOp: Operation): OperationResult => { 2160 if (forwardOp.key === 1) { 2161 return { ...queryResponse, operation: opOne, data: authorsQueryData }; 2162 } else if (forwardOp.key === 2) { 2163 return { 2164 ...queryResponse, 2165 operation: opMutation, 2166 data: { 2167 __typename: 'Mutation', 2168 deleteAuthor: optimisticMutationData.deleteAuthor, 2169 }, 2170 }; 2171 } 2172 2173 return undefined as any; 2174 }); 2175 2176 const result = vi.fn(); 2177 const forward: ExchangeIO = ops$ => 2178 pipe(ops$, delay(1), map(response), share); 2179 2180 const optimistic = { 2181 deleteAuthor: vi.fn(() => optimisticMutationData.deleteAuthor) as any, 2182 }; 2183 2184 const updates = { 2185 Mutation: { 2186 deleteAuthor: vi.fn((_data, _, cache) => { 2187 cache.invalidate({ 2188 __typename: 'Author', 2189 id: optimisticMutationData.deleteAuthor.id, 2190 }); 2191 }), 2192 }, 2193 }; 2194 2195 pipe( 2196 cacheExchange({ optimistic, updates })({ 2197 forward, 2198 client, 2199 dispatchDebug, 2200 })(ops$), 2201 tap(result), 2202 publish 2203 ); 2204 2205 next(opOne); 2206 vi.runAllTimers(); 2207 expect(response).toHaveBeenCalledTimes(1); 2208 expect(result).toHaveBeenCalledTimes(1); 2209 2210 next(opMutation); 2211 expect(response).toHaveBeenCalledTimes(1); 2212 expect(optimistic.deleteAuthor).toHaveBeenCalledTimes(1); 2213 expect(updates.Mutation.deleteAuthor).toHaveBeenCalledTimes(1); 2214 expect(reexec).toHaveBeenCalledTimes(1); 2215 expect(result).toHaveBeenCalledTimes(1); 2216 2217 vi.runAllTimers(); 2218 2219 expect(updates.Mutation.deleteAuthor).toHaveBeenCalledTimes(2); 2220 expect(response).toHaveBeenCalledTimes(2); 2221 expect(result).toHaveBeenCalledTimes(2); 2222 expect(reexec).toHaveBeenCalledTimes(2); 2223 expect(reexec.mock.calls[1][0]).toMatchObject(opOne); 2224 2225 next(opOne); 2226 vi.runAllTimers(); 2227 expect(result).toHaveBeenCalledTimes(3); 2228 }); 2229}); 2230 2231describe('mutation updates', () => { 2232 it('invalidates the type when the entity is not present in the cache', () => { 2233 vi.useFakeTimers(); 2234 2235 const authorsQuery = gql` 2236 query { 2237 authors { 2238 id 2239 name 2240 } 2241 } 2242 `; 2243 2244 const authorsQueryData = { 2245 __typename: 'Query', 2246 authors: [ 2247 { 2248 __typename: 'Author', 2249 id: '1', 2250 name: 'Author', 2251 }, 2252 ], 2253 }; 2254 2255 const mutation = gql` 2256 mutation { 2257 addAuthor { 2258 id 2259 name 2260 } 2261 } 2262 `; 2263 2264 const client = createClient({ 2265 url: 'http://0.0.0.0', 2266 exchanges: [], 2267 }); 2268 const { source: ops$, next } = makeSubject<Operation>(); 2269 2270 const reexec = vi 2271 .spyOn(client, 'reexecuteOperation') 2272 .mockImplementation(next); 2273 2274 const opOne = client.createRequestOperation('query', { 2275 key: 1, 2276 query: authorsQuery, 2277 variables: undefined, 2278 }); 2279 2280 const opMutation = client.createRequestOperation('mutation', { 2281 key: 2, 2282 query: mutation, 2283 variables: undefined, 2284 }); 2285 2286 const response = vi.fn((forwardOp: Operation): OperationResult => { 2287 if (forwardOp.key === 1) { 2288 return { ...queryResponse, operation: opOne, data: authorsQueryData }; 2289 } else if (forwardOp.key === 2) { 2290 return { 2291 ...queryResponse, 2292 operation: opMutation, 2293 data: { 2294 __typename: 'Mutation', 2295 addAuthor: { id: '2', name: 'Author 2', __typename: 'Author' }, 2296 }, 2297 }; 2298 } 2299 2300 return undefined as any; 2301 }); 2302 2303 const result = vi.fn(); 2304 const forward: ExchangeIO = ops$ => 2305 pipe(ops$, delay(1), map(response), share); 2306 2307 pipe( 2308 cacheExchange()({ 2309 forward, 2310 client, 2311 dispatchDebug, 2312 })(ops$), 2313 tap(result), 2314 publish 2315 ); 2316 2317 next(opOne); 2318 vi.runAllTimers(); 2319 expect(response).toHaveBeenCalledTimes(1); 2320 2321 next(opMutation); 2322 expect(response).toHaveBeenCalledTimes(1); 2323 expect(reexec).toHaveBeenCalledTimes(0); 2324 2325 vi.runAllTimers(); 2326 2327 expect(response).toHaveBeenCalledTimes(2); 2328 expect(result).toHaveBeenCalledTimes(2); 2329 expect(reexec).toHaveBeenCalledTimes(1); 2330 2331 next(opOne); 2332 vi.runAllTimers(); 2333 expect(response).toHaveBeenCalledTimes(3); 2334 expect(result).toHaveBeenCalledTimes(3); 2335 expect(result.mock.calls[1][0].data).toEqual({ 2336 addAuthor: { 2337 id: '2', 2338 name: 'Author 2', 2339 }, 2340 }); 2341 }); 2342}); 2343 2344describe('extra variables', () => { 2345 it('allows extra variables to be applied to updates', () => { 2346 vi.useFakeTimers(); 2347 2348 const mutation = gql` 2349 mutation TestMutation($test: Boolean) { 2350 test(test: $test) { 2351 id 2352 } 2353 } 2354 `; 2355 2356 const mutationData = { 2357 __typename: 'Mutation', 2358 test: { 2359 __typename: 'Author', 2360 id: '123', 2361 }, 2362 }; 2363 2364 const client = createClient({ 2365 url: 'http://0.0.0.0', 2366 exchanges: [], 2367 }); 2368 2369 const { source: ops$, next } = makeSubject<Operation>(); 2370 2371 const opQuery = client.createRequestOperation('query', { 2372 key: 1, 2373 query: queryOne, 2374 variables: undefined, 2375 }); 2376 2377 const opMutation = client.createRequestOperation('mutation', { 2378 key: 2, 2379 query: mutation, 2380 variables: { 2381 test: true, 2382 extra: 'extra', 2383 }, 2384 }); 2385 2386 const response = vi.fn((forwardOp: Operation): OperationResult => { 2387 if (forwardOp.key === 1) { 2388 return { ...queryResponse, operation: forwardOp, data: queryOneData }; 2389 } else if (forwardOp.key === 2) { 2390 return { 2391 ...queryResponse, 2392 operation: forwardOp, 2393 data: mutationData, 2394 }; 2395 } 2396 2397 return undefined as any; 2398 }); 2399 2400 const result = vi.fn(); 2401 const forward: ExchangeIO = ops$ => 2402 pipe(ops$, delay(3), map(response), share); 2403 2404 const optimistic = { 2405 test: vi.fn() as any, 2406 }; 2407 2408 const updates = { 2409 Mutation: { 2410 test: vi.fn() as any, 2411 }, 2412 }; 2413 2414 pipe( 2415 cacheExchange({ optimistic, updates })({ 2416 forward, 2417 client, 2418 dispatchDebug, 2419 })(ops$), 2420 filter(x => x.operation.kind === 'mutation'), 2421 tap(result), 2422 publish 2423 ); 2424 2425 next(opQuery); 2426 vi.runAllTimers(); 2427 expect(response).toHaveBeenCalledTimes(1); 2428 expect(result).toHaveBeenCalledTimes(0); 2429 2430 next(opMutation); 2431 vi.advanceTimersByTime(1); 2432 2433 expect(response).toHaveBeenCalledTimes(1); 2434 expect(result).toHaveBeenCalledTimes(0); 2435 expect(optimistic.test).toHaveBeenCalledTimes(1); 2436 2437 expect(optimistic.test.mock.calls[0][2].variables).toEqual({ 2438 test: true, 2439 extra: 'extra', 2440 }); 2441 2442 vi.runAllTimers(); 2443 2444 expect(response).toHaveBeenCalledTimes(2); 2445 expect(result).toHaveBeenCalledTimes(1); 2446 expect(updates.Mutation.test).toHaveBeenCalledTimes(2); 2447 2448 expect(updates.Mutation.test.mock.calls[1][3].variables).toEqual({ 2449 test: true, 2450 extra: 'extra', 2451 }); 2452 }); 2453}); 2454 2455describe('custom resolvers', () => { 2456 it('follows resolvers on initial write', () => { 2457 const client = createClient({ 2458 url: 'http://0.0.0.0', 2459 exchanges: [], 2460 }); 2461 const { source: ops$, next } = makeSubject<Operation>(); 2462 2463 const opOne = client.createRequestOperation('query', { 2464 key: 1, 2465 query: queryOne, 2466 variables: undefined, 2467 }); 2468 2469 const response = vi.fn((forwardOp: Operation): OperationResult => { 2470 if (forwardOp.key === 1) { 2471 return { ...queryResponse, operation: opOne, data: queryOneData }; 2472 } 2473 2474 return undefined as any; 2475 }); 2476 2477 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 2478 2479 const result = vi.fn(); 2480 const fakeResolver = vi.fn(); 2481 2482 pipe( 2483 cacheExchange({ 2484 resolvers: { 2485 Author: { 2486 name: () => { 2487 fakeResolver(); 2488 return 'newName'; 2489 }, 2490 }, 2491 }, 2492 })({ forward, client, dispatchDebug })(ops$), 2493 tap(result), 2494 publish 2495 ); 2496 2497 next(opOne); 2498 expect(response).toHaveBeenCalledTimes(1); 2499 expect(fakeResolver).toHaveBeenCalledTimes(1); 2500 expect(result).toHaveBeenCalledTimes(1); 2501 expect(result.mock.calls[0][0].data).toMatchObject({ 2502 author: { 2503 id: '123', 2504 name: 'newName', 2505 }, 2506 }); 2507 }); 2508 2509 it('follows resolvers for mutations', () => { 2510 vi.useFakeTimers(); 2511 2512 const mutation = gql` 2513 mutation { 2514 concealAuthor { 2515 id 2516 name 2517 __typename 2518 } 2519 } 2520 `; 2521 2522 const mutationData = { 2523 __typename: 'Mutation', 2524 concealAuthor: { 2525 __typename: 'Author', 2526 id: '123', 2527 name: '[REDACTED ONLINE]', 2528 }, 2529 }; 2530 2531 const client = createClient({ 2532 url: 'http://0.0.0.0', 2533 exchanges: [], 2534 }); 2535 const { source: ops$, next } = makeSubject<Operation>(); 2536 2537 const opOne = client.createRequestOperation('query', { 2538 key: 1, 2539 query: queryOne, 2540 variables: undefined, 2541 }); 2542 2543 const opMutation = client.createRequestOperation('mutation', { 2544 key: 2, 2545 query: mutation, 2546 variables: undefined, 2547 }); 2548 2549 const response = vi.fn((forwardOp: Operation): OperationResult => { 2550 if (forwardOp.key === 1) { 2551 return { ...queryResponse, operation: opOne, data: queryOneData }; 2552 } else if (forwardOp.key === 2) { 2553 return { 2554 ...queryResponse, 2555 operation: opMutation, 2556 data: mutationData, 2557 }; 2558 } 2559 2560 return undefined as any; 2561 }); 2562 2563 const result = vi.fn(); 2564 const forward: ExchangeIO = ops$ => 2565 pipe(ops$, delay(1), map(response), share); 2566 2567 const fakeResolver = vi.fn(); 2568 2569 pipe( 2570 cacheExchange({ 2571 resolvers: { 2572 Author: { 2573 name: () => { 2574 fakeResolver(); 2575 return 'newName'; 2576 }, 2577 }, 2578 }, 2579 })({ forward, client, dispatchDebug })(ops$), 2580 tap(result), 2581 publish 2582 ); 2583 2584 next(opOne); 2585 vi.runAllTimers(); 2586 expect(response).toHaveBeenCalledTimes(1); 2587 2588 next(opMutation); 2589 expect(response).toHaveBeenCalledTimes(1); 2590 expect(fakeResolver).toHaveBeenCalledTimes(1); 2591 2592 vi.runAllTimers(); 2593 expect(result.mock.calls[1][0].data).toEqual({ 2594 concealAuthor: { 2595 __typename: 'Author', 2596 id: '123', 2597 name: 'newName', 2598 }, 2599 }); 2600 }); 2601 2602 it('follows nested resolvers for mutations', () => { 2603 vi.useFakeTimers(); 2604 2605 const mutation = gql` 2606 mutation { 2607 concealAuthors { 2608 id 2609 name 2610 book { 2611 id 2612 title 2613 __typename 2614 } 2615 __typename 2616 } 2617 } 2618 `; 2619 2620 const client = createClient({ 2621 url: 'http://0.0.0.0', 2622 exchanges: [], 2623 }); 2624 const { source: ops$, next } = makeSubject<Operation>(); 2625 2626 const query = gql` 2627 query { 2628 authors { 2629 id 2630 name 2631 book { 2632 id 2633 title 2634 __typename 2635 } 2636 __typename 2637 } 2638 } 2639 `; 2640 2641 const queryOperation = client.createRequestOperation('query', { 2642 key: 1, 2643 query, 2644 variables: undefined, 2645 }); 2646 2647 const mutationOperation = client.createRequestOperation('mutation', { 2648 key: 2, 2649 query: mutation, 2650 variables: undefined, 2651 }); 2652 2653 const mutationData = { 2654 __typename: 'Mutation', 2655 concealAuthors: [ 2656 { 2657 __typename: 'Author', 2658 id: '123', 2659 book: null, 2660 name: '[REDACTED ONLINE]', 2661 }, 2662 { 2663 __typename: 'Author', 2664 id: '456', 2665 name: 'Formidable', 2666 book: { 2667 id: '1', 2668 title: 'AwesomeGQL', 2669 __typename: 'Book', 2670 }, 2671 }, 2672 ], 2673 }; 2674 2675 const queryData = { 2676 __typename: 'Query', 2677 authors: [ 2678 { 2679 __typename: 'Author', 2680 id: '123', 2681 name: '[REDACTED ONLINE]', 2682 book: null, 2683 }, 2684 { 2685 __typename: 'Author', 2686 id: '456', 2687 name: 'Formidable', 2688 book: { 2689 id: '1', 2690 title: 'AwesomeGQL', 2691 __typename: 'Book', 2692 }, 2693 }, 2694 ], 2695 }; 2696 2697 const response = vi.fn((forwardOp: Operation): OperationResult => { 2698 if (forwardOp.key === 1) { 2699 return { 2700 ...queryResponse, 2701 operation: queryOperation, 2702 data: queryData, 2703 }; 2704 } else if (forwardOp.key === 2) { 2705 return { 2706 ...queryResponse, 2707 operation: mutationOperation, 2708 data: mutationData, 2709 }; 2710 } 2711 2712 return undefined as any; 2713 }); 2714 2715 const result = vi.fn(); 2716 const forward: ExchangeIO = ops$ => 2717 pipe(ops$, delay(1), map(response), share); 2718 2719 const fakeResolver = vi.fn(); 2720 const called: any[] = []; 2721 2722 pipe( 2723 cacheExchange({ 2724 resolvers: { 2725 Query: { 2726 // TS-check 2727 author: (_parent, args) => ({ __typename: 'Author', id: args.id }), 2728 }, 2729 Author: { 2730 name: parent => { 2731 called.push(parent.name); 2732 fakeResolver(); 2733 return 'Secret Author'; 2734 }, 2735 }, 2736 Book: { 2737 title: parent => { 2738 called.push(parent.title); 2739 fakeResolver(); 2740 return 'Secret Book'; 2741 }, 2742 }, 2743 }, 2744 })({ forward, client, dispatchDebug })(ops$), 2745 tap(result), 2746 publish 2747 ); 2748 2749 next(queryOperation); 2750 vi.runAllTimers(); 2751 expect(response).toHaveBeenCalledTimes(1); 2752 expect(fakeResolver).toHaveBeenCalledTimes(3); 2753 2754 next(mutationOperation); 2755 vi.runAllTimers(); 2756 expect(response).toHaveBeenCalledTimes(2); 2757 expect(fakeResolver).toHaveBeenCalledTimes(6); 2758 expect(result.mock.calls[1][0].data).toEqual({ 2759 concealAuthors: [ 2760 { 2761 __typename: 'Author', 2762 id: '123', 2763 book: null, 2764 name: 'Secret Author', 2765 }, 2766 { 2767 __typename: 'Author', 2768 id: '456', 2769 name: 'Secret Author', 2770 book: { 2771 id: '1', 2772 title: 'Secret Book', 2773 __typename: 'Book', 2774 }, 2775 }, 2776 ], 2777 }); 2778 2779 expect(called).toEqual([ 2780 // Query 2781 '[REDACTED ONLINE]', 2782 'Formidable', 2783 'AwesomeGQL', 2784 // Mutation 2785 '[REDACTED ONLINE]', 2786 'Formidable', 2787 'AwesomeGQL', 2788 ]); 2789 }); 2790}); 2791 2792describe('schema awareness', () => { 2793 it('reexecutes query and returns data on partial result', () => { 2794 vi.useFakeTimers(); 2795 const client = createClient({ 2796 url: 'http://0.0.0.0', 2797 exchanges: [], 2798 }); 2799 const { source: ops$, next } = makeSubject<Operation>(); 2800 const reexec = vi 2801 .spyOn(client, 'reexecuteOperation') 2802 // Empty mock to avoid going in an endless loop, since we would again return 2803 // partial data. 2804 .mockImplementation(() => undefined); 2805 2806 const initialQuery = gql` 2807 query { 2808 todos { 2809 id 2810 text 2811 __typename 2812 } 2813 } 2814 `; 2815 2816 const query = gql` 2817 query { 2818 todos { 2819 id 2820 text 2821 complete 2822 author { 2823 id 2824 name 2825 __typename 2826 } 2827 __typename 2828 } 2829 } 2830 `; 2831 2832 const initialQueryOperation = client.createRequestOperation('query', { 2833 key: 1, 2834 query: initialQuery, 2835 variables: undefined, 2836 }); 2837 2838 const queryOperation = client.createRequestOperation('query', { 2839 key: 2, 2840 query, 2841 variables: undefined, 2842 }); 2843 2844 const queryData = { 2845 __typename: 'Query', 2846 todos: [ 2847 { 2848 __typename: 'Todo', 2849 id: '123', 2850 text: 'Learn', 2851 }, 2852 { 2853 __typename: 'Todo', 2854 id: '456', 2855 text: 'Teach', 2856 }, 2857 ], 2858 }; 2859 2860 const response = vi.fn((forwardOp: Operation): OperationResult => { 2861 if (forwardOp.key === 1) { 2862 return { 2863 ...queryResponse, 2864 operation: initialQueryOperation, 2865 data: queryData, 2866 }; 2867 } else if (forwardOp.key === 2) { 2868 return { 2869 ...queryResponse, 2870 operation: queryOperation, 2871 data: queryData, 2872 }; 2873 } 2874 2875 return undefined as any; 2876 }); 2877 2878 const result = vi.fn(); 2879 const forward: ExchangeIO = ops$ => 2880 pipe(ops$, delay(1), map(response), share); 2881 2882 pipe( 2883 cacheExchange({ 2884 schema: minifyIntrospectionQuery( 2885 // eslint-disable-next-line 2886 require('./test-utils/simple_schema.json') 2887 ), 2888 })({ forward, client, dispatchDebug })(ops$), 2889 tap(result), 2890 publish 2891 ); 2892 2893 next(initialQueryOperation); 2894 vi.runAllTimers(); 2895 expect(response).toHaveBeenCalledTimes(1); 2896 expect(reexec).toHaveBeenCalledTimes(0); 2897 expect(result.mock.calls[0][0].data).toMatchObject({ 2898 todos: [ 2899 { 2900 __typename: 'Todo', 2901 id: '123', 2902 text: 'Learn', 2903 }, 2904 { 2905 __typename: 'Todo', 2906 id: '456', 2907 text: 'Teach', 2908 }, 2909 ], 2910 }); 2911 2912 next(queryOperation); 2913 vi.runAllTimers(); 2914 expect(result).toHaveBeenCalledTimes(2); 2915 expect(reexec).toHaveBeenCalledTimes(1); 2916 expect(result.mock.calls[1][0].stale).toBe(true); 2917 expect(result.mock.calls[1][0].data).toEqual({ 2918 todos: [ 2919 { 2920 __typename: 'Todo', 2921 author: null, 2922 complete: null, 2923 id: '123', 2924 text: 'Learn', 2925 }, 2926 { 2927 __typename: 'Todo', 2928 author: null, 2929 complete: null, 2930 id: '456', 2931 text: 'Teach', 2932 }, 2933 ], 2934 }); 2935 2936 expect(result.mock.calls[1][0]).toHaveProperty( 2937 'operation.context.meta.cacheOutcome', 2938 'partial' 2939 ); 2940 }); 2941 2942 it('reexecutes query and returns data on partial results for nullable lists', () => { 2943 vi.useFakeTimers(); 2944 const client = createClient({ 2945 url: 'http://0.0.0.0', 2946 exchanges: [], 2947 }); 2948 const { source: ops$, next } = makeSubject<Operation>(); 2949 const reexec = vi 2950 .spyOn(client, 'reexecuteOperation') 2951 // Empty mock to avoid going in an endless loop, since we would again return 2952 // partial data. 2953 .mockImplementation(() => undefined); 2954 2955 const initialQuery = gql` 2956 query { 2957 todos { 2958 id 2959 __typename 2960 } 2961 } 2962 `; 2963 2964 const query = gql` 2965 query { 2966 todos { 2967 id 2968 text 2969 __typename 2970 } 2971 } 2972 `; 2973 2974 const initialQueryOperation = client.createRequestOperation('query', { 2975 key: 1, 2976 query: initialQuery, 2977 variables: undefined, 2978 }); 2979 2980 const queryOperation = client.createRequestOperation('query', { 2981 key: 2, 2982 query, 2983 variables: undefined, 2984 }); 2985 2986 const queryData = { 2987 __typename: 'Query', 2988 todos: [ 2989 { 2990 __typename: 'Todo', 2991 id: '123', 2992 }, 2993 { 2994 __typename: 'Todo', 2995 id: '456', 2996 }, 2997 ], 2998 }; 2999 3000 const response = vi.fn((forwardOp: Operation): OperationResult => { 3001 if (forwardOp.key === 1) { 3002 return { 3003 ...queryResponse, 3004 operation: initialQueryOperation, 3005 data: queryData, 3006 }; 3007 } else if (forwardOp.key === 2) { 3008 return { 3009 ...queryResponse, 3010 operation: queryOperation, 3011 data: queryData, 3012 }; 3013 } 3014 3015 return undefined as any; 3016 }); 3017 3018 const result = vi.fn(); 3019 const forward: ExchangeIO = ops$ => 3020 pipe(ops$, delay(1), map(response), share); 3021 3022 pipe( 3023 cacheExchange({ 3024 schema: minifyIntrospectionQuery( 3025 // eslint-disable-next-line 3026 require('./test-utils/simple_schema.json') 3027 ), 3028 })({ forward, client, dispatchDebug })(ops$), 3029 tap(result), 3030 publish 3031 ); 3032 3033 next(initialQueryOperation); 3034 vi.runAllTimers(); 3035 expect(response).toHaveBeenCalledTimes(1); 3036 expect(reexec).toHaveBeenCalledTimes(0); 3037 expect(result.mock.calls[0][0].data).toMatchObject({ 3038 todos: [ 3039 { 3040 __typename: 'Todo', 3041 id: '123', 3042 }, 3043 { 3044 __typename: 'Todo', 3045 id: '456', 3046 }, 3047 ], 3048 }); 3049 3050 next(queryOperation); 3051 vi.runAllTimers(); 3052 expect(result).toHaveBeenCalledTimes(2); 3053 expect(reexec).toHaveBeenCalledTimes(1); 3054 expect(result.mock.calls[1][0].stale).toBe(true); 3055 expect(result.mock.calls[1][0].data).toEqual({ 3056 todos: [null, null], 3057 }); 3058 3059 expect(result.mock.calls[1][0]).toHaveProperty( 3060 'operation.context.meta.cacheOutcome', 3061 'partial' 3062 ); 3063 }); 3064}); 3065 3066describe('looping protection', () => { 3067 it('applies stale to blocked looping queries', () => { 3068 let normalData: OperationResult | undefined; 3069 let extendedData: OperationResult | undefined; 3070 3071 const client = createClient({ 3072 url: 'http://0.0.0.0', 3073 exchanges: [], 3074 }); 3075 3076 const { source: ops$, next: nextOp } = makeSubject<Operation>(); 3077 const { source: res$, next: nextRes } = makeSubject<OperationResult>(); 3078 3079 vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp); 3080 3081 const normalQuery = gql` 3082 { 3083 __typename 3084 item { 3085 __typename 3086 id 3087 } 3088 } 3089 `; 3090 3091 const extendedQuery = gql` 3092 { 3093 __typename 3094 item { 3095 __typename 3096 extended: id 3097 extra @_optional 3098 } 3099 } 3100 `; 3101 3102 const forward = (ops$: Source<Operation>): Source<OperationResult> => 3103 share( 3104 merge([ 3105 pipe( 3106 ops$, 3107 filter(() => false) 3108 ) as any, 3109 res$, 3110 ]) 3111 ); 3112 3113 pipe( 3114 cacheExchange()({ forward, client, dispatchDebug })(ops$), 3115 tap(result => { 3116 if (result.operation.kind === 'query') { 3117 if (result.operation.key === 1) { 3118 normalData = result; 3119 } else if (result.operation.key === 2) { 3120 extendedData = result; 3121 } 3122 } 3123 }), 3124 publish 3125 ); 3126 3127 const normalOp = client.createRequestOperation( 3128 'query', 3129 { 3130 key: 1, 3131 query: normalQuery, 3132 variables: undefined, 3133 }, 3134 { 3135 requestPolicy: 'cache-first', 3136 } 3137 ); 3138 3139 const extendedOp = client.createRequestOperation( 3140 'query', 3141 { 3142 key: 2, 3143 query: extendedQuery, 3144 variables: undefined, 3145 }, 3146 { 3147 requestPolicy: 'cache-first', 3148 } 3149 ); 3150 3151 nextOp(normalOp); 3152 3153 nextRes({ 3154 operation: normalOp, 3155 data: { 3156 __typename: 'Query', 3157 item: { 3158 __typename: 'Node', 3159 id: 'id', 3160 }, 3161 }, 3162 stale: false, 3163 hasNext: false, 3164 }); 3165 3166 expect(normalData).toMatchObject({ stale: false }); 3167 expect(client.reexecuteOperation).toHaveBeenCalledTimes(0); 3168 3169 nextOp(extendedOp); 3170 3171 expect(extendedData).toMatchObject({ stale: true }); 3172 expect(client.reexecuteOperation).toHaveBeenCalledTimes(1); 3173 3174 // Out of band re-execute first operation 3175 nextOp(normalOp); 3176 nextRes({ 3177 ...queryResponse, 3178 operation: normalOp, 3179 data: { 3180 __typename: 'Query', 3181 item: { 3182 __typename: 'Node', 3183 id: 'id', 3184 }, 3185 }, 3186 }); 3187 3188 expect(normalData).toMatchObject({ stale: false }); 3189 expect(extendedData).toMatchObject({ stale: true }); 3190 expect(client.reexecuteOperation).toHaveBeenCalledTimes(3); 3191 3192 nextOp(extendedOp); 3193 3194 expect(normalData).toMatchObject({ stale: false }); 3195 expect(extendedData).toMatchObject({ stale: true }); 3196 expect(client.reexecuteOperation).toHaveBeenCalledTimes(3); 3197 3198 nextRes({ 3199 ...queryResponse, 3200 operation: extendedOp, 3201 data: { 3202 __typename: 'Query', 3203 item: { 3204 __typename: 'Node', 3205 extended: 'id', 3206 extra: 'extra', 3207 }, 3208 }, 3209 }); 3210 3211 expect(extendedData).toMatchObject({ stale: false }); 3212 expect(client.reexecuteOperation).toHaveBeenCalledTimes(4); 3213 }); 3214}); 3215 3216describe('commutativity', () => { 3217 it('applies results that come in out-of-order commutatively and consistently', () => { 3218 vi.useFakeTimers(); 3219 3220 let data: any; 3221 3222 const client = createClient({ 3223 url: 'http://0.0.0.0', 3224 requestPolicy: 'cache-and-network', 3225 exchanges: [], 3226 }); 3227 const { source: ops$, next: next } = makeSubject<Operation>(); 3228 const query = gql` 3229 { 3230 index 3231 } 3232 `; 3233 3234 const result = (operation: Operation): Source<OperationResult> => 3235 pipe( 3236 fromValue({ 3237 ...queryResponse, 3238 operation, 3239 data: { 3240 __typename: 'Query', 3241 index: operation.key, 3242 }, 3243 }), 3244 delay(operation.key === 2 ? 5 : operation.key * 10) 3245 ); 3246 3247 const output = vi.fn(result => { 3248 data = result.data; 3249 }); 3250 3251 const forward = (ops$: Source<Operation>): Source<OperationResult> => 3252 pipe( 3253 ops$, 3254 filter(op => op.kind !== 'teardown'), 3255 mergeMap(result) 3256 ); 3257 3258 pipe( 3259 cacheExchange()({ forward, client, dispatchDebug })(ops$), 3260 tap(output), 3261 publish 3262 ); 3263 3264 next( 3265 client.createRequestOperation('query', { 3266 key: 1, 3267 query, 3268 variables: undefined, 3269 }) 3270 ); 3271 3272 next( 3273 client.createRequestOperation('query', { 3274 key: 2, 3275 query, 3276 variables: undefined, 3277 }) 3278 ); 3279 3280 // This shouldn't have any effect: 3281 next( 3282 client.createRequestOperation('teardown', { 3283 key: 2, 3284 query, 3285 variables: undefined, 3286 }) 3287 ); 3288 3289 next( 3290 client.createRequestOperation('query', { 3291 key: 3, 3292 query, 3293 variables: undefined, 3294 }) 3295 ); 3296 3297 vi.advanceTimersByTime(5); 3298 expect(output).toHaveBeenCalledTimes(1); 3299 expect(data.index).toBe(2); 3300 3301 vi.advanceTimersByTime(10); 3302 expect(output).toHaveBeenCalledTimes(2); 3303 expect(data.index).toBe(2); 3304 3305 vi.advanceTimersByTime(30); 3306 expect(output).toHaveBeenCalledTimes(3); 3307 expect(data.index).toBe(3); 3308 }); 3309 3310 it('applies optimistic updates on top of commutative queries as query result comes in', () => { 3311 let data: any; 3312 const client = createClient({ 3313 url: 'http://0.0.0.0', 3314 exchanges: [], 3315 }); 3316 const { source: ops$, next: nextOp } = makeSubject<Operation>(); 3317 const { source: res$, next: nextRes } = makeSubject<OperationResult>(); 3318 3319 const reexec = vi 3320 .spyOn(client, 'reexecuteOperation') 3321 .mockImplementation(nextOp); 3322 3323 const query = gql` 3324 { 3325 node { 3326 id 3327 name 3328 } 3329 } 3330 `; 3331 3332 const mutation = gql` 3333 mutation { 3334 node { 3335 id 3336 name 3337 } 3338 } 3339 `; 3340 3341 const forward = (ops$: Source<Operation>): Source<OperationResult> => 3342 share( 3343 merge([ 3344 pipe( 3345 ops$, 3346 filter(() => false) 3347 ) as any, 3348 res$, 3349 ]) 3350 ); 3351 3352 const optimistic = { 3353 node: () => ({ 3354 __typename: 'Node', 3355 id: 'node', 3356 name: 'optimistic', 3357 }), 3358 }; 3359 3360 pipe( 3361 cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), 3362 tap(result => { 3363 if (result.operation.kind === 'query') { 3364 data = result.data; 3365 } 3366 }), 3367 publish 3368 ); 3369 3370 const queryOpA = client.createRequestOperation('query', { 3371 key: 1, 3372 query, 3373 variables: undefined, 3374 }); 3375 3376 const mutationOp = client.createRequestOperation('mutation', { 3377 key: 2, 3378 query: mutation, 3379 variables: undefined, 3380 }); 3381 3382 expect(data).toBe(undefined); 3383 3384 nextOp(queryOpA); 3385 3386 nextRes({ 3387 ...queryResponse, 3388 operation: queryOpA, 3389 data: { 3390 __typename: 'Query', 3391 node: { 3392 __typename: 'Node', 3393 id: 'node', 3394 name: 'query a', 3395 }, 3396 }, 3397 }); 3398 3399 expect(data).toHaveProperty('node.name', 'query a'); 3400 3401 nextOp(mutationOp); 3402 expect(reexec).toHaveBeenCalledTimes(1); 3403 expect(data).toHaveProperty('node.name', 'optimistic'); 3404 }); 3405 3406 it('applies mutation results on top of commutative queries', () => { 3407 let data: any; 3408 const client = createClient({ 3409 url: 'http://0.0.0.0', 3410 exchanges: [], 3411 }); 3412 const { source: ops$, next: nextOp } = makeSubject<Operation>(); 3413 const { source: res$, next: nextRes } = makeSubject<OperationResult>(); 3414 3415 const reexec = vi 3416 .spyOn(client, 'reexecuteOperation') 3417 .mockImplementation(nextOp); 3418 3419 const query = gql` 3420 { 3421 node { 3422 id 3423 name 3424 } 3425 } 3426 `; 3427 3428 const mutation = gql` 3429 mutation { 3430 node { 3431 id 3432 name 3433 } 3434 } 3435 `; 3436 3437 const forward = (ops$: Source<Operation>): Source<OperationResult> => 3438 share( 3439 merge([ 3440 pipe( 3441 ops$, 3442 filter(() => false) 3443 ) as any, 3444 res$, 3445 ]) 3446 ); 3447 3448 pipe( 3449 cacheExchange()({ forward, client, dispatchDebug })(ops$), 3450 tap(result => { 3451 if (result.operation.kind === 'query') { 3452 data = result.data; 3453 } 3454 }), 3455 publish 3456 ); 3457 3458 const queryOpA = client.createRequestOperation('query', { 3459 key: 1, 3460 query, 3461 variables: undefined, 3462 }); 3463 3464 const mutationOp = client.createRequestOperation('mutation', { 3465 key: 2, 3466 query: mutation, 3467 variables: undefined, 3468 }); 3469 3470 const queryOpB = client.createRequestOperation('query', { 3471 key: 3, 3472 query, 3473 variables: undefined, 3474 }); 3475 3476 expect(data).toBe(undefined); 3477 3478 nextOp(queryOpA); 3479 nextOp(mutationOp); 3480 nextOp(queryOpB); 3481 3482 nextRes({ 3483 ...queryResponse, 3484 operation: queryOpA, 3485 data: { 3486 __typename: 'Query', 3487 node: { 3488 __typename: 'Node', 3489 id: 'node', 3490 name: 'query a', 3491 }, 3492 }, 3493 }); 3494 3495 expect(data).toHaveProperty('node.name', 'query a'); 3496 3497 nextRes({ 3498 ...queryResponse, 3499 operation: mutationOp, 3500 data: { 3501 __typename: 'Mutation', 3502 node: { 3503 __typename: 'Node', 3504 id: 'node', 3505 name: 'mutation', 3506 }, 3507 }, 3508 }); 3509 3510 expect(reexec).toHaveBeenCalledTimes(3); 3511 expect(data).toHaveProperty('node.name', 'mutation'); 3512 3513 nextRes({ 3514 ...queryResponse, 3515 operation: queryOpB, 3516 data: { 3517 __typename: 'Query', 3518 node: { 3519 __typename: 'Node', 3520 id: 'node', 3521 name: 'query b', 3522 }, 3523 }, 3524 }); 3525 3526 expect(reexec).toHaveBeenCalledTimes(4); 3527 expect(data).toHaveProperty('node.name', 'mutation'); 3528 }); 3529 3530 it('applies optimistic updates on top of commutative queries until mutation resolves', () => { 3531 let data: any; 3532 const client = createClient({ 3533 url: 'http://0.0.0.0', 3534 exchanges: [], 3535 }); 3536 const { source: ops$, next: nextOp } = makeSubject<Operation>(); 3537 const { source: res$, next: nextRes } = makeSubject<OperationResult>(); 3538 3539 vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp); 3540 3541 const query = gql` 3542 { 3543 node { 3544 id 3545 name 3546 } 3547 } 3548 `; 3549 3550 const mutation = gql` 3551 mutation { 3552 node { 3553 id 3554 name 3555 optimistic 3556 } 3557 } 3558 `; 3559 3560 const forward = (ops$: Source<Operation>): Source<OperationResult> => 3561 share( 3562 merge([ 3563 pipe( 3564 ops$, 3565 filter(() => false) 3566 ) as any, 3567 res$, 3568 ]) 3569 ); 3570 3571 const optimistic = { 3572 node: () => ({ 3573 __typename: 'Node', 3574 id: 'node', 3575 name: 'optimistic', 3576 }), 3577 }; 3578 3579 pipe( 3580 cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), 3581 tap(result => { 3582 if (result.operation.kind === 'query') { 3583 data = result.data; 3584 } 3585 }), 3586 publish 3587 ); 3588 3589 const queryOp = client.createRequestOperation('query', { 3590 key: 1, 3591 query, 3592 variables: undefined, 3593 }); 3594 const mutationOp = client.createRequestOperation('mutation', { 3595 key: 2, 3596 query: mutation, 3597 variables: undefined, 3598 }); 3599 3600 expect(data).toBe(undefined); 3601 3602 nextOp(queryOp); 3603 nextOp(mutationOp); 3604 3605 nextRes({ 3606 ...queryResponse, 3607 operation: queryOp, 3608 data: { 3609 __typename: 'Query', 3610 node: { 3611 __typename: 'Node', 3612 id: 'node', 3613 name: 'query a', 3614 }, 3615 }, 3616 }); 3617 3618 expect(data).toHaveProperty('node.name', 'optimistic'); 3619 3620 nextRes({ 3621 ...queryResponse, 3622 operation: mutationOp, 3623 data: { 3624 __typename: 'Query', 3625 node: { 3626 __typename: 'Node', 3627 id: 'node', 3628 name: 'mutation', 3629 }, 3630 }, 3631 }); 3632 3633 expect(data).toHaveProperty('node.name', 'mutation'); 3634 }); 3635 3636 it('allows subscription results to be commutative when necessary', () => { 3637 let data: any; 3638 const client = createClient({ 3639 url: 'http://0.0.0.0', 3640 exchanges: [], 3641 }); 3642 const { source: ops$, next: nextOp } = makeSubject<Operation>(); 3643 const { source: res$, next: nextRes } = makeSubject<OperationResult>(); 3644 3645 vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp); 3646 3647 const query = gql` 3648 { 3649 node { 3650 id 3651 name 3652 } 3653 } 3654 `; 3655 3656 const subscription = gql` 3657 subscription { 3658 node { 3659 id 3660 name 3661 } 3662 } 3663 `; 3664 3665 const forward = (ops$: Source<Operation>): Source<OperationResult> => 3666 share( 3667 merge([ 3668 pipe( 3669 ops$, 3670 filter(() => false) 3671 ) as any, 3672 res$, 3673 ]) 3674 ); 3675 3676 pipe( 3677 cacheExchange()({ forward, client, dispatchDebug })(ops$), 3678 tap(result => { 3679 if (result.operation.kind === 'query') { 3680 data = result.data; 3681 } 3682 }), 3683 publish 3684 ); 3685 3686 const queryOpA = client.createRequestOperation('query', { 3687 key: 1, 3688 query, 3689 variables: undefined, 3690 }); 3691 3692 const subscriptionOp = client.createRequestOperation('subscription', { 3693 key: 3, 3694 query: subscription, 3695 variables: undefined, 3696 }); 3697 3698 nextOp(queryOpA); 3699 // Force commutative layers to be created: 3700 nextOp( 3701 client.createRequestOperation('query', { 3702 key: 2, 3703 query, 3704 variables: undefined, 3705 }) 3706 ); 3707 3708 nextOp(subscriptionOp); 3709 3710 nextRes({ 3711 ...queryResponse, 3712 operation: queryOpA, 3713 data: { 3714 __typename: 'Query', 3715 node: { 3716 __typename: 'Node', 3717 id: 'node', 3718 name: 'query a', 3719 }, 3720 }, 3721 }); 3722 3723 nextRes({ 3724 ...queryResponse, 3725 operation: subscriptionOp, 3726 data: { 3727 node: { 3728 __typename: 'Node', 3729 id: 'node', 3730 name: 'subscription', 3731 }, 3732 }, 3733 }); 3734 3735 expect(data).toHaveProperty('node.name', 'subscription'); 3736 }); 3737 3738 it('allows subscription results to be commutative above mutations', () => { 3739 let data: any; 3740 const client = createClient({ 3741 url: 'http://0.0.0.0', 3742 exchanges: [], 3743 }); 3744 const { source: ops$, next: nextOp } = makeSubject<Operation>(); 3745 const { source: res$, next: nextRes } = makeSubject<OperationResult>(); 3746 3747 vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp); 3748 3749 const query = gql` 3750 { 3751 node { 3752 id 3753 name 3754 } 3755 } 3756 `; 3757 3758 const subscription = gql` 3759 subscription { 3760 node { 3761 id 3762 name 3763 } 3764 } 3765 `; 3766 3767 const mutation = gql` 3768 mutation { 3769 node { 3770 id 3771 name 3772 } 3773 } 3774 `; 3775 3776 const forward = (ops$: Source<Operation>): Source<OperationResult> => 3777 share( 3778 merge([ 3779 pipe( 3780 ops$, 3781 filter(() => false) 3782 ) as any, 3783 res$, 3784 ]) 3785 ); 3786 3787 pipe( 3788 cacheExchange()({ forward, client, dispatchDebug })(ops$), 3789 tap(result => { 3790 if (result.operation.kind === 'query') { 3791 data = result.data; 3792 } 3793 }), 3794 publish 3795 ); 3796 3797 const queryOpA = client.createRequestOperation('query', { 3798 key: 1, 3799 query, 3800 variables: undefined, 3801 }); 3802 3803 const subscriptionOp = client.createRequestOperation('subscription', { 3804 key: 2, 3805 query: subscription, 3806 variables: undefined, 3807 }); 3808 3809 const mutationOp = client.createRequestOperation('mutation', { 3810 key: 3, 3811 query: mutation, 3812 variables: undefined, 3813 }); 3814 3815 nextOp(queryOpA); 3816 // Force commutative layers to be created: 3817 nextOp( 3818 client.createRequestOperation('query', { 3819 key: 2, 3820 query, 3821 variables: undefined, 3822 }) 3823 ); 3824 3825 nextOp(subscriptionOp); 3826 3827 nextRes({ 3828 ...queryResponse, 3829 operation: queryOpA, 3830 data: { 3831 __typename: 'Query', 3832 node: { 3833 __typename: 'Node', 3834 id: 'node', 3835 name: 'query a', 3836 }, 3837 }, 3838 }); 3839 3840 nextOp(mutationOp); 3841 3842 nextRes({ 3843 ...queryResponse, 3844 operation: mutationOp, 3845 data: { 3846 node: { 3847 __typename: 'Node', 3848 id: 'node', 3849 name: 'mutation', 3850 }, 3851 }, 3852 }); 3853 3854 nextRes({ 3855 ...queryResponse, 3856 operation: subscriptionOp, 3857 data: { 3858 node: { 3859 __typename: 'Node', 3860 id: 'node', 3861 name: 'subscription a', 3862 }, 3863 }, 3864 }); 3865 3866 nextRes({ 3867 ...queryResponse, 3868 operation: subscriptionOp, 3869 data: { 3870 node: { 3871 __typename: 'Node', 3872 id: 'node', 3873 name: 'subscription b', 3874 }, 3875 }, 3876 }); 3877 3878 expect(data).toHaveProperty('node.name', 'subscription b'); 3879 }); 3880 3881 it('applies deferred results to previous layers', () => { 3882 let normalData: OperationResult | undefined; 3883 let deferredData: OperationResult | undefined; 3884 let combinedData: OperationResult | undefined; 3885 3886 const client = createClient({ 3887 url: 'http://0.0.0.0', 3888 exchanges: [], 3889 }); 3890 const { source: ops$, next: nextOp } = makeSubject<Operation>(); 3891 const { source: res$, next: nextRes } = makeSubject<OperationResult>(); 3892 client.reexecuteOperation = nextOp; 3893 3894 const normalQuery = gql` 3895 { 3896 node { 3897 id 3898 name 3899 } 3900 } 3901 `; 3902 3903 const deferredQuery = gql` 3904 { 3905 ... @defer { 3906 deferred { 3907 id 3908 name 3909 } 3910 } 3911 } 3912 `; 3913 3914 const combinedQuery = gql` 3915 { 3916 node { 3917 id 3918 name 3919 } 3920 ... @defer { 3921 deferred { 3922 id 3923 name 3924 } 3925 } 3926 } 3927 `; 3928 3929 const forward = (operations$: Source<Operation>): Source<OperationResult> => 3930 share( 3931 merge([ 3932 pipe( 3933 operations$, 3934 filter(() => false) 3935 ) as any, 3936 res$, 3937 ]) 3938 ); 3939 3940 pipe( 3941 cacheExchange()({ forward, client, dispatchDebug })(ops$), 3942 tap(result => { 3943 if (result.operation.kind === 'query') { 3944 if (result.operation.key === 1) { 3945 deferredData = result; 3946 } else if (result.operation.key === 42) { 3947 combinedData = result; 3948 } else { 3949 normalData = result; 3950 } 3951 } 3952 }), 3953 publish 3954 ); 3955 3956 const combinedOp = client.createRequestOperation('query', { 3957 key: 42, 3958 query: combinedQuery, 3959 variables: undefined, 3960 }); 3961 const deferredOp = client.createRequestOperation('query', { 3962 key: 1, 3963 query: deferredQuery, 3964 variables: undefined, 3965 }); 3966 const normalOp = client.createRequestOperation('query', { 3967 key: 2, 3968 query: normalQuery, 3969 variables: undefined, 3970 }); 3971 3972 nextOp(combinedOp); 3973 nextOp(deferredOp); 3974 nextOp(normalOp); 3975 3976 nextRes({ 3977 ...queryResponse, 3978 operation: deferredOp, 3979 data: { 3980 __typename: 'Query', 3981 }, 3982 hasNext: true, 3983 }); 3984 3985 expect(deferredData).not.toHaveProperty('deferred'); 3986 3987 nextRes({ 3988 ...queryResponse, 3989 operation: normalOp, 3990 data: { 3991 __typename: 'Query', 3992 node: { 3993 __typename: 'Node', 3994 id: 2, 3995 name: 'normal', 3996 }, 3997 }, 3998 }); 3999 4000 expect(normalData).toHaveProperty('data.node.id', 2); 4001 expect(combinedData).not.toHaveProperty('data.deferred'); 4002 expect(combinedData).toHaveProperty('data.node.id', 2); 4003 4004 nextRes({ 4005 ...queryResponse, 4006 operation: deferredOp, 4007 data: { 4008 __typename: 'Query', 4009 deferred: { 4010 __typename: 'Node', 4011 id: 1, 4012 name: 'deferred', 4013 }, 4014 }, 4015 hasNext: true, 4016 }); 4017 4018 expect(deferredData).toHaveProperty('hasNext', true); 4019 expect(deferredData).toHaveProperty('data.deferred.id', 1); 4020 4021 expect(combinedData).toHaveProperty('hasNext', false); 4022 expect(combinedData).toHaveProperty('data.deferred.id', 1); 4023 expect(combinedData).toHaveProperty('data.node.id', 2); 4024 }); 4025 4026 it('applies deferred logic only to deferred operations', () => { 4027 let failingData: OperationResult | undefined; 4028 4029 const client = createClient({ 4030 url: 'http://0.0.0.0', 4031 exchanges: [], 4032 }); 4033 4034 const { source: ops$, next: nextOp } = makeSubject<Operation>(); 4035 const { source: res$ } = makeSubject<OperationResult>(); 4036 4037 const deferredQuery = gql` 4038 { 4039 ... @defer { 4040 deferred { 4041 id 4042 name 4043 } 4044 } 4045 } 4046 `; 4047 4048 const failingQuery = gql` 4049 { 4050 deferred { 4051 id 4052 name 4053 } 4054 } 4055 `; 4056 4057 const forward = (ops$: Source<Operation>): Source<OperationResult> => 4058 share( 4059 merge([ 4060 pipe( 4061 ops$, 4062 filter(() => false) 4063 ) as any, 4064 res$, 4065 ]) 4066 ); 4067 4068 pipe( 4069 cacheExchange()({ forward, client, dispatchDebug })(ops$), 4070 tap(result => { 4071 if (result.operation.kind === 'query') { 4072 if (result.operation.key === 1) { 4073 failingData = result; 4074 } 4075 } 4076 }), 4077 publish 4078 ); 4079 4080 const failingOp = client.createRequestOperation('query', { 4081 key: 1, 4082 query: failingQuery, 4083 variables: undefined, 4084 }); 4085 const deferredOp = client.createRequestOperation('query', { 4086 key: 2, 4087 query: deferredQuery, 4088 variables: undefined, 4089 }); 4090 4091 nextOp(deferredOp); 4092 nextOp(failingOp); 4093 4094 expect(failingData).not.toMatchObject({ hasNext: true }); 4095 }); 4096}); 4097 4098describe('abstract types', () => { 4099 it('works with two responses giving different concrete types for a union', () => { 4100 const query = gql` 4101 query ($id: ID!) { 4102 field(id: $id) { 4103 id 4104 union { 4105 ... on Type1 { 4106 id 4107 name 4108 __typename 4109 } 4110 ... on Type2 { 4111 id 4112 title 4113 __typename 4114 } 4115 } 4116 __typename 4117 } 4118 } 4119 `; 4120 const client = createClient({ 4121 url: 'http://0.0.0.0', 4122 exchanges: [], 4123 }); 4124 const { source: ops$, next } = makeSubject<Operation>(); 4125 const operation1 = client.createRequestOperation('query', { 4126 key: 1, 4127 query, 4128 variables: { id: '1' }, 4129 }); 4130 const operation2 = client.createRequestOperation('query', { 4131 key: 2, 4132 query, 4133 variables: { id: '2' }, 4134 }); 4135 const queryResult1: OperationResult = { 4136 ...queryResponse, 4137 operation: operation1, 4138 data: { 4139 __typename: 'Query', 4140 field: { 4141 id: '1', 4142 __typename: 'Todo', 4143 union: { 4144 id: '1', 4145 name: 'test', 4146 __typename: 'Type1', 4147 }, 4148 }, 4149 }, 4150 }; 4151 4152 const queryResult2: OperationResult = { 4153 ...queryResponse, 4154 operation: operation2, 4155 data: { 4156 __typename: 'Query', 4157 field: { 4158 id: '2', 4159 __typename: 'Todo', 4160 union: { 4161 id: '2', 4162 title: 'test', 4163 __typename: 'Type2', 4164 }, 4165 }, 4166 }, 4167 }; 4168 4169 vi.spyOn(client, 'reexecuteOperation').mockImplementation(next); 4170 const response = vi.fn((forwardOp: Operation): OperationResult => { 4171 if (forwardOp.key === 1) return queryResult1; 4172 if (forwardOp.key === 2) return queryResult2; 4173 return undefined as any; 4174 }); 4175 4176 const result = vi.fn(); 4177 const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 4178 4179 pipe( 4180 cacheExchange({})({ forward, client, dispatchDebug })(ops$), 4181 tap(result), 4182 publish 4183 ); 4184 4185 next(operation1); 4186 expect(response).toHaveBeenCalledTimes(1); 4187 expect(result).toHaveBeenCalledTimes(1); 4188 expect(result.mock.calls[0][0].data).toEqual({ 4189 field: { 4190 __typename: 'Todo', 4191 id: '1', 4192 union: { 4193 __typename: 'Type1', 4194 id: '1', 4195 name: 'test', 4196 }, 4197 }, 4198 }); 4199 4200 next(operation2); 4201 expect(response).toHaveBeenCalledTimes(2); 4202 expect(result).toHaveBeenCalledTimes(2); 4203 expect(result.mock.calls[1][0].data).toEqual({ 4204 field: { 4205 __typename: 'Todo', 4206 id: '2', 4207 union: { 4208 __typename: 'Type2', 4209 id: '2', 4210 title: 'test', 4211 }, 4212 }, 4213 }); 4214 }); 4215});