From 6ec93aed6520ffd8937b22059a8ac89e980dae30 Mon Sep 17 00:00:00 2001 From: Al Harris <91494052+alharris-at@users.noreply.github.com> Date: Mon, 25 Sep 2023 13:15:30 -0700 Subject: [PATCH] generate client code command (#265) * feat: add mocked generate graphql-client-code to cli * Update packages/cli/src/commands/generate/generate_command_factory.test.ts fix: update test name Co-authored-by: Kamil Sobol * chore: add missing descriptions for targets, use new shared backendIdentifierResolver * chore: remove adapter, we may need to reintroduce in the future, but simplifying until we have an impl in place * chore: add feature flags * chore: flip to use real generator, not mocked api --------- Co-authored-by: Kamil Sobol --- .changeset/old-pumpkins-peel.md | 5 + .eslint_dictionary.js | 1 + package-lock.json | 8 +- packages/cli/package.json | 1 + .../src/commands/generate/generate_command.ts | 9 +- .../generate/generate_command_factory.test.ts | 8 +- .../generate/generate_command_factory.ts | 14 +- .../generate_api_code_adapter.ts | 33 ++ ...nerate_graphql_client_code_command.test.ts | 255 ++++++++++++++++ .../generate_graphql_client_code_command.ts | 289 ++++++++++++++++++ packages/cli/tsconfig.json | 1 + packages/model-generator/API.md | 68 ++++- .../model-generator/src/generate_api_code.ts | 51 +++- 13 files changed, 716 insertions(+), 27 deletions(-) create mode 100644 .changeset/old-pumpkins-peel.md create mode 100644 packages/cli/src/commands/generate/graphql-client-code/generate_api_code_adapter.ts create mode 100644 packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.test.ts create mode 100644 packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts diff --git a/.changeset/old-pumpkins-peel.md b/.changeset/old-pumpkins-peel.md new file mode 100644 index 0000000000..2995d29876 --- /dev/null +++ b/.changeset/old-pumpkins-peel.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-cli': minor +--- + +Add generate graphql-client-code command with mocked implementation diff --git a/.eslint_dictionary.js b/.eslint_dictionary.js index ca66dbccb3..200e688185 100644 --- a/.eslint_dictionary.js +++ b/.eslint_dictionary.js @@ -14,6 +14,7 @@ export default [ 'codegen', 'cognito', 'ctor', + 'datastore', 'debounce', 'declarator', 'deployer', diff --git a/package-lock.json b/package-lock.json index b45c298c99..1ed6952ba0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18500,10 +18500,13 @@ }, "packages/cli": { "name": "@aws-amplify/backend-cli", - "version": "1.0.0-alpha.5", + "version": "0.2.0-alpha.3", "license": "Apache-2.0", "dependencies": { + "@aws-amplify/client-config": "^0.2.0-alpha.5", "@aws-amplify/deployed-backend-client": "^0.1.0", + "@aws-amplify/model-generator": "^0.2.0-alpha.1", + "@aws-amplify/sandbox": "^0.1.1-alpha.5", "@aws-sdk/credential-providers": "^3.360.0", "@inquirer/prompts": "^3.0.0", "execa": "^7.2.0", @@ -18521,9 +18524,6 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-amplify/client-config": "^0.2.0-alpha.5", - "@aws-amplify/model-generator": "0.2.0-alpha.1", - "@aws-amplify/sandbox": "^0.1.1-alpha.5", "@aws-sdk/types": "^3.347.0" } }, diff --git a/packages/cli/package.json b/packages/cli/package.json index 3841f6d1c2..cfa379969b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,6 +29,7 @@ "dependencies": { "@aws-amplify/client-config": "^0.2.0-alpha.5", "@aws-amplify/deployed-backend-client": "^0.1.0", + "@aws-amplify/model-generator": "^0.2.0-alpha.1", "@aws-amplify/sandbox": "^0.1.1-alpha.5", "@aws-sdk/credential-providers": "^3.360.0", "@inquirer/prompts": "^3.0.0", diff --git a/packages/cli/src/commands/generate/generate_command.ts b/packages/cli/src/commands/generate/generate_command.ts index 5b5578acfb..3b42236f9f 100644 --- a/packages/cli/src/commands/generate/generate_command.ts +++ b/packages/cli/src/commands/generate/generate_command.ts @@ -1,5 +1,6 @@ import { Argv, CommandModule } from 'yargs'; import { GenerateConfigCommand } from './config/generate_config_command.js'; +import { GenerateGraphqlClientCodeCommand } from './graphql-client-code/generate_graphql_client_code_command.js'; /** * An entry point for generate command. @@ -18,7 +19,10 @@ export class GenerateCommand implements CommandModule { /** * Creates top level entry point for generate command. */ - constructor(private readonly generateConfigCommand: GenerateConfigCommand) { + constructor( + private readonly generateConfigCommand: GenerateConfigCommand, + private readonly generateGraphqlClientCodeCommand: GenerateGraphqlClientCodeCommand + ) { this.command = 'generate'; this.describe = 'Generates post deployment artifacts'; } @@ -38,6 +42,9 @@ export class GenerateCommand implements CommandModule { yargs // Cast to erase options types used in internal sub command implementation. Otherwise, compiler fails here. .command(this.generateConfigCommand as unknown as CommandModule) + .command( + this.generateGraphqlClientCodeCommand as unknown as CommandModule + ) .demandCommand() .strictCommands() .recommendCommands() diff --git a/packages/cli/src/commands/generate/generate_command_factory.test.ts b/packages/cli/src/commands/generate/generate_command_factory.test.ts index 03f54bf83c..64d73ab6c4 100644 --- a/packages/cli/src/commands/generate/generate_command_factory.test.ts +++ b/packages/cli/src/commands/generate/generate_command_factory.test.ts @@ -16,10 +16,14 @@ describe('top level generate command', () => { const parser = yargs().command(generateCommand); const commandRunner = new TestCommandRunner(parser); - it('includes generate config in help output', async () => { + it('includes generate subcommands in help output', async () => { const output = await commandRunner.runCommand('generate --help'); assert.match(output, /Commands:/); - assert.match(output, /generate config {2}Generates client config/); + assert.match(output, /generate config\W*Generates client config/); + assert.match( + output, + /generate graphql-client-code\W*Generates graphql API code/ + ); }); it('fails if subcommand is not provided', async () => { diff --git a/packages/cli/src/commands/generate/generate_command_factory.ts b/packages/cli/src/commands/generate/generate_command_factory.ts index a4e75cbe0e..2e439936d0 100644 --- a/packages/cli/src/commands/generate/generate_command_factory.ts +++ b/packages/cli/src/commands/generate/generate_command_factory.ts @@ -4,8 +4,10 @@ import { GenerateConfigCommand } from './config/generate_config_command.js'; import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; import { ClientConfigGeneratorAdapter } from './config/client_config_generator_adapter.js'; import { CwdPackageJsonLoader } from '../../cwd_package_json_loader.js'; +import { GenerateGraphqlClientCodeCommand } from './graphql-client-code/generate_graphql_client_code_command.js'; import { LocalAppNameResolver } from '../../backend-identifier/local_app_name_resolver.js'; import { BackendIdentifierResolver } from '../../backend-identifier/backend_identifier_resolver.js'; +import { GenerateApiCodeAdapter } from './graphql-client-code/generate_api_code_adapter.js'; /** * Creates wired generate command. @@ -28,5 +30,15 @@ export const createGenerateCommand = (): CommandModule => { backendIdentifierResolver ); - return new GenerateCommand(generateConfigCommand); + const generateApiCodeAdapter = new GenerateApiCodeAdapter(credentialProvider); + + const generateGraphqlClientCodeCommand = new GenerateGraphqlClientCodeCommand( + generateApiCodeAdapter, + backendIdentifierResolver + ); + + return new GenerateCommand( + generateConfigCommand, + generateGraphqlClientCodeCommand + ); }; diff --git a/packages/cli/src/commands/generate/graphql-client-code/generate_api_code_adapter.ts b/packages/cli/src/commands/generate/graphql-client-code/generate_api_code_adapter.ts new file mode 100644 index 0000000000..c241a0a6c1 --- /dev/null +++ b/packages/cli/src/commands/generate/graphql-client-code/generate_api_code_adapter.ts @@ -0,0 +1,33 @@ +import { BackendIdentifier } from '@aws-amplify/deployed-backend-client'; +import { + GenerateOptions, + GenerationResult, + generateApiCode, +} from '@aws-amplify/model-generator'; +import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; + +// For some reason using `omit` is causing type errors, so reconstructing without the credentialProvider. +export type InvokeGenerateApiCodeProps = GenerateOptions & BackendIdentifier; + +/** + * Class to wrap static generateApiCode method to facilitate testing. + */ +export class GenerateApiCodeAdapter { + /** + * Creates graphql api code adapter. + */ + constructor( + private readonly credentialProvider: AwsCredentialIdentityProvider + ) {} + + /** + * Invoke the generateApiCode method, using the constructor injected credentialProvider, and remaining props. + */ + invokeGenerateApiCode = ( + props: InvokeGenerateApiCodeProps + ): Promise => + generateApiCode({ + ...props, + credentialProvider: this.credentialProvider, + }); +} diff --git a/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.test.ts b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.test.ts new file mode 100644 index 0000000000..ba94956da3 --- /dev/null +++ b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.test.ts @@ -0,0 +1,255 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; +import { GenerateGraphqlClientCodeCommand } from './generate_graphql_client_code_command.js'; +import yargs, { CommandModule } from 'yargs'; +import { + TestCommandError, + TestCommandRunner, +} from '../../../test-utils/command_runner.js'; +import assert from 'node:assert'; +import path from 'path'; +import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js'; +import { GenerateApiCodeAdapter } from './generate_api_code_adapter.js'; +import { + GenerateApiCodeFormat, + GenerateApiCodeModelTarget, + GenerateApiCodeStatementTarget, +} from '@aws-amplify/model-generator'; + +describe('generate graphql-client-code command', () => { + const generateApiCodeAdapter = new GenerateApiCodeAdapter( + fromNodeProviderChain() + ); + + const writeToDirectoryMock = mock.fn(); + const invokeGenerateApiCodeMock = mock.method( + generateApiCodeAdapter, + 'invokeGenerateApiCode', + () => + Promise.resolve({ + writeToDirectory: writeToDirectoryMock, + }) + ); + + const backendIdentifierResolver = new BackendIdentifierResolver({ + resolve: () => Promise.resolve('testAppName'), + }); + const generateGraphqlClientCodeCommand = new GenerateGraphqlClientCodeCommand( + generateApiCodeAdapter, + backendIdentifierResolver + ); + const parser = yargs().command( + generateGraphqlClientCodeCommand as unknown as CommandModule + ); + const commandRunner = new TestCommandRunner(parser); + + beforeEach(() => { + invokeGenerateApiCodeMock.mock.resetCalls(); + writeToDirectoryMock.mock.resetCalls(); + }); + + it('generates and writes graphql client code for stack', async () => { + await commandRunner.runCommand('graphql-client-code --stack stack_name'); + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', + format: GenerateApiCodeFormat.GRAPHQL_CODEGEN, + statementTarget: GenerateApiCodeStatementTarget.JAVASCRIPT, + }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); + }); + + it('generates and writes graphql client code for branch', async () => { + await commandRunner.runCommand('graphql-client-code --branch branch_name'); + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + appName: 'testAppName', + branchName: 'branch_name', + format: GenerateApiCodeFormat.GRAPHQL_CODEGEN, + statementTarget: GenerateApiCodeStatementTarget.JAVASCRIPT, + }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); + }); + + it('generates and writes graphql client code for appID and branch', async () => { + await commandRunner.runCommand( + 'graphql-client-code --branch branch_name --appId app_id' + ); + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + backendId: 'app_id', + branchName: 'branch_name', + format: GenerateApiCodeFormat.GRAPHQL_CODEGEN, + statementTarget: GenerateApiCodeStatementTarget.JAVASCRIPT, + }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); + }); + + it('can generate to custom relative path', async () => { + await commandRunner.runCommand( + 'graphql-client-code --stack stack_name --out foo/bar' + ); + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', + format: GenerateApiCodeFormat.GRAPHQL_CODEGEN, + statementTarget: GenerateApiCodeStatementTarget.JAVASCRIPT, + }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + path.join(process.cwd(), 'foo', 'bar') + ); + }); + + it('shows available options in help output', async () => { + const output = await commandRunner.runCommand('graphql-client-code --help'); + assert.match(output, /--stack/); + assert.match(output, /--appId/); + assert.match(output, /--branch/); + assert.match(output, /--format/); + assert.match(output, /--statementTarget/); + assert.match(output, /--typeTarget/); + assert.match(output, /--modelTarget/); + assert.match(output, /--out/); + assert.match(output, /--all/); + }); + + it('shows all available options in help output', async () => { + const output = await commandRunner.runCommand( + 'graphql-client-code --help --all' + ); + assert.match(output, /--stack/); + assert.match(output, /--appId/); + assert.match(output, /--branch/); + assert.match(output, /--format/); + assert.match(output, /--statementTarget/); + assert.match(output, /--typeTarget/); + assert.match(output, /--modelTarget/); + assert.match(output, /--out/); + assert.match(output, /--all/); + assert.match(output, /--modelGenerateIndexRules/); + assert.match(output, /--modelEmitAuthProvider/); + assert.match(output, /--modelAddTimestampFields/); + assert.match(output, /--statementMaxDepth/); + assert.match(output, /--statementTypenameIntrospection/); + assert.match(output, /--typeMultipleSwiftFiles/); + }); + + it('can be invoked explicitly with graphql-codegen format', async () => { + await commandRunner.runCommand( + 'graphql-client-code --stack stack_name --format graphql-codegen' + ); + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', + format: GenerateApiCodeFormat.GRAPHQL_CODEGEN, + statementTarget: GenerateApiCodeStatementTarget.JAVASCRIPT, + }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); + }); + + it('can be invoked explicitly with modelgen format', async () => { + await commandRunner.runCommand( + 'graphql-client-code --stack stack_name --format modelgen' + ); + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', + format: GenerateApiCodeFormat.MODELGEN, + modelTarget: GenerateApiCodeModelTarget.JAVASCRIPT, + }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); + }); + + it('can be invoked explicitly with introspection format', async () => { + await commandRunner.runCommand( + 'graphql-client-code --stack stack_name --format introspection' + ); + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', + format: GenerateApiCodeFormat.INTROSPECTION, + }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); + }); + + it('passes in feature flags on modelgen', async () => { + await commandRunner.runCommand( + 'graphql-client-code --stack stack_name --format modelgen --modelGenerateIndexRules true --modelEmitAuthProvider true --modelGenerateModelsForLazyLoadAndCustomSelectionSet false' + ); + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', + format: GenerateApiCodeFormat.MODELGEN, + modelTarget: GenerateApiCodeModelTarget.JAVASCRIPT, + generateIndexRules: true, + emitAuthProvider: true, + generateModelsForLazyLoadAndCustomSelectionSet: false, + }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); + }); + + it('passes in feature flags on graphql-codegen', async () => { + await commandRunner.runCommand( + 'graphql-client-code --stack stack_name --format graphql-codegen --statementTarget typescript --statementMaxDepth 3 --statementTypenameIntrospection true' + ); + assert.equal(invokeGenerateApiCodeMock.mock.callCount(), 1); + assert.deepEqual(invokeGenerateApiCodeMock.mock.calls[0].arguments[0], { + stackName: 'stack_name', + format: GenerateApiCodeFormat.GRAPHQL_CODEGEN, + statementTarget: GenerateApiCodeStatementTarget.TYPESCRIPT, + maxDepth: 3, + typenameIntrospection: true, + }); + assert.equal(writeToDirectoryMock.mock.callCount(), 1); + assert.equal( + writeToDirectoryMock.mock.calls[0].arguments[0], + process.cwd() + ); + }); + + // Note: after this test, future tests seem to be in a weird state, leaving this at the + it('fails if both stack and branch are present', async () => { + await assert.rejects( + () => + commandRunner.runCommand( + 'graphql-client-code --stack foo --branch baz' + ), + (err: TestCommandError) => { + assert.equal(err.error.name, 'YError'); + assert.match(err.error.message, /Arguments .* mutually exclusive/); + assert.match(err.output, /Arguments .* are mutually exclusive/); + return true; + } + ); + }); +}); diff --git a/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts new file mode 100644 index 0000000000..1911b7b583 --- /dev/null +++ b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts @@ -0,0 +1,289 @@ +import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; +import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js'; +import { isAbsolute, resolve } from 'path'; +import { GenerateApiCodeAdapter } from './generate_api_code_adapter.js'; +import { + GenerateApiCodeFormat, + GenerateApiCodeModelTarget, + GenerateApiCodeStatementTarget, + GenerateApiCodeTypeTarget, + GenerateGraphqlCodegenOptions, + GenerateModelsOptions, +} from '@aws-amplify/model-generator'; + +export type GenerateGraphqlClientCodeCommandOptions = { + stack: string | undefined; + appId: string | undefined; + branch: string | undefined; + format: GenerateApiCodeFormat | undefined; + modelTarget: GenerateApiCodeModelTarget | undefined; + statementTarget: GenerateApiCodeStatementTarget | undefined; + typeTarget: GenerateApiCodeTypeTarget | undefined; + out: string | undefined; + modelGenerateIndexRules: boolean | undefined; + modelEmitAuthProvider: boolean | undefined; + modelRespectPrimaryKeyAttributesOnConnectionField: boolean | undefined; + modelGenerateModelsForLazyLoadAndCustomSelectionSet: boolean | undefined; + modelAddTimestampFields: boolean | undefined; + modelHandleListNullabilityTransparently: boolean | undefined; + statementMaxDepth: number | undefined; + statementTypenameIntrospection: boolean | undefined; + typeMultipleSwiftFiles: boolean | undefined; +}; + +/** + * Command that generates client config. + */ +export class GenerateGraphqlClientCodeCommand + implements CommandModule +{ + /** + * @inheritDoc + */ + readonly command: string; + + /** + * @inheritDoc + */ + readonly describe: string; + + private readonly missingArgsError = new Error( + 'Either --stack or --branch must be provided' + ); + + /** + * Creates graphql client code generation command. + */ + constructor( + private readonly generateApiCodeAdapter: GenerateApiCodeAdapter, + private readonly backendIdentifierResolver: BackendIdentifierResolver + ) { + this.command = 'graphql-client-code'; + this.describe = 'Generates graphql API code'; + } + + private getFormatParams = ( + format: string, + args: ArgumentsCamelCase + ): + | {} + | Pick< + GenerateGraphqlCodegenOptions, + 'statementTarget' | 'typeTarget' | 'maxDepth' | 'multipleSwiftFiles' + > + | Pick => { + switch (format) { + case 'graphql-codegen': + return { + statementTarget: args.statementTarget ?? 'javascript', + ...('typeTarget' in args ? { typeTarget: args.typeTarget } : {}), + ...('statementMaxDepth' in args + ? { maxDepth: args.statementMaxDepth } + : {}), + ...('statementTypenameIntrospection' in args + ? { typenameIntrospection: args.statementTypenameIntrospection } + : {}), + ...('typeMultipleSwiftFiles' in args + ? { multipleSwiftFiles: args.typeMultipleSwiftFiles } + : {}), + }; + case 'modelgen': + return { + modelTarget: args.modelTarget ?? 'javascript', + ...('modelGenerateIndexRules' in args + ? { generateIndexRules: args.modelGenerateIndexRules } + : {}), + ...('modelEmitAuthProvider' in args + ? { emitAuthProvider: args.modelEmitAuthProvider } + : {}), + ...('modelRespectPrimaryKeyAttributesOnConnectionField' in args + ? { + respectPrimaryKeyAttributesOnConnectionField: + args.modelRespectPrimaryKeyAttributesOnConnectionField, + } + : {}), + ...('modelGenerateModelsForLazyLoadAndCustomSelectionSet' in args + ? { + generateModelsForLazyLoadAndCustomSelectionSet: + args.modelGenerateModelsForLazyLoadAndCustomSelectionSet, + } + : {}), + ...('modelAddTimestampFields' in args + ? { addTimestampFields: args.modelAddTimestampFields } + : {}), + ...('modelHandleListNullabilityTransparently' in args + ? { + handleListNullabilityTransparently: + args.modelHandleListNullabilityTransparently, + } + : {}), + }; + case 'introspection': + return {}; + default: + throw new Error(`Unexpected format ${format} received`); + } + }; + + private getOutDir = ( + args: ArgumentsCamelCase + ) => { + const cwd = process.cwd(); + if (!args.out) { + return cwd; + } + return isAbsolute(args.out) ? args.out : resolve(cwd, args.out); + }; + + /** + * @inheritDoc + */ + handler = async ( + args: ArgumentsCamelCase + ): Promise => { + const backendIdentifier = await this.backendIdentifierResolver.resolve( + args + ); + const out = this.getOutDir(args); + const format = args.format ?? ('graphql-codegen' as unknown as any); + const formatParams = this.getFormatParams(format, args); + + const result = await this.generateApiCodeAdapter.invokeGenerateApiCode({ + ...backendIdentifier, + format, + ...formatParams, + }); + + await result.writeToDirectory(out); + }; + + /** + * @inheritDoc + */ + builder = (yargs: Argv): Argv => { + return yargs + .option('stack', { + conflicts: ['appId', 'branch'], + describe: 'A stack name that contains an Amplify backend', + type: 'string', + array: false, + group: 'Stack identifier', + }) + .option('appId', { + conflicts: ['stack'], + describe: 'The Amplify App ID of the project', + type: 'string', + array: false, + implies: 'branch', + group: 'Project identifier', + }) + .option('branch', { + conflicts: ['stack'], + describe: 'A git branch of the Amplify project', + type: 'string', + array: false, + group: 'Project identifier', + }) + .option('out', { + describe: + 'A path to directory where config is written. If not provided defaults to current process working directory.', + type: 'string', + array: false, + }) + .option('format', { + describe: + 'The format that the GraphQL client code should be generated in.', + type: 'string', + array: false, + choices: Object.values(GenerateApiCodeFormat), + }) + .option('modelTarget', { + describe: + 'The modelgen export target. Only applies when the `--format` parameter is set to `modelgen`', + type: 'string', + array: false, + choices: Object.values(GenerateApiCodeModelTarget), + }) + .option('statementTarget', { + describe: + 'The graphql-codegen statement export target. Only applies when the `--format` parameter is set to `graphql-codegen`', + type: 'string', + array: false, + choices: Object.values(GenerateApiCodeStatementTarget), + }) + .option('typeTarget', { + describe: + 'The optional graphql-codegen type export target. Only applies when the `--format` parameter is set to `graphql-codegen`', + type: 'string', + array: false, + choices: Object.values(GenerateApiCodeTypeTarget), + }) + .option('modelGenerateIndexRules', { + description: 'Adds key/index details to iOS models', + type: 'boolean', + array: false, + hidden: true, + }) + .option('modelEmitAuthProvider', { + description: 'Adds auth provider details to iOS models', + type: 'boolean', + array: false, + hidden: true, + }) + .option('modelRespectPrimaryKeyAttributesOnConnectionField', { + description: + 'If enabled, Datastore queries will respect the primary + sort key fields, rather than a default id field', + type: 'boolean', + array: false, + hidden: true, + }) + .option('modelGenerateModelsForLazyLoadAndCustomSelectionSet', { + description: + 'Generates lazy model type definitions, required or amplify-js v5+', + type: 'boolean', + array: false, + hidden: true, + }) + .option('modelAddTimestampFields', { + description: 'Add read-only timestamp fields in modelgen.', + type: 'boolean', + array: false, + hidden: true, + }) + .option('modelHandleListNullabilityTransparently', { + description: + 'Configure the nullability of the List and List components in Datastore Models generation', + type: 'boolean', + array: false, + hidden: true, + }) + .option('statementMaxDepth', { + description: + 'Determines how deeply nested to generate graphql statements.', + type: 'number', + array: false, + hidden: true, + }) + .option('statementTypenameIntrospection', { + description: + 'Determines whether to include default __typename for all generated statements', + type: 'boolean', + array: false, + hidden: true, + }) + .option('typeMultipleSwiftFiles', { + description: + 'Determines whether or not to generate a single API.swift, or multiple per-model swift files.', + type: 'boolean', + array: false, + hidden: true, + }) + .showHidden('all') + .check((argv) => { + if (!argv.stack && !argv.branch) { + throw this.missingArgsError; + } + return true; + }); + }; +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index ca7ebacb55..ea291bde69 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -4,6 +4,7 @@ "references": [ { "path": "../client-config" }, { "path": "../deployed-backend-client" }, + { "path": "../model-generator" }, { "path": "../sandbox" } ] } diff --git a/packages/model-generator/API.md b/packages/model-generator/API.md index 97087da629..61b7b552a0 100644 --- a/packages/model-generator/API.md +++ b/packages/model-generator/API.md @@ -23,30 +23,86 @@ export type DocumentGenerationParameters = { // @public export const generateApiCode: (props: GenerateApiCodeProps) => Promise; +// @public (undocumented) +export enum GenerateApiCodeFormat { + // (undocumented) + GRAPHQL_CODEGEN = "graphql-codegen", + // (undocumented) + INTROSPECTION = "introspection", + // (undocumented) + MODELGEN = "modelgen" +} + +// @public (undocumented) +export enum GenerateApiCodeModelTarget { + // (undocumented) + DART = "dart", + // (undocumented) + JAVA = "java", + // (undocumented) + JAVASCRIPT = "javascript", + // (undocumented) + SWIFT = "swift", + // (undocumented) + TYPESCRIPT = "typescript" +} + // @public (undocumented) export type GenerateApiCodeProps = GenerateOptions & BackendIdentifier & { credentialProvider: AwsCredentialIdentityProvider; }; +// @public (undocumented) +export enum GenerateApiCodeStatementTarget { + // (undocumented) + ANGULAR = "angular", + // (undocumented) + FLOW = "flow", + // (undocumented) + GRAPHQL = "graphql", + // (undocumented) + JAVASCRIPT = "javascript", + // (undocumented) + TYPESCRIPT = "typescript" +} + +// @public (undocumented) +export enum GenerateApiCodeTypeTarget { + // (undocumented) + ANGULAR = "angular", + // (undocumented) + FLOW = "flow", + // (undocumented) + FLOW_MODERN = "flow-modern", + // (undocumented) + JSON = "json", + // (undocumented) + SCALA = "scala", + // (undocumented) + SWIFT = "swift", + // (undocumented) + TYPESCRIPT = "typescript" +} + // @public (undocumented) export type GenerateGraphqlCodegenOptions = { - format: 'graphql-codegen'; - statementTarget: 'javascript' | 'graphql' | 'flow' | 'typescript' | 'angular'; + format: GenerateApiCodeFormat.GRAPHQL_CODEGEN; + statementTarget: GenerateApiCodeStatementTarget; maxDepth?: number; typeNameIntrospection?: boolean; - typeTarget?: 'json' | 'swift' | 'typescript' | 'flow' | 'scala' | 'flow-modern' | 'angular'; + typeTarget?: GenerateApiCodeTypeTarget; multipleSwiftFiles?: boolean; }; // @public (undocumented) export type GenerateIntrospectionOptions = { - format: 'introspection'; + format: GenerateApiCodeFormat.INTROSPECTION; }; // @public (undocumented) export type GenerateModelsOptions = { - format: 'modelgen'; - modelTarget: 'java' | 'swift' | 'javascript' | 'typescript' | 'dart'; + format: GenerateApiCodeFormat.MODELGEN; + modelTarget: GenerateApiCodeModelTarget; generateIndexRules?: boolean; emitAuthProvider?: boolean; useExperimentalPipelinedTransformer?: boolean; diff --git a/packages/model-generator/src/generate_api_code.ts b/packages/model-generator/src/generate_api_code.ts index 6841ef8bbf..313fc464fb 100644 --- a/packages/model-generator/src/generate_api_code.ts +++ b/packages/model-generator/src/generate_api_code.ts @@ -7,9 +7,41 @@ import { createGraphqlModelsGenerator } from './create_graphql_models_generator. import { createGraphqlTypesGenerator } from './create_graphql_types_generator.js'; import { createGraphqlDocumentGenerator } from './create_graphql_document_generator.js'; +export enum GenerateApiCodeFormat { + MODELGEN = 'modelgen', + GRAPHQL_CODEGEN = 'graphql-codegen', + INTROSPECTION = 'introspection', +} + +export enum GenerateApiCodeModelTarget { + JAVA = 'java', + SWIFT = 'swift', + JAVASCRIPT = 'javascript', + TYPESCRIPT = 'typescript', + DART = 'dart', +} + +export enum GenerateApiCodeStatementTarget { + JAVASCRIPT = 'javascript', + GRAPHQL = 'graphql', + FLOW = 'flow', + TYPESCRIPT = 'typescript', + ANGULAR = 'angular', +} + +export enum GenerateApiCodeTypeTarget { + JSON = 'json', + SWIFT = 'swift', + TYPESCRIPT = 'typescript', + FLOW = 'flow', + SCALA = 'scala', + FLOW_MODERN = 'flow-modern', + ANGULAR = 'angular', +} + export type GenerateModelsOptions = { - format: 'modelgen'; - modelTarget: 'java' | 'swift' | 'javascript' | 'typescript' | 'dart'; + format: GenerateApiCodeFormat.MODELGEN; + modelTarget: GenerateApiCodeModelTarget; generateIndexRules?: boolean; emitAuthProvider?: boolean; useExperimentalPipelinedTransformer?: boolean; @@ -21,23 +53,16 @@ export type GenerateModelsOptions = { }; export type GenerateGraphqlCodegenOptions = { - format: 'graphql-codegen'; - statementTarget: 'javascript' | 'graphql' | 'flow' | 'typescript' | 'angular'; + format: GenerateApiCodeFormat.GRAPHQL_CODEGEN; + statementTarget: GenerateApiCodeStatementTarget; maxDepth?: number; typeNameIntrospection?: boolean; - typeTarget?: - | 'json' - | 'swift' - | 'typescript' - | 'flow' - | 'scala' - | 'flow-modern' - | 'angular'; + typeTarget?: GenerateApiCodeTypeTarget; multipleSwiftFiles?: boolean; }; export type GenerateIntrospectionOptions = { - format: 'introspection'; + format: GenerateApiCodeFormat.INTROSPECTION; }; export type GenerateOptions =