Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 18 kB view raw
1import { 2 buildSchema, 3 print, 4 introspectionFromSchema, 5 visit, 6 DocumentNode, 7 ASTKindToNode, 8 Kind, 9} from 'graphql'; 10import { vi, expect, it, describe } from 'vitest'; 11 12import { fromValue, pipe, fromArray, toArray } from 'wonka'; 13import { 14 gql, 15 Client, 16 Operation, 17 OperationContext, 18 makeOperation, 19} from '@urql/core'; 20 21import { populateExchange } from './populateExchange'; 22 23const schemaDef = ` 24 interface Node { 25 id: ID! 26 } 27 28 type User implements Node { 29 id: ID! 30 name: String! 31 age: Int! 32 todos: [Todo] 33 } 34 35 type Todo implements Node { 36 id: ID! 37 text: String! 38 createdAt(timezone: String): String! 39 creator: User! 40 } 41 42 union UnionType = User | Todo 43 44 interface Product { 45 id: ID! 46 name: String! 47 price: Int! 48 } 49 50 interface Store { 51 id: ID! 52 name: String! 53 } 54 55 type PhysicalStore implements Store { 56 id: ID! 57 name: String! 58 address: String 59 } 60 61 type OnlineStore implements Store { 62 id: ID! 63 name: String! 64 website: String 65 } 66 67 type SimpleProduct implements Product { 68 id: ID! 69 name: String! 70 price: Int! 71 store: PhysicalStore 72 } 73 74 type ComplexProduct implements Product { 75 id: ID! 76 name: String! 77 price: Int! 78 tax: Int! 79 store: OnlineStore 80 } 81 82 type Company { 83 id: String 84 employees: [User] 85 } 86 87 type Query { 88 todos: [Todo!] 89 users: [User!]! 90 products: [Product]! 91 company: Company 92 } 93 94 type Mutation { 95 addTodo: [Todo] 96 removeTodo: [Node] 97 updateTodo: [UnionType] 98 addProduct: Product 99 removeCompany: Company 100 } 101`; 102 103const context = {} as OperationContext; 104 105const getNodesByType = <T extends keyof ASTKindToNode, N = ASTKindToNode[T]>( 106 query: DocumentNode, 107 type: T 108) => { 109 let result: N[] = []; 110 111 visit(query, { 112 [type]: n => { 113 result = [...result, n]; 114 }, 115 }); 116 return result; 117}; 118 119const schema = introspectionFromSchema(buildSchema(schemaDef)); 120 121const exchangeArgs = { 122 forward: a => a as any, 123 client: {} as Client, 124 dispatchDebug: vi.fn(), 125}; 126 127describe('on mutation', () => { 128 const operation = makeOperation( 129 'mutation', 130 { 131 key: 1234, 132 variables: undefined, 133 query: gql` 134 mutation MyMutation { 135 addTodo @populate 136 } 137 `, 138 }, 139 context 140 ); 141 142 describe('mutation query', () => { 143 it('matches snapshot', async () => { 144 const response = pipe<Operation, any, Operation[]>( 145 fromValue(operation), 146 populateExchange({ schema })(exchangeArgs), 147 toArray 148 ); 149 expect(print(response[0].query)).toMatchInlineSnapshot(` 150 "mutation MyMutation { 151 addTodo { 152 __typename 153 } 154 }" 155 `); 156 }); 157 }); 158}); 159 160describe('on query -> mutation', () => { 161 const queryOp = makeOperation( 162 'query', 163 { 164 key: 1234, 165 variables: undefined, 166 query: gql` 167 query { 168 todos { 169 id 170 text 171 creator { 172 id 173 name 174 } 175 } 176 users { 177 todos { 178 text 179 } 180 } 181 } 182 `, 183 }, 184 context 185 ); 186 187 const mutationOp = makeOperation( 188 'mutation', 189 { 190 key: 5678, 191 variables: undefined, 192 query: gql` 193 mutation MyMutation { 194 addTodo @populate 195 } 196 `, 197 }, 198 context 199 ); 200 201 describe('mutation query', () => { 202 it('matches snapshot', async () => { 203 const response = pipe<Operation, any, Operation[]>( 204 fromArray([queryOp, mutationOp]), 205 populateExchange({ schema })(exchangeArgs), 206 toArray 207 ); 208 209 expect(print(response[1].query)).toMatchInlineSnapshot(` 210 "mutation MyMutation { 211 addTodo { 212 __typename 213 id 214 text 215 creator { 216 __typename 217 id 218 name 219 } 220 } 221 }" 222 `); 223 }); 224 }); 225}); 226 227describe('on query -> mutation', () => { 228 const queryOp = makeOperation( 229 'query', 230 { 231 key: 1234, 232 variables: undefined, 233 query: gql` 234 query { 235 todos { 236 id 237 text 238 createdAt(timezone: "GMT+1") 239 } 240 } 241 `, 242 }, 243 context 244 ); 245 246 const mutationOp = makeOperation( 247 'mutation', 248 { 249 key: 5678, 250 variables: undefined, 251 query: gql` 252 mutation MyMutation { 253 addTodo @populate 254 } 255 `, 256 }, 257 context 258 ); 259 260 describe('mutation query', () => { 261 it('matches snapshot', async () => { 262 const response = pipe<Operation, any, Operation[]>( 263 fromArray([queryOp, mutationOp]), 264 populateExchange({ schema })(exchangeArgs), 265 toArray 266 ); 267 268 expect(print(response[1].query)).toMatchInlineSnapshot(` 269 "mutation MyMutation { 270 addTodo { 271 __typename 272 id 273 text 274 createdAt(timezone: "GMT+1") 275 } 276 }" 277 `); 278 }); 279 }); 280}); 281 282describe('on (query w/ fragment) -> mutation', () => { 283 const queryOp = makeOperation( 284 'query', 285 { 286 key: 1234, 287 variables: undefined, 288 query: gql` 289 query { 290 todos { 291 ...TodoFragment 292 creator { 293 ...CreatorFragment 294 } 295 } 296 } 297 298 fragment TodoFragment on Todo { 299 id 300 text 301 } 302 303 fragment CreatorFragment on User { 304 id 305 name 306 } 307 `, 308 }, 309 context 310 ); 311 312 const mutationOp = makeOperation( 313 'mutation', 314 { 315 key: 5678, 316 variables: undefined, 317 query: gql` 318 mutation MyMutation { 319 addTodo @populate { 320 ...TodoFragment 321 } 322 } 323 324 fragment TodoFragment on Todo { 325 id 326 text 327 } 328 `, 329 }, 330 context 331 ); 332 333 describe('mutation query', () => { 334 it('matches snapshot', async () => { 335 const response = pipe<Operation, any, Operation[]>( 336 fromArray([queryOp, mutationOp]), 337 populateExchange({ schema })(exchangeArgs), 338 toArray 339 ); 340 341 expect(print(response[1].query)).toMatchInlineSnapshot(` 342 "mutation MyMutation { 343 addTodo { 344 ...TodoFragment 345 __typename 346 id 347 text 348 creator { 349 __typename 350 id 351 name 352 } 353 } 354 } 355 356 fragment TodoFragment on Todo { 357 id 358 text 359 }" 360 `); 361 }); 362 }); 363}); 364 365describe('on (query w/ unused fragment) -> mutation', () => { 366 const queryOp = makeOperation( 367 'query', 368 { 369 key: 1234, 370 variables: undefined, 371 query: gql` 372 query { 373 todos { 374 id 375 text 376 } 377 users { 378 ...UserFragment 379 } 380 } 381 382 fragment UserFragment on User { 383 id 384 name 385 } 386 `, 387 }, 388 context 389 ); 390 391 const mutationOp = makeOperation( 392 'mutation', 393 { 394 key: 5678, 395 variables: undefined, 396 query: gql` 397 mutation MyMutation { 398 addTodo @populate 399 } 400 `, 401 }, 402 context 403 ); 404 405 describe('mutation query', () => { 406 it('matches snapshot', async () => { 407 const response = pipe<Operation, any, Operation[]>( 408 fromArray([queryOp, mutationOp]), 409 populateExchange({ schema })(exchangeArgs), 410 toArray 411 ); 412 413 expect(print(response[1].query)).toMatchInlineSnapshot(` 414 "mutation MyMutation { 415 addTodo { 416 __typename 417 id 418 text 419 } 420 }" 421 `); 422 }); 423 424 it('excludes user fragment', () => { 425 const response = pipe<Operation, any, Operation[]>( 426 fromArray([queryOp, mutationOp]), 427 populateExchange({ schema })(exchangeArgs), 428 toArray 429 ); 430 431 const fragments = getNodesByType( 432 response[1].query, 433 Kind.FRAGMENT_DEFINITION 434 ); 435 expect( 436 fragments.filter(f => 'name' in f && f.name.value === 'UserFragment') 437 ).toHaveLength(0); 438 }); 439 }); 440}); 441 442describe('on query -> (mutation w/ interface return type)', () => { 443 const queryOp = makeOperation( 444 'query', 445 { 446 key: 1234, 447 variables: undefined, 448 query: gql` 449 query { 450 todos { 451 id 452 name 453 } 454 users { 455 id 456 text 457 } 458 } 459 `, 460 }, 461 context 462 ); 463 464 const mutationOp = makeOperation( 465 'mutation', 466 { 467 key: 5678, 468 variables: undefined, 469 query: gql` 470 mutation MyMutation { 471 removeTodo @populate 472 } 473 `, 474 }, 475 context 476 ); 477 478 describe('mutation query', () => { 479 it('matches snapshot', async () => { 480 const response = pipe<Operation, any, Operation[]>( 481 fromArray([queryOp, mutationOp]), 482 populateExchange({ schema })(exchangeArgs), 483 toArray 484 ); 485 486 expect(print(response[1].query)).toMatchInlineSnapshot(` 487 "mutation MyMutation { 488 removeTodo { 489 ... on User { 490 __typename 491 id 492 } 493 ... on Todo { 494 __typename 495 id 496 } 497 } 498 }" 499 `); 500 }); 501 }); 502}); 503 504describe('on query -> (mutation w/ union return type)', () => { 505 const queryOp = makeOperation( 506 'query', 507 { 508 key: 1234, 509 variables: undefined, 510 query: gql` 511 query { 512 todos { 513 id 514 text 515 } 516 users { 517 id 518 name 519 } 520 } 521 `, 522 }, 523 context 524 ); 525 526 const mutationOp = makeOperation( 527 'mutation', 528 { 529 key: 5678, 530 variables: undefined, 531 query: gql` 532 mutation MyMutation { 533 updateTodo @populate 534 } 535 `, 536 }, 537 context 538 ); 539 540 describe('mutation query', () => { 541 it('matches snapshot', async () => { 542 const response = pipe<Operation, any, Operation[]>( 543 fromArray([queryOp, mutationOp]), 544 populateExchange({ schema })(exchangeArgs), 545 toArray 546 ); 547 548 expect(print(response[1].query)).toMatchInlineSnapshot(` 549 "mutation MyMutation { 550 updateTodo { 551 ... on User { 552 __typename 553 id 554 name 555 } 556 ... on Todo { 557 __typename 558 id 559 text 560 } 561 } 562 }" 563 `); 564 }); 565 }); 566}); 567 568// TODO: figure out how to behave with teardown, just removing and 569// not requesting fields feels kinda incorrect as we would start having 570// stale cache values here 571describe.skip('on query -> teardown -> mutation', () => { 572 const queryOp = makeOperation( 573 'query', 574 { 575 key: 1234, 576 variables: undefined, 577 query: gql` 578 query { 579 todos { 580 id 581 text 582 } 583 } 584 `, 585 }, 586 context 587 ); 588 589 const teardownOp = makeOperation('teardown', queryOp, context); 590 591 const mutationOp = makeOperation( 592 'mutation', 593 { 594 key: 5678, 595 variables: undefined, 596 query: gql` 597 mutation MyMutation { 598 addTodo @populate 599 } 600 `, 601 }, 602 context 603 ); 604 605 describe('mutation query', () => { 606 it('matches snapshot', async () => { 607 const response = pipe<Operation, any, Operation[]>( 608 fromArray([queryOp, teardownOp, mutationOp]), 609 populateExchange({ schema })(exchangeArgs), 610 toArray 611 ); 612 613 expect(print(response[2].query)).toMatchInlineSnapshot(` 614 "mutation MyMutation { 615 addTodo { 616 __typename 617 } 618 }" 619 `); 620 }); 621 622 it('only requests __typename', () => { 623 const response = pipe<Operation, any, Operation[]>( 624 fromArray([queryOp, teardownOp, mutationOp]), 625 populateExchange({ schema })(exchangeArgs), 626 toArray 627 ); 628 getNodesByType(response[2].query, Kind.FIELD).forEach(field => { 629 expect((field as any).name.value).toMatch(/addTodo|__typename/); 630 }); 631 }); 632 }); 633}); 634 635describe('interface returned in mutation', () => { 636 const queryOp = makeOperation( 637 'query', 638 { 639 key: 1234, 640 variables: undefined, 641 query: gql` 642 query { 643 products { 644 id 645 text 646 price 647 tax 648 } 649 } 650 `, 651 }, 652 context 653 ); 654 655 const mutationOp = makeOperation( 656 'mutation', 657 { 658 key: 5678, 659 variables: undefined, 660 query: gql` 661 mutation MyMutation { 662 addProduct @populate 663 } 664 `, 665 }, 666 context 667 ); 668 669 it('should correctly make the inline-fragments', () => { 670 const response = pipe<Operation, any, Operation[]>( 671 fromArray([queryOp, mutationOp]), 672 populateExchange({ schema })(exchangeArgs), 673 toArray 674 ); 675 676 expect(print(response[1].query)).toMatchInlineSnapshot(` 677 "mutation MyMutation { 678 addProduct { 679 ... on SimpleProduct { 680 __typename 681 id 682 price 683 } 684 ... on ComplexProduct { 685 __typename 686 id 687 price 688 tax 689 } 690 } 691 }" 692 `); 693 }); 694}); 695 696describe('nested interfaces', () => { 697 const queryOp = makeOperation( 698 'query', 699 { 700 key: 1234, 701 variables: undefined, 702 query: gql` 703 query { 704 products { 705 id 706 text 707 price 708 tax 709 store { 710 id 711 name 712 address 713 website 714 } 715 } 716 } 717 `, 718 }, 719 context 720 ); 721 722 const mutationOp = makeOperation( 723 'mutation', 724 { 725 key: 5678, 726 variables: undefined, 727 query: gql` 728 mutation MyMutation { 729 addProduct @populate 730 } 731 `, 732 }, 733 context 734 ); 735 736 it('should correctly make the inline-fragments', () => { 737 const response = pipe<Operation, any, Operation[]>( 738 fromArray([queryOp, mutationOp]), 739 populateExchange({ schema })(exchangeArgs), 740 toArray 741 ); 742 743 expect(print(response[1].query)).toMatchInlineSnapshot(` 744 "mutation MyMutation { 745 addProduct { 746 ... on SimpleProduct { 747 __typename 748 id 749 price 750 store { 751 __typename 752 id 753 name 754 address 755 } 756 } 757 ... on ComplexProduct { 758 __typename 759 id 760 price 761 tax 762 store { 763 __typename 764 id 765 name 766 website 767 } 768 } 769 } 770 }" 771 `); 772 }); 773}); 774 775describe('nested fragment', () => { 776 const fragment = gql` 777 fragment TodoFragment on Todo { 778 id 779 author { 780 id 781 } 782 } 783 `; 784 785 const queryOp = makeOperation( 786 'query', 787 { 788 key: 1234, 789 variables: undefined, 790 query: gql` 791 query { 792 todos { 793 ...TodoFragment 794 } 795 } 796 ${fragment} 797 `, 798 }, 799 context 800 ); 801 802 const mutationOp = makeOperation( 803 'mutation', 804 { 805 key: 5678, 806 variables: undefined, 807 query: gql` 808 mutation MyMutation { 809 updateTodo @populate 810 } 811 `, 812 }, 813 context 814 ); 815 816 it('should work with nested fragments', () => { 817 const response = pipe<Operation, any, Operation[]>( 818 fromArray([queryOp, mutationOp]), 819 populateExchange({ schema })(exchangeArgs), 820 toArray 821 ); 822 823 expect(print(response[1].query)).toMatchInlineSnapshot(` 824 "mutation MyMutation { 825 updateTodo { 826 ... on Todo { 827 __typename 828 id 829 } 830 } 831 }" 832 `); 833 }); 834}); 835 836describe('respects max-depth', () => { 837 const queryOp = makeOperation( 838 'query', 839 { 840 key: 1234, 841 variables: undefined, 842 query: gql` 843 query { 844 company { 845 id 846 employees { 847 id 848 todos { 849 id 850 } 851 } 852 } 853 } 854 `, 855 }, 856 context 857 ); 858 859 const mutationOp = makeOperation( 860 'mutation', 861 { 862 key: 5678, 863 variables: undefined, 864 query: gql` 865 mutation MyMutation { 866 removeCompany @populate 867 } 868 `, 869 }, 870 context 871 ); 872 873 describe('mutation query', () => { 874 it('matches snapshot', async () => { 875 const response = pipe<Operation, any, Operation[]>( 876 fromArray([queryOp, mutationOp]), 877 populateExchange({ schema, options: { maxDepth: 1 } })(exchangeArgs), 878 toArray 879 ); 880 881 expect(print(response[1].query)).toMatchInlineSnapshot(` 882 "mutation MyMutation { 883 removeCompany { 884 __typename 885 id 886 employees { 887 __typename 888 id 889 } 890 } 891 }" 892 `); 893 }); 894 895 it('respects skip syntax', async () => { 896 const response = pipe<Operation, any, Operation[]>( 897 fromArray([queryOp, mutationOp]), 898 populateExchange({ 899 schema, 900 options: { maxDepth: 1, skipType: /User/ }, 901 })(exchangeArgs), 902 toArray 903 ); 904 905 expect(print(response[1].query)).toMatchInlineSnapshot(` 906 "mutation MyMutation { 907 removeCompany { 908 __typename 909 id 910 employees { 911 __typename 912 id 913 todos { 914 __typename 915 id 916 } 917 } 918 } 919 }" 920 `); 921 }); 922 }); 923});