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 { gql } from '@urql/core'; 2import { it, expect, afterEach } from 'vitest'; 3import { __initAnd_query as query } from '../operations/query'; 4import { 5 __initAnd_write as write, 6 __initAnd_writeOptimistic as writeOptimistic, 7} from '../operations/write'; 8import * as InMemoryData from '../store/data'; 9import { Store } from '../store/store'; 10import { Data } from '../types'; 11 12const Todos = gql` 13 query { 14 __typename 15 todos { 16 __typename 17 id 18 complete 19 text 20 } 21 } 22`; 23 24const TodoFragment = gql` 25 fragment _ on Todo { 26 __typename 27 id 28 text 29 complete 30 } 31`; 32 33const Todo = gql` 34 query ($id: ID!) { 35 __typename 36 todo(id: $id) { 37 id 38 text 39 complete 40 } 41 } 42`; 43 44const ToggleTodo = gql` 45 mutation ($id: ID!) { 46 __typename 47 toggleTodo(id: $id) { 48 __typename 49 id 50 text 51 complete 52 } 53 } 54`; 55 56const NestedClearNameTodo = gql` 57 mutation ($id: ID!) { 58 __typename 59 clearName(id: $id) { 60 __typename 61 todo { 62 __typename 63 id 64 text 65 complete 66 } 67 } 68 } 69`; 70 71afterEach(() => { 72 expect(console.warn).not.toHaveBeenCalled(); 73}); 74 75it('passes the "getting-started" example', () => { 76 const store = new Store(); 77 const todosData = { 78 __typename: 'Query', 79 todos: [ 80 { id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo' }, 81 { id: '1', text: 'Pick up the kids', complete: true, __typename: 'Todo' }, 82 { id: '2', text: 'Install urql', complete: false, __typename: 'Todo' }, 83 ], 84 }; 85 86 const writeRes = write(store, { query: Todos }, todosData); 87 88 expect(writeRes.dependencies).toEqual( 89 new Set(['Query.todos', 'Todo:0', 'Todo:1', 'Todo:2']) 90 ); 91 92 let queryRes = query(store, { query: Todos }); 93 94 expect(queryRes.data).toEqual(todosData); 95 expect(queryRes.dependencies).toEqual(writeRes.dependencies); 96 expect(queryRes.partial).toBe(false); 97 98 const mutatedTodo = { 99 ...todosData.todos[2], 100 complete: true, 101 }; 102 103 const mutationRes = write( 104 store, 105 { query: ToggleTodo, variables: { id: '2' } }, 106 { 107 __typename: 'Mutation', 108 toggleTodo: mutatedTodo, 109 } 110 ); 111 112 expect(mutationRes.dependencies).toEqual(new Set(['Todo:2'])); 113 114 queryRes = query(store, { query: Todos }); 115 116 expect(queryRes.partial).toBe(false); 117 expect(queryRes.data).toEqual({ 118 ...todosData, 119 todos: [...todosData.todos.slice(0, 2), mutatedTodo], 120 }); 121 122 const newMutatedTodo = { 123 ...mutatedTodo, 124 text: '', 125 }; 126 127 const newMutationRes = write( 128 store, 129 { query: NestedClearNameTodo, variables: { id: '2' } }, 130 { 131 __typename: 'Mutation', 132 clearName: { 133 __typename: 'ClearName', 134 todo: newMutatedTodo, 135 }, 136 } 137 ); 138 139 expect(newMutationRes.dependencies).toEqual(new Set(['Todo:2'])); 140 141 queryRes = query(store, { query: Todos }); 142 143 expect(queryRes.partial).toBe(false); 144 expect(queryRes.data).toEqual({ 145 ...todosData, 146 todos: [...todosData.todos.slice(0, 2), newMutatedTodo], 147 }); 148}); 149 150it('resolves missing, nullable arguments on fields', () => { 151 const store = new Store(); 152 153 const GetWithVariables = gql` 154 query { 155 __typename 156 todo(first: null) { 157 __typename 158 id 159 } 160 } 161 `; 162 163 const GetWithoutVariables = gql` 164 query { 165 __typename 166 todo { 167 __typename 168 id 169 } 170 } 171 `; 172 173 const dataToWrite = { 174 __typename: 'Query', 175 todo: { 176 __typename: 'Todo', 177 id: '123', 178 }, 179 }; 180 181 write(store, { query: GetWithVariables }, dataToWrite); 182 const { data } = query(store, { query: GetWithoutVariables }); 183 expect(data).toEqual(dataToWrite); 184}); 185 186it('should link entities', () => { 187 const store = new Store({ 188 resolvers: { 189 Query: { 190 todo: (_parent, args) => { 191 return { __typename: 'Todo', ...args }; 192 }, 193 }, 194 }, 195 }); 196 197 const todosData = { 198 __typename: 'Query', 199 todos: [ 200 { id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo' }, 201 { id: '1', text: 'Pick up the kids', complete: true, __typename: 'Todo' }, 202 { id: '2', text: 'Install urql', complete: false, __typename: 'Todo' }, 203 ], 204 }; 205 206 write(store, { query: Todos }, todosData); 207 const res = query(store, { query: Todo, variables: { id: '0' } }); 208 expect(res.data).toEqual({ 209 __typename: 'Query', 210 todo: { 211 id: '0', 212 text: 'Go to the shops', 213 complete: false, 214 }, 215 }); 216}); 217 218it('should not link entities when writing', () => { 219 const store = new Store({ 220 resolvers: { 221 Todo: { 222 text: () => '[redacted]', 223 }, 224 }, 225 }); 226 227 const todosData = { 228 __typename: 'Query', 229 todos: [ 230 { id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo' }, 231 { id: '1', text: 'Pick up the kids', complete: true, __typename: 'Todo' }, 232 { id: '2', text: 'Install urql', complete: false, __typename: 'Todo' }, 233 ], 234 }; 235 236 write(store, { query: Todos }, todosData); 237 238 InMemoryData.initDataState('write', store.data, null); 239 let data = store.readFragment(TodoFragment, { __typename: 'Todo', id: '0' }); 240 241 expect(data).toEqual({ 242 id: '0', 243 text: 'Go to the shops', 244 complete: false, 245 __typename: 'Todo', 246 }); 247 248 InMemoryData.initDataState('read', store.data, null); 249 data = store.readFragment(TodoFragment, { __typename: 'Todo', id: '0' }); 250 251 expect(data).toEqual({ 252 id: '0', 253 text: '[redacted]', 254 complete: false, 255 __typename: 'Todo', 256 }); 257}); 258 259it('respects property-level resolvers when given', () => { 260 const store = new Store({ 261 resolvers: { 262 Todo: { text: () => 'hi' }, 263 }, 264 }); 265 const todosData = { 266 __typename: 'Query', 267 todos: [ 268 { id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo' }, 269 { id: '1', text: 'Pick up the kids', complete: true, __typename: 'Todo' }, 270 { id: '2', text: 'Install urql', complete: false, __typename: 'Todo' }, 271 ], 272 }; 273 274 const writeRes = write(store, { query: Todos }, todosData); 275 276 expect(writeRes.dependencies).toEqual( 277 new Set(['Query.todos', 'Todo:0', 'Todo:1', 'Todo:2']) 278 ); 279 280 let queryRes = query(store, { query: Todos }); 281 282 expect(queryRes.data).toEqual({ 283 __typename: 'Query', 284 todos: [ 285 { id: '0', text: 'hi', complete: false, __typename: 'Todo' }, 286 { id: '1', text: 'hi', complete: true, __typename: 'Todo' }, 287 { id: '2', text: 'hi', complete: false, __typename: 'Todo' }, 288 ], 289 }); 290 expect(queryRes.dependencies).toEqual(writeRes.dependencies); 291 expect(queryRes.partial).toBe(false); 292 293 const mutatedTodo = { 294 ...todosData.todos[2], 295 complete: true, 296 }; 297 298 const mutationRes = write( 299 store, 300 { query: ToggleTodo, variables: { id: '2' } }, 301 { 302 __typename: 'Mutation', 303 toggleTodo: mutatedTodo, 304 } 305 ); 306 307 expect(mutationRes.dependencies).toEqual(new Set(['Todo:2'])); 308 309 queryRes = query(store, { query: Todos }); 310 311 expect(queryRes.partial).toBe(false); 312 expect(queryRes.data).toEqual({ 313 ...todosData, 314 todos: [ 315 { id: '0', text: 'hi', complete: false, __typename: 'Todo' }, 316 { id: '1', text: 'hi', complete: true, __typename: 'Todo' }, 317 { id: '2', text: 'hi', complete: true, __typename: 'Todo' }, 318 ], 319 }); 320}); 321 322it('respects Mutation update functions', () => { 323 const store = new Store({ 324 updates: { 325 Mutation: { 326 toggleTodo: function toggleTodo(result, _, cache) { 327 cache.updateQuery({ query: Todos }, data => { 328 if ( 329 data && 330 data.todos && 331 result && 332 result.toggleTodo && 333 (result.toggleTodo as any).id === '1' 334 ) { 335 data.todos[1] = { 336 id: '1', 337 text: `${data.todos[1].text} (Updated)`, 338 complete: (result.toggleTodo as any).complete, 339 __typename: 'Todo', 340 }; 341 } else if (data && data.todos) { 342 data.todos[Number((result.toggleTodo as any).id)] = { 343 ...data.todos[Number((result.toggleTodo as any).id)], 344 complete: (result.toggleTodo as any).complete, 345 }; 346 } 347 return data as Data; 348 }); 349 }, 350 }, 351 }, 352 }); 353 354 const todosData = { 355 __typename: 'Query', 356 todos: [ 357 { id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo' }, 358 { 359 id: '1', 360 text: 'Pick up the kids', 361 complete: false, 362 __typename: 'Todo', 363 }, 364 { id: '2', text: 'Install urql', complete: false, __typename: 'Todo' }, 365 ], 366 }; 367 368 write(store, { query: Todos }, todosData); 369 370 write( 371 store, 372 { query: ToggleTodo, variables: { id: '1' } }, 373 { 374 __typename: 'Mutation', 375 toggleTodo: { 376 ...todosData.todos[1], 377 complete: true, 378 }, 379 } 380 ); 381 382 write( 383 store, 384 { query: ToggleTodo, variables: { id: '2' } }, 385 { 386 __typename: 'Mutation', 387 toggleTodo: { 388 ...todosData.todos[2], 389 complete: true, 390 }, 391 } 392 ); 393 394 const queryRes = query(store, { query: Todos }); 395 396 expect(queryRes.partial).toBe(false); 397 expect(queryRes.data).toEqual({ 398 ...todosData, 399 todos: [ 400 todosData.todos[0], 401 { 402 id: '1', 403 text: 'Pick up the kids (Updated)', 404 complete: true, 405 __typename: 'Todo', 406 }, 407 { id: '2', text: 'Install urql', complete: true, __typename: 'Todo' }, 408 ], 409 }); 410}); 411 412it('respects arbitrary type update functions', () => { 413 const store = new Store({ 414 updates: { 415 Todo: { 416 text(result, _, cache) { 417 const fragment = gql` 418 fragment _ on Todo { 419 id 420 complete 421 } 422 `; 423 424 cache.writeFragment(fragment, { 425 id: result.id, 426 complete: true, 427 }); 428 }, 429 }, 430 }, 431 }); 432 433 const todosData = { 434 __typename: 'Query', 435 todos: [ 436 { id: '1', text: 'First', complete: false, __typename: 'Todo' }, 437 { id: '2', text: 'Second', complete: false, __typename: 'Todo' }, 438 ], 439 }; 440 441 write(store, { query: Todos }, todosData); 442 const queryRes = query(store, { query: Todos }); 443 444 expect(queryRes.partial).toBe(false); 445 expect(queryRes.data).toEqual({ 446 ...todosData, 447 todos: [ 448 { 449 ...todosData.todos[0], 450 complete: true, 451 }, 452 { 453 ...todosData.todos[1], 454 complete: true, 455 }, 456 ], 457 }); 458}); 459 460it('correctly resolves optimistic updates on Relay schemas', () => { 461 const store = new Store({ 462 optimistic: { 463 updateItem: variables => ({ 464 __typename: 'UpdateItemPayload', 465 item: { 466 __typename: 'Item', 467 id: variables.id as string, 468 name: 'Offline', 469 }, 470 }), 471 }, 472 }); 473 474 const queryData = { 475 __typename: 'Query', 476 root: { 477 __typename: 'Root', 478 id: 'root', 479 items: { 480 __typename: 'ItemConnection', 481 edges: [ 482 { 483 __typename: 'ItemEdge', 484 node: { 485 __typename: 'Item', 486 id: '1', 487 name: 'Number One', 488 }, 489 }, 490 { 491 __typename: 'ItemEdge', 492 node: { 493 __typename: 'Item', 494 id: '2', 495 name: 'Number Two', 496 }, 497 }, 498 ], 499 }, 500 }, 501 }; 502 503 const getRoot = gql` 504 query GetRoot { 505 root { 506 __typename 507 id 508 items { 509 __typename 510 edges { 511 __typename 512 node { 513 __typename 514 id 515 name 516 } 517 } 518 } 519 } 520 } 521 `; 522 523 const updateItem = gql` 524 mutation UpdateItem($id: ID!) { 525 updateItem(id: $id) { 526 __typename 527 item { 528 __typename 529 id 530 name 531 } 532 } 533 } 534 `; 535 536 write(store, { query: getRoot }, queryData); 537 const { dependencies } = writeOptimistic( 538 store, 539 { query: updateItem, variables: { id: '2' } }, 540 1 541 ); 542 expect(dependencies.size).not.toBe(0); 543 InMemoryData.noopDataState(store.data, 1); 544 const queryRes = query(store, { query: getRoot }); 545 546 expect(queryRes.partial).toBe(false); 547 expect(queryRes.data).not.toBe(null); 548}); 549 550it('skips non-optimistic mutation fields on writes', () => { 551 const store = new Store(); 552 553 const updateItem = gql` 554 mutation UpdateItem($id: ID!) { 555 updateItem(id: $id) { 556 __typename 557 item { 558 __typename 559 id 560 name 561 } 562 } 563 } 564 `; 565 566 const { dependencies } = writeOptimistic( 567 store, 568 { query: updateItem, variables: { id: '2' } }, 569 1 570 ); 571 expect(dependencies.size).toBe(0); 572}); 573 574it('allows cumulative optimistic updates', () => { 575 let counter = 1; 576 577 const store = new Store({ 578 updates: { 579 Mutation: { 580 addTodo: (result, _, cache) => { 581 cache.updateQuery({ query: Todos }, data => { 582 (data as any).todos.push(result.addTodo); 583 return data as Data; 584 }); 585 }, 586 }, 587 }, 588 optimistic: { 589 addTodo: () => ({ 590 __typename: 'Todo', 591 id: 'optimistic_' + ++counter, 592 text: '', 593 complete: false, 594 }), 595 }, 596 }); 597 598 const todosData = { 599 __typename: 'Query', 600 todos: [ 601 { id: '0', complete: true, text: '0', __typename: 'Todo' }, 602 { id: '1', complete: true, text: '1', __typename: 'Todo' }, 603 ], 604 }; 605 606 write(store, { query: Todos }, todosData); 607 608 const AddTodo = gql` 609 mutation { 610 __typename 611 addTodo { 612 __typename 613 complete 614 text 615 id 616 } 617 } 618 `; 619 620 writeOptimistic(store, { query: AddTodo }, 1); 621 writeOptimistic(store, { query: AddTodo }, 2); 622 623 const queryRes = query(store, { query: Todos }); 624 625 expect(queryRes.partial).toBe(false); 626 expect(queryRes.data).toEqual({ 627 ...todosData, 628 todos: [ 629 todosData.todos[0], 630 todosData.todos[1], 631 { __typename: 'Todo', text: '', complete: false, id: 'optimistic_2' }, 632 { __typename: 'Todo', text: '', complete: false, id: 'optimistic_3' }, 633 ], 634 }); 635}); 636 637it('supports clearing a layer then reapplying optimistic updates', () => { 638 let counter = 1; 639 640 const store = new Store({ 641 updates: { 642 Mutation: { 643 addTodo: (result, _, cache) => { 644 cache.updateQuery({ query: Todos }, data => { 645 (data as any).todos.push(result.addTodo); 646 return data as Data; 647 }); 648 }, 649 }, 650 }, 651 optimistic: { 652 addTodo: () => ({ 653 __typename: 'Todo', 654 id: 'optimistic_' + ++counter, 655 text: '', 656 complete: false, 657 }), 658 }, 659 }); 660 661 const todosData = { 662 __typename: 'Query', 663 todos: [ 664 { id: '0', complete: true, text: '0', __typename: 'Todo' }, 665 { id: '1', complete: true, text: '1', __typename: 'Todo' }, 666 ], 667 }; 668 669 write(store, { query: Todos }, todosData); 670 671 const AddTodo = gql` 672 mutation { 673 __typename 674 addTodo { 675 __typename 676 complete 677 text 678 id 679 } 680 } 681 `; 682 683 writeOptimistic(store, { query: AddTodo }, 1); 684 writeOptimistic(store, { query: AddTodo }, 1); 685 686 InMemoryData.noopDataState(store.data, 1); 687 688 writeOptimistic(store, { query: AddTodo }, 1); 689 writeOptimistic(store, { query: AddTodo }, 1); 690 691 const queryRes = query(store, { query: Todos }); 692 693 expect(queryRes.partial).toBe(false); 694 expect(queryRes.data).toEqual({ 695 ...todosData, 696 todos: [ 697 todosData.todos[0], 698 todosData.todos[1], 699 { __typename: 'Todo', text: '', complete: false, id: 'optimistic_4' }, 700 { __typename: 'Todo', text: '', complete: false, id: 'optimistic_5' }, 701 ], 702 }); 703}); 704 705it('supports seeing the same optimistic key multiple times (correctly reorders)', () => { 706 const store = new Store({ 707 optimistic: { 708 updateTodo: (args: any) => ({ 709 __typename: 'Todo', 710 id: args.id, 711 complete: args.completed, 712 }), 713 }, 714 }); 715 716 const todosData = { 717 __typename: 'Query', 718 todos: [ 719 { id: '0', complete: false, text: '0', __typename: 'Todo' }, 720 { id: '1', complete: false, text: '1', __typename: 'Todo' }, 721 ], 722 }; 723 724 write(store, { query: Todos }, todosData); 725 726 const updateTodo = gql` 727 mutation ($id: ID!, $completed: Boolean!) { 728 __typename 729 updateTodo(id: $id, completed: $completed) { 730 __typename 731 complete 732 id 733 } 734 } 735 `; 736 737 writeOptimistic( 738 store, 739 { query: updateTodo, variables: { id: '0', completed: true } }, 740 1 741 ); 742 743 let queryRes = query(store, { query: Todos }); 744 expect(queryRes.partial).toBe(false); 745 expect(queryRes.data?.todos?.[0]?.complete).toEqual(true); 746 747 writeOptimistic( 748 store, 749 { query: updateTodo, variables: { id: '0', completed: false } }, 750 2 751 ); 752 753 queryRes = query(store, { query: Todos }); 754 755 expect(queryRes.partial).toBe(false); 756 expect(queryRes.data?.todos?.[0]?.complete).toEqual(false); 757 758 writeOptimistic( 759 store, 760 { query: updateTodo, variables: { id: '0', completed: true } }, 761 1 762 ); 763 queryRes = query(store, { query: Todos }); 764 expect(queryRes.partial).toBe(false); 765 expect(queryRes.data?.todos?.[0]?.complete).toEqual(true); 766 767 writeOptimistic( 768 store, 769 { query: updateTodo, variables: { id: '0', completed: false } }, 770 2 771 ); 772 773 queryRes = query(store, { query: Todos }); 774 775 expect(queryRes.partial).toBe(false); 776 expect(queryRes.data?.todos?.[0]?.complete).toEqual(false); 777});