@luvio/graphql-parser 0.97.1 → 0.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,684 @@
1
+ import {
2
+ gql,
3
+ docMap,
4
+ astResolver,
5
+ processSubstitutions,
6
+ stripLocation,
7
+ addMetaschemaDirectives,
8
+ enableAddMetaschemaDirective,
9
+ disableAddMetaschemaDirective,
10
+ } from '../gql';
11
+ import ast from './ast.json';
12
+ import astNoLoc from './astNoLoc.json';
13
+
14
+ var mockParse;
15
+
16
+ jest.mock('graphql/language', () => {
17
+ const originalModule = jest.requireActual('graphql/language');
18
+ // we keep the original behavior, just wrapping it with a mock so we can spy
19
+ mockParse = jest.fn().mockImplementation((source, options) => {
20
+ return originalModule.parse(source, options);
21
+ });
22
+
23
+ return {
24
+ __esModule: true,
25
+ ...originalModule,
26
+ parse: mockParse,
27
+ };
28
+ });
29
+
30
+ const testProcessSubstitution = (literals: any, ...args: any[]) => {
31
+ return processSubstitutions(literals, args);
32
+ };
33
+
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+ docMap.clear();
37
+ });
38
+
39
+ const mockFragment1 = `fragment fragmentName1 on SomeType1 { someField1 }`;
40
+ const mockFragment2 = `fragment fragmentName2 on SomeType2 { someField2 }`;
41
+ const mockTwoFragments = `${mockFragment1} ${mockFragment2}`;
42
+
43
+ describe('processSubstitutions', () => {
44
+ it('should return correct operation string when no substitution is present', () => {
45
+ const result = testProcessSubstitution/* GraphQL */ `
46
+ query accounts {
47
+ uiapi {
48
+ query {
49
+ Account {
50
+ edges {
51
+ node {
52
+ Id
53
+ Name {
54
+ value
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ `;
63
+
64
+ expect(result.operationString.trim()).toBe(
65
+ /* GraphQL */ `
66
+ query accounts {
67
+ uiapi {
68
+ query {
69
+ Account {
70
+ edges {
71
+ node {
72
+ Id
73
+ Name {
74
+ value
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ `.trim()
83
+ );
84
+ expect(result.fragments).toStrictEqual([]);
85
+ });
86
+
87
+ it('should substitute strings correctly in the operation string', () => {
88
+ const field = `Contact`;
89
+ const subField = `displayName`;
90
+ const result = testProcessSubstitution/* GraphQL */ `
91
+ query accounts {
92
+ uiapi {
93
+ query {
94
+ Account {
95
+ edges {
96
+ node {
97
+ Id
98
+ Name {
99
+ value
100
+ ${subField}
101
+ }
102
+ ${field}
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ `;
110
+
111
+ expect(result.operationString.trim()).toBe(
112
+ /* GraphQL */ `
113
+ query accounts {
114
+ uiapi {
115
+ query {
116
+ Account {
117
+ edges {
118
+ node {
119
+ Id
120
+ Name {
121
+ value
122
+ displayName
123
+ }
124
+ Contact
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ `.trim()
132
+ );
133
+ expect(result.fragments).toStrictEqual([]);
134
+ });
135
+
136
+ it('should return correct operation string and fragments', () => {
137
+ const fragment = gql(mockFragment1);
138
+ const result = testProcessSubstitution/* GraphQL */ `
139
+ query accounts {
140
+ uiapi {
141
+ query {
142
+ Account {
143
+ edges {
144
+ node {
145
+ Id
146
+ Name {
147
+ value
148
+ }
149
+ ...fragmentName1
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ ${fragment}
157
+ `;
158
+
159
+ expect(result.operationString.trim()).toBe(
160
+ /* GraphQL */ `
161
+ query accounts {
162
+ uiapi {
163
+ query {
164
+ Account {
165
+ edges {
166
+ node {
167
+ Id
168
+ Name {
169
+ value
170
+ }
171
+ ...fragmentName1
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+ `.trim()
179
+ );
180
+
181
+ expect(result.fragments.length).toBe(1);
182
+ expect(result.fragments[0].kind).toBe('FragmentDefinition');
183
+ });
184
+
185
+ it('should return correct operation string and fragments when has multiple fragments in one substitute', () => {
186
+ const fragment = gql(mockTwoFragments);
187
+ const result = testProcessSubstitution/* GraphQL */ `
188
+ query accounts {
189
+ uiapi {
190
+ query {
191
+ Account {
192
+ edges {
193
+ node {
194
+ Id
195
+ Name {
196
+ value
197
+ }
198
+ ...fragmentName1
199
+ ...fragmentName2
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ ${fragment}
207
+ `;
208
+
209
+ expect(result.operationString.trim()).toBe(
210
+ /* GraphQL */ `
211
+ query accounts {
212
+ uiapi {
213
+ query {
214
+ Account {
215
+ edges {
216
+ node {
217
+ Id
218
+ Name {
219
+ value
220
+ }
221
+ ...fragmentName1
222
+ ...fragmentName2
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ `.trim()
230
+ );
231
+
232
+ expect(result.fragments.length).toBe(2);
233
+ expect(result.fragments[0].kind).toBe('FragmentDefinition');
234
+ expect(result.fragments[0].name.value).toBe('fragmentName1');
235
+ expect(result.fragments[1].kind).toBe('FragmentDefinition');
236
+ expect(result.fragments[1].name.value).toBe('fragmentName2');
237
+ });
238
+
239
+ it('should return correct operation string and fragments when both type of substitutions are present', () => {
240
+ const field = `Contact`;
241
+ const subField = `displayValue`;
242
+ const fragment1 = gql(mockFragment1);
243
+ const fragment2 = gql(mockFragment2);
244
+
245
+ const result = testProcessSubstitution/* GraphQL */ `
246
+ query accounts {
247
+ uiapi {
248
+ query {
249
+ Account {
250
+ edges {
251
+ node {
252
+ Id
253
+ Name {
254
+ value
255
+ ${subField}
256
+ }
257
+ ${field}
258
+ ...fragmentName1
259
+ ...fragmentName2
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ ${fragment1}
267
+ ${fragment2}
268
+ `;
269
+
270
+ expect(result.operationString.trim()).toBe(
271
+ /* GraphQL */ `
272
+ query accounts {
273
+ uiapi {
274
+ query {
275
+ Account {
276
+ edges {
277
+ node {
278
+ Id
279
+ Name {
280
+ value
281
+ displayValue
282
+ }
283
+ Contact
284
+ ...fragmentName1
285
+ ...fragmentName2
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+ `.trim()
293
+ );
294
+ expect(result.fragments.length).toBe(2);
295
+ expect(result.fragments[0].kind).toBe('FragmentDefinition');
296
+ expect(result.fragments[1].kind).toBe('FragmentDefinition');
297
+ });
298
+
299
+ it('should throw if the fragment document is not found', () => {
300
+ expect(() => {
301
+ return testProcessSubstitution/* GraphQL */ `
302
+ query accounts {
303
+ uiapi {
304
+ query {
305
+ Account {
306
+ edges {
307
+ node {
308
+ Id
309
+ Name {
310
+ value
311
+ }
312
+ ...fragmentName1
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+ ${{}}
320
+ `;
321
+ }).toThrowError('Invalid substitution fragment');
322
+ });
323
+
324
+ it('should throw if an unsupported substitution is found', () => {
325
+ expect(() => {
326
+ return testProcessSubstitution/* GraphQL */ `
327
+ query accounts {
328
+ uiapi {
329
+ query {
330
+ Account {
331
+ edges {
332
+ node {
333
+ Id
334
+ Name {
335
+ value
336
+ }
337
+ ...fragmentName1
338
+ }
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
344
+ ${true}
345
+ `;
346
+ }).toThrowError('Unsupported substitution type');
347
+ });
348
+ });
349
+
350
+ describe('gql', () => {
351
+ it(`should call the parsing api when response isn't cached`, () => {
352
+ const ref = gql`
353
+ query accounts {
354
+ uiapi {
355
+ query {
356
+ Account {
357
+ edges {
358
+ node {
359
+ Id
360
+ Name {
361
+ value
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+ }
368
+ }
369
+ `;
370
+ const doc = astResolver(ref);
371
+
372
+ expect(mockParse).toHaveBeenCalledTimes(1);
373
+ expect(doc.kind).toEqual('Document');
374
+ });
375
+
376
+ it(`should call the parsing api when query is passed as an argument and isn't cached`, () => {
377
+ const ref = gql(/* GraphQL */ `
378
+ query accounts {
379
+ uiapi {
380
+ query {
381
+ Account {
382
+ edges {
383
+ node {
384
+ Id
385
+ Name {
386
+ value
387
+ }
388
+ }
389
+ }
390
+ }
391
+ }
392
+ }
393
+ }
394
+ `);
395
+ const doc = astResolver(ref);
396
+
397
+ expect(mockParse).toHaveBeenCalledTimes(1);
398
+ expect(doc.kind).toEqual('Document');
399
+ });
400
+
401
+ it('should get the document from cache when same query is passed again', () => {
402
+ // Populate doc map cache
403
+ const ref1 = gql`
404
+ query accounts {
405
+ uiapi {
406
+ query {
407
+ Account {
408
+ edges {
409
+ node {
410
+ Id
411
+ Name {
412
+ value
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
419
+ }
420
+ `;
421
+
422
+ const ref2 = gql`
423
+ query accounts {
424
+ uiapi {
425
+ query {
426
+ Account {
427
+ edges {
428
+ node {
429
+ Id
430
+ Name {
431
+ value
432
+ }
433
+ }
434
+ }
435
+ }
436
+ }
437
+ }
438
+ }
439
+ `;
440
+ const doc1 = astResolver(ref1);
441
+ const doc2 = astResolver(ref2);
442
+
443
+ expect(mockParse).toHaveBeenCalledTimes(1);
444
+ expect(doc2.kind).toEqual('Document');
445
+ expect(doc1).toBe(doc2);
446
+ });
447
+
448
+ it('should get the document from cache when we get a semantically similar query again', () => {
449
+ // Populate doc map cache
450
+ const ref1 = gql`
451
+ query accounts {
452
+ uiapi {
453
+ query {
454
+ Account {
455
+ edges {
456
+ node {
457
+ Id
458
+ Name {
459
+ value
460
+ displayValue
461
+ }
462
+ }
463
+ }
464
+ }
465
+ }
466
+ }
467
+ }
468
+ `;
469
+
470
+ const ref2 = gql`
471
+ query accounts {
472
+ uiapi {
473
+ query {
474
+ Account {
475
+ edges {
476
+ node {
477
+ # This is a comment
478
+ Id
479
+
480
+ Name {
481
+ value
482
+ displayValue
483
+ }
484
+ }
485
+ }
486
+ }
487
+ }
488
+ }
489
+ }
490
+ `;
491
+ const doc1 = astResolver(ref1);
492
+ const doc2 = astResolver(ref2);
493
+
494
+ expect(mockParse).toHaveBeenCalledTimes(1);
495
+ expect(doc2.kind).toEqual('Document');
496
+ expect(doc1).toBe(doc2);
497
+ });
498
+
499
+ it('should return the document with appended substitutions', () => {
500
+ const field = `Contact`;
501
+ const fragment = gql(mockFragment1);
502
+ const ref = gql`
503
+ query accounts {
504
+ uiapi {
505
+ query {
506
+ Account {
507
+ edges {
508
+ node {
509
+ Id
510
+ Name {
511
+ value
512
+ }
513
+ ${field}
514
+ ...ReusableFragment1
515
+ }
516
+ }
517
+ }
518
+ }
519
+ }
520
+ }
521
+ ${fragment}
522
+ `;
523
+ const doc = astResolver(ref);
524
+ const fragmentDoc = astResolver(fragment);
525
+
526
+ expect(mockParse).toHaveBeenCalledTimes(2);
527
+ expect(fragmentDoc.kind).toBe('Document');
528
+ expect(fragmentDoc.definitions.length).toBe(1);
529
+ expect(doc.kind).toBe('Document');
530
+ expect(doc.definitions.length).toBe(2);
531
+ expect(doc.definitions[0].kind).toBe('OperationDefinition');
532
+ expect(doc.definitions[1].kind).toBe('FragmentDefinition');
533
+ });
534
+
535
+ describe('invalid queries', () => {
536
+ it('should return null in case of an invalid query - in prod', () => {
537
+ process.env.NODE_ENV = 'production';
538
+ try {
539
+ expect(gql(undefined)).toEqual(null);
540
+ expect(mockParse).not.toHaveBeenCalled();
541
+ } finally {
542
+ process.env.NODE_ENV = 'test';
543
+ }
544
+ });
545
+
546
+ it('should throw in case of an undefined query', () => {
547
+ // eslint-disable-next-line no-unused-expressions
548
+ expect(() => gql(undefined)).toThrowError('Invalid query');
549
+ expect(mockParse).not.toHaveBeenCalled();
550
+ });
551
+
552
+ it('should throw in case of an invalid query', () => {
553
+ // eslint-disable-next-line no-unused-expressions
554
+ expect(() => gql``).toThrowError('Invalid query');
555
+ expect(mockParse).not.toHaveBeenCalled();
556
+ });
557
+
558
+ it('should return null in case of an invalid query - prod', () => {
559
+ process.env.NODE_ENV = 'production';
560
+ try {
561
+ expect(gql``).toBe(null);
562
+ expect(mockParse).not.toHaveBeenCalled();
563
+ } finally {
564
+ process.env.NODE_ENV = 'test';
565
+ }
566
+ });
567
+
568
+ it('should throw in case a substitution reference is invalid', () => {
569
+ const sub = '';
570
+ expect(
571
+ () => gql`
572
+ query accounts {
573
+ uiapi {
574
+ query {
575
+ Account {
576
+ edges {
577
+ node {
578
+ Id
579
+ Name {
580
+ value
581
+ }
582
+ ${sub}
583
+ ...someFragment
584
+ }
585
+ }
586
+ }
587
+ }
588
+ }
589
+ }
590
+ ${{}}
591
+ `
592
+ ).toThrow(`Invalid substitution fragment`);
593
+ expect(mockParse).not.toHaveBeenCalled();
594
+ });
595
+
596
+ it('should return null when in production environment and passed an invalid reference', () => {
597
+ process.env.NODE_ENV = 'production';
598
+ try {
599
+ const ref = gql`
600
+ query accounts {
601
+ uiapi {
602
+ query {
603
+ Account {
604
+ edges {
605
+ node {
606
+ Id
607
+ Name {
608
+ value
609
+ }
610
+ ...someFragment
611
+ }
612
+ }
613
+ }
614
+ }
615
+ }
616
+ }
617
+ ${{}}
618
+ `;
619
+
620
+ expect(ref).toBe(null);
621
+ } finally {
622
+ process.env.NODE_ENV = 'test';
623
+ }
624
+ });
625
+
626
+ it('should return null when in production environment and parsed document is invalid', () => {
627
+ process.env.NODE_ENV = 'production';
628
+ try {
629
+ mockParse.mockImplementation(() => ({}));
630
+ const ref = gql`
631
+ query accounts {
632
+ uiapi {
633
+ query {
634
+ Account {
635
+ edges {
636
+ node {
637
+ Id
638
+ Name {
639
+ value
640
+ }
641
+ ...someFragment
642
+ }
643
+ }
644
+ }
645
+ }
646
+ }
647
+ }
648
+ `;
649
+ expect(ref).toBe(null);
650
+ } finally {
651
+ process.env.NODE_ENV = 'test';
652
+ }
653
+ });
654
+ });
655
+ });
656
+
657
+ describe('stripLocation', () => {
658
+ it('should remove all loc references from the doc', () => {
659
+ const doc = stripLocation({
660
+ loc: 1,
661
+ arr: [{ loc: 2, a: 3 }],
662
+ obj: { b: 4, loc: {} },
663
+ noLoc: 5,
664
+ });
665
+
666
+ expect(doc).toStrictEqual({ arr: [{ a: 3 }], obj: { b: 4 }, noLoc: 5 });
667
+ });
668
+
669
+ it('should remove all loc references from the doc and deeply nested objects', () => {
670
+ const doc = stripLocation(ast);
671
+
672
+ expect(doc).toStrictEqual(astNoLoc);
673
+ });
674
+ });
675
+
676
+ describe('addMetaschemaDirectives', () => {
677
+ it('flips', () => {
678
+ expect(addMetaschemaDirectives).toBe(false);
679
+ enableAddMetaschemaDirective();
680
+ expect(addMetaschemaDirectives).toBe(true);
681
+ disableAddMetaschemaDirective();
682
+ expect(addMetaschemaDirectives).toBe(false);
683
+ });
684
+ });