diff --git a/packages/amplify-cli-core/API.md b/packages/amplify-cli-core/API.md index c5f833aab56..f5976b28766 100644 --- a/packages/amplify-cli-core/API.md +++ b/packages/amplify-cli-core/API.md @@ -2204,7 +2204,7 @@ export function validateExportDirectoryPath(directoryPath: any, defaultPath: str export class ViewResourceTableParams { constructor(cliParams: CLIParams); // (undocumented) - get categoryList(): string[] | []; + get categoryList(): [] | string[]; // (undocumented) get command(): string; // (undocumented) diff --git a/packages/amplify-gen1-codegen-data-adapter/API.md b/packages/amplify-gen1-codegen-data-adapter/API.md index 7b0dd3d2eb9..aac4d5aca9a 100644 --- a/packages/amplify-gen1-codegen-data-adapter/API.md +++ b/packages/amplify-gen1-codegen-data-adapter/API.md @@ -4,11 +4,11 @@ ```ts -import { DataDefinition } from '@aws-amplify/amplify-gen2-codegen'; +import { DataTableMapping } from '@aws-amplify/amplify-gen2-codegen'; import { Stack } from '@aws-sdk/client-cloudformation'; // @public (undocumented) -export const getDataDefinition: (dataStack: Stack) => DataDefinition; +export const getDataDefinition: (dataStack: Stack) => DataTableMapping; // (No @packageDocumentation comment for this package) diff --git a/packages/amplify-gen1-codegen-data-adapter/src/get_data_definition.test.ts b/packages/amplify-gen1-codegen-data-adapter/src/get_data_definition.test.ts index eeeae428c4c..919bb7fe352 100644 --- a/packages/amplify-gen1-codegen-data-adapter/src/get_data_definition.test.ts +++ b/packages/amplify-gen1-codegen-data-adapter/src/get_data_definition.test.ts @@ -13,6 +13,6 @@ describe('Data definition', () => { ], } as Stack; const result = getDataDefinition(stack); - assert.equal(result.tableMapping.hello, 'world'); + assert.equal(result.hello, 'world'); }); }); diff --git a/packages/amplify-gen1-codegen-data-adapter/src/get_data_definition.ts b/packages/amplify-gen1-codegen-data-adapter/src/get_data_definition.ts index 1139ea2d14e..18f5d2cddf1 100644 --- a/packages/amplify-gen1-codegen-data-adapter/src/get_data_definition.ts +++ b/packages/amplify-gen1-codegen-data-adapter/src/get_data_definition.ts @@ -1,12 +1,12 @@ import assert from 'node:assert'; -import { DataDefinition } from '@aws-amplify/amplify-gen2-codegen'; +import { DataTableMapping } from '@aws-amplify/amplify-gen2-codegen'; import { Stack } from '@aws-sdk/client-cloudformation'; export const tableMappingKey = 'DataSourceMappingOutput'; -export const getDataDefinition = (dataStack: Stack): DataDefinition => { +export const getDataDefinition = (dataStack: Stack): DataTableMapping => { const rawTableMapping = dataStack.Outputs?.find((o) => o.OutputKey === tableMappingKey)?.OutputValue; assert(rawTableMapping); const tableMapping = JSON.parse(rawTableMapping); - return { tableMapping }; + return tableMapping; }; diff --git a/packages/amplify-gen2-codegen/API.md b/packages/amplify-gen2-codegen/API.md index 1f2fd31e9cc..398698f865a 100644 --- a/packages/amplify-gen2-codegen/API.md +++ b/packages/amplify-gen2-codegen/API.md @@ -81,9 +81,12 @@ export type CustomAttributes = Partial; + tableMappings: Record; }; +// @public (undocumented) +export type DataTableMapping = Record; + // @public (undocumented) export type EmailOptions = { emailVerificationBody: string; diff --git a/packages/amplify-gen2-codegen/src/data/source_builder.test.ts b/packages/amplify-gen2-codegen/src/data/source_builder.test.ts index 74ac6df9529..b8eaaa84b2e 100644 --- a/packages/amplify-gen2-codegen/src/data/source_builder.test.ts +++ b/packages/amplify-gen2-codegen/src/data/source_builder.test.ts @@ -16,23 +16,40 @@ describe('Data Category code generation', () => { }); describe('import map', () => { it('is rendered', () => { - const tableMapping = { Todo: 'my-todo-mapping' }; - const source = printNodeArray(generateDataSource({ tableMapping })); - assert.match(source, /importedAmplifyDynamoDBTableMap: \{\s+Todo: ['"]my-todo-mapping['"]/); + const tableMappings = { dev: { Todo: 'my-todo-mapping' } }; + const source = printNodeArray(generateDataSource({ tableMappings })); + assert.match( + source, + /\/\/ Replace each environment name with the corresponding branch name. Use ['"]sandbox['"] for your sandbox environment.\n\s+importedAmplifyDynamoDBTableMap: \{\s+dev: { Todo: ['"]my-todo-mapping['"] } }/, + ); }); - it('shows each key in the mapping table in the `importedModels` array', () => { - const tables = ['Todo', 'Foo', 'Bar']; - const tableMapping = tables.reduce((prev, curr) => ({ ...prev, [curr]: 'baz' }), {}); - const source = printNodeArray(generateDataSource({ tableMapping })); - const array = source.match(/importedModels:\s+\[(.*?)\]/); - assert.deepEqual(tables, array?.[1].replaceAll('"', '').split(', ')); + it('includes multiple table mappings', () => { + const tableMappings = { + dev: { Todo: 'my-todo-mapping' }, + prod: { Todo: 'my-todo-mapping-prod' }, + }; + const source = printNodeArray(generateDataSource({ tableMappings })); + assert.match( + source, + /importedAmplifyDynamoDBTableMap: \{\s+dev: { Todo: ['"]my-todo-mapping['"] }, prod: { Todo: ['"]my-todo-mapping-prod['"] } }/, + ); + }); + it('includes a comment for missing table mappings', () => { + const tableMappings = { + dev: undefined, + }; + const source = printNodeArray(generateDataSource({ tableMappings })); + assert.match( + source, + /\/\*\*\n\s+\* Unable to find the table mapping for this environment.\n\s+\* This could be due the enableGen2Migration feature flag not being set to true for this environment.\n\s+\* Please enable the feature flag and push the backend resources.\n\s+\* If you are not planning to migrate this environment, you can remove this key.\n\s+\*\/\n\s+dev: {}/, + ); //\s+dev: {}/); }); it('has each each key in defineData', () => { - const tableMapping = { Todo: 'my-todo-mapping' }; - const source = printNodeArray(generateDataSource({ tableMapping })); + const tableMappings = { dev: { Todo: 'my-todo-mapping' } }; + const source = printNodeArray(generateDataSource({ tableMappings })); assert.match( source, - /defineData\({\n\s+importedAmplifyDynamoDBTableMap: \{\s+Todo: ['"]my-todo-mapping['"] },\n\s+importedModels:\s+\[.*?\],\n\s+schema: "TODO: Add your existing graphql schema here"\n}\)/, + /defineData\({\n\s+\/\/ Replace each environment name with the corresponding branch name. Use ['"]sandbox['"] for your sandbox environment.\n\s+importedAmplifyDynamoDBTableMap: \{\s+dev: { Todo: ['"]my-todo-mapping['"] } },\n\s+schema: "TODO: Add your existing graphql schema here"\n}\)/, ); }); }); diff --git a/packages/amplify-gen2-codegen/src/data/source_builder.ts b/packages/amplify-gen2-codegen/src/data/source_builder.ts index 23cdaf0de8e..667f73277ee 100644 --- a/packages/amplify-gen2-codegen/src/data/source_builder.ts +++ b/packages/amplify-gen2-codegen/src/data/source_builder.ts @@ -3,12 +3,12 @@ import { renderResourceTsFile } from '../resource/resource'; import { createTodoError } from '../todo_error'; const factory = ts.factory; +export type DataTableMapping = Record; export type DataDefinition = { - tableMapping: Record; + tableMappings: Record; }; const importedAmplifyDynamoDBTableMapKeyName = 'importedAmplifyDynamoDBTableMap'; -const importedModelsKey = 'importedModels'; export const schemaPlaceholderComment = 'TODO: Add your existing graphql schema here'; @@ -17,25 +17,45 @@ export const generateDataSource = (dataDefinition?: DataDefinition): ts.NodeArra const namedImports: Record> = { '@aws-amplify/backend': new Set() }; namedImports['@aws-amplify/backend'].add('defineData'); - if (dataDefinition?.tableMapping) { - const tableMappingProperties: ObjectLiteralElementLike[] = []; - for (const [tableName, tableId] of Object.entries(dataDefinition.tableMapping)) { - tableMappingProperties.push( - factory.createPropertyAssignment(factory.createIdentifier(tableName), factory.createStringLiteral(tableId)), + if (dataDefinition?.tableMappings) { + const tableMappingEnvironments: ObjectLiteralElementLike[] = []; + for (const [environmentName, tableMapping] of Object.entries(dataDefinition.tableMappings)) { + const tableMappingProperties: ObjectLiteralElementLike[] = []; + if (tableMapping) { + for (const [tableName, tableId] of Object.entries(tableMapping)) { + tableMappingProperties.push( + factory.createPropertyAssignment(factory.createIdentifier(tableName), factory.createStringLiteral(tableId)), + ); + } + } + + let tableMappingExpression = factory.createPropertyAssignment( + factory.createIdentifier(environmentName), + factory.createObjectLiteralExpression(tableMappingProperties), ); + if (tableMappingProperties.length === 0) { + tableMappingExpression = ts.addSyntheticLeadingComment( + tableMappingExpression, + ts.SyntaxKind.MultiLineCommentTrivia, + '*\n' + + '* Unable to find the table mapping for this environment.\n' + + '* This could be due the enableGen2Migration feature flag not being set to true for this environment.\n' + + '* Please enable the feature flag and push the backend resources.\n' + + '* If you are not planning to migrate this environment, you can remove this key.\n', + true, + ); + } + tableMappingEnvironments.push(tableMappingExpression); } dataRenderProperties.push( - factory.createPropertyAssignment( - importedAmplifyDynamoDBTableMapKeyName, - factory.createObjectLiteralExpression(tableMappingProperties), - ), - ); - dataRenderProperties.push( - factory.createPropertyAssignment( - importedModelsKey, - factory.createArrayLiteralExpression( - Object.keys(dataDefinition.tableMapping).map((tableName) => factory.createStringLiteral(tableName)), + ts.addSyntheticLeadingComment( + factory.createPropertyAssignment( + importedAmplifyDynamoDBTableMapKeyName, + factory.createObjectLiteralExpression(tableMappingEnvironments), ), + ts.SyntaxKind.SingleLineCommentTrivia, + ` Replace each environment name with the corresponding branch name. Use "sandbox" for your sandbox environment.`, + true, ), ); } diff --git a/packages/amplify-gen2-codegen/src/index.ts b/packages/amplify-gen2-codegen/src/index.ts index d4cc3ff9273..3882a02c157 100644 --- a/packages/amplify-gen2-codegen/src/index.ts +++ b/packages/amplify-gen2-codegen/src/index.ts @@ -43,7 +43,7 @@ import { ServerSideEncryptionConfiguration, } from './storage/source_builder.js'; -import { DataDefinition, generateDataSource } from './data/source_builder'; +import { DataDefinition, DataTableMapping, generateDataSource } from './data/source_builder'; import { FunctionDefinition, renderFunctions } from './function/source_builder'; @@ -242,6 +242,7 @@ export { AuthLambdaTriggers, StorageTriggerEvent, DataDefinition, + DataTableMapping, SamlOptions, OidcEndPoints, MetadataOptions, diff --git a/packages/amplify-migration/src/backend_environment_selector.ts b/packages/amplify-migration/src/backend_environment_selector.ts index 6c1629e001c..ae00e336dae 100644 --- a/packages/amplify-migration/src/backend_environment_selector.ts +++ b/packages/amplify-migration/src/backend_environment_selector.ts @@ -1,5 +1,5 @@ import assert from 'node:assert'; -import { AmplifyClient, BackendEnvironment, GetBackendEnvironmentCommand } from '@aws-sdk/client-amplify'; +import { AmplifyClient, BackendEnvironment, GetBackendEnvironmentCommand, ListBackendEnvironmentsCommand } from '@aws-sdk/client-amplify'; import { getEnvInfo } from '@aws-amplify/cli-internal/lib/extensions/amplify-helpers/get-env-info'; export class BackendEnvironmentResolver { @@ -19,4 +19,16 @@ export class BackendEnvironmentResolver { this.selectedEnvironment = backendEnvironment; return this.selectedEnvironment; }; + + getAllBackendEnvironments = async (): Promise => { + const envInfo = getEnvInfo(); + assert(envInfo); + const { backendEnvironments } = await this.amplifyClient.send( + new ListBackendEnvironmentsCommand({ + appId: this.appId, + }), + ); + assert(backendEnvironments, 'No backend environments found'); + return backendEnvironments; + }; } diff --git a/packages/amplify-migration/src/data_definition_fetcher.test.ts b/packages/amplify-migration/src/data_definition_fetcher.test.ts index 1716497ba51..bf856733dd8 100644 --- a/packages/amplify-migration/src/data_definition_fetcher.test.ts +++ b/packages/amplify-migration/src/data_definition_fetcher.test.ts @@ -11,10 +11,13 @@ describe('DataDefinitionFetcher', () => { it('maps cloudformation stack output to table mapping', async () => { const mapping = { hello: 'world' }; const mockBackendEnvResolver: BackendEnvironmentResolver = { - selectBackendEnvironment: async () => { - return { - stackName: 'asdf', - } as BackendEnvironment; + getAllBackendEnvironments: async () => { + return [ + { + environmentName: 'dev', + stackName: 'asdf', + }, + ] as BackendEnvironment[]; }, } as BackendEnvironmentResolver; const mockAmplifyStackParser: AmplifyStackParser = { @@ -32,14 +35,17 @@ describe('DataDefinitionFetcher', () => { } as unknown as AmplifyStackParser; const dataDefinitionFetcher = new DataDefinitionFetcher(mockBackendEnvResolver, mockAmplifyStackParser); const results = await dataDefinitionFetcher.getDefinition(); - assert(results?.tableMapping); + assert(results?.tableMappings); }); - it('throws an error if the json cannot be parsed', async () => { + it('return undefined for mapping if json cannot be parsed', async () => { const mockBackendEnvResolver: BackendEnvironmentResolver = { - selectBackendEnvironment: async () => { - return { - stackName: 'asdf', - } as BackendEnvironment; + getAllBackendEnvironments: async () => { + return [ + { + environmentName: 'dev', + stackName: 'asdf', + }, + ] as BackendEnvironment[]; }, } as BackendEnvironmentResolver; const mockAmplifyStackParser: AmplifyStackParser = { @@ -56,16 +62,21 @@ describe('DataDefinitionFetcher', () => { } as AmplifyStacks), } as unknown as AmplifyStackParser; const dataDefinitionFetcher = new DataDefinitionFetcher(mockBackendEnvResolver, mockAmplifyStackParser); - await assert.rejects(() => dataDefinitionFetcher.getDefinition(), { message: 'Could not parse the Amplify Data table mapping' }); + const results = await dataDefinitionFetcher.getDefinition(); + assert(results?.tableMappings); + assert.equal(JSON.stringify(results?.tableMappings), JSON.stringify({ dev: undefined })); }); }); describe('table mapping is not defined', () => { - it('reject with table mapping assertion', async () => { + it('return undefined for table mapping', async () => { const mockBackendEnvResolver: BackendEnvironmentResolver = { - selectBackendEnvironment: async () => { - return { - stackName: 'asdf', - } as BackendEnvironment; + getAllBackendEnvironments: async () => { + return [ + { + environmentName: 'dev', + stackName: 'asdf', + }, + ] as BackendEnvironment[]; }, } as BackendEnvironmentResolver; const mockAmplifyStackParser: AmplifyStackParser = { @@ -75,17 +86,22 @@ describe('DataDefinitionFetcher', () => { } as AmplifyStacks), } as unknown as AmplifyStackParser; const dataDefinitionFetcher = new DataDefinitionFetcher(mockBackendEnvResolver, mockAmplifyStackParser); - await assert.rejects(dataDefinitionFetcher.getDefinition); + const results = await dataDefinitionFetcher.getDefinition(); + assert(results?.tableMappings); + assert.equal(JSON.stringify(results?.tableMappings), JSON.stringify({ dev: undefined })); }); }); }); describe('if data stack is undefined', () => { it('does not reject with table mapping assertion', async () => { const mockBackendEnvResolver: BackendEnvironmentResolver = { - selectBackendEnvironment: async () => { - return { - stackName: 'asdf', - } as BackendEnvironment; + getAllBackendEnvironments: async () => { + return [ + { + environmentName: 'dev', + stackName: 'asdf', + }, + ] as BackendEnvironment[]; }, } as BackendEnvironmentResolver; const mockAmplifyStackParser: AmplifyStackParser = { @@ -97,12 +113,15 @@ describe('DataDefinitionFetcher', () => { const dataDefinitionFetcher = new DataDefinitionFetcher(mockBackendEnvResolver, mockAmplifyStackParser); await assert.doesNotReject(dataDefinitionFetcher.getDefinition); }); - it('returns undefined', async () => { + it('returns undefined for table mapping', async () => { const mockBackendEnvResolver: BackendEnvironmentResolver = { - selectBackendEnvironment: async () => { - return { - stackName: 'asdf', - } as BackendEnvironment; + getAllBackendEnvironments: async () => { + return [ + { + environmentName: 'dev', + stackName: 'asdf', + }, + ] as BackendEnvironment[]; }, } as BackendEnvironmentResolver; const mockAmplifyStackParser: AmplifyStackParser = { @@ -112,8 +131,9 @@ describe('DataDefinitionFetcher', () => { } as AmplifyStacks), } as unknown as AmplifyStackParser; const dataDefinitionFetcher = new DataDefinitionFetcher(mockBackendEnvResolver, mockAmplifyStackParser); - const definition = await dataDefinitionFetcher.getDefinition(); - assert.equal(definition, undefined); + const results = await dataDefinitionFetcher.getDefinition(); + assert(results?.tableMappings); + assert.equal(JSON.stringify(results?.tableMappings), JSON.stringify({ dev: undefined })); }); }); }); diff --git a/packages/amplify-migration/src/data_definition_fetcher.ts b/packages/amplify-migration/src/data_definition_fetcher.ts index cb363ec80a6..199c22efc69 100644 --- a/packages/amplify-migration/src/data_definition_fetcher.ts +++ b/packages/amplify-migration/src/data_definition_fetcher.ts @@ -1,4 +1,3 @@ -import assert from 'node:assert'; import { DataDefinition } from '@aws-amplify/amplify-gen2-codegen'; import { AmplifyStackParser } from './amplify_stack_parser.js'; import { BackendEnvironmentResolver } from './backend_environment_selector.js'; @@ -8,20 +7,29 @@ const dataSourceMappingOutputKey = 'DataSourceMappingOutput'; export class DataDefinitionFetcher { constructor(private backendEnvironmentResolver: BackendEnvironmentResolver, private amplifyStackClient: AmplifyStackParser) {} getDefinition = async (): Promise => { - const backendEnvironment = await this.backendEnvironmentResolver.selectBackendEnvironment(); - assert(backendEnvironment?.stackName); - const amplifyStacks = await this.amplifyStackClient.getAmplifyStacks(backendEnvironment?.stackName); - if (amplifyStacks.dataStack) { - const tableMappingText = amplifyStacks.dataStack?.Outputs?.find((o) => o.OutputKey === dataSourceMappingOutputKey)?.OutputValue; - assert(tableMappingText, 'Amplify Data table mapping not found.'); - try { - return { - tableMapping: JSON.parse(tableMappingText), - }; - } catch (e) { - throw new Error('Could not parse the Amplify Data table mapping'); - } - } - return undefined; + const backendEnvironments = await this.backendEnvironmentResolver.getAllBackendEnvironments(); + const tableMappings = await Promise.all( + backendEnvironments.map(async (backendEnvironment) => { + if (!backendEnvironment?.stackName) { + return [backendEnvironment.environmentName, undefined]; + } + const amplifyStacks = await this.amplifyStackClient.getAmplifyStacks(backendEnvironment?.stackName); + if (amplifyStacks.dataStack) { + const tableMappingText = amplifyStacks.dataStack?.Outputs?.find((o) => o.OutputKey === dataSourceMappingOutputKey)?.OutputValue; + if (!tableMappingText) { + return [backendEnvironment.environmentName, undefined]; + } + try { + return [backendEnvironment.environmentName, JSON.parse(tableMappingText)]; + } catch (e) { + return [backendEnvironment.environmentName, undefined]; + } + } + return [backendEnvironment.environmentName, undefined]; + }), + ); + return { + tableMappings: Object.fromEntries(tableMappings), + }; }; }