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