Skip to content

migratedAmplifyGen1DynamoDbTableMappings: imported table validation ignores @primaryKey, fails with "Imported table properties did not match the expected table properties" #3281

Description

@kevinxu3

Environment

  • @aws-amplify/backend: 1.23.0
  • @aws-amplify/backend-cli: 1.8.3
  • @aws-amplify/backend-data: 1.7.0
  • @aws-amplify/data-construct: 1.17.7
  • @aws-amplify/graphql-model-transformer: 3.5.0 (nested under data-construct)
  • Node.js: 24.x locally (also reproduced on the Amplify Hosting build image, Node 20)
  • Deployment: branch deployment via ampx pipeline-deploy (Gen 1 → Gen 2 migration flow)

Description

When migrating a Gen 1 backend to Gen 2 using migratedAmplifyGen1DynamoDbTableMappings, any model whose schema declares a custom @primaryKey (i.e. the partition key is not id) fails deployment. The Custom::ImportedAmplifyDynamoDBTable resource is synthesized with an expected key schema of id/HASH, ignoring the @primaryKey directive, so the import validation rejects the (correct) live Gen 1 table:

Received response status [FAILED] from custom resource. Message returned:
Imported table properties did not match the expected table properties.

The whole branch stack then rolls back (ROLLBACK_COMPLETE), and because it is a failed CREATE, the stack must be deleted manually before any retry — a rough failure mode in the middle of a migration flow.

Reproduction

Minimal schema (Gen 1 and Gen 2 identical):

type AuthSessionToken
  @model
  @auth(rules: [{ allow: public, operations: [create, update] }]) {
  userAuthenticating: ID! @primaryKey
  sessionToken: String!
}
  1. Deploy with Gen 1 (transformer v2). The table is correctly created with partition key userAuthenticating (HASH).
  2. Migrate to Gen 2 with the table mapped:
defineData({
  migratedAmplifyGen1DynamoDbTableMappings: [{
    branchName: "gen2-dev",
    modelNameToTableNameMapping: {
      AuthSessionToken: "AuthSessionToken-<gen1ApiId>-dev",
    },
  }],
  schema, // same schema string
});
  1. Deploy the branch → the AuthSessionToken nested stack fails with the error above.

Evidence

The TableManager custom resource receives these expected properties (from its CloudWatch logs):

"attributeDefinitions": [{ "attributeType": "S", "attributeName": "id" }],
"keySchema": [{ "attributeName": "id", "keyType": "HASH" }],
"isImported": "true",
"tableName": "AuthSessionToken-<gen1ApiId>-dev"

while the live Gen 1 table is:

"KeySchema": [{ "AttributeName": "userAuthenticating", "KeyType": "HASH" }],
"AttributeDefinitions": [{ "AttributeName": "userAuthenticating", "AttributeType": "S" }]

Notably, @primaryKey is honored everywhere else in the same synth:

  • The transformed GraphQL schema is correct: getAuthSessionToken(userAuthenticating: ID!), no injected id field, and CreateAuthSessionTokenInput is keyed on userAuthenticating.
  • Sandbox deployments of the same schema (no mapping → owned tables) create the table with the correct userAuthenticating key.
  • GSIs on imported tables are derived correctly from the model — in our 19-model schema, 18 tables (all with default id keys, several with multiple GSIs) validated fine; the only model with a custom @primaryKey was the only failure.

Root cause (best guess)

@aws-amplify/graphql-model-transformer, lib/resources/amplify-dynamodb-table/amplify-dynamo-model-resource-generator.js (~L159): the AmplifyDynamoDBTable construct is instantiated with a hardcoded partition key:

partitionKey: {
  name: 'id',
  type: AttributeType.STRING,
},
...(isTableImported ? { isImported: true } : undefined),

For owned tables the @primaryKey transformer corrects the key schema downstream, but for imported tables the TableManager validation consumes these initial construct properties directly, so the correction never applies.

Workaround

CDK escape hatch overriding the imported table's expected properties to match reality:

const imported = backend.data.stack.node.findAll().filter(
  (c): c is CfnResource =>
    CfnResource.isCfnResource(c) &&
    c.cfnResourceType === "Custom::ImportedAmplifyDynamoDBTable"
);
const t = imported.find((c) => c.node.path.includes("AuthSessionToken"));
t?.addPropertyOverride("keySchema", [
  { attributeName: "userAuthenticating", keyType: "HASH" },
]);
t?.addPropertyOverride("attributeDefinitions", [
  { attributeName: "userAuthenticating", attributeType: "S" },
]);

Expected behavior

The imported-table expected properties should derive the primary key from the model definition (as GSIs already are), so schemas with @primaryKey can migrate without manual overrides.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions