@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,191 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
// Import this in your jest setup, to mock out graphql-tag!
|
|
3
|
+
import type {DocumentNode, IntrospectionQuery} from 'graphql';
|
|
4
|
+
import type {Schema, Options, Scalars} from './types';
|
|
5
|
+
import {validate} from 'graphql/validation';
|
|
6
|
+
import {buildClientSchema} from 'graphql';
|
|
7
|
+
import {print} from 'graphql/language/printer';
|
|
8
|
+
import {addTypenameToDocument} from 'apollo-utilities'; // eslint-disable-line flowtype-errors/uncovered
|
|
9
|
+
import {schemaFromIntrospectionData} from './schemaFromIntrospectionData';
|
|
10
|
+
|
|
11
|
+
const indexPrelude = `// @flow
|
|
12
|
+
//
|
|
13
|
+
// AUTOGENERATED
|
|
14
|
+
// NOTE: New response types are added to this file automatically.
|
|
15
|
+
// Outdated response types can be removed manually as they are deprecated.
|
|
16
|
+
//
|
|
17
|
+
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const generateTypeFiles = (
|
|
21
|
+
schema: Schema,
|
|
22
|
+
document: DocumentNode,
|
|
23
|
+
options: Options,
|
|
24
|
+
) => {
|
|
25
|
+
const {documentToFlowTypes} = require('.');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const format: ({text: string}) => string = require('prettier-eslint'); // eslint-disable-line flowtype-errors/uncovered
|
|
29
|
+
|
|
30
|
+
const indexFile = (generatedDir) => path.join(generatedDir, 'index.js');
|
|
31
|
+
|
|
32
|
+
const maybeCreateGeneratedDir = (generatedDir) => {
|
|
33
|
+
if (!fs.existsSync(generatedDir)) {
|
|
34
|
+
fs.mkdirSync(generatedDir, {recursive: true});
|
|
35
|
+
|
|
36
|
+
// Now write an index.js for each __generated__ dir.
|
|
37
|
+
fs.writeFileSync(indexFile(generatedDir), indexPrelude);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/// Write export for __generated__/index.js if it doesn't exist
|
|
42
|
+
const writeToIndex = (filePath, typeName) => {
|
|
43
|
+
const index = indexFile(path.dirname(filePath));
|
|
44
|
+
const indexContents = fs.readFileSync(index, 'utf8');
|
|
45
|
+
const newLine = `export type {${typeName}} from './${path.basename(
|
|
46
|
+
filePath,
|
|
47
|
+
)}';`;
|
|
48
|
+
if (indexContents.indexOf(path.basename(filePath)) === -1) {
|
|
49
|
+
fs.appendFileSync(index, newLine + '\n');
|
|
50
|
+
} else {
|
|
51
|
+
const lines = indexContents.split('\n').map((line) => {
|
|
52
|
+
if (line.includes(path.basename(filePath))) {
|
|
53
|
+
return newLine;
|
|
54
|
+
}
|
|
55
|
+
return line;
|
|
56
|
+
});
|
|
57
|
+
fs.writeFileSync(index, lines.join('\n'));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Get the name of the file that `gql` was called from
|
|
62
|
+
const errorLines = new Error().stack.split('\n');
|
|
63
|
+
const fileName = errorLines[3].split('(').slice(-1)[0].split(':')[0];
|
|
64
|
+
|
|
65
|
+
const generated = documentToFlowTypes(document, schema, options);
|
|
66
|
+
generated.forEach(({name, typeName, code}) => {
|
|
67
|
+
// We write all generated files to a `__generated__` subdir to keep
|
|
68
|
+
// things tidy.
|
|
69
|
+
const targetFileName = `${typeName}.js`;
|
|
70
|
+
const generatedDir = path.join(path.dirname(fileName), '__generated__');
|
|
71
|
+
const targetPath = path.join(generatedDir, targetFileName);
|
|
72
|
+
|
|
73
|
+
maybeCreateGeneratedDir(generatedDir);
|
|
74
|
+
|
|
75
|
+
// NOTE: Uncomment this to write the query definitions to disk if
|
|
76
|
+
// you need to add new features to the flow type generation
|
|
77
|
+
// fs.writeFileSync(
|
|
78
|
+
// targetFileName + '.query',
|
|
79
|
+
// JSON.stringify(definitions, null, 2),
|
|
80
|
+
// );
|
|
81
|
+
const fileContents = format({
|
|
82
|
+
text:
|
|
83
|
+
'// @' +
|
|
84
|
+
`flow\n// AUTOGENERATED -- DO NOT EDIT\n` +
|
|
85
|
+
`// Generated for operation '${name}' in file '../${path.basename(
|
|
86
|
+
fileName,
|
|
87
|
+
)}'\n` +
|
|
88
|
+
`// To regenerate, run 'yarn test queries'.\n` +
|
|
89
|
+
code,
|
|
90
|
+
});
|
|
91
|
+
fs.writeFileSync(targetPath, fileContents);
|
|
92
|
+
|
|
93
|
+
writeToIndex(targetPath, typeName);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type GraphqlTagFn = (raw: string, ...args: Array<any>) => DocumentNode;
|
|
98
|
+
|
|
99
|
+
type SpyOptions = {
|
|
100
|
+
pragma?: string,
|
|
101
|
+
loosePragma?: string,
|
|
102
|
+
scalars?: Scalars,
|
|
103
|
+
strictNullability?: boolean,
|
|
104
|
+
readOnlyArray?: boolean,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// This function is expected to be called like so:
|
|
108
|
+
//
|
|
109
|
+
// jest.mock('graphql-tag', () => {
|
|
110
|
+
// const introspectionData = jest.requireActual(
|
|
111
|
+
// './our-introspection-query.json',
|
|
112
|
+
// );
|
|
113
|
+
// const {spyOnGraphqlTagToCollectQueries} = jest.requireActual(
|
|
114
|
+
// 'graphql-flow/jest-mock-graphql-tag.js');
|
|
115
|
+
//
|
|
116
|
+
// return spyOnGraphqlTagToCollectQueries(
|
|
117
|
+
// jest.requireActual('graphql-tag'),
|
|
118
|
+
// introspectionData,
|
|
119
|
+
// );
|
|
120
|
+
// });
|
|
121
|
+
//
|
|
122
|
+
// If both pragma and loosePragma are empty, then all graphql
|
|
123
|
+
// documents will be processed. Otherwise, only documents
|
|
124
|
+
// with one of the pragmas will be processed.
|
|
125
|
+
const spyOnGraphqlTagToCollectQueries = (
|
|
126
|
+
realGraphqlTag: GraphqlTagFn,
|
|
127
|
+
introspectionData: IntrospectionQuery,
|
|
128
|
+
options: SpyOptions = {},
|
|
129
|
+
): GraphqlTagFn => {
|
|
130
|
+
const collection: Array<{
|
|
131
|
+
raw: string,
|
|
132
|
+
errors: $ReadOnlyArray<Error>,
|
|
133
|
+
}> = [];
|
|
134
|
+
|
|
135
|
+
const clientSchema = buildClientSchema(introspectionData);
|
|
136
|
+
const schema = schemaFromIntrospectionData(introspectionData);
|
|
137
|
+
|
|
138
|
+
const wrapper = function gql() {
|
|
139
|
+
const document: DocumentNode = realGraphqlTag.apply(this, arguments); // eslint-disable-line flowtype-errors/uncovered
|
|
140
|
+
const hasNonFragments = document.definitions.some(
|
|
141
|
+
({kind}) => kind !== 'FragmentDefinition',
|
|
142
|
+
);
|
|
143
|
+
if (hasNonFragments) {
|
|
144
|
+
// eslint-disable-next-line flowtype-errors/uncovered
|
|
145
|
+
const withTypeNames: DocumentNode = addTypenameToDocument(document);
|
|
146
|
+
collection.push({
|
|
147
|
+
raw: print(withTypeNames),
|
|
148
|
+
errors: validate(clientSchema, document),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const rawSource: string = arguments[0].raw[0]; // eslint-disable-line flowtype-errors/uncovered
|
|
152
|
+
const processedOptions = processPragmas(options, rawSource);
|
|
153
|
+
if (processedOptions) {
|
|
154
|
+
generateTypeFiles(schema, withTypeNames, processedOptions);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return document;
|
|
158
|
+
};
|
|
159
|
+
wrapper.collectedQueries = collection;
|
|
160
|
+
return wrapper;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const processPragmas = (
|
|
164
|
+
options: SpyOptions,
|
|
165
|
+
rawSource: string,
|
|
166
|
+
): null | Options => {
|
|
167
|
+
const autogen = options.loosePragma
|
|
168
|
+
? rawSource.includes(options.loosePragma)
|
|
169
|
+
: false;
|
|
170
|
+
const autogenStrict = options.pragma
|
|
171
|
+
? rawSource.includes(options.pragma)
|
|
172
|
+
: false;
|
|
173
|
+
const noPragmas = !options.loosePragma && !options.pragma;
|
|
174
|
+
|
|
175
|
+
if (autogen || autogenStrict || noPragmas) {
|
|
176
|
+
return {
|
|
177
|
+
strictNullability: noPragmas
|
|
178
|
+
? options.strictNullability
|
|
179
|
+
: autogenStrict || !autogen,
|
|
180
|
+
readOnlyArray: options.readOnlyArray,
|
|
181
|
+
scalars: options.scalars,
|
|
182
|
+
};
|
|
183
|
+
} else {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
processPragmas,
|
|
190
|
+
spyOnGraphqlTagToCollectQueries,
|
|
191
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Takes the introspectionQuery response and parses it into the "Schema"
|
|
4
|
+
* type that we use to look up types, interfaces, etc.
|
|
5
|
+
*/
|
|
6
|
+
import type {IntrospectionQuery} from 'graphql';
|
|
7
|
+
import type {Schema} from './types';
|
|
8
|
+
|
|
9
|
+
export const schemaFromIntrospectionData = (
|
|
10
|
+
schema: IntrospectionQuery,
|
|
11
|
+
): Schema => {
|
|
12
|
+
const result: Schema = {
|
|
13
|
+
interfacesByName: {},
|
|
14
|
+
typesByName: {},
|
|
15
|
+
inputObjectsByName: {},
|
|
16
|
+
unionsByName: {},
|
|
17
|
+
enumsByName: {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
schema.__schema.types.forEach((type) => {
|
|
21
|
+
if (type.kind === 'ENUM') {
|
|
22
|
+
result.enumsByName[type.name] = type;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (type.kind === 'UNION') {
|
|
26
|
+
result.unionsByName[type.name] = type;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (type.kind === 'INTERFACE') {
|
|
30
|
+
result.interfacesByName[type.name] = {
|
|
31
|
+
...type,
|
|
32
|
+
possibleTypesByName: {},
|
|
33
|
+
fieldsByName: {},
|
|
34
|
+
};
|
|
35
|
+
type.possibleTypes.forEach(
|
|
36
|
+
(p) =>
|
|
37
|
+
(result.interfacesByName[type.name].possibleTypesByName[
|
|
38
|
+
p.name
|
|
39
|
+
] = true),
|
|
40
|
+
);
|
|
41
|
+
type.fields.forEach((field) => {
|
|
42
|
+
result.interfacesByName[type.name].fieldsByName[field.name] =
|
|
43
|
+
field;
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (type.kind === 'INPUT_OBJECT') {
|
|
48
|
+
result.inputObjectsByName[type.name] = type;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (type.kind === 'SCALAR') {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
result.typesByName[type.name] = {
|
|
55
|
+
...type,
|
|
56
|
+
fieldsByName: {},
|
|
57
|
+
};
|
|
58
|
+
if (!type.fields) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type.fields.forEach((field) => {
|
|
63
|
+
result.typesByName[type.name].fieldsByName[field.name] = field;
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
};
|
package/src/types.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
FragmentDefinitionNode,
|
|
5
|
+
IntrospectionEnumType,
|
|
6
|
+
IntrospectionField,
|
|
7
|
+
IntrospectionInputObjectType,
|
|
8
|
+
IntrospectionInterfaceType,
|
|
9
|
+
IntrospectionObjectType,
|
|
10
|
+
IntrospectionUnionType,
|
|
11
|
+
SelectionNode,
|
|
12
|
+
} from 'graphql';
|
|
13
|
+
|
|
14
|
+
export type Selections = $ReadOnlyArray<SelectionNode>;
|
|
15
|
+
|
|
16
|
+
export type Options = {|
|
|
17
|
+
strictNullability?: boolean, // default true
|
|
18
|
+
readOnlyArray?: boolean, // default true
|
|
19
|
+
scalars?: Scalars,
|
|
20
|
+
|};
|
|
21
|
+
|
|
22
|
+
export type Schema = {
|
|
23
|
+
interfacesByName: {
|
|
24
|
+
[key: string]: IntrospectionInterfaceType & {
|
|
25
|
+
fieldsByName: {[key: string]: IntrospectionField},
|
|
26
|
+
possibleTypesByName: {[key: string]: boolean},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
inputObjectsByName: {
|
|
30
|
+
[key: string]: IntrospectionInputObjectType,
|
|
31
|
+
},
|
|
32
|
+
typesByName: {
|
|
33
|
+
[key: string]: IntrospectionObjectType & {
|
|
34
|
+
fieldsByName: {[key: string]: IntrospectionField},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
unionsByName: {
|
|
38
|
+
[key: string]: IntrospectionUnionType,
|
|
39
|
+
},
|
|
40
|
+
enumsByName: {
|
|
41
|
+
[key: string]: IntrospectionEnumType,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type Config = {
|
|
46
|
+
strictNullability: boolean,
|
|
47
|
+
readOnlyArray: boolean,
|
|
48
|
+
fragments: {[key: string]: FragmentDefinitionNode},
|
|
49
|
+
|
|
50
|
+
schema: Schema,
|
|
51
|
+
scalars: Scalars,
|
|
52
|
+
errors: Array<string>,
|
|
53
|
+
};
|
|
54
|
+
export type Scalars = {[key: string]: 'string' | 'number' | 'boolean'};
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import * as babelTypes from '@babel/types';
|
|
4
|
+
import {BabelNodeObjectTypeProperty} from '@babel/types';
|
|
5
|
+
|
|
6
|
+
export const liftLeadingPropertyComments = (
|
|
7
|
+
property: BabelNodeObjectTypeProperty,
|
|
8
|
+
): BabelNodeObjectTypeProperty => {
|
|
9
|
+
return transferLeadingComments(property.value, property);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const maybeAddDescriptionComment = <T: babelTypes.BabelNode>(
|
|
13
|
+
description: ?string,
|
|
14
|
+
node: T,
|
|
15
|
+
): T => {
|
|
16
|
+
if (description) {
|
|
17
|
+
addCommentAsLineComments(description, node);
|
|
18
|
+
}
|
|
19
|
+
return node;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function addCommentAsLineComments(
|
|
23
|
+
description: string,
|
|
24
|
+
res: babelTypes.BabelNode,
|
|
25
|
+
) {
|
|
26
|
+
if (res.leadingComments?.length) {
|
|
27
|
+
res.leadingComments[0].value += '\n\n---\n\n' + description;
|
|
28
|
+
} else {
|
|
29
|
+
babelTypes.addComment(
|
|
30
|
+
res,
|
|
31
|
+
'leading',
|
|
32
|
+
'* ' + description,
|
|
33
|
+
false, // this specifies that it's a block comment, not a line comment
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const transferLeadingComments = <T: babelTypes.BabelNode>(
|
|
39
|
+
source: babelTypes.BabelNode,
|
|
40
|
+
dest: T,
|
|
41
|
+
): T => {
|
|
42
|
+
if (source.leadingComments?.length) {
|
|
43
|
+
dest.leadingComments = [
|
|
44
|
+
...(dest.leadingComments || []),
|
|
45
|
+
...source.leadingComments,
|
|
46
|
+
];
|
|
47
|
+
source.leadingComments = [];
|
|
48
|
+
}
|
|
49
|
+
return dest;
|
|
50
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const {execSync} = require('child_process');
|
|
4
|
+
|
|
5
|
+
export const findRepoRoot = (): string => {
|
|
6
|
+
try {
|
|
7
|
+
const res = execSync('git rev-parse --show-toplevel', {
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
});
|
|
10
|
+
return res.trim();
|
|
11
|
+
// eslint-disable-next-line flowtype-errors/uncovered
|
|
12
|
+
} catch (err) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
// eslint-disable-next-line flowtype-errors/uncovered
|
|
15
|
+
`Unable to use git rev-parse to find the repository root. ${err.message}`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const findGraphqlTagReferences = (root: string): Array<string> => {
|
|
21
|
+
try {
|
|
22
|
+
const response = execSync(
|
|
23
|
+
"git grep -I --word-regexp --name-only --fixed-strings 'gql`' -- '*.js'",
|
|
24
|
+
{
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
cwd: root,
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
return response
|
|
30
|
+
.trim()
|
|
31
|
+
.split('\n')
|
|
32
|
+
.map((relative) => path.join(root, relative));
|
|
33
|
+
// eslint-disable-next-line flowtype-errors/uncovered
|
|
34
|
+
} catch (err) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
// eslint-disable-next-line flowtype-errors/uncovered
|
|
37
|
+
`Unable to use git grep to find files with gql tags. ${err.message}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
};
|