feat(gen2-migration): add geo category code generation for gen2-migration #14596
feat(gen2-migration): add geo category code generation for gen2-migration #14596sai-ray wants to merge 18 commits intogen2-migrationfrom
Conversation
…discriminated union
…p level resource.ts
packages/amplify-cli/src/commands/gen2-migration/generate/backend/synthesizer.ts
Fixed
Show fixed
Hide fixed
packages/amplify-cli/src/commands/gen2-migration/generate/backend/synthesizer.ts
Fixed
Show fixed
Hide fixed
packages/amplify-cli/src/commands/gen2-migration/generate/backend/synthesizer.ts
Fixed
Show fixed
Hide fixed
|
Question - is there an app that tests this? Can you add the input |
|
Request - In the pr description, can you add a quick run-down of the apis to cdk-from-cfn which are exposed to us? What are we using and what does that do? Doesn't need to be super detailed, just enough to understand whats going on |
"geo": {
"storeLocatorDemoGeofence": {
"accessType": "CognitoGroups",
"dependsOn": [
{
"attributes": [
"UserPoolId"
],
"category": "auth",
"resourceName": "storelocatordemoXXXXXX"
},
{
"attributes": [
"storeLocatorDemoAdminGroupRole"
],
"category": "auth",
"resourceName": "userPoolGroups"
}
],
"isDefault": true,
"providerPlugin": "awscloudformation",
"service": "GeofenceCollection",
"providerMetadata": {
"s3TemplateURL": "https://s3.amazonaws.com/amplify-storelocatordemo-mainn-XXXXX-deployment/amplify-cfn-templates/geo/storeLocatorDemoGeofence-cloudformation-template.json",
"logicalId": "geostoreLocatorDemoGeofence"
},
"lastPushTimeStamp": "2026-02-27T17:34:39.687Z",
"output": {
"Region": "us-east-1",
"Arn": "arn:aws:geo:us-east-XXXXXgeofence-collection/storeLocatorDemoGeofence-mainn",
"Name": "storeLocatorDemoGeofence-mainn"
},
"lastPushDirHash": "XXXXXX"
},
"storeLocatorDemoMap": {
"accessType": "AuthorizedAndGuestUsers",
"dependsOn": [
{
"category": "auth",
"resourceName": "storelocatordemoXXXXX",
"attributes": [
"UserPoolId"
]
},
{
"category": "auth",
"resourceName": "userPoolGroups",
"attributes": [
"storeLocatorDemoAdminGroupRole"
]
}
],
"isDefault": true,
"mapStyle": "VectorEsriStreets",
"providerPlugin": "awscloudformation",
"service": "Map",
"providerMetadata": {
"s3TemplateURL": "https://s3.amazonaws.com/amplify-storelocatordemo-mainn-XXXXX-deployment/amplify-cfn-templates/geo/storeLocatorDemoMap-cloudformation-template.json",
"logicalId": "geostoreLocatorDemoMap"
},
"lastPushTimeStamp": "2026-02-27T17:37:02.728Z",
"output": {
"Style": "VectorEsriStreets",
"Region": "us-east-1",
"Arn": "arn:aws:geo:us-east-XXXXXXX:map/storeLocatorDemoMap-mainn",
"Name": "storeLocatorDemoMap-mainn"
},
"lastPushDirHash": "XXXXX"
},
"storeLocatorDemoSearch": {
"accessType": "AuthorizedAndGuestUsers",
"dataProvider": "HERE",
"dataSourceIntendedUse": "SingleUse",
"dependsOn": [
{
"category": "auth",
"resourceName": "storelocatordemoXXXXX",
"attributes": [
"UserPoolId"
]
},
{
"category": "auth",
"resourceName": "userPoolGroups",
"attributes": [
"storeLocatorDemoAdminGroupRole"
]
}
],
"isDefault": true,
"providerPlugin": "awscloudformation",
"service": "PlaceIndex",
"providerMetadata": {
"s3TemplateURL": "https://s3.amazonaws.com/amplify-storelocatordemo-mainn-XXXXX-deployment/amplify-cfn-templates/geo/storeLocatorDemoSearch-cloudformation-template.json",
"logicalId": "geostoreLocatorDemoSearch"
},
"lastPushTimeStamp": "2026-02-27T17:34:39.694Z",
"output": {
"Region": "us-east-1",
"Arn": "arn:aws:geo:us-east-XXXXX:place-index/storeLocatorDemoSearch-mainn",
"Name": "storeLocatorDemoSearch-mainn"
},
"lastPushDirHash": "XXXXXX"
}
}Gen1 Geo Stack:Geo Fences :
{
"groupPermissions": {
"storeLocatorDemoAdmin": [
"Read geofence",
"Create/Update geofence",
"Delete geofence",
"List geofences"
]
}
}
{
"collectionName": "storeLocatorDemoGeofence",
"isDefault": true
}
Geo Maps :
{
"groupPermissions": [
"storeLocatorDemoAdmin"
]
}
"authRoleName": {
"Ref": "AuthRoleName"
},
"unauthRoleName": {
"Ref": "UnauthRoleName"
},
"mapName": "storeLocatorDemoMap",
"mapStyle": "VectorEsriStreets",
"isDefault": true
}
{
"Mappings": {
"RegionMapping": {
"us-east-1": {
"locationServiceRegion": "us-east-1"
},
"us-east-2": {
"locationServiceRegion": "us-east-2"
},
"us-west-2": {
"locationServiceRegion": "us-west-2"
},
"ap-southeast-1": {
"locationServiceRegion": "ap-southeast-1"
},
"ap-southeast-2": {
"locationServiceRegion": "ap-southeast-2"
},
"ap-northeast-1": {
"locationServiceRegion": "ap-northeast-1"
},
"eu-central-1": {
"locationServiceRegion": "eu-central-1"
},
"eu-north-1": {
"locationServiceRegion": "eu-north-1"
},
"eu-west-1": {
"locationServiceRegion": "eu-west-1"
},
"sa-east-1": {
"locationServiceRegion": "us-east-1"
},
"ca-central-1": {
"locationServiceRegion": "us-east-1"
},
"us-west-1": {
"locationServiceRegion": "us-west-2"
},
"cn-north-1": {
"locationServiceRegion": "us-west-2"
},
"cn-northwest-1": {
"locationServiceRegion": "us-west-2"
},
"ap-south-1": {
"locationServiceRegion": "us-west-2"
},
"ap-northeast-3": {
"locationServiceRegion": "us-west-2"
},
"ap-northeast-2": {
"locationServiceRegion": "us-west-2"
},
"eu-west-2": {
"locationServiceRegion": "eu-west-1"
},
"eu-west-3": {
"locationServiceRegion": "eu-west-1"
},
"me-south-1": {
"locationServiceRegion": "ap-southeast-1"
}
}
},
"Parameters": {
"authuserPoolGroupsstoreLocatorDemoAdminGroupRole": {
"Type": "String"
},
"authstorelocatordemoXXXXXUserPoolId": {
"Type": "String"
},
"authRoleName": {
"Type": "String"
},
"unauthRoleName": {
"Type": "String"
},
"mapName": {
"Type": "String"
},
"mapStyle": {
"Type": "String"
},
"env": {
"Type": "String"
},
"isDefault": {
"Type": "String"
}
},
"Resources": {
"CustomMapLambdaServiceRoleXXXXX": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
]
}
]
}
},
"CustomMapLambdaServiceRoleDefaultPolicyXXXXX": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "geo:CreateMap",
"Effect": "Allow",
"Resource": "*"
},
{
"Action": [
"geo:UpdateMap",
"geo:DeleteMap"
],
"Effect": "Allow",
"Resource": {
"Fn::Sub": [
"arn:aws:geo:${region}:${account}:map/${mapName}",
{
"region": {
"Fn::FindInMap": [
"RegionMapping",
{
"Ref": "AWS::Region"
},
"locationServiceRegion"
]
},
"account": {
"Ref": "AWS::AccountId"
},
"mapName": {
"Fn::Join": [
"-",
[
{
"Ref": "mapName"
},
{
"Ref": "env"
}
]
]
}
}
]
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "CustomMapLambdaServiceRoleDefaultPolicyXXXXX",
"Roles": [
{
"Ref": "CustomMapLambdaServiceRoleXXXXX"
}
]
}
},
"CustomMapLambdaXXXXX": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"ZipFile": "const response = require('cfn-response');\nconst { LocationClient, CreateMapCommand, DeleteMapCommand, UpdateMapCommand } = require('@aws-sdk/client-location');\nexports.handler = async function (event, context) {\n try {\n console.log('REQUEST RECEIVED:' + JSON.stringify(event));\n const pricingPlan = 'RequestBasedUsage';\n if (event.RequestType === 'Create') {\n let params = {\n MapName: event.ResourceProperties.mapName,\n Configuration: {\n Style: event.ResourceProperties.mapStyle,\n },\n PricingPlan: pricingPlan,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new CreateMapCommand(params));\n console.log('create resource response data' + JSON.stringify(res));\n if (res.MapName && res.MapArn) {\n await response.send(event, context, response.SUCCESS, res, params.MapName);\n } else {\n await response.send(event, context, response.FAILED, res, params.MapName);\n }\n }\n if (event.RequestType === 'Update') {\n let params = {\n MapName: event.ResourceProperties.mapName,\n PricingPlan: pricingPlan,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new UpdateMapCommand(params));\n console.log('update resource response data' + JSON.stringify(res));\n if (res.MapName && res.MapArn) {\n await response.send(event, context, response.SUCCESS, res, params.MapName);\n } else {\n await response.send(event, context, response.FAILED, res, params.MapName);\n }\n }\n if (event.RequestType === 'Delete') {\n let params = {\n MapName: event.ResourceProperties.mapName,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new DeleteMapCommand(params));\n console.log('delete resource response data' + JSON.stringify(res));\n await response.send(event, context, response.SUCCESS, res, params.MapName);\n }\n } catch (err) {\n console.log(err.stack);\n const res = { Error: err };\n await response.send(event, context, response.FAILED, res, event.ResourceProperties.mapName);\n throw err;\n }\n};\n"
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"CustomMapLambdaServiceRoleXXXXX",
"Arn"
]
},
"Runtime": "nodejs22.x",
"Timeout": 300
},
"DependsOn": [
"CustomMapLambdaServiceRoleDefaultPolicyXXXXX",
"CustomMapLambdaServiceRoleXXXXX"
]
},
"CustomMap": {
"Type": "Custom::LambdaCallout",
"Properties": {
"ServiceToken": {
"Fn::GetAtt": [
"CustomMapLambdaXXXXX",
"Arn"
]
},
"mapName": {
"Fn::Join": [
"-",
[
{
"Ref": "mapName"
},
{
"Ref": "env"
}
]
]
},
"mapStyle": {
"Ref": "mapStyle"
},
"region": {
"Fn::FindInMap": [
"RegionMapping",
{
"Ref": "AWS::Region"
},
"locationServiceRegion"
]
},
"env": {
"Ref": "env"
}
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"MapPolicy": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"geo:GetMapStyleDescriptor",
"geo:GetMapGlyphs",
"geo:GetMapSprites",
"geo:GetMapTile"
],
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"CustomMap",
"MapArn"
]
}
}
],
"Version": "2012-10-17"
},
"PolicyName": {
"Fn::Join": [
"",
[
{
"Fn::Join": [
"-",
[
{
"Ref": "mapName"
},
{
"Ref": "env"
}
]
]
},
"Policy"
]
]
},
"Roles": [
{
"Ref": "authRoleName"
},
{
"Ref": "unauthRoleName"
},
{
"Fn::Join": [
"-",
[
{
"Ref": "authstorelocatordemoXXXXXUserPoolId"
},
"storeLocatorDemoAdminGroupRole"
]
]
}
]
}
}
},
"Outputs": {
"Name": {
"Value": {
"Fn::GetAtt": [
"CustomMap",
"MapName"
]
}
},
"Style": {
"Value": {
"Ref": "mapStyle"
}
},
"Region": {
"Value": {
"Fn::FindInMap": [
"RegionMapping",
{
"Ref": "AWS::Region"
},
"locationServiceRegion"
]
}
},
"Arn": {
"Value": {
"Fn::GetAtt": [
"CustomMap",
"MapArn"
]
}
}
}
}Geo Location Search
{
"groupPermissions": [
"storeLocatorDemoAdmin"
]
}
{
"authRoleName": {
"Ref": "AuthRoleName"
},
"unauthRoleName": {
"Ref": "UnauthRoleName"
},
"indexName": "storeLocatorDemoSearch",
"dataProvider": "Here",
"dataSourceIntendedUse": "SingleUse",
"isDefault": true
}
Gen2 Geo Codegen OutputGeoFence Collections
import { geostoreLocatorDemoGeofence } from "./storeLocatorDemoGeofence-construct";
import { Backend } from "@aws-amplify/backend";
const branchName = process.env.AWS_BRANCH ?? "sandbox";
export const defineStoreLocatorDemoGeofence = (backend: Backend<any>) => {
const storeLocatorDemoGeofenceStack = backend.createStack("storeLocatorDemoGeofence");
const storeLocatorDemoGeofence = new geostoreLocatorDemoGeofence(storeLocatorDemoGeofenceStack, "storeLocatorDemoGeofence", {
authstorelocatordemoXXXXXUserPoolId: backend.auth.resources.userPool.userPoolId,
authuserPoolGroupsstoreLocatorDemoAdminGroupRole: backend.auth.resources.groups["storeLocatorDemoAdmin"].role.roleName,
collectionName: "storeLocatorDemoGeofence",
branchName,
isDefault: "true"
});
return storeLocatorDemoGeofence;
};
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export interface geostoreLocatorDemoGeofenceProps {
/**
*/
readonly authuserPoolGroupsstoreLocatorDemoAdminGroupRole: string;
/**
*/
readonly authstorelocatordemoXXXXXUserPoolId: string;
/**
*/
readonly collectionName: string;
/**
*/
readonly isDefault: string;
/**
*/
readonly branchName: string;
}
/**
* {"createdOn":"Mac","createdBy":"Amplify","createdWith":"14.2.5","stackType":"geo-GeofenceCollection","metadata":{"whyContinueWithGen1":""}}
*/
export class geostoreLocatorDemoGeofence extends Construct {
public readonly name;
public readonly region;
public readonly arn;
public constructor(scope: Construct, id: string, props: geostoreLocatorDemoGeofenceProps) {
super(scope, id);
// Mappings
const regionMapping = new cdk.CfnMapping(this, 'RegionMapping', {
mapping: {
'us-east-1': {
'locationServiceRegion': 'us-east-1',
},
'us-east-2': {
'locationServiceRegion': 'us-east-2',
},
'us-west-2': {
'locationServiceRegion': 'us-west-2',
},
'ap-southeast-1': {
'locationServiceRegion': 'ap-southeast-1',
},
'ap-southeast-2': {
'locationServiceRegion': 'ap-southeast-2',
},
'ap-northeast-1': {
'locationServiceRegion': 'ap-northeast-1',
},
'eu-central-1': {
'locationServiceRegion': 'eu-central-1',
},
'eu-north-1': {
'locationServiceRegion': 'eu-north-1',
},
'eu-west-1': {
'locationServiceRegion': 'eu-west-1',
},
'sa-east-1': {
'locationServiceRegion': 'us-east-1',
},
'ca-central-1': {
'locationServiceRegion': 'us-east-1',
},
'us-west-1': {
'locationServiceRegion': 'us-west-2',
},
'cn-north-1': {
'locationServiceRegion': 'us-west-2',
},
'cn-northwest-1': {
'locationServiceRegion': 'us-west-2',
},
'ap-south-1': {
'locationServiceRegion': 'us-west-2',
},
'ap-northeast-3': {
'locationServiceRegion': 'us-west-2',
},
'ap-northeast-2': {
'locationServiceRegion': 'us-west-2',
},
'eu-west-2': {
'locationServiceRegion': 'eu-west-1',
},
'eu-west-3': {
'locationServiceRegion': 'eu-west-1',
},
'me-south-1': {
'locationServiceRegion': 'ap-southeast-1',
},
},
});
// Resources
const customGeofenceCollectionLambdaServiceRoleXXXXX = new iam.CfnRole(this, 'CustomGeofenceCollectionLambdaServiceRoleXXXXX', {
assumeRolePolicyDocument: {
Statement: [
{
Action: 'sts:AssumeRole',
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
},
],
Version: '2012-10-17',
},
managedPolicyArns: [
[
'arn:',
cdk.Stack.of(this).partition,
':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
].join(''),
],
});
const customGeofenceCollectionLambdaServiceRoleDefaultPolicyXXXXX = new iam.CfnPolicy(this, 'CustomGeofenceCollectionLambdaServiceRoleDefaultPolicyXXXXX', {
policyDocument: {
Statement: [
{
Action: 'geo:CreateGeofenceCollection',
Effect: 'Allow',
Resource: '*',
},
{
Action: [
'geo:UpdateGeofenceCollection',
'geo:DeleteGeofenceCollection',
],
Effect: 'Allow',
Resource: `arn:aws:geo:${regionMapping.findInMap(cdk.Stack.of(this).region, 'locationServiceRegion')}:${cdk.Stack.of(this).account}:geofence-collection/${[
props.collectionName!,
props.branchName!,
].join('-')}`,
},
],
Version: '2012-10-17',
},
policyName: 'CustomGeofenceCollectionLambdaServiceRoleDefaultPolicyXXXXXX',
roles: [
customGeofenceCollectionLambdaServiceRoleXXXXXX.ref,
],
});
const customGeofenceCollectionLambdaXXXXX = new lambda.CfnFunction(this, 'CustomGeofenceCollectionLambdaXXXXX', {
code: {
zipFile: 'const response = require(\'cfn-response\');\nconst {\n LocationClient,\n CreateGeofenceCollectionCommand,\n DeleteGeofenceCollectionCommand,\n UpdateGeofenceCollectionCommand,\n} = require(\'@aws-sdk/client-location\');\nexports.handler = async function (event, context) {\n try {\n console.log(\'REQUEST RECEIVED:\' + JSON.stringify(event));\n const pricingPlan = \'RequestBasedUsage\';\n if (event.RequestType === \'Create\') {\n const params = {\n CollectionName: event.ResourceProperties.collectionName,\n PricingPlan: pricingPlan,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new CreateGeofenceCollectionCommand(params));\n console.log(\'create resource response data\' + JSON.stringify(res));\n if (res.CollectionName && res.CollectionArn) {\n await response.send(event, context, response.SUCCESS, res, params.CollectionName);\n } else {\n await response.send(event, context, response.FAILED, res, params.CollectionName);\n }\n }\n if (event.RequestType === \'Update\') {\n const params = {\n CollectionName: event.ResourceProperties.collectionName,\n PricingPlan: pricingPlan,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new UpdateGeofenceCollectionCommand(params));\n console.log(\'update resource response data\' + JSON.stringify(res));\n if (res.CollectionName) {\n await response.send(event, context, response.SUCCESS, res, params.CollectionName);\n } else {\n await response.send(event, context, response.FAILED, res, params.CollectionName);\n }\n }\n if (event.RequestType === \'Delete\') {\n const params = {\n CollectionName: event.ResourceProperties.collectionName,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new DeleteGeofenceCollectionCommand(params));\n console.log(\'delete resource response data\' + JSON.stringify(res));\n await response.send(event, context, response.SUCCESS, res, params.CollectionName);\n }\n } catch (err) {\n console.log(err.stack);\n const res = { Error: err };\n await response.send(event, context, response.FAILED, res, event.ResourceProperties.collectionName);\n throw err;\n }\n};\n',
},
handler: 'index.handler',
role: customGeofenceCollectionLambdaServiceRoleXXXXXX.attrArn,
runtime: 'nodejs22.x',
timeout: 300,
});
customGeofenceCollectionLambdaXXXXX.addDependency(customGeofenceCollectionLambdaServiceRoleDefaultPolicyXXXXX);
customGeofenceCollectionLambdaXXXXX.addDependency(customGeofenceCollectionLambdaServiceRoleXXXXX);
const customGeofenceCollection = new cdk.CfnCustomResource(this, 'CustomGeofenceCollection', {
serviceToken: customGeofenceCollectionLambdaXXXXX.attrArn,
});
customGeofenceCollection.addOverride('Type', 'Custom::LambdaCallout');
customGeofenceCollection.addPropertyOverride('collectionName', [
props.collectionName!,
props.branchName!,
].join('-'));
customGeofenceCollection.addPropertyOverride('region', regionMapping.findInMap(cdk.Stack.of(this).region, 'locationServiceRegion'));
customGeofenceCollection.addPropertyOverride('env', props.branchName!);
customGeofenceCollection.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const storeLocatorDemoAdminGeofenceCollectionPolicy = new iam.CfnPolicy(this, 'storeLocatorDemoAdminGeofenceCollectionPolicy', {
policyDocument: {
Statement: [
{
Action: [
'geo:GetGeofence',
'geo:PutGeofence',
'geo:BatchPutGeofence',
'geo:BatchDeleteGeofence',
'geo:ListGeofences',
],
Effect: 'Allow',
Resource: `arn:aws:geo:${regionMapping.findInMap(cdk.Stack.of(this).region, 'locationServiceRegion')}:${cdk.Stack.of(this).account}:geofence-collection/${customGeofenceCollection.getAtt('CollectionName').toString()}`,
},
],
Version: '2012-10-17',
},
policyName: [
'storeLocatorDemoAdmin',
[
props.collectionName!,
props.branchName!,
].join('-'),
'Policy',
].join(''),
roles: [
props.authuserPoolGroupsstoreLocatorDemoAdminGroupRole!,
],
});
// Outputs
this.name = customGeofenceCollection.getAtt('CollectionName').toString();
new cdk.CfnOutput(this, 'CfnOutputName', {
key: 'Name',
value: this.name!.toString(),
});
this.region = regionMapping.findInMap(cdk.Stack.of(this).region, 'locationServiceRegion');
new cdk.CfnOutput(this, 'CfnOutputRegion', {
key: 'Region',
value: this.region!.toString(),
});
this.arn = `arn:aws:geo:${regionMapping.findInMap(cdk.Stack.of(this).region, 'locationServiceRegion')}:${cdk.Stack.of(this).account}:geofence-collection/${customGeofenceCollection.getAtt('CollectionName').toString()}`;
new cdk.CfnOutput(this, 'CfnOutputArn', {
key: 'Arn',
value: this.arn!.toString(),
});
}
}Geo Map
import { geostoreLocatorDemoMap } from "./storeLocatorDemoMap-construct";
import { Backend } from "@aws-amplify/backend";
const branchName = process.env.AWS_BRANCH ?? "sandbox";
export const defineStoreLocatorDemoMap = (backend: Backend<any>) => {
const storeLocatorDemoMapStack = backend.createStack("storeLocatorDemoMap");
const storeLocatorDemoMap = new geostoreLocatorDemoMap(storeLocatorDemoMapStack, "storeLocatorDemoMap", {
authRoleName: backend.auth.resources.authenticatedUserIamRole.roleName,
unauthRoleName: backend.auth.resources.unauthenticatedUserIamRole.roleName,
authstorelocatordemoXXXXXUserPoolId: backend.auth.resources.userPool.userPoolId,
authuserPoolGroupsstoreLocatorDemoAdminGroupRole: backend.auth.resources.groups["storeLocatorDemoAdmin"].role.roleName,
mapName: "storeLocatorDemoMap",
mapStyle: "VectorEsriStreets",
branchName,
isDefault: "true"
});
return storeLocatorDemoMap;
};
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export interface geostoreLocatorDemoMapProps {
/**
*/
readonly authuserPoolGroupsstoreLocatorDemoAdminGroupRole: string;
/**
*/
readonly authstorelocatordemoXXXXXUserPoolId: string;
/**
*/
readonly authRoleName: string;
/**
*/
readonly unauthRoleName: string;
/**
*/
readonly mapName: string;
/**
*/
readonly mapStyle: string;
/**
*/
readonly isDefault: string;
/**
*/
readonly branchName: string;
}
export class geostoreLocatorDemoMap extends Construct {
public readonly name;
public readonly style;
public readonly region;
public readonly arn;
public constructor(scope: Construct, id: string, props: geostoreLocatorDemoMapProps) {
super(scope, id);
// Mappings
const regionMapping = new cdk.CfnMapping(this, 'RegionMapping', {
mapping: {
'us-east-1': {
'locationServiceRegion': 'us-east-1',
},
'us-east-2': {
'locationServiceRegion': 'us-east-2',
},
'us-west-2': {
'locationServiceRegion': 'us-west-2',
},
'ap-southeast-1': {
'locationServiceRegion': 'ap-southeast-1',
},
'ap-southeast-2': {
'locationServiceRegion': 'ap-southeast-2',
},
'ap-northeast-1': {
'locationServiceRegion': 'ap-northeast-1',
},
'eu-central-1': {
'locationServiceRegion': 'eu-central-1',
},
'eu-north-1': {
'locationServiceRegion': 'eu-north-1',
},
'eu-west-1': {
'locationServiceRegion': 'eu-west-1',
},
'sa-east-1': {
'locationServiceRegion': 'us-east-1',
},
'ca-central-1': {
'locationServiceRegion': 'us-east-1',
},
'us-west-1': {
'locationServiceRegion': 'us-west-2',
},
'cn-north-1': {
'locationServiceRegion': 'us-west-2',
},
'cn-northwest-1': {
'locationServiceRegion': 'us-west-2',
},
'ap-south-1': {
'locationServiceRegion': 'us-west-2',
},
'ap-northeast-3': {
'locationServiceRegion': 'us-west-2',
},
'ap-northeast-2': {
'locationServiceRegion': 'us-west-2',
},
'eu-west-2': {
'locationServiceRegion': 'eu-west-1',
},
'eu-west-3': {
'locationServiceRegion': 'eu-west-1',
},
'me-south-1': {
'locationServiceRegion': 'ap-southeast-1',
},
},
});
// Resources
const customMapLambdaServiceRoleXXXX = new iam.CfnRole(this, 'CustomMapLambdaServiceRoleXXXXX', {
assumeRolePolicyDocument: {
Statement: [
{
Action: 'sts:AssumeRole',
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
},
],
Version: '2012-10-17',
},
managedPolicyArns: [
[
'arn:',
cdk.Stack.of(this).partition,
':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
].join(''),
],
});
const customMapLambdaServiceRoleDefaultPolicyXXXXX = new iam.CfnPolicy(this, 'CustomMapLambdaServiceRoleDefaultPolicyXXXXX', {
policyDocument: {
Statement: [
{
Action: 'geo:CreateMap',
Effect: 'Allow',
Resource: '*',
},
{
Action: [
'geo:UpdateMap',
'geo:DeleteMap',
],
Effect: 'Allow',
Resource: `arn:aws:geo:${regionMapping.findInMap(cdk.Stack.of(this).region, 'locationServiceRegion')}:${cdk.Stack.of(this).account}:map/${[
props.mapName!,
props.branchName!,
].join('-')}`,
},
],
Version: '2012-10-17',
},
policyName: 'CustomMapLambdaServiceRoleDefaultPolicyXXXXX',
roles: [
customMapLambdaServiceRoleXXXXX.ref,
],
});
const customMapLambdaXXXXX = new lambda.CfnFunction(this, 'CustomMapLambdaXXXXX', {
code: {
zipFile: 'const response = require(\'cfn-response\');\nconst { LocationClient, CreateMapCommand, DeleteMapCommand, UpdateMapCommand } = require(\'@aws-sdk/client-location\');\nexports.handler = async function (event, context) {\n try {\n console.log(\'REQUEST RECEIVED:\' + JSON.stringify(event));\n const pricingPlan = \'RequestBasedUsage\';\n if (event.RequestType === \'Create\') {\n let params = {\n MapName: event.ResourceProperties.mapName,\n Configuration: {\n Style: event.ResourceProperties.mapStyle,\n },\n PricingPlan: pricingPlan,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new CreateMapCommand(params));\n console.log(\'create resource response data\' + JSON.stringify(res));\n if (res.MapName && res.MapArn) {\n await response.send(event, context, response.SUCCESS, res, params.MapName);\n } else {\n await response.send(event, context, response.FAILED, res, params.MapName);\n }\n }\n if (event.RequestType === \'Update\') {\n let params = {\n MapName: event.ResourceProperties.mapName,\n PricingPlan: pricingPlan,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new UpdateMapCommand(params));\n console.log(\'update resource response data\' + JSON.stringify(res));\n if (res.MapName && res.MapArn) {\n await response.send(event, context, response.SUCCESS, res, params.MapName);\n } else {\n await response.send(event, context, response.FAILED, res, params.MapName);\n }\n }\n if (event.RequestType === \'Delete\') {\n let params = {\n MapName: event.ResourceProperties.mapName,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new DeleteMapCommand(params));\n console.log(\'delete resource response data\' + JSON.stringify(res));\n await response.send(event, context, response.SUCCESS, res, params.MapName);\n }\n } catch (err) {\n console.log(err.stack);\n const res = { Error: err };\n await response.send(event, context, response.FAILED, res, event.ResourceProperties.mapName);\n throw err;\n }\n};\n',
},
handler: 'index.handler',
role: customMapLambdaServiceRoleXXXXX.attrArn,
runtime: 'nodejs22.x',
timeout: 300,
});
customMapLambdaXXXXX.addDependency(customMapLambdaServiceRoleDefaultPolicyXXXXX);
customMapLambdaXXXXX.addDependency(customMapLambdaServiceRoleXXXXX);
const customMap = new cdk.CfnCustomResource(this, 'CustomMap', {
serviceToken: customMapLambdaXXXXX,
});
customMap.addOverride('Type', 'Custom::LambdaCallout');
customMap.addPropertyOverride('mapName', [
props.mapName!,
props.branchName!,
].join('-'));
customMap.addPropertyOverride('mapStyle', props.mapStyle!);
customMap.addPropertyOverride('region', regionMapping.findInMap(cdk.Stack.of(this).region, 'locationServiceRegion'));
customMap.addPropertyOverride('env', props.branchName!);
customMap.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const mapPolicy = new iam.CfnPolicy(this, 'MapPolicy', {
policyDocument: {
Statement: [
{
Action: [
'geo:GetMapStyleDescriptor',
'geo:GetMapGlyphs',
'geo:GetMapSprites',
'geo:GetMapTile',
],
Effect: 'Allow',
Resource: customMap.getAtt('MapArn').toString(),
},
],
Version: '2012-10-17',
},
policyName: [
[
props.mapName!,
props.branchName!,
].join('-'),
'Policy',
].join(''),
roles: [
props.authRoleName!,
props.unauthRoleName!,
props.authuserPoolGroupsstoreLocatorDemoAdminGroupRole!,
],
});
// Outputs
this.name = customMap.getAtt('MapName').toString();
new cdk.CfnOutput(this, 'CfnOutputName', {
key: 'Name',
value: this.name!.toString(),
});
this.style = props.mapStyle!;
new cdk.CfnOutput(this, 'CfnOutputStyle', {
key: 'Style',
value: this.style!.toString(),
});
this.region = regionMapping.findInMap(cdk.Stack.of(this).region, 'locationServiceRegion');
new cdk.CfnOutput(this, 'CfnOutputRegion', {
key: 'Region',
value: this.region!.toString(),
});
this.arn = customMap.getAtt('MapArn').toString();
new cdk.CfnOutput(this, 'CfnOutputArn', {
key: 'Arn',
value: this.arn!.toString(),
});
}
}Geo Location Search
import { geostoreLocatorDemoSearch } from "./storeLocatorDemoSearch-construct";
import { Backend } from "@aws-amplify/backend";
const branchName = process.env.AWS_BRANCH ?? "sandbox";
export const defineStoreLocatorDemoSearch = (backend: Backend<any>) => {
const storeLocatorDemoSearchStack = backend.createStack("storeLocatorDemoSearch");
const storeLocatorDemoSearch = new geostoreLocatorDemoSearch(storeLocatorDemoSearchStack, "storeLocatorDemoSearch", {
authRoleName: backend.auth.resources.authenticatedUserIamRole.roleName,
unauthRoleName: backend.auth.resources.unauthenticatedUserIamRole.roleName,
authstorelocatordemoXXXXXUserPoolId: backend.auth.resources.userPool.userPoolId,
authuserPoolGroupsstoreLocatorDemoAdminGroupRole: backend.auth.resources.groups["storeLocatorDemoAdmin"].role.roleName,
indexName: "storeLocatorDemoSearch",
dataProvider: "Here",
dataSourceIntendedUse: "SingleUse",
branchName,
isDefault: "true"
});
return storeLocatorDemoSearch;
};
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export interface geostoreLocatorDemoSearchProps {
/**
*/
readonly authuserPoolGroupsstoreLocatorDemoAdminGroupRole: string;
/**
*/
readonly authstorelocatordemoXXXXXUserPoolId: string;
/**
*/
readonly authRoleName: string;
/**
*/
readonly unauthRoleName: string;
/**
*/
readonly indexName: string;
/**
*/
readonly dataProvider: string;
/**
*/
readonly dataSourceIntendedUse: string;
/**
*/
readonly isDefault: string;
/**
*/
readonly branchName: string;
}
export class geostoreLocatorDemoSearch extends Construct {
public readonly name;
public readonly region;
public readonly arn;
public constructor(scope: Construct, id: string, props: geostoreLocatorDemoSearchProps) {
super(scope, id);
// Mappings
const regionMapping = new cdk.CfnMapping(this, 'RegionMapping', {
mapping: {
'us-east-1': {
'locationServiceRegion': 'us-east-1',
},
'us-east-2': {
'locationServiceRegion': 'us-east-2',
},
'us-west-2': {
'locationServiceRegion': 'us-west-2',
},
'ap-southeast-1': {
'locationServiceRegion': 'ap-southeast-1',
},
'ap-southeast-2': {
'locationServiceRegion': 'ap-southeast-2',
},
'ap-northeast-1': {
'locationServiceRegion': 'ap-northeast-1',
},
'eu-central-1': {
'locationServiceRegion': 'eu-central-1',
},
'eu-north-1': {
'locationServiceRegion': 'eu-north-1',
},
'eu-west-1': {
'locationServiceRegion': 'eu-west-1',
},
'sa-east-1': {
'locationServiceRegion': 'us-east-1',
},
'ca-central-1': {
'locationServiceRegion': 'us-east-1',
},
'us-west-1': {
'locationServiceRegion': 'us-west-2',
},
'cn-north-1': {
'locationServiceRegion': 'us-west-2',
},
'cn-northwest-1': {
'locationServiceRegion': 'us-west-2',
},
'ap-south-1': {
'locationServiceRegion': 'us-west-2',
},
'ap-northeast-3': {
'locationServiceRegion': 'us-west-2',
},
'ap-northeast-2': {
'locationServiceRegion': 'us-west-2',
},
'eu-west-2': {
'locationServiceRegion': 'eu-west-1',
},
'eu-west-3': {
'locationServiceRegion': 'eu-west-1',
},
'me-south-1': {
'locationServiceRegion': 'ap-southeast-1',
},
},
});
// Resources
const customPlaceIndexLambdaServiceRoleXXXXX= new iam.CfnRole(this, 'CustomPlaceIndexLambdaServiceRoleXXXXX', {
assumeRolePolicyDocument: {
Statement: [
{
Action: 'sts:AssumeRole',
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
},
],
Version: '2012-10-17',
},
managedPolicyArns: [
[
'arn:',
cdk.Stack.of(this).partition,
':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
].join(''),
],
});
const customPlaceIndexLambdaServiceRoleDefaultPolicyXXXXX= new iam.CfnPolicy(this, 'CustomPlaceIndexLambdaServiceRoleDefaultPolicyXXXXX', {
policyDocument: {
Statement: [
{
Action: 'geo:CreatePlaceIndex',
Effect: 'Allow',
Resource: '*',
},
{
Action: [
'geo:UpdatePlaceIndex',
'geo:DeletePlaceIndex',
],
Effect: 'Allow',
Resource: `arn:aws:geo:${regionMapping.findInMap(cdk.Stack.of(this).region, 'locationServiceRegion')}:${cdk.Stack.of(this).account}:place-index/${[
props.indexName!,
props.branchName!,
].join('-')}`,
},
],
Version: '2012-10-17',
},
policyName: 'CustomPlaceIndexLambdaServiceRoleDefaultPolicyXXXXX',
roles: [
customPlaceIndexLambdaServiceRoleXXXXX.ref,
],
});
const customPlaceIndexLambdaXXXXX = new lambda.CfnFunction(this, 'CustomPlaceIndexLambdaXXXXX', {
code: {
zipFile: 'const response = require(\'cfn-response\');\nconst { LocationClient, CreatePlaceIndexCommand, DeletePlaceIndexCommand, UpdatePlaceIndexCommand } = require(\'@aws-sdk/client-location\');\nexports.handler = async function (event, context) {\n try {\n console.log(\'REQUEST RECEIVED:\' + JSON.stringify(event));\n const pricingPlan = \'RequestBasedUsage\';\n if (event.RequestType === \'Create\') {\n const params = {\n IndexName: event.ResourceProperties.indexName,\n DataSource: event.ResourceProperties.dataSource,\n DataSourceConfiguration: {\n IntendedUse: event.ResourceProperties.dataSourceIntendedUse,\n },\n PricingPlan: pricingPlan,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new CreatePlaceIndexCommand(params));\n console.log(\'create resource response data\' + JSON.stringify(res));\n if (res.IndexName && res.IndexArn) {\n event.PhysicalResourceId = res.IndexName;\n await response.send(event, context, response.SUCCESS, res, params.IndexName);\n } else {\n await response.send(event, context, response.FAILED, res, params.IndexName);\n }\n }\n if (event.RequestType === \'Update\') {\n const params = {\n IndexName: event.ResourceProperties.indexName,\n DataSourceConfiguration: {\n IntendedUse: event.ResourceProperties.dataSourceIntendedUse,\n },\n PricingPlan: pricingPlan,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new UpdatePlaceIndexCommand(params));\n console.log(\'update resource response data\' + JSON.stringify(res));\n if (res.IndexName && res.IndexArn) {\n event.PhysicalResourceId = res.IndexName;\n await response.send(event, context, response.SUCCESS, res, params.IndexName);\n } else {\n await response.send(event, context, response.FAILED, res, params.IndexName);\n }\n }\n if (event.RequestType === \'Delete\') {\n const params = {\n IndexName: event.ResourceProperties.indexName,\n };\n const locationClient = new LocationClient({ region: event.ResourceProperties.region });\n const res = await locationClient.send(new DeletePlaceIndexCommand(params));\n event.PhysicalResourceId = event.ResourceProperties.indexName;\n console.log(\'delete resource response data\' + JSON.stringify(res));\n await response.send(event, context, response.SUCCESS, res, params.IndexName);\n }\n } catch (err) {\n console.log(err.stack);\n const res = { Error: err };\n await response.send(event, context, response.FAILED, res, event.ResourceProperties.indexName);\n throw err;\n }\n};\n',
},
handler: 'index.handler',
role: customPlaceIndexLambdaServiceRoleXXXXX.attrArn,
runtime: 'nodejs22.x',
timeout: 300,
});
customPlaceIndexLambdaXXXXX.addDependency(customPlaceIndexLambdaServiceRoleDefaultPolicyXXXXX);
customPlaceIndexLambdaXXXXX.addDependency(customPlaceIndexLambdaServiceRoleXXXXX);
const customPlaceIndex = new cdk.CfnCustomResource(this, 'CustomPlaceIndex', {
serviceToken: customPlaceIndexLambdaXXXXX.attrArn,
});
customPlaceIndex.addOverride('Type', 'Custom::LambdaCallout');
customPlaceIndex.addPropertyOverride('indexName', [
props.indexName!,
props.branchName!,
].join('-'));
customPlaceIndex.addPropertyOverride('dataSource', props.dataProvider!);
customPlaceIndex.addPropertyOverride('dataSourceIntendedUse', props.dataSourceIntendedUse!);
customPlaceIndex.addPropertyOverride('region', regionMapping.findInMap(cdk.Stack.of(this).region, 'locationServiceRegion'));
customPlaceIndex.addPropertyOverride('env', props.branchName!);
customPlaceIndex.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const placeIndexPolicy = new iam.CfnPolicy(this, 'PlaceIndexPolicy', {
policyDocument: {
Statement: [
{
Action: [
'geo:SearchPlaceIndexForPosition',
'geo:SearchPlaceIndexForText',
'geo:SearchPlaceIndexForSuggestions',
'geo:GetPlace',
],
Effect: 'Allow',
Resource: customPlaceIndex.getAtt('IndexArn').toString(),
},
],
Version: '2012-10-17',
},
policyName: [
[
props.indexName!,
props.branchName!,
].join('-'),
'Policy',
].join(''),
roles: [
props.authRoleName!,
props.unauthRoleName!,
props.authuserPoolGroupsstoreLocatorDemoAdminGroupRole!,
],
});
// Outputs
this.name = customPlaceIndex.getAtt('IndexName').toString();
new cdk.CfnOutput(this, 'CfnOutputName', {
key: 'Name',
value: this.name!.toString(),
});
this.region = regionMapping.findInMap(cdk.Stack.of(this).region, 'locationServiceRegion');
new cdk.CfnOutput(this, 'CfnOutputRegion', {
key: 'Region',
value: this.region!.toString(),
});
this.arn = customPlaceIndex.getAtt('IndexArn').toString();
new cdk.CfnOutput(this, 'CfnOutputArn', {
key: 'Arn',
value: this.arn!.toString(),
});
}
}Top Level
|
|
Bunch of clarifying questions incoming. Haven't looked at the code yet. Q1) There are three resources in geo Gen1 -> fences, map, and location search. And the only resource we can have multiple of is fence & map? |
|
Q5) What is the plan for this? If this isn't being handled, we need to make a note of it in the migration guide with a reason why this is the case. Anything which gen1 supports but we don't support needs to be added to the guide - https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/google-migration/ |
|
Q11) Are we handling uploading our own GeoJSON file? https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/configure-geofencing/ Can you update the pr description with a list of everything this pr currenlty does and doesn't handle |
|
Q1) There are three resources in geo Gen1 -> fences, map, and location search. And the only resource we can have multiple of is fence & map?
Q2) In Gen1, each resource must be given a unique name by the user via the CLI during amplify add geo. We use that same user-provided name for stack naming and directory structure.
Q3) Can we restrict auth access of map + location search to userPoolGroups as well? If so, are we handling this possibility? This page says we can - https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/configure-geofencing/ . We can also do both.
Q4) Conversely, if we have access restricted by fences to one userPoolGroup, can we go and update it to restrict it to both?
Q5) What is the plan for this? If this isn't being handled, we need to make a note of it in the migration guide with a reason why this is the case. Anything which gen1 supports but we don't support needs to be added to the guide - https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/google-migration/
Q6) Are we handling map styles? https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/configure-maps/
Q7) Are we handling all the data providers? https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/configure-location-search/
Q8) Are we handling more than one location search index? https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/configure-location-search/
Q9) Are we handling all the location search storage locations? https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/configure-location-search/
Q10) If we can have multiple maps and geofences, we should be able to have multiple search locators too right? And the way geofences work is you can define multiple collections, with each collection being a set of geofences? If that follows, are we scoping access policies on a userPoolGroup on a collections level? https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/configure-geofencing/
Q11) Are we handling uploading our own GeoJSON file? https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/configure-geofencing/
From a codegen perspective, this PR fully handles all Gen1 geo features. Refactor for geo will be a separate follow up PR added a comment for specifying that. Please let me know if you need any more info. or references that might help you with the review. |
This PR adds geo category (
Map,PlaceIndex,GeofenceCollection) codegen support to the gen2-migration pipeline. When a Gen1 app has geo resources, thegeneratestep now produces per-resource CDK L1 constructs viacdk-from-cfn, wraps each in its own independent CloudFormation stack, and wires them intobackend.tsthrough a top-leveldefineGeo(backend)aggregator.Description of changes
Geo Definition Fetcher (
app_geo_definition_fetcher.ts)AppGeoDefinitionFetcherReads thegeokey from the deployedamplify-meta.jsonin S3 and returns a record ofGeoResourceDefinitionentries keyed by resource name. Follows the same pattern asAppAnalyticsDefinitionFetcherbut uses explicitif/throwinstead ofassert.Per-Resource Generator (
geo-resource-renderer.ts)renderGeoResourcegenerates aresource.tsfor each geo resource that creates its own independent stack viabackend.createStack(), adds a dependency on the auth stack, and instantiates the construct with dynamic props (branchName,authRoleName,unauthRoleName,userPoolId, group roles frombackend.auth.resources) and static props (mapName,mapStyle,indexName, etc. from deployed parameters). Handles all three service types with a switch onserviceName.Top-Level Aggregator (
geo-renderer.ts)renderGeogenerates the top-levelamplify/geo/resource.tsthat imports all per-resourcedefine*functions, calls each one assigning the return value to a variable using the resource name, then callsbackend.addOutput()with geo configuration using construct output properties (.name,.region,.style) for maps, search indices, and geofence collections.Backend Synthesizer (
synthesizer.ts)geotoBackendRenderParameterswith animportFrompath. Whengeois present, therender()method addsimport { defineGeo }andconst geo = defineGeo(backend)afterdefineBackend(), following the analytics pattern.Migration Pipeline (
migration-pipeline.ts)geotoGen2RenderingOptions. When geo resources are present,createGen2Renderer()creates theamplify/geo/directory, iterates over each resource creating per-resource directories and renderers (callinggenerateGeoL1Code()thenrenderGeoResource()), collects all render params, then creates a renderer for the top-levelrenderGeo()aggregator.Command Handlers (
command-handlers.ts)geofrom theunsupportedCategoriesmap. AddedAppGeoDefinitionFetcherinstantiation inprepare()and wired it throughCodegenCommandParameterstogenerateGen2Code(), which fetches geo definitions and passes them toGen2RenderingOptions.cdk-from-cfn.tsType Interfaces : Added
GeoResourceDefinitionto model geo entries fromamplify-meta.json(service type, S3 template URL, logical ID). AddedGeoCodegenResultas a discriminated union ofMapCodegenResult,PlaceIndexCodegenResult, andGeofenceCollectionCodegenResult, each carrying typed service-specific fields (mapName/mapStyle,indexName/dataProvider/dataSourceIntendedUse,collectionName) plus common auth parameter metadata (userPoolIdParamName,groupRoles).postTransmuteFn::FindInMapToken Crash Fix: Thecdk-from-cfnlibrary translates CFNFn::FindInMapinto plain JS dictionary lookups that crash at CDK synth time becausethis.regionis a CDK Token, not a concrete string.postTransmutereplacesRecord<string, Record<string, string>>declarations withcdk.CfnMappingconstructors and bracket lookups with.findInMap()calls, producing properFn::FindInMap` intrinsics in synthesized CloudFormation.preTransmuteFn::JoinGroupRole Fix: Gen1 geo templates construct group role names viaFn::JoinofUserPoolIdand aGroupRolesuffix.cdk-from-cfntranslates this into.join('-')which produces a plain string, breaking CloudFormation's cross-stack dependency detection. The fix replaces theseFn::Joinpatterns with a directRefto the correspondingGroupRoleparameter, preserving CDK token chains for correct deployment ordering.Geo L1 Code Generation:
generateGeoL1CodeFetches the CFN template from S3, fetches deployed stack parameters, appliespreTransmute(envrename, condition resolution,GroupRolefix), callscdk_from_cfn.transmute(), appliespostTransmute(FindInMapfix), writes the construct file, extracts the class name, and categorizes deployed parameters into the typedGeoCodegenResult.Generated Output Structure
cdk-from-cfnreferenceThe
cdk-from-cfnlibrary converts CloudFormation templates into CDK code. It exposes transmute(template, language, className, classType?) function. It takes a CFN template as a JSON string, a target language ('typescript' in our case), a class name (we pass the nested stack's logical ID), and an class type ('construct'). Returns a string of generated TypeScript containing a CDK construct class with L1 resources matching the original template. All CFN parameters become construct props, all resources become L1 constructs.Our
CdkFromCfnwrapper class inen2-migration/generate/unsupported/cdk-from-cfn.tsadds pre- and post-processing aroundtransmute()because the library's output needs fixes for our use cases:preTransmute— modifies the CFN template before passing it to transmute(): renames env → branchName, replaces Fn::Join patterns for group roles with direct Refs (to preserve CDK tokens), and resolves CFN conditions using deployed stack parameters (sincecdk-from-cfngenerates broken TypeScript for conditions).postTransmute— fixes the generated TypeScript aftertransmute()returns: replaces plain dictionary lookups for Fn::FindInMap with cdk.CfnMapping + findInMap() calls, because the library generates mapping[this.region]['key'] which fails at synth time since this.region is a CDK token, not a concrete string.Geo uses all three preTransmute fixes and the postTransmute fix, unlike kinesis which used only the preTransmute ones.
Dependencies -
cdk-from-cfnversion needs to be bumped to includeCustom::LambdaCalloutresource type support. Until that version is published, thetransmute()call will fail on geo CFN templates.This PR covers the
npx amplify gen2-migration generatecodegen step for geo category migration. Refactor for geo (migrating existing Gen1 resources into Gen2 stacks) will be a separate follow-up PR.Issue #, if available
Description of how you validated changes
Checklist
yarn testpassesBy submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.