@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,419 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import generate from '@babel/generator'; // eslint-disable-line flowtype-errors/uncovered
|
|
4
|
+
import * as babelTypes from '@babel/types';
|
|
5
|
+
import {type BabelNodeFlowType} from '@babel/types';
|
|
6
|
+
import type {
|
|
7
|
+
FieldNode,
|
|
8
|
+
IntrospectionOutputTypeRef,
|
|
9
|
+
OperationDefinitionNode,
|
|
10
|
+
} from 'graphql';
|
|
11
|
+
import type {Config, Schema, Selections} from './types';
|
|
12
|
+
import {
|
|
13
|
+
liftLeadingPropertyComments,
|
|
14
|
+
maybeAddDescriptionComment,
|
|
15
|
+
transferLeadingComments,
|
|
16
|
+
} from './utils';
|
|
17
|
+
import {enumTypeToFlow, scalarTypeToFlow} from './enums';
|
|
18
|
+
import type {
|
|
19
|
+
IntrospectionField,
|
|
20
|
+
IntrospectionInterfaceType,
|
|
21
|
+
IntrospectionObjectType,
|
|
22
|
+
IntrospectionUnionType,
|
|
23
|
+
} from 'graphql/utilities/introspectionQuery';
|
|
24
|
+
import type {
|
|
25
|
+
BabelNodeObjectTypeProperty,
|
|
26
|
+
BabelNodeObjectTypeSpreadProperty,
|
|
27
|
+
} from '@babel/types';
|
|
28
|
+
|
|
29
|
+
export const generateResponseType = (
|
|
30
|
+
schema: Schema,
|
|
31
|
+
query: OperationDefinitionNode,
|
|
32
|
+
config: Config,
|
|
33
|
+
): string => {
|
|
34
|
+
const ast = querySelectionToObjectType(
|
|
35
|
+
config,
|
|
36
|
+
query.selectionSet.selections,
|
|
37
|
+
query.operation === 'mutation'
|
|
38
|
+
? schema.typesByName.Mutation
|
|
39
|
+
: schema.typesByName.Query,
|
|
40
|
+
query.operation === 'mutation' ? 'mutation' : 'query',
|
|
41
|
+
);
|
|
42
|
+
// eslint-disable-next-line flowtype-errors/uncovered
|
|
43
|
+
return generate(ast).code;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const _typeToFlow = (
|
|
47
|
+
config: Config,
|
|
48
|
+
type,
|
|
49
|
+
selection,
|
|
50
|
+
): babelTypes.BabelNodeFlowType => {
|
|
51
|
+
if (type.kind === 'SCALAR') {
|
|
52
|
+
return scalarTypeToFlow(config, type.name);
|
|
53
|
+
}
|
|
54
|
+
if (type.kind === 'LIST') {
|
|
55
|
+
return babelTypes.genericTypeAnnotation(
|
|
56
|
+
config.readOnlyArray
|
|
57
|
+
? babelTypes.identifier('$ReadOnlyArray')
|
|
58
|
+
: babelTypes.identifier('Array'),
|
|
59
|
+
babelTypes.typeParameterInstantiation([
|
|
60
|
+
typeToFlow(config, type.ofType, selection),
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (type.kind === 'UNION') {
|
|
65
|
+
const union = config.schema.unionsByName[type.name];
|
|
66
|
+
if (!selection.selectionSet) {
|
|
67
|
+
console.log('no selection set', selection);
|
|
68
|
+
return babelTypes.anyTypeAnnotation();
|
|
69
|
+
}
|
|
70
|
+
return unionOrInterfaceToFlow(
|
|
71
|
+
config,
|
|
72
|
+
union,
|
|
73
|
+
selection.selectionSet.selections,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (type.kind === 'INTERFACE') {
|
|
78
|
+
if (!selection.selectionSet) {
|
|
79
|
+
console.log('no selection set', selection);
|
|
80
|
+
return babelTypes.anyTypeAnnotation();
|
|
81
|
+
}
|
|
82
|
+
return unionOrInterfaceToFlow(
|
|
83
|
+
config,
|
|
84
|
+
config.schema.interfacesByName[type.name],
|
|
85
|
+
selection.selectionSet.selections,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (type.kind === 'ENUM') {
|
|
89
|
+
return enumTypeToFlow(config, type.name);
|
|
90
|
+
}
|
|
91
|
+
if (type.kind !== 'OBJECT') {
|
|
92
|
+
console.log('not object', type);
|
|
93
|
+
return babelTypes.anyTypeAnnotation();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const tname = type.name;
|
|
97
|
+
if (!config.schema.typesByName[tname]) {
|
|
98
|
+
console.log('unknown referenced type', tname);
|
|
99
|
+
return babelTypes.anyTypeAnnotation();
|
|
100
|
+
}
|
|
101
|
+
const childType = config.schema.typesByName[tname];
|
|
102
|
+
if (!selection.selectionSet) {
|
|
103
|
+
console.log('no selection set', selection);
|
|
104
|
+
return babelTypes.anyTypeAnnotation();
|
|
105
|
+
}
|
|
106
|
+
return maybeAddDescriptionComment(
|
|
107
|
+
childType.description,
|
|
108
|
+
querySelectionToObjectType(
|
|
109
|
+
config,
|
|
110
|
+
selection.selectionSet.selections,
|
|
111
|
+
childType,
|
|
112
|
+
tname,
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const typeToFlow = (
|
|
118
|
+
config: Config,
|
|
119
|
+
type: IntrospectionOutputTypeRef,
|
|
120
|
+
selection: FieldNode,
|
|
121
|
+
): babelTypes.BabelNodeFlowType => {
|
|
122
|
+
// throw new Error('npoe');
|
|
123
|
+
if (type.kind === 'NON_NULL') {
|
|
124
|
+
return _typeToFlow(config, type.ofType, selection);
|
|
125
|
+
}
|
|
126
|
+
// If we don'babelTypes care about strict nullability checking, then pretend everything is non-null
|
|
127
|
+
if (!config.strictNullability) {
|
|
128
|
+
return _typeToFlow(config, type, selection);
|
|
129
|
+
}
|
|
130
|
+
const inner = _typeToFlow(config, type, selection);
|
|
131
|
+
const result = babelTypes.nullableTypeAnnotation(inner);
|
|
132
|
+
return transferLeadingComments(inner, result);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const querySelectionToObjectType = (
|
|
136
|
+
config: Config,
|
|
137
|
+
selections,
|
|
138
|
+
type,
|
|
139
|
+
typeName: string,
|
|
140
|
+
): BabelNodeFlowType => {
|
|
141
|
+
let seenTypeName = false;
|
|
142
|
+
return babelTypes.objectTypeAnnotation(
|
|
143
|
+
objectPropertiesToFlow(config, type, typeName, selections).filter(
|
|
144
|
+
(type) => {
|
|
145
|
+
// The apollo-utilities "addTypeName" utility will add it
|
|
146
|
+
// even if it's already specified :( so we have to filter out
|
|
147
|
+
// the extra one here.
|
|
148
|
+
if (
|
|
149
|
+
type.type === 'ObjectTypeProperty' &&
|
|
150
|
+
type.key.name === '__typename'
|
|
151
|
+
) {
|
|
152
|
+
if (seenTypeName) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
seenTypeName = true;
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
},
|
|
159
|
+
),
|
|
160
|
+
undefined /* indexers */,
|
|
161
|
+
undefined /* callProperties */,
|
|
162
|
+
undefined /* internalSlots */,
|
|
163
|
+
true /* exact */,
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const objectPropertiesToFlow = (
|
|
168
|
+
config: Config,
|
|
169
|
+
type: IntrospectionObjectType & {
|
|
170
|
+
fieldsByName: {[name: string]: IntrospectionField},
|
|
171
|
+
},
|
|
172
|
+
typeName: string,
|
|
173
|
+
selections: Selections,
|
|
174
|
+
): Array<BabelNodeObjectTypeProperty | BabelNodeObjectTypeSpreadProperty> => {
|
|
175
|
+
return [].concat(
|
|
176
|
+
...selections.map((selection) => {
|
|
177
|
+
switch (selection.kind) {
|
|
178
|
+
case 'FragmentSpread':
|
|
179
|
+
if (!config.fragments[selection.name.value]) {
|
|
180
|
+
config.errors.push(
|
|
181
|
+
`No fragment named '${selection.name.value}'. Did you forget to include it in the template literal?`,
|
|
182
|
+
);
|
|
183
|
+
return [
|
|
184
|
+
babelTypes.objectTypeProperty(
|
|
185
|
+
babelTypes.identifier(selection.name.value),
|
|
186
|
+
babelTypes.genericTypeAnnotation(
|
|
187
|
+
babelTypes.identifier(`UNKNOWN_FRAGMENT`),
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return objectPropertiesToFlow(
|
|
194
|
+
config,
|
|
195
|
+
type,
|
|
196
|
+
typeName,
|
|
197
|
+
config.fragments[selection.name.value].selectionSet
|
|
198
|
+
.selections,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
case 'Field':
|
|
202
|
+
const name = selection.name.value;
|
|
203
|
+
const alias: string = selection.alias
|
|
204
|
+
? selection.alias.value
|
|
205
|
+
: name;
|
|
206
|
+
if (name === '__typename') {
|
|
207
|
+
return [
|
|
208
|
+
babelTypes.objectTypeProperty(
|
|
209
|
+
babelTypes.identifier(alias),
|
|
210
|
+
babelTypes.stringLiteralTypeAnnotation(
|
|
211
|
+
typeName,
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
if (!type.fieldsByName[name]) {
|
|
217
|
+
config.errors.push(
|
|
218
|
+
`Unknown field '${name}' for type '${typeName}'`,
|
|
219
|
+
);
|
|
220
|
+
return babelTypes.objectTypeProperty(
|
|
221
|
+
babelTypes.identifier(alias),
|
|
222
|
+
babelTypes.genericTypeAnnotation(
|
|
223
|
+
babelTypes.identifier(
|
|
224
|
+
`UNKNOWN_FIELD["${name}"]`,
|
|
225
|
+
),
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const typeField = type.fieldsByName[name];
|
|
230
|
+
|
|
231
|
+
return [
|
|
232
|
+
maybeAddDescriptionComment(
|
|
233
|
+
typeField.description,
|
|
234
|
+
liftLeadingPropertyComments(
|
|
235
|
+
babelTypes.objectTypeProperty(
|
|
236
|
+
babelTypes.identifier(alias),
|
|
237
|
+
typeToFlow(
|
|
238
|
+
config,
|
|
239
|
+
typeField.type,
|
|
240
|
+
selection,
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
),
|
|
244
|
+
),
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
default:
|
|
248
|
+
config.errors.push(
|
|
249
|
+
`Unsupported selection kind '${selection.kind}'`,
|
|
250
|
+
);
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export const unionOrInterfaceToFlow = (
|
|
258
|
+
config: Config,
|
|
259
|
+
type:
|
|
260
|
+
| IntrospectionUnionType
|
|
261
|
+
| (IntrospectionInterfaceType & {
|
|
262
|
+
fieldsByName: {[key: string]: IntrospectionField},
|
|
263
|
+
}),
|
|
264
|
+
selections: Selections,
|
|
265
|
+
): BabelNodeFlowType => {
|
|
266
|
+
const selectedAttributes: Array<
|
|
267
|
+
Array<BabelNodeObjectTypeProperty | BabelNodeObjectTypeSpreadProperty>,
|
|
268
|
+
> = type.possibleTypes.map((possible) => {
|
|
269
|
+
let seenTypeName = false;
|
|
270
|
+
return selections
|
|
271
|
+
.map((selection) =>
|
|
272
|
+
unionOrInterfaceSelection(config, type, possible, selection),
|
|
273
|
+
)
|
|
274
|
+
.flat()
|
|
275
|
+
.filter((type) => {
|
|
276
|
+
// The apollo-utilities "addTypeName" utility will add it
|
|
277
|
+
// even if it's already specified :( so we have to filter out
|
|
278
|
+
// the extra one here.
|
|
279
|
+
if (
|
|
280
|
+
type.type === 'ObjectTypeProperty' &&
|
|
281
|
+
type.key.name === '__typename'
|
|
282
|
+
) {
|
|
283
|
+
if (seenTypeName) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
seenTypeName = true;
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
const allFields = selections.every(
|
|
292
|
+
(selection) => selection.kind === 'Field',
|
|
293
|
+
);
|
|
294
|
+
if (selectedAttributes.length === 1 || allFields) {
|
|
295
|
+
return babelTypes.objectTypeAnnotation(
|
|
296
|
+
selectedAttributes[0],
|
|
297
|
+
undefined /* indexers */,
|
|
298
|
+
undefined /* callProperties */,
|
|
299
|
+
undefined /* internalSlots */,
|
|
300
|
+
true /* exact */,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return babelTypes.unionTypeAnnotation(
|
|
304
|
+
selectedAttributes.map((properties) =>
|
|
305
|
+
babelTypes.objectTypeAnnotation(
|
|
306
|
+
properties,
|
|
307
|
+
undefined /* indexers */,
|
|
308
|
+
undefined /* callProperties */,
|
|
309
|
+
undefined /* internalSlots */,
|
|
310
|
+
true /* exact */,
|
|
311
|
+
),
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
const unionOrInterfaceSelection = (
|
|
316
|
+
config,
|
|
317
|
+
type,
|
|
318
|
+
possible,
|
|
319
|
+
selection,
|
|
320
|
+
): Array<BabelNodeObjectTypeProperty | BabelNodeObjectTypeSpreadProperty> => {
|
|
321
|
+
if (selection.kind === 'Field' && selection.name.value === '__typename') {
|
|
322
|
+
const alias = selection.alias
|
|
323
|
+
? selection.alias.value
|
|
324
|
+
: selection.name.value;
|
|
325
|
+
return [
|
|
326
|
+
babelTypes.objectTypeProperty(
|
|
327
|
+
babelTypes.identifier(alias),
|
|
328
|
+
babelTypes.stringLiteralTypeAnnotation(possible.name),
|
|
329
|
+
),
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
if (selection.kind === 'Field' && type.kind !== 'UNION') {
|
|
333
|
+
// this is an interface
|
|
334
|
+
const name = selection.name.value;
|
|
335
|
+
const alias = selection.alias ? selection.alias.value : name;
|
|
336
|
+
if (!type.fieldsByName[name]) {
|
|
337
|
+
config.errors.push(
|
|
338
|
+
'Unknown field: ' +
|
|
339
|
+
name +
|
|
340
|
+
' on type ' +
|
|
341
|
+
type.name +
|
|
342
|
+
' for possible ' +
|
|
343
|
+
possible.name,
|
|
344
|
+
);
|
|
345
|
+
return [
|
|
346
|
+
babelTypes.objectTypeProperty(
|
|
347
|
+
babelTypes.identifier(alias),
|
|
348
|
+
babelTypes.genericTypeAnnotation(
|
|
349
|
+
babelTypes.identifier(`UNKNOWN_FIELD`),
|
|
350
|
+
),
|
|
351
|
+
),
|
|
352
|
+
];
|
|
353
|
+
}
|
|
354
|
+
const typeField = type.fieldsByName[name];
|
|
355
|
+
return [
|
|
356
|
+
liftLeadingPropertyComments(
|
|
357
|
+
babelTypes.objectTypeProperty(
|
|
358
|
+
babelTypes.identifier(alias),
|
|
359
|
+
typeToFlow(config, typeField.type, selection),
|
|
360
|
+
),
|
|
361
|
+
),
|
|
362
|
+
];
|
|
363
|
+
}
|
|
364
|
+
if (selection.kind === 'FragmentSpread') {
|
|
365
|
+
const fragment = config.fragments[selection.name.value];
|
|
366
|
+
const typeName = fragment.typeCondition.name.value;
|
|
367
|
+
if (
|
|
368
|
+
(config.schema.interfacesByName[typeName] &&
|
|
369
|
+
config.schema.interfacesByName[typeName].possibleTypesByName[
|
|
370
|
+
possible.name
|
|
371
|
+
]) ||
|
|
372
|
+
typeName === possible.name
|
|
373
|
+
) {
|
|
374
|
+
return [].concat(
|
|
375
|
+
...fragment.selectionSet.selections.map((selection) =>
|
|
376
|
+
unionOrInterfaceSelection(
|
|
377
|
+
config,
|
|
378
|
+
config.schema.typesByName[possible.name],
|
|
379
|
+
possible,
|
|
380
|
+
selection,
|
|
381
|
+
),
|
|
382
|
+
),
|
|
383
|
+
);
|
|
384
|
+
} else {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (selection.kind !== 'InlineFragment') {
|
|
389
|
+
config.errors.push(
|
|
390
|
+
`union selectors must be inline fragment: found ${selection.kind}`,
|
|
391
|
+
);
|
|
392
|
+
if (type.kind === 'UNION') {
|
|
393
|
+
config.errors
|
|
394
|
+
.push(`You're trying to select a field from the union ${type.name},
|
|
395
|
+
but the only field you're allowed to select is "__typename".
|
|
396
|
+
Try using an inline fragment "... on SomeType {}".`);
|
|
397
|
+
}
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
if (!selection.typeCondition) {
|
|
401
|
+
throw new Error('Expected selection to have a typeCondition');
|
|
402
|
+
}
|
|
403
|
+
const typeName = selection.typeCondition.name.value;
|
|
404
|
+
if (
|
|
405
|
+
(config.schema.interfacesByName[typeName] &&
|
|
406
|
+
config.schema.interfacesByName[typeName].possibleTypesByName[
|
|
407
|
+
possible.name
|
|
408
|
+
]) ||
|
|
409
|
+
typeName === possible.name
|
|
410
|
+
) {
|
|
411
|
+
return objectPropertiesToFlow(
|
|
412
|
+
config,
|
|
413
|
+
config.schema.typesByName[possible.name],
|
|
414
|
+
possible.name,
|
|
415
|
+
selection.selectionSet.selections,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return [];
|
|
419
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import generate from '@babel/generator'; // eslint-disable-line flowtype-errors/uncovered
|
|
3
|
+
import type {
|
|
4
|
+
BabelNodeFlowType,
|
|
5
|
+
BabelNodeObjectTypeProperty,
|
|
6
|
+
} from '@babel/types';
|
|
7
|
+
import * as babelTypes from '@babel/types';
|
|
8
|
+
import type {OperationDefinitionNode, TypeNode} from 'graphql/language/ast';
|
|
9
|
+
import type {IntrospectionInputTypeRef} from 'graphql/utilities/introspectionQuery';
|
|
10
|
+
import {builtinScalars, enumTypeToFlow, scalarTypeToFlow} from './enums';
|
|
11
|
+
import type {Config, Schema} from './types';
|
|
12
|
+
import {
|
|
13
|
+
liftLeadingPropertyComments,
|
|
14
|
+
maybeAddDescriptionComment,
|
|
15
|
+
transferLeadingComments,
|
|
16
|
+
} from './utils';
|
|
17
|
+
|
|
18
|
+
export const inputObjectToFlow = (
|
|
19
|
+
config: Config,
|
|
20
|
+
name: string,
|
|
21
|
+
): BabelNodeFlowType => {
|
|
22
|
+
const inputObject = config.schema.inputObjectsByName[name];
|
|
23
|
+
if (!inputObject) {
|
|
24
|
+
config.errors.push(`Unknown input object ${name}`);
|
|
25
|
+
return babelTypes.stringLiteralTypeAnnotation(
|
|
26
|
+
`Unknown input object ${name}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return maybeAddDescriptionComment(
|
|
31
|
+
inputObject.description,
|
|
32
|
+
babelTypes.objectTypeAnnotation(
|
|
33
|
+
inputObject.inputFields.map((vbl) =>
|
|
34
|
+
maybeAddDescriptionComment(
|
|
35
|
+
vbl.description,
|
|
36
|
+
maybeOptionalObjectTypeProperty(
|
|
37
|
+
vbl.name,
|
|
38
|
+
inputRefToFlow(config, vbl.type),
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
undefined /* indexers */,
|
|
43
|
+
undefined /* callProperties */,
|
|
44
|
+
undefined /* internalSlots */,
|
|
45
|
+
true /* exact */,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const maybeOptionalObjectTypeProperty = (
|
|
51
|
+
name: string,
|
|
52
|
+
type: babelTypes.BabelNodeFlowType,
|
|
53
|
+
): BabelNodeObjectTypeProperty => {
|
|
54
|
+
const prop = liftLeadingPropertyComments(
|
|
55
|
+
babelTypes.objectTypeProperty(babelTypes.identifier(name), type),
|
|
56
|
+
);
|
|
57
|
+
if (type.type === 'NullableTypeAnnotation') {
|
|
58
|
+
prop.optional = true;
|
|
59
|
+
}
|
|
60
|
+
return prop;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const inputRefToFlow = (
|
|
64
|
+
config: Config,
|
|
65
|
+
inputRef: IntrospectionInputTypeRef,
|
|
66
|
+
): BabelNodeFlowType => {
|
|
67
|
+
if (inputRef.kind === 'NON_NULL') {
|
|
68
|
+
return _inputRefToFlow(config, inputRef.ofType);
|
|
69
|
+
}
|
|
70
|
+
const result = _inputRefToFlow(config, inputRef);
|
|
71
|
+
return transferLeadingComments(
|
|
72
|
+
result,
|
|
73
|
+
babelTypes.nullableTypeAnnotation(result),
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const _inputRefToFlow = (
|
|
78
|
+
config: Config,
|
|
79
|
+
inputRef: IntrospectionInputTypeRef,
|
|
80
|
+
) => {
|
|
81
|
+
if (inputRef.kind === 'SCALAR') {
|
|
82
|
+
return scalarTypeToFlow(config, inputRef.name);
|
|
83
|
+
}
|
|
84
|
+
if (inputRef.kind === 'ENUM') {
|
|
85
|
+
return enumTypeToFlow(config, inputRef.name);
|
|
86
|
+
}
|
|
87
|
+
if (inputRef.kind === 'INPUT_OBJECT') {
|
|
88
|
+
return inputObjectToFlow(config, inputRef.name);
|
|
89
|
+
}
|
|
90
|
+
if (inputRef.kind === 'LIST') {
|
|
91
|
+
return babelTypes.genericTypeAnnotation(
|
|
92
|
+
babelTypes.identifier('$ReadOnlyArray'),
|
|
93
|
+
babelTypes.typeParameterInstantiation([
|
|
94
|
+
inputRefToFlow(config, inputRef.ofType),
|
|
95
|
+
]),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return babelTypes.stringLiteralTypeAnnotation(JSON.stringify(inputRef));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const variableToFlow = (config: Config, type: TypeNode) => {
|
|
102
|
+
if (type.kind === 'NonNullType') {
|
|
103
|
+
return _variableToFlow(config, type.type);
|
|
104
|
+
}
|
|
105
|
+
const result = _variableToFlow(config, type);
|
|
106
|
+
return transferLeadingComments(
|
|
107
|
+
result,
|
|
108
|
+
babelTypes.nullableTypeAnnotation(result),
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const _variableToFlow = (config: Config, type: TypeNode) => {
|
|
113
|
+
if (type.kind === 'NamedType') {
|
|
114
|
+
if (builtinScalars[type.name.value]) {
|
|
115
|
+
return scalarTypeToFlow(config, type.name.value);
|
|
116
|
+
}
|
|
117
|
+
if (config.schema.enumsByName[type.name.value]) {
|
|
118
|
+
return enumTypeToFlow(config, type.name.value);
|
|
119
|
+
}
|
|
120
|
+
return inputObjectToFlow(config, type.name.value);
|
|
121
|
+
}
|
|
122
|
+
if (type.kind === 'ListType') {
|
|
123
|
+
return babelTypes.genericTypeAnnotation(
|
|
124
|
+
babelTypes.identifier('$ReadOnlyArray'),
|
|
125
|
+
babelTypes.typeParameterInstantiation([
|
|
126
|
+
variableToFlow(config, type.type),
|
|
127
|
+
]),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return babelTypes.stringLiteralTypeAnnotation(
|
|
131
|
+
'UNKNOWN' + JSON.stringify(type),
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const generateVariablesType = (
|
|
136
|
+
schema: Schema,
|
|
137
|
+
item: OperationDefinitionNode,
|
|
138
|
+
config: Config,
|
|
139
|
+
): string => {
|
|
140
|
+
const variableObject = babelTypes.objectTypeAnnotation(
|
|
141
|
+
(item.variableDefinitions || []).map((vbl) => {
|
|
142
|
+
return maybeOptionalObjectTypeProperty(
|
|
143
|
+
vbl.variable.name.value,
|
|
144
|
+
variableToFlow(config, vbl.type),
|
|
145
|
+
);
|
|
146
|
+
}),
|
|
147
|
+
undefined /* indexers */,
|
|
148
|
+
undefined /* callProperties */,
|
|
149
|
+
undefined /* internalSlots */,
|
|
150
|
+
true /* exact */,
|
|
151
|
+
);
|
|
152
|
+
return generate(variableObject).code; // eslint-disable-line flowtype-errors/uncovered
|
|
153
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/* flow-uncovered-file */
|
|
3
|
+
// @flow
|
|
4
|
+
/**
|
|
5
|
+
* This tool generates flowtype definitions from graphql queries.
|
|
6
|
+
*
|
|
7
|
+
* It relies on `introspection-query.json` existing in this directory,
|
|
8
|
+
* which is produced by running `./tools/graphql-flow/sendIntrospection.js`.
|
|
9
|
+
*/
|
|
10
|
+
import type {DefinitionNode, DocumentNode} from 'graphql';
|
|
11
|
+
|
|
12
|
+
import {generateResponseType} from './generateResponseType';
|
|
13
|
+
import {generateVariablesType} from './generateVariablesType';
|
|
14
|
+
export {spyOnGraphqlTagToCollectQueries} from './jest-mock-graphql-tag';
|
|
15
|
+
|
|
16
|
+
import type {Config, Options, Schema} from './types';
|
|
17
|
+
|
|
18
|
+
const optionsToConfig = (
|
|
19
|
+
schema: Schema,
|
|
20
|
+
definitions: $ReadOnlyArray<DefinitionNode>,
|
|
21
|
+
options?: Options,
|
|
22
|
+
errors: Array<string> = [],
|
|
23
|
+
): Config => {
|
|
24
|
+
const internalOptions = {
|
|
25
|
+
strictNullability: options?.strictNullability ?? true,
|
|
26
|
+
readOnlyArray: options?.readOnlyArray ?? true,
|
|
27
|
+
scalars: options?.scalars ?? {},
|
|
28
|
+
};
|
|
29
|
+
const fragments = {};
|
|
30
|
+
definitions.forEach((def) => {
|
|
31
|
+
if (def.kind === 'FragmentDefinition') {
|
|
32
|
+
fragments[def.name.value] = def;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const config = {
|
|
36
|
+
fragments,
|
|
37
|
+
schema,
|
|
38
|
+
errors,
|
|
39
|
+
...internalOptions,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return config;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export class FlowGenerationError extends Error {
|
|
46
|
+
messages: Array<string>;
|
|
47
|
+
constructor(errors: Array<string>) {
|
|
48
|
+
super(`Graphql-flow type generation failed! ${errors.join('; ')}`);
|
|
49
|
+
this.messages = errors;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const documentToFlowTypes = (
|
|
54
|
+
document: DocumentNode,
|
|
55
|
+
schema: Schema,
|
|
56
|
+
options?: Options,
|
|
57
|
+
): $ReadOnlyArray<{
|
|
58
|
+
name: string,
|
|
59
|
+
typeName: string,
|
|
60
|
+
code: string,
|
|
61
|
+
}> => {
|
|
62
|
+
const errors: Array<string> = [];
|
|
63
|
+
const config = optionsToConfig(
|
|
64
|
+
schema,
|
|
65
|
+
document.definitions,
|
|
66
|
+
options,
|
|
67
|
+
errors,
|
|
68
|
+
);
|
|
69
|
+
const result = document.definitions
|
|
70
|
+
.map((item) => {
|
|
71
|
+
if (
|
|
72
|
+
item.kind === 'OperationDefinition' &&
|
|
73
|
+
(item.operation === 'query' || item.operation === 'mutation') &&
|
|
74
|
+
item.name
|
|
75
|
+
) {
|
|
76
|
+
const name = item.name.value;
|
|
77
|
+
const response = generateResponseType(schema, item, config);
|
|
78
|
+
const variables = generateVariablesType(schema, item, config);
|
|
79
|
+
|
|
80
|
+
const typeName = `${name}Type`;
|
|
81
|
+
// TODO(jared): Maybe make this template configurable?
|
|
82
|
+
// We'll see what's required to get webapp on board.
|
|
83
|
+
const code = `export type ${typeName} = {|\n variables: ${variables},\n response: ${response}\n|};`;
|
|
84
|
+
|
|
85
|
+
return {name, typeName, code};
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
if (errors.length) {
|
|
90
|
+
throw new FlowGenerationError(errors);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
};
|