Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add type branding to graphql in TS outputs #623

Merged
merged 9 commits into from
Aug 23, 2023
29 changes: 19 additions & 10 deletions packages/amplify-codegen/src/commands/statements.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ const Ora = require('ora');

const { loadConfig } = require('../codegen-config');
const constants = require('../constants');
const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails, readSchemaFromFile, GraphQLStatementsFormatter } = require('../utils');
const {
ensureIntrospectionSchema,
getFrontEndHandler,
getAppSyncAPIDetails,
readSchemaFromFile,
GraphQLStatementsFormatter,
} = require('../utils');
const { generateGraphQLDocuments } = require('@aws-amplify/graphql-docs-generator');

async function generateStatements(context, forceDownloadSchema, maxDepth, withoutInit = false, decoupleFrontend = '') {
Expand Down Expand Up @@ -62,16 +68,19 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou
const schemaData = readSchemaFromFile(schemaPath);
const generatedOps = generateGraphQLDocuments(schemaData, {
maxDepth: maxDepth || cfg.amplifyExtension.maxDepth,
useExternalFragmentForS3Object: (language === 'graphql'),
useExternalFragmentForS3Object: language === 'graphql',
// default typenameIntrospection to true when not set
typenameIntrospection:
cfg.amplifyExtension.typenameIntrospection === undefined ? true : !!cfg.amplifyExtension.typenameIntrospection,
includeMetaData: true,
});
if(!generatedOps) {
if (!generatedOps) {
context.print.warning('No GraphQL statements are generated. Check if the introspection schema has GraphQL operations defined.');
}
else {
await writeGeneratedDocuments(language, generatedOps, opsGenDirectory);
} else {
const relativeTypesPath = cfg.amplifyExtension.generatedFileName
? path.relative(opsGenDirectory, cfg.amplifyExtension.generatedFileName)
: null;
await writeGeneratedDocuments(language, generatedOps, opsGenDirectory, relativeTypesPath);
opsGenSpinner.succeed(constants.INFO_MESSAGE_OPS_GEN_SUCCESS + path.relative(path.resolve('.'), opsGenDirectory));
}
} finally {
Expand All @@ -80,13 +89,13 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou
}
}

async function writeGeneratedDocuments(language, generatedStatements, outputPath) {
async function writeGeneratedDocuments(language, generatedStatements, outputPath, relativeTypesPath) {
const fileExtension = FILE_EXTENSION_MAP[language];

['queries', 'mutations', 'subscriptions'].forEach(op => {
const ops = generatedStatements[op];
if (ops && ops.size) {
const formattedStatements = (new GraphQLStatementsFormatter(language)).format(ops);
const formattedStatements = new GraphQLStatementsFormatter(language, op, relativeTypesPath).format(ops);
const outputFile = path.resolve(path.join(outputPath, `${op}.${fileExtension}`));
fs.writeFileSync(outputFile, formattedStatements);
}
Expand All @@ -96,7 +105,7 @@ async function writeGeneratedDocuments(language, generatedStatements, outputPath
// External Fragments are rendered only for GraphQL targets
const fragments = generatedStatements['fragments'];
if (fragments.size) {
const formattedStatements = (new GraphQLStatementsFormatter(language)).format(fragments);
const formattedStatements = new GraphQLStatementsFormatter(language).format(fragments);
const outputFile = path.resolve(path.join(outputPath, `fragments.${fileExtension}`));
fs.writeFileSync(outputFile, formattedStatements);
}
Expand All @@ -109,6 +118,6 @@ const FILE_EXTENSION_MAP = {
flow: 'js',
typescript: 'ts',
angular: 'graphql',
}
};

module.exports = generateStatements;
56 changes: 43 additions & 13 deletions packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
const prettier = require('prettier');
const {
interfaceNameFromOperation,
interfaceVariablesNameFromOperation,
} = require('@aws-amplify/graphql-types-generator/lib/typescript/codeGeneration');

const CODEGEN_WARNING = 'this is an auto generated file. This will be overwritten';
const LINE_DELIMITOR = '\n';
Expand All @@ -7,10 +11,28 @@ const LINE_DELIMITOR = '\n';
* Utility class to format the generated GraphQL statements based on frontend language type
*/
class GraphQLStatementsFormatter {
constructor(language) {
constructor(language, op, typesPath) {
this.language = language || 'graphql';
this.opTypeName = {
queries: 'Query',
mutations: 'Mutation',
subscriptions: 'Subscription',
}[op];
this.lintOverrides = [];
this.headerComments = [];
this.typesPath = typesPath ? typesPath.replace(/.ts/i, '') : null;
this.includeTypeScriptTypes = this.language === 'typescript' && this.opTypeName && this.typesPath;
}

get typeDefs() {
if (!this.includeTypeScriptTypes) return '';
return [
`import * as APITypes from '${this.typesPath}';`,
`type Generated${this.opTypeName}<InputType, OutputType> = string & {`,
` __generated${this.opTypeName}Input: InputType;`,
` __generated${this.opTypeName}Output: OutputType;`,
`};`,
].join(LINE_DELIMITOR);
}

format(statements) {
Expand All @@ -21,10 +43,7 @@ class GraphQLStatementsFormatter {
return this.prettify(this.formatJS(statements));
case 'typescript':
this.headerComments.push(CODEGEN_WARNING);
this.lintOverrides.push(...[
'/* tslint:disable */',
'/* eslint-disable */'
]);
this.lintOverrides.push(...['/* tslint:disable */', '/* eslint-disable */']);
return this.prettify(this.formatJS(statements));
case 'flow':
this.headerComments.push('@flow', CODEGEN_WARNING);
Expand All @@ -36,27 +55,38 @@ class GraphQLStatementsFormatter {
}

formatGraphQL(statements) {
const headerBuffer = this.headerComments.map( comment => `# ${comment}`).join(LINE_DELIMITOR);
const statementsBuffer = statements ? [...statements.values()].join(LINE_DELIMITOR) : '';
const headerBuffer = this.headerComments.map(comment => `# ${comment}`).join(LINE_DELIMITOR);
const statementsBuffer = statements ? [...statements.values()].map(s => s.graphql).join(LINE_DELIMITOR) : '';
const formattedOutput = [headerBuffer, LINE_DELIMITOR, statementsBuffer].join(LINE_DELIMITOR);
return formattedOutput;
}

formatJS(statements) {
const lintOverridesBuffer = this.lintOverrides.join(LINE_DELIMITOR);
const headerBuffer = this.headerComments.map( comment => `// ${comment}`).join(LINE_DELIMITOR);
const headerBuffer = this.headerComments.map(comment => `// ${comment}`).join(LINE_DELIMITOR);
const formattedStatements = [];
if (statements) {
for (const [key, value] of statements) {
formattedStatements.push(
`export const ${key} = /* GraphQL */ \`${value}\``
);
for (const [key, { graphql, operationName, operationType }] of statements) {
const typeTag = this.buildTypeTag(operationName, operationType);
const formattedGraphQL = prettier.format(graphql, { parser: 'graphql' });
formattedStatements.push(`export const ${key} = /* GraphQL */ \`${formattedGraphQL}\`${typeTag}`);
}
}
const formattedOutput = [lintOverridesBuffer, headerBuffer, LINE_DELIMITOR, ...formattedStatements].join(LINE_DELIMITOR);
const typeDefs = this.includeTypeScriptTypes ? [LINE_DELIMITOR, this.typeDefs] : [];
const formattedOutput = [lintOverridesBuffer, headerBuffer, ...typeDefs, LINE_DELIMITOR, ...formattedStatements].join(LINE_DELIMITOR);
return formattedOutput;
}

buildTypeTag(operationName, operationType) {
if (!this.includeTypeScriptTypes) return '';

const operationDef = { operationName, operationType };
const resultTypeName = `APITypes.${interfaceNameFromOperation(operationDef)}`;
const variablesTypeName = `APITypes.${interfaceVariablesNameFromOperation(operationDef)}`;

return ` as Generated${this.opTypeName}<${variablesTypeName}, ${resultTypeName}>;`;
}

prettify(output) {
const parserMap = {
javascript: 'babel',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const { GraphQLStatementsFormatter } = require('../../src/utils');
const { GraphQLStatementsFormatter } = require('../../src/utils');

describe('GraphQL statements Formatter', () => {
const statements = new Map();
statements.set('getTodo', `

const graphql = `
query GetProject($id: ID!) {
getProject(id: $id) {
id
Expand All @@ -11,31 +12,38 @@ describe('GraphQL statements Formatter', () => {
updatedAt
}
}
`);
`;

statements.set('getProject', {
graphql,
operationName: 'GetProject',
operationType: 'query',
fieldName: 'getProject',
});

it('Generates formatted output for JS frontend', () => {
const formattedOutput = (new GraphQLStatementsFormatter('javascript')).format(statements);
const formattedOutput = new GraphQLStatementsFormatter('javascript').format(statements);
expect(formattedOutput).toMatchSnapshot();
});

it('Generates formatted output for TS frontend', () => {
const formattedOutput = (new GraphQLStatementsFormatter('typescript')).format(statements);
const formattedOutput = new GraphQLStatementsFormatter('typescript', 'queries', '../API.ts').format(statements);
expect(formattedOutput).toMatchSnapshot();
});

it('Generates formatted output for Flow frontend', () => {
const formattedOutput = (new GraphQLStatementsFormatter('flow')).format(statements);
const formattedOutput = new GraphQLStatementsFormatter('flow').format(statements);
expect(formattedOutput).toMatchSnapshot();
});

it('Generates formatted output for Angular frontend', () => {
const formattedOutput = (new GraphQLStatementsFormatter('angular')).format(statements);
const formattedOutput = new GraphQLStatementsFormatter('angular').format(statements);
// Note that for Angular, we generate in GraphQL language itself.
expect(formattedOutput).toMatchSnapshot();
});

it('Generates formatted output for GraphQL language', () => {
const formattedOutput = (new GraphQLStatementsFormatter('graphql')).format(statements);
const formattedOutput = new GraphQLStatementsFormatter('graphql').format(statements);
expect(formattedOutput).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ exports[`GraphQL statements Formatter Generates formatted output for Flow fronte
"// @flow
// this is an auto generated file. This will be overwritten

export const getTodo = /* GraphQL */ \`
export const getProject = /* GraphQL */ \`
query GetProject($id: ID!) {
getProject(id: $id) {
id
Expand Down Expand Up @@ -49,7 +49,7 @@ exports[`GraphQL statements Formatter Generates formatted output for JS frontend
"/* eslint-disable */
// this is an auto generated file. This will be overwritten

export const getTodo = /* GraphQL */ \`
export const getProject = /* GraphQL */ \`
query GetProject($id: ID!) {
getProject(id: $id) {
id
Expand All @@ -67,15 +67,23 @@ exports[`GraphQL statements Formatter Generates formatted output for TS frontend
/* eslint-disable */
// this is an auto generated file. This will be overwritten

export const getTodo = /* GraphQL */ \`
query GetProject($id: ID!) {
getProject(id: $id) {
id
name
createdAt
updatedAt
}
import * as APITypes from \\"../API\\";
type GeneratedQuery<InputType, OutputType> = string & {
__generatedQueryInput: InputType;
__generatedQueryOutput: OutputType;
};

export const getProject = /* GraphQL */ \`query GetProject($id: ID!) {
getProject(id: $id) {
id
name
createdAt
updatedAt
}
\`;
}
\` as GeneratedQuery<
APITypes.GetProjectQueryVariables,
APITypes.GetProjectQuery
>;
"
`;
6 changes: 4 additions & 2 deletions packages/graphql-docs-generator/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import { GraphQLSchema } from 'graphql';
export function buildSchema(schema: string): GraphQLSchema;

// Warning: (ae-forgotten-export) The symbol "GeneratedOperations" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "MapValueType" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export function generateGraphQLDocuments(schema: string, options: {
export function generateGraphQLDocuments<INCLUDE_META extends boolean>(schema: string, options: {
maxDepth?: number;
useExternalFragmentForS3Object?: boolean;
typenameIntrospection?: boolean;
}): GeneratedOperations;
includeMetaData?: INCLUDE_META;
}): GeneratedOperations<MapValueType<INCLUDE_META>>;

// (No @packageDocumentation comment for this package)

Expand Down
Loading