Skip to content

Commit

Permalink
feat: initial clone of generate config command into generate graphql-…
Browse files Browse the repository at this point in the history
…client-code
  • Loading branch information
alharris-at committed Sep 21, 2023
1 parent f75fa53 commit 0b72138
Show file tree
Hide file tree
Showing 6 changed files with 362 additions and 4 deletions.
9 changes: 8 additions & 1 deletion packages/cli/src/commands/generate/generate_command.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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';
}
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 config and graphql-client-code 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 () => {
Expand Down
15 changes: 14 additions & 1 deletion packages/cli/src/commands/generate/generate_command_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { ClientConfigGeneratorAdapter } from './config/client_config_generator_adapter.js';
import { LocalAppNameResolver } from '../../local_app_name_resolver.js';
import { CwdPackageJsonLoader } from '../../cwd_package_json_loader.js';
import { GenerateGraphqlClientCodeCommand } from './graphql-client-code/generate_graphql_client_code_command.js';
import { GraphqlClientCodeGeneratorAdapter } from './graphql-client-code/generate_graphql_client_code_generator_adapter.js';

/**
* Creates wired generate command.
Expand All @@ -23,5 +25,16 @@ export const createGenerateCommand = (): CommandModule => {
localAppNameResolver
);

return new GenerateCommand(generateConfigCommand);
const graphqlClientCodeGeneratorAdapter =
new GraphqlClientCodeGeneratorAdapter(credentialProvider);

const generateGraphqlClientCodeCommand = new GenerateGraphqlClientCodeCommand(
graphqlClientCodeGeneratorAdapter,
localAppNameResolver
);

return new GenerateCommand(
generateConfigCommand,
generateGraphqlClientCodeCommand
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
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 yargs, { CommandModule } from 'yargs';
import {
TestCommandError,
TestCommandRunner,
} from '../../../test-utils/command_runner.js';
import assert from 'node:assert';
import path from 'path';
import { GraphqlClientCodeGeneratorAdapter } from './generate_graphql_client_code_generator_adapter.js';

describe('generate graphql-client-code command', () => {
const graphqlClientCodeGeneratorAdapter =
new GraphqlClientCodeGeneratorAdapter(fromNodeProviderChain());

const generateClientConfigMock = mock.method(
graphqlClientCodeGeneratorAdapter,
'generateGraphqlClientCodeToFile',
() => Promise.resolve()
);

const generateGraphqlClientCodeCommand = new GenerateGraphqlClientCodeCommand(
graphqlClientCodeGeneratorAdapter,
{ resolve: () => Promise.resolve('testAppName') }
);
const parser = yargs().command(
generateGraphqlClientCodeCommand as unknown as CommandModule
);
const commandRunner = new TestCommandRunner(parser);

beforeEach(() => {
generateClientConfigMock.mock.resetCalls();
});

it('generates and writes graphql client code for stack', async () => {
await commandRunner.runCommand('graphql-client-code --stack stack_name');
assert.equal(generateClientConfigMock.mock.callCount(), 1);
assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], {
stackName: 'stack_name',
});
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 () => {
await commandRunner.runCommand('graphql-client-code --branch branch_name');
assert.equal(generateClientConfigMock.mock.callCount(), 1);
assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], {
appName: 'testAppName',
branchName: 'branch_name',
});
// 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 () => {
await commandRunner.runCommand(
'graphql-client-code --branch branch_name --appId app_id'
);
assert.equal(generateClientConfigMock.mock.callCount(), 1);
assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], {
backendId: 'app_id',
branchName: 'branch_name',
});
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 () => {
await commandRunner.runCommand(
'graphql-client-code --stack stack_name --out /foo/bar --format ts'
);
assert.equal(generateClientConfigMock.mock.callCount(), 1);
assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], {
stackName: 'stack_name',
});
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 () => {
await commandRunner.runCommand(
'graphql-client-code --stack stack_name --out foo/bar --format js'
);
assert.equal(generateClientConfigMock.mock.callCount(), 1);
assert.deepEqual(generateClientConfigMock.mock.calls[0].arguments[0], {
stackName: 'stack_name',
});
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 () => {
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, /--out/);
});

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;
}
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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';

export const formatChoices = ['js', 'json', 'ts'] as const;
export const configFileName = 'amplifyconfiguration';

export type GenerateGraphqlClientCodeCommandOptions = {
stack: string | undefined;
appId: string | undefined;
branch: string | undefined;
format: (typeof formatChoices)[number] | undefined;
out: string | undefined;
};

/**
* Command that generates client config.
*/
export class GenerateGraphqlClientCodeCommand
implements CommandModule<object, GenerateGraphqlClientCodeCommandOptions>
{
/**
* @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 graphqlClientCodeGeneratorAdapter: GraphqlClientCodeGeneratorAdapter,
private readonly appNameResolver: AppNameResolver
) {
this.command = 'graphql-client-code';
this.describe = 'Generates graphql API code';
}

/**
* @inheritDoc
*/
handler = async (
args: ArgumentsCamelCase<GenerateGraphqlClientCodeCommandOptions>
): Promise<void> => {
const defaultArgs = {
out: process.cwd(),
format: 'js',
};
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}`
);

await this.graphqlClientCodeGeneratorAdapter.generateGraphqlClientCodeToFile(
backendIdentifier,
targetPath
);
};

/**
* Translates args to BackendIdentifier.
* Throws if translation can't be made (this should never happen if command validation works correctly).
*/
private getBackendIdentifier = async (
args: ArgumentsCamelCase<GenerateGraphqlClientCodeCommandOptions>
): Promise<BackendIdentifier> => {
if (args.stack) {
return { stackName: args.stack };
} else if (args.appId && args.branch) {
return { backendId: args.appId, branchName: args.branch };
} else if (args.branch) {
return {
appName: await this.appNameResolver.resolve(),
branchName: args.branch,
};
}
throw this.missingArgsError;
};

/**
* @inheritDoc
*/
builder = (yargs: Argv): Argv<GenerateGraphqlClientCodeCommandOptions> => {
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('format', {
describe: 'The format which the configuration should be exported into.',
type: 'string',
array: false,
choices: formatChoices,
})
.option('out', {
describe:
'A path to directory where config is written. If not provided defaults to current process working directory.',
type: 'string',
array: false,
})
.check((argv) => {
if (!argv.stack && !argv.branch) {
throw this.missingArgsError;
}
return true;
});
};
}
Loading

0 comments on commit 0b72138

Please sign in to comment.