@go-to-k/cdkd 0.103.1 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { CreateTopicCommand, DeleteTopicCommand, GetSubscriptionAttributesComman
9
9
  import { AddPermissionCommand, CreateEventSourceMappingCommand, CreateFunctionCommand, CreateFunctionUrlConfigCommand, DeleteEventSourceMappingCommand, DeleteFunctionCommand, DeleteFunctionUrlConfigCommand, DeleteLayerVersionCommand, GetEventSourceMappingCommand, GetFunctionCommand, GetFunctionUrlConfigCommand, GetLayerVersionByArnCommand, GetPolicyCommand, LambdaClient, ListFunctionsCommand, ListLayersCommand, ListTagsCommand, PublishLayerVersionCommand, RemovePermissionCommand, ResourceNotFoundException, TagResourceCommand as TagResourceCommand$1, UntagResourceCommand as UntagResourceCommand$1, UpdateEventSourceMappingCommand, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, UpdateFunctionUrlConfigCommand, waitUntilFunctionUpdatedV2 } from "@aws-sdk/client-lambda";
10
10
  import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
11
11
  import { AssociateRouteTableCommand, AttachInternetGatewayCommand, AuthorizeSecurityGroupEgressCommand, AuthorizeSecurityGroupIngressCommand, CreateInternetGatewayCommand, CreateNatGatewayCommand, CreateNetworkAclCommand, CreateNetworkAclEntryCommand, CreateRouteCommand, CreateRouteTableCommand, CreateSecurityGroupCommand, CreateSubnetCommand, CreateTagsCommand, CreateVpcCommand, DeleteInternetGatewayCommand, DeleteNatGatewayCommand, DeleteNetworkAclCommand, DeleteNetworkAclEntryCommand, DeleteNetworkInterfaceCommand, DeleteRouteCommand, DeleteRouteTableCommand, DeleteSecurityGroupCommand, DeleteSubnetCommand, DeleteTagsCommand, DeleteVpcCommand, DescribeAvailabilityZonesCommand, DescribeInstanceAttributeCommand, DescribeInstancesCommand, DescribeInternetGatewaysCommand, DescribeNatGatewaysCommand, DescribeNetworkAclsCommand, DescribeNetworkInterfacesCommand, DescribeRouteTablesCommand, DescribeSecurityGroupsCommand, DescribeSubnetsCommand, DescribeVolumesCommand, DescribeVpcAttributeCommand, DescribeVpcsCommand, DetachInternetGatewayCommand, DisassociateRouteTableCommand, EC2Client, ModifyInstanceAttributeCommand, ModifySubnetAttributeCommand, ModifyVpcAttributeCommand, ReplaceNetworkAclAssociationCommand, RevokeSecurityGroupEgressCommand, RevokeSecurityGroupIngressCommand, RunInstancesCommand, TerminateInstancesCommand, waitUntilInstanceRunning, waitUntilInstanceTerminated, waitUntilNatGatewayAvailable, waitUntilNatGatewayDeleted } from "@aws-sdk/client-ec2";
12
- import { CreateTableCommand, DeleteTableCommand, DescribeTableCommand, DynamoDBClient, ListTablesCommand, ListTagsOfResourceCommand, ResourceNotFoundException as ResourceNotFoundException$1, TagResourceCommand as TagResourceCommand$2, UntagResourceCommand as UntagResourceCommand$2, UpdateTableCommand, UpdateTimeToLiveCommand } from "@aws-sdk/client-dynamodb";
12
+ import { CreateTableCommand, DeleteTableCommand, DescribeContinuousBackupsCommand, DescribeContributorInsightsCommand, DescribeKinesisStreamingDestinationCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DynamoDBClient, ListTablesCommand, ListTagsOfResourceCommand, ResourceNotFoundException as ResourceNotFoundException$1, TagResourceCommand as TagResourceCommand$2, UntagResourceCommand as UntagResourceCommand$2, UpdateTableCommand, UpdateTimeToLiveCommand } from "@aws-sdk/client-dynamodb";
13
13
  import { CloudFormationClient, CreateChangeSetCommand, DeleteChangeSetCommand, DeleteStackCommand, DescribeChangeSetCommand, DescribeStackEventsCommand, DescribeStackResourcesCommand, DescribeStacksCommand, DescribeTypeCommand, ExecuteChangeSetCommand, GetTemplateCommand, UpdateStackCommand, waitUntilChangeSetCreateComplete, waitUntilStackDeleteComplete, waitUntilStackImportComplete, waitUntilStackUpdateComplete } from "@aws-sdk/client-cloudformation";
14
14
  import { APIGatewayClient, CreateAuthorizerCommand, CreateDeploymentCommand, CreateResourceCommand, CreateStageCommand, DeleteAuthorizerCommand, DeleteDeploymentCommand, DeleteMethodCommand, DeleteResourceCommand, DeleteStageCommand, GetAccountCommand, GetAuthorizerCommand, GetDeploymentCommand, GetMethodCommand, GetResourceCommand, GetStageCommand, NotFoundException as NotFoundException$1, PutIntegrationCommand, PutIntegrationResponseCommand, PutMethodCommand, PutMethodResponseCommand, TagResourceCommand as TagResourceCommand$3, UntagResourceCommand as UntagResourceCommand$3, UpdateAccountCommand, UpdateAuthorizerCommand, UpdateMethodCommand, UpdateStageCommand } from "@aws-sdk/client-api-gateway";
15
15
  import { CreateEventBusCommand, DeleteEventBusCommand, DeleteRuleCommand, DescribeEventBusCommand, DescribeRuleCommand, EventBridgeClient, ListEventBusesCommand, ListRulesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$1, ListTargetsByRuleCommand, PutRuleCommand, PutTargetsCommand, RemoveTargetsCommand, ResourceNotFoundException as ResourceNotFoundException$2, TagResourceCommand as TagResourceCommand$4, UntagResourceCommand as UntagResourceCommand$4, UpdateEventBusCommand } from "@aws-sdk/client-eventbridge";
@@ -30,7 +30,7 @@ import { CreateAliasCommand, CreateKeyCommand, DeleteAliasCommand, DescribeKeyCo
30
30
  import { promisify } from "node:util";
31
31
  import { CreateRepositoryCommand, DeleteLifecyclePolicyCommand, DeleteRepositoryCommand, DeleteRepositoryPolicyCommand, DescribeRepositoriesCommand, ECRClient, GetAuthorizationTokenCommand, GetLifecyclePolicyCommand, LifecyclePolicyNotFoundException, ListTagsForResourceCommand as ListTagsForResourceCommand$7, PutImageScanningConfigurationCommand, PutImageTagMutabilityCommand, PutLifecyclePolicyCommand, RepositoryNotFoundException, SetRepositoryPolicyCommand, TagResourceCommand as TagResourceCommand$9 } from "@aws-sdk/client-ecr";
32
32
  import graphlib from "graphlib";
33
- import { AddTagsToResourceCommand as AddTagsToResourceCommand$1, CreateDBClusterCommand, CreateDBInstanceCommand, CreateDBSubnetGroupCommand, DeleteDBClusterCommand, DeleteDBInstanceCommand, DeleteDBSubnetGroupCommand, DescribeDBClustersCommand, DescribeDBInstancesCommand, DescribeDBSubnetGroupsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, ModifyDBClusterCommand, ModifyDBInstanceCommand, ModifyDBSubnetGroupCommand, RDSClient, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$1 } from "@aws-sdk/client-rds";
33
+ import { AddTagsToResourceCommand as AddTagsToResourceCommand$1, CreateDBClusterCommand, CreateDBInstanceCommand, CreateDBSubnetGroupCommand, DBProxyNotFoundFault, DBProxyTargetGroupNotFoundFault, DBProxyTargetNotFoundFault, DeleteDBClusterCommand, DeleteDBInstanceCommand, DeleteDBSubnetGroupCommand, DeregisterDBProxyTargetsCommand, DescribeDBClustersCommand, DescribeDBInstancesCommand, DescribeDBProxyTargetGroupsCommand, DescribeDBSubnetGroupsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, ModifyDBClusterCommand, ModifyDBInstanceCommand, ModifyDBProxyTargetGroupCommand, ModifyDBSubnetGroupCommand, RDSClient, RegisterDBProxyTargetsCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$1 } from "@aws-sdk/client-rds";
34
34
  import { Command, Option } from "commander";
35
35
  import { writeFileSync as writeFileSync$1 } from "fs";
36
36
  import { join as join$1 } from "path";
@@ -7864,20 +7864,36 @@ var DynamoDBTableProvider = class {
7864
7864
  * is the 2019.11.21 generation, which uses the regular DynamoDB CRUD API
7865
7865
  * (`CreateTableCommand` + `UpdateTableCommand` with `ReplicaUpdates`).
7866
7866
  *
7867
- * MVP scope:
7868
- * - `update()` throws `ResourceUpdateNotSupportedError`. In-place GlobalTable
7869
- * updates (replica add/remove, GSI add/remove, BillingMode flip, throughput
7870
- * rewrites) are out of scope and a follow-up PR.
7871
- * - `getDriftUnknownPaths` declares TTL + throughput-settings paths because
7872
- * cdkd's create/update flows surface them but the read-side reverse
7873
- * mapping is non-trivial and would fire false drift.
7867
+ * In-place update support (post-PR #384 follow-up):
7868
+ * - `update()` covers every mutable surface — Tags, DeletionProtection,
7869
+ * TableClass, SSE, StreamSpec, OnDemand throughput, BillingMode flip,
7870
+ * Replica add / remove / modify, GSI add / remove / modify, TTL toggle.
7871
+ * - The serialization is load-bearing: AWS's `UpdateTable` accepts only
7872
+ * ONE of `{BillingMode, ReplicaUpdates, GlobalSecondaryIndexUpdates}`
7873
+ * per call, so each category is its own SDK round-trip with a wait-for
7874
+ * -ACTIVE in between. Immutable property changes (TableName, KeySchema,
7875
+ * AttributeDefinitions removal, LocalSecondaryIndexes) throw
7876
+ * `ProvisioningError` naming the offending field — the deploy engine's
7877
+ * diff classification should catch these as REPLACEMENT before ever
7878
+ * calling `update()`, but the guard is defense-in-depth.
7874
7879
  * - Per-replica drift (`ContributorInsightsSpecification` /
7875
- * `PointInTimeRecoverySpecification` / `KinesisStreamSpecification`) is
7876
- * out of scope for v1.
7880
+ * `PointInTimeRecoverySpecification` / `KinesisStreamSpecification`)
7881
+ * is surfaced for the LOCAL replica only; cross-region replicas
7882
+ * require per-region SDK clients which are out of scope for v1.
7877
7883
  */
7878
7884
  var DynamoDBGlobalTableProvider = class {
7879
7885
  dynamoDBClient;
7880
7886
  logger = getLogger().child("DynamoDBGlobalTableProvider");
7887
+ /**
7888
+ * Caches `getAttribute(physicalId, attribute)` results for the lifetime
7889
+ * of this provider instance (one deploy run). Safe under the current
7890
+ * `update()` contract because `update()` cannot mid-deploy mutate
7891
+ * StreamArn / Arn / TableId — those are AWS-managed identifiers that
7892
+ * only change on REPLACEMENT (which destroys the provider instance).
7893
+ * If a future PR adds a stream toggle path that flips StreamArn on the
7894
+ * same physicalId, the cache must be invalidated on the matching
7895
+ * UpdateTable success.
7896
+ */
7881
7897
  attributeCache = /* @__PURE__ */ new Map();
7882
7898
  handledProperties = new Map([["AWS::DynamoDB::GlobalTable", new Set([
7883
7899
  "TableName",
@@ -7893,8 +7909,7 @@ var DynamoDBGlobalTableProvider = class {
7893
7909
  "TimeToLiveSpecification",
7894
7910
  "WriteProvisionedThroughputSettings",
7895
7911
  "WriteOnDemandThroughputSettings",
7896
- "DeletionProtectionEnabled",
7897
- "Tags"
7912
+ "DeletionProtectionEnabled"
7898
7913
  ])]]);
7899
7914
  constructor() {
7900
7915
  const awsClients = getAwsClients();
@@ -7935,15 +7950,7 @@ var DynamoDBGlobalTableProvider = class {
7935
7950
  AttributeDefinitions: attributeDefinitions,
7936
7951
  BillingMode: billingMode
7937
7952
  };
7938
- if (billingMode === "PROVISIONED") {
7939
- const writeAutoScaling = properties["WriteProvisionedThroughputSettings"]?.["WriteCapacityAutoScalingSettings"];
7940
- const writeCapacity = Number(writeAutoScaling?.["MinCapacity"] ?? 5);
7941
- const readAutoScaling = (replicas.find((r) => r["Region"] === currentRegion)?.["ReadProvisionedThroughputSettings"])?.["ReadCapacityAutoScalingSettings"];
7942
- createParams.ProvisionedThroughput = {
7943
- ReadCapacityUnits: Number(readAutoScaling?.["MinCapacity"] ?? 5),
7944
- WriteCapacityUnits: writeCapacity
7945
- };
7946
- }
7953
+ if (billingMode === "PROVISIONED") createParams.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion);
7947
7954
  const streamSpecInput = properties["StreamSpecification"];
7948
7955
  const needsStream = replicas.some((r) => r["Region"] !== currentRegion) || replicas.length > 1;
7949
7956
  if (streamSpecInput) createParams.StreamSpecification = {
@@ -7969,7 +7976,8 @@ var DynamoDBGlobalTableProvider = class {
7969
7976
  if (properties["TableClass"]) createParams.TableClass = properties["TableClass"];
7970
7977
  const wodts = properties["WriteOnDemandThroughputSettings"];
7971
7978
  if (wodts?.["MaxWriteRequestUnits"] !== void 0) createParams.OnDemandThroughput = { MaxWriteRequestUnits: Number(wodts["MaxWriteRequestUnits"]) };
7972
- if (properties["Tags"]) createParams.Tags = properties["Tags"];
7979
+ const localReplicaTags = replicas.find((r) => r["Region"] === currentRegion)?.["Tags"];
7980
+ if (localReplicaTags && localReplicaTags.length > 0) createParams.Tags = localReplicaTags;
7973
7981
  try {
7974
7982
  await this.dynamoDBClient.send(new CreateTableCommand(createParams));
7975
7983
  this.logger.debug(`CreateTable initiated for ${tableName}, waiting for ACTIVE`);
@@ -7978,11 +7986,11 @@ var DynamoDBGlobalTableProvider = class {
7978
7986
  throw new ProvisioningError(`Failed to create DynamoDB GlobalTable ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, tableName, cause);
7979
7987
  }
7980
7988
  try {
7981
- const tableInfo = await this.waitForTableActive(tableName);
7989
+ const tableInfo = await this.waitForTableActive(tableName, logicalId);
7982
7990
  for (const replica of replicas) {
7983
7991
  const region = replica["Region"];
7984
7992
  if (!region || region === currentRegion) continue;
7985
- await this.addReplica(tableName, replica, region);
7993
+ await this.addReplica(tableName, replica, region, logicalId);
7986
7994
  }
7987
7995
  if (properties["TimeToLiveSpecification"]) {
7988
7996
  const ttl = properties["TimeToLiveSpecification"];
@@ -8009,6 +8017,21 @@ var DynamoDBGlobalTableProvider = class {
8009
8017
  } catch (wiringError) {
8010
8018
  this.logger.warn(`Wiring failed after CreateTable for ${tableName}; attempting best-effort cleanup`);
8011
8019
  try {
8020
+ const replicasForCleanup = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas ?? [];
8021
+ for (const replica of replicasForCleanup) {
8022
+ const region = replica.RegionName;
8023
+ if (!region || region === currentRegion) continue;
8024
+ try {
8025
+ await this.dynamoDBClient.send(new UpdateTableCommand({
8026
+ TableName: tableName,
8027
+ ReplicaUpdates: [{ Delete: { RegionName: region } }]
8028
+ }));
8029
+ await this.waitForReplicaGone(tableName, region, logicalId);
8030
+ } catch (replicaCleanupErr) {
8031
+ const msg = replicaCleanupErr instanceof Error ? replicaCleanupErr.message : String(replicaCleanupErr);
8032
+ this.logger.warn(`Partial-create cleanup: failed to drop replica ${region} on ${tableName}: ${msg}. Run: aws dynamodb update-table --table-name ${tableName} --replica-updates 'Delete={RegionName=${region}}' --region ${currentRegion}`);
8033
+ }
8034
+ }
8012
8035
  await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: tableName }));
8013
8036
  } catch (cleanupErr) {
8014
8037
  const cleanupMsg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
@@ -8025,7 +8048,7 @@ var DynamoDBGlobalTableProvider = class {
8025
8048
  * Capped at 10 minutes per replica (AWS Replica provisioning typically
8026
8049
  * takes 1–5 min).
8027
8050
  */
8028
- async addReplica(tableName, replica, region) {
8051
+ async addReplica(tableName, replica, region, logicalId) {
8029
8052
  const create = { RegionName: region };
8030
8053
  if (replica["KMSMasterKeyId"]) create.KMSMasterKeyId = replica["KMSMasterKeyId"];
8031
8054
  if (replica["GlobalSecondaryIndexes"]) create.GlobalSecondaryIndexes = replica["GlobalSecondaryIndexes"];
@@ -8035,19 +8058,239 @@ var DynamoDBGlobalTableProvider = class {
8035
8058
  TableName: tableName,
8036
8059
  ReplicaUpdates: replicaUpdates
8037
8060
  }));
8038
- await this.waitForReplicaActive(tableName, region);
8061
+ await this.waitForReplicaActive(tableName, region, logicalId);
8062
+ }
8063
+ /**
8064
+ * Update a DynamoDB Global Table in place.
8065
+ *
8066
+ * AWS-side state-machine constraint: `UpdateTable` accepts only ONE of
8067
+ * `{BillingMode, ReplicaUpdates, GlobalSecondaryIndexUpdates}` per call,
8068
+ * so each category must serialize into its own SDK round-trip with a
8069
+ * `waitForTableActiveAfterUpdate` between every step. Order:
8070
+ * 1. Wait for current ACTIVE (defensive).
8071
+ * 2. Tags diff (TagResource / UntagResource — no wait needed).
8072
+ * 3. Non-conflicting flat fields (DeletionProtectionEnabled / TableClass
8073
+ * / SSESpecification / StreamSpecification / OnDemandThroughput)
8074
+ * in one combined `UpdateTableCommand`. Wait ACTIVE.
8075
+ * 4. BillingMode flip (separate UpdateTable). Wait ACTIVE.
8076
+ * 5. Replica diff (serial per Create / Update / Delete). Wait ACTIVE
8077
+ * after each.
8078
+ * 6. GSI diff (serial per Create / Update / Delete; new GSIs may need
8079
+ * additional AttributeDefinitions). Wait ACTIVE after each.
8080
+ * 7. TimeToLiveSpecification toggle.
8081
+ *
8082
+ * Immutable properties (TableName / KeySchema / AttributeDefinitions
8083
+ * removals / LocalSecondaryIndexes changes) throw `ProvisioningError`
8084
+ * naming the offending field — the deploy engine's diff classifier
8085
+ * should catch these as REPLACEMENT before ever calling `update()`,
8086
+ * but the guard is defense-in-depth.
8087
+ */
8088
+ async update(logicalId, physicalId, resourceType, properties, previousProperties) {
8089
+ this.logger.debug(`Updating DynamoDB GlobalTable ${logicalId}: ${physicalId}`);
8090
+ if (properties["TableName"] !== void 0 && previousProperties["TableName"] !== void 0 && properties["TableName"] !== previousProperties["TableName"]) throw new ProvisioningError(`TableName is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
8091
+ if (properties["KeySchema"] !== void 0 && previousProperties["KeySchema"] !== void 0 && !deepEqual$1(properties["KeySchema"], previousProperties["KeySchema"])) throw new ProvisioningError(`KeySchema is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
8092
+ if (properties["LocalSecondaryIndexes"] !== void 0 && previousProperties["LocalSecondaryIndexes"] !== void 0 && !deepEqual$1(properties["LocalSecondaryIndexes"], previousProperties["LocalSecondaryIndexes"])) throw new ProvisioningError(`LocalSecondaryIndexes is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
8093
+ const oldAttrs = previousProperties["AttributeDefinitions"] ?? [];
8094
+ const newAttrs = properties["AttributeDefinitions"] ?? [];
8095
+ const removedAttrs = oldAttrs.filter((o) => !newAttrs.some((n) => n.AttributeName === o.AttributeName));
8096
+ if (removedAttrs.length > 0) throw new ProvisioningError(`AttributeDefinitions removals are immutable on AWS::DynamoDB::GlobalTable (offenders: ${removedAttrs.map((a) => a.AttributeName).join(", ")}); replacement required`, resourceType, logicalId, physicalId);
8097
+ const currentRegion = await this.dynamoDBClient.config.region() ?? "";
8098
+ try {
8099
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
8100
+ const tableArn = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }))).Table?.TableArn;
8101
+ const extractLocalTags = (props) => {
8102
+ return (props["Replicas"] ?? []).find((r) => r["Region"] === currentRegion)?.["Tags"];
8103
+ };
8104
+ if (tableArn) await this.applyTagDiff(tableArn, extractLocalTags(previousProperties), extractLocalTags(properties));
8105
+ const flatUpdate = { TableName: physicalId };
8106
+ let flatChanged = false;
8107
+ if (properties["DeletionProtectionEnabled"] !== previousProperties["DeletionProtectionEnabled"]) {
8108
+ flatUpdate.DeletionProtectionEnabled = Boolean(properties["DeletionProtectionEnabled"] ?? false);
8109
+ flatChanged = true;
8110
+ }
8111
+ if (properties["TableClass"] !== void 0 && properties["TableClass"] !== previousProperties["TableClass"]) {
8112
+ flatUpdate.TableClass = properties["TableClass"];
8113
+ flatChanged = true;
8114
+ }
8115
+ if (properties["SSESpecification"] !== void 0 && !deepEqual$1(properties["SSESpecification"], previousProperties["SSESpecification"])) {
8116
+ const sse = properties["SSESpecification"];
8117
+ flatUpdate.SSESpecification = {
8118
+ Enabled: sse["SSEEnabled"] !== void 0 ? Boolean(sse["SSEEnabled"]) : true,
8119
+ ...sse["SSEType"] !== void 0 && { SSEType: sse["SSEType"] }
8120
+ };
8121
+ flatChanged = true;
8122
+ }
8123
+ if (properties["StreamSpecification"] !== void 0 && !deepEqual$1(properties["StreamSpecification"], previousProperties["StreamSpecification"])) {
8124
+ flatUpdate.StreamSpecification = {
8125
+ StreamEnabled: true,
8126
+ StreamViewType: properties["StreamSpecification"]["StreamViewType"]
8127
+ };
8128
+ flatChanged = true;
8129
+ }
8130
+ if (!deepEqual$1(properties["WriteOnDemandThroughputSettings"], previousProperties["WriteOnDemandThroughputSettings"])) {
8131
+ const wodts = properties["WriteOnDemandThroughputSettings"];
8132
+ if (wodts?.["MaxWriteRequestUnits"] !== void 0) {
8133
+ flatUpdate.OnDemandThroughput = { MaxWriteRequestUnits: Number(wodts["MaxWriteRequestUnits"]) };
8134
+ flatChanged = true;
8135
+ }
8136
+ }
8137
+ if (flatChanged) {
8138
+ await this.dynamoDBClient.send(new UpdateTableCommand(flatUpdate));
8139
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
8140
+ }
8141
+ const oldBilling = previousProperties["BillingMode"] ?? "PAY_PER_REQUEST";
8142
+ const newBilling = properties["BillingMode"] ?? "PAY_PER_REQUEST";
8143
+ if (oldBilling !== newBilling) {
8144
+ const billingUpdate = {
8145
+ TableName: physicalId,
8146
+ BillingMode: newBilling
8147
+ };
8148
+ if (newBilling === "PROVISIONED") billingUpdate.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion);
8149
+ await this.dynamoDBClient.send(new UpdateTableCommand(billingUpdate));
8150
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
8151
+ }
8152
+ const replicaDiff = diffReplicas(previousProperties["Replicas"] ?? [], properties["Replicas"] ?? []);
8153
+ for (const replica of replicaDiff.removed) {
8154
+ const region = replica["Region"];
8155
+ if (!region || region === currentRegion) continue;
8156
+ await this.dynamoDBClient.send(new UpdateTableCommand({
8157
+ TableName: physicalId,
8158
+ ReplicaUpdates: [{ Delete: { RegionName: region } }]
8159
+ }));
8160
+ await this.waitForReplicaGone(physicalId, region, logicalId);
8161
+ }
8162
+ for (const replica of replicaDiff.added) {
8163
+ const region = replica["Region"];
8164
+ if (!region || region === currentRegion) continue;
8165
+ await this.addReplica(physicalId, replica, region, logicalId);
8166
+ }
8167
+ for (const replica of replicaDiff.modified) {
8168
+ const region = replica["Region"];
8169
+ if (!region || region === currentRegion) continue;
8170
+ const updateAction = { RegionName: region };
8171
+ if (replica["KMSMasterKeyId"] !== void 0) updateAction.KMSMasterKeyId = replica["KMSMasterKeyId"];
8172
+ if (replica["GlobalSecondaryIndexes"]) updateAction.GlobalSecondaryIndexes = replica["GlobalSecondaryIndexes"];
8173
+ if (replica["TableClassOverride"]) updateAction.TableClassOverride = replica["TableClassOverride"];
8174
+ if (!(updateAction.KMSMasterKeyId !== void 0 || updateAction.GlobalSecondaryIndexes !== void 0 || updateAction.TableClassOverride !== void 0)) {
8175
+ this.logger.debug(`Cross-region replica ${region} of ${physicalId}: only Tags-style changes detected; skipping UpdateReplica (AWS rejects empty Update actions). Per-region Tag propagation is deferred.`);
8176
+ continue;
8177
+ }
8178
+ await this.dynamoDBClient.send(new UpdateTableCommand({
8179
+ TableName: physicalId,
8180
+ ReplicaUpdates: [{ Update: updateAction }]
8181
+ }));
8182
+ await this.waitForReplicaActive(physicalId, region, logicalId);
8183
+ }
8184
+ const gsiDiff = diffGlobalSecondaryIndexes(previousProperties["GlobalSecondaryIndexes"] ?? [], properties["GlobalSecondaryIndexes"] ?? []);
8185
+ for (const gsi of gsiDiff.removed) {
8186
+ if (!gsi.IndexName) continue;
8187
+ const gsiUpdate = { Delete: { IndexName: gsi.IndexName } };
8188
+ await this.dynamoDBClient.send(new UpdateTableCommand({
8189
+ TableName: physicalId,
8190
+ GlobalSecondaryIndexUpdates: [gsiUpdate]
8191
+ }));
8192
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
8193
+ }
8194
+ for (const gsi of gsiDiff.added) {
8195
+ if (!gsi.IndexName || !gsi.KeySchema || !gsi.Projection) continue;
8196
+ const gsiUpdate = { Create: {
8197
+ IndexName: gsi.IndexName,
8198
+ KeySchema: gsi.KeySchema,
8199
+ Projection: gsi.Projection,
8200
+ ...gsi.ProvisionedThroughput && { ProvisionedThroughput: gsi.ProvisionedThroughput },
8201
+ ...gsi.OnDemandThroughput && { OnDemandThroughput: gsi.OnDemandThroughput }
8202
+ } };
8203
+ await this.dynamoDBClient.send(new UpdateTableCommand({
8204
+ TableName: physicalId,
8205
+ AttributeDefinitions: newAttrs,
8206
+ GlobalSecondaryIndexUpdates: [gsiUpdate]
8207
+ }));
8208
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
8209
+ }
8210
+ for (const gsi of gsiDiff.modified) {
8211
+ if (!gsi.IndexName) continue;
8212
+ const gsiUpdate = { Update: {
8213
+ IndexName: gsi.IndexName,
8214
+ ...gsi.ProvisionedThroughput && { ProvisionedThroughput: gsi.ProvisionedThroughput },
8215
+ ...gsi.OnDemandThroughput && { OnDemandThroughput: gsi.OnDemandThroughput }
8216
+ } };
8217
+ await this.dynamoDBClient.send(new UpdateTableCommand({
8218
+ TableName: physicalId,
8219
+ GlobalSecondaryIndexUpdates: [gsiUpdate]
8220
+ }));
8221
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
8222
+ }
8223
+ if (!deepEqual$1(properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"])) {
8224
+ const ttl = properties["TimeToLiveSpecification"];
8225
+ if (ttl?.["AttributeName"]) await this.dynamoDBClient.send(new UpdateTimeToLiveCommand({
8226
+ TableName: physicalId,
8227
+ TimeToLiveSpecification: {
8228
+ Enabled: ttl["Enabled"] !== void 0 ? Boolean(ttl["Enabled"]) : true,
8229
+ AttributeName: ttl["AttributeName"]
8230
+ }
8231
+ }));
8232
+ else if (previousProperties["TimeToLiveSpecification"]) {
8233
+ const prevTtl = previousProperties["TimeToLiveSpecification"];
8234
+ if (prevTtl["AttributeName"]) await this.dynamoDBClient.send(new UpdateTimeToLiveCommand({
8235
+ TableName: physicalId,
8236
+ TimeToLiveSpecification: {
8237
+ Enabled: false,
8238
+ AttributeName: prevTtl["AttributeName"]
8239
+ }
8240
+ }));
8241
+ }
8242
+ }
8243
+ const finalDescribe = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }));
8244
+ return {
8245
+ physicalId,
8246
+ wasReplaced: false,
8247
+ attributes: {
8248
+ Arn: finalDescribe.Table?.TableArn,
8249
+ TableId: finalDescribe.Table?.TableId,
8250
+ StreamArn: finalDescribe.Table?.LatestStreamArn,
8251
+ TableName: physicalId
8252
+ }
8253
+ };
8254
+ } catch (error) {
8255
+ if (error instanceof ProvisioningError) throw error;
8256
+ const cause = error instanceof Error ? error : void 0;
8257
+ throw new ProvisioningError(`Failed to update DynamoDB GlobalTable ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
8258
+ }
8039
8259
  }
8040
8260
  /**
8041
- * Update a DynamoDB Global Table.
8042
- *
8043
- * MVP: in-place updates are out of scope — replica add/remove, GSI
8044
- * add/remove, BillingMode flip, and throughput rewrites each have
8045
- * distinct UpdateTable shapes and ordering rules. `cdkd drift --revert`
8046
- * surfaces this as `ResourceUpdateNotSupportedError` (exit code 2);
8047
- * the user falls back to `cdkd deploy --replace` or destroy + redeploy.
8261
+ * Apply a diff between old and new CFn-shape Tags arrays via DynamoDB's
8262
+ * `TagResource` / `UntagResource` APIs. Both take the table ARN as
8263
+ * `ResourceArn`.
8048
8264
  */
8049
- async update(logicalId, _physicalId, resourceType, _properties, _previousProperties) {
8050
- throw new ResourceUpdateNotSupportedError(resourceType, logicalId, "GlobalTable in-place updates are not yet supported; use 'cdkd deploy --replace' or destroy + redeploy");
8265
+ async applyTagDiff(tableArn, oldTagsRaw, newTagsRaw) {
8266
+ const toMap = (tags) => {
8267
+ const m = /* @__PURE__ */ new Map();
8268
+ for (const t of tags ?? []) if (t.Key !== void 0 && t.Value !== void 0) m.set(t.Key, t.Value);
8269
+ return m;
8270
+ };
8271
+ const oldMap = toMap(oldTagsRaw);
8272
+ const newMap = toMap(newTagsRaw);
8273
+ const tagsToAdd = [];
8274
+ for (const [k, v] of newMap) if (oldMap.get(k) !== v) tagsToAdd.push({
8275
+ Key: k,
8276
+ Value: v
8277
+ });
8278
+ const tagsToRemove = [];
8279
+ for (const k of oldMap.keys()) if (!newMap.has(k)) tagsToRemove.push(k);
8280
+ if (tagsToRemove.length > 0) {
8281
+ await this.dynamoDBClient.send(new UntagResourceCommand$2({
8282
+ ResourceArn: tableArn,
8283
+ TagKeys: tagsToRemove
8284
+ }));
8285
+ this.logger.debug(`Removed ${tagsToRemove.length} tag(s) from DynamoDB GlobalTable ${tableArn}`);
8286
+ }
8287
+ if (tagsToAdd.length > 0) {
8288
+ await this.dynamoDBClient.send(new TagResourceCommand$2({
8289
+ ResourceArn: tableArn,
8290
+ Tags: tagsToAdd
8291
+ }));
8292
+ this.logger.debug(`Added/updated ${tagsToAdd.length} tag(s) on DynamoDB GlobalTable ${tableArn}`);
8293
+ }
8051
8294
  }
8052
8295
  /**
8053
8296
  * Delete a DynamoDB Global Table.
@@ -8075,7 +8318,7 @@ var DynamoDBGlobalTableProvider = class {
8075
8318
  }));
8076
8319
  this.logger.debug(`Disabled DeletionProtectionEnabled on ${logicalId}, waiting for ACTIVE`);
8077
8320
  try {
8078
- await this.waitForTableActiveAfterUpdate(physicalId);
8321
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
8079
8322
  } catch (waitErr) {
8080
8323
  this.logger.debug(`Could not wait for table ${physicalId} ACTIVE after protection flip: ${waitErr instanceof Error ? waitErr.message : String(waitErr)}`);
8081
8324
  }
@@ -8099,7 +8342,7 @@ var DynamoDBGlobalTableProvider = class {
8099
8342
  TableName: physicalId,
8100
8343
  ReplicaUpdates: [{ Delete: { RegionName: region } }]
8101
8344
  }));
8102
- await this.waitForReplicaGone(physicalId, region);
8345
+ await this.waitForReplicaGone(physicalId, region, logicalId);
8103
8346
  } catch (replicaErr) {
8104
8347
  if (!(replicaErr instanceof ResourceNotFoundException$1)) throw replicaErr;
8105
8348
  }
@@ -8112,7 +8355,7 @@ var DynamoDBGlobalTableProvider = class {
8112
8355
  }
8113
8356
  try {
8114
8357
  await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
8115
- await this.waitForTableGone(physicalId);
8358
+ await this.waitForTableGone(physicalId, logicalId);
8116
8359
  this.logger.debug(`Successfully deleted DynamoDB GlobalTable ${logicalId}`);
8117
8360
  } catch (error) {
8118
8361
  if (error instanceof ResourceNotFoundException$1) {
@@ -8158,22 +8401,29 @@ var DynamoDBGlobalTableProvider = class {
8158
8401
  /**
8159
8402
  * Read the AWS-current DynamoDB GlobalTable configuration in CFn-property shape.
8160
8403
  *
8161
- * Reverse-maps `DescribeTable` + `ListTagsOfResource` into the
8162
- * `AWS::DynamoDB::GlobalTable` property set.
8404
+ * Reverse-maps `DescribeTable` + `ListTagsOfResource` + `DescribeTimeToLive`
8405
+ * + per-replica `DescribeContributorInsights` /
8406
+ * `DescribeContinuousBackups` / `DescribeKinesisStreamingDestination`
8407
+ * into the `AWS::DynamoDB::GlobalTable` property set.
8163
8408
  *
8164
8409
  * Type-discriminator gating (memory rule
8165
8410
  * `feedback_always_emit_check_type_discriminator.md`):
8166
- * - `ProvisionedThroughput`-bearing fields are only surfaced when
8167
- * `BillingMode === 'PROVISIONED'`. Emitting placeholders on
8168
- * PAY_PER_REQUEST tables (or vice versa) would fire false drift on
8169
- * every clean run.
8170
8411
  * - StreamSpecification / SSESpecification follow the existing
8171
8412
  * DynamoDB::Table provider's Class 1 guard: only surfaced when AWS
8172
8413
  * reports the feature actually enabled.
8173
- *
8174
- * `getDriftUnknownPaths` declares TTL + write-throughput-settings
8175
- * those round-trip in the create path but the reverse-mapping is not
8176
- * yet implemented and would fire guaranteed false drift.
8414
+ * - `ProvisionedThroughput`-bearing fields are declared in
8415
+ * `getDriftUnknownPaths` and intentionally not emitted in v1 the
8416
+ * reverse-mapping from AWS's `ProvisionedThroughput` shape into
8417
+ * CFn's `WriteProvisionedThroughputSettings` /
8418
+ * `ReadProvisionedThroughputSettings` wrappers (which carry
8419
+ * `WriteCapacityAutoScalingSettings` etc.) needs more work to round
8420
+ * -trip cleanly.
8421
+ *
8422
+ * Per-replica sub-specifications (`ContributorInsightsSpecification` /
8423
+ * `PointInTimeRecoverySpecification` / `KinesisStreamSpecification`)
8424
+ * are surfaced only for the LOCAL replica. Cross-region replicas
8425
+ * require per-region SDK clients (`new DynamoDBClient({region})`),
8426
+ * deferred to a follow-up PR.
8177
8427
  */
8178
8428
  async readCurrentState(physicalId, _logicalId, _resourceType) {
8179
8429
  try {
@@ -8196,19 +8446,38 @@ var DynamoDBGlobalTableProvider = class {
8196
8446
  if (table.SSEDescription.SSEType !== void 0) sse["SSEType"] = table.SSEDescription.SSEType;
8197
8447
  result["SSESpecification"] = sse;
8198
8448
  }
8199
- result["Replicas"] = (table.Replicas ?? []).map((r) => ({
8200
- Region: r.RegionName,
8201
- ...r.KMSMasterKeyId !== void 0 && { KMSMasterKeyId: r.KMSMasterKeyId }
8202
- }));
8203
8449
  if (table.TableClassSummary?.TableClass) result["TableClass"] = table.TableClassSummary.TableClass;
8204
8450
  if (table.DeletionProtectionEnabled !== void 0) result["DeletionProtectionEnabled"] = table.DeletionProtectionEnabled;
8205
- if (table.TableArn) try {
8206
- result["Tags"] = normalizeAwsTagsToCfn((await this.dynamoDBClient.send(new ListTagsOfResourceCommand({ ResourceArn: table.TableArn }))).Tags);
8207
- } catch (err) {
8208
- if (err instanceof ResourceNotFoundException$1) return void 0;
8209
- result["Tags"] = [];
8451
+ const currentRegion = await this.dynamoDBClient.config.region() ?? "";
8452
+ const tableNameForSubs = table.TableName ?? physicalId;
8453
+ result["Replicas"] = await Promise.all((table.Replicas ?? []).map(async (r) => {
8454
+ const entry = { Region: r.RegionName };
8455
+ if (r.KMSMasterKeyId !== void 0) entry["KMSMasterKeyId"] = r.KMSMasterKeyId;
8456
+ if (r.RegionName && r.RegionName === currentRegion) {
8457
+ const subs = await this.readLocalReplicaSubSpecs(tableNameForSubs);
8458
+ Object.assign(entry, subs);
8459
+ if (table.TableArn) try {
8460
+ entry["Tags"] = normalizeAwsTagsToCfn((await this.dynamoDBClient.send(new ListTagsOfResourceCommand({ ResourceArn: table.TableArn }))).Tags);
8461
+ } catch (tagErr) {
8462
+ if (tagErr instanceof ResourceNotFoundException$1) throw tagErr;
8463
+ this.logger.warn(`Could not fetch tags for DynamoDB GlobalTable ${tableNameForSubs}: ${tagErr instanceof Error ? tagErr.message : String(tagErr)}`);
8464
+ entry["Tags"] = [];
8465
+ }
8466
+ else entry["Tags"] = [];
8467
+ }
8468
+ return entry;
8469
+ }));
8470
+ try {
8471
+ const ttlDesc = (await this.dynamoDBClient.send(new DescribeTimeToLiveCommand({ TableName: tableNameForSubs }))).TimeToLiveDescription;
8472
+ const ttlStatus = ttlDesc?.TimeToLiveStatus;
8473
+ if (ttlStatus === "ENABLED" && ttlDesc?.AttributeName) result["TimeToLiveSpecification"] = {
8474
+ AttributeName: ttlDesc.AttributeName,
8475
+ Enabled: true
8476
+ };
8477
+ else if (ttlStatus === "DISABLED") {}
8478
+ } catch (ttlErr) {
8479
+ this.logger.debug(`Could not read TimeToLive for ${tableNameForSubs}: ${ttlErr instanceof Error ? ttlErr.message : String(ttlErr)}`);
8210
8480
  }
8211
- else result["Tags"] = [];
8212
8481
  return result;
8213
8482
  } catch (err) {
8214
8483
  if (err instanceof ResourceNotFoundException$1) return void 0;
@@ -8216,25 +8485,64 @@ var DynamoDBGlobalTableProvider = class {
8216
8485
  }
8217
8486
  }
8218
8487
  /**
8488
+ * Read per-replica sub-specifications for the LOCAL replica:
8489
+ * - `ContributorInsightsSpecification` via `DescribeContributorInsights`
8490
+ * (table-level; GSI overrides are NOT surfaced in v1 — they would
8491
+ * require one call per GSI and a different CFn nesting under the
8492
+ * `Replicas[].GlobalSecondaryIndexes[]` shape).
8493
+ * - `PointInTimeRecoverySpecification` via `DescribeContinuousBackups`.
8494
+ * - `KinesisStreamSpecification` via
8495
+ * `DescribeKinesisStreamingDestination` (filtered to the local
8496
+ * region's destination when AWS reports more than one).
8497
+ *
8498
+ * Each call is best-effort: errors omit the offending key rather than
8499
+ * fail the whole drift read.
8500
+ *
8501
+ * Cross-region replicas would need per-region SDK clients (the calls
8502
+ * are region-scoped to the replica) — deferred to a follow-up PR.
8503
+ */
8504
+ async readLocalReplicaSubSpecs(tableName) {
8505
+ const out = {};
8506
+ try {
8507
+ const ci = await this.dynamoDBClient.send(new DescribeContributorInsightsCommand({ TableName: tableName }));
8508
+ if (ci.ContributorInsightsStatus) out["ContributorInsightsSpecification"] = { Enabled: ci.ContributorInsightsStatus === "ENABLED" };
8509
+ } catch (err) {
8510
+ this.logger.debug(`Could not read ContributorInsights for ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
8511
+ }
8512
+ try {
8513
+ const pitrStatus = (await this.dynamoDBClient.send(new DescribeContinuousBackupsCommand({ TableName: tableName }))).ContinuousBackupsDescription?.PointInTimeRecoveryDescription?.PointInTimeRecoveryStatus;
8514
+ if (pitrStatus) out["PointInTimeRecoverySpecification"] = { PointInTimeRecoveryEnabled: pitrStatus === "ENABLED" };
8515
+ } catch (err) {
8516
+ this.logger.debug(`Could not read PointInTimeRecovery for ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
8517
+ }
8518
+ try {
8519
+ const active = ((await this.dynamoDBClient.send(new DescribeKinesisStreamingDestinationCommand({ TableName: tableName }))).KinesisDataStreamDestinations ?? []).find((d) => d.DestinationStatus === "ACTIVE" || d.DestinationStatus === "ENABLING");
8520
+ if (active?.StreamArn) {
8521
+ const ksOut = { StreamArn: active.StreamArn };
8522
+ if (active.ApproximateCreationDateTimePrecision !== void 0) ksOut["ApproximateCreationDateTimePrecision"] = active.ApproximateCreationDateTimePrecision;
8523
+ out["KinesisStreamSpecification"] = ksOut;
8524
+ }
8525
+ } catch (err) {
8526
+ this.logger.debug(`Could not read KinesisStreamingDestination for ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
8527
+ }
8528
+ return out;
8529
+ }
8530
+ /**
8219
8531
  * State property paths cdkd's GlobalTable readCurrentState cannot (yet)
8220
8532
  * reverse-map. The drift comparator skips these so a templated value
8221
8533
  * doesn't fire guaranteed false drift on every clean run.
8222
8534
  *
8223
- * - `TimeToLiveSpecification`: cdkd's create() applies it via
8224
- * UpdateTimeToLive, but the reverse-mapping needs a separate
8225
- * DescribeTimeToLive call (not yet implemented).
8226
8535
  * - `WriteProvisionedThroughputSettings` /
8227
8536
  * `WriteOnDemandThroughputSettings`: CFn's shapes wrap
8228
8537
  * auto-scaling / on-demand max-RU settings whose reverse-mapping
8229
8538
  * from `DescribeTable.ProvisionedThroughput` / `OnDemandThroughput`
8230
8539
  * is non-trivial and would fire false drift in v1.
8540
+ *
8541
+ * `TimeToLiveSpecification` is reverse-mapped via `DescribeTimeToLive`
8542
+ * in `readCurrentState` (no longer in this list).
8231
8543
  */
8232
8544
  getDriftUnknownPaths(_resourceType) {
8233
- return [
8234
- "TimeToLiveSpecification",
8235
- "WriteProvisionedThroughputSettings",
8236
- "WriteOnDemandThroughputSettings"
8237
- ];
8545
+ return ["WriteProvisionedThroughputSettings", "WriteOnDemandThroughputSettings"];
8238
8546
  }
8239
8547
  /**
8240
8548
  * Adopt an existing DynamoDB GlobalTable into cdkd state.
@@ -8278,7 +8586,7 @@ var DynamoDBGlobalTableProvider = class {
8278
8586
  } while (exclusiveStartTableName);
8279
8587
  return null;
8280
8588
  }
8281
- async waitForTableActive(tableName, maxAttempts = 120) {
8589
+ async waitForTableActive(tableName, logicalId, maxAttempts = 120) {
8282
8590
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
8283
8591
  const response = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }));
8284
8592
  const status = response.Table?.TableStatus;
@@ -8288,10 +8596,10 @@ var DynamoDBGlobalTableProvider = class {
8288
8596
  tableId: response.Table?.TableId,
8289
8597
  streamArn: response.Table?.LatestStreamArn
8290
8598
  };
8291
- if (status !== "CREATING" && status !== "UPDATING") throw new Error(`Unexpected table status: ${status}`);
8599
+ if (status !== "CREATING" && status !== "UPDATING") throw new ProvisioningError(`Unexpected table status while waiting for ACTIVE on ${tableName}: ${status}`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
8292
8600
  await new Promise((resolve) => setTimeout(resolve, 1e3));
8293
8601
  }
8294
- throw new Error(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s`);
8602
+ throw new ProvisioningError(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
8295
8603
  }
8296
8604
  /**
8297
8605
  * Wait for the table to reach ACTIVE after an UpdateTable call. Unlike
@@ -8299,32 +8607,32 @@ var DynamoDBGlobalTableProvider = class {
8299
8607
  * table may already be ACTIVE on the no-op path (already-disabled
8300
8608
  * protection) or transition through UPDATING.
8301
8609
  */
8302
- async waitForTableActiveAfterUpdate(tableName, maxAttempts = 120) {
8610
+ async waitForTableActiveAfterUpdate(tableName, logicalId, maxAttempts = 600) {
8303
8611
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
8304
8612
  if ((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.TableStatus === "ACTIVE") return;
8305
8613
  await new Promise((resolve) => setTimeout(resolve, 1e3));
8306
8614
  }
8307
- throw new Error(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s after UpdateTable`);
8615
+ throw new ProvisioningError(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s after UpdateTable`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
8308
8616
  }
8309
8617
  /**
8310
8618
  * Wait until a specific replica's `ReplicaStatus` flips to ACTIVE.
8311
8619
  * Replica provisioning typically takes 1–5 min; cap at 10 min.
8312
8620
  */
8313
- async waitForReplicaActive(tableName, region, maxAttempts = 600) {
8621
+ async waitForReplicaActive(tableName, region, logicalId, maxAttempts = 600) {
8314
8622
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
8315
8623
  const replica = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas?.find((r) => r.RegionName === region);
8316
8624
  if (replica?.ReplicaStatus === "ACTIVE") return;
8317
8625
  this.logger.debug(`Replica ${region} status: ${replica?.ReplicaStatus} (attempt ${attempt}/${maxAttempts})`);
8318
8626
  await new Promise((resolve) => setTimeout(resolve, 1e3));
8319
8627
  }
8320
- throw new Error(`Replica ${region} for table ${tableName} did not reach ACTIVE within ${maxAttempts}s`);
8628
+ throw new ProvisioningError(`Replica ${region} for table ${tableName} did not reach ACTIVE within ${maxAttempts}s`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
8321
8629
  }
8322
8630
  /**
8323
8631
  * Wait until a specific replica disappears from `Replicas[]` after a
8324
8632
  * Delete replica update. Replica deletion typically takes 1–5 min;
8325
8633
  * cap at 10 min.
8326
8634
  */
8327
- async waitForReplicaGone(tableName, region, maxAttempts = 600) {
8635
+ async waitForReplicaGone(tableName, region, logicalId, maxAttempts = 600) {
8328
8636
  for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
8329
8637
  if (!(await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas?.find((r) => r.RegionName === region)) return;
8330
8638
  await new Promise((resolve) => setTimeout(resolve, 1e3));
@@ -8332,7 +8640,7 @@ var DynamoDBGlobalTableProvider = class {
8332
8640
  if (err instanceof ResourceNotFoundException$1) return;
8333
8641
  throw err;
8334
8642
  }
8335
- throw new Error(`Replica ${region} for table ${tableName} did not disappear within ${maxAttempts}s`);
8643
+ throw new ProvisioningError(`Replica ${region} for table ${tableName} did not disappear within ${maxAttempts}s`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
8336
8644
  }
8337
8645
  /**
8338
8646
  * Wait for `DescribeTable` to return `ResourceNotFoundException`,
@@ -8345,7 +8653,7 @@ var DynamoDBGlobalTableProvider = class {
8345
8653
  * Typical small-table delete completes in 5–30s; cap at 10 min for
8346
8654
  * worst-case large-table / replica-cascade scenarios.
8347
8655
  */
8348
- async waitForTableGone(tableName, maxAttempts = 600) {
8656
+ async waitForTableGone(tableName, logicalId, maxAttempts = 600) {
8349
8657
  for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
8350
8658
  await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }));
8351
8659
  await new Promise((resolve) => setTimeout(resolve, 1e3));
@@ -8353,9 +8661,104 @@ var DynamoDBGlobalTableProvider = class {
8353
8661
  if (err instanceof ResourceNotFoundException$1) return;
8354
8662
  throw err;
8355
8663
  }
8356
- throw new Error(`Table ${tableName} did not disappear within ${maxAttempts}s`);
8664
+ throw new ProvisioningError(`Table ${tableName} did not disappear within ${maxAttempts}s`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
8357
8665
  }
8358
8666
  };
8667
+ /**
8668
+ * Diff CFn `Replicas[]` arrays. Keyed by `Region`. Returns adds, removes,
8669
+ * and modifies (entries whose other keys — KMSMasterKeyId,
8670
+ * GlobalSecondaryIndexes, TableClassOverride — differ from the old shape).
8671
+ */
8672
+ /**
8673
+ * Derive the per-call `ProvisionedThroughput` shape required by
8674
+ * `CreateTableCommand` / `UpdateTableCommand` when BillingMode flips to
8675
+ * PROVISIONED. Shared between create() and the BillingMode-flip path in
8676
+ * update() so a user template's non-default read/write capacity is
8677
+ * preserved consistently across both code paths.
8678
+ *
8679
+ * Source of truth (CFn `AWS::DynamoDB::GlobalTable` shape):
8680
+ * - WriteCapacityUnits → `properties.WriteProvisionedThroughputSettings`
8681
+ * (top-level on the table). Literal `WriteCapacityUnits` wins over
8682
+ * auto-scaling `MinCapacity`; both default to 5 if absent.
8683
+ * - ReadCapacityUnits → `Replicas[?Region==<region>].ReadProvisionedThroughputSettings`
8684
+ * (per-replica, the deploy region's setting). Same literal-vs-auto-
8685
+ * scaling-vs-default-5 precedence.
8686
+ */
8687
+ function derivePerCallProvisionedThroughput(properties, region) {
8688
+ const wps = properties["WriteProvisionedThroughputSettings"];
8689
+ const writeAutoScaling = wps?.["WriteCapacityAutoScalingSettings"];
8690
+ const writeCapacity = Number(wps?.["WriteCapacityUnits"] ?? writeAutoScaling?.["MinCapacity"] ?? 5);
8691
+ const localReadSettings = (properties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["ReadProvisionedThroughputSettings"];
8692
+ const readAutoScaling = localReadSettings?.["ReadCapacityAutoScalingSettings"];
8693
+ return {
8694
+ ReadCapacityUnits: Number(localReadSettings?.["ReadCapacityUnits"] ?? readAutoScaling?.["MinCapacity"] ?? 5),
8695
+ WriteCapacityUnits: writeCapacity
8696
+ };
8697
+ }
8698
+ function diffReplicas(oldReplicas, newReplicas) {
8699
+ const oldByRegion = /* @__PURE__ */ new Map();
8700
+ const newByRegion = /* @__PURE__ */ new Map();
8701
+ for (const r of oldReplicas) {
8702
+ const region = r["Region"];
8703
+ if (region) oldByRegion.set(region, r);
8704
+ }
8705
+ for (const r of newReplicas) {
8706
+ const region = r["Region"];
8707
+ if (region) newByRegion.set(region, r);
8708
+ }
8709
+ const added = [];
8710
+ const removed = [];
8711
+ const modified = [];
8712
+ for (const [region, replica] of newByRegion) if (!oldByRegion.has(region)) added.push(replica);
8713
+ else if (!deepEqual$1(oldByRegion.get(region), replica)) modified.push(replica);
8714
+ for (const [region, replica] of oldByRegion) if (!newByRegion.has(region)) removed.push(replica);
8715
+ return {
8716
+ added,
8717
+ removed,
8718
+ modified
8719
+ };
8720
+ }
8721
+ /**
8722
+ * Diff CFn `GlobalSecondaryIndexes[]` arrays. Keyed by `IndexName`.
8723
+ * Modified = same IndexName but other fields (ProvisionedThroughput /
8724
+ * OnDemandThroughput) differ. KeySchema / Projection changes count as
8725
+ * "modified" too — AWS rejects those via UpdateGSI, but the diff caller
8726
+ * surfaces the AWS-side error rather than this helper second-guessing.
8727
+ */
8728
+ function diffGlobalSecondaryIndexes(oldGsi, newGsi) {
8729
+ const oldByName = /* @__PURE__ */ new Map();
8730
+ const newByName = /* @__PURE__ */ new Map();
8731
+ for (const g of oldGsi) if (g.IndexName) oldByName.set(g.IndexName, g);
8732
+ for (const g of newGsi) if (g.IndexName) newByName.set(g.IndexName, g);
8733
+ const added = [];
8734
+ const removed = [];
8735
+ const modified = [];
8736
+ for (const [name, gsi] of newByName) if (!oldByName.has(name)) added.push(gsi);
8737
+ else if (!deepEqual$1(oldByName.get(name), gsi)) modified.push(gsi);
8738
+ for (const [name, gsi] of oldByName) if (!newByName.has(name)) removed.push(gsi);
8739
+ return {
8740
+ added,
8741
+ removed,
8742
+ modified
8743
+ };
8744
+ }
8745
+ /**
8746
+ * Structural equality via JSON.stringify. Both inputs are CFn-shape
8747
+ * POJOs (no functions, no symbols, no cycles), so JSON round-trip is
8748
+ * sufficient and free of the property-order pitfalls of deeper
8749
+ * comparators. Object property order from `Object.keys` is insertion
8750
+ * order in modern engines; AWS-SDK shapes are constructed by the SDK
8751
+ * in stable order, so this is safe in practice.
8752
+ */
8753
+ function deepEqual$1(a, b) {
8754
+ if (a === b) return true;
8755
+ if (a === void 0 || b === void 0) return false;
8756
+ try {
8757
+ return JSON.stringify(a) === JSON.stringify(b);
8758
+ } catch {
8759
+ return false;
8760
+ }
8761
+ }
8359
8762
 
8360
8763
  //#endregion
8361
8764
  //#region src/provisioning/providers/logs-loggroup-provider.ts
@@ -17748,6 +18151,178 @@ var RDSProvider = class {
17748
18151
  }
17749
18152
  };
17750
18153
 
18154
+ //#endregion
18155
+ //#region src/provisioning/providers/rds-dbproxy-targetgroup-provider.ts
18156
+ /**
18157
+ * AWS RDS DBProxyTargetGroup Provider
18158
+ *
18159
+ * Implements resource provisioning for `AWS::RDS::DBProxyTargetGroup`.
18160
+ *
18161
+ * **Why a dedicated SDK provider** (per `feedback_dedicated_provider_over_special_case.md`):
18162
+ * pre-PR this type went through Cloud Control API, but CC API's resource
18163
+ * handler for `AWS::RDS::DBProxyTargetGroup` fails the delete path with
18164
+ * `Value null at 'dBProxyName' failed to satisfy constraint`. The handler
18165
+ * cannot derive `DBProxyName` from the TargetGroup ARN (the primary
18166
+ * identifier), so the underlying RDS API call goes out with
18167
+ * `DBProxyName: null` and AWS rejects it (Issue #385).
18168
+ *
18169
+ * **What this resource actually does on AWS**: a CFn
18170
+ * `AWS::RDS::DBProxyTargetGroup` is a wiring resource — every DBProxy gets
18171
+ * a default TargetGroup (`TargetGroupName: 'default'`) auto-created by AWS;
18172
+ * the CFn resource only manages target REGISTRATIONS
18173
+ * (`RegisterDBProxyTargets` / `DeregisterDBProxyTargets`) and the
18174
+ * connection pool config (`ModifyDBProxyTargetGroup`). The TargetGroup
18175
+ * object itself is not deleted on resource delete — it lives and dies with
18176
+ * the parent DBProxy.
18177
+ *
18178
+ * **Lifecycle**:
18179
+ * - `create`: optionally `ModifyDBProxyTargetGroup` (connection pool), then
18180
+ * `RegisterDBProxyTargets` (cluster IDs and / or instance IDs), then
18181
+ * `DescribeDBProxyTargetGroups` to recover the TargetGroupArn for state.
18182
+ * - `update`: rejected via `ResourceUpdateNotSupportedError` in MVP. The
18183
+ * per-property update surface (add/remove targets, pool config rewrites)
18184
+ * is a follow-up.
18185
+ * - `delete`: `DeregisterDBProxyTargets` for every registered target.
18186
+ * `DBProxyNotFoundFault` / `DBProxyTargetGroupNotFoundFault` /
18187
+ * `DBProxyTargetNotFoundFault` are treated as idempotent success
18188
+ * (region-match-gated) — the parent DBProxy may already have been
18189
+ * deleted by a sibling cdkd delete or by AWS CASCADE.
18190
+ * - `getAttribute`: `TargetGroupArn` returns the physicalId; `TargetGroupName`
18191
+ * returns `'default'`.
18192
+ *
18193
+ * **physicalId** = TargetGroupArn (matches the CFn `primaryIdentifier`).
18194
+ */
18195
+ var RDSDBProxyTargetGroupProvider = class {
18196
+ rdsClient;
18197
+ providerRegion = process.env["AWS_REGION"];
18198
+ logger = getLogger().child("RDSDBProxyTargetGroupProvider");
18199
+ handledProperties = new Map([["AWS::RDS::DBProxyTargetGroup", new Set([
18200
+ "DBProxyName",
18201
+ "TargetGroupName",
18202
+ "DBClusterIdentifiers",
18203
+ "DBInstanceIdentifiers",
18204
+ "ConnectionPoolConfigurationInfo"
18205
+ ])]]);
18206
+ getClient() {
18207
+ if (!this.rdsClient) this.rdsClient = new RDSClient(this.providerRegion ? { region: this.providerRegion } : {});
18208
+ return this.rdsClient;
18209
+ }
18210
+ async create(logicalId, resourceType, properties) {
18211
+ const dbProxyName = properties["DBProxyName"];
18212
+ if (!dbProxyName) throw new ProvisioningError(`DBProxyName is required for AWS::RDS::DBProxyTargetGroup ${logicalId}`, resourceType, logicalId);
18213
+ const targetGroupName = properties["TargetGroupName"] ?? "default";
18214
+ const dbClusterIdentifiers = properties["DBClusterIdentifiers"];
18215
+ const dbInstanceIdentifiers = properties["DBInstanceIdentifiers"];
18216
+ const connectionPoolConfig = properties["ConnectionPoolConfigurationInfo"];
18217
+ const client = this.getClient();
18218
+ if (connectionPoolConfig) {
18219
+ this.logger.debug(`Applying connection pool config to ${dbProxyName}/${targetGroupName}`);
18220
+ try {
18221
+ await client.send(new ModifyDBProxyTargetGroupCommand({
18222
+ DBProxyName: dbProxyName,
18223
+ TargetGroupName: targetGroupName,
18224
+ ConnectionPoolConfig: connectionPoolConfig
18225
+ }));
18226
+ } catch (error) {
18227
+ throw this.wrapError(error, "CREATE (pool config)", resourceType, logicalId, void 0);
18228
+ }
18229
+ }
18230
+ if (dbClusterIdentifiers && dbClusterIdentifiers.length > 0 || dbInstanceIdentifiers && dbInstanceIdentifiers.length > 0) {
18231
+ this.logger.debug(`Registering targets for ${dbProxyName}/${targetGroupName}: clusters=[${dbClusterIdentifiers?.join(",") ?? ""}], instances=[${dbInstanceIdentifiers?.join(",") ?? ""}]`);
18232
+ try {
18233
+ await client.send(new RegisterDBProxyTargetsCommand({
18234
+ DBProxyName: dbProxyName,
18235
+ TargetGroupName: targetGroupName,
18236
+ DBClusterIdentifiers: dbClusterIdentifiers,
18237
+ DBInstanceIdentifiers: dbInstanceIdentifiers
18238
+ }));
18239
+ } catch (error) {
18240
+ throw this.wrapError(error, "CREATE (register targets)", resourceType, logicalId, void 0);
18241
+ }
18242
+ }
18243
+ let targetGroupArn;
18244
+ try {
18245
+ targetGroupArn = (await client.send(new DescribeDBProxyTargetGroupsCommand({
18246
+ DBProxyName: dbProxyName,
18247
+ TargetGroupName: targetGroupName
18248
+ }))).TargetGroups?.[0]?.TargetGroupArn;
18249
+ } catch (error) {
18250
+ throw this.wrapError(error, "CREATE (describe)", resourceType, logicalId, void 0);
18251
+ }
18252
+ if (!targetGroupArn) throw new ProvisioningError(`Failed to recover TargetGroupArn for ${dbProxyName}/${targetGroupName} after create`, resourceType, logicalId);
18253
+ return {
18254
+ physicalId: targetGroupArn,
18255
+ attributes: {
18256
+ TargetGroupArn: targetGroupArn,
18257
+ TargetGroupName: targetGroupName
18258
+ }
18259
+ };
18260
+ }
18261
+ async update(logicalId, _physicalId, resourceType, _oldProperties, _newProperties) {
18262
+ throw new ResourceUpdateNotSupportedError(resourceType, logicalId, "destroy + redeploy; in-place updates of registered targets / connection pool config are not yet supported");
18263
+ }
18264
+ async delete(logicalId, physicalId, resourceType, properties, context) {
18265
+ const props = properties ?? {};
18266
+ const dbProxyName = props["DBProxyName"];
18267
+ const targetGroupName = props["TargetGroupName"] ?? "default";
18268
+ const dbClusterIdentifiers = props["DBClusterIdentifiers"];
18269
+ const dbInstanceIdentifiers = props["DBInstanceIdentifiers"];
18270
+ if (!dbProxyName) throw new ProvisioningError(`DBProxyName missing from state.properties for AWS::RDS::DBProxyTargetGroup ${logicalId}; cannot deregister targets. Manually run: aws rds deregister-db-proxy-targets --db-proxy-name <proxy-name> --target-group-name ${targetGroupName} ...`, resourceType, logicalId, physicalId);
18271
+ if (!(dbClusterIdentifiers && dbClusterIdentifiers.length > 0 || dbInstanceIdentifiers && dbInstanceIdentifiers.length > 0)) {
18272
+ this.logger.debug(`No targets registered for ${dbProxyName}/${targetGroupName}; nothing to deregister`);
18273
+ return;
18274
+ }
18275
+ this.logger.debug(`Deregistering targets from ${dbProxyName}/${targetGroupName}: clusters=[${dbClusterIdentifiers?.join(",") ?? ""}], instances=[${dbInstanceIdentifiers?.join(",") ?? ""}]`);
18276
+ try {
18277
+ await this.getClient().send(new DeregisterDBProxyTargetsCommand({
18278
+ DBProxyName: dbProxyName,
18279
+ TargetGroupName: targetGroupName,
18280
+ DBClusterIdentifiers: dbClusterIdentifiers,
18281
+ DBInstanceIdentifiers: dbInstanceIdentifiers
18282
+ }));
18283
+ } catch (error) {
18284
+ if (error instanceof DBProxyNotFoundFault || error instanceof DBProxyTargetGroupNotFoundFault || error instanceof DBProxyTargetNotFoundFault) {
18285
+ assertRegionMatch(await this.getClient().config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
18286
+ this.logger.debug(`${dbProxyName}/${targetGroupName} or its target is already gone, treating as success`);
18287
+ return;
18288
+ }
18289
+ throw this.wrapError(error, "DELETE", resourceType, logicalId, physicalId);
18290
+ }
18291
+ }
18292
+ async getAttribute(physicalId, _resourceType, attributeName) {
18293
+ switch (attributeName) {
18294
+ case "TargetGroupArn": return physicalId;
18295
+ case "TargetGroupName": return "default";
18296
+ default:
18297
+ this.logger.warn(`Unknown attribute ${attributeName} for AWS::RDS::DBProxyTargetGroup, returning undefined`);
18298
+ return;
18299
+ }
18300
+ }
18301
+ /**
18302
+ * Adopt an existing DBProxyTargetGroup into cdkd state.
18303
+ *
18304
+ * **Explicit override only.** The TargetGroup itself has no tags
18305
+ * (the parent DBProxy carries the cdkd path tag, not the wiring child),
18306
+ * so there is no `aws:cdk:path`-based auto-lookup. Users must pass
18307
+ * `--resource <logicalId>=<TargetGroupArn>`.
18308
+ */
18309
+ async import(input) {
18310
+ if (input.knownPhysicalId) return {
18311
+ physicalId: input.knownPhysicalId,
18312
+ attributes: {
18313
+ TargetGroupArn: input.knownPhysicalId,
18314
+ TargetGroupName: "default"
18315
+ }
18316
+ };
18317
+ return null;
18318
+ }
18319
+ wrapError(error, op, resourceType, logicalId, physicalId) {
18320
+ const message = error instanceof Error ? error.message : String(error);
18321
+ const cause = error instanceof Error ? error : void 0;
18322
+ return new ProvisioningError(`${op} failed for ${logicalId}: ${message}`, resourceType, logicalId, physicalId, cause);
18323
+ }
18324
+ };
18325
+
17751
18326
  //#endregion
17752
18327
  //#region src/provisioning/providers/docdb-provider.ts
17753
18328
  /**
@@ -28473,6 +29048,7 @@ function registerAllProviders(registry) {
28473
29048
  registry.register("AWS::RDS::DBSubnetGroup", rdsProvider);
28474
29049
  registry.register("AWS::RDS::DBCluster", rdsProvider);
28475
29050
  registry.register("AWS::RDS::DBInstance", rdsProvider);
29051
+ registry.register("AWS::RDS::DBProxyTargetGroup", new RDSDBProxyTargetGroupProvider());
28476
29052
  const docdbProvider = new DocDBProvider();
28477
29053
  registry.register("AWS::DocDB::DBSubnetGroup", docdbProvider);
28478
29054
  registry.register("AWS::DocDB::DBCluster", docdbProvider);
@@ -43452,7 +44028,7 @@ function reorderArgs(argv) {
43452
44028
  */
43453
44029
  async function main() {
43454
44030
  const program = new Command();
43455
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.103.1");
44031
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.104.0");
43456
44032
  program.addCommand(createBootstrapCommand());
43457
44033
  program.addCommand(createSynthCommand());
43458
44034
  program.addCommand(createListCommand());