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 index bd87ac06a9..e4dccfdc30 100644 --- 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 @@ -1,10 +1,6 @@ import { beforeEach, describe, it, mock } from 'node:test'; import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; -import { - GenerateGraphqlClientCodeCommand, - configFileName, - formatChoices, -} from './generate_graphql_client_code_command.js'; +import { GenerateGraphqlClientCodeCommand } from './generate_graphql_client_code_command.js'; import yargs, { CommandModule } from 'yargs'; import { TestCommandError, @@ -42,12 +38,10 @@ describe('generate graphql-client-code command', () => { assert.equal(generateClientConfigMock.mock.callCount(), 1); assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], { stackName: 'stack_name', + format: 'graphql-codegen', + out: process.cwd(), + statementTarget: 'javascript', }); - assert.equal(generateClientConfigMock.mock.callCount(), 1); - assert.deepEqual( - generateClientConfigMock.mock.calls[0].arguments[1], - path.join(process.cwd(), `${configFileName}.${formatChoices[0]}`) - ); }); it('generates and writes graphql client code for branch', async () => { @@ -56,15 +50,10 @@ describe('generate graphql-client-code command', () => { assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], { appName: 'testAppName', branchName: 'branch_name', + out: process.cwd(), + format: 'graphql-codegen', + statementTarget: 'javascript', }); - // I can't find any open node:test or yargs issues that would explain why this is necessary - // but for some reason the mock call count does not update without this 0ms wait - await new Promise((resolve) => setTimeout(resolve, 0)); - assert.equal(generateClientConfigMock.mock.callCount(), 1); - assert.deepStrictEqual( - generateClientConfigMock.mock.calls[0].arguments[1], - path.join(process.cwd(), `${configFileName}.${formatChoices[0]}`) - ); }); it('generates and writes graphql client code for appID and branch', async () => { @@ -75,12 +64,10 @@ describe('generate graphql-client-code command', () => { assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], { backendId: 'app_id', branchName: 'branch_name', + out: process.cwd(), + format: 'graphql-codegen', + statementTarget: 'javascript', }); - assert.equal(generateClientConfigMock.mock.callCount(), 1); - assert.deepStrictEqual( - generateClientConfigMock.mock.calls[0].arguments[1], - path.join(process.cwd(), `${configFileName}.${formatChoices[0]}`) - ); }); it('can generate to custom absolute path', async () => { @@ -90,18 +77,10 @@ describe('generate graphql-client-code command', () => { assert.equal(generateClientConfigMock.mock.callCount(), 1); assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], { stackName: 'stack_name', + out: path.join('/', 'foo', 'bar'), + format: 'graphql-codegen', + statementTarget: 'javascript', }); - assert.equal(generateClientConfigMock.mock.callCount(), 1); - const actualPath = generateClientConfigMock.mock.calls[0].arguments[1]; - // normalize the path root across unix and windows platforms - const normalizedPath = actualPath?.replace( - path.parse(actualPath).root, - path.sep - ); - assert.equal( - normalizedPath, - path.join('/', 'foo', 'bar', `${configFileName}.${formatChoices[2]}`) - ); }); it('can generate to custom relative path', async () => { @@ -111,12 +90,10 @@ describe('generate graphql-client-code command', () => { assert.equal(generateClientConfigMock.mock.callCount(), 1); assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], { stackName: 'stack_name', + format: 'graphql-codegen', + statementTarget: 'javascript', + out: path.join(process.cwd(), 'foo', 'bar'), }); - assert.equal(generateClientConfigMock.mock.callCount(), 1); - assert.equal( - generateClientConfigMock.mock.calls[0].arguments[1], - path.join(process.cwd(), 'foo', 'bar', 'amplifyconfiguration.js') - ); }); it('shows available options in help output', async () => { @@ -125,8 +102,9 @@ describe('generate graphql-client-code command', () => { assert.match(output, /--appId/); assert.match(output, /--branch/); assert.match(output, /--format/); - assert.match(output, /--platform/); - assert.match(output, /--target/); + assert.match(output, /--statementTarget/); + assert.match(output, /--typeTarget/); + assert.match(output, /--modelTarget/); assert.match(output, /--out/); }); @@ -144,4 +122,36 @@ describe('generate graphql-client-code command', () => { } ); }); + + // it('can be invoked explicitly with graphql-codegen format', async () => { + // await commandRunner.runCommand('graphql-client-code --stack stack_name --format graphql-codegen'); + // assert.equal(generateClientConfigMock.mock.callCount(), 1); + // assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], { + // stackName: 'stack_name', + // format: 'graphql-codegen', + // statementType: 'javascript', + // out: process.cwd(), + // }); + // }); + + // it('can be invoked explicitly with modelgen format', async () => { + // await commandRunner.runCommand('graphql-client-code --stack stack_name --format modelgen'); + // assert.equal(generateClientConfigMock.mock.callCount(), 1); + // assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], { + // stackName: 'stack_name', + // format: 'modelgen', + // modelType: 'javascript', + // out: process.cwd(), + // }); + // }); + + // it('can be invoked explicitly with introspection format', async () => { + // await commandRunner.runCommand('graphql-client-code --stack stack_name --format introspection'); + // assert.equal(generateClientConfigMock.mock.callCount(), 1); + // assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], { + // stackName: 'stack_name', + // format: 'introspection', + // out: process.cwd(), + // }); + // }); }); 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 index 8999f64f00..4ab2f2a822 100644 --- 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 @@ -1,25 +1,43 @@ import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; -import path from 'path'; import { BackendIdentifier } from '@aws-amplify/client-config'; import { AppNameResolver } from '../../../local_app_name_resolver.js'; import { GraphqlClientCodeGeneratorAdapter } from './generate_graphql_client_code_generator_adapter.js'; +import { isAbsolute, resolve } from 'path'; export const formatChoices = ['amplify-codegen', 'introspection', 'modelgen']; -export const platformChoices = ['js', 'ts', 'dart', 'android', 'swift']; -export const configFileName = 'amplifyconfiguration'; - -export const modelgenTargetChoices = ['java', 'swift', 'javascript', 'typescript', 'dart', 'introspection']; -export const statementsTargetChoices = ['javascript', 'graphql', 'flow', 'typescript', 'angular'] -export const typesTargetChoice = ['json', 'swift', 'ts', 'typescript', 'flow', 'scala', 'flow-modern', 'angular'] -export const targetChoices = ['javascript', 'java', 'swift', 'typescript', 'dart', 'introspection', 'graphql', 'flow', 'angular', 'json', 'ts', 'scala', 'flow-modern']; +export const modelgenTargetChoices = [ + 'java', + 'swift', + 'javascript', + 'typescript', + 'dart', +]; +export const statementsTargetChoices = [ + 'javascript', + 'graphql', + 'flow', + 'typescript', + 'angular', +]; +export const typesTargetChoice = [ + 'json', + 'swift', + 'ts', + 'typescript', + 'flow', + 'scala', + 'flow-modern', + 'angular', +]; export type GenerateGraphqlClientCodeCommandOptions = { stack: string | undefined; appId: string | undefined; branch: string | undefined; format: (typeof formatChoices)[number] | undefined; - platform: (typeof platformChoices)[number] | undefined; - target: (typeof targetChoices)[number] | undefined; + modelTarget: (typeof modelgenTargetChoices)[number] | undefined; + statementTarget: (typeof statementsTargetChoices)[number] | undefined; + typeTarget: (typeof typesTargetChoice)[number] | undefined; out: string | undefined; }; @@ -54,37 +72,55 @@ export class GenerateGraphqlClientCodeCommand this.describe = 'Generates graphql API code'; } + private getTargetParts = ( + format: string, + args: ArgumentsCamelCase + ): Record => { + switch (format) { + case 'graphql-codegen': + return { + statementTarget: args.statementTarget ?? 'javascript', + ...(args.typeTarget ? { typeTarget: args.typeTarget } : {}), + }; + case 'modelgen': + return { + modelTarget: args.modelTarget ?? 'javascript', + }; + 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 defaultArgs = { - out: process.cwd(), - format: 'amplify-codegen', - platform: 'js', - target: 'javascript', - }; - const backendIdentifier = await this.getBackendIdentifier(args); - - let targetPath: string; - if (args.out) { - targetPath = path.isAbsolute(args.out) - ? args.out - : path.resolve(process.cwd(), args.out); - } else { - targetPath = defaultArgs.out; - } - - targetPath = path.resolve( - targetPath, - `${configFileName}.${args.format || defaultArgs.format}` - ); + const backendIdentifierParts = await this.getBackendIdentifier(args); + const out = this.getOutDir(args); + const format = args.format ?? ('graphql-codegen' as unknown as any); + const targetParts = this.getTargetParts(format, args); await this.graphqlClientCodeGeneratorAdapter.generateGraphqlClientCodeToFile( - backendIdentifier, - targetPath + { + ...backendIdentifierParts, + out, + format, + ...targetParts, + } ); }; @@ -135,28 +171,36 @@ export class GenerateGraphqlClientCodeCommand 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.', + describe: + 'The format that the GraphQL client code should be generated in.', type: 'string', array: false, choices: formatChoices, }) - .option('platform', { - describe: 'The platform for which the configuration should be exported for.', + .option('modelTarget', { + describe: 'TK', type: 'string', array: false, - choices: platformChoices, + choices: modelgenTargetChoices, }) - .option('target', { - describe: 'The platform for which the configuration should be exported for.', + .option('statementTarget', { + describe: 'TK', type: 'string', array: false, - choices: targetChoices, + choices: statementsTargetChoices, }) - .option('out', { - describe: 'A path to directory where config is written. If not provided defaults to current process working directory.', + .option('typeTarget', { + describe: 'TK', type: 'string', array: false, + choices: typesTargetChoice, }) .check((argv) => { if (!argv.stack && !argv.branch) { diff --git a/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_generator_adapter.ts b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_generator_adapter.ts index d3e9f44e1c..e718b18f42 100644 --- a/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_generator_adapter.ts +++ b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_generator_adapter.ts @@ -1,13 +1,20 @@ -import { - BackendIdentifier, - ClientConfig, - generateClientConfig, - generateClientConfigToFile, -} from '@aws-amplify/client-config'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; +import { + GenerateAPICodeProps, + GeneratedOutput, + generateAPICode, +} from './mock_code_generator.js'; + +export type GenerateGraphqlClientCodeProps = GenerateAPICodeProps; +export type GenerateGraphqlClientCodeToFileProps = + GenerateGraphqlClientCodeProps & { + out: string; + }; /** - * Adapts static generateClientConfigToFile from @aws-amplify/client-config call to make it injectable and testable. + * Adapts wraps calls to the code generator, and generates credentials providers, etc. */ export class GraphqlClientCodeGeneratorAdapter { /** @@ -20,23 +27,29 @@ export class GraphqlClientCodeGeneratorAdapter { /** * Generates the platform-specific graphql client code for a given backend */ - generateGraphqlClientCode = async ( - backendIdentifier: BackendIdentifier - ): Promise => { - return generateClientConfig(this.awsCredentialProvider, backendIdentifier); - }; + generateGraphqlClientCode = ( + props: GenerateGraphqlClientCodeProps + ): Promise => + generateAPICode({ + ...props, + // credentialProvider: this.awsCredentialProvider, + }); /** * Generates the platform-specific graphql client code for a given backend, and write the outputs to the specified target. */ generateGraphqlClientCodeToFile = async ( - backendIdentifier: BackendIdentifier, - targetPath: string + props: GenerateGraphqlClientCodeToFileProps ): Promise => { - await generateClientConfigToFile( - this.awsCredentialProvider, - backendIdentifier, - targetPath - ); + const { out, ...rest } = props; + + const generatedCode = await generateAPICode({ + ...rest, + // credentialProvider: this.awsCredentialProvider, + }); + + Object.entries(generatedCode).forEach(([filePath, fileContents]) => { + writeFileSync(join(out, filePath), fileContents); + }); }; } diff --git a/packages/cli/src/commands/generate/graphql-client-code/mock_code_generator.ts b/packages/cli/src/commands/generate/graphql-client-code/mock_code_generator.ts index 791afba740..38c291203b 100644 --- a/packages/cli/src/commands/generate/graphql-client-code/mock_code_generator.ts +++ b/packages/cli/src/commands/generate/graphql-client-code/mock_code_generator.ts @@ -1,26 +1,74 @@ -import { BackendIdentifier } from "@aws-amplify/client-config"; -import { AwsCredentialIdentityProvider } from "@aws-sdk/types"; +import { BackendIdentifier } from '@aws-amplify/client-config'; +import path from 'path'; -export const formatChoices = ['introspection', 'amplify-codegen', 'modelgen']; -export const platformChoices = ['ts', 'js', 'dart', 'android', 'swift']; -export const configFileName = 'amplifyconfiguration'; +export type GeneratedOutput = { [filename: string]: string }; -export const modelgenTargetChoices = ['java', 'swift', 'javascript', 'typescript', 'dart', 'introspection']; -export const statementsTargetChoices = ['javascript', 'graphql', 'flow', 'typescript', 'angular'] -export const typesTargetChoice = ['json', 'swift', 'ts', 'typescript', 'flow', 'scala', 'flow-modern', 'angular'] +export type GenerateModelsOptions = { + format: 'modelgen'; + modelTarget: 'java' | 'swift' | 'javascript' | 'typescript' | 'dart'; + generateIndexRules?: boolean; + emitAuthProvider?: boolean; + useExperimentalPipelinedTranformer?: boolean; + transformerVersion?: boolean; + respectPrimaryKeyAttributesOnConnectionField?: boolean; + generateModelsForLazyLoadAndCustomSelectionSet?: boolean; + addTimestampFields?: boolean; + handleListNullabilityTransparently?: boolean; +}; + +export type GenerateGraphqlCodegenOptions = { + format: 'graphql-codegen'; + statementTarget: 'javascript' | 'graphql' | 'flow' | 'typescript' | 'angular'; + maxDepth?: number; + typenameIntrospection?: boolean; + typeTarget?: + | 'json' + | 'swift' + | 'typescript' + | 'flow' + | 'scala' + | 'flow-modern' + | 'angular'; + multipleSwiftFiles?: boolean; +}; -export type GenerateAPICodeProps = BackendIdentifier & { - credentialProvider: AwsCredentialIdentityProvider, - format: string; - platform: string; - target?: string; +export type GenerateIntrospectionOptions = { + format: 'introspection'; }; +export type GenerateOptions = + | GenerateGraphqlCodegenOptions + | GenerateModelsOptions + | GenerateIntrospectionOptions; + +export type GenerateAPICodeProps = BackendIdentifier & GenerateOptions; + /** * Mock generateApiCode command. */ -export const generateAPICode = (props: GenerateAPICodeProps): Record => { - // eslint-disable-next-line no-console - console.log(`generateAPICode invoked with ${JSON.stringify(props)}`); - return {}; +export const generateAPICode = async ( + props: GenerateAPICodeProps +): Promise => { + switch (props.format) { + case 'graphql-codegen': + return { + [path.join('src', 'graphq', 'mutations.js')]: 'type Mutations {}', + [path.join('src', 'graphq', 'queries.js')]: 'type Queries {}', + [path.join('src', 'graphq', 'subscriptions.js')]: + 'type Subscriptions {}', + }; + case 'modelgen': + return { + [path.join('src', 'models', 'index.js')]: 'export me', + [path.join('src', 'models', 'models.js')]: 'im a models', + }; + case 'introspection': + return { + 'model-introspection-schema.json': JSON.stringify( + { version: 1, models: [], nonModels: [] }, + null, + 4 + ), + }; + } };