Skip to content

Commit

Permalink
generate client code command (#265)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>
  • Loading branch information
2 people authored and Abhishek Raj committed Sep 27, 2023
1 parent 2fd970e commit 5523bc9
Show file tree
Hide file tree
Showing 12 changed files with 712 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/old-pumpkins-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/backend-cli': minor
---

Add generate graphql-client-code command with mocked implementation
1 change: 1 addition & 0 deletions .eslint_dictionary.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default [
'codegen',
'cognito',
'ctor',
'datastore',
'debounce',
'declarator',
'deployer',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
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 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 () => {
Expand Down
14 changes: 13 additions & 1 deletion packages/cli/src/commands/generate/generate_command_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
);
};
Original file line number Diff line number Diff line change
@@ -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<GenerationResult> =>
generateApiCode({
...props,
credentialProvider: this.credentialProvider,
});
}
Original file line number Diff line number Diff line change
@@ -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;
}
);
});
});
Loading

0 comments on commit 5523bc9

Please sign in to comment.