Skip to content

Commit b517ec3

Browse files
authored
feat: add type branding to graphql in TS outputs (#623)
* feat: add types to graphql in TS outputs * wip: alt type tagged not inferring from fieldName * made types path computed from config * cruft cleanup * regenerated api.md * refactor: small helper prop refactor * fixed graphql formatting in ts output, types spacing in API.ts output * removed unused type guard function, fixes code coverage limit violation
1 parent e8d63f0 commit b517ec3

File tree

7 files changed

+172
-63
lines changed

7 files changed

+172
-63
lines changed

packages/amplify-codegen/src/commands/statements.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ const Ora = require('ora');
44

55
const { loadConfig } = require('../codegen-config');
66
const constants = require('../constants');
7-
const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails, readSchemaFromFile, GraphQLStatementsFormatter } = require('../utils');
7+
const {
8+
ensureIntrospectionSchema,
9+
getFrontEndHandler,
10+
getAppSyncAPIDetails,
11+
readSchemaFromFile,
12+
GraphQLStatementsFormatter,
13+
} = require('../utils');
814
const { generateGraphQLDocuments } = require('@aws-amplify/graphql-docs-generator');
915

1016
async function generateStatements(context, forceDownloadSchema, maxDepth, withoutInit = false, decoupleFrontend = '') {
@@ -62,16 +68,19 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou
6268
const schemaData = readSchemaFromFile(schemaPath);
6369
const generatedOps = generateGraphQLDocuments(schemaData, {
6470
maxDepth: maxDepth || cfg.amplifyExtension.maxDepth,
65-
useExternalFragmentForS3Object: (language === 'graphql'),
71+
useExternalFragmentForS3Object: language === 'graphql',
6672
// default typenameIntrospection to true when not set
6773
typenameIntrospection:
6874
cfg.amplifyExtension.typenameIntrospection === undefined ? true : !!cfg.amplifyExtension.typenameIntrospection,
75+
includeMetaData: true,
6976
});
70-
if(!generatedOps) {
77+
if (!generatedOps) {
7178
context.print.warning('No GraphQL statements are generated. Check if the introspection schema has GraphQL operations defined.');
72-
}
73-
else {
74-
await writeGeneratedDocuments(language, generatedOps, opsGenDirectory);
79+
} else {
80+
const relativeTypesPath = cfg.amplifyExtension.generatedFileName
81+
? path.relative(opsGenDirectory, cfg.amplifyExtension.generatedFileName)
82+
: null;
83+
await writeGeneratedDocuments(language, generatedOps, opsGenDirectory, relativeTypesPath);
7584
opsGenSpinner.succeed(constants.INFO_MESSAGE_OPS_GEN_SUCCESS + path.relative(path.resolve('.'), opsGenDirectory));
7685
}
7786
} finally {
@@ -80,13 +89,13 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou
8089
}
8190
}
8291

83-
async function writeGeneratedDocuments(language, generatedStatements, outputPath) {
92+
async function writeGeneratedDocuments(language, generatedStatements, outputPath, relativeTypesPath) {
8493
const fileExtension = FILE_EXTENSION_MAP[language];
8594

8695
['queries', 'mutations', 'subscriptions'].forEach(op => {
8796
const ops = generatedStatements[op];
8897
if (ops && ops.size) {
89-
const formattedStatements = (new GraphQLStatementsFormatter(language)).format(ops);
98+
const formattedStatements = new GraphQLStatementsFormatter(language, op, relativeTypesPath).format(ops);
9099
const outputFile = path.resolve(path.join(outputPath, `${op}.${fileExtension}`));
91100
fs.writeFileSync(outputFile, formattedStatements);
92101
}
@@ -96,7 +105,7 @@ async function writeGeneratedDocuments(language, generatedStatements, outputPath
96105
// External Fragments are rendered only for GraphQL targets
97106
const fragments = generatedStatements['fragments'];
98107
if (fragments.size) {
99-
const formattedStatements = (new GraphQLStatementsFormatter(language)).format(fragments);
108+
const formattedStatements = new GraphQLStatementsFormatter(language).format(fragments);
100109
const outputFile = path.resolve(path.join(outputPath, `fragments.${fileExtension}`));
101110
fs.writeFileSync(outputFile, formattedStatements);
102111
}
@@ -109,6 +118,6 @@ const FILE_EXTENSION_MAP = {
109118
flow: 'js',
110119
typescript: 'ts',
111120
angular: 'graphql',
112-
}
121+
};
113122

114123
module.exports = generateStatements;

packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
const prettier = require('prettier');
2+
const {
3+
interfaceNameFromOperation,
4+
interfaceVariablesNameFromOperation,
5+
} = require('@aws-amplify/graphql-types-generator/lib/typescript/codeGeneration');
26

37
const CODEGEN_WARNING = 'this is an auto generated file. This will be overwritten';
48
const LINE_DELIMITOR = '\n';
@@ -7,10 +11,28 @@ const LINE_DELIMITOR = '\n';
711
* Utility class to format the generated GraphQL statements based on frontend language type
812
*/
913
class GraphQLStatementsFormatter {
10-
constructor(language) {
14+
constructor(language, op, typesPath) {
1115
this.language = language || 'graphql';
16+
this.opTypeName = {
17+
queries: 'Query',
18+
mutations: 'Mutation',
19+
subscriptions: 'Subscription',
20+
}[op];
1221
this.lintOverrides = [];
1322
this.headerComments = [];
23+
this.typesPath = typesPath ? typesPath.replace(/.ts/i, '') : null;
24+
this.includeTypeScriptTypes = this.language === 'typescript' && this.opTypeName && this.typesPath;
25+
}
26+
27+
get typeDefs() {
28+
if (!this.includeTypeScriptTypes) return '';
29+
return [
30+
`import * as APITypes from '${this.typesPath}';`,
31+
`type Generated${this.opTypeName}<InputType, OutputType> = string & {`,
32+
` __generated${this.opTypeName}Input: InputType;`,
33+
` __generated${this.opTypeName}Output: OutputType;`,
34+
`};`,
35+
].join(LINE_DELIMITOR);
1436
}
1537

1638
format(statements) {
@@ -21,10 +43,7 @@ class GraphQLStatementsFormatter {
2143
return this.prettify(this.formatJS(statements));
2244
case 'typescript':
2345
this.headerComments.push(CODEGEN_WARNING);
24-
this.lintOverrides.push(...[
25-
'/* tslint:disable */',
26-
'/* eslint-disable */'
27-
]);
46+
this.lintOverrides.push(...['/* tslint:disable */', '/* eslint-disable */']);
2847
return this.prettify(this.formatJS(statements));
2948
case 'flow':
3049
this.headerComments.push('@flow', CODEGEN_WARNING);
@@ -36,27 +55,38 @@ class GraphQLStatementsFormatter {
3655
}
3756

3857
formatGraphQL(statements) {
39-
const headerBuffer = this.headerComments.map( comment => `# ${comment}`).join(LINE_DELIMITOR);
40-
const statementsBuffer = statements ? [...statements.values()].join(LINE_DELIMITOR) : '';
58+
const headerBuffer = this.headerComments.map(comment => `# ${comment}`).join(LINE_DELIMITOR);
59+
const statementsBuffer = statements ? [...statements.values()].map(s => s.graphql).join(LINE_DELIMITOR) : '';
4160
const formattedOutput = [headerBuffer, LINE_DELIMITOR, statementsBuffer].join(LINE_DELIMITOR);
4261
return formattedOutput;
4362
}
4463

4564
formatJS(statements) {
4665
const lintOverridesBuffer = this.lintOverrides.join(LINE_DELIMITOR);
47-
const headerBuffer = this.headerComments.map( comment => `// ${comment}`).join(LINE_DELIMITOR);
66+
const headerBuffer = this.headerComments.map(comment => `// ${comment}`).join(LINE_DELIMITOR);
4867
const formattedStatements = [];
4968
if (statements) {
50-
for (const [key, value] of statements) {
51-
formattedStatements.push(
52-
`export const ${key} = /* GraphQL */ \`${value}\``
53-
);
69+
for (const [key, { graphql, operationName, operationType }] of statements) {
70+
const typeTag = this.buildTypeTag(operationName, operationType);
71+
const formattedGraphQL = prettier.format(graphql, { parser: 'graphql' });
72+
formattedStatements.push(`export const ${key} = /* GraphQL */ \`${formattedGraphQL}\`${typeTag}`);
5473
}
5574
}
56-
const formattedOutput = [lintOverridesBuffer, headerBuffer, LINE_DELIMITOR, ...formattedStatements].join(LINE_DELIMITOR);
75+
const typeDefs = this.includeTypeScriptTypes ? [LINE_DELIMITOR, this.typeDefs] : [];
76+
const formattedOutput = [lintOverridesBuffer, headerBuffer, ...typeDefs, LINE_DELIMITOR, ...formattedStatements].join(LINE_DELIMITOR);
5777
return formattedOutput;
5878
}
5979

80+
buildTypeTag(operationName, operationType) {
81+
if (!this.includeTypeScriptTypes) return '';
82+
83+
const operationDef = { operationName, operationType };
84+
const resultTypeName = `APITypes.${interfaceNameFromOperation(operationDef)}`;
85+
const variablesTypeName = `APITypes.${interfaceVariablesNameFromOperation(operationDef)}`;
86+
87+
return ` as Generated${this.opTypeName}<${variablesTypeName}, ${resultTypeName}>;`;
88+
}
89+
6090
prettify(output) {
6191
const parserMap = {
6292
javascript: 'babel',
Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
const { GraphQLStatementsFormatter } = require('../../src/utils');
1+
const { GraphQLStatementsFormatter } = require('../../src/utils');
22

33
describe('GraphQL statements Formatter', () => {
44
const statements = new Map();
5-
statements.set('getTodo', `
5+
6+
const graphql = `
67
query GetProject($id: ID!) {
78
getProject(id: $id) {
89
id
@@ -11,31 +12,38 @@ describe('GraphQL statements Formatter', () => {
1112
updatedAt
1213
}
1314
}
14-
`);
15+
`;
16+
17+
statements.set('getProject', {
18+
graphql,
19+
operationName: 'GetProject',
20+
operationType: 'query',
21+
fieldName: 'getProject',
22+
});
1523

1624
it('Generates formatted output for JS frontend', () => {
17-
const formattedOutput = (new GraphQLStatementsFormatter('javascript')).format(statements);
25+
const formattedOutput = new GraphQLStatementsFormatter('javascript').format(statements);
1826
expect(formattedOutput).toMatchSnapshot();
1927
});
2028

2129
it('Generates formatted output for TS frontend', () => {
22-
const formattedOutput = (new GraphQLStatementsFormatter('typescript')).format(statements);
30+
const formattedOutput = new GraphQLStatementsFormatter('typescript', 'queries', '../API.ts').format(statements);
2331
expect(formattedOutput).toMatchSnapshot();
2432
});
2533

2634
it('Generates formatted output for Flow frontend', () => {
27-
const formattedOutput = (new GraphQLStatementsFormatter('flow')).format(statements);
35+
const formattedOutput = new GraphQLStatementsFormatter('flow').format(statements);
2836
expect(formattedOutput).toMatchSnapshot();
2937
});
3038

3139
it('Generates formatted output for Angular frontend', () => {
32-
const formattedOutput = (new GraphQLStatementsFormatter('angular')).format(statements);
40+
const formattedOutput = new GraphQLStatementsFormatter('angular').format(statements);
3341
// Note that for Angular, we generate in GraphQL language itself.
3442
expect(formattedOutput).toMatchSnapshot();
3543
});
3644

3745
it('Generates formatted output for GraphQL language', () => {
38-
const formattedOutput = (new GraphQLStatementsFormatter('graphql')).format(statements);
46+
const formattedOutput = new GraphQLStatementsFormatter('graphql').format(statements);
3947
expect(formattedOutput).toMatchSnapshot();
4048
});
4149
});

packages/amplify-codegen/tests/utils/__snapshots__/GraphQLStatementsFormatter.test.js.snap

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ exports[`GraphQL statements Formatter Generates formatted output for Flow fronte
1818
"// @flow
1919
// this is an auto generated file. This will be overwritten
2020
21-
export const getTodo = /* GraphQL */ \`
21+
export const getProject = /* GraphQL */ \`
2222
query GetProject($id: ID!) {
2323
getProject(id: $id) {
2424
id
@@ -49,7 +49,7 @@ exports[`GraphQL statements Formatter Generates formatted output for JS frontend
4949
"/* eslint-disable */
5050
// this is an auto generated file. This will be overwritten
5151
52-
export const getTodo = /* GraphQL */ \`
52+
export const getProject = /* GraphQL */ \`
5353
query GetProject($id: ID!) {
5454
getProject(id: $id) {
5555
id
@@ -67,15 +67,23 @@ exports[`GraphQL statements Formatter Generates formatted output for TS frontend
6767
/* eslint-disable */
6868
// this is an auto generated file. This will be overwritten
6969
70-
export const getTodo = /* GraphQL */ \`
71-
query GetProject($id: ID!) {
72-
getProject(id: $id) {
73-
id
74-
name
75-
createdAt
76-
updatedAt
77-
}
70+
import * as APITypes from \\"../API\\";
71+
type GeneratedQuery<InputType, OutputType> = string & {
72+
__generatedQueryInput: InputType;
73+
__generatedQueryOutput: OutputType;
74+
};
75+
76+
export const getProject = /* GraphQL */ \`query GetProject($id: ID!) {
77+
getProject(id: $id) {
78+
id
79+
name
80+
createdAt
81+
updatedAt
7882
}
79-
\`;
83+
}
84+
\` as GeneratedQuery<
85+
APITypes.GetProjectQueryVariables,
86+
APITypes.GetProjectQuery
87+
>;
8088
"
8189
`;

packages/graphql-docs-generator/API.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import { GraphQLSchema } from 'graphql';
1010
export function buildSchema(schema: string): GraphQLSchema;
1111

1212
// Warning: (ae-forgotten-export) The symbol "GeneratedOperations" needs to be exported by the entry point index.d.ts
13+
// Warning: (ae-forgotten-export) The symbol "MapValueType" needs to be exported by the entry point index.d.ts
1314
//
1415
// @public (undocumented)
15-
export function generateGraphQLDocuments(schema: string, options: {
16+
export function generateGraphQLDocuments<INCLUDE_META extends boolean>(schema: string, options: {
1617
maxDepth?: number;
1718
useExternalFragmentForS3Object?: boolean;
1819
typenameIntrospection?: boolean;
19-
}): GeneratedOperations;
20+
includeMetaData?: INCLUDE_META;
21+
}): GeneratedOperations<MapValueType<INCLUDE_META>>;
2022

2123
// (No @packageDocumentation comment for this package)
2224

0 commit comments

Comments
 (0)