From 4d4434f29b8c598cabe1740df8f03e0c4970fda7 Mon Sep 17 00:00:00 2001 From: Surbhi Bansal Date: Thu, 13 Mar 2025 11:26:20 -0700 Subject: [PATCH 1/4] fix: update amplify.yml file with the gen2 command --- .../src/command-handler.test.ts | 89 +++++++++++++++++++ .../amplify-migration/src/command-handlers.ts | 35 +++++++- 2 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 packages/amplify-migration/src/command-handler.test.ts diff --git a/packages/amplify-migration/src/command-handler.test.ts b/packages/amplify-migration/src/command-handler.test.ts new file mode 100644 index 00000000000..947b061ecf4 --- /dev/null +++ b/packages/amplify-migration/src/command-handler.test.ts @@ -0,0 +1,89 @@ +import fs from 'node:fs/promises'; +import { AmplifyClient, GetAppCommand } from '@aws-sdk/client-amplify'; +import { updateAmplifyYmlFile } from './command-handlers'; +import { pathManager } from '@aws-amplify/amplify-cli-core'; + +jest.mock('node:fs/promises', () => ({ + access: jest.fn(), + readFile: jest.fn(), + writeFile: jest.fn(), +})); + +jest.mock('@aws-amplify/amplify-cli-core'); + +jest.mock('@aws-sdk/client-amplify'); + +describe('updateAmplifyYmlFile', () => { + const amplifyClient = new AmplifyClient(); + const mockAppId = 'testAppId'; + const amplifyYmlPath = '/mockRootDir/amplify.yml'; + const mockBuildSpec = `version: 1 +backend: + phases: + build: + commands: + - '# Execute Amplify CLI with the helper script' + - amplifyPush --simple +frontend: + phases: + preBuild: + commands: + - npm ci --cache .npm --prefer-offline + build: + commands: + - npm run build + artifacts: + baseDirectory: build + files: + - '**/*' + cache: + paths: + - .npm/**/*`; + + beforeEach(() => { + jest.clearAllMocks(); + (pathManager.findProjectRoot as jest.Mock).mockReturnValue('/mockRootDir'); + }); + + it('should update amplify.yml file if it exists', async () => { + (fs.access as jest.Mock).mockResolvedValue(undefined); + (fs.readFile as jest.Mock).mockResolvedValue(mockBuildSpec); + + await updateAmplifyYmlFile(amplifyClient, mockAppId); + + expect(fs.readFile).toHaveBeenCalledWith(amplifyYmlPath, 'utf-8'); + expect(fs.writeFile).toHaveBeenCalledWith(amplifyYmlPath, mockBuildSpec.replace(/amplifyPush --simple/g, 'npx ampx pipeline-deploy'), { + encoding: 'utf-8', + }); + }); + + it('should create amplify.yml file with updated buildSpec if it does not exist', async () => { + (fs.access as jest.Mock).mockRejectedValue(new Error('File not found')); + (AmplifyClient.prototype.send as jest.Mock).mockResolvedValue({ + app: { buildSpec: mockBuildSpec }, + }); + + await updateAmplifyYmlFile(amplifyClient, mockAppId); + + expect(AmplifyClient.prototype.send).toHaveBeenCalledWith(expect.any(GetAppCommand)); + expect(fs.writeFile).toHaveBeenCalledWith(amplifyYmlPath, mockBuildSpec.replace(/amplifyPush --simple/g, 'npx ampx pipeline-deploy'), { + encoding: 'utf-8', + }); + }); + + it('should throw an error if buildSpec is not found in the app', async () => { + (fs.access as jest.Mock).mockRejectedValue(new Error('File not found')); + (AmplifyClient.prototype.send as jest.Mock).mockResolvedValue({ + app: {}, + }); + + await expect(updateAmplifyYmlFile(amplifyClient, mockAppId)).rejects.toThrow('buildSpec not found in the app'); + }); + + it('should throw an error if app is not found', async () => { + (fs.access as jest.Mock).mockRejectedValue(new Error('File not found')); + (AmplifyClient.prototype.send as jest.Mock).mockResolvedValue({}); + + await expect(updateAmplifyYmlFile(amplifyClient, mockAppId)).rejects.toThrow('App not found'); + }); +}); diff --git a/packages/amplify-migration/src/command-handlers.ts b/packages/amplify-migration/src/command-handlers.ts index 33b8c49818c..0df5c5e7bb9 100644 --- a/packages/amplify-migration/src/command-handlers.ts +++ b/packages/amplify-migration/src/command-handlers.ts @@ -7,7 +7,7 @@ import { v4 as uuid } from 'uuid'; import { createGen2Renderer } from '@aws-amplify/amplify-gen2-codegen'; import { UsageData } from '@aws-amplify/cli-internal'; -import { AmplifyClient, UpdateAppCommand } from '@aws-sdk/client-amplify'; +import { AmplifyClient, UpdateAppCommand, GetAppCommand } from '@aws-sdk/client-amplify'; import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; import { CognitoIdentityProviderClient, LambdaConfigType } from '@aws-sdk/client-cognito-identity-provider'; import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'; @@ -21,7 +21,7 @@ import { BackendEnvironmentResolver } from './backend_environment_selector'; import { Analytics, AppAnalytics } from './analytics'; import { AppAuthDefinitionFetcher } from './app_auth_definition_fetcher'; import { AppStorageDefinitionFetcher } from './app_storage_definition_fetcher'; -import { AmplifyCategories, IUsageData, stateManager } from '@aws-amplify/amplify-cli-core'; +import { AmplifyCategories, IUsageData, stateManager, pathManager } from '@aws-amplify/amplify-cli-core'; import { AuthTriggerConnection } from '@aws-amplify/amplify-gen1-codegen-auth-adapter'; import { DataDefinitionFetcher } from './data_definition_fetcher'; import { AmplifyStackParser } from './amplify_stack_parser'; @@ -214,6 +214,35 @@ const unsupportedCategories = (): Map => { return unsupportedCategoriesList; }; +export async function updateAmplifyYmlFile(amplifyClient: AmplifyClient, appId: string) { + const rootDir = pathManager.findProjectRoot(); + assert(rootDir); + const amplifyYmlPath = path.join(rootDir, 'amplify.yml'); + + try { + // Read the content of amplify.yml file if it exists + await fs.access(amplifyYmlPath); + const amplifyYmlContent = await fs.readFile(amplifyYmlPath, 'utf-8'); + + await writeToAmplifyYmlFile(amplifyYmlPath, amplifyYmlContent); + } catch (error) { + // If amplify.yml file doesn't exist, make a getApp call to get buildSpec + const getAppResponse = await amplifyClient.send(new GetAppCommand({ appId })); + + assert(getAppResponse.app, 'App not found'); + const buildSpec = getAppResponse.app.buildSpec; + assert(buildSpec, 'buildSpec not found in the app'); + + await writeToAmplifyYmlFile(amplifyYmlPath, buildSpec); + } +} + +async function writeToAmplifyYmlFile(amplifyYmlPath: string, content: string) { + // Replace 'amplifyPush --simple' with 'npx ampx pipeline-deploy' + content = content.replace(/amplifyPush --simple/g, 'npx ampx pipeline-deploy'); + await fs.writeFile(amplifyYmlPath, content, { encoding: 'utf-8' }); +} + async function updateGitIgnoreForGen2() { const cwd = process.cwd(); const updateGitIgnore = ora('Updating gitignore contents').start(); @@ -295,6 +324,8 @@ export async function execute() { appId: appId, }); + await updateAmplifyYmlFile(amplifyClient, appId); + await updateGitIgnoreForGen2(); const movingGen1BackendFiles = ora(`Moving your Gen1 backend files to ${format.highlight(MIGRATION_DIR)}`).start(); From 8bd4496e10ff377ee6b534c7ffa841a143b2cdd1 Mon Sep 17 00:00:00 2001 From: Surbhi Bansal Date: Mon, 17 Mar 2025 13:33:44 -0700 Subject: [PATCH 2/4] chore: updated the gen2 command --- .../src/command-handler.test.ts | 21 +++++++++++++----- .../amplify-migration/src/command-handlers.ts | 22 ++++++++++++------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/amplify-migration/src/command-handler.test.ts b/packages/amplify-migration/src/command-handler.test.ts index 947b061ecf4..cff864dac16 100644 --- a/packages/amplify-migration/src/command-handler.test.ts +++ b/packages/amplify-migration/src/command-handler.test.ts @@ -13,6 +13,9 @@ jest.mock('@aws-amplify/amplify-cli-core'); jest.mock('@aws-sdk/client-amplify'); +const GEN1_COMMAND = 'amplifyPush --simple'; +const GEN2_COMMAND = 'npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID'; + describe('updateAmplifyYmlFile', () => { const amplifyClient = new AmplifyClient(); const mockAppId = 'testAppId'; @@ -46,19 +49,18 @@ frontend: }); it('should update amplify.yml file if it exists', async () => { - (fs.access as jest.Mock).mockResolvedValue(undefined); (fs.readFile as jest.Mock).mockResolvedValue(mockBuildSpec); await updateAmplifyYmlFile(amplifyClient, mockAppId); expect(fs.readFile).toHaveBeenCalledWith(amplifyYmlPath, 'utf-8'); - expect(fs.writeFile).toHaveBeenCalledWith(amplifyYmlPath, mockBuildSpec.replace(/amplifyPush --simple/g, 'npx ampx pipeline-deploy'), { + expect(fs.writeFile).toHaveBeenCalledWith(amplifyYmlPath, mockBuildSpec.replace(new RegExp(GEN1_COMMAND, 'g'), GEN2_COMMAND), { encoding: 'utf-8', }); }); it('should create amplify.yml file with updated buildSpec if it does not exist', async () => { - (fs.access as jest.Mock).mockRejectedValue(new Error('File not found')); + (fs.readFile as jest.Mock).mockRejectedValue({ code: 'ENOENT' }); (AmplifyClient.prototype.send as jest.Mock).mockResolvedValue({ app: { buildSpec: mockBuildSpec }, }); @@ -66,13 +68,13 @@ frontend: await updateAmplifyYmlFile(amplifyClient, mockAppId); expect(AmplifyClient.prototype.send).toHaveBeenCalledWith(expect.any(GetAppCommand)); - expect(fs.writeFile).toHaveBeenCalledWith(amplifyYmlPath, mockBuildSpec.replace(/amplifyPush --simple/g, 'npx ampx pipeline-deploy'), { + expect(fs.writeFile).toHaveBeenCalledWith(amplifyYmlPath, mockBuildSpec.replace(new RegExp(GEN1_COMMAND, 'g'), GEN2_COMMAND), { encoding: 'utf-8', }); }); it('should throw an error if buildSpec is not found in the app', async () => { - (fs.access as jest.Mock).mockRejectedValue(new Error('File not found')); + (fs.readFile as jest.Mock).mockRejectedValue({ code: 'ENOENT' }); (AmplifyClient.prototype.send as jest.Mock).mockResolvedValue({ app: {}, }); @@ -81,9 +83,16 @@ frontend: }); it('should throw an error if app is not found', async () => { - (fs.access as jest.Mock).mockRejectedValue(new Error('File not found')); + (fs.readFile as jest.Mock).mockRejectedValue({ code: 'ENOENT' }); (AmplifyClient.prototype.send as jest.Mock).mockResolvedValue({}); await expect(updateAmplifyYmlFile(amplifyClient, mockAppId)).rejects.toThrow('App not found'); }); + + it('should throw the original error if it is not related to file not found', async () => { + const error = new Error('Some other error'); + (fs.readFile as jest.Mock).mockRejectedValue(error); + + await expect(updateAmplifyYmlFile(amplifyClient, mockAppId)).rejects.toThrow(error); + }); }); diff --git a/packages/amplify-migration/src/command-handlers.ts b/packages/amplify-migration/src/command-handlers.ts index 0df5c5e7bb9..2a917f2dd31 100644 --- a/packages/amplify-migration/src/command-handlers.ts +++ b/packages/amplify-migration/src/command-handlers.ts @@ -46,6 +46,8 @@ interface CodegenCommandParameters { const TEMP_GEN_2_OUTPUT_DIR = 'amplify-gen2'; const AMPLIFY_DIR = 'amplify'; const MIGRATION_DIR = '.amplify/migration'; +const GEN1_COMMAND = 'amplifyPush --simple'; +const GEN2_COMMAND = 'npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID'; enum GEN2_AMPLIFY_GITIGNORE_FILES_OR_DIRS { DOT_AMPLIFY = '.amplify', @@ -221,25 +223,29 @@ export async function updateAmplifyYmlFile(amplifyClient: AmplifyClient, appId: try { // Read the content of amplify.yml file if it exists - await fs.access(amplifyYmlPath); const amplifyYmlContent = await fs.readFile(amplifyYmlPath, 'utf-8'); await writeToAmplifyYmlFile(amplifyYmlPath, amplifyYmlContent); } catch (error) { - // If amplify.yml file doesn't exist, make a getApp call to get buildSpec - const getAppResponse = await amplifyClient.send(new GetAppCommand({ appId })); + if (error.code === 'ENOENT') { + // If amplify.yml file doesn't exist, make a getApp call to get buildSpec + const getAppResponse = await amplifyClient.send(new GetAppCommand({ appId })); - assert(getAppResponse.app, 'App not found'); - const buildSpec = getAppResponse.app.buildSpec; - assert(buildSpec, 'buildSpec not found in the app'); + assert(getAppResponse.app, 'App not found'); + const buildSpec = getAppResponse.app.buildSpec; + assert(buildSpec, 'buildSpec not found in the app'); - await writeToAmplifyYmlFile(amplifyYmlPath, buildSpec); + await writeToAmplifyYmlFile(amplifyYmlPath, buildSpec); + } else { + // Throw the original error if it's not related to file not found + throw error; + } } } async function writeToAmplifyYmlFile(amplifyYmlPath: string, content: string) { // Replace 'amplifyPush --simple' with 'npx ampx pipeline-deploy' - content = content.replace(/amplifyPush --simple/g, 'npx ampx pipeline-deploy'); + content = content.replace(new RegExp(GEN1_COMMAND, 'g'), GEN2_COMMAND); await fs.writeFile(amplifyYmlPath, content, { encoding: 'utf-8' }); } From 6a024a8fc13029ed9267c7ead7aa1c70670718dc Mon Sep 17 00:00:00 2001 From: Surbhi Bansal Date: Tue, 18 Mar 2025 14:24:28 -0700 Subject: [PATCH 3/4] chore: updated data definition fetcher to include the api category check from amplify-meta.json file --- .../src/function_render_adapter.ts | 2 +- packages/amplify-gen2-codegen/API.md | 2 +- packages/amplify-gen2-codegen/src/index.ts | 1 - .../amplify-migration/src/command-handlers.ts | 6 +- .../src/data_definition_fetcher.test.ts | 306 +++++++++--------- .../src/data_definition_fetcher.ts | 82 +++-- 6 files changed, 207 insertions(+), 192 deletions(-) diff --git a/packages/amplify-gen1-codegen-function-adapter/src/function_render_adapter.ts b/packages/amplify-gen1-codegen-function-adapter/src/function_render_adapter.ts index acb645f15d0..d6c65ab9f1d 100644 --- a/packages/amplify-gen1-codegen-function-adapter/src/function_render_adapter.ts +++ b/packages/amplify-gen1-codegen-function-adapter/src/function_render_adapter.ts @@ -29,7 +29,7 @@ export const getFunctionDefinition = ( funcDef.runtime = configuration?.Runtime; const functionName = configuration?.FunctionName; assert(functionName); - const functionRecordInMeta = Object.entries(meta.function).find(([_, value]) => value.output.Name === functionName); + const functionRecordInMeta = Object.entries(meta.function).find(([, value]) => value.output.Name === functionName); assert(functionRecordInMeta); funcDef.category = functionCategoryMap.get(functionRecordInMeta[0]) ?? 'function'; funcDef.resourceName = functionRecordInMeta[0]; diff --git a/packages/amplify-gen2-codegen/API.md b/packages/amplify-gen2-codegen/API.md index 401fe601dd3..4612566ff93 100644 --- a/packages/amplify-gen2-codegen/API.md +++ b/packages/amplify-gen2-codegen/API.md @@ -64,7 +64,7 @@ export type AuthLambdaTriggers = Record; export type AuthTriggerEvents = 'createAuthChallenge' | 'customMessage' | 'defineAuthChallenge' | 'postAuthentication' | 'postConfirmation' | 'preAuthentication' | 'preSignUp' | 'preTokenGeneration' | 'userMigration' | 'verifyAuthChallengeResponse'; // @public (undocumented) -export const createGen2Renderer: ({ outputDir, appId, backendEnvironmentName, auth, storage, data, functions, unsupportedCategories, fileWriter, }: Readonly) => Renderer; +export const createGen2Renderer: ({ outputDir, backendEnvironmentName, auth, storage, data, functions, unsupportedCategories, fileWriter, }: Readonly) => Renderer; // @public (undocumented) export type CustomAttribute = { diff --git a/packages/amplify-gen2-codegen/src/index.ts b/packages/amplify-gen2-codegen/src/index.ts index cb8f988d596..f8d6a225d28 100644 --- a/packages/amplify-gen2-codegen/src/index.ts +++ b/packages/amplify-gen2-codegen/src/index.ts @@ -63,7 +63,6 @@ const createFileWriter = (path: string) => async (content: string) => fs.writeFi export const createGen2Renderer = ({ outputDir, - appId, backendEnvironmentName, auth, storage, diff --git a/packages/amplify-migration/src/command-handlers.ts b/packages/amplify-migration/src/command-handlers.ts index 2a917f2dd31..35b46064cad 100644 --- a/packages/amplify-migration/src/command-handlers.ts +++ b/packages/amplify-migration/src/command-handlers.ts @@ -36,7 +36,6 @@ interface CodegenCommandParameters { logger: AppContextLogger; outputDirectory: string; backendEnvironmentName: string | undefined; - appId: string; dataDefinitionFetcher: DataDefinitionFetcher; authDefinitionFetcher: AppAuthDefinitionFetcher; storageDefinitionFetcher: AppStorageDefinitionFetcher; @@ -59,7 +58,6 @@ enum GEN2_AMPLIFY_GITIGNORE_FILES_OR_DIRS { const generateGen2Code = async ({ outputDirectory, backendEnvironmentName, - appId, authDefinitionFetcher, dataDefinitionFetcher, storageDefinitionFetcher, @@ -68,7 +66,6 @@ const generateGen2Code = async ({ const fetchingAWSResourceDetails = ora('Fetching resource details from AWS').start(); const gen2RenderOptions = { outputDir: outputDirectory, - appId: appId, backendEnvironmentName: backendEnvironmentName, auth: await authDefinitionFetcher.getDefinition(), storage: await storageDefinitionFetcher.getDefinition(), @@ -322,12 +319,11 @@ export async function execute() { () => getAuthTriggersConnections(), ccbFetcher, ), - dataDefinitionFetcher: new DataDefinitionFetcher(backendEnvironmentResolver, amplifyStackParser), + dataDefinitionFetcher: new DataDefinitionFetcher(backendEnvironmentResolver, new BackendDownloader(s3Client), amplifyStackParser), functionsDefinitionFetcher: new AppFunctionsDefinitionFetcher(lambdaClient, backendEnvironmentResolver, stateManager), analytics: new AppAnalytics(appId), logger: new AppContextLogger(appId), backendEnvironmentName: backendEnvironment?.environmentName, - appId: appId, }); await updateAmplifyYmlFile(amplifyClient, appId); diff --git a/packages/amplify-migration/src/data_definition_fetcher.test.ts b/packages/amplify-migration/src/data_definition_fetcher.test.ts index 268e46c633a..a42ccde51f0 100644 --- a/packages/amplify-migration/src/data_definition_fetcher.test.ts +++ b/packages/amplify-migration/src/data_definition_fetcher.test.ts @@ -1,56 +1,111 @@ import assert from 'node:assert'; import { BackendEnvironment } from '@aws-sdk/client-amplify'; +import { Stack } from '@aws-sdk/client-cloudformation'; import { BackendEnvironmentResolver } from './backend_environment_selector'; import { DataDefinitionFetcher } from './data_definition_fetcher'; import { AmplifyStackParser, AmplifyStacks } from './amplify_stack_parser'; -import { Stack } from '@aws-sdk/client-cloudformation'; -import { stateManager, pathManager } from '@aws-amplify/amplify-cli-core'; +import { BackendDownloader } from './backend_downloader'; +import { fileOrDirectoryExists } from './directory_exists'; +import { pathManager } from '@aws-amplify/amplify-cli-core'; import * as path from 'path'; - import fs from 'node:fs/promises'; import glob from 'glob'; jest.mock('node:fs/promises'); jest.mock('glob'); jest.mock('@aws-amplify/amplify-cli-core'); +jest.mock('./directory_exists'); + +// Test constants +const MOCK_ROOT_DIR = '/mock/root/dir'; +const MOCK_CLOUD_BACKEND = '/mock/cloud/backend'; +const MOCK_APP_ID = 'mockAppId'; + +// Type definitions +interface MockBackendEnvironment extends BackendEnvironmentResolver { + getAllBackendEnvironments: () => Promise; + selectBackendEnvironment: () => Promise; +} + +// Test helpers +const createMockBackendResolver = (environmentName = 'dev'): MockBackendEnvironment => + ({ + getAllBackendEnvironments: async () => + [ + { + environmentName, + stackName: 'asdf', + }, + ] as BackendEnvironment[], + selectBackendEnvironment: async () => + ({ + environmentName, + stackName: 'asdf', + deploymentArtifacts: 'asdf', + } as BackendEnvironment), + } as MockBackendEnvironment); + +const createMockAmplifyStackParser = (stackData: Partial = {}): AmplifyStackParser => + ({ + getAmplifyStacks: async () => + ({ + dataStack: stackData as Stack, + } as AmplifyStacks), + } as unknown as AmplifyStackParser); describe('DataDefinitionFetcher', () => { let dataDefinitionFetcher: DataDefinitionFetcher; let backendEnvironmentResolver: BackendEnvironmentResolver; let amplifyStackParser: AmplifyStackParser; + let ccbFetcher: BackendDownloader; + let mockAmplifyMeta: Record; beforeEach(() => { - backendEnvironmentResolver = new BackendEnvironmentResolver('mockAppId', {} as any); + // Setup basic mocks + backendEnvironmentResolver = new BackendEnvironmentResolver(MOCK_APP_ID, {} as any); amplifyStackParser = new AmplifyStackParser({} as any); - dataDefinitionFetcher = new DataDefinitionFetcher(backendEnvironmentResolver, amplifyStackParser); + ccbFetcher = { + getCurrentCloudBackend: jest.fn().mockResolvedValue(MOCK_CLOUD_BACKEND), + } as unknown as BackendDownloader; + + // Initialize fetcher + dataDefinitionFetcher = new DataDefinitionFetcher(backendEnvironmentResolver, ccbFetcher, amplifyStackParser); - (stateManager.getMeta as jest.Mock).mockReturnValue({ + // Setup mock data + mockAmplifyMeta = { api: { mockResource: { service: 'AppSync', }, }, - }); + }; - (pathManager.findProjectRoot as jest.Mock).mockReturnValue('/mock/root/dir'); + // Setup common mocks + (pathManager.findProjectRoot as jest.Mock).mockReturnValue(MOCK_ROOT_DIR); + (fileOrDirectoryExists as jest.Mock).mockResolvedValue(true); - (fs.stat as jest.Mock).mockImplementation((filePath: string) => { - return { - isDirectory: () => filePath.includes('schema'), - }; - }); + // Setup file system mocks + setupFileSystemMocks(); + }); - (glob.sync as jest.Mock).mockImplementation((pattern: string) => { - if (pattern.includes('schema')) { - return [ - path.join('/mock/root/dir/amplify/backend/api/mockResource/schema/schema1.graphql'), - path.join('/mock/root/dir/amplify/backend/api/mockResource/schema/schema2.graphql'), - ]; - } - return []; - }); + const setupFileSystemMocks = () => { + (fs.stat as jest.Mock).mockImplementation((filePath: string) => ({ + isDirectory: () => filePath.includes('schema'), + })); + + (glob.sync as jest.Mock).mockImplementation((pattern: string) => + pattern.includes('schema') + ? [ + path.join(MOCK_ROOT_DIR, 'amplify/backend/api/mockResource/schema/schema1.graphql'), + path.join(MOCK_ROOT_DIR, 'amplify/backend/api/mockResource/schema/schema2.graphql'), + ] + : [], + ); (fs.readFile as jest.Mock).mockImplementation((filePath: string) => { + if (filePath.includes('amplify-meta.json')) { + return JSON.stringify(mockAmplifyMeta); + } if (filePath.includes('schema1.graphql')) { return 'type Query { getSchema1: String }'; } @@ -59,166 +114,103 @@ describe('DataDefinitionFetcher', () => { } return 'type Query { getSchema: String }'; }); - }); + }; - describe('if data stack is defined', () => { - describe('table mapping is defined', () => { - it('maps cloudformation stack output to table mapping', async () => { + describe('Table Mapping Tests', () => { + describe('with defined data stack', () => { + it('should correctly map cloudformation stack output to table mapping', async () => { const mapping = { hello: 'world' }; - const mockBackendEnvResolver: BackendEnvironmentResolver = { - getAllBackendEnvironments: async () => { - return [ - { - environmentName: 'dev', - stackName: 'asdf', - }, - ] as BackendEnvironment[]; - }, - } as BackendEnvironmentResolver; - const mockAmplifyStackParser: AmplifyStackParser = { - getAmplifyStacks: async () => - ({ - dataStack: { - Outputs: [ - { - OutputKey: 'DataSourceMappingOutput', - OutputValue: JSON.stringify(mapping), - }, - ], - } as unknown as Stack, - } as AmplifyStacks), - } as unknown as AmplifyStackParser; - const dataDefinitionFetcher = new DataDefinitionFetcher(mockBackendEnvResolver, mockAmplifyStackParser); - const results = await dataDefinitionFetcher.getDefinition(); + const mockResolver = createMockBackendResolver(); + const mockParser = createMockAmplifyStackParser({ + Outputs: [ + { + OutputKey: 'DataSourceMappingOutput', + OutputValue: JSON.stringify(mapping), + }, + ], + }); + + const fetcher = new DataDefinitionFetcher(mockResolver, ccbFetcher, mockParser); + const results = await fetcher.getDefinition(); + assert(results?.tableMappings); }); - it('return undefined for mapping if json cannot be parsed', async () => { - const mockBackendEnvResolver: BackendEnvironmentResolver = { - getAllBackendEnvironments: async () => { - return [ - { - environmentName: 'dev', - stackName: 'asdf', - }, - ] as BackendEnvironment[]; - }, - } as BackendEnvironmentResolver; - const mockAmplifyStackParser: AmplifyStackParser = { - getAmplifyStacks: async () => - ({ - dataStack: { - Outputs: [ - { - OutputKey: 'DataSourceMappingOutput', - OutputValue: '(}', - }, - ], - } as unknown as Stack, - } as AmplifyStacks), - } as unknown as AmplifyStackParser; - const dataDefinitionFetcher = new DataDefinitionFetcher(mockBackendEnvResolver, mockAmplifyStackParser); - const results = await dataDefinitionFetcher.getDefinition(); + + it('should return undefined mapping when JSON parsing fails', async () => { + const mockResolver = createMockBackendResolver(); + const mockParser = createMockAmplifyStackParser({ + Outputs: [ + { + OutputKey: 'DataSourceMappingOutput', + OutputValue: '(}', // Invalid JSON + }, + ], + }); + + const fetcher = new DataDefinitionFetcher(mockResolver, ccbFetcher, mockParser); + const results = await fetcher.getDefinition(); + assert(results?.tableMappings); - assert.equal(JSON.stringify(results?.tableMappings), JSON.stringify({ dev: undefined })); + assert.deepStrictEqual(results?.tableMappings, { dev: undefined }); }); }); + describe('table mapping is not defined', () => { it('return undefined for table mapping', async () => { - const mockBackendEnvResolver: BackendEnvironmentResolver = { - getAllBackendEnvironments: async () => { - return [ - { - environmentName: 'dev', - stackName: 'asdf', - }, - ] as BackendEnvironment[]; - }, - } as BackendEnvironmentResolver; - const mockAmplifyStackParser: AmplifyStackParser = { - getAmplifyStacks: async () => - ({ - dataStack: {}, - } as AmplifyStacks), - } as unknown as AmplifyStackParser; - const dataDefinitionFetcher = new DataDefinitionFetcher(mockBackendEnvResolver, mockAmplifyStackParser); - const results = await dataDefinitionFetcher.getDefinition(); + const mockResolver = createMockBackendResolver(); + const mockParser = createMockAmplifyStackParser({}); + + const fetcher = new DataDefinitionFetcher(mockResolver, ccbFetcher, mockParser); + const results = await fetcher.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 = { - getAllBackendEnvironments: async () => { - return [ - { - environmentName: 'dev', - stackName: 'asdf', - }, - ] as BackendEnvironment[]; - }, - } as BackendEnvironmentResolver; - const mockAmplifyStackParser: AmplifyStackParser = { - getAmplifyStacks: async () => - ({ - dataStack: undefined, - } as AmplifyStacks), - } as unknown as AmplifyStackParser; - const dataDefinitionFetcher = new DataDefinitionFetcher(mockBackendEnvResolver, mockAmplifyStackParser); - await assert.doesNotReject(dataDefinitionFetcher.getDefinition); - }); - it('returns undefined for table mapping', async () => { - const mockBackendEnvResolver: BackendEnvironmentResolver = { - getAllBackendEnvironments: async () => { - return [ - { - environmentName: 'dev', - stackName: 'asdf', - }, - ] as BackendEnvironment[]; - }, - } as BackendEnvironmentResolver; - const mockAmplifyStackParser: AmplifyStackParser = { - getAmplifyStacks: async () => - ({ - dataStack: undefined, - } as AmplifyStacks), - } as unknown as AmplifyStackParser; - const dataDefinitionFetcher = new DataDefinitionFetcher(mockBackendEnvResolver, mockAmplifyStackParser); - const results = await dataDefinitionFetcher.getDefinition(); - assert(results?.tableMappings); - assert.equal(JSON.stringify(results?.tableMappings), JSON.stringify({ dev: undefined })); + + describe('with undefined data stack', () => { + it('should handle undefined data stack gracefully', async () => { + const mockResolver = createMockBackendResolver(); + const mockParser = createMockAmplifyStackParser(undefined); + + const fetcher = new DataDefinitionFetcher(mockResolver, ccbFetcher, mockParser); + await assert.doesNotReject(fetcher.getDefinition); + const results = await fetcher.getDefinition(); + + assert(results?.tableMappings); + assert.deepStrictEqual(results?.tableMappings, { dev: undefined }); + }); }); }); - it('should return merged schema from schema folder', async () => { - const schema = await dataDefinitionFetcher.getSchema(); - expect(schema).toContain('type Query { getSchema1: String }'); - expect(schema).toContain('type Mutation { updateSchema2: String }'); - }); + describe('Schema Tests', () => { + it('should merge multiple schema files from schema folder', async () => { + const schema = await dataDefinitionFetcher.getSchema({ + mockResource: { service: 'AppSync' }, + }); - describe('when only schema.graphql exists', () => { - it('should return the content of schema.graphql', async () => { - (fs.stat as jest.Mock).mockImplementation(() => { - return { - isDirectory: () => false, - }; + expect(schema).toContain('type Query { getSchema1: String }'); + expect(schema).toContain('type Mutation { updateSchema2: String }'); + }); + + it('should return single schema.graphql content when only it exists', async () => { + (fs.stat as jest.Mock).mockImplementation(() => ({ + isDirectory: () => false, + })); + + const schema = await dataDefinitionFetcher.getSchema({ + mockResource: { service: 'AppSync' }, }); - const schema = await dataDefinitionFetcher.getSchema(); + expect(schema).toBe('type Query { getSchema: String }'); }); - }); - describe('when no schema exists', () => { it('should throw error when no schema is found', async () => { - // Mock fs.stat to simulate non-existent directory (fs.stat as jest.Mock).mockRejectedValue(new Error('ENOENT')); - - // Mock fs.readFile to simulate non-existent schema file (fs.readFile as jest.Mock).mockRejectedValue(new Error('ENOENT')); - await expect(dataDefinitionFetcher.getSchema()).rejects.toThrow('No GraphQL schema found in the project'); + await expect(dataDefinitionFetcher.getSchema({ mockResource: { service: 'AppSync' } })).rejects.toThrow( + 'No GraphQL schema found in the project', + ); }); }); }); diff --git a/packages/amplify-migration/src/data_definition_fetcher.ts b/packages/amplify-migration/src/data_definition_fetcher.ts index 67baaae1e0d..2a5e0a1a10b 100644 --- a/packages/amplify-migration/src/data_definition_fetcher.ts +++ b/packages/amplify-migration/src/data_definition_fetcher.ts @@ -6,18 +6,28 @@ 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'; -import { stateManager, pathManager } from '@aws-amplify/amplify-cli-core'; +import { BackendDownloader } from './backend_downloader.js'; +import { pathManager } from '@aws-amplify/amplify-cli-core'; +import { fileOrDirectoryExists } from './directory_exists'; const dataSourceMappingOutputKey = 'DataSourceMappingOutput'; export class DataDefinitionFetcher { - constructor(private backendEnvironmentResolver: BackendEnvironmentResolver, private amplifyStackClient: AmplifyStackParser) {} + constructor( + private backendEnvironmentResolver: BackendEnvironmentResolver, + private ccbFetcher: BackendDownloader, + private amplifyStackClient: AmplifyStackParser, + ) {} - getSchema = async (): Promise => { + private readJsonFile = async (filePath: string) => { + const contents = await fs.readFile(filePath, { encoding: 'utf8' }); + return JSON.parse(contents); + }; + + getSchema = async (apis: any): Promise => { try { let apiName; - const meta = stateManager.getMeta(); - const apis = meta?.api ?? {}; + Object.keys(apis).forEach((api) => { const apiObj = apis[api]; if (apiObj.service === 'AppSync') { @@ -65,32 +75,50 @@ export class DataDefinitionFetcher { getDefinition = async (): Promise => { 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) { + + const backendEnvironment = await this.backendEnvironmentResolver.selectBackendEnvironment(); + if (!backendEnvironment?.deploymentArtifacts) return undefined; + + const currentCloudBackendDirectory = await this.ccbFetcher.getCurrentCloudBackend(backendEnvironment.deploymentArtifacts); + + const amplifyMetaPath = path.join(currentCloudBackendDirectory, 'amplify-meta.json'); + + if (!(await fileOrDirectoryExists(amplifyMetaPath))) { + throw new Error('Could not find amplify-meta.json'); + } + + const amplifyMeta = (await this.readJsonFile(amplifyMetaPath)) ?? {}; + + if ('api' in amplifyMeta && Object.keys(amplifyMeta.api).length) { + const tableMappings = await Promise.all( + backendEnvironments.map(async (backendEnvironment) => { + if (!backendEnvironment?.stackName) { return [backendEnvironment.environmentName, undefined]; } - try { - return [backendEnvironment.environmentName, JSON.parse(tableMappingText)]; - } catch (e) { - 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 [backendEnvironment.environmentName, undefined]; + }), + ); - const schema = await this.getSchema(); + const schema = await this.getSchema(amplifyMeta.api); + + return { + tableMappings: Object.fromEntries(tableMappings), + schema, + }; + } - return { - tableMappings: Object.fromEntries(tableMappings), - schema, - }; + return undefined; }; } From 419d7caa9ffaa2433d75abd2fec99caf1e0a2c90 Mon Sep 17 00:00:00 2001 From: Surbhi Bansal Date: Tue, 18 Mar 2025 15:21:51 -0700 Subject: [PATCH 4/4] chore: added >0 check --- packages/amplify-migration/src/data_definition_fetcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/amplify-migration/src/data_definition_fetcher.ts b/packages/amplify-migration/src/data_definition_fetcher.ts index 2a5e0a1a10b..67c1f5c0c23 100644 --- a/packages/amplify-migration/src/data_definition_fetcher.ts +++ b/packages/amplify-migration/src/data_definition_fetcher.ts @@ -89,7 +89,7 @@ export class DataDefinitionFetcher { const amplifyMeta = (await this.readJsonFile(amplifyMetaPath)) ?? {}; - if ('api' in amplifyMeta && Object.keys(amplifyMeta.api).length) { + if ('api' in amplifyMeta && Object.keys(amplifyMeta.api).length > 0) { const tableMappings = await Promise.all( backendEnvironments.map(async (backendEnvironment) => { if (!backendEnvironment?.stackName) {