Skip to content

Commit dce8a0e

Browse files
authored
Feat: Use sandbox id by default for generation commands (#669)
* feat(formgen): default to sandbox identifier when branch or stack is not passed to the formgen command
1 parent 730afd8 commit dce8a0e

11 files changed

+191
-28
lines changed

.changeset/giant-dolls-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/backend-cli': minor
3+
---
4+
5+
Defaults to the sandbox identifier when no branch or stack is passed in the CLI

packages/cli/src/backend-identifier/backend_identifier_resolver.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import assert from 'node:assert';
22
import { describe, it } from 'node:test';
3-
import { BackendIdentifierResolver } from './backend_identifier_resolver.js';
3+
import { AppBackendIdentifierResolver } from './backend_identifier_resolver.js';
44

55
void describe('BackendIdentifierResolver', () => {
66
void it('returns an App Name and Branch identifier', async () => {
7-
const backendIdResolver = new BackendIdentifierResolver({
7+
const backendIdResolver = new AppBackendIdentifierResolver({
88
resolve: () => Promise.resolve('testAppName'),
99
});
1010
assert.deepStrictEqual(
@@ -16,7 +16,7 @@ void describe('BackendIdentifierResolver', () => {
1616
);
1717
});
1818
void it('returns a App Id identifier', async () => {
19-
const backendIdResolver = new BackendIdentifierResolver({
19+
const backendIdResolver = new AppBackendIdentifierResolver({
2020
resolve: () => Promise.resolve('testAppName'),
2121
});
2222
const actual = await backendIdResolver.resolve({
@@ -30,7 +30,7 @@ void describe('BackendIdentifierResolver', () => {
3030
});
3131
});
3232
void it('returns a Stack name identifier', async () => {
33-
const backendIdResolver = new BackendIdentifierResolver({
33+
const backendIdResolver = new AppBackendIdentifierResolver({
3434
resolve: () => Promise.resolve('testAppName'),
3535
});
3636
assert.deepEqual(await backendIdResolver.resolve({ stack: 'my-stack' }), {

packages/cli/src/backend-identifier/backend_identifier_resolver.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
import { DeployedBackendIdentifier } from '@aws-amplify/deployed-backend-client';
22
import { NamespaceResolver } from './local_namespace_resolver.js';
33

4-
type BackendIdentifierParameters = {
4+
export type BackendIdentifierParameters = {
55
stack?: string;
66
appId?: string;
77
branch?: string;
88
};
9+
10+
export type BackendIdentifierResolver = {
11+
resolve: (
12+
args: BackendIdentifierParameters
13+
) => Promise<DeployedBackendIdentifier | undefined>;
14+
};
15+
916
/**
1017
* Translates args to BackendIdentifier.
1118
* Throws if translation can't be made (this should never happen if command validation works correctly).
1219
*/
13-
export class BackendIdentifierResolver {
20+
export class AppBackendIdentifierResolver implements BackendIdentifierResolver {
1421
/**
1522
* Instantiates BackendIdentifierResolver
1623
*/
1724
constructor(private readonly namespaceResolver: NamespaceResolver) {}
1825
resolve = async (
1926
args: BackendIdentifierParameters
20-
): Promise<DeployedBackendIdentifier> => {
27+
): Promise<DeployedBackendIdentifier | undefined> => {
2128
if (args.stack) {
2229
return { stackName: args.stack };
2330
} else if (args.appId && args.branch) {
@@ -32,8 +39,6 @@ export class BackendIdentifierResolver {
3239
branchName: args.branch,
3340
};
3441
}
35-
throw new Error(
36-
'Unable to resolve BackendIdentifier with provided parameters'
37-
);
42+
return undefined;
3843
};
3944
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import assert from 'node:assert';
2+
import { it } from 'node:test';
3+
import { SandboxBackendIdResolver } from '../commands/sandbox/sandbox_id_resolver.js';
4+
import { AppBackendIdentifierResolver } from './backend_identifier_resolver.js';
5+
import { BackendIdentifierResolverWithFallback } from './backend_identifier_with_sandbox_fallback.js';
6+
7+
void it('if backend identifier resolves without error, the resolved id is returned', async () => {
8+
const namespaceResolver = {
9+
resolve: () => Promise.resolve('testAppName'),
10+
};
11+
12+
const defaultResolver = new AppBackendIdentifierResolver(namespaceResolver);
13+
const sandboxResolver = new SandboxBackendIdResolver(namespaceResolver);
14+
const backendIdResolver = new BackendIdentifierResolverWithFallback(
15+
defaultResolver,
16+
sandboxResolver
17+
);
18+
const resolvedId = await backendIdResolver.resolve({
19+
appId: 'hello',
20+
branch: 'world',
21+
});
22+
assert.deepEqual(resolvedId, {
23+
namespace: 'hello',
24+
name: 'world',
25+
type: 'branch',
26+
});
27+
});
28+
29+
void it('uses the sandbox id if the default identifier resolver fails', async () => {
30+
const appName = 'testAppName';
31+
const namespaceResolver = {
32+
resolve: () => Promise.resolve(appName),
33+
};
34+
35+
const defaultResolver = new AppBackendIdentifierResolver(namespaceResolver);
36+
const username = 'test-user';
37+
const sandboxResolver = new SandboxBackendIdResolver(
38+
namespaceResolver,
39+
() =>
40+
({
41+
username,
42+
} as never)
43+
);
44+
const backendIdResolver = new BackendIdentifierResolverWithFallback(
45+
defaultResolver,
46+
sandboxResolver
47+
);
48+
const resolvedId = await backendIdResolver.resolve({});
49+
assert.deepEqual(resolvedId, {
50+
namespace: appName,
51+
type: 'sandbox',
52+
name: 'test-user',
53+
});
54+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { SandboxBackendIdResolver } from '../commands/sandbox/sandbox_id_resolver.js';
2+
import {
3+
BackendIdentifierParameters,
4+
BackendIdentifierResolver,
5+
} from './backend_identifier_resolver.js';
6+
7+
/**
8+
* Resolves the backend id when branch or stack is passed as an arg, otherwise returns a sandbox backend identifier
9+
*/
10+
export class BackendIdentifierResolverWithFallback
11+
implements BackendIdentifierResolver
12+
{
13+
/**
14+
* Accepts the sandbox id resolver as a parameter
15+
*/
16+
constructor(
17+
private defaultResolver: BackendIdentifierResolver,
18+
private fallbackResolver: SandboxBackendIdResolver
19+
) {}
20+
/**
21+
* resolves the backend id, falling back to the sandbox id if there is an error
22+
*/
23+
resolve = async (args: BackendIdentifierParameters) => {
24+
return (
25+
(await this.defaultResolver.resolve(args)) ??
26+
(await this.fallbackResolver.resolve())
27+
);
28+
};
29+
}

packages/cli/src/commands/generate/config/generate_config_command.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ClientConfigFormat } from '@aws-amplify/client-config';
55
import yargs, { CommandModule } from 'yargs';
66
import { TestCommandRunner } from '../../../test-utils/command_runner.js';
77
import assert from 'node:assert';
8-
import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
8+
import { AppBackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
99
import { ClientConfigGeneratorAdapter } from '../../../client-config/client_config_generator_adapter.js';
1010

1111
void describe('generate config command', () => {
@@ -19,7 +19,7 @@ void describe('generate config command', () => {
1919
() => Promise.resolve()
2020
);
2121

22-
const backendIdResolver = new BackendIdentifierResolver({
22+
const backendIdResolver = new AppBackendIdentifierResolver({
2323
resolve: () => Promise.resolve('testAppName'),
2424
});
2525
const generateConfigCommand = new GenerateConfigCommand(

packages/cli/src/commands/generate/config/generate_config_command.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export class GenerateConfigCommand
5151
args
5252
);
5353

54+
if (!backendIdentifier) {
55+
throw new Error('Could not resolve the backend identifier');
56+
}
57+
5458
await this.clientConfigGenerator.generateClientConfigToFile(
5559
backendIdentifier,
5660
args['out-dir'],

packages/cli/src/commands/generate/forms/generate_forms_command.test.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@ import assert from 'node:assert';
55
import path from 'node:path';
66
import { describe, it, mock } from 'node:test';
77
import yargs, { CommandModule } from 'yargs';
8-
import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
8+
import { AppBackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
9+
import { BackendIdentifierResolverWithFallback } from '../../../backend-identifier/backend_identifier_with_sandbox_fallback.js';
910
import { FormGenerationHandler } from '../../../form-generation/form_generation_handler.js';
1011
import { TestCommandRunner } from '../../../test-utils/command_runner.js';
12+
import { SandboxBackendIdResolver } from '../../sandbox/sandbox_id_resolver.js';
1113
import { GenerateFormsCommand } from './generate_forms_command.js';
1214

1315
void describe('generate forms command', () => {
1416
void describe('form generation validation', () => {
1517
void it('models are generated in ${out-dir}/graphql', async () => {
1618
const credentialProvider = fromNodeProviderChain();
1719

18-
const backendIdResolver = new BackendIdentifierResolver({
20+
const backendIdResolver = new AppBackendIdentifierResolver({
1921
resolve: () => Promise.resolve('testAppName'),
2022
});
2123
const formGenerationHandler = new FormGenerationHandler({
@@ -62,7 +64,7 @@ void describe('generate forms command', () => {
6264
void it('out-dir path can be customized', async () => {
6365
const credentialProvider = fromNodeProviderChain();
6466

65-
const backendIdResolver = new BackendIdentifierResolver({
67+
const backendIdResolver = new AppBackendIdentifierResolver({
6668
resolve: () => Promise.resolve('testAppName'),
6769
});
6870
const formGenerationHandler = new FormGenerationHandler({
@@ -109,7 +111,7 @@ void describe('generate forms command', () => {
109111
void it('./ui-components is the default graphql model generation path', async () => {
110112
const credentialProvider = fromNodeProviderChain();
111113

112-
const backendIdResolver = new BackendIdentifierResolver({
114+
const backendIdResolver = new AppBackendIdentifierResolver({
113115
resolve: () => Promise.resolve('testAppName'),
114116
});
115117
const formGenerationHandler = new FormGenerationHandler({
@@ -150,4 +152,63 @@ void describe('generate forms command', () => {
150152
);
151153
});
152154
});
155+
void it('if neither branch nor stack are provided, the sandbox id is used by default', async () => {
156+
const credentialProvider = fromNodeProviderChain();
157+
158+
const appNameResolver = {
159+
resolve: () => Promise.resolve('testAppName'),
160+
};
161+
162+
const defaultResolver = new AppBackendIdentifierResolver(appNameResolver);
163+
164+
const mockedSandboxIdResolver = new SandboxBackendIdResolver(
165+
appNameResolver
166+
);
167+
168+
const fakeSandboxId = 'my-fake-app-my-fake-username';
169+
170+
const sandboxIdResolver = mock.method(mockedSandboxIdResolver, 'resolve');
171+
sandboxIdResolver.mock.mockImplementation(() => fakeSandboxId);
172+
173+
const backendIdResolver = new BackendIdentifierResolverWithFallback(
174+
defaultResolver,
175+
mockedSandboxIdResolver
176+
);
177+
const formGenerationHandler = new FormGenerationHandler({
178+
credentialProvider,
179+
});
180+
181+
const fakedBackendOutputClient = BackendOutputClientFactory.getInstance({
182+
credentials: credentialProvider,
183+
});
184+
185+
const generateFormsCommand = new GenerateFormsCommand(
186+
backendIdResolver,
187+
() => fakedBackendOutputClient,
188+
formGenerationHandler
189+
);
190+
191+
const generationMock = mock.method(formGenerationHandler, 'generate');
192+
generationMock.mock.mockImplementation(async () => undefined);
193+
mock
194+
.method(fakedBackendOutputClient, 'getOutput')
195+
.mock.mockImplementation(async () => ({
196+
[graphqlOutputKey]: {
197+
payload: {
198+
awsAppsyncApiId: 'test_api_id',
199+
amplifyApiModelSchemaS3Uri: 'test_schema',
200+
awsAppsyncApiEndpoint: 'test_endpoint',
201+
},
202+
},
203+
}));
204+
const parser = yargs().command(
205+
generateFormsCommand as unknown as CommandModule
206+
);
207+
const commandRunner = new TestCommandRunner(parser);
208+
await commandRunner.runCommand('forms');
209+
assert.deepEqual(
210+
generationMock.mock.calls[0].arguments[0].backendIdentifier,
211+
fakeSandboxId
212+
);
213+
});
153214
});

packages/cli/src/commands/generate/forms/generate_forms_command.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export class GenerateFormsCommand
4747
this.describe = 'Generates UI forms';
4848
}
4949

50+
getBackendIdentifier = async (args: GenerateFormsCommandOptions) => {
51+
return await this.backendIdentifierResolver.resolve(args);
52+
};
53+
5054
/**
5155
* @inheritDoc
5256
*/
@@ -55,6 +59,10 @@ export class GenerateFormsCommand
5559
args
5660
);
5761

62+
if (!backendIdentifier) {
63+
throw new Error('Could not resolve the backend identifier');
64+
}
65+
5866
const backendOutputClient = this.backendOutputClientBuilder();
5967

6068
const output = await backendOutputClient.getOutput(backendIdentifier);
@@ -121,12 +129,6 @@ export class GenerateFormsCommand
121129
array: true,
122130
group: 'Form Generation',
123131
})
124-
.check((argv) => {
125-
if (!argv.stack && !argv.branch) {
126-
throw new Error('Either --stack or --branch must be provided');
127-
}
128-
return true;
129-
})
130132
.fail((msg, err) => {
131133
handleCommandFailure(msg, err, yargs);
132134
yargs.exit(1, err);

packages/cli/src/commands/generate/generate_command_factory.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { GenerateFormsCommand } from './forms/generate_forms_command.js';
66
import { CwdPackageJsonReader } from '@aws-amplify/platform-core';
77
import { GenerateGraphqlClientCodeCommand } from './graphql-client-code/generate_graphql_client_code_command.js';
88
import { LocalNamespaceResolver } from '../../backend-identifier/local_namespace_resolver.js';
9-
import { BackendIdentifierResolver } from '../../backend-identifier/backend_identifier_resolver.js';
109
import { ClientConfigGeneratorAdapter } from '../../client-config/client_config_generator_adapter.js';
1110
import { GenerateApiCodeAdapter } from './graphql-client-code/generate_api_code_adapter.js';
1211
import { FormGenerationHandler } from '../../form-generation/form_generation_handler.js';
1312
import { BackendOutputClientFactory } from '@aws-amplify/deployed-backend-client';
13+
import { SandboxBackendIdResolver } from '../sandbox/sandbox_id_resolver.js';
1414
import { CommandMiddleware } from '../../command_middleware.js';
15+
import { BackendIdentifierResolverWithFallback } from '../../backend-identifier/backend_identifier_with_sandbox_fallback.js';
16+
import { AppBackendIdentifierResolver } from '../../backend-identifier/backend_identifier_resolver.js';
1517

1618
/**
1719
* Creates wired generate command.
@@ -21,12 +23,13 @@ export const createGenerateCommand = (): CommandModule => {
2123
const clientConfigGenerator = new ClientConfigGeneratorAdapter(
2224
credentialProvider
2325
);
24-
const localAppNameResolver = new LocalNamespaceResolver(
26+
const namespaceResolver = new LocalNamespaceResolver(
2527
new CwdPackageJsonReader()
2628
);
2729

28-
const backendIdentifierResolver = new BackendIdentifierResolver(
29-
localAppNameResolver
30+
const backendIdentifierResolver = new BackendIdentifierResolverWithFallback(
31+
new AppBackendIdentifierResolver(namespaceResolver),
32+
new SandboxBackendIdResolver(namespaceResolver)
3033
);
3134

3235
const generateConfigCommand = new GenerateConfigCommand(

packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import yargs, { CommandModule } from 'yargs';
55
import { TestCommandRunner } from '../../../test-utils/command_runner.js';
66
import assert from 'node:assert';
77
import path from 'path';
8-
import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
8+
import { AppBackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js';
99
import { GenerateApiCodeAdapter } from './generate_api_code_adapter.js';
1010
import {
1111
GenerateApiCodeFormat,
@@ -29,7 +29,7 @@ void describe('generate graphql-client-code command', () => {
2929
})
3030
);
3131

32-
const backendIdentifierResolver = new BackendIdentifierResolver({
32+
const backendIdentifierResolver = new AppBackendIdentifierResolver({
3333
resolve: () => Promise.resolve('testAppName'),
3434
});
3535
const generateGraphqlClientCodeCommand = new GenerateGraphqlClientCodeCommand(

0 commit comments

Comments
 (0)