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