Skip to content
Closed
5 changes: 3 additions & 2 deletions amplify-migration-apps/product-catalog/configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

set -euxo pipefail

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
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
cp -f custom-roles.json ./amplify/backend/api/${api_name}/custom-roles.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ const { SSMClient, GetParametersCommand } = require('@aws-sdk/client-ssm');

const Sha256 = crypto.Sha256;

const GRAPHQL_ENDPOINT = process.env.API_PRODUCTCATALOG_GRAPHQLAPIENDPOINTOUTPUT;
const GRAPHQL_ENDPOINT = Object.keys(process.env)
.filter((k) => k.startsWith('API_') && k.endsWith('_GRAPHQLAPIENDPOINTOUTPUT'))
.map((k) => process.env[k])
.find(Boolean);
const AWS_REGION = process.env.AWS_REGION || 'us-east-1';
const LOW_STOCK_THRESHOLD = parseInt(process.env.LOW_STOCK_THRESHOLD) || 5;

Expand Down
11 changes: 10 additions & 1 deletion amplify-migration-apps/product-catalog/migration-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@
{
"name": "lowstockproducts",
"runtime": "nodejs",
"template": "hello-world"
"template": "hello-world",
"environment": {
"LOW_STOCK_THRESHOLD": "5"
},
"secrets": {
"PRODUCT_CATALOG_SECRET": "product-catalog-secret-value"
},
"apiAccess": {
"operations": ["Query"]
}
}
]
},
Expand Down
200 changes: 200 additions & 0 deletions amplify-migration-apps/product-catalog/post-generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#!/usr/bin/env npx ts-node
/**
* Post-generate script for product-catalog app.
*
* Applies manual edits required after `amplify gen2-migration generate`:
* 1. Convert lowstockproducts function from CommonJS to ESM + update secret fetching
* 2. Convert S3 trigger function from CommonJS to ESM
* 3. Update lowstockproducts/resource.ts to use secret() for PRODUCT_CATALOG_SECRET
* 4. 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 convertLowStockToESM(appPath: string): Promise<void> {
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 exports.handler to ESM export
let updated = content.replace(
/exports\.handler\s*=\s*async\s*\((\w*)\)\s*=>\s*\{/g,
'export async function handler($1) {',
);

// Replace SSM secret fetching with env var read:
// const secretValue = await fetchSecret();
// becomes:
// const secretValue = process.env['PRODUCT_CATALOG_SECRET'];
updated = updated.replace(
/const secretValue = await fetchSecret\(\);/g,
"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 updated secret fetching');
}

async function convertS3TriggerToESM(appPath: string): Promise<void> {
// Find the S3 trigger function directory (name varies per deployment)
const storagePath = path.join(appPath, 'amplify', 'storage');

let triggerDirs: string[];
try {
const entries = await fs.readdir(storagePath, { withFileTypes: true });
triggerDirs = entries
.filter((e) => e.isDirectory() && e.name.startsWith('S3Trigger'))
.map((e) => e.name);
} catch {
console.log(' amplify/storage/ not found, skipping');
return;
}

for (const triggerDir of triggerDirs) {
const handlerPath = path.join(storagePath, triggerDir, 'index.js');

console.log(`Converting ${triggerDir} to ESM in ${handlerPath}...`);

let content: string;
try {
content = await fs.readFile(handlerPath, 'utf-8');
} catch {
console.log(' index.js not found, skipping');
continue;
}

// Convert exports.handler = async function (event) { to export async function handler(event) {
let updated = content.replace(
/exports\.handler\s*=\s*async\s*function\s*\((\w*)\)\s*\{/g,
'export async function handler($1) {',
);

// Also handle arrow function pattern
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');
continue;
}

await fs.writeFile(handlerPath, updated, 'utf-8');
console.log(' Converted to ESM syntax');
}
}


async function updateLowStockResourceTs(appPath: string): Promise<void> {
const resourcePath = path.join(appPath, 'amplify', 'function', 'lowstockproducts', 'resource.ts');

console.log(`Updating lowstockproducts/resource.ts to use secret()...`);

let content: string;
try {
content = await fs.readFile(resourcePath, 'utf-8');
} catch {
console.log(' resource.ts not found, skipping');
return;
}

// Add secret import if not present
let updated = content.replace(
/import \{ defineFunction \} from ["']@aws-amplify\/backend["'];/,
'import { defineFunction, secret } from "@aws-amplify/backend";',
);

// Replace the SSM path with secret() call
// The generated code has something like:
// PRODUCT_CATALOG_SECRET: "/amplify/..."
// Replace with:
// PRODUCT_CATALOG_SECRET: secret("PRODUCT_CATALOG_SECRET")
updated = updated.replace(
/PRODUCT_CATALOG_SECRET:\s*\n?\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()');
}

async function updateFrontendConfig(appPath: string): Promise<void> {
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;
}

// Change: import amplifyconfig from './amplifyconfiguration.json';
// To: import amplifyconfig from '../amplify_outputs.json';
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<void> {
const { appPath } = options;

console.log(`Running post-generate for product-catalog at ${appPath}`);
console.log('');

await convertLowStockToESM(appPath);
await convertS3TriggerToESM(appPath);
await updateLowStockResourceTs(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);
});
}
70 changes: 70 additions & 0 deletions amplify-migration-apps/product-catalog/post-refactor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/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 preserve the original bucket name.
*/

import fs from 'fs/promises';
import path from 'path';

interface PostRefactorOptions {
appPath: string;
envName?: string;
}

async function uncommentBucketName(appPath: string): Promise<void> {
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;
}

// The generated code has:
// // s3Bucket.bucketName = '...';
// Uncomment it:
// s3Bucket.bucketName = '...';
const updated = content.replace(
/\/\/\s*(s3Bucket\.bucketName\s*=\s*['"][^'"]+['"];)/,
'$1',
);

if (updated === content) {
console.log(' No commented bucketName found, skipping');
return;
}

await fs.writeFile(backendPath, updated, 'utf-8');
console.log(' Uncommented s3Bucket.bucketName');
}

export async function postRefactor(options: PostRefactorOptions): Promise<void> {
const { appPath } = options;

console.log(`Running post-refactor for product-catalog at ${appPath}`);
console.log('');

await uncommentBucketName(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);
});
}
Loading
Loading