From 0314257059a40c7dc24c74a4854475272665d677 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Wed, 12 Jul 2023 12:50:32 -0500 Subject: [PATCH 1/8] feat: add types to graphql in TS outputs --- .../src/commands/statements.js | 21 +++++--- .../src/utils/GraphQLStatementsFormatter.js | 52 +++++++++++++++---- .../utils/GraphQLStatementsFormatter.test.js | 20 ++++--- .../GraphQLStatementsFormatter.test.js.snap | 22 +++++--- packages/graphql-docs-generator/src/index.ts | 14 +++-- 5 files changed, 89 insertions(+), 40 deletions(-) diff --git a/packages/amplify-codegen/src/commands/statements.js b/packages/amplify-codegen/src/commands/statements.js index d06e984f9..4fc343191 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 f9a74322d..ebfda889a 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 4c03d684d..faae69b6a 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 4089216cf..da126f95b 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 ea92edb02..fccf44acd 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); }); } From 1c37267c89b84c1c28aca56448e98da6132077a0 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Wed, 12 Jul 2023 17:21:47 -0500 Subject: [PATCH 2/8] wip: alt type tagged not inferring from fieldName --- .../src/commands/statements.js | 1 + .../src/utils/GraphQLStatementsFormatter.js | 35 +++++--- .../utils/GraphQLStatementsFormatter.test.js | 16 +++- .../GraphQLStatementsFormatter.test.js.snap | 11 ++- packages/graphql-docs-generator/src/index.ts | 80 +++++++++++++++---- .../src/typescript/codeGeneration.ts | 6 +- 6 files changed, 117 insertions(+), 32 deletions(-) diff --git a/packages/amplify-codegen/src/commands/statements.js b/packages/amplify-codegen/src/commands/statements.js index 4fc343191..7366647f5 100644 --- a/packages/amplify-codegen/src/commands/statements.js +++ b/packages/amplify-codegen/src/commands/statements.js @@ -72,6 +72,7 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou // default typenameIntrospection to true when not set typenameIntrospection: cfg.amplifyExtension.typenameIntrospection === undefined ? true : !!cfg.amplifyExtension.typenameIntrospection, + includeMetaData: true }); if (!generatedOps) { context.print.warning('No GraphQL statements are generated. Check if the introspection schema has GraphQL operations defined.'); diff --git a/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js b/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js index ebfda889a..775dd4c3e 100644 --- a/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js +++ b/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js @@ -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'; @@ -52,7 +56,7 @@ class GraphQLStatementsFormatter { formatGraphQL(statements) { const headerBuffer = this.headerComments.map(comment => `# ${comment}`).join(LINE_DELIMITOR); - const statementsBuffer = statements ? [...statements.values()].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; } @@ -62,9 +66,10 @@ class GraphQLStatementsFormatter { const headerBuffer = this.headerComments.map(comment => `// ${comment}`).join(LINE_DELIMITOR); const formattedStatements = []; if (statements) { - for (const [key, value] of statements) { - const typeTag = this.buildTypeTag(key); - formattedStatements.push(`export const ${key} = /* GraphQL */ \`${value}\`${typeTag}`); + console.log('STATEMENTS', { statements }); + for (const [key, {graphql, operationName, operationType}] of statements) { + const typeTag = this.buildTypeTag(operationName, operationType); + formattedStatements.push(`export const ${key} = /* GraphQL */ \`${graphql}\`${typeTag}`); } } const formattedOutput = [lintOverridesBuffer, headerBuffer, LINE_DELIMITOR, this.typeDefs, LINE_DELIMITOR, ...formattedStatements].join( @@ -73,13 +78,25 @@ class GraphQLStatementsFormatter { return formattedOutput; } - buildTypeTag(name) { - if (!this.opTypeName) return ''; + buildTypeTag(operationName, operationType) { if (this.language !== 'typescript') return ''; + if (!operationType) return ''; - const titleCasedName = `${name[0].toUpperCase()}${name.slice(1)}`; - const variablesTypeName = `APITypes.${titleCasedName}${this.opTypeName}Variables`; - const resultTypeName = `APITypes.${titleCasedName}${this.opTypeName}`; + // const titleCasedName = `${name[0].toUpperCase()}${name.slice(1)}`; + + const resultTypeName = `APITypes.${interfaceNameFromOperation({ + operationName, + operationType, + // operationName: titleCasedName, + // operationType: this.opTypeName, + })}`; + + const variablesTypeName = `APITypes.${interfaceVariablesNameFromOperation({ + operationName, + operationType, + // operationName: titleCasedName, + // operationType: this.opTypeName, + })}`; return ` as Generated${this.opTypeName}< ${variablesTypeName}, diff --git a/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js b/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js index faae69b6a..c79b28073 100644 --- a/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js +++ b/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js @@ -2,9 +2,8 @@ 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 @@ -13,7 +12,16 @@ describe('GraphQL statements Formatter', () => { updatedAt } } - `, + `; + + statements.set( + 'getProject', + { + graphql, + operationName: 'GetProject', + operationType: 'query', + fieldName: 'getProject' + }, ); it('Generates formatted output for JS frontend', () => { 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 da126f95b..a3ef8ea09 100644 --- a/packages/amplify-codegen/tests/utils/__snapshots__/GraphQLStatementsFormatter.test.js.snap +++ b/packages/amplify-codegen/tests/utils/__snapshots__/GraphQLStatementsFormatter.test.js.snap @@ -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 @@ -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 @@ -73,7 +73,7 @@ type GeneratedQuery = string & { __generatedQueryOutput: OutputType; }; -export const getTodo = /* GraphQL */ \` +export const getProject = /* GraphQL */ \` query GetProject($id: ID!) { getProject(id: $id) { id @@ -82,6 +82,9 @@ export const getTodo = /* GraphQL */ \` updatedAt } } - \` as GeneratedQuery; + \` as GeneratedQuery< + APITypes.GetProjectQueryVariables, + APITypes.GetProjectQuery +>; " `; diff --git a/packages/graphql-docs-generator/src/index.ts b/packages/graphql-docs-generator/src/index.ts index fccf44acd..37bfeecb5 100644 --- a/packages/graphql-docs-generator/src/index.ts +++ b/packages/graphql-docs-generator/src/index.ts @@ -4,10 +4,15 @@ import { buildSchema } from './generator/utils/loading'; import { getTemplatePartials, getOperationPartial, getExternalFragmentPartial } from './generator/utils/templates'; export { buildSchema } from './generator/utils/loading'; -export function generateGraphQLDocuments( +export function generateGraphQLDocuments( schema: string, - options: { maxDepth?: number; useExternalFragmentForS3Object?: boolean; typenameIntrospection?: boolean }, -): GeneratedOperations { + options: { + maxDepth?: number; + useExternalFragmentForS3Object?: boolean; + typenameIntrospection?: boolean; + includeMetaData?: INCLUDE_META; + }, +): GeneratedOperations> { const opts = { maxDepth: 2, useExternalFragmentForS3Object: true, @@ -25,16 +30,17 @@ export function generateGraphQLDocuments( registerHelpers(); const allOperations = { - queries: new Map(), - mutations: new Map(), - subscriptions: new Map(), + queries: new Map>(), + mutations: new Map>(), + subscriptions: new Map>(), fragments: new Map(), }; ['queries', 'mutations', 'subscriptions'].forEach(op => { const ops = gqlOperations[op]; + console.log({ ops }); if (ops.length) { - const renderedOperations = renderOperations(gqlOperations[op]); + const renderedOperations = renderOperations(gqlOperations[op], options.includeMetaData); allOperations[op] = renderedOperations; } }); @@ -47,26 +53,72 @@ export function generateGraphQLDocuments( return allOperations; } -type GeneratedOperations = { - queries: Map; - mutations: Map; - subscriptions: Map; +type GraphQLWithMeta = { + /** + * The generated graphql string. + */ + graphql: string; + + /** + * E.g., `GetMyModel` or `ListMyModels`. + * + * This is used for generating type names. + * + * `undefined` for fragments. + */ + operationName: string | undefined; + + /** + * `undefined` for fragments. + */ + operationType: "query" | "mutation" | "subscription" | undefined; + + /** + * E.g., `getMyModel` or `listMyModels`. + * + * It's the name of the operation that lives under Queries, Mutations, or Subscriptions + * in the schema and is generally used as the key + variable name referring to the query. + */ + fieldName: string; +} + +type GeneratedOperations = { + queries: Map; + mutations: Map; + subscriptions: Map; fragments: Map; }; -function renderOperations(operations: Array): Map { - const renderedOperations = new Map(); +type MapValueType = INCLUDE_META extends true ? GraphQLWithMeta : string; + +function renderOperations< + INCLUDE_META extends boolean, +>(operations: Array, includeMetaData: INCLUDE_META): Map> { + const renderedOperations = new Map>(); if (operations?.length) { operations.forEach(op => { const name = op.fieldName || op.name; const gql = renderOperation(op); - renderedOperations.set(name, gql); + if (includeMetaData) { + renderedOperations.set(name, { + graphql: gql, + operationName: op.name, + operationType: op.type, + fieldName: op.fieldName + } as any); + } else { + renderedOperations.set(name, gql as any); + } }); } return renderedOperations; } +function isMetaIncluded(includeMetaData: boolean, operationsMap: any): operationsMap is GraphQLWithMeta { + return includeMetaData === true; +} + function renderOperation(operation: GQLTemplateOp): string { // TODO: cleanup // console.log('rendering operation', operation); diff --git a/packages/graphql-types-generator/src/typescript/codeGeneration.ts b/packages/graphql-types-generator/src/typescript/codeGeneration.ts index c6a751e9a..050576b34 100644 --- a/packages/graphql-types-generator/src/typescript/codeGeneration.ts +++ b/packages/graphql-types-generator/src/typescript/codeGeneration.ts @@ -165,6 +165,10 @@ export function interfaceNameFromOperation({ operationName, operationType }: { o } } +export function interfaceVariablesNameFromOperation({ operationName, operationType }: { operationName: string; operationType: string }) { + return `${interfaceNameFromOperation({ operationName, operationType })}Variables`; +} + export function interfaceVariablesDeclarationForOperation( generator: CodeGenerator, { operationName, operationType, variables }: LegacyOperation, @@ -172,7 +176,7 @@ export function interfaceVariablesDeclarationForOperation( if (!variables || variables.length < 1) { return; } - const interfaceName = `${interfaceNameFromOperation({ operationName, operationType })}Variables`; + const interfaceName = interfaceVariablesNameFromOperation({operationName, operationType}); interfaceDeclaration( generator, From 4d03b462395d3bacda45560f74dc984fe69198c9 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Thu, 13 Jul 2023 08:59:19 -0500 Subject: [PATCH 3/8] made types path computed from config --- .../src/commands/statements.js | 11 ++++++---- .../src/utils/GraphQLStatementsFormatter.js | 21 +++++++------------ .../utils/GraphQLStatementsFormatter.test.js | 18 +++++++--------- packages/graphql-docs-generator/src/index.ts | 21 ++++++++++--------- 4 files changed, 32 insertions(+), 39 deletions(-) diff --git a/packages/amplify-codegen/src/commands/statements.js b/packages/amplify-codegen/src/commands/statements.js index 7366647f5..28646d447 100644 --- a/packages/amplify-codegen/src/commands/statements.js +++ b/packages/amplify-codegen/src/commands/statements.js @@ -72,12 +72,15 @@ async function generateStatements(context, forceDownloadSchema, maxDepth, withou // default typenameIntrospection to true when not set typenameIntrospection: cfg.amplifyExtension.typenameIntrospection === undefined ? true : !!cfg.amplifyExtension.typenameIntrospection, - includeMetaData: true + includeMetaData: true, }); 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); + 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 { @@ -86,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, op).format(ops); + const formattedStatements = new GraphQLStatementsFormatter(language, op, relativeTypesPath).format(ops); const outputFile = path.resolve(path.join(outputPath, `${op}.${fileExtension}`)); fs.writeFileSync(outputFile, formattedStatements); } diff --git a/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js b/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js index 775dd4c3e..5578f65b4 100644 --- a/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js +++ b/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js @@ -1,7 +1,7 @@ const prettier = require('prettier'); const { interfaceNameFromOperation, - interfaceVariablesNameFromOperation + interfaceVariablesNameFromOperation, } = require('@aws-amplify/graphql-types-generator/lib/typescript/codeGeneration'); const CODEGEN_WARNING = 'this is an auto generated file. This will be overwritten'; @@ -11,7 +11,7 @@ const LINE_DELIMITOR = '\n'; * Utility class to format the generated GraphQL statements based on frontend language type */ class GraphQLStatementsFormatter { - constructor(language, op) { + constructor(language, op, typesPath) { this.language = language || 'graphql'; this.opTypeName = { queries: 'Query', @@ -20,12 +20,13 @@ class GraphQLStatementsFormatter { }[op]; this.lintOverrides = []; this.headerComments = []; + this.typesPath = typesPath ? typesPath.replace(/.ts/i, '') : null; } get typeDefs() { - if (this.language === 'typescript' && this.opTypeName) { + if (this.language === 'typescript' && this.opTypeName && this.typesPath) { return [ - `import * as APITypes from '../API';`, + `import * as APITypes from '${this.typesPath}';`, `type Generated${this.opTypeName} = string & {`, ` __generated${this.opTypeName}Input: InputType;`, ` __generated${this.opTypeName}Output: OutputType;`, @@ -66,8 +67,7 @@ class GraphQLStatementsFormatter { const headerBuffer = this.headerComments.map(comment => `// ${comment}`).join(LINE_DELIMITOR); const formattedStatements = []; if (statements) { - console.log('STATEMENTS', { statements }); - for (const [key, {graphql, operationName, operationType}] of statements) { + for (const [key, { graphql, operationName, operationType }] of statements) { const typeTag = this.buildTypeTag(operationName, operationType); formattedStatements.push(`export const ${key} = /* GraphQL */ \`${graphql}\`${typeTag}`); } @@ -79,23 +79,16 @@ class GraphQLStatementsFormatter { } buildTypeTag(operationName, operationType) { - if (this.language !== 'typescript') return ''; - if (!operationType) return ''; + if (!operationType || !this.typesPath || this.language !== 'typescript') return ''; - // const titleCasedName = `${name[0].toUpperCase()}${name.slice(1)}`; - const resultTypeName = `APITypes.${interfaceNameFromOperation({ operationName, operationType, - // operationName: titleCasedName, - // operationType: this.opTypeName, })}`; const variablesTypeName = `APITypes.${interfaceVariablesNameFromOperation({ operationName, operationType, - // operationName: titleCasedName, - // operationType: this.opTypeName, })}`; return ` as Generated${this.opTypeName}< diff --git a/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js b/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js index c79b28073..9fb25990b 100644 --- a/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js +++ b/packages/amplify-codegen/tests/utils/GraphQLStatementsFormatter.test.js @@ -14,15 +14,12 @@ describe('GraphQL statements Formatter', () => { } `; - statements.set( - 'getProject', - { - graphql, - operationName: 'GetProject', - operationType: 'query', - fieldName: 'getProject' - }, - ); + statements.set('getProject', { + graphql, + operationName: 'GetProject', + operationType: 'query', + fieldName: 'getProject', + }); it('Generates formatted output for JS frontend', () => { const formattedOutput = new GraphQLStatementsFormatter('javascript').format(statements); @@ -30,8 +27,7 @@ describe('GraphQL statements Formatter', () => { }); it('Generates formatted output for TS frontend', () => { - console.log({ statements }); - const formattedOutput = new GraphQLStatementsFormatter('typescript', 'queries').format(statements); + const formattedOutput = new GraphQLStatementsFormatter('typescript', 'queries', '../API.ts').format(statements); expect(formattedOutput).toMatchSnapshot(); }); diff --git a/packages/graphql-docs-generator/src/index.ts b/packages/graphql-docs-generator/src/index.ts index 37bfeecb5..c3bbf49b6 100644 --- a/packages/graphql-docs-generator/src/index.ts +++ b/packages/graphql-docs-generator/src/index.ts @@ -38,7 +38,7 @@ export function generateGraphQLDocuments( ['queries', 'mutations', 'subscriptions'].forEach(op => { const ops = gqlOperations[op]; - console.log({ ops }); + // console.log({ ops }); if (ops.length) { const renderedOperations = renderOperations(gqlOperations[op], options.includeMetaData); allOperations[op] = renderedOperations; @@ -61,9 +61,9 @@ type GraphQLWithMeta = { /** * E.g., `GetMyModel` or `ListMyModels`. - * + * * This is used for generating type names. - * + * * `undefined` for fragments. */ operationName: string | undefined; @@ -71,16 +71,16 @@ type GraphQLWithMeta = { /** * `undefined` for fragments. */ - operationType: "query" | "mutation" | "subscription" | undefined; + operationType: 'query' | 'mutation' | 'subscription' | undefined; /** * E.g., `getMyModel` or `listMyModels`. - * + * * It's the name of the operation that lives under Queries, Mutations, or Subscriptions * in the schema and is generally used as the key + variable name referring to the query. */ fieldName: string; -} +}; type GeneratedOperations = { queries: Map; @@ -91,9 +91,10 @@ type GeneratedOperations = { type MapValueType = INCLUDE_META extends true ? GraphQLWithMeta : string; -function renderOperations< - INCLUDE_META extends boolean, ->(operations: Array, includeMetaData: INCLUDE_META): Map> { +function renderOperations( + operations: Array, + includeMetaData: INCLUDE_META, +): Map> { const renderedOperations = new Map>(); if (operations?.length) { operations.forEach(op => { @@ -104,7 +105,7 @@ function renderOperations< graphql: gql, operationName: op.name, operationType: op.type, - fieldName: op.fieldName + fieldName: op.fieldName, } as any); } else { renderedOperations.set(name, gql as any); From f8c62ff1260d3ba40b02fcb6eb53e02b43f14d4a Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Thu, 13 Jul 2023 09:15:09 -0500 Subject: [PATCH 4/8] cruft cleanup --- packages/graphql-docs-generator/src/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/graphql-docs-generator/src/index.ts b/packages/graphql-docs-generator/src/index.ts index c3bbf49b6..eb066a3da 100644 --- a/packages/graphql-docs-generator/src/index.ts +++ b/packages/graphql-docs-generator/src/index.ts @@ -121,8 +121,6 @@ function isMetaIncluded(includeMetaData: boolean, operationsMap: any): operation } function renderOperation(operation: GQLTemplateOp): string { - // TODO: cleanup - // console.log('rendering operation', operation); const templateStr = getOperationPartial(); const template = handlebars.compile(templateStr, { noEscape: true, @@ -132,8 +130,6 @@ 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 => { From 4717e4e9d0b16805a17dea91dc0492c71bf53ded Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Thu, 13 Jul 2023 09:24:46 -0500 Subject: [PATCH 5/8] regenerated api.md --- packages/graphql-docs-generator/API.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/graphql-docs-generator/API.md b/packages/graphql-docs-generator/API.md index 908d8e074..d1d2e2d4c 100644 --- a/packages/graphql-docs-generator/API.md +++ b/packages/graphql-docs-generator/API.md @@ -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(schema: string, options: { maxDepth?: number; useExternalFragmentForS3Object?: boolean; typenameIntrospection?: boolean; -}): GeneratedOperations; + includeMetaData?: INCLUDE_META; +}): GeneratedOperations>; // (No @packageDocumentation comment for this package) From f6b7d9b96de4758ee2f9600c1ec8ee5f2d5e0461 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Tue, 1 Aug 2023 12:05:14 -0500 Subject: [PATCH 6/8] refactor: small helper prop refactor --- .../src/utils/GraphQLStatementsFormatter.js | 38 +++++++------------ packages/graphql-docs-generator/src/index.ts | 1 - 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js b/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js index 5578f65b4..e2efca911 100644 --- a/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js +++ b/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js @@ -21,19 +21,18 @@ class GraphQLStatementsFormatter { 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.language === 'typescript' && this.opTypeName && this.typesPath) { - return [ - `import * as APITypes from '${this.typesPath}';`, - `type Generated${this.opTypeName} = string & {`, - ` __generated${this.opTypeName}Input: InputType;`, - ` __generated${this.opTypeName}Output: OutputType;`, - `};`, - ].join(LINE_DELIMITOR); - } - return ''; + if (!this.includeTypeScriptTypes) return ''; + return [ + `import * as APITypes from '${this.typesPath}';`, + `type Generated${this.opTypeName} = string & {`, + ` __generated${this.opTypeName}Input: InputType;`, + ` __generated${this.opTypeName}Output: OutputType;`, + `};`, + ].join(LINE_DELIMITOR); } format(statements) { @@ -79,22 +78,13 @@ class GraphQLStatementsFormatter { } buildTypeTag(operationName, operationType) { - if (!operationType || !this.typesPath || this.language !== 'typescript') return ''; - - const resultTypeName = `APITypes.${interfaceNameFromOperation({ - operationName, - operationType, - })}`; + if (!this.includeTypeScriptTypes) return ''; - const variablesTypeName = `APITypes.${interfaceVariablesNameFromOperation({ - operationName, - operationType, - })}`; + const operationDef = { operationName, operationType }; + const resultTypeName = `APITypes.${interfaceNameFromOperation(operationDef)}`; + const variablesTypeName = `APITypes.${interfaceVariablesNameFromOperation(operationDef)}`; - return ` as Generated${this.opTypeName}< - ${variablesTypeName}, - ${resultTypeName} - >;`; + return ` as Generated${this.opTypeName}<${variablesTypeName}, ${resultTypeName}>;`; } prettify(output) { diff --git a/packages/graphql-docs-generator/src/index.ts b/packages/graphql-docs-generator/src/index.ts index eb066a3da..ed3bbf043 100644 --- a/packages/graphql-docs-generator/src/index.ts +++ b/packages/graphql-docs-generator/src/index.ts @@ -38,7 +38,6 @@ export function generateGraphQLDocuments( ['queries', 'mutations', 'subscriptions'].forEach(op => { const ops = gqlOperations[op]; - // console.log({ ops }); if (ops.length) { const renderedOperations = renderOperations(gqlOperations[op], options.includeMetaData); allOperations[op] = renderedOperations; From efce894406d7df59451cb5dac95457202ae640da Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Tue, 1 Aug 2023 14:29:06 -0500 Subject: [PATCH 7/8] fixed graphql formatting in ts output, types spacing in API.ts output --- .../src/utils/GraphQLStatementsFormatter.js | 8 ++++---- .../GraphQLStatementsFormatter.test.js.snap | 19 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js b/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js index e2efca911..87b1503ae 100644 --- a/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js +++ b/packages/amplify-codegen/src/utils/GraphQLStatementsFormatter.js @@ -68,12 +68,12 @@ class GraphQLStatementsFormatter { if (statements) { for (const [key, { graphql, operationName, operationType }] of statements) { const typeTag = this.buildTypeTag(operationName, operationType); - formattedStatements.push(`export const ${key} = /* GraphQL */ \`${graphql}\`${typeTag}`); + const formattedGraphQL = prettier.format(graphql, { parser: 'graphql' }); + formattedStatements.push(`export const ${key} = /* GraphQL */ \`${formattedGraphQL}\`${typeTag}`); } } - const formattedOutput = [lintOverridesBuffer, headerBuffer, LINE_DELIMITOR, this.typeDefs, 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; } 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 a3ef8ea09..4873bdc95 100644 --- a/packages/amplify-codegen/tests/utils/__snapshots__/GraphQLStatementsFormatter.test.js.snap +++ b/packages/amplify-codegen/tests/utils/__snapshots__/GraphQLStatementsFormatter.test.js.snap @@ -73,16 +73,15 @@ type GeneratedQuery = string & { __generatedQueryOutput: OutputType; }; -export const getProject = /* GraphQL */ \` - query GetProject($id: ID!) { - getProject(id: $id) { - id - name - createdAt - updatedAt - } - } - \` as GeneratedQuery< +export const getProject = /* GraphQL */ \`query GetProject($id: ID!) { + getProject(id: $id) { + id + name + createdAt + updatedAt + } +} +\` as GeneratedQuery< APITypes.GetProjectQueryVariables, APITypes.GetProjectQuery >; From aa43fce137ed4a3a58c64829be93d66134d9defb Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Wed, 2 Aug 2023 10:19:02 -0500 Subject: [PATCH 8/8] removed unused type guard function, fixes code coverage limit violation --- packages/graphql-docs-generator/src/index.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/graphql-docs-generator/src/index.ts b/packages/graphql-docs-generator/src/index.ts index ed3bbf043..73bf4225b 100644 --- a/packages/graphql-docs-generator/src/index.ts +++ b/packages/graphql-docs-generator/src/index.ts @@ -105,9 +105,9 @@ function renderOperations( operationName: op.name, operationType: op.type, fieldName: op.fieldName, - } as any); + } as MapValueType); } else { - renderedOperations.set(name, gql as any); + renderedOperations.set(name, gql as MapValueType); } }); } @@ -115,10 +115,6 @@ function renderOperations( return renderedOperations; } -function isMetaIncluded(includeMetaData: boolean, operationsMap: any): operationsMap is GraphQLWithMeta { - return includeMetaData === true; -} - function renderOperation(operation: GQLTemplateOp): string { const templateStr = getOperationPartial(); const template = handlebars.compile(templateStr, {