@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,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
+ };