From bcde2f578e2f5551d2807fd662df20f6d00ef4de Mon Sep 17 00:00:00 2001 From: sanjanaravikumar-az Date: Sat, 28 Mar 2026 20:47:05 -0400 Subject: [PATCH 1/4] chore: changes made to product-catalog app --- .../product-catalog/configure.sh | 175 ++++++++- .../product-catalog/custom-roles.json | 6 +- .../product-catalog/lowstockproducts.js | 33 +- .../product-catalog/onimageuploaded.js | 3 +- .../product-catalog/post-generate.ts | 336 ++++++++++++++++++ .../product-catalog/post-refactor.ts | 73 ++++ .../src/core/category-initializer.ts | 20 +- .../src/core/gen2-migration-executor.ts | 3 +- 8 files changed, 634 insertions(+), 15 deletions(-) create mode 100644 amplify-migration-apps/product-catalog/post-generate.ts create mode 100644 amplify-migration-apps/product-catalog/post-refactor.ts diff --git a/amplify-migration-apps/product-catalog/configure.sh b/amplify-migration-apps/product-catalog/configure.sh index b4f6510f709..d7b369c9873 100755 --- a/amplify-migration-apps/product-catalog/configure.sh +++ b/amplify-migration-apps/product-catalog/configure.sh @@ -2,11 +2,182 @@ set -euxo pipefail +# Discover directory names dynamically — amplify add creates directories +# with generated names that don't match the config app name. +api_name=$(ls amplify/backend/api) s3_trigger_function_name=$(ls amplify/backend/function | grep S3Trigger) -cp -f schema.graphql ./amplify/backend/api/productcatalog/schema.graphql +# Resolve the deployment name from the project config to use in custom-roles.json. +deployment_name=$(python3 -c "import json; print(json.load(open('amplify/.config/project-config.json'))['projectName'])") +sed "s/\${appId}/${deployment_name}/g" custom-roles.json > ./amplify/backend/api/${api_name}/custom-roles.json + +cp -f schema.graphql ./amplify/backend/api/${api_name}/schema.graphql cp -f lowstockproducts.js ./amplify/backend/function/lowstockproducts/src/index.js cp -f lowstockproducts.package.json ./amplify/backend/function/lowstockproducts/src/package.json cp -f onimageuploaded.js ./amplify/backend/function/${s3_trigger_function_name}/src/index.js cp -f onimageuploaded.package.json ./amplify/backend/function/${s3_trigger_function_name}/src/package.json -cp -f custom-roles.json ./amplify/backend/api/productcatalog/custom-roles.json \ No newline at end of file + +# Wire up API access for both Lambda functions. +# The Amplify CLI does this via: +# 1. Adding dependsOn entries in backend-config.json (declares the dependency) +# 2. Adding parameters to the function CFN template (receives API outputs) +# 3. Using {Ref: paramName} in env vars (resolves at deploy time) +# The root stack reads dependsOn to pass API stack outputs as parameters +# and adds DependsOn to ensure correct deployment ordering. +env_name=$(python3 -c "import json; print(list(json.load(open('amplify/team-provider-info.json')).keys())[0])") +app_id=$(python3 -c "import json; d=json.load(open('amplify/team-provider-info.json')); env=list(d.keys())[0]; print(d[env]['awscloudformation']['AmplifyAppId'])") + +export APP_ID="${app_id}" +export ENV_NAME="${env_name}" +export API_NAME="${api_name}" +export S3_TRIGGER_NAME="${s3_trigger_function_name}" + +python3 << 'PYTHON_SCRIPT' +import json, os + +app_id = os.environ['APP_ID'] +env_name = os.environ['ENV_NAME'] +api_name = os.environ['API_NAME'] +s3_trigger_name = os.environ['S3_TRIGGER_NAME'] + +# The Amplify CLI parameter naming convention for API access: +# Parameter name: api +# Env var keys: API__GRAPHQLAPIENDPOINTOUTPUT, etc. +api_param_name = f'api{api_name}' +api_upper = api_name.upper().replace('-', '') + +def patch_function_cfn(func_name, cfn_path, add_ssm=False): + with open(cfn_path) as f: + t = json.load(f) + + # Add API parameters to the function CFN template. + # The root stack will pass the API stack's outputs as values for these. + t['Parameters'][f'{api_param_name}GraphQLAPIIdOutput'] = { + 'Type': 'String', + 'Default': f'{api_param_name}GraphQLAPIIdOutput' + } + t['Parameters'][f'{api_param_name}GraphQLAPIEndpointOutput'] = { + 'Type': 'String', + 'Default': f'{api_param_name}GraphQLAPIEndpointOutput' + } + t['Parameters'][f'{api_param_name}GraphQLAPIKeyOutput'] = { + 'Type': 'String', + 'Default': f'{api_param_name}GraphQLAPIKeyOutput' + } + + # Set env vars to reference the parameters (resolved at deploy time) + env_vars = t['Resources']['LambdaFunction']['Properties']['Environment']['Variables'] + env_vars[f'API_{api_upper}_GRAPHQLAPIENDPOINTOUTPUT'] = {'Ref': f'{api_param_name}GraphQLAPIEndpointOutput'} + env_vars[f'API_{api_upper}_GRAPHQLAPIIDOUTPUT'] = {'Ref': f'{api_param_name}GraphQLAPIIdOutput'} + env_vars[f'API_{api_upper}_GRAPHQLAPIKEYOUTPUT'] = {'Ref': f'{api_param_name}GraphQLAPIKeyOutput'} + + if add_ssm: + t['Resources']['SsmSecretsPolicy'] = { + 'DependsOn': ['LambdaExecutionRole'], + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyName': 'ssm-secrets-access', + 'Roles': [{'Ref': 'LambdaExecutionRole'}], + 'PolicyDocument': { + 'Version': '2012-10-17', + 'Statement': [{ + 'Effect': 'Allow', + 'Action': ['ssm:GetParameter', 'ssm:GetParameters'], + 'Resource': {'Fn::Sub': 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/amplify/*'} + }, + { + 'Effect': 'Allow', + 'Action': ['appsync:GraphQL'], + 'Resource': {'Fn::Sub': 'arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/*'} + }] + } + } + } + secret_path = f'/amplify/{app_id}/{env_name}/AMPLIFY_lowstockproducts_PRODUCT_CATALOG_SECRET' + env_vars['PRODUCT_CATALOG_SECRET'] = secret_path + + with open(cfn_path, 'w') as f: + json.dump(t, f, indent=2) + f.write('\n') + print(f'Patched CFN template: {cfn_path}') + +def patch_backend_config(func_name): + """Add dependsOn entry for the API to the function in backend-config.json.""" + bc_path = 'amplify/backend/backend-config.json' + with open(bc_path) as f: + bc = json.load(f) + + func_config = bc.get('function', {}).get(func_name, {}) + depends_on = func_config.get('dependsOn', []) + + # Check if API dependency already exists + has_api_dep = any(d.get('category') == 'api' and d.get('resourceName') == api_name for d in depends_on) + if not has_api_dep: + depends_on.append({ + 'category': 'api', + 'resourceName': api_name, + 'attributes': ['GraphQLAPIIdOutput', 'GraphQLAPIEndpointOutput', 'GraphQLAPIKeyOutput'] + }) + func_config['dependsOn'] = depends_on + bc['function'][func_name] = func_config + + with open(bc_path, 'w') as f: + json.dump(bc, f, indent=2) + f.write('\n') + print(f'Added API dependsOn for {func_name} in backend-config.json') + +def patch_function_parameters(func_name): + """Add permissions and dependsOn to function-parameters.json so the root stack passes API outputs.""" + fp_path = f'amplify/backend/function/{func_name}/function-parameters.json' + + # Create the file if it doesn't exist (S3 trigger functions don't have one) + if not os.path.exists(fp_path): + fp = {'lambdaLayers': []} + else: + with open(fp_path) as f: + fp = json.load(f) + + fp['permissions'] = fp.get('permissions', {}) + fp['permissions']['api'] = fp['permissions'].get('api', {}) + fp['permissions']['api'][api_name] = ['Query'] + + fp['dependsOn'] = fp.get('dependsOn', []) + has_api_dep = any(d.get('category') == 'api' and d.get('resourceName') == api_name for d in fp['dependsOn']) + if not has_api_dep: + fp['dependsOn'].append({ + 'category': 'api', + 'resourceName': api_name, + 'attributes': ['GraphQLAPIIdOutput', 'GraphQLAPIEndpointOutput', 'GraphQLAPIKeyOutput'] + }) + + with open(fp_path, 'w') as f: + json.dump(fp, f, indent=2) + f.write('\n') + print(f'Patched function-parameters.json for {func_name}') + +# Patch lowstockproducts +patch_function_cfn( + 'lowstockproducts', + 'amplify/backend/function/lowstockproducts/lowstockproducts-cloudformation-template.json', + add_ssm=True +) +patch_backend_config('lowstockproducts') +patch_function_parameters('lowstockproducts') + +# Patch S3 trigger +patch_function_cfn( + s3_trigger_name, + f'amplify/backend/function/{s3_trigger_name}/{s3_trigger_name}-cloudformation-template.json', + add_ssm=False +) +patch_backend_config(s3_trigger_name) +patch_function_parameters(s3_trigger_name) +PYTHON_SCRIPT + +# Create the SSM parameter for the secret +aws ssm put-parameter \ + --name "/amplify/${app_id}/${env_name}/AMPLIFY_lowstockproducts_PRODUCT_CATALOG_SECRET" \ + --value "test-secret-value" \ + --type SecureString \ + --overwrite \ + --region us-east-1 2>/dev/null || echo "Note: SSM parameter creation skipped (will be created during push)" diff --git a/amplify-migration-apps/product-catalog/custom-roles.json b/amplify-migration-apps/product-catalog/custom-roles.json index 0454e98f10a..59cdb2d206b 100644 --- a/amplify-migration-apps/product-catalog/custom-roles.json +++ b/amplify-migration-apps/product-catalog/custom-roles.json @@ -1,5 +1,7 @@ { "adminRoleNames": [ - "amplify-${appId}" + "amplify-${appId}", + "${appId}LambdaRole", + "amplifyAuthauthenticatedU" ] -} \ No newline at end of file +} diff --git a/amplify-migration-apps/product-catalog/lowstockproducts.js b/amplify-migration-apps/product-catalog/lowstockproducts.js index 23653856e33..90f6481989b 100644 --- a/amplify-migration-apps/product-catalog/lowstockproducts.js +++ b/amplify-migration-apps/product-catalog/lowstockproducts.js @@ -6,10 +6,32 @@ const { SSMClient, GetParametersCommand } = require('@aws-sdk/client-ssm'); const Sha256 = crypto.Sha256; -const GRAPHQL_ENDPOINT = process.env.API_PRODUCTCATALOG_GRAPHQLAPIENDPOINTOUTPUT; const AWS_REGION = process.env.AWS_REGION || 'us-east-1'; const LOW_STOCK_THRESHOLD = parseInt(process.env.LOW_STOCK_THRESHOLD) || 5; +// Resolve the GraphQL endpoint from (in order): +// 1. Environment variable (works when Amplify CLI wires up API access) +// 2. The invoking AppSync event's host header (works for @function invocations) +function getGraphQLEndpoint(event) { + // Try the standard env var name + const fromEnv = process.env.API_PRODUCTCATALOG_GRAPHQLAPIENDPOINTOUTPUT + || Object.entries(process.env).find(([k]) => k.startsWith('API_') && k.endsWith('_GRAPHQLAPIENDPOINTOUTPUT'))?.[1]; + + if (fromEnv && fromEnv.startsWith('http')) return fromEnv; + + // Extract from the invoking AppSync API's host header. + // When AppSync invokes a Lambda via @function, the event contains the + // request headers from the original GraphQL call, including the host. + const host = event?.request?.headers?.host; + if (host && host.includes('appsync-api')) { + const endpoint = `https://${host}/graphql`; + console.log(`Using endpoint from event host header: ${endpoint}`); + return endpoint; + } + + throw new Error('Could not determine GraphQL endpoint from env vars or event'); +} + const listProductsQuery = ` query ListProducts { listProducts { @@ -29,7 +51,7 @@ exports.handler = async (event) => { try { const secretValue = await fetchSecret(); - const products = await fetchProducts(); + const products = await fetchProducts(event); const lowStockProducts = products.filter((product) => product.stock !== null && product.stock < LOW_STOCK_THRESHOLD); console.log(`Found ${lowStockProducts.length} low stock products`); @@ -48,8 +70,9 @@ exports.handler = async (event) => { } }; -async function fetchProducts() { - const endpoint = new URL(GRAPHQL_ENDPOINT); +async function fetchProducts(event) { + const graphqlEndpoint = getGraphQLEndpoint(event); + const endpoint = new URL(graphqlEndpoint); const signer = new SignatureV4({ credentials: defaultProvider(), @@ -70,7 +93,7 @@ async function fetchProducts() { }); const signed = await signer.sign(requestToBeSigned); - const request = new Request(GRAPHQL_ENDPOINT, signed); + const request = new Request(graphqlEndpoint, signed); const response = await fetch(request); const status = response.status; diff --git a/amplify-migration-apps/product-catalog/onimageuploaded.js b/amplify-migration-apps/product-catalog/onimageuploaded.js index 3f1c409afb5..5ae80ca5c7d 100644 --- a/amplify-migration-apps/product-catalog/onimageuploaded.js +++ b/amplify-migration-apps/product-catalog/onimageuploaded.js @@ -13,7 +13,8 @@ const { HttpRequest } = require('@aws-sdk/protocol-http'); const Sha256 = crypto.Sha256; -const GRAPHQL_ENDPOINT = process.env.API_PRODUCTCATALOG_GRAPHQLAPIENDPOINTOUTPUT; +const GRAPHQL_ENDPOINT = process.env.API_PRODUCTCATALOG_GRAPHQLAPIENDPOINTOUTPUT + || Object.entries(process.env).find(([k]) => k.startsWith('API_') && k.endsWith('_GRAPHQLAPIENDPOINTOUTPUT'))?.[1]; const AWS_REGION = process.env.AWS_REGION || 'us-east-1'; function updateProductImageUploadedAtMutation(productId, date) { diff --git a/amplify-migration-apps/product-catalog/post-generate.ts b/amplify-migration-apps/product-catalog/post-generate.ts new file mode 100644 index 00000000000..0762b0ad62b --- /dev/null +++ b/amplify-migration-apps/product-catalog/post-generate.ts @@ -0,0 +1,336 @@ +#!/usr/bin/env npx ts-node +/** + * Post-generate script for product-catalog app. + * + * Applies manual edits required after `amplify gen2-migration generate`: + * 1. Update branchName in amplify/data/resource.ts to "sandbox" + * 2. Add aws_iam import and appsync:GraphQL policy to amplify/backend.ts + * 3. Fix missing awsRegion in GraphQL API userPoolConfig + * 4. Convert lowstockproducts function from CommonJS to ESM and use secret() + * 5. Convert S3 trigger function from CommonJS to ESM + * 6. Update frontend import from amplifyconfiguration.json to amplify_outputs.json + */ + +import fs from 'fs/promises'; +import path from 'path'; + +interface PostGenerateOptions { + appPath: string; + envName?: string; +} + +async function updateBranchName(appPath: string): Promise { + const resourcePath = path.join(appPath, 'amplify', 'data', 'resource.ts'); + console.log(`Updating branchName in ${resourcePath}...`); + + const content = await fs.readFile(resourcePath, 'utf-8'); + const updated = content.replace( + /branchName:\s*['"]([^'"]+)['"]/, + `branchName: 'sandbox'`, + ); + + if (updated === content) { + console.log(' No branchName found to update, skipping'); + return; + } + + await fs.writeFile(resourcePath, updated, 'utf-8'); + console.log(' Updated branchName to "sandbox"'); +} + +/** + * Read the Gen1 AppSync API ID from amplify-meta.json. + * The API name is dynamic (based on deployment name), so we scan for the first API entry. + */ +async function getGen1ApiId(appPath: string): Promise { + const metaPath = path.join(appPath, 'amplify', 'backend', 'amplify-meta.json'); + try { + const meta = JSON.parse(await fs.readFile(metaPath, 'utf-8')) as Record>>; + const apis = meta.api ?? {}; + for (const apiConfig of Object.values(apis)) { + const output = apiConfig.output as Record | undefined; + if (output?.GraphQLAPIIdOutput) { + return output.GraphQLAPIIdOutput; + } + } + } catch { + // amplify-meta.json may not exist yet + } + return undefined; +} + +/** + * Add aws_iam import and appsync:GraphQL policy for the Gen1 API to backend.ts. + * + * The Gen2 auth role needs permission to call the Gen1 AppSync API during the + * transition period when both Gen1 and Gen2 stacks coexist. + */ +async function addAppsyncPolicyToBackend(appPath: string): Promise { + const backendPath = path.join(appPath, 'amplify', 'backend.ts'); + console.log(`Adding appsync:GraphQL policy to ${backendPath}...`); + + let content: string; + try { + content = await fs.readFile(backendPath, 'utf-8'); + } catch { + console.log(' backend.ts not found, skipping'); + return; + } + + // Add aws_iam to the cdk import + if (!content.includes('aws_iam')) { + content = content.replace( + /import\s*\{([^}]*)\}\s*from\s*["']aws-cdk-lib["']/, + (match, imports: string) => { + const trimmed = imports.trim().replace(/,\s*$/, ''); + return `import { ${trimmed}, aws_iam } from "aws-cdk-lib"`; + }, + ); + } + + // Get the Gen1 API ID to scope the policy + const apiId = await getGen1ApiId(appPath); + const resourceArn = apiId + ? `\`arn:aws:appsync:\${backend.data.stack.region}:\${backend.data.stack.account}:apis/${apiId}/*\`` + : '`arn:aws:appsync:${backend.data.stack.region}:${backend.data.stack.account}:apis/*`'; + + // Add the policy statement before the last line (export or closing) + // Look for a good insertion point — after defineBackend call or before export + const policyBlock = ` +backend.auth.resources.authenticatedUserIamRole.addToPrincipalPolicy(new aws_iam.PolicyStatement({ + effect: aws_iam.Effect.ALLOW, + actions: ['appsync:GraphQL'], + resources: [${resourceArn}], +})); +`; + + if (content.includes('addToPrincipalPolicy')) { + console.log(' appsync:GraphQL policy already present, skipping'); + return; + } + + // Insert before the last line + const lines = content.split('\n'); + lines.splice(lines.length - 1, 0, policyBlock); + content = lines.join('\n'); + + await fs.writeFile(backendPath, content, 'utf-8'); + console.log(` Added appsync:GraphQL policy${apiId ? ` for API ${apiId}` : ' (wildcard)'}`); +} + +/** + * Convert lowstockproducts Lambda from CommonJS to ESM and switch to secret(). + * + * - exports.handler → export async function handler + * - Replace SSM secret fetch with process.env['PRODUCT_CATALOG_SECRET'] + */ +async function convertLowstockproductsToESM(appPath: string): Promise { + const handlerPath = path.join(appPath, 'amplify', 'function', 'lowstockproducts', 'index.js'); + console.log(`Converting lowstockproducts to ESM in ${handlerPath}...`); + + let content: string; + try { + content = await fs.readFile(handlerPath, 'utf-8'); + } catch { + console.log(' index.js not found, skipping'); + return; + } + + // Convert CommonJS exports to ESM + let updated = content.replace( + /exports\.handler\s*=\s*async\s*\((\w*)\)\s*=>\s*\{/g, + 'export async function handler($1) {', + ); + + // Replace SSM secret fetch with direct env var access + updated = updated.replace( + /const secretValue = await fetchSecret\(\);/, + "const secretValue = process.env['PRODUCT_CATALOG_SECRET'];", + ); + + if (updated === content) { + console.log(' No changes needed, skipping'); + return; + } + + await fs.writeFile(handlerPath, updated, 'utf-8'); + console.log(' Converted to ESM and switched to env var secret'); +} + +/** + * Update lowstockproducts resource.ts to use secret() instead of hardcoded SSM path. + */ +async function updateLowstockproductsResource(appPath: string): Promise { + const resourcePath = path.join(appPath, 'amplify', 'function', 'lowstockproducts', 'resource.ts'); + console.log(`Updating lowstockproducts resource.ts...`); + + let content: string; + try { + content = await fs.readFile(resourcePath, 'utf-8'); + } catch { + console.log(' resource.ts not found, skipping'); + return; + } + + // Add secret import + let updated = content.replace( + /import\s*\{\s*defineFunction\s*\}\s*from\s*["']@aws-amplify\/backend["']/, + 'import { defineFunction, secret } from "@aws-amplify/backend"', + ); + + // Replace hardcoded SSM path with secret() + updated = updated.replace( + /PRODUCT_CATALOG_SECRET:\s*["'][^"']+["']/, + 'PRODUCT_CATALOG_SECRET: secret("PRODUCT_CATALOG_SECRET")', + ); + + if (updated === content) { + console.log(' No changes needed, skipping'); + return; + } + + await fs.writeFile(resourcePath, updated, 'utf-8'); + console.log(' Updated to use secret()'); +} + +/** + * Convert S3 trigger function from CommonJS to ESM. + */ +async function convertS3TriggerToESM(appPath: string): Promise { + // Find the S3 trigger directory (name varies) + const functionDir = path.join(appPath, 'amplify', 'function'); + let triggerDir: string | undefined; + + try { + const entries = await fs.readdir(functionDir); + triggerDir = entries.find((e) => e.toLowerCase().includes('s3trigger')); + } catch { + console.log(' amplify/function directory not found, skipping'); + return; + } + + if (!triggerDir) { + console.log(' No S3 trigger function found, skipping'); + return; + } + + const handlerPath = path.join(functionDir, triggerDir, 'index.js'); + console.log(`Converting S3 trigger to ESM in ${handlerPath}...`); + + let content: string; + try { + content = await fs.readFile(handlerPath, 'utf-8'); + } catch { + console.log(' index.js not found, skipping'); + return; + } + + // Convert CommonJS exports to ESM (handles both arrow and function forms) + let updated = content.replace( + /exports\.handler\s*=\s*async\s*function\s*\((\w*)\)\s*\{/g, + 'export async function handler($1) {', + ); + updated = updated.replace( + /exports\.handler\s*=\s*async\s*\((\w*)\)\s*=>\s*\{/g, + 'export async function handler($1) {', + ); + + if (updated === content) { + console.log(' No CommonJS exports found, skipping'); + return; + } + + await fs.writeFile(handlerPath, updated, 'utf-8'); + console.log(' Converted to ESM syntax'); +} + +/** + * Fix missing awsRegion in GraphQL API userPoolConfig. + * + * The generated backend.ts sets additionalAuthenticationProviders with + * userPoolConfig but omits awsRegion, causing AppSync to reject the config. + */ +async function fixUserPoolRegionInGraphqlApi(appPath: string): Promise { + const backendPath = path.join(appPath, 'amplify', 'backend.ts'); + console.log(`Fixing user pool region in GraphQL API config in ${backendPath}...`); + + let content: string; + try { + content = await fs.readFile(backendPath, 'utf-8'); + } catch { + console.log(' backend.ts not found, skipping'); + return; + } + + const updated = content.replace( + /userPoolConfig:\s*\{\s*userPoolId:\s*backend\.auth\.resources\.userPool\.userPoolId,?\s*\}/g, + `userPoolConfig: { + userPoolId: backend.auth.resources.userPool.userPoolId, + awsRegion: backend.auth.stack.region, + }`, + ); + + if (updated === content) { + console.log(' No userPoolConfig found to fix, skipping'); + return; + } + + await fs.writeFile(backendPath, updated, 'utf-8'); + console.log(' Added awsRegion to userPoolConfig'); +} + +async function updateFrontendConfig(appPath: string): Promise { + const mainPath = path.join(appPath, 'src', 'main.tsx'); + console.log(`Updating frontend config import in ${mainPath}...`); + + let content: string; + try { + content = await fs.readFile(mainPath, 'utf-8'); + } catch { + console.log(' main.tsx not found, skipping'); + return; + } + + const updated = content.replace( + /from\s*["']\.\/amplifyconfiguration\.json["']/g, + "from '../amplify_outputs.json'", + ); + + if (updated === content) { + console.log(' No amplifyconfiguration.json import found, skipping'); + return; + } + + await fs.writeFile(mainPath, updated, 'utf-8'); + console.log(' Updated import to amplify_outputs.json'); +} + +export async function postGenerate(options: PostGenerateOptions): Promise { + const { appPath } = options; + + console.log(`Running post-generate for product-catalog at ${appPath}`); + console.log(''); + + await updateBranchName(appPath); + await addAppsyncPolicyToBackend(appPath); + await fixUserPoolRegionInGraphqlApi(appPath); + await convertLowstockproductsToESM(appPath); + await updateLowstockproductsResource(appPath); + await convertS3TriggerToESM(appPath); + await updateFrontendConfig(appPath); + + console.log(''); + console.log('Post-generate completed'); +} + +// CLI entry point +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { + const appPath = process.argv[2] || process.cwd(); + const envName = process.argv[3] || 'main'; + + postGenerate({ appPath, envName }).catch((error) => { + console.error('Post-generate failed:', error); + process.exit(1); + }); +} diff --git a/amplify-migration-apps/product-catalog/post-refactor.ts b/amplify-migration-apps/product-catalog/post-refactor.ts new file mode 100644 index 00000000000..ed2980e89a6 --- /dev/null +++ b/amplify-migration-apps/product-catalog/post-refactor.ts @@ -0,0 +1,73 @@ +#!/usr/bin/env npx ts-node +/** + * Post-refactor script for product-catalog app. + * + * Applies manual edits required after `amplify gen2-migration refactor`: + * 1. Uncomment s3Bucket.bucketName in amplify/backend.ts to sync with deployed template + */ + +import fs from 'fs/promises'; +import path from 'path'; + +interface PostRefactorOptions { + appPath: string; + envName?: string; +} + +/** + * Uncomment the s3Bucket.bucketName line in backend.ts. + * + * The generate step produces a commented line like: + * // s3Bucket.bucketName = 'bucket-name-here'; + * + * After refactor, we need to uncomment it to sync with the deployed template. + */ +async function uncommentS3BucketName(appPath: string): Promise { + const backendPath = path.join(appPath, 'amplify', 'backend.ts'); + console.log(`Uncommenting s3Bucket.bucketName in ${backendPath}...`); + + let content: string; + try { + content = await fs.readFile(backendPath, 'utf-8'); + } catch { + console.log(' backend.ts not found, skipping'); + return; + } + + const updated = content.replace( + /\/\/\s*(s3Bucket\.bucketName\s*=\s*['"][^'"]+['"];?)/g, + '$1', + ); + + if (updated === content) { + console.log(' No commented s3Bucket.bucketName found, skipping'); + return; + } + + await fs.writeFile(backendPath, updated, 'utf-8'); + console.log(' Uncommented s3Bucket.bucketName'); +} + +export async function postRefactor(options: PostRefactorOptions): Promise { + const { appPath } = options; + + console.log(`Running post-refactor for product-catalog at ${appPath}`); + console.log(''); + + await uncommentS3BucketName(appPath); + + console.log(''); + console.log('Post-refactor completed'); +} + +// CLI entry point +const isMainModule = import.meta.url === `file://${process.argv[1]}`; +if (isMainModule) { + const appPath = process.argv[2] || process.cwd(); + const envName = process.argv[3] || 'main'; + + postRefactor({ appPath, envName }).catch((error) => { + console.error('Post-refactor failed:', error); + process.exit(1); + }); +} diff --git a/packages/amplify-gen2-migration-e2e-system/src/core/category-initializer.ts b/packages/amplify-gen2-migration-e2e-system/src/core/category-initializer.ts index 67b895da3ff..40374dd7e3c 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/core/category-initializer.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/core/category-initializer.ts @@ -204,10 +204,22 @@ export class CategoryInitializer { // Use addApi with explicit auth types config. // Pass requireAuthSetup = false because the auth category is already initialized, // so the CLI won't prompt for Cognito setup — it reuses the existing user pool. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const authTypesConfig: Record> = { 'API key': {} }; - if (needsCognitoAuth) authTypesConfig['Amazon Cognito User Pool'] = {}; - if (needsIamAuth) authTypesConfig['IAM'] = {}; + // Build authTypesConfig in the order specified by migration-config.json so the + // first auth mode becomes the default (addApi uses the first key as default). + const authModeMap: Record = { + IAM: 'IAM', + API_KEY: 'API key', + COGNITO_USER_POOLS: 'Amazon Cognito User Pool', + }; + const authTypesConfig: Record> = {}; + for (const mode of apiConfig.authModes ?? []) { + const mapped = authModeMap[mode]; + if (mapped) authTypesConfig[mapped] = {}; + } + // Fallback: ensure at least API key is present + if (Object.keys(authTypesConfig).length === 0) { + authTypesConfig['API key'] = {}; + } await addApi(appPath, authTypesConfig, false); } else { await addApiWithBlankSchema(appPath); diff --git a/packages/amplify-gen2-migration-e2e-system/src/core/gen2-migration-executor.ts b/packages/amplify-gen2-migration-e2e-system/src/core/gen2-migration-executor.ts index 55fdecd4961..f65cbe9e448 100644 --- a/packages/amplify-gen2-migration-e2e-system/src/core/gen2-migration-executor.ts +++ b/packages/amplify-gen2-migration-e2e-system/src/core/gen2-migration-executor.ts @@ -106,8 +106,9 @@ export class Gen2MigrationExecutor { * Enables deletion protection on DynamoDB tables, sets a deny-all stack policy, * and adds GEN2_MIGRATION_ENVIRONMENT_NAME env var to the Amplify app. */ + /** Run gen2-migration lock, skipping drift validations for known S3 trigger issues. */ public async lock(appPath: string): Promise { - await this.executeStep('lock', appPath); + await this.executeStep('lock', appPath, ['--skip-validations']); } /** From 91a516d50a312d38896e5e1f8137c4c13f9e0493 Mon Sep 17 00:00:00 2001 From: sanjanaravikumar-az Date: Sat, 28 Mar 2026 21:03:00 -0400 Subject: [PATCH 2/4] chore: changes made to product-catalog app --- .../product-catalog/post-generate.ts | 54 +------------------ 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/amplify-migration-apps/product-catalog/post-generate.ts b/amplify-migration-apps/product-catalog/post-generate.ts index 0762b0ad62b..d1a70c7fed9 100644 --- a/amplify-migration-apps/product-catalog/post-generate.ts +++ b/amplify-migration-apps/product-catalog/post-generate.ts @@ -7,8 +7,7 @@ * 2. Add aws_iam import and appsync:GraphQL policy to amplify/backend.ts * 3. Fix missing awsRegion in GraphQL API userPoolConfig * 4. Convert lowstockproducts function from CommonJS to ESM and use secret() - * 5. Convert S3 trigger function from CommonJS to ESM - * 6. Update frontend import from amplifyconfiguration.json to amplify_outputs.json + * 5. Update frontend import from amplifyconfiguration.json to amplify_outputs.json */ import fs from 'fs/promises'; @@ -193,56 +192,6 @@ async function updateLowstockproductsResource(appPath: string): Promise { console.log(' Updated to use secret()'); } -/** - * Convert S3 trigger function from CommonJS to ESM. - */ -async function convertS3TriggerToESM(appPath: string): Promise { - // Find the S3 trigger directory (name varies) - const functionDir = path.join(appPath, 'amplify', 'function'); - let triggerDir: string | undefined; - - try { - const entries = await fs.readdir(functionDir); - triggerDir = entries.find((e) => e.toLowerCase().includes('s3trigger')); - } catch { - console.log(' amplify/function directory not found, skipping'); - return; - } - - if (!triggerDir) { - console.log(' No S3 trigger function found, skipping'); - return; - } - - const handlerPath = path.join(functionDir, triggerDir, 'index.js'); - console.log(`Converting S3 trigger to ESM in ${handlerPath}...`); - - let content: string; - try { - content = await fs.readFile(handlerPath, 'utf-8'); - } catch { - console.log(' index.js not found, skipping'); - return; - } - - // Convert CommonJS exports to ESM (handles both arrow and function forms) - let updated = content.replace( - /exports\.handler\s*=\s*async\s*function\s*\((\w*)\)\s*\{/g, - 'export async function handler($1) {', - ); - updated = updated.replace( - /exports\.handler\s*=\s*async\s*\((\w*)\)\s*=>\s*\{/g, - 'export async function handler($1) {', - ); - - if (updated === content) { - console.log(' No CommonJS exports found, skipping'); - return; - } - - await fs.writeFile(handlerPath, updated, 'utf-8'); - console.log(' Converted to ESM syntax'); -} /** * Fix missing awsRegion in GraphQL API userPoolConfig. @@ -316,7 +265,6 @@ export async function postGenerate(options: PostGenerateOptions): Promise await fixUserPoolRegionInGraphqlApi(appPath); await convertLowstockproductsToESM(appPath); await updateLowstockproductsResource(appPath); - await convertS3TriggerToESM(appPath); await updateFrontendConfig(appPath); console.log(''); From 64712dd4a5125331e0d8f384111f7e1b9708c568 Mon Sep 17 00:00:00 2001 From: sanjanaravikumar-az Date: Wed, 1 Apr 2026 14:01:34 -0400 Subject: [PATCH 3/4] Delete amplify-migration-apps/product-catalog/configure.sh --- .../product-catalog/configure.sh | 183 ------------------ 1 file changed, 183 deletions(-) delete mode 100755 amplify-migration-apps/product-catalog/configure.sh diff --git a/amplify-migration-apps/product-catalog/configure.sh b/amplify-migration-apps/product-catalog/configure.sh deleted file mode 100755 index d7b369c9873..00000000000 --- a/amplify-migration-apps/product-catalog/configure.sh +++ /dev/null @@ -1,183 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -# Discover directory names dynamically — amplify add creates directories -# with generated names that don't match the config app name. -api_name=$(ls amplify/backend/api) -s3_trigger_function_name=$(ls amplify/backend/function | grep S3Trigger) - -# Resolve the deployment name from the project config to use in custom-roles.json. -deployment_name=$(python3 -c "import json; print(json.load(open('amplify/.config/project-config.json'))['projectName'])") -sed "s/\${appId}/${deployment_name}/g" custom-roles.json > ./amplify/backend/api/${api_name}/custom-roles.json - -cp -f schema.graphql ./amplify/backend/api/${api_name}/schema.graphql -cp -f lowstockproducts.js ./amplify/backend/function/lowstockproducts/src/index.js -cp -f lowstockproducts.package.json ./amplify/backend/function/lowstockproducts/src/package.json -cp -f onimageuploaded.js ./amplify/backend/function/${s3_trigger_function_name}/src/index.js -cp -f onimageuploaded.package.json ./amplify/backend/function/${s3_trigger_function_name}/src/package.json - -# Wire up API access for both Lambda functions. -# The Amplify CLI does this via: -# 1. Adding dependsOn entries in backend-config.json (declares the dependency) -# 2. Adding parameters to the function CFN template (receives API outputs) -# 3. Using {Ref: paramName} in env vars (resolves at deploy time) -# The root stack reads dependsOn to pass API stack outputs as parameters -# and adds DependsOn to ensure correct deployment ordering. -env_name=$(python3 -c "import json; print(list(json.load(open('amplify/team-provider-info.json')).keys())[0])") -app_id=$(python3 -c "import json; d=json.load(open('amplify/team-provider-info.json')); env=list(d.keys())[0]; print(d[env]['awscloudformation']['AmplifyAppId'])") - -export APP_ID="${app_id}" -export ENV_NAME="${env_name}" -export API_NAME="${api_name}" -export S3_TRIGGER_NAME="${s3_trigger_function_name}" - -python3 << 'PYTHON_SCRIPT' -import json, os - -app_id = os.environ['APP_ID'] -env_name = os.environ['ENV_NAME'] -api_name = os.environ['API_NAME'] -s3_trigger_name = os.environ['S3_TRIGGER_NAME'] - -# The Amplify CLI parameter naming convention for API access: -# Parameter name: api -# Env var keys: API__GRAPHQLAPIENDPOINTOUTPUT, etc. -api_param_name = f'api{api_name}' -api_upper = api_name.upper().replace('-', '') - -def patch_function_cfn(func_name, cfn_path, add_ssm=False): - with open(cfn_path) as f: - t = json.load(f) - - # Add API parameters to the function CFN template. - # The root stack will pass the API stack's outputs as values for these. - t['Parameters'][f'{api_param_name}GraphQLAPIIdOutput'] = { - 'Type': 'String', - 'Default': f'{api_param_name}GraphQLAPIIdOutput' - } - t['Parameters'][f'{api_param_name}GraphQLAPIEndpointOutput'] = { - 'Type': 'String', - 'Default': f'{api_param_name}GraphQLAPIEndpointOutput' - } - t['Parameters'][f'{api_param_name}GraphQLAPIKeyOutput'] = { - 'Type': 'String', - 'Default': f'{api_param_name}GraphQLAPIKeyOutput' - } - - # Set env vars to reference the parameters (resolved at deploy time) - env_vars = t['Resources']['LambdaFunction']['Properties']['Environment']['Variables'] - env_vars[f'API_{api_upper}_GRAPHQLAPIENDPOINTOUTPUT'] = {'Ref': f'{api_param_name}GraphQLAPIEndpointOutput'} - env_vars[f'API_{api_upper}_GRAPHQLAPIIDOUTPUT'] = {'Ref': f'{api_param_name}GraphQLAPIIdOutput'} - env_vars[f'API_{api_upper}_GRAPHQLAPIKEYOUTPUT'] = {'Ref': f'{api_param_name}GraphQLAPIKeyOutput'} - - if add_ssm: - t['Resources']['SsmSecretsPolicy'] = { - 'DependsOn': ['LambdaExecutionRole'], - 'Type': 'AWS::IAM::Policy', - 'Properties': { - 'PolicyName': 'ssm-secrets-access', - 'Roles': [{'Ref': 'LambdaExecutionRole'}], - 'PolicyDocument': { - 'Version': '2012-10-17', - 'Statement': [{ - 'Effect': 'Allow', - 'Action': ['ssm:GetParameter', 'ssm:GetParameters'], - 'Resource': {'Fn::Sub': 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/amplify/*'} - }, - { - 'Effect': 'Allow', - 'Action': ['appsync:GraphQL'], - 'Resource': {'Fn::Sub': 'arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/*'} - }] - } - } - } - secret_path = f'/amplify/{app_id}/{env_name}/AMPLIFY_lowstockproducts_PRODUCT_CATALOG_SECRET' - env_vars['PRODUCT_CATALOG_SECRET'] = secret_path - - with open(cfn_path, 'w') as f: - json.dump(t, f, indent=2) - f.write('\n') - print(f'Patched CFN template: {cfn_path}') - -def patch_backend_config(func_name): - """Add dependsOn entry for the API to the function in backend-config.json.""" - bc_path = 'amplify/backend/backend-config.json' - with open(bc_path) as f: - bc = json.load(f) - - func_config = bc.get('function', {}).get(func_name, {}) - depends_on = func_config.get('dependsOn', []) - - # Check if API dependency already exists - has_api_dep = any(d.get('category') == 'api' and d.get('resourceName') == api_name for d in depends_on) - if not has_api_dep: - depends_on.append({ - 'category': 'api', - 'resourceName': api_name, - 'attributes': ['GraphQLAPIIdOutput', 'GraphQLAPIEndpointOutput', 'GraphQLAPIKeyOutput'] - }) - func_config['dependsOn'] = depends_on - bc['function'][func_name] = func_config - - with open(bc_path, 'w') as f: - json.dump(bc, f, indent=2) - f.write('\n') - print(f'Added API dependsOn for {func_name} in backend-config.json') - -def patch_function_parameters(func_name): - """Add permissions and dependsOn to function-parameters.json so the root stack passes API outputs.""" - fp_path = f'amplify/backend/function/{func_name}/function-parameters.json' - - # Create the file if it doesn't exist (S3 trigger functions don't have one) - if not os.path.exists(fp_path): - fp = {'lambdaLayers': []} - else: - with open(fp_path) as f: - fp = json.load(f) - - fp['permissions'] = fp.get('permissions', {}) - fp['permissions']['api'] = fp['permissions'].get('api', {}) - fp['permissions']['api'][api_name] = ['Query'] - - fp['dependsOn'] = fp.get('dependsOn', []) - has_api_dep = any(d.get('category') == 'api' and d.get('resourceName') == api_name for d in fp['dependsOn']) - if not has_api_dep: - fp['dependsOn'].append({ - 'category': 'api', - 'resourceName': api_name, - 'attributes': ['GraphQLAPIIdOutput', 'GraphQLAPIEndpointOutput', 'GraphQLAPIKeyOutput'] - }) - - with open(fp_path, 'w') as f: - json.dump(fp, f, indent=2) - f.write('\n') - print(f'Patched function-parameters.json for {func_name}') - -# Patch lowstockproducts -patch_function_cfn( - 'lowstockproducts', - 'amplify/backend/function/lowstockproducts/lowstockproducts-cloudformation-template.json', - add_ssm=True -) -patch_backend_config('lowstockproducts') -patch_function_parameters('lowstockproducts') - -# Patch S3 trigger -patch_function_cfn( - s3_trigger_name, - f'amplify/backend/function/{s3_trigger_name}/{s3_trigger_name}-cloudformation-template.json', - add_ssm=False -) -patch_backend_config(s3_trigger_name) -patch_function_parameters(s3_trigger_name) -PYTHON_SCRIPT - -# Create the SSM parameter for the secret -aws ssm put-parameter \ - --name "/amplify/${app_id}/${env_name}/AMPLIFY_lowstockproducts_PRODUCT_CATALOG_SECRET" \ - --value "test-secret-value" \ - --type SecureString \ - --overwrite \ - --region us-east-1 2>/dev/null || echo "Note: SSM parameter creation skipped (will be created during push)" From d6c97d6018bd4043f3a6cffb2fc3665e902edb44 Mon Sep 17 00:00:00 2001 From: sanjanaravikumar-az Date: Wed, 1 Apr 2026 14:02:41 -0400 Subject: [PATCH 4/4] Delete amplify-migration-apps/product-catalog/custom-roles.json --- amplify-migration-apps/product-catalog/custom-roles.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 amplify-migration-apps/product-catalog/custom-roles.json diff --git a/amplify-migration-apps/product-catalog/custom-roles.json b/amplify-migration-apps/product-catalog/custom-roles.json deleted file mode 100644 index 59cdb2d206b..00000000000 --- a/amplify-migration-apps/product-catalog/custom-roles.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "adminRoleNames": [ - "amplify-${appId}", - "${appId}LambdaRole", - "amplifyAuthauthenticatedU" - ] -}