@khanacademy/graphql-flow 0.3.0 → 1.0.0

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.
Files changed (55) hide show
  1. package/.github/workflows/pr-checks.yml +4 -6
  2. package/CHANGELOG.md +22 -0
  3. package/Readme.md +41 -167
  4. package/dist/__test__/example-schema.graphql +67 -0
  5. package/dist/__test__/generateTypeFileContents.test.js +157 -0
  6. package/dist/__test__/graphql-flow.test.js +639 -0
  7. package/dist/__test__/processPragmas.test.js +76 -0
  8. package/dist/cli/__test__/config.test.js +120 -0
  9. package/dist/cli/config.js +45 -106
  10. package/dist/cli/config.js.flow +37 -159
  11. package/dist/cli/config.js.map +1 -1
  12. package/dist/cli/run.js +40 -36
  13. package/dist/cli/run.js.flow +56 -42
  14. package/dist/cli/run.js.map +1 -1
  15. package/dist/cli/schema.json +91 -0
  16. package/dist/enums.js +9 -9
  17. package/dist/enums.js.flow +11 -11
  18. package/dist/enums.js.map +1 -1
  19. package/dist/generateResponseType.js +47 -47
  20. package/dist/generateResponseType.js.flow +55 -57
  21. package/dist/generateResponseType.js.map +1 -1
  22. package/dist/generateTypeFiles.js +13 -17
  23. package/dist/generateTypeFiles.js.flow +21 -43
  24. package/dist/generateTypeFiles.js.map +1 -1
  25. package/dist/generateVariablesType.js +24 -24
  26. package/dist/generateVariablesType.js.flow +25 -28
  27. package/dist/generateVariablesType.js.map +1 -1
  28. package/dist/index.js +0 -8
  29. package/dist/index.js.flow +4 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/parser/__test__/parse.test.js +247 -0
  32. package/dist/types.js.flow +26 -6
  33. package/package.json +3 -2
  34. package/src/__test__/generateTypeFileContents.test.js +3 -1
  35. package/src/__test__/graphql-flow.test.js +7 -7
  36. package/src/__test__/processPragmas.test.js +28 -15
  37. package/src/cli/__test__/config.test.js +110 -84
  38. package/src/cli/config.js +37 -159
  39. package/src/cli/run.js +56 -42
  40. package/src/cli/schema.json +91 -0
  41. package/src/enums.js +11 -11
  42. package/src/generateResponseType.js +55 -57
  43. package/src/generateTypeFiles.js +21 -43
  44. package/src/generateVariablesType.js +25 -28
  45. package/src/index.js +4 -5
  46. package/src/types.js +26 -6
  47. package/dist/cli/utils.js +0 -21
  48. package/dist/cli/utils.js.flow +0 -14
  49. package/dist/cli/utils.js.map +0 -1
  50. package/dist/jest-mock-graphql-tag.js +0 -88
  51. package/dist/jest-mock-graphql-tag.js.flow +0 -96
  52. package/dist/jest-mock-graphql-tag.js.map +0 -1
  53. package/src/cli/__test__/utils.test.js +0 -19
  54. package/src/cli/utils.js +0 -14
  55. package/src/jest-mock-graphql-tag.js +0 -96
@@ -0,0 +1,639 @@
1
+ // @flow
2
+
3
+ /**
4
+ * Tests for our graphql flow generation!
5
+ */
6
+
7
+ import {getSchemas} from '../cli/config';
8
+ import {documentToFlowTypes} from '..';
9
+ import gql from 'graphql-tag';
10
+
11
+ import type {GenerateConfig} from '../types';
12
+
13
+ // This allows us to "snapshot" a string cleanly.
14
+ /* eslint-disable flowtype-errors/uncovered */
15
+ expect.addSnapshotSerializer({
16
+ test: (value) => value && typeof value === 'string',
17
+ print: (value, _, __) => value,
18
+ });
19
+ /* eslint-enable flowtype-errors/uncovered */
20
+
21
+ const [_, exampleSchema] = getSchemas(__dirname + '/example-schema.graphql');
22
+
23
+ const rawQueryToFlowTypes = (
24
+ query: string,
25
+ options?: $Partial<GenerateConfig>,
26
+ ): string => {
27
+ const node = gql(query);
28
+ return documentToFlowTypes(node, exampleSchema, {
29
+ schemaFilePath: '',
30
+ scalars: {PositiveNumber: 'number'},
31
+ ...options,
32
+ })
33
+ .map(
34
+ ({typeName, code, extraTypes}) =>
35
+ `// ${typeName}.js\n${code}` +
36
+ Object.keys(extraTypes)
37
+ .sort()
38
+ .map((k) => `\nexport type ${k} = ${extraTypes[k]};`)
39
+ .join(''),
40
+ )
41
+ .join('\n\n');
42
+ };
43
+
44
+ describe('graphql-flow generation', () => {
45
+ it('should allow custom scalars as input', () => {
46
+ const result = rawQueryToFlowTypes(`
47
+ query SomeQuery($candies: PositiveNumber!) {
48
+ candies(number: $candies)
49
+ }
50
+ `);
51
+
52
+ expect(result).toMatchInlineSnapshot(`
53
+ // SomeQueryType.js
54
+ export type SomeQueryType = {|
55
+ variables: {|
56
+ candies: number
57
+ |},
58
+ response: {|
59
+ candies: ?string
60
+ |}
61
+ |};
62
+ `);
63
+ });
64
+
65
+ it('should split types', () => {
66
+ const result = rawQueryToFlowTypes(
67
+ `
68
+ query SomeQuery($id: String!) {
69
+ human(id: $id) { id }
70
+ }
71
+ `,
72
+ {splitTypes: true},
73
+ );
74
+
75
+ expect(result).toMatchInlineSnapshot(`
76
+ // SomeQueryType.js
77
+ export type SomeQueryType = {|
78
+ variables: {|
79
+ id: string
80
+ |},
81
+ response: {|
82
+
83
+ /** A human character*/
84
+ human: ?{|
85
+ id: string
86
+ |}
87
+ |}
88
+ |};
89
+ `);
90
+ });
91
+
92
+ it('should work with a basic query', () => {
93
+ const result = rawQueryToFlowTypes(`
94
+ query SomeQuery {
95
+ human(id: "Han Solo") {
96
+ id
97
+ name
98
+ homePlanet
99
+ friends {
100
+ name
101
+ }
102
+ }
103
+ }
104
+ `);
105
+
106
+ expect(result).toMatchInlineSnapshot(`
107
+ // SomeQueryType.js
108
+ export type SomeQueryType = {|
109
+ variables: {||},
110
+ response: {|
111
+
112
+ /** A human character*/
113
+ human: ?{|
114
+ friends: ?$ReadOnlyArray<?{|
115
+ name: ?string
116
+ |}>,
117
+ homePlanet: ?string,
118
+ id: string,
119
+
120
+ /** The person's name*/
121
+ name: ?string,
122
+ |}
123
+ |}
124
+ |};
125
+ `);
126
+ });
127
+
128
+ it('renames', () => {
129
+ const result = rawQueryToFlowTypes(`
130
+ query SomeQuery {
131
+ human(id: "Han Solo") {
132
+ notDead: alive
133
+ }
134
+ }
135
+ `);
136
+
137
+ expect(result).toMatchInlineSnapshot(`
138
+ // SomeQueryType.js
139
+ export type SomeQueryType = {|
140
+ variables: {||},
141
+ response: {|
142
+
143
+ /** A human character*/
144
+ human: ?{|
145
+ notDead: ?boolean
146
+ |}
147
+ |}
148
+ |};
149
+ `);
150
+ });
151
+
152
+ it('should work with unions', () => {
153
+ const result = rawQueryToFlowTypes(`
154
+ query SomeQuery {
155
+ friend(id: "Han Solo") {
156
+ __typename
157
+ ... on Human {
158
+ id
159
+ hands
160
+ }
161
+ ... on Droid {
162
+ primaryFunction
163
+ }
164
+ }
165
+ }
166
+ `);
167
+ expect(result).toMatchInlineSnapshot(`
168
+ // SomeQueryType.js
169
+ export type SomeQueryType = {|
170
+ variables: {||},
171
+ response: {|
172
+ friend: ?({|
173
+ __typename: "Animal"
174
+ |} | {|
175
+ __typename: "Droid",
176
+
177
+ /** The robot's primary function*/
178
+ primaryFunction: string,
179
+ |} | {|
180
+ __typename: "Human",
181
+ hands: ?number,
182
+ id: string,
183
+ |})
184
+ |}
185
+ |};
186
+ `);
187
+ });
188
+
189
+ it('should work with fragments on interface', () => {
190
+ const result = rawQueryToFlowTypes(`
191
+ query SomeQuery {
192
+ human(id: "Han Solo") {
193
+ id
194
+ name
195
+ homePlanet
196
+ hands
197
+ alive
198
+ friends {
199
+ ...Profile
200
+ ... on Human {
201
+ hands
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ fragment Profile on Character {
208
+ __typename
209
+ id
210
+ name
211
+ friends {
212
+ id
213
+ }
214
+ appearsIn
215
+ }
216
+ `);
217
+
218
+ expect(result).toMatchInlineSnapshot(`
219
+ // SomeQueryType.js
220
+ export type SomeQueryType = {|
221
+ variables: {||},
222
+ response: {|
223
+
224
+ /** A human character*/
225
+ human: ?{|
226
+ alive: ?boolean,
227
+ friends: ?$ReadOnlyArray<?({|
228
+ __typename: "Droid",
229
+ appearsIn: ?$ReadOnlyArray<
230
+ /** - NEW_HOPE
231
+ - EMPIRE
232
+ - JEDI*/
233
+ ?("NEW_HOPE" | "EMPIRE" | "JEDI")>,
234
+ friends: ?$ReadOnlyArray<?{|
235
+ id: string
236
+ |}>,
237
+ id: string,
238
+ name: ?string,
239
+ |} | {|
240
+ __typename: "Human",
241
+ appearsIn: ?$ReadOnlyArray<
242
+ /** - NEW_HOPE
243
+ - EMPIRE
244
+ - JEDI*/
245
+ ?("NEW_HOPE" | "EMPIRE" | "JEDI")>,
246
+ friends: ?$ReadOnlyArray<?{|
247
+ id: string
248
+ |}>,
249
+ hands: ?number,
250
+ id: string,
251
+ name: ?string,
252
+ |})>,
253
+ hands: ?number,
254
+ homePlanet: ?string,
255
+ id: string,
256
+
257
+ /** The person's name*/
258
+ name: ?string,
259
+ |}
260
+ |}
261
+ |};
262
+
263
+ // Profile.js
264
+ export type Profile = {|
265
+ __typename: "Droid" | "Human",
266
+ appearsIn: ?$ReadOnlyArray<
267
+ /** - NEW_HOPE
268
+ - EMPIRE
269
+ - JEDI*/
270
+ ?("NEW_HOPE" | "EMPIRE" | "JEDI")>,
271
+ friends: ?$ReadOnlyArray<?{|
272
+ id: string
273
+ |}>,
274
+ id: string,
275
+ name: ?string,
276
+ |};
277
+ `);
278
+ });
279
+
280
+ it('should work with a readOnlyArray turned off', () => {
281
+ const result = rawQueryToFlowTypes(
282
+ `
283
+ query SomeQuery {
284
+ human(id: "Han Solo") {
285
+ friends {
286
+ name
287
+ }
288
+ }
289
+ }
290
+ `,
291
+ {readOnlyArray: false},
292
+ );
293
+
294
+ expect(result).toMatchInlineSnapshot(`
295
+ // SomeQueryType.js
296
+ export type SomeQueryType = {|
297
+ variables: {||},
298
+ response: {|
299
+
300
+ /** A human character*/
301
+ human: ?{|
302
+ friends: ?Array<?{|
303
+ name: ?string
304
+ |}>
305
+ |}
306
+ |}
307
+ |};
308
+ `);
309
+ });
310
+
311
+ describe('Object properties', () => {
312
+ it('should reject invalid field', () => {
313
+ expect(() =>
314
+ rawQueryToFlowTypes(`
315
+ query SomeQuery {
316
+ human(id: "Me") {
317
+ invalidField
318
+ }
319
+ }
320
+ `),
321
+ ).toThrowErrorMatchingInlineSnapshot(
322
+ `Graphql-flow type generation failed! Unknown field 'invalidField' for type 'Human'`,
323
+ );
324
+ });
325
+
326
+ it('should reject an unknown fragment', () => {
327
+ expect(() =>
328
+ rawQueryToFlowTypes(`
329
+ query SomeQuery {
330
+ human(id: "Me") {
331
+ ...UnknownFragment
332
+ }
333
+ }
334
+ `),
335
+ ).toThrowErrorMatchingInlineSnapshot(
336
+ `Graphql-flow type generation failed! No fragment named 'UnknownFragment'. Did you forget to include it in the template literal?`,
337
+ );
338
+ });
339
+ });
340
+
341
+ describe('Fragments', () => {
342
+ it('should resolve correctly, and produce a type file for the fragment', () => {
343
+ const result = rawQueryToFlowTypes(
344
+ `query Hello {
345
+ hero(episode: JEDI) {
346
+ ...onChar
347
+ }
348
+ }
349
+
350
+ fragment onChar on Character {
351
+ __typename
352
+ ... on Droid {
353
+ primaryFunction
354
+ }
355
+ }`,
356
+ );
357
+ expect(result).toMatchInlineSnapshot(`
358
+ // HelloType.js
359
+ export type HelloType = {|
360
+ variables: {||},
361
+ response: {|
362
+ hero: ?({|
363
+ __typename: "Droid",
364
+
365
+ /** The robot's primary function*/
366
+ primaryFunction: string,
367
+ |} | {|
368
+ __typename: "Human"
369
+ |})
370
+ |}
371
+ |};
372
+
373
+ // onChar.js
374
+ export type onChar = {|
375
+ __typename: "Droid",
376
+
377
+ /** The robot's primary function*/
378
+ primaryFunction: string,
379
+ |} | {|
380
+ __typename: "Human"
381
+ |};
382
+ `);
383
+ });
384
+
385
+ it('Should specialize the fragment type correctly', () => {
386
+ const result = rawQueryToFlowTypes(
387
+ `query Deps {
388
+ droid(id: "hello") {
389
+ ...Hello
390
+ }
391
+ }
392
+
393
+ fragment Hello on Character {
394
+ __typename
395
+ name
396
+ ... on Droid {
397
+ primaryFunction
398
+ }
399
+ ... on Human {
400
+ homePlanet
401
+ }
402
+ }`,
403
+ );
404
+
405
+ // Note how `homePlanet` is ommitted in
406
+ // `DepsType.response.droid`
407
+ expect(result).toMatchInlineSnapshot(`
408
+ // DepsType.js
409
+ export type DepsType = {|
410
+ variables: {||},
411
+ response: {|
412
+
413
+ /** A robot character*/
414
+ droid: ?{|
415
+ __typename: "Droid",
416
+ name: ?string,
417
+
418
+ /** The robot's primary function*/
419
+ primaryFunction: string,
420
+ |}
421
+ |}
422
+ |};
423
+
424
+ // Hello.js
425
+ export type Hello = {|
426
+ __typename: "Droid",
427
+ name: ?string,
428
+
429
+ /** The robot's primary function*/
430
+ primaryFunction: string,
431
+ |} | {|
432
+ __typename: "Human",
433
+ homePlanet: ?string,
434
+ name: ?string,
435
+ |};
436
+ `);
437
+ });
438
+ });
439
+
440
+ it('should generate all types when exportAllObjectTypes is set', () => {
441
+ const result = rawQueryToFlowTypes(
442
+ `
443
+ query SomeQuery {
444
+ human(id: "Han Solo") {
445
+ id
446
+ name
447
+ homePlanet
448
+ hands
449
+ alive
450
+ friends {
451
+ __typename
452
+ ... on Human {
453
+ hands
454
+ }
455
+ }
456
+ }
457
+ }
458
+ `,
459
+ {exportAllObjectTypes: true},
460
+ );
461
+
462
+ expect(result).toMatchInlineSnapshot(`
463
+ // SomeQueryType.js
464
+ export type SomeQueryType = {|
465
+ variables: {||},
466
+ response: {|
467
+
468
+ /** A human character*/
469
+ human: ?SomeQuery_human
470
+ |}
471
+ |};
472
+ export type SomeQuery_human = {|
473
+ alive: ?boolean,
474
+ friends: ?$ReadOnlyArray<?SomeQuery_human_friends>,
475
+ hands: ?number,
476
+ homePlanet: ?string,
477
+ id: string,
478
+
479
+ /** The person's name*/
480
+ name: ?string,
481
+ |};
482
+ export type SomeQuery_human_friends = SomeQuery_human_friends_Droid | SomeQuery_human_friends_Human;
483
+ export type SomeQuery_human_friends_Droid = {|
484
+ __typename: "Droid"
485
+ |};
486
+ export type SomeQuery_human_friends_Human = {|
487
+ __typename: "Human",
488
+ hands: ?number,
489
+ |};
490
+ `);
491
+ });
492
+
493
+ describe('Input variables', () => {
494
+ it('should generate a variables type', () => {
495
+ const result = rawQueryToFlowTypes(
496
+ `query SomeQuery($id: String!, $episode: Episode) {
497
+ human(id: $id) {
498
+ friends {
499
+ name
500
+ }
501
+ }
502
+ hero(episode: $episode) {
503
+ name
504
+ }
505
+ }`,
506
+ {readOnlyArray: false},
507
+ );
508
+
509
+ expect(result).toMatchInlineSnapshot(`
510
+ // SomeQueryType.js
511
+ export type SomeQueryType = {|
512
+ variables: {|
513
+ id: string,
514
+
515
+ /** - NEW_HOPE
516
+ - EMPIRE
517
+ - JEDI*/
518
+ episode?: ?("NEW_HOPE" | "EMPIRE" | "JEDI"),
519
+ |},
520
+ response: {|
521
+ hero: ?{|
522
+ name: ?string
523
+ |},
524
+
525
+ /** A human character*/
526
+ human: ?{|
527
+ friends: ?Array<?{|
528
+ name: ?string
529
+ |}>
530
+ |},
531
+ |}
532
+ |};
533
+ `);
534
+ });
535
+
536
+ it('should handle an inline fragment on an interface without a typeCondition', () => {
537
+ const result = rawQueryToFlowTypes(
538
+ `
539
+ query SomeQuery {
540
+ hero(episode: JEDI) {
541
+ id
542
+ ... {
543
+ name
544
+ }
545
+ }
546
+ }`,
547
+ {readOnlyArray: false},
548
+ );
549
+ expect(result).toMatchInlineSnapshot(`
550
+ // SomeQueryType.js
551
+ export type SomeQueryType = {|
552
+ variables: {||},
553
+ response: {|
554
+ hero: ?({|
555
+ id: string,
556
+ name: ?string,
557
+ |} | {|
558
+ id: string,
559
+
560
+ /** The person's name*/
561
+ name: ?string,
562
+ |})
563
+ |}
564
+ |};
565
+ `);
566
+ });
567
+
568
+ it('should handle an inline fragment on an object (not an interface)', () => {
569
+ const result = rawQueryToFlowTypes(
570
+ `
571
+ query SomeQuery {
572
+ human(id: "hi") {
573
+ id
574
+ ... {
575
+ name
576
+ }
577
+ }
578
+ }`,
579
+ {readOnlyArray: false},
580
+ );
581
+ expect(result).toMatchInlineSnapshot(`
582
+ // SomeQueryType.js
583
+ export type SomeQueryType = {|
584
+ variables: {||},
585
+ response: {|
586
+
587
+ /** A human character*/
588
+ human: ?{|
589
+ id: string,
590
+
591
+ /** The person's name*/
592
+ name: ?string,
593
+ |}
594
+ |}
595
+ |};
596
+ `);
597
+ });
598
+
599
+ it('should handle a complex input variable', () => {
600
+ const result = rawQueryToFlowTypes(
601
+ `mutation addCharacter($character: CharacterInput!) {
602
+ addCharacter(character: $character) {
603
+ id
604
+ }
605
+ }`,
606
+ {readOnlyArray: false},
607
+ );
608
+
609
+ expect(result).toMatchInlineSnapshot(`
610
+ // addCharacterType.js
611
+ export type addCharacterType = {|
612
+ variables: {|
613
+
614
+ /** A character to add*/
615
+ character: {|
616
+
617
+ /** The new character's name*/
618
+ name: string,
619
+
620
+ /** The character's friends*/
621
+ friends?: ?$ReadOnlyArray<string>,
622
+ appearsIn?: ?$ReadOnlyArray<
623
+ /** - NEW_HOPE
624
+ - EMPIRE
625
+ - JEDI*/
626
+ "NEW_HOPE" | "EMPIRE" | "JEDI">,
627
+ candies: number,
628
+ |}
629
+ |},
630
+ response: {|
631
+ addCharacter: ?{|
632
+ id: string
633
+ |}
634
+ |}
635
+ |};
636
+ `);
637
+ });
638
+ });
639
+ });
@@ -0,0 +1,76 @@
1
+ // @flow
2
+ import type {CrawlConfig, GenerateConfig} from '../types';
3
+
4
+ import {processPragmas} from '../generateTypeFiles';
5
+
6
+ const pragma = '# @autogen\n';
7
+ const loosePragma = '# @autogen-loose\n';
8
+
9
+ const baseGenerate: GenerateConfig = {schemaFilePath: ''};
10
+ const baseCrawl: CrawlConfig = {root: ''};
11
+
12
+ describe('processPragmas', () => {
13
+ it('should work with no pragmas', () => {
14
+ expect(
15
+ processPragmas(baseGenerate, baseCrawl, `query X { Y }`),
16
+ ).toEqual({
17
+ generate: true,
18
+ strict: undefined,
19
+ });
20
+ });
21
+
22
+ it('should reject query without required pragma', () => {
23
+ expect(
24
+ processPragmas(
25
+ baseGenerate,
26
+ {...baseCrawl, pragma},
27
+ `query X { Y }`,
28
+ ),
29
+ ).toEqual({generate: false});
30
+ });
31
+
32
+ it('should accept query with required pragma', () => {
33
+ expect(
34
+ processPragmas(
35
+ baseGenerate,
36
+ {...baseCrawl, pragma},
37
+ `query X {
38
+ # @autogen
39
+ Y
40
+ }`,
41
+ ),
42
+ ).toEqual({
43
+ strict: true,
44
+ generate: true,
45
+ });
46
+ });
47
+
48
+ it('should accept query with loose pragma', () => {
49
+ expect(
50
+ processPragmas(
51
+ baseGenerate,
52
+ {...baseCrawl, pragma, loosePragma},
53
+ `query X {
54
+ # @autogen-loose
55
+ Y
56
+ }`,
57
+ ),
58
+ ).toEqual({
59
+ strict: false,
60
+ generate: true,
61
+ });
62
+ });
63
+
64
+ it('should reject query with ignore pragma', () => {
65
+ expect(
66
+ processPragmas(
67
+ baseGenerate,
68
+ {...baseCrawl, ignorePragma: '# @ignore\n'},
69
+ `query X {
70
+ # @ignore
71
+ Y
72
+ }`,
73
+ ),
74
+ ).toEqual({generate: false});
75
+ });
76
+ });