diff --git a/packages/amplify-codegen/src/commands/statements.js b/packages/amplify-codegen/src/commands/statements.js index d06e984f..4fc34319 100644 --- a/packages/amplify-codegen/src/commands/statements.js +++ b/packages/amplify-codegen/src/commands/statements.js @@ -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 = '') { @@ -62,15 +68,14 @@ 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, }); - if(!generatedOps) { + if (!generatedOps) { context.print.warning('No GraphQL statements are generated. Check if the introspection schema has GraphQL operations defined.'); - } - else { + } else { await writeGeneratedDocuments(language, generatedOps, opsGenDirectory); opsGenSpinner.succeed(constants.INFO_MESSAGE_OPS_GEN_SUCCESS + path.relative(path.resolve('.'), opsGenDirectory)); } @@ -86,7 +91,7 @@ async function writeGeneratedDocuments(language, generatedStatements, outputPath ['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).format(ops); const outputFile = path.resolve(path.join(outputPath, `${op}.${fileExtension}`)); fs.writeFileSync(outputFile, formattedStatements); } @@ -96,7 +101,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); } @@ -109,6 +114,6 @@ const FILE_EXTENSION_MAP = { flow: 'js', typescript: 'ts', angular: 'graphql', -} +}; module.exports = generateStatements; diff --git a/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js b/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js index f9a74322..ebfda889 100644 --- a/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js +++ b/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js @@ -7,12 +7,30 @@ const LINE_DELIMITOR = '\n'; * Utility class to format the generated GraphQL statements based on frontend language type */ class GraphQLStatementsFormatter { - constructor(language) { + constructor(language, op) { this.language = language || 'graphql'; + this.opTypeName = { + queries: 'Query', + mutations: 'Mutation', + subscriptions: 'Subscription', + }[op]; this.lintOverrides = []; this.headerComments = []; } + get typeDefs() { + if (this.language === 'typescript' && this.opTypeName) { + return [ + `import * as APITypes from '../API';`, + `type Generated${this.opTypeName} = string & {`, + ` __generated${this.opTypeName}Input: InputType;`, + ` __generated${this.opTypeName}Output: OutputType;`, + `};`, + ].join(LINE_DELIMITOR); + } + return ''; + } + format(statements) { switch (this.language) { case 'javascript': @@ -21,10 +39,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); @@ -36,7 +51,7 @@ class GraphQLStatementsFormatter { } formatGraphQL(statements) { - const headerBuffer = this.headerComments.map( comment => `# ${comment}`).join(LINE_DELIMITOR); + const headerBuffer = this.headerComments.map(comment => `# ${comment}`).join(LINE_DELIMITOR); const statementsBuffer = statements ? [...statements.values()].join(LINE_DELIMITOR) : ''; const formattedOutput = [headerBuffer, LINE_DELIMITOR, statementsBuffer].join(LINE_DELIMITOR); return formattedOutput; @@ -44,19 +59,34 @@ class GraphQLStatementsFormatter { 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}\`` - ); + const typeTag = this.buildTypeTag(key); + formattedStatements.push(`export const ${key} = /* GraphQL */ \`${value}\`${typeTag}`); } } - const formattedOutput = [lintOverridesBuffer, headerBuffer, LINE_DELIMITOR, ...formattedStatements].join(LINE_DELIMITOR); + const formattedOutput = [lintOverridesBuffer, headerBuffer, LINE_DELIMITOR, this.typeDefs, LINE_DELIMITOR, ...formattedStatements].join( + LINE_DELIMITOR, + ); return formattedOutput; } + buildTypeTag(name) { + if (!this.opTypeName) return ''; + if (this.language !== 'typescript') return ''; + + const titleCasedName = `${name[0].toUpperCase()}${name.slice(1)}`; + const variablesTypeName = `APITypes.${titleCasedName}${this.opTypeName}Variables`; + const resultTypeName = `APITypes.${titleCasedName}${this.opTypeName}`; + + return ` as Generated${this.opTypeName}< + ${variablesTypeName}, + ${resultTypeName} + >;`; + } + prettify(output) { const parserMap = { javascript: 'babel', diff --git a/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js b/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js index 4c03d684..faae69b6 100644 --- a/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js +++ b/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js @@ -1,8 +1,10 @@ -const { GraphQLStatementsFormatter } = require('../../src/utils'); +const { GraphQLStatementsFormatter } = require('../../src/utils'); describe('GraphQL statements Formatter', () => { const statements = new Map(); - statements.set('getTodo', ` + statements.set( + 'getTodo', + ` query GetProject($id: ID!) { getProject(id: $id) { id @@ -11,31 +13,33 @@ describe('GraphQL statements Formatter', () => { updatedAt } } - `); + `, + ); 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); + console.log({ statements }); + const formattedOutput = new GraphQLStatementsFormatter('typescript', 'queries').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(); }); }); diff --git a/packages/amplify-codegen/tests/utils/__snapshots__/GraphQLStatementsFormatter.test.js.snap b/packages/amplify-codegen/tests/utils/__snapshots__/GraphQLStatementsFormatter.test.js.snap index 4089216c..da126f95 100644 --- a/packages/amplify-codegen/tests/utils/__snapshots__/GraphQLStatementsFormatter.test.js.snap +++ b/packages/amplify-codegen/tests/utils/__snapshots__/GraphQLStatementsFormatter.test.js.snap @@ -67,15 +67,21 @@ exports[`GraphQL statements Formatter Generates formatted output for TS frontend /* eslint-disable */ // this is an auto generated file. This will be overwritten +import * as APITypes from \\"../API\\"; +type GeneratedQuery = string & { + __generatedQueryInput: InputType; + __generatedQueryOutput: OutputType; +}; + export const getTodo = /* GraphQL */ \` - query GetProject($id: ID!) { - getProject(id: $id) { - id - name - createdAt - updatedAt + query GetProject($id: ID!) { + getProject(id: $id) { + id + name + createdAt + updatedAt + } } - } -\`; + \` as GeneratedQuery; " `; diff --git a/packages/graphql-docs-generator/src/index.ts b/packages/graphql-docs-generator/src/index.ts index ea92edb0..fccf44ac 100644 --- a/packages/graphql-docs-generator/src/index.ts +++ b/packages/graphql-docs-generator/src/index.ts @@ -6,7 +6,7 @@ export { buildSchema } from './generator/utils/loading'; export function generateGraphQLDocuments( schema: string, - options: { maxDepth?: number, useExternalFragmentForS3Object?: boolean; typenameIntrospection?: boolean }, + options: { maxDepth?: number; useExternalFragmentForS3Object?: boolean; typenameIntrospection?: boolean }, ): GeneratedOperations { const opts = { maxDepth: 2, @@ -19,7 +19,7 @@ export function generateGraphQLDocuments( const gqlOperations: GQLAllOperations = generateAllOps(extendedSchema, opts.maxDepth, { useExternalFragmentForS3Object: opts.useExternalFragmentForS3Object, - typenameIntrospection: opts.typenameIntrospection + typenameIntrospection: opts.typenameIntrospection, }); registerPartials(); registerHelpers(); @@ -28,7 +28,7 @@ export function generateGraphQLDocuments( queries: new Map(), mutations: new Map(), subscriptions: new Map(), - fragments: new Map() + fragments: new Map(), }; ['queries', 'mutations', 'subscriptions'].forEach(op => { @@ -52,7 +52,7 @@ type GeneratedOperations = { mutations: Map; subscriptions: Map; fragments: Map; -} +}; function renderOperations(operations: Array): Map { const renderedOperations = new Map(); @@ -68,6 +68,8 @@ function renderOperations(operations: Array): Map } function renderOperation(operation: GQLTemplateOp): string { + // TODO: cleanup + // console.log('rendering operation', operation); const templateStr = getOperationPartial(); const template = handlebars.compile(templateStr, { noEscape: true, @@ -77,11 +79,13 @@ function renderOperation(operation: GQLTemplateOp): string { } function renderFragments(fragments: Array, useExternalFragmentForS3Object: boolean): Map { + // TODO: does it make sense to ferry operation details through in this map + // so that TypeScript downstream can map queries to types more safely? const renderedFragments = new Map(); if (fragments?.length) { fragments.forEach(fragment => { const name = fragment.name; - const gql = renderFragment(fragment,useExternalFragmentForS3Object ); + const gql = renderFragment(fragment, useExternalFragmentForS3Object); renderedFragments.set(name, gql); }); }