@khanacademy/graphql-flow 0.0.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.
Files changed (48) hide show
  1. package/.babelrc +6 -0
  2. package/.changeset/README.md +8 -0
  3. package/.changeset/config.json +11 -0
  4. package/.eslintignore +2 -0
  5. package/.eslintrc.js +10 -0
  6. package/.flowconfig +13 -0
  7. package/.github/actions/filter-files/action.yml +37 -0
  8. package/.github/actions/full-or-limited/action.yml +27 -0
  9. package/.github/actions/json-args/action.yml +32 -0
  10. package/.github/actions/setup/action.yml +28 -0
  11. package/.github/workflows/changeset-release.yml +80 -0
  12. package/.github/workflows/pr-checks.yml +64 -0
  13. package/.prettierrc +7 -0
  14. package/CHANGELOG.md +14 -0
  15. package/Readme.md +172 -0
  16. package/build-copy-source.js +28 -0
  17. package/dist/enums.js +57 -0
  18. package/dist/enums.js.flow +69 -0
  19. package/dist/generateResponseType.js +267 -0
  20. package/dist/generateResponseType.js.flow +419 -0
  21. package/dist/generateVariablesType.js +132 -0
  22. package/dist/generateVariablesType.js.flow +153 -0
  23. package/dist/index.js +88 -0
  24. package/dist/index.js.flow +93 -0
  25. package/dist/jest-mock-graphql-tag.js +169 -0
  26. package/dist/jest-mock-graphql-tag.js.flow +191 -0
  27. package/dist/schemaFromIntrospectionData.js +69 -0
  28. package/dist/schemaFromIntrospectionData.js.flow +68 -0
  29. package/dist/types.js +1 -0
  30. package/dist/types.js.flow +54 -0
  31. package/dist/utils.js +53 -0
  32. package/dist/utils.js.flow +50 -0
  33. package/flow-typed/npm/@babel/types_vx.x.x.js +5317 -0
  34. package/flow-typed/npm/jest_v23.x.x.js +1155 -0
  35. package/flow-typed/overrides.js +435 -0
  36. package/package.json +41 -0
  37. package/src/__test__/example-schema.graphql +65 -0
  38. package/src/__test__/graphql-flow.test.js +364 -0
  39. package/src/__test__/jest-mock-graphql-tag.test.js +51 -0
  40. package/src/enums.js +69 -0
  41. package/src/generateResponseType.js +419 -0
  42. package/src/generateVariablesType.js +153 -0
  43. package/src/index.js +93 -0
  44. package/src/jest-mock-graphql-tag.js +191 -0
  45. package/src/schemaFromIntrospectionData.js +68 -0
  46. package/src/types.js +54 -0
  47. package/src/utils.js +50 -0
  48. package/tools/find-files-with-gql.js +40 -0
@@ -0,0 +1,364 @@
1
+ // @flow
2
+
3
+ /**
4
+ * Tests for our graphql flow generation!
5
+ */
6
+
7
+ import type {IntrospectionQuery, DocumentNode} from 'graphql';
8
+ import type {Schema, Options} from '../types';
9
+ import {buildSchema, getIntrospectionQuery, graphqlSync} from 'graphql';
10
+ import fs from 'fs';
11
+ const {documentToFlowTypes} = require('..');
12
+ const {schemaFromIntrospectionData} = require('../schemaFromIntrospectionData');
13
+
14
+ // This allows us to "snapshot" a string cleanly.
15
+ /* eslint-disable flowtype-errors/uncovered */
16
+ expect.addSnapshotSerializer({
17
+ test: (value) => value && typeof value === 'string',
18
+ print: (value, _, __) => value,
19
+ });
20
+ /* eslint-enable flowtype-errors/uncovered */
21
+
22
+ const generateTestSchema = (): Schema => {
23
+ const raw = fs.readFileSync(__dirname + '/example-schema.graphql', 'utf8');
24
+ const queryResponse = graphqlSync(
25
+ buildSchema(raw),
26
+ getIntrospectionQuery({descriptions: true}),
27
+ );
28
+ if (!queryResponse.data) {
29
+ throw new Error(
30
+ 'Failed to parse example schema: ' + JSON.stringify(queryResponse),
31
+ );
32
+ }
33
+ return schemaFromIntrospectionData(
34
+ // eslint-disable-next-line flowtype-errors/uncovered
35
+ ((queryResponse.data: any): IntrospectionQuery),
36
+ );
37
+ };
38
+
39
+ const exampleSchema = generateTestSchema();
40
+
41
+ const rawQueryToFlowTypes = (query: string, options?: Options): string => {
42
+ // We need the "requireActual" because we mock graphql-tag in jest-setup.js
43
+ // eslint-disable-next-line flowtype-errors/uncovered
44
+ const gql: (string) => DocumentNode = jest.requireActual('graphql-tag');
45
+ const node = gql(query);
46
+ return documentToFlowTypes(node, exampleSchema, {
47
+ scalars: {PositiveNumber: 'number'},
48
+ ...options,
49
+ })
50
+ .map(({code}) => code)
51
+ .join('\n\n');
52
+ };
53
+
54
+ describe('graphql-flow generation', () => {
55
+ it('should work with a basic query', () => {
56
+ const result = rawQueryToFlowTypes(`
57
+ query SomeQuery {
58
+ human(id: "Han Solo") {
59
+ id
60
+ name
61
+ homePlanet
62
+ friends {
63
+ name
64
+ }
65
+ }
66
+ }
67
+ `);
68
+
69
+ expect(result).toMatchInlineSnapshot(`
70
+ export type SomeQueryType = {|
71
+ variables: {||},
72
+ response: {|
73
+
74
+ /** A human character*/
75
+ human: ?{|
76
+ id: string,
77
+
78
+ /** The person's name*/
79
+ name: ?string,
80
+ homePlanet: ?string,
81
+ friends: ?$ReadOnlyArray<?{|
82
+ name: ?string
83
+ |}>,
84
+ |}
85
+ |}
86
+ |};
87
+ `);
88
+ });
89
+
90
+ it('renames', () => {
91
+ const result = rawQueryToFlowTypes(`
92
+ query SomeQuery {
93
+ human(id: "Han Solo") {
94
+ notDead: alive
95
+ }
96
+ }
97
+ `);
98
+
99
+ expect(result).toMatchInlineSnapshot(`
100
+ export type SomeQueryType = {|
101
+ variables: {||},
102
+ response: {|
103
+
104
+ /** A human character*/
105
+ human: ?{|
106
+ notDead: ?boolean
107
+ |}
108
+ |}
109
+ |};
110
+ `);
111
+ });
112
+
113
+ it('should work with unions', () => {
114
+ const result = rawQueryToFlowTypes(`
115
+ query SomeQuery {
116
+ friend(id: "Han Solo") {
117
+ __typename
118
+ ... on Human {
119
+ id
120
+ hands
121
+ }
122
+ ... on Droid {
123
+ primaryFunction
124
+ }
125
+ }
126
+ }
127
+ `);
128
+ expect(result).toMatchInlineSnapshot(`
129
+ export type SomeQueryType = {|
130
+ variables: {||},
131
+ response: {|
132
+ friend: ?({|
133
+ __typename: "Human",
134
+ id: string,
135
+ hands: ?number,
136
+ |} | {|
137
+ __typename: "Droid",
138
+
139
+ /** The robot's primary function*/
140
+ primaryFunction: ?string,
141
+ |} | {|
142
+ __typename: "Animal"
143
+ |})
144
+ |}
145
+ |};
146
+ `);
147
+ });
148
+
149
+ it('should work with fragments on interface', () => {
150
+ const result = rawQueryToFlowTypes(`
151
+ query SomeQuery {
152
+ human(id: "Han Solo") {
153
+ id
154
+ name
155
+ homePlanet
156
+ hands
157
+ alive
158
+ friends {
159
+ ...Profile
160
+ ... on Human {
161
+ hands
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ fragment Profile on Character {
168
+ __typename
169
+ id
170
+ name
171
+ friends {
172
+ id
173
+ }
174
+ appearsIn
175
+ }
176
+ `);
177
+
178
+ expect(result).toMatchInlineSnapshot(`
179
+ export type SomeQueryType = {|
180
+ variables: {||},
181
+ response: {|
182
+
183
+ /** A human character*/
184
+ human: ?{|
185
+ id: string,
186
+
187
+ /** The person's name*/
188
+ name: ?string,
189
+ homePlanet: ?string,
190
+ hands: ?number,
191
+ alive: ?boolean,
192
+ friends: ?$ReadOnlyArray<?({|
193
+ __typename: "Human",
194
+ id: string,
195
+ name: ?string,
196
+ friends: ?$ReadOnlyArray<?{|
197
+ id: string
198
+ |}>,
199
+ appearsIn: ?$ReadOnlyArray<
200
+ /** - NEW_HOPE
201
+ - EMPIRE
202
+ - JEDI*/
203
+ ?("NEW_HOPE" | "EMPIRE" | "JEDI")>,
204
+ hands: ?number,
205
+ |} | {|
206
+ __typename: "Droid",
207
+ id: string,
208
+ name: ?string,
209
+ friends: ?$ReadOnlyArray<?{|
210
+ id: string
211
+ |}>,
212
+ appearsIn: ?$ReadOnlyArray<
213
+ /** - NEW_HOPE
214
+ - EMPIRE
215
+ - JEDI*/
216
+ ?("NEW_HOPE" | "EMPIRE" | "JEDI")>,
217
+ |})>,
218
+ |}
219
+ |}
220
+ |};
221
+ `);
222
+ });
223
+
224
+ it('should work with a readOnlyArray turned off', () => {
225
+ const result = rawQueryToFlowTypes(
226
+ `
227
+ query SomeQuery {
228
+ human(id: "Han Solo") {
229
+ friends {
230
+ name
231
+ }
232
+ }
233
+ }
234
+ `,
235
+ {readOnlyArray: false},
236
+ );
237
+
238
+ expect(result).toMatchInlineSnapshot(`
239
+ export type SomeQueryType = {|
240
+ variables: {||},
241
+ response: {|
242
+
243
+ /** A human character*/
244
+ human: ?{|
245
+ friends: ?Array<?{|
246
+ name: ?string
247
+ |}>
248
+ |}
249
+ |}
250
+ |};
251
+ `);
252
+ });
253
+
254
+ describe('Object properties', () => {
255
+ it('should reject invalid field', () => {
256
+ expect(() =>
257
+ rawQueryToFlowTypes(`
258
+ query SomeQuery {
259
+ human(id: "Me") {
260
+ invalidField
261
+ }
262
+ }
263
+ `),
264
+ ).toThrowErrorMatchingInlineSnapshot(
265
+ `Graphql-flow type generation failed! Unknown field 'invalidField' for type 'Human'`,
266
+ );
267
+ });
268
+
269
+ it('should reject an unknown fragment', () => {
270
+ expect(() =>
271
+ rawQueryToFlowTypes(`
272
+ query SomeQuery {
273
+ human(id: "Me") {
274
+ ...UnknownFragment
275
+ }
276
+ }
277
+ `),
278
+ ).toThrowErrorMatchingInlineSnapshot(
279
+ `Graphql-flow type generation failed! No fragment named 'UnknownFragment'. Did you forget to include it in the template literal?`,
280
+ );
281
+ });
282
+ });
283
+
284
+ describe('Input variables', () => {
285
+ it('should generate a variables type', () => {
286
+ const result = rawQueryToFlowTypes(
287
+ `query SomeQuery($id: String!, $episode: Episode) {
288
+ human(id: $id) {
289
+ friends {
290
+ name
291
+ }
292
+ }
293
+ hero(episode: $episode) {
294
+ name
295
+ }
296
+ }`,
297
+ {readOnlyArray: false},
298
+ );
299
+
300
+ expect(result).toMatchInlineSnapshot(`
301
+ export type SomeQueryType = {|
302
+ variables: {|
303
+ id: string,
304
+
305
+ /** - NEW_HOPE
306
+ - EMPIRE
307
+ - JEDI*/
308
+ episode?: ?("NEW_HOPE" | "EMPIRE" | "JEDI"),
309
+ |},
310
+ response: {|
311
+
312
+ /** A human character*/
313
+ human: ?{|
314
+ friends: ?Array<?{|
315
+ name: ?string
316
+ |}>
317
+ |},
318
+ hero: ?{|
319
+ name: ?string
320
+ |},
321
+ |}
322
+ |};
323
+ `);
324
+ });
325
+
326
+ it('should handle a complex input variable', () => {
327
+ const result = rawQueryToFlowTypes(
328
+ `mutation addCharacter($character: CharacterInput!) {
329
+ addCharacter(character: $character) {
330
+ id
331
+ }
332
+ }`,
333
+ {readOnlyArray: false},
334
+ );
335
+
336
+ expect(result).toMatchInlineSnapshot(`
337
+ export type addCharacterType = {|
338
+ variables: {|
339
+
340
+ /** A character to add*/
341
+ character: {|
342
+
343
+ /** The new character's name*/
344
+ name: string,
345
+
346
+ /** The character's friends*/
347
+ friends?: ?$ReadOnlyArray<string>,
348
+ appearsIn?: ?$ReadOnlyArray<
349
+ /** - NEW_HOPE
350
+ - EMPIRE
351
+ - JEDI*/
352
+ "NEW_HOPE" | "EMPIRE" | "JEDI">,
353
+ |}
354
+ |},
355
+ response: {|
356
+ addCharacter: ?{|
357
+ id: string
358
+ |}
359
+ |}
360
+ |};
361
+ `);
362
+ });
363
+ });
364
+ });
@@ -0,0 +1,51 @@
1
+ // @flow
2
+ import {processPragmas} from '../jest-mock-graphql-tag';
3
+
4
+ const pragma = '# @autogen\n';
5
+ const loosePragma = '# @autogen-loose\n';
6
+
7
+ describe('processPragmas', () => {
8
+ it('should work with no pragmas', () => {
9
+ expect(processPragmas({}, `query X { Y }`)).toEqual({
10
+ strictNullability: undefined,
11
+ readOnlyArray: undefined,
12
+ scalars: undefined,
13
+ });
14
+ });
15
+
16
+ it('should reject query without required pragma', () => {
17
+ expect(processPragmas({pragma}, `query X { Y }`)).toEqual(null);
18
+ });
19
+
20
+ it('should accept query with required pragma', () => {
21
+ expect(
22
+ processPragmas(
23
+ {pragma},
24
+ `query X {
25
+ # @autogen
26
+ Y
27
+ }`,
28
+ ),
29
+ ).toEqual({
30
+ strictNullability: true,
31
+ readOnlyArray: undefined,
32
+ scalars: undefined,
33
+ });
34
+ });
35
+
36
+ it('should accept query with loose pragma', () => {
37
+ expect(
38
+ processPragmas(
39
+ {pragma, loosePragma},
40
+ `query X {
41
+ # @autogen-loose
42
+ Y
43
+ }`,
44
+ ),
45
+ ).toEqual({
46
+ strictNullability: false,
47
+ readOnlyArray: undefined,
48
+ scalars: undefined,
49
+ });
50
+ });
51
+ });
package/src/enums.js ADDED
@@ -0,0 +1,69 @@
1
+ // @flow
2
+ /**
3
+ * Both input & output types can have enums & scalars.
4
+ */
5
+ import * as babelTypes from '@babel/types';
6
+ import {type BabelNodeFlowType} from '@babel/types';
7
+ import type {Config} from './types';
8
+ import {maybeAddDescriptionComment} from './utils';
9
+
10
+ export const enumTypeToFlow = (
11
+ config: Config,
12
+ name: string,
13
+ ): BabelNodeFlowType => {
14
+ const enumConfig = config.schema.enumsByName[name];
15
+ let combinedDescription = enumConfig.enumValues
16
+ .map(
17
+ (n) =>
18
+ `- ${n.name}` +
19
+ (n.description
20
+ ? '\n\n ' + n.description.replace(/\n/g, '\n ')
21
+ : ''),
22
+ )
23
+ .join('\n');
24
+ if (enumConfig.description) {
25
+ combinedDescription =
26
+ enumConfig.description + '\n\n' + combinedDescription;
27
+ }
28
+ return maybeAddDescriptionComment(
29
+ combinedDescription,
30
+ babelTypes.unionTypeAnnotation(
31
+ enumConfig.enumValues.map((n) =>
32
+ babelTypes.stringLiteralTypeAnnotation(n.name),
33
+ ),
34
+ ),
35
+ );
36
+ };
37
+
38
+ export const builtinScalars: {[key: string]: string} = {
39
+ Boolean: 'boolean',
40
+ String: 'string',
41
+ DateTime: 'string',
42
+ Date: 'string',
43
+ ID: 'string',
44
+ Int: 'number',
45
+ Float: 'number',
46
+ };
47
+
48
+ export const scalarTypeToFlow = (
49
+ config: Config,
50
+ name: string,
51
+ ): BabelNodeFlowType => {
52
+ if (builtinScalars[name]) {
53
+ return babelTypes.genericTypeAnnotation(
54
+ babelTypes.identifier(builtinScalars[name]),
55
+ );
56
+ }
57
+ const underlyingType = config.scalars[name];
58
+ if (underlyingType != null) {
59
+ return babelTypes.genericTypeAnnotation(
60
+ babelTypes.identifier(underlyingType),
61
+ );
62
+ }
63
+ config.errors.push(
64
+ `Unexpected scalar '${name}'! Please add it to the "scalars" argument at the callsite of 'generateFlowTypes()'.`,
65
+ );
66
+ return babelTypes.genericTypeAnnotation(
67
+ babelTypes.identifier(`UNKNOWN_SCALAR["${name}"]`),
68
+ );
69
+ };