Skip to content

feat(gen2-migration): add geo category code generation for gen2-migration #14596

Open
sai-ray wants to merge 18 commits intogen2-migrationfrom
sai/geo-codegen-for-gen2-migration
Open

feat(gen2-migration): add geo category code generation for gen2-migration #14596
sai-ray wants to merge 18 commits intogen2-migrationfrom
sai/geo-codegen-for-gen2-migration

Conversation

@sai-ray
Copy link
Contributor

@sai-ray sai-ray commented Feb 16, 2026

This PR adds geo category (Map, PlaceIndex, GeofenceCollection) codegen support to the gen2-migration pipeline. When a Gen1 app has geo resources, the generate step now produces per-resource CDK L1 constructs via cdk-from-cfn, wraps each in its own independent CloudFormation stack, and wires them into backend.ts through a top-level defineGeo(backend) aggregator.

Description of changes

  1. Geo Definition Fetcher (app_geo_definition_fetcher.ts)

  • AppGeoDefinitionFetcher Reads the geo key from the deployed amplify-meta.json in S3 and returns a record of GeoResourceDefinition entries keyed by resource name. Follows the same pattern as AppAnalyticsDefinitionFetcher but uses explicit if/throw instead of assert.
  1. Per-Resource Generator (geo-resource-renderer.ts)

  • renderGeoResource generates a resource.ts for each geo resource that creates its own independent stack via backend.createStack(), adds a dependency on the auth stack, and instantiates the construct with dynamic props (branchName, authRoleName, unauthRoleName, userPoolId, group roles from backend.auth.resources) and static props (mapName, mapStyle, indexName, etc. from deployed parameters). Handles all three service types with a switch on serviceName.
  1. Top-Level Aggregator (geo-renderer.ts)

  • renderGeo generates the top-level amplify/geo/resource.ts that imports all per-resource define* functions, calls each one assigning the return value to a variable using the resource name, then calls backend.addOutput() with geo configuration using construct output properties (.name, .region, .style) for maps, search indices, and geofence collections.
  1. Backend Synthesizer (synthesizer.ts)

  • Added geo to BackendRenderParameters with an importFrom path. When geo is present, the render() method adds import { defineGeo } and const geo = defineGeo(backend) after defineBackend(), following the analytics pattern.
  1. Migration Pipeline (migration-pipeline.ts)

  • Added geo to Gen2RenderingOptions. When geo resources are present, createGen2Renderer() creates the amplify/geo/ directory, iterates over each resource creating per-resource directories and renderers (calling generateGeoL1Code() then renderGeoResource()), collects all render params, then creates a renderer for the top-level renderGeo() aggregator.
  1. Command Handlers (command-handlers.ts)

  • Removed geo from the unsupportedCategories map. Added AppGeoDefinitionFetcher instantiation in prepare() and wired it through CodegenCommandParameters to generateGen2Code(), which fetches geo definitions and passes them to Gen2RenderingOptions.
  1. cdk-from-cfn.ts

  • Type Interfaces : Added GeoResourceDefinition to model geo entries from amplify-meta.json (service type, S3 template URL, logical ID). Added GeoCodegenResult as a discriminated union of MapCodegenResult, PlaceIndexCodegenResult, and GeofenceCollectionCodegenResult, each carrying typed service-specific fields (mapName/mapStyle, indexName/dataProvider/dataSourceIntendedUse, collectionName) plus common auth parameter metadata (userPoolIdParamName, groupRoles).

  • postTransmute Fn::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.

  • preTransmute Fn::Join GroupRole Fix: Gen1 geo templates construct group role names via Fn::Join of UserPoolId and a GroupRole suffix. cdk-from-cfn translates this into .join('-') which produces a plain string, breaking CloudFormation's cross-stack dependency detection. The fix replaces these Fn::Join patterns with a direct Ref to the corresponding GroupRole parameter, preserving CDK token chains for correct deployment ordering.

  • Geo L1 Code Generation: generateGeoL1Code Fetches the CFN template from S3, fetches deployed stack parameters, applies preTransmute (env rename, condition resolution, GroupRole fix), calls cdk_from_cfn.transmute(), applies postTransmute (FindInMap fix), writes the construct file, extracts the class name, and categorizes deployed parameters into the typed GeoCodegenResult.

Generated Output Structure

amplify/
  geo/
    resource.ts                        # defineGeo() aggregator + backend.addOutput()
    MyMap/
      MyMap-construct.ts               # L1 CDK code (cdk-from-cfn + postTransmute)
      resource.ts                      # defineMyMap() (independent stack)
    MySearch/
      MySearch-construct.ts
      resource.ts                      # defineMySearch() (independent stack)
    MyGeofence/
      MyGeofence-construct.ts
      resource.ts                      # defineMyGeofence() (independent stack)
  backend.ts                           # import { defineGeo } + defineGeo(backend)

cdk-from-cfn reference

The cdk-from-cfn library 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 CdkFromCfn wrapper class in en2-migration/generate/unsupported/cdk-from-cfn.ts adds pre- and post-processing around transmute() 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 (since cdk-from-cfn generates broken TypeScript for conditions).

  • postTransmute — fixes the generated TypeScript after transmute() 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 -

  • To fully support geo codegen migration, the cdk-from-cfn version needs to be bumped to include Custom::LambdaCallout resource type support. Until that version is published, the transmute() call will fail on geo CFN templates.

This PR covers the npx amplify gen2-migration generate codegen 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

  • PR description included
  • yarn test passes
  • Tests are changed or added
  • Relevant documentation is changed or added (and PR referenced)
  • New AWS SDK calls or CloudFormation actions have been added to relevant test and service IAM policies
  • Pull request labels are added

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@sai-ray sai-ray changed the title feat(gen2-migration): geo category code generation for gen2-migration [WIP] feat(gen2-migration): add geo category code generation for gen2-migration Feb 20, 2026
@sai-ray sai-ray marked this pull request as ready for review February 20, 2026 18:00
@sai-ray sai-ray requested a review from a team as a code owner February 20, 2026 18:00
@dgandhi62
Copy link
Contributor

Question - is there an app that tests this? Can you add the input amplify-meta.json and the output files in markdown to the pr description?

@dgandhi62
Copy link
Contributor

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

@sai-ray
Copy link
Contributor Author

sai-ray commented Mar 2, 2026

Question - is there an app that tests this? Can you add the input amplify-meta.json and the output files in markdown to the pr description?

amplify-meta.json:

  "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 :

cli-inputs.json:

{
  "groupPermissions": {
    "storeLocatorDemoAdmin": [
      "Read geofence",
      "Create/Update geofence",
      "Delete geofence",
      "List geofences"
    ]
  }
}

parameter.json

{
  "collectionName": "storeLocatorDemoGeofence",
  "isDefault": true
}

geofence-collection-cloudformation-template.json

{
  "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"
    },
    "collectionName": {
      "Type": "String"
    },
    "env": {
      "Type": "String"
    },
    "isDefault": {
      "Type": "String"
    }
  },
  "Resources": {
    "CustomGeofenceCollectionLambdaServiceRoleXXXXX": {
      "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"
              ]
            ]
          }
        ]
      }
    },
    "CustomGeofenceCollectionLambdaServiceRoleDefaultPolicyXXXXX": {
      "Type": "AWS::IAM::Policy",
      "Properties": {
        "PolicyDocument": {
          "Statement": [
            {
              "Action": "geo:CreateGeofenceCollection",
              "Effect": "Allow",
              "Resource": "*"
            },
            {
              "Action": [
                "geo:UpdateGeofenceCollection",
                "geo:DeleteGeofenceCollection"
              ],
              "Effect": "Allow",
              "Resource": {
                "Fn::Sub": [
                  "arn:aws:geo:${region}:${account}:geofence-collection/${collectionName}",
                  {
                    "region": {
                      "Fn::FindInMap": [
                        "RegionMapping",
                        {
                          "Ref": "AWS::Region"
                        },
                        "locationServiceRegion"
                      ]
                    },
                    "account": {
                      "Ref": "AWS::AccountId"
                    },
                    "collectionName": {
                      "Fn::Join": [
                        "-",
                        [
                          {
                            "Ref": "collectionName"
                          },
                          {
                            "Ref": "env"
                          }
                        ]
                      ]
                    }
                  }
                ]
              }
            }
          ],
          "Version": "2012-10-17"
        },
        "PolicyName": "CustomGeofenceCollectionLambdaServiceRoleDefaultPolicy0A18B369",
        "Roles": [
          {
            "Ref": "CustomGeofenceCollectionLambdaServiceRoleXXXXXX"
          }
        ]
      }
    },
    "CustomGeofenceCollectionLambdaXXXXXXX": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "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": {
          "Fn::GetAtt": [
            "CustomGeofenceCollectionLambdaServiceRoleXXXXX",
            "Arn"
          ]
        },
        "Runtime": "nodejs22.x",
        "Timeout": 300
      },
      "DependsOn": [
        "CustomGeofenceCollectionLambdaServiceRoleDefaultPolicyXXXXX",
        "CustomGeofenceCollectionLambdaServiceRoleXXXXX"
      ]
    },
    "CustomGeofenceCollection": {
      "Type": "Custom::LambdaCallout",
      "Properties": {
        "ServiceToken": {
          "Fn::GetAtt": [
            "CustomGeofenceCollectionLambdaXXXXX",
            "Arn"
          ]
        },
        "collectionName": {
          "Fn::Join": [
            "-",
            [
              {
                "Ref": "collectionName"
              },
              {
                "Ref": "env"
              }
            ]
          ]
        },
        "region": {
          "Fn::FindInMap": [
            "RegionMapping",
            {
              "Ref": "AWS::Region"
            },
            "locationServiceRegion"
          ]
        },
        "env": {
          "Ref": "env"
        }
      },
      "UpdateReplacePolicy": "Delete",
      "DeletionPolicy": "Delete"
    },
    "storeLocatorDemoAdminGeofenceCollectionPolicy": {
      "Type": "AWS::IAM::Policy",
      "Properties": {
        "PolicyDocument": {
          "Statement": [
            {
              "Action": [
                "geo:GetGeofence",
                "geo:PutGeofence",
                "geo:BatchPutGeofence",
                "geo:BatchDeleteGeofence",
                "geo:ListGeofences"
              ],
              "Effect": "Allow",
              "Resource": {
                "Fn::Sub": [
                  "arn:aws:geo:${region}:${account}:geofence-collection/${collectionName}",
                  {
                    "region": {
                      "Fn::FindInMap": [
                        "RegionMapping",
                        {
                          "Ref": "AWS::Region"
                        },
                        "locationServiceRegion"
                      ]
                    },
                    "account": {
                      "Ref": "AWS::AccountId"
                    },
                    "collectionName": {
                      "Fn::GetAtt": [
                        "CustomGeofenceCollection",
                        "CollectionName"
                      ]
                    }
                  }
                ]
              }
            }
          ],
          "Version": "2012-10-17"
        },
        "PolicyName": {
          "Fn::Join": [
            "",
            [
              "storeLocatorDemoAdmin",
              {
                "Fn::Join": [
                  "-",
                  [
                    {
                      "Ref": "collectionName"
                    },
                    {
                      "Ref": "env"
                    }
                  ]
                ]
              },
              "Policy"
            ]
          ]
        },
        "Roles": [
          {
            "Fn::Join": [
              "-",
              [
                {
                  "Ref": "authstorelocatordemoXXXXXUserPoolId"
                },
                "storeLocatorDemoAdminGroupRole"
              ]
            ]
          }
        ]
      }
    }
  },
  "Outputs": {
    "Name": {
      "Value": {
        "Fn::GetAtt": [
          "CustomGeofenceCollection",
          "CollectionName"
        ]
      }
    },
    "Region": {
      "Value": {
        "Fn::FindInMap": [
          "RegionMapping",
          {
            "Ref": "AWS::Region"
          },
          "locationServiceRegion"
        ]
      }
    },
    "Arn": {
      "Value": {
        "Fn::Sub": [
          "arn:aws:geo:${region}:${account}:geofence-collection/${collectionName}",
          {
            "region": {
              "Fn::FindInMap": [
                "RegionMapping",
                {
                  "Ref": "AWS::Region"
                },
                "locationServiceRegion"
              ]
            },
            "account": {
              "Ref": "AWS::AccountId"
            },
            "collectionName": {
              "Fn::GetAtt": [
                "CustomGeofenceCollection",
                "CollectionName"
              ]
            }
          }
        ]
      }
    }
  },
  "Description": "{\"createdOn\":\"Mac\",\"createdBy\":\"Amplify\",\"createdWith\":\"14.2.5\",\"stackType\":\"geo-GeofenceCollection\",\"metadata\":{\"whyContinueWithGen1\":\"\"}}"
}

Geo Maps :

cli-inputs.json

{
  "groupPermissions": [
    "storeLocatorDemoAdmin"
  ]
}

parameters.json

  "authRoleName": {
    "Ref": "AuthRoleName"
  },
  "unauthRoleName": {
    "Ref": "UnauthRoleName"
  },
  "mapName": "storeLocatorDemoMap",
  "mapStyle": "VectorEsriStreets",
  "isDefault": true
}

geo-map-cloudformation-template.json

{
  "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

cli-inputs.json

{
  "groupPermissions": [
    "storeLocatorDemoAdmin"
  ]
}

paramters.json

{
  "authRoleName": {
    "Ref": "AuthRoleName"
  },
  "unauthRoleName": {
    "Ref": "UnauthRoleName"
  },
  "indexName": "storeLocatorDemoSearch",
  "dataProvider": "Here",
  "dataSourceIntendedUse": "SingleUse",
  "isDefault": true
}

geo-location-search-cloudformation-template.json

{
  "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"
    },
    "indexName": {
      "Type": "String"
    },
    "dataProvider": {
      "Type": "String"
    },
    "dataSourceIntendedUse": {
      "Type": "String"
    },
    "env": {
      "Type": "String"
    },
    "isDefault": {
      "Type": "String"
    }
  },
  "Resources": {
    "CustomPlaceIndexLambdaServiceRoleXXXXX": {
      "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"
              ]
            ]
          }
        ]
      }
    },
    "CustomPlaceIndexLambdaServiceRoleDefaultPolicyXXXXX": {
      "Type": "AWS::IAM::Policy",
      "Properties": {
        "PolicyDocument": {
          "Statement": [
            {
              "Action": "geo:CreatePlaceIndex",
              "Effect": "Allow",
              "Resource": "*"
            },
            {
              "Action": [
                "geo:UpdatePlaceIndex",
                "geo:DeletePlaceIndex"
              ],
              "Effect": "Allow",
              "Resource": {
                "Fn::Sub": [
                  "arn:aws:geo:${region}:${account}:place-index/${indexName}",
                  {
                    "region": {
                      "Fn::FindInMap": [
                        "RegionMapping",
                        {
                          "Ref": "AWS::Region"
                        },
                        "locationServiceRegion"
                      ]
                    },
                    "account": {
                      "Ref": "AWS::AccountId"
                    },
                    "indexName": {
                      "Fn::Join": [
                        "-",
                        [
                          {
                            "Ref": "indexName"
                          },
                          {
                            "Ref": "env"
                          }
                        ]
                      ]
                    }
                  }
                ]
              }
            }
          ],
          "Version": "2012-10-17"
        },
        "PolicyName": "CustomPlaceIndexLambdaServiceRoleDefaultPolicyXXXXX",
        "Roles": [
          {
            "Ref": "CustomPlaceIndexLambdaServiceRoleXXXXXX"
          }
        ]
      }
    },
    "CustomPlaceIndexLambda7XXXXX: {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "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": {
          "Fn::GetAtt": [
            "CustomPlaceIndexLambdaServiceRoleXXXXX",
            "Arn"
          ]
        },
        "Runtime": "nodejs22.x",
        "Timeout": 300
      },
      "DependsOn": [
        "CustomPlaceIndexLambdaServiceRoleDefaultPolicyXXXXX",
        "CustomPlaceIndexLambdaServiceRoleXXXXX"
      ]
    },
    "CustomPlaceIndex": {
      "Type": "Custom::LambdaCallout",
      "Properties": {
        "ServiceToken": {
          "Fn::GetAtt": [
            "CustomPlaceIndexLambdaXXXXX",
            "Arn"
          ]
        },
        "indexName": {
          "Fn::Join": [
            "-",
            [
              {
                "Ref": "indexName"
              },
              {
                "Ref": "env"
              }
            ]
          ]
        },
        "dataSource": {
          "Ref": "dataProvider"
        },
        "dataSourceIntendedUse": {
          "Ref": "dataSourceIntendedUse"
        },
        "region": {
          "Fn::FindInMap": [
            "RegionMapping",
            {
              "Ref": "AWS::Region"
            },
            "locationServiceRegion"
          ]
        },
        "env": {
          "Ref": "env"
        }
      },
      "UpdateReplacePolicy": "Delete",
      "DeletionPolicy": "Delete"
    },
    "PlaceIndexPolicy": {
      "Type": "AWS::IAM::Policy",
      "Properties": {
        "PolicyDocument": {
          "Statement": [
            {
              "Action": [
                "geo:SearchPlaceIndexForPosition",
                "geo:SearchPlaceIndexForText",
                "geo:SearchPlaceIndexForSuggestions",
                "geo:GetPlace"
              ],
              "Effect": "Allow",
              "Resource": {
                "Fn::GetAtt": [
                  "CustomPlaceIndex",
                  "IndexArn"
                ]
              }
            }
          ],
          "Version": "2012-10-17"
        },
        "PolicyName": {
          "Fn::Join": [
            "",
            [
              {
                "Fn::Join": [
                  "-",
                  [
                    {
                      "Ref": "indexName"
                    },
                    {
                      "Ref": "env"
                    }
                  ]
                ]
              },
              "Policy"
            ]
          ]
        },
        "Roles": [
          {
            "Ref": "authRoleName"
          },
          {
            "Ref": "unauthRoleName"
          },
          {
            "Fn::Join": [
              "-",
              [
                {
                  "Ref": "authstorelocatordemoXXXXXUserPoolId"
                },
                "storeLocatorDemoAdminGroupRole"
              ]
            ]
          }
        ]
      }
    }
  },
  "Outputs": {
    "Name": {
      "Value": {
        "Fn::GetAtt": [
          "CustomPlaceIndex",
          "IndexName"
        ]
      }
    },
    "Region": {
      "Value": {
        "Fn::FindInMap": [
          "RegionMapping",
          {
            "Ref": "AWS::Region"
          },
          "locationServiceRegion"
        ]
      }
    },
    "Arn": {
      "Value": {
        "Fn::GetAtt": [
          "CustomPlaceIndex",
          "IndexArn"
        ]
      }
    }
  }
}

Gen2 Geo Codegen Output

GeoFence Collections

resource.ts

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;
};

Geofence-construct.ts

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

resource.ts

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;
};

map-construct.ts

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

resource.ts

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;
};

geo-location-search-construct.ts

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 resource.ts

import { defineStoreLocatorDemoGeofence } from "./storeLocatorDemoGeofence/resource";
import { defineStoreLocatorDemoMap } from "./storeLocatorDemoMap/resource";
import { defineStoreLocatorDemoSearch } from "./storeLocatorDemoSearch/resource";
import { Backend } from "@aws-amplify/backend";

export const defineGeo = (backend: Backend<any>) => {
    const storeLocatorDemoGeofence = defineStoreLocatorDemoGeofence(backend);
    const storeLocatorDemoMap = defineStoreLocatorDemoMap(backend);
    const storeLocatorDemoSearch = defineStoreLocatorDemoSearch(backend);
    backend.addOutput({
        geo: {
            aws_region: storeLocatorDemoMap.region,
            maps: {
                items: {
                    [storeLocatorDemoMap.name]: { style: storeLocatorDemoMap.style }
                },
                default: storeLocatorDemoMap.name
            },
            search_indices: {
                items: [storeLocatorDemoSearch.name],
                default: storeLocatorDemoSearch.name
            },
            geofence_collections: {
                items: [storeLocatorDemoGeofence.name],
                default: storeLocatorDemoGeofence.name
            }
        }
    });
};

backend.ts

// ... other imports and config ...

import { defineGeo } from "./geo/resource";

// ... other backend definitions ...

const geo = defineGeo(backend);

@dgandhi62
Copy link
Contributor

dgandhi62 commented Mar 2, 2026

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?
Q2) How is stack naming happening currently for fences & maps? If we can have n number of fences, how are we distinguishing between them?
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?

@dgandhi62
Copy link
Contributor

dgandhi62 commented Mar 2, 2026

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/

@dgandhi62
Copy link
Contributor

dgandhi62 commented Mar 2, 2026

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

@sai-ray
Copy link
Contributor Author

sai-ray commented Mar 2, 2026

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?

  • All 3 resource types can have multiple resources.

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.

  • Yes, all three resource types support userPoolGroup based access. The codegen discovers all authuserPoolGroupsXXXXXGroupRole parameters from the deployed stack and emits them for any resource type, not just geofences. The only difference is Map and PlaceIndex also get authRoleName/unauthRoleName for general auth/unauth IAM access, while GeofenceCollection is group only.

Q4) Conversely, if we have access restricted by fences to one userPoolGroup, can we go and update it to restrict it to both?

  • No. The geofence CLI walkthrough only offers userPoolGroup options like which groups get access and what kind of access. There's no option to switch to or add authenticated/guest access like maps and search have. It's strictly userPoolGroup scoped.

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/

  • That page is just a frontend migration guide, it shows developers how to swap Google Maps API calls for Amplify Geo + MapLibre equivalents. It's not a Gen1 backend feature.

Q6) Are we handling map styles? https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/configure-maps/

  • Yes, mapStyle is read from deployed stack params and passed through as a static string literal.

Q7) Are we handling all the data providers? https://docs.amplify.aws/gen1/react/build-a-backend/more-features/geo/configure-location-search/

  • Yes, dataProvider and dataSourceIntendedUse are read from deployed stack params and passed through as it is.

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/

  • Location Search/ Location Search Index/ Place Index all refer to the same kind of resource. Multiple Location Search Index resources each get their own stack/construct/directory, same as maps and geofences.

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/

  • The "result storage location" setting maps to the dataSourceIntendedUse CFN parameter (SingleUse or Storage). It's not an actual caching mechanism, it's a flag that tells Amazon Location Service whether you intend to store/cache search results, which affects pricing tier. We read it from the deployed stack params and pass it through as-is in the generated construct. https://docs.aws.amazon.com/location/latest/developerguide/places-intended-use.html

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/

  • Yes, yes and yes. Your understanding is correct we can have multiple search indexes (same as maps and geofences), geofence collections are containers for individual geofences, and yes, we are scoping access policies at the collection level per userPoolGroup.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants