Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 32 kB view raw
1/* eslint-disable @typescript-eslint/no-var-requires */ 2import { minifyIntrospectionQuery } from '@urql/introspection'; 3import { formatDocument, gql } from '@urql/core'; 4import { vi, expect, it, beforeEach, describe } from 'vitest'; 5 6import { 7 executeSync, 8 getIntrospectionQuery, 9 buildClientSchema, 10 parse, 11} from 'graphql'; 12 13import { Data, StorageAdapter } from '../types'; 14import { makeContext, updateContext } from '../operations/shared'; 15import * as InMemoryData from './data'; 16import { Store } from './store'; 17import { noop } from '../test-utils/utils'; 18 19import { __initAnd_query as query } from '../operations/query'; 20import { 21 __initAnd_write as write, 22 __initAnd_writeOptimistic as writeOptimistic, 23} from '../operations/write'; 24 25const mocked = (x: any): any => x; 26 27const Appointment = gql` 28 query appointment($id: String) { 29 __typename 30 appointment(id: $id) { 31 __typename 32 id 33 info 34 } 35 } 36`; 37 38const Todos = gql` 39 query { 40 __typename 41 todos { 42 __typename 43 id 44 text 45 complete 46 author { 47 __typename 48 id 49 name 50 } 51 } 52 } 53`; 54 55const TodosWithoutTypename = gql` 56 query { 57 __typename 58 todos { 59 id 60 text 61 complete 62 author { 63 id 64 name 65 } 66 } 67 } 68`; 69 70const todosData = { 71 __typename: 'Query', 72 todos: [ 73 { 74 id: '0', 75 text: 'Go to the shops', 76 complete: false, 77 __typename: 'Todo', 78 author: { id: '0', name: 'Jovi', __typename: 'Author' }, 79 }, 80 { 81 id: '1', 82 text: 'Pick up the kids', 83 complete: true, 84 __typename: 'Todo', 85 author: { id: '1', name: 'Phil', __typename: 'Author' }, 86 }, 87 { 88 id: '2', 89 text: 'Install urql', 90 complete: false, 91 __typename: 'Todo', 92 author: { id: '0', name: 'Jovi', __typename: 'Author' }, 93 }, 94 ], 95} as any; 96 97describe('Store', () => { 98 it('supports unformatted query documents', () => { 99 const store = new Store(); 100 101 // NOTE: This is the query without __typename annotations 102 write(store, { query: TodosWithoutTypename }, todosData); 103 const result = query(store, { query: TodosWithoutTypename }); 104 expect(result.data).toEqual({ 105 todos: [ 106 { 107 id: '0', 108 text: 'Go to the shops', 109 complete: false, 110 author: { id: '0', name: 'Jovi' }, 111 }, 112 { 113 id: '1', 114 text: 'Pick up the kids', 115 complete: true, 116 author: { id: '1', name: 'Phil' }, 117 }, 118 { 119 id: '2', 120 text: 'Install urql', 121 complete: false, 122 author: { id: '0', name: 'Jovi' }, 123 }, 124 ], 125 __typename: 'Query', 126 }); 127 }); 128}); 129 130describe('Store with UpdatesConfig', () => { 131 it("sets the store's updates field to the given argument", () => { 132 const updatesOption = { 133 Mutation: { 134 toggleTodo: noop, 135 }, 136 Subscription: { 137 newTodo: noop, 138 }, 139 }; 140 141 const store = new Store({ 142 updates: updatesOption, 143 }); 144 145 expect(store.updates.Mutation).toBe(updatesOption.Mutation); 146 expect(store.updates.Subscription).toBe(updatesOption.Subscription); 147 }); 148 149 it('should not warn if Mutation/Subscription operations do exist in the schema', function () { 150 new Store({ 151 schema: minifyIntrospectionQuery( 152 require('../test-utils/simple_schema.json') 153 ), 154 updates: { 155 Mutation: { 156 toggleTodo: noop, 157 }, 158 Subscription: { 159 newTodo: noop, 160 }, 161 }, 162 }); 163 164 expect(console.warn).not.toBeCalled(); 165 }); 166 167 it("should warn if Mutation operations don't exist in the schema", function () { 168 new Store({ 169 schema: minifyIntrospectionQuery( 170 require('../test-utils/simple_schema.json') 171 ), 172 updates: { 173 Mutation: { 174 doTheChaChaSlide: noop, 175 }, 176 }, 177 }); 178 179 expect(console.warn).toBeCalledTimes(1); 180 const warnMessage = mocked(console.warn).mock.calls[0][0]; 181 expect(warnMessage).toContain( 182 'Invalid updates field: `doTheChaChaSlide` on `Mutation` is not in the defined schema' 183 ); 184 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#22'); 185 }); 186 187 it("should warn if Subscription operations don't exist in the schema", function () { 188 new Store({ 189 schema: minifyIntrospectionQuery( 190 require('../test-utils/simple_schema.json') 191 ), 192 updates: { 193 Subscription: { 194 someoneDidTheChaChaSlide: noop, 195 }, 196 }, 197 }); 198 199 expect(console.warn).toBeCalledTimes(1); 200 const warnMessage = mocked(console.warn).mock.calls[0][0]; 201 expect(warnMessage).toContain( 202 'Invalid updates field: `someoneDidTheChaChaSlide` on `Subscription` is not in the defined schema' 203 ); 204 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#22'); 205 }); 206}); 207 208describe('Store with KeyingConfig', () => { 209 it('generates keys from custom keying function', () => { 210 const store = new Store({ 211 keys: { 212 User: () => 'me', 213 None: () => null, 214 }, 215 }); 216 217 expect(store.keyOfEntity({ __typename: 'Any', id: '123' })).toBe('Any:123'); 218 expect(store.keyOfEntity({ __typename: 'Any', _id: '123' })).toBe( 219 'Any:123' 220 ); 221 expect(store.keyOfEntity({ __typename: 'Any' })).toBe(null); 222 expect(store.keyOfEntity({ __typename: 'User' })).toBe('User:me'); 223 expect(store.keyOfEntity({ __typename: 'None' })).toBe(null); 224 }); 225 226 it('should not warn if keys do exist in the schema', function () { 227 new Store({ 228 schema: minifyIntrospectionQuery( 229 require('../test-utils/simple_schema.json') 230 ), 231 keys: { 232 Todo: () => 'Todo', 233 }, 234 }); 235 236 expect(console.warn).not.toBeCalled(); 237 }); 238 239 it("should warn if a key doesn't exist in the schema", function () { 240 new Store({ 241 schema: minifyIntrospectionQuery( 242 require('../test-utils/simple_schema.json') 243 ), 244 keys: { 245 Todo: () => 'todo', 246 NotInSchema: () => 'foo', 247 }, 248 }); 249 250 expect(console.warn).toBeCalledTimes(1); 251 const warnMessage = mocked(console.warn).mock.calls[0][0]; 252 expect(warnMessage).toContain( 253 'The type `NotInSchema` is not an object in the defined schema, but the `keys` option is referencing it' 254 ); 255 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#20'); 256 }); 257}); 258 259describe('Store with Global IDs', () => { 260 it('generates keys without typenames when set to true', () => { 261 const store = new Store({ globalIDs: true }); 262 expect(store.keyOfEntity({ __typename: 'Any', id: '123' })).toBe('123'); 263 expect(store.keyOfEntity({ __typename: 'None', id: '123' })).toBe('123'); 264 }); 265 266 it('generates keys without typenames when matching an input set', () => { 267 const store = new Store({ globalIDs: ['User'] }); 268 expect(store.keyOfEntity({ __typename: 'Any', id: '123' })).toBe('Any:123'); 269 expect(store.keyOfEntity({ __typename: 'User', id: '123' })).toBe('123'); 270 }); 271}); 272 273describe('Store with ResolverConfig', () => { 274 it("sets the store's resolvers field to the given argument", () => { 275 const resolversOption = { 276 Query: { 277 latestTodo: () => 'todo', 278 }, 279 }; 280 281 const store = new Store({ 282 resolvers: resolversOption, 283 }); 284 285 expect(store.resolvers).toBe(resolversOption); 286 }); 287 288 it("sets the store's resolvers field to an empty default if not provided", () => { 289 const store = new Store({}); 290 291 expect(store.resolvers).toEqual({}); 292 }); 293 294 it('should not warn if resolvers do exist in the schema', function () { 295 new Store({ 296 schema: minifyIntrospectionQuery( 297 require('../test-utils/simple_schema.json') 298 ), 299 resolvers: { 300 Query: { 301 latestTodo: () => 'todo', 302 todos: () => ['todo 1', 'todo 2'], 303 }, 304 Todo: { 305 text: todo => (todo.text as string).toUpperCase(), 306 author: todo => (todo.author as string).toUpperCase(), 307 }, 308 }, 309 }); 310 311 expect(console.warn).not.toBeCalled(); 312 }); 313 314 it("should warn if a Query doesn't exist in the schema", function () { 315 new Store({ 316 schema: minifyIntrospectionQuery( 317 require('../test-utils/simple_schema.json') 318 ), 319 resolvers: { 320 Query: { 321 todos: () => ['todo 1', 'todo 2'], 322 // This query should be warned about. 323 findDeletedTodos: () => ['todo 1', 'todo 2'], 324 }, 325 }, 326 }); 327 328 expect(console.warn).toBeCalledTimes(1); 329 const warnMessage = mocked(console.warn).mock.calls[0][0]; 330 expect(warnMessage).toContain( 331 'Invalid resolver: `Query.findDeletedTodos` is not in the defined schema, but the `resolvers` option is referencing it' 332 ); 333 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23'); 334 }); 335 336 it("should warn if a type doesn't exist in the schema", function () { 337 new Store({ 338 schema: minifyIntrospectionQuery( 339 require('../test-utils/simple_schema.json') 340 ), 341 resolvers: { 342 Todo: { 343 complete: () => true, 344 }, 345 // This type should be warned about. 346 Dinosaur: { 347 isExtinct: () => true, 348 }, 349 }, 350 }); 351 352 expect(console.warn).toBeCalledTimes(1); 353 const warnMessage = mocked(console.warn).mock.calls[0][0]; 354 expect(warnMessage).toContain( 355 'Invalid resolver: `Dinosaur` is not in the defined schema, but the `resolvers` option is referencing it' 356 ); 357 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23'); 358 }); 359 360 it('should warn when we use an interface type', function () { 361 new Store({ 362 schema: minifyIntrospectionQuery( 363 require('../test-utils/simple_schema.json') 364 ), 365 resolvers: { 366 ITodo: { 367 complete: () => true, 368 }, 369 }, 370 }); 371 372 expect(console.warn).toBeCalledTimes(1); 373 const warnMessage = mocked(console.warn).mock.calls[0][0]; 374 expect(warnMessage).toContain( 375 'Invalid resolver: `ITodo` does not match to a concrete type in the schema, but the `resolvers` option is referencing it. Implement the resolver for the types that implement the interface instead.' 376 ); 377 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#26'); 378 }); 379 380 it("should warn if a type's property doesn't exist in the schema", function () { 381 new Store({ 382 schema: minifyIntrospectionQuery( 383 require('../test-utils/simple_schema.json') 384 ), 385 resolvers: { 386 Todo: { 387 complete: () => true, 388 // This property should be warned about. 389 isAboutDinosaurs: () => true, 390 }, 391 }, 392 }); 393 394 expect(console.warn).toBeCalledTimes(1); 395 const warnMessage = mocked(console.warn).mock.calls[0][0]; 396 expect(warnMessage).toContain( 397 'Invalid resolver: `Todo.isAboutDinosaurs` is not in the defined schema, but the `resolvers` option is referencing it' 398 ); 399 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23'); 400 }); 401}); 402 403describe('Store with OptimisticMutationConfig', () => { 404 let store; 405 let context; 406 407 beforeEach(() => { 408 store = new Store({ 409 optimistic: { 410 addTodo: variables => { 411 return { 412 ...variables, 413 } as Data; 414 }, 415 }, 416 }); 417 418 context = makeContext(store, {}, {}, 'Query', 'Query', undefined); 419 write(store, { query: Todos }, todosData); 420 InMemoryData.initDataState('read', store.data, null); 421 }); 422 423 it('should resolve a property', () => { 424 const todoResult = store.resolve({ __typename: 'Todo', id: '0' }, 'text'); 425 expect(todoResult).toEqual('Go to the shops'); 426 const authorResult = store.resolve( 427 { __typename: 'Author', id: '0' }, 428 'name' 429 ); 430 expect(authorResult).toBe('Jovi'); 431 const result = store.resolve({ id: 0, __typename: 'Todo' }, 'text'); 432 expect(result).toEqual('Go to the shops'); 433 // TODO: we have no way of asserting this to really be the case. 434 const deps = InMemoryData.getCurrentDependencies(); 435 expect(deps).toEqual(new Set(['Todo:0', 'Author:0'])); 436 InMemoryData.clearDataState(); 437 }); 438 439 it('should resolve current parent argument fields', () => { 440 const randomData = { __typename: 'Todo', id: 1, createdAt: '2020-12-09' }; 441 442 updateContext( 443 context, 444 randomData, 445 'Todo', 446 'Todo:1', 447 'createdAt', 448 'createdAt' 449 ); 450 451 expect(store.keyOfEntity(randomData)).toBe(context.parentKey); 452 expect(store.keyOfEntity({})).not.toBe(context.parentKey); 453 454 // Should work without a __typename field 455 delete (randomData as any).__typename; 456 expect(store.keyOfEntity(randomData)).toBe(context.parentKey); 457 }); 458 459 it('should resolve with a key as first argument', () => { 460 const authorResult = store.resolve('Author:0', 'name'); 461 expect(authorResult).toBe('Jovi'); 462 const deps = InMemoryData.getCurrentDependencies(); 463 expect(deps).toEqual(new Set(['Author:0'])); 464 InMemoryData.clearDataState(); 465 }); 466 467 it('should resolve a link property', () => { 468 const parent = { 469 id: '0', 470 text: 'test', 471 author: undefined, 472 __typename: 'Todo', 473 }; 474 const result = store.resolve(parent, 'author'); 475 expect(result).toEqual('Author:0'); 476 const deps = InMemoryData.getCurrentDependencies(); 477 expect(deps).toEqual(new Set(['Todo:0'])); 478 InMemoryData.clearDataState(); 479 }); 480 481 it('should invalidate null keys correctly', () => { 482 const connection = gql` 483 query test { 484 exercisesConnection(page: { after: null, first: 10 }) { 485 id 486 } 487 } 488 `; 489 490 write( 491 store, 492 { 493 query: connection, 494 }, 495 { 496 exercisesConnection: null, 497 } as any 498 ); 499 let { data } = query(store, { query: connection }); 500 501 InMemoryData.initDataState('write', store.data, null); 502 expect((data as any).exercisesConnection).toEqual(null); 503 const fields = store.inspectFields({ __typename: 'Query' }); 504 fields.forEach(({ fieldName, arguments: args }) => { 505 if (fieldName === 'exercisesConnection') { 506 store.invalidate('Query', fieldName, args); 507 } 508 }); 509 InMemoryData.clearDataState(); 510 511 ({ data } = query(store, { query: connection })); 512 expect(data).toBe(null); 513 }); 514 515 it('should be able to write a fragment', () => { 516 InMemoryData.initDataState('write', store.data, null); 517 518 store.writeFragment( 519 gql` 520 fragment _ on Todo { 521 id 522 text 523 complete 524 } 525 `, 526 { 527 id: '0', 528 text: 'update', 529 complete: true, 530 } 531 ); 532 533 const deps = InMemoryData.getCurrentDependencies(); 534 expect(deps).toEqual(new Set(['Todo:0'])); 535 536 const { data } = query(store, { query: Todos }); 537 538 expect(data).toEqual({ 539 __typename: 'Query', 540 todos: [ 541 { 542 ...todosData.todos[0], 543 text: 'update', 544 complete: true, 545 }, 546 todosData.todos[1], 547 todosData.todos[2], 548 ], 549 }); 550 }); 551 552 it('should be able to write a fragment by name', () => { 553 InMemoryData.initDataState('write', store.data, null); 554 555 store.writeFragment( 556 gql` 557 fragment authorFields on Author { 558 id 559 } 560 561 fragment todoFields on Todo { 562 id 563 text 564 complete 565 } 566 `, 567 { 568 id: '0', 569 text: 'update', 570 complete: true, 571 }, 572 undefined, 573 'todoFields' 574 ); 575 576 const deps = InMemoryData.getCurrentDependencies(); 577 expect(deps).toEqual(new Set(['Todo:0'])); 578 579 const { data } = query(store, { query: Todos }); 580 581 expect(data).toEqual({ 582 __typename: 'Query', 583 todos: [ 584 { 585 ...todosData.todos[0], 586 text: 'update', 587 complete: true, 588 }, 589 todosData.todos[1], 590 todosData.todos[2], 591 ], 592 }); 593 }); 594 595 it('should be able to read a fragment', () => { 596 InMemoryData.initDataState('read', store.data, null); 597 const result = store.readFragment( 598 gql` 599 fragment _ on Todo { 600 id 601 text 602 complete 603 __typename 604 } 605 `, 606 { id: '0' } 607 ); 608 609 const deps = InMemoryData.getCurrentDependencies(); 610 expect(deps).toEqual(new Set(['Todo:0'])); 611 612 expect(result).toEqual({ 613 id: '0', 614 text: 'Go to the shops', 615 complete: false, 616 __typename: 'Todo', 617 }); 618 619 InMemoryData.clearDataState(); 620 }); 621 622 it('should be able to read a fragment by name', () => { 623 InMemoryData.initDataState('read', store.data, null); 624 const result = store.readFragment( 625 gql` 626 fragment authorFields on Author { 627 id 628 text 629 complete 630 __typename 631 } 632 633 fragment todoFields on Todo { 634 id 635 text 636 complete 637 __typename 638 } 639 `, 640 { id: '0' }, 641 undefined, 642 'todoFields' 643 ); 644 645 const deps = InMemoryData.getCurrentDependencies(); 646 expect(deps).toEqual(new Set(['Todo:0'])); 647 648 expect(result).toEqual({ 649 id: '0', 650 text: 'Go to the shops', 651 complete: false, 652 __typename: 'Todo', 653 }); 654 655 InMemoryData.clearDataState(); 656 }); 657 658 it('should be able to update a query', () => { 659 InMemoryData.initDataState('write', store.data, null); 660 store.updateQuery({ query: Todos }, data => ({ 661 ...data, 662 todos: [ 663 ...data.todos, 664 { 665 __typename: 'Todo', 666 id: '4', 667 text: 'Test updateQuery', 668 complete: false, 669 author: { 670 __typename: 'Author', 671 id: '3', 672 name: 'Andy', 673 }, 674 }, 675 ], 676 })); 677 InMemoryData.clearDataState(); 678 679 const { data: result } = query(store, { 680 query: Todos, 681 }); 682 683 expect(result).toEqual({ 684 __typename: 'Query', 685 todos: [ 686 ...todosData.todos, 687 { 688 __typename: 'Todo', 689 id: '4', 690 text: 'Test updateQuery', 691 complete: false, 692 author: { 693 __typename: 'Author', 694 id: '3', 695 name: 'Andy', 696 }, 697 }, 698 ], 699 }); 700 }); 701 702 it('should be able to update a query with variables', () => { 703 write( 704 store, 705 { 706 query: Appointment, 707 variables: { id: '1' }, 708 }, 709 { 710 __typename: 'Query', 711 appointment: { 712 __typename: 'Appointment', 713 id: '1', 714 info: 'urql meeting', 715 }, 716 } 717 ); 718 719 InMemoryData.initDataState('write', store.data, null); 720 store.updateQuery({ query: Appointment, variables: { id: '1' } }, data => ({ 721 ...data, 722 appointment: { 723 ...data.appointment, 724 info: 'urql meeting revisited', 725 }, 726 })); 727 InMemoryData.clearDataState(); 728 729 const { data: result } = query(store, { 730 query: Appointment, 731 variables: { id: '1' }, 732 }); 733 expect(result).toEqual({ 734 __typename: 'Query', 735 appointment: { 736 id: '1', 737 info: 'urql meeting revisited', 738 __typename: 'Appointment', 739 }, 740 }); 741 }); 742 743 it('should be able to read a query', () => { 744 InMemoryData.initDataState('read', store.data, null); 745 const result = store.readQuery({ query: Todos }); 746 747 const deps = InMemoryData.getCurrentDependencies(); 748 expect(deps).toEqual( 749 new Set([ 750 'Query.todos', 751 'Todo:0', 752 'Todo:1', 753 'Todo:2', 754 'Author:0', 755 'Author:1', 756 ]) 757 ); 758 759 expect(result).toEqual({ 760 __typename: 'Query', 761 todos: todosData.todos, 762 }); 763 InMemoryData.clearDataState(); 764 }); 765 766 it('should be able to optimistically mutate', () => { 767 const { dependencies } = writeOptimistic( 768 store, 769 { 770 query: gql` 771 mutation { 772 addTodo( 773 id: "1" 774 text: "I'm optimistic about this feature" 775 complete: true 776 __typename: "Todo" 777 ) { 778 id 779 text 780 complete 781 __typename 782 } 783 } 784 `, 785 }, 786 1 787 ); 788 expect(dependencies).toEqual(new Set(['Todo:1'])); 789 let { data } = query(store, { query: Todos }); 790 expect(data).toEqual({ 791 __typename: 'Query', 792 todos: [ 793 todosData.todos[0], 794 { 795 id: '1', 796 text: "I'm optimistic about this feature", 797 complete: true, 798 __typename: 'Todo', 799 author: { 800 __typename: 'Author', 801 id: '1', 802 name: 'Phil', 803 }, 804 }, 805 todosData.todos[2], 806 ], 807 }); 808 809 InMemoryData.noopDataState(store.data, 1); 810 811 ({ data } = query(store, { query: Todos })); 812 expect(data).toEqual({ 813 __typename: 'Query', 814 todos: todosData.todos, 815 }); 816 }); 817 818 it('should be able to optimistically mutate with partial data', () => { 819 const { dependencies } = writeOptimistic( 820 store, 821 { 822 query: gql` 823 mutation { 824 addTodo(id: "0", complete: true, __typename: "Todo") { 825 id 826 text 827 complete 828 __typename 829 } 830 } 831 `, 832 }, 833 1 834 ); 835 expect(dependencies).toEqual(new Set(['Todo:0'])); 836 let { data } = query(store, { query: Todos }); 837 expect(data).toEqual({ 838 __typename: 'Query', 839 todos: [ 840 { 841 ...todosData.todos[0], 842 complete: true, 843 }, 844 todosData.todos[1], 845 todosData.todos[2], 846 ], 847 }); 848 849 InMemoryData.noopDataState(store.data, 1); 850 851 ({ data } = query(store, { query: Todos })); 852 expect(data).toEqual({ 853 __typename: 'Query', 854 todos: todosData.todos, 855 }); 856 }); 857 858 describe('Invalidating an entity', () => { 859 it('removes an entity from a list by object-key.', () => { 860 InMemoryData.initDataState('write', store.data, null); 861 store.invalidate(todosData.todos[1]); 862 const { data } = query(store, { query: Todos }); 863 expect(data).toBe(null); 864 }); 865 866 it('removes an entity from a list by string-key.', () => { 867 InMemoryData.initDataState('write', store.data, null); 868 store.invalidate(store.keyOfEntity(todosData.todos[1])); 869 const { data } = query(store, { query: Todos }); 870 expect(data).toBe(null); 871 }); 872 }); 873 874 describe('Invalidating a type', () => { 875 it('removes an entity from a list.', () => { 876 InMemoryData.initDataState('write', store.data, null); 877 store.invalidate('Todo'); 878 const { data } = query(store, { query: Todos }); 879 expect(data).toBe(null); 880 }); 881 }); 882}); 883 884describe('Store with storage', () => { 885 let store: Store; 886 887 const expectedData = { 888 __typename: 'Query', 889 appointment: { 890 __typename: 'Appointment', 891 id: '1', 892 info: 'urql meeting', 893 }, 894 }; 895 896 beforeEach(() => { 897 store = new Store(); 898 }); 899 900 it('should be able to store and rehydrate data', () => { 901 const storage: StorageAdapter = { 902 readData: vi.fn(), 903 writeData: vi.fn(), 904 }; 905 906 store.data.storage = storage; 907 908 write( 909 store, 910 { 911 query: Appointment, 912 variables: { id: '1' }, 913 }, 914 expectedData 915 ); 916 917 InMemoryData.initDataState('write', store.data, null); 918 InMemoryData.persistData(); 919 InMemoryData.clearDataState(); 920 921 expect(storage.writeData).toHaveBeenCalled(); 922 923 const serialisedStore = (storage.writeData as any).mock.calls[0][0]; 924 expect(serialisedStore).toMatchSnapshot(); 925 926 store = new Store(); 927 InMemoryData.hydrateData(store.data, storage, serialisedStore); 928 929 const { data } = query(store, { 930 query: Appointment, 931 variables: { id: '1' }, 932 }); 933 934 expect(data).toEqual(expectedData); 935 }); 936 937 it('should be able to persist embedded data', () => { 938 const EmbeddedAppointment = gql` 939 query appointment($id: String) { 940 __typename 941 appointment(id: $id) { 942 __typename 943 info 944 } 945 } 946 `; 947 948 const embeddedData = { 949 ...expectedData, 950 appointment: { 951 ...expectedData.appointment, 952 id: undefined, 953 }, 954 } as any; 955 956 const storage: StorageAdapter = { 957 readData: vi.fn(), 958 writeData: vi.fn(), 959 }; 960 961 store.data.storage = storage; 962 963 write( 964 store, 965 { 966 query: EmbeddedAppointment, 967 variables: { id: '1' }, 968 }, 969 embeddedData 970 ); 971 972 InMemoryData.initDataState('write', store.data, null); 973 InMemoryData.persistData(); 974 InMemoryData.clearDataState(); 975 976 expect(storage.writeData).toHaveBeenCalled(); 977 978 const serialisedStore = (storage.writeData as any).mock.calls[0][0]; 979 expect(serialisedStore).toMatchSnapshot(); 980 981 store = new Store(); 982 InMemoryData.hydrateData(store.data, storage, serialisedStore); 983 984 const { data } = query(store, { 985 query: EmbeddedAppointment, 986 variables: { id: '1' }, 987 }); 988 989 expect(data).toEqual(embeddedData); 990 }); 991 992 it('persists commutative layers and ignores optimistic layers', () => { 993 const storage: StorageAdapter = { 994 readData: vi.fn(), 995 writeData: vi.fn(), 996 }; 997 998 store.data.storage = storage; 999 1000 InMemoryData.reserveLayer(store.data, 1); 1001 1002 InMemoryData.initDataState('write', store.data, 1); 1003 InMemoryData.writeRecord('Query', 'base', true); 1004 InMemoryData.clearDataState(); 1005 1006 InMemoryData.initDataState('write', store.data, 2, true); 1007 InMemoryData.writeRecord('Query', 'base', false); 1008 InMemoryData.clearDataState(); 1009 1010 InMemoryData.initDataState('read', store.data, null); 1011 expect(InMemoryData.readRecord('Query', 'base')).toBe(false); 1012 InMemoryData.persistData(); 1013 InMemoryData.clearDataState(); 1014 1015 expect(storage.writeData).toHaveBeenCalled(); 1016 const serialisedStore = (storage.writeData as any).mock.calls[0][0]; 1017 1018 expect(serialisedStore).toEqual({ 1019 'Query.base': 'true', 1020 }); 1021 1022 store = new Store(); 1023 InMemoryData.hydrateData(store.data, storage, serialisedStore); 1024 1025 InMemoryData.initDataState('write', store.data, null); 1026 expect(InMemoryData.readRecord('Query', 'base')).toBe(true); 1027 InMemoryData.clearDataState(); 1028 }); 1029 1030 it("should warn if an optimistic field doesn't exist in the schema's mutations", () => { 1031 new Store({ 1032 schema: minifyIntrospectionQuery( 1033 require('../test-utils/simple_schema.json') 1034 ), 1035 updates: { 1036 Mutation: { 1037 toggleTodo: noop, 1038 }, 1039 }, 1040 optimistic: { 1041 toggleTodo: () => null, 1042 // This field should be warned about. 1043 deleteTodo: () => null, 1044 }, 1045 }); 1046 1047 expect(console.warn).toBeCalledTimes(1); 1048 const warnMessage = mocked(console.warn).mock.calls[0][0]; 1049 expect(warnMessage).toContain( 1050 'Invalid optimistic mutation field: `deleteTodo` is not a mutation field in the defined schema, but the `optimistic` option is referencing it.' 1051 ); 1052 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#24'); 1053 }); 1054 1055 it('should use different rootConfigs', () => { 1056 const fakeUpdater = vi.fn(); 1057 1058 const store = new Store({ 1059 schema: { 1060 __schema: { 1061 queryType: { 1062 name: 'query_root', 1063 }, 1064 mutationType: { 1065 name: 'mutation_root', 1066 }, 1067 subscriptionType: { 1068 name: 'subscription_root', 1069 }, 1070 }, 1071 }, 1072 updates: { 1073 mutation_root: { 1074 toggleTodo: fakeUpdater, 1075 }, 1076 }, 1077 }); 1078 1079 const mutationData = { 1080 toggleTodo: { 1081 __typename: 'Todo', 1082 id: 1, 1083 }, 1084 }; 1085 write(store, { query: Todos }, todosData); 1086 write( 1087 store, 1088 { 1089 query: gql` 1090 mutation { 1091 toggleTodo(id: 1) { 1092 id 1093 } 1094 } 1095 `, 1096 }, 1097 mutationData as any 1098 ); 1099 1100 expect(fakeUpdater).toBeCalledTimes(1); 1101 }); 1102 1103 it('should warn when __typename is missing when store.writeFragment is called', () => { 1104 InMemoryData.initDataState('write', store.data, null); 1105 1106 store.writeFragment( 1107 parse(` 1108 fragment _ on Test { 1109 __typename 1110 id 1111 sub { 1112 id 1113 } 1114 } 1115 `), 1116 { 1117 id: 'test', 1118 sub: { 1119 id: 'test', 1120 }, 1121 } 1122 ); 1123 1124 InMemoryData.clearDataState(); 1125 1126 expect(console.warn).toBeCalledTimes(1); 1127 const warnMessage = mocked(console.warn).mock.calls[0][0]; 1128 expect(warnMessage).toContain( 1129 "Couldn't find __typename when writing.\nIf you're writing to the cache manually have to pass a `__typename` property on each entity in your data." 1130 ); 1131 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#14'); 1132 }); 1133}); 1134 1135describe('Store introspection', () => { 1136 it('should not warn for an introspection result root (of an unminified schema)', function () { 1137 // NOTE: Do not wrap this require in `minifyIntrospectionQuery`! 1138 // eslint-disable-next-line 1139 const schema = require('../test-utils/simple_schema.json'); 1140 const store = new Store({ schema }); 1141 1142 const introspectionQuery = formatDocument(parse(getIntrospectionQuery())); 1143 1144 query(store, { query: introspectionQuery }, schema); 1145 expect(console.warn).toBeCalledTimes(0); 1146 }); 1147 1148 it('should not warn for an introspection result root (of a minified schema)', function () { 1149 // NOTE: Do not wrap this require in `minifyIntrospectionQuery`! 1150 // eslint-disable-next-line 1151 const schema = require('../test-utils/simple_schema.json'); 1152 const store = new Store({ schema: minifyIntrospectionQuery(schema) }); 1153 1154 const introspectionQuery = formatDocument(parse(getIntrospectionQuery())); 1155 1156 query(store, { query: introspectionQuery }, schema); 1157 expect(console.warn).toBeCalledTimes(0); 1158 }); 1159 1160 it('should not warn for an introspection result with typenames', function () { 1161 const schema = buildClientSchema( 1162 require('../test-utils/simple_schema.json') 1163 ); 1164 const introspectionQuery = formatDocument(parse(getIntrospectionQuery())); 1165 1166 const introspectionResult = executeSync({ 1167 document: introspectionQuery, 1168 schema, 1169 }).data as any; 1170 1171 const store = new Store({ 1172 schema: minifyIntrospectionQuery(introspectionResult), 1173 }); 1174 1175 write(store, { query: introspectionQuery }, introspectionResult); 1176 query(store, { query: introspectionQuery }); 1177 expect(console.warn).toBeCalledTimes(0); 1178 }); 1179}); 1180 1181it('should link up entities', () => { 1182 const store = new Store(); 1183 const todo = gql` 1184 query test { 1185 todo(id: "1") { 1186 id 1187 title 1188 __typename 1189 } 1190 } 1191 `; 1192 const author = gql` 1193 query testAuthor { 1194 author(id: "1") { 1195 id 1196 name 1197 __typename 1198 } 1199 } 1200 `; 1201 write( 1202 store, 1203 { 1204 query: todo, 1205 }, 1206 { 1207 todo: { 1208 id: '1', 1209 title: 'learn urql', 1210 __typename: 'Todo', 1211 }, 1212 __typename: 'Query', 1213 } as any 1214 ); 1215 let { data } = query(store, { query: todo }); 1216 expect((data as any).todo).toEqual({ 1217 id: '1', 1218 title: 'learn urql', 1219 __typename: 'Todo', 1220 }); 1221 write( 1222 store, 1223 { 1224 query: author, 1225 }, 1226 { 1227 author: { __typename: 'Author', id: '1', name: 'Formidable' }, 1228 __typename: 'Query', 1229 } as any 1230 ); 1231 InMemoryData.initDataState('write', store.data, null); 1232 store.link((data as any).todo, 'author', { 1233 __typename: 'Author', 1234 id: '1', 1235 name: 'Formidable', 1236 }); 1237 InMemoryData.clearDataState(); 1238 const todoWithAuthor = gql` 1239 query test { 1240 todo(id: "1") { 1241 id 1242 title 1243 __typename 1244 author { 1245 id 1246 name 1247 __typename 1248 } 1249 } 1250 } 1251 `; 1252 ({ data } = query(store, { query: todoWithAuthor })); 1253 expect((data as any).todo).toEqual({ 1254 id: '1', 1255 title: 'learn urql', 1256 __typename: 'Todo', 1257 author: { 1258 __typename: 'Author', 1259 id: '1', 1260 name: 'Formidable', 1261 }, 1262 }); 1263});