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});