@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.
- package/.babelrc +6 -0
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.eslintignore +2 -0
- package/.eslintrc.js +10 -0
- package/.flowconfig +13 -0
- package/.github/actions/filter-files/action.yml +37 -0
- package/.github/actions/full-or-limited/action.yml +27 -0
- package/.github/actions/json-args/action.yml +32 -0
- package/.github/actions/setup/action.yml +28 -0
- package/.github/workflows/changeset-release.yml +80 -0
- package/.github/workflows/pr-checks.yml +64 -0
- package/.prettierrc +7 -0
- package/CHANGELOG.md +14 -0
- package/Readme.md +172 -0
- package/build-copy-source.js +28 -0
- package/dist/enums.js +57 -0
- package/dist/enums.js.flow +69 -0
- package/dist/generateResponseType.js +267 -0
- package/dist/generateResponseType.js.flow +419 -0
- package/dist/generateVariablesType.js +132 -0
- package/dist/generateVariablesType.js.flow +153 -0
- package/dist/index.js +88 -0
- package/dist/index.js.flow +93 -0
- package/dist/jest-mock-graphql-tag.js +169 -0
- package/dist/jest-mock-graphql-tag.js.flow +191 -0
- package/dist/schemaFromIntrospectionData.js +69 -0
- package/dist/schemaFromIntrospectionData.js.flow +68 -0
- package/dist/types.js +1 -0
- package/dist/types.js.flow +54 -0
- package/dist/utils.js +53 -0
- package/dist/utils.js.flow +50 -0
- package/flow-typed/npm/@babel/types_vx.x.x.js +5317 -0
- package/flow-typed/npm/jest_v23.x.x.js +1155 -0
- package/flow-typed/overrides.js +435 -0
- package/package.json +41 -0
- package/src/__test__/example-schema.graphql +65 -0
- package/src/__test__/graphql-flow.test.js +364 -0
- package/src/__test__/jest-mock-graphql-tag.test.js +51 -0
- package/src/enums.js +69 -0
- package/src/generateResponseType.js +419 -0
- package/src/generateVariablesType.js +153 -0
- package/src/index.js +93 -0
- package/src/jest-mock-graphql-tag.js +191 -0
- package/src/schemaFromIntrospectionData.js +68 -0
- package/src/types.js +54 -0
- package/src/utils.js +50 -0
- 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
|
+
};
|