Skip to content

Feat: Use sandbox id by default for generation commands #669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/giant-dolls-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/backend-cli': minor
---

Defaults to the sandbox identifier when no branch or stack is passed in the CLI
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ type BackendIdentifierParameters = {
appId?: string;
branch?: string;
};

export type BackendIdentityResolver = {
Copy link
Contributor

@edwardfoyle edwardfoyle Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having BackendIdentifierResolver and BackendIdentityResolver is confusing. It's difficult to know what the difference between each is. Could we have a single BackendIdentifierResolver type with two implementation: one that resolves a single type of BackendId and another that goes through the fallback chain?

If that proves to be too much refactoring, I think I'd rather just add the sandbox fallback directly to the existing BackendIdentifierResolver

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve: (
args: BackendIdentifierParameters
) => Promise<DeployedBackendIdentifier | undefined>;
};

/**
* Translates args to BackendIdentifier.
* Throws if translation can't be made (this should never happen if command validation works correctly).
Expand All @@ -17,7 +24,7 @@ export class BackendIdentifierResolver {
constructor(private readonly namespaceResolver: NamespaceResolver) {}
resolve = async (
args: BackendIdentifierParameters
): Promise<DeployedBackendIdentifier> => {
): Promise<DeployedBackendIdentifier | undefined> => {
if (args.stack) {
return { stackName: args.stack };
} else if (args.appId && args.branch) {
Expand All @@ -32,8 +39,6 @@ export class BackendIdentifierResolver {
branchName: args.branch,
};
}
throw new Error(
'Unable to resolve BackendIdentifier with provided parameters'
);
return undefined;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import assert from 'node:assert';
import { it } from 'node:test';
import { SandboxBackendIdResolver } from '../commands/sandbox/sandbox_id_resolver.js';
import { BackendIdentifierResolver } from './backend_identifier_resolver.js';
import { BackendIdentifierResolverWithFallback } from './backend_identifier_with_sandbox_fallback.js';

void it('if backend identifier resolves without error, the resolved id is returned', async () => {
const namespaceResolver = {
resolve: () => Promise.resolve('testAppName'),
};

const defaultResolver = new BackendIdentifierResolver(namespaceResolver);
const sandboxResolver = new SandboxBackendIdResolver(namespaceResolver);
const backendIdResolver = new BackendIdentifierResolverWithFallback(
defaultResolver,
sandboxResolver
);
const resolvedId = await backendIdResolver.resolve({
appId: 'hello',
branch: 'world',
});
assert.deepEqual(resolvedId, {
namespace: 'hello',
name: 'world',
type: 'branch',
});
});

void it('uses the sandbox id if the default identifier resolver fails', async () => {
const appName = 'testAppName';
const namespaceResolver = {
resolve: () => Promise.resolve(appName),
};

const defaultResolver = new BackendIdentifierResolver(namespaceResolver);
const username = 'test-user';
const sandboxResolver = new SandboxBackendIdResolver(
namespaceResolver,
() =>
({
username,
} as never)
);
const backendIdResolver = new BackendIdentifierResolverWithFallback(
defaultResolver,
sandboxResolver
);
const resolvedId = await backendIdResolver.resolve({});
assert.deepEqual(resolvedId, {
namespace: appName,
type: 'sandbox',
name: 'test-user',
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SandboxBackendIdResolver } from '../commands/sandbox/sandbox_id_resolver.js';
import { BackendIdentifierResolver } from './backend_identifier_resolver.js';

/**
* Resolves the backend id when branch or stack is passed as an arg, otherwise returns a sandbox backend identifier
*/
export class BackendIdentifierResolverWithFallback {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this implement BackendIdentityResolver?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
* Accepts the sandbox id resolver as a parameter
*/
constructor(
private defaultResolver: BackendIdentifierResolver,
private fallbackResolver: SandboxBackendIdResolver
) {}
/**
* resolves the backend id, falling back to the sandbox id if there is an error
*/
public resolve = async (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: public is the default

...args: Parameters<BackendIdentifierResolver['resolve']>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty sure this type will be inferred from the implementing interface. If not, just type it with BackendIdentifierParameters

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yep now that this is implementing the interface, this should be removed.

) => {
return (
(await this.defaultResolver.resolve(...args)) ??
(await this.fallbackResolver.resolve())
);
};
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Argv, CommandModule } from 'yargs';
import { ClientConfigFormat } from '@aws-amplify/client-config';
import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
import { BackendIdentityResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
import { ClientConfigGeneratorAdapter } from '../../../client-config/client_config_generator_adapter.js';
import { ArgumentsKebabCase } from '../../../kebab_case.js';
import { handleCommandFailure } from '../../../command_failure_handler.js';
Expand Down Expand Up @@ -37,7 +37,7 @@ export class GenerateConfigCommand
*/
constructor(
private readonly clientConfigGenerator: ClientConfigGeneratorAdapter,
private readonly backendIdentifierResolver: BackendIdentifierResolver
private readonly backendIdentifierResolver: BackendIdentityResolver
) {
this.command = 'config';
this.describe = 'Generates client config';
Expand All @@ -51,6 +51,10 @@ export class GenerateConfigCommand
args
);

if (!backendIdentifier) {
throw new Error('Could not resolve the backend identifier');
}

await this.clientConfigGenerator.generateClientConfigToFile(
backendIdentifier,
args['out-dir'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import path from 'node:path';
import { describe, it, mock } from 'node:test';
import yargs, { CommandModule } from 'yargs';
import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
import { BackendIdentifierResolverWithFallback } from '../../../backend-identifier/backend_identifier_with_sandbox_fallback.js';
import { FormGenerationHandler } from '../../../form-generation/form_generation_handler.js';
import { TestCommandRunner } from '../../../test-utils/command_runner.js';
import { SandboxBackendIdResolver } from '../../sandbox/sandbox_id_resolver.js';
import { GenerateFormsCommand } from './generate_forms_command.js';

void describe('generate forms command', () => {
Expand Down Expand Up @@ -150,4 +152,63 @@ void describe('generate forms command', () => {
);
});
});
void it('if neither branch nor stack are provided, the sandbox id is used by default', async () => {
const credentialProvider = fromNodeProviderChain();

const appNameResolver = {
resolve: () => Promise.resolve('testAppName'),
};

const defaultResolver = new BackendIdentifierResolver(appNameResolver);

const mockedSandboxIdResolver = new SandboxBackendIdResolver(
appNameResolver
);

const fakeSandboxId = 'my-fake-app-my-fake-username';

const sandboxIdResolver = mock.method(mockedSandboxIdResolver, 'resolve');
sandboxIdResolver.mock.mockImplementation(() => fakeSandboxId);

const backendIdResolver = new BackendIdentifierResolverWithFallback(
defaultResolver,
mockedSandboxIdResolver
);
const formGenerationHandler = new FormGenerationHandler({
credentialProvider,
});

const fakedBackendOutputClient = BackendOutputClientFactory.getInstance({
credentials: credentialProvider,
});

const generateFormsCommand = new GenerateFormsCommand(
backendIdResolver,
() => fakedBackendOutputClient,
formGenerationHandler
);

const generationMock = mock.method(formGenerationHandler, 'generate');
generationMock.mock.mockImplementation(async () => undefined);
mock
.method(fakedBackendOutputClient, 'getOutput')
.mock.mockImplementation(async () => ({
[graphqlOutputKey]: {
payload: {
awsAppsyncApiId: 'test_api_id',
amplifyApiModelSchemaS3Uri: 'test_schema',
awsAppsyncApiEndpoint: 'test_endpoint',
},
},
}));
const parser = yargs().command(
generateFormsCommand as unknown as CommandModule
);
const commandRunner = new TestCommandRunner(parser);
await commandRunner.runCommand('forms');
assert.deepEqual(
generationMock.mock.calls[0].arguments[0].backendIdentifier,
fakeSandboxId
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import path from 'path';
import { Argv, CommandModule } from 'yargs';
import { BackendOutputClient } from '@aws-amplify/deployed-backend-client';
import { graphqlOutputKey } from '@aws-amplify/backend-output-schemas';
import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
import { BackendIdentityResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
import { DEFAULT_UI_PATH } from '../../../form-generation/default_form_generation_output_paths.js';
import { FormGenerationHandler } from '../../../form-generation/form_generation_handler.js';
import { ArgumentsKebabCase } from '../../../kebab_case.js';
Expand Down Expand Up @@ -39,14 +39,18 @@ export class GenerateFormsCommand
* Creates client config generation command.
*/
constructor(
private readonly backendIdentifierResolver: BackendIdentifierResolver,
private readonly backendIdentifierResolver: BackendIdentityResolver,
private readonly backendOutputClientBuilder: () => BackendOutputClient,
private readonly formGenerationHandler: FormGenerationHandler
) {
this.command = 'forms';
this.describe = 'Generates UI forms';
}

getBackendIdentifier = async (args: GenerateFormsCommandOptions) => {
return await this.backendIdentifierResolver.resolve(args);
};

/**
* @inheritDoc
*/
Expand All @@ -55,6 +59,10 @@ export class GenerateFormsCommand
args
);

if (!backendIdentifier) {
throw new Error('Could not resolve the backend identifier');
}

const backendOutputClient = this.backendOutputClientBuilder();

const output = await backendOutputClient.getOutput(backendIdentifier);
Expand Down Expand Up @@ -121,12 +129,6 @@ export class GenerateFormsCommand
array: true,
group: 'Form Generation',
})
.check((argv) => {
if (!argv.stack && !argv.branch) {
throw new Error('Either --stack or --branch must be provided');
}
return true;
})
.fail((msg, err) => {
handleCommandFailure(msg, err, yargs);
yargs.exit(1, err);
Expand Down
11 changes: 7 additions & 4 deletions packages/cli/src/commands/generate/generate_command_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { GenerateFormsCommand } from './forms/generate_forms_command.js';
import { CwdPackageJsonReader } from '@aws-amplify/platform-core';
import { GenerateGraphqlClientCodeCommand } from './graphql-client-code/generate_graphql_client_code_command.js';
import { LocalNamespaceResolver } from '../../backend-identifier/local_namespace_resolver.js';
import { BackendIdentifierResolver } from '../../backend-identifier/backend_identifier_resolver.js';
import { ClientConfigGeneratorAdapter } from '../../client-config/client_config_generator_adapter.js';
import { GenerateApiCodeAdapter } from './graphql-client-code/generate_api_code_adapter.js';
import { FormGenerationHandler } from '../../form-generation/form_generation_handler.js';
import { BackendOutputClientFactory } from '@aws-amplify/deployed-backend-client';
import { SandboxBackendIdResolver } from '../sandbox/sandbox_id_resolver.js';
import { CommandMiddleware } from '../../command_middleware.js';
import { BackendIdentifierResolverWithFallback } from '../../backend-identifier/backend_identifier_with_sandbox_fallback.js';
import { BackendIdentifierResolver } from '../../backend-identifier/backend_identifier_resolver.js';

/**
* Creates wired generate command.
Expand All @@ -21,12 +23,13 @@ export const createGenerateCommand = (): CommandModule => {
const clientConfigGenerator = new ClientConfigGeneratorAdapter(
credentialProvider
);
const localAppNameResolver = new LocalNamespaceResolver(
const namespaceResolver = new LocalNamespaceResolver(
new CwdPackageJsonReader()
);

const backendIdentifierResolver = new BackendIdentifierResolver(
localAppNameResolver
const backendIdentifierResolver = new BackendIdentifierResolverWithFallback(
new BackendIdentifierResolver(namespaceResolver),
new SandboxBackendIdResolver(namespaceResolver)
);

const generateConfigCommand = new GenerateConfigCommand(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Argv, CommandModule } from 'yargs';
import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
import { BackendIdentityResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
import { isAbsolute, resolve } from 'path';
import {
GenerateApiCodeAdapter,
Expand Down Expand Up @@ -78,7 +78,7 @@ export class GenerateGraphqlClientCodeCommand
*/
constructor(
private readonly generateApiCodeAdapter: GenerateApiCodeAdapter,
private readonly backendIdentifierResolver: BackendIdentifierResolver
private readonly backendIdentifierResolver: BackendIdentityResolver
) {
this.command = 'graphql-client-code';
this.describe = 'Generates graphql API code';
Expand Down