@go-to-k/cdkd 0.278.30 → 0.278.32

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as withRetry, $t as getAwsClients, A as red, At as synthesisStatusMessage, B as isTerminationProtectionPropagationError, Bt as stateBucketExistenceConfirmed, C as isStatefulRecreateTargetSync, Cn as normalizeAwsError, Ct as formatDockerLoginError, D as cyan, Dt as AssetManifestLoader, E as bold, Et as runDockerStreaming, F as ProviderRegistry, Ft as resolveCaptureObservedState, G as normalizeAwsTagsToCfn, Gt as findLargeInlineResources, H as cfnRefValueFromPhysicalId, Ht as CFN_TEMPLATE_BODY_LIMIT, I as findActionableSilentDrops, It as resolveSkipPrefix, J as readConfigString, Jt as AssemblyReader, K as resolveExplicitPhysicalId, Kt as uploadCfnTemplate, L as CloudControlProvider, Lt as resolveStateBucketWithDefault, M as IAMRoleProvider, Mt as getLegacyStateBucketName, N as collectInlinePolicyNamesManagedBySiblings, Nt as resolveApp, O as gray, Ot as getDockerImageBySourceHash, P as clearOnUpdateRemoval, Pt as resolveAutoAssetStorage, Q as describeTypeWithThrottleRetry, Qt as AwsClients, R as slowCcOperationTimeoutMs, Rt as resolveStateBucketWithDefaultAndSource, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, St as buildDockerImage, T as formatResourceLine, Tt as runDockerForeground, U as refStateLookupFromResource, Ut as CFN_TEMPLATE_URL_LIMIT, V as IntrinsicFunctionResolver, Vt as warnDeprecatedNoPrefixCliFlag, W as WAFv2WebACLProvider, Wt as MIGRATE_TMP_PREFIX, X as applyRoleArnIfSet, Y as requireConfigString, Yt as processStackMessages, Z as DiffCalculator, Zt as resolveBucketRegion, _ as createPreDeleteFinalSnapshot, _n as StackHasActiveImportsError, _t as ensureAssetStorage, a as DeploymentEventsStore, at as S3StateBackend, b as unsupportedFinalSnapshotError, bt as validateAssetBucketName, c as replayFailedOperations, cn as LocalMigrateError, ct as AssetPublisher, d as IMPLICIT_DELETE_DEPENDENCIES, dn as MissingCdkCliError, dt as buildAssetRedirectMap, en as resetAwsClients, et as isRetryableTransientError, f as computeImplicitDeleteEdges, fn as NestedStackChildDirectDestroyError, ft as createAssetRedirectResolver, g as ccRoutedFinalSnapshotError, gn as ResourceUpdateNotSupportedError, gt as BOOTSTRAP_MARKER_PREFIX, h as buildFinalSnapshotIdentifier, hn as ResourceTimeoutError, ht as AssetModeResolver, i as DeploymentEventsReader, it as LockManager, j as yellow, jt as getDefaultStateBucketName, k as green, kt as Synthesizer, l as replayRollback, ln as LocalStartServiceError, lt as stringifyValue, m as PRE_DELETE_SNAPSHOT_TYPES, mn as ProvisioningError, mt as rewriteTemplateAssetReferences, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as DagBuilder, o as planFailedOps, on as DeployCancelledError, ot as rebuildClientForBucketRegion, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as PartialFailureError, pt as loadPublishableAssetManifest, q as assertRegionMatch, qt as expectedOwnerParam, r as DeployEngine, rn as CdkdError, rt as TemplateParser, s as planRollback, sn as LocalInvokeBuildError$1, st as shouldRetainResource, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as setAwsClients, tt as isThrottlingError, u as withResourceDeadline, ut as WorkGraph, v as isFinalSnapshotError, vn as StackTerminationProtectionError, vt as getBootstrapMarkerKey, w as renderStatefulReason, wn as withErrorHandling, wt as getDockerCmd, x as extractDeploymentEventError, xt as validateContainerRepoName, y as refusesFinalSnapshot, yt as parseBootstrapMarker, z as disableInstanceApiTermination, zt as resolveUseCdkBootstrapAssets } from "./deploy-engine-BLIg6xfm.js";
2
+ import { $ as withRetry, $t as getAwsClients, A as red, At as synthesisStatusMessage, B as isTerminationProtectionPropagationError, Bt as stateBucketExistenceConfirmed, C as isStatefulRecreateTargetSync, Cn as normalizeAwsError, Ct as formatDockerLoginError, D as cyan, Dt as AssetManifestLoader, E as bold, Et as runDockerStreaming, F as ProviderRegistry, Ft as resolveCaptureObservedState, G as normalizeAwsTagsToCfn, Gt as findLargeInlineResources, H as cfnRefValueFromPhysicalId, Ht as CFN_TEMPLATE_BODY_LIMIT, I as findActionableSilentDrops, It as resolveSkipPrefix, J as readConfigString, Jt as AssemblyReader, K as resolveExplicitPhysicalId, Kt as uploadCfnTemplate, L as CloudControlProvider, Lt as resolveStateBucketWithDefault, M as IAMRoleProvider, Mt as getLegacyStateBucketName, N as collectInlinePolicyNamesManagedBySiblings, Nt as resolveApp, O as gray, Ot as getDockerImageBySourceHash, P as clearOnUpdateRemoval, Pt as resolveAutoAssetStorage, Q as describeTypeWithThrottleRetry, Qt as AwsClients, R as slowCcOperationTimeoutMs, Rt as resolveStateBucketWithDefaultAndSource, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, St as buildDockerImage, T as formatResourceLine, Tt as runDockerForeground, U as refStateLookupFromResource, Ut as CFN_TEMPLATE_URL_LIMIT, V as IntrinsicFunctionResolver, Vt as warnDeprecatedNoPrefixCliFlag, W as WAFv2WebACLProvider, Wt as MIGRATE_TMP_PREFIX, X as applyRoleArnIfSet, Y as requireConfigString, Yt as processStackMessages, Z as DiffCalculator, Zt as resolveBucketRegion, _ as createPreDeleteFinalSnapshot, _n as StackHasActiveImportsError, _t as ensureAssetStorage, a as DeploymentEventsStore, at as S3StateBackend, b as unsupportedFinalSnapshotError, bt as validateAssetBucketName, c as replayFailedOperations, cn as LocalMigrateError, ct as AssetPublisher, d as IMPLICIT_DELETE_DEPENDENCIES, dn as MissingCdkCliError, dt as buildAssetRedirectMap, en as resetAwsClients, et as isRetryableTransientError, f as computeImplicitDeleteEdges, fn as NestedStackChildDirectDestroyError, ft as createAssetRedirectResolver, g as ccRoutedFinalSnapshotError, gn as ResourceUpdateNotSupportedError, gt as BOOTSTRAP_MARKER_PREFIX, h as buildFinalSnapshotIdentifier, hn as ResourceTimeoutError, ht as AssetModeResolver, i as DeploymentEventsReader, it as LockManager, j as yellow, jt as getDefaultStateBucketName, k as green, kt as Synthesizer, l as replayRollback, ln as LocalStartServiceError, lt as stringifyValue, m as PRE_DELETE_SNAPSHOT_TYPES, mn as ProvisioningError, mt as rewriteTemplateAssetReferences, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as DagBuilder, o as planFailedOps, on as DeployCancelledError, ot as rebuildClientForBucketRegion, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as PartialFailureError, pt as loadPublishableAssetManifest, q as assertRegionMatch, qt as expectedOwnerParam, r as DeployEngine, rn as CdkdError, rt as TemplateParser, s as planRollback, sn as LocalInvokeBuildError$1, st as shouldRetainResource, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as setAwsClients, tt as isThrottlingError, u as withResourceDeadline, ut as WorkGraph, v as isFinalSnapshotError, vn as StackTerminationProtectionError, vt as getBootstrapMarkerKey, w as renderStatefulReason, wn as withErrorHandling, wt as getDockerCmd, x as extractDeploymentEventError, xt as validateContainerRepoName, y as refusesFinalSnapshot, yt as parseBootstrapMarker, z as disableInstanceApiTermination, zt as resolveUseCdkBootstrapAssets } from "./deploy-engine-CEVFHtcP.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as withSkipPrefix, i as runStackBuffered, n as getLogger, o as PATTERN_B_NAME_OPTIONS, p as withStackName, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-BYMEE-BS.js";
4
- import { t as ASGProvider } from "./asg-provider-CfVwvyCi.js";
4
+ import { t as ASGProvider } from "./asg-provider-DLlrKwbd.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -12328,9 +12328,10 @@ var DynamoDBGlobalTableProvider = class {
12328
12328
  StreamViewType: "NEW_AND_OLD_IMAGES"
12329
12329
  };
12330
12330
  }
12331
+ const diagnostics = [];
12331
12332
  let sdkIndexes;
12332
12333
  try {
12333
- sdkIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, billingMode);
12334
+ sdkIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, billingMode, "min", diagnostics);
12334
12335
  } catch (error) {
12335
12336
  throw new ProvisioningError(error instanceof Error ? error.message : String(error), resourceType, logicalId, void 0, error instanceof Error ? error : void 0);
12336
12337
  }
@@ -12345,10 +12346,21 @@ var DynamoDBGlobalTableProvider = class {
12345
12346
  const dpeResolved = extractLocalDeletionProtection(properties, currentRegion);
12346
12347
  if (dpeResolved !== void 0) createParams.DeletionProtectionEnabled = dpeResolved;
12347
12348
  if (properties["TableClass"]) createParams.TableClass = properties["TableClass"];
12348
- const wodts = properties["WriteOnDemandThroughputSettings"];
12349
- if (wodts?.["MaxWriteRequestUnits"] !== void 0) createParams.OnDemandThroughput = { MaxWriteRequestUnits: Number(wodts["MaxWriteRequestUnits"]) };
12349
+ const createCeilings = collectTableOnDemandCeilings(properties, currentRegion, diagnostics);
12350
+ const createOnDemand = {
12351
+ ...createCeilings.read.value !== void 0 && { MaxReadRequestUnits: createCeilings.read.value },
12352
+ ...createCeilings.write.value !== void 0 && { MaxWriteRequestUnits: createCeilings.write.value }
12353
+ };
12354
+ if (billingMode !== "PROVISIONED" && Object.keys(createOnDemand).length > 0) createParams.OnDemandThroughput = createOnDemand;
12350
12355
  const localReplicaTags = replicas.find((r) => r["Region"] === currentRegion)?.["Tags"];
12351
12356
  if (localReplicaTags && localReplicaTags.length > 0) createParams.Tags = localReplicaTags;
12357
+ for (const replica of replicas) {
12358
+ const replicaRegion = replica["Region"];
12359
+ if (!replicaRegion || replicaRegion === currentRegion) continue;
12360
+ toSdkReplicaGlobalSecondaryIndexes(replica["GlobalSecondaryIndexes"], billingMode, diagnostics);
12361
+ toSdkReplicaThroughputOverrides(replica, billingMode, diagnostics, {});
12362
+ }
12363
+ this.reportThroughputDiagnostics(diagnostics, logicalId, tableName);
12352
12364
  try {
12353
12365
  await this.dynamoDBClient.send(new CreateTableCommand(createParams));
12354
12366
  this.logger.debug(`CreateTable initiated for ${tableName}, waiting for ACTIVE`);
@@ -12361,7 +12373,7 @@ var DynamoDBGlobalTableProvider = class {
12361
12373
  for (const replica of replicas) {
12362
12374
  const region = replica["Region"];
12363
12375
  if (!region || region === currentRegion) continue;
12364
- await this.addReplica(tableName, replica, region, logicalId);
12376
+ await this.addReplica(tableName, replica, region, logicalId, billingMode);
12365
12377
  }
12366
12378
  for (const replica of replicas) {
12367
12379
  const region = replica["Region"];
@@ -12425,6 +12437,24 @@ var DynamoDBGlobalTableProvider = class {
12425
12437
  }
12426
12438
  }
12427
12439
  /**
12440
+ * Log a batch of {@link ThroughputDiagnostic}s at WARN, de-duplicated.
12441
+ *
12442
+ * The translators run over every index on both the create and the update
12443
+ * path, so the same malformed block can be reported more than once in one
12444
+ * call; a repeated warning reads as repeated damage and is worse than
12445
+ * useless. Kept as ONE formatter so the five translation call sites cannot
12446
+ * word the same defect differently.
12447
+ */
12448
+ reportThroughputDiagnostics(diagnostics, logicalId, physicalId) {
12449
+ const seen = /* @__PURE__ */ new Set();
12450
+ for (const diagnostic of diagnostics) {
12451
+ const line = `DynamoDB GlobalTable ${logicalId}: ${diagnostic.indexName ? `GSI '${diagnostic.indexName}' on ${physicalId}` : physicalId}: ${diagnostic.message}`;
12452
+ if (seen.has(line)) continue;
12453
+ seen.add(line);
12454
+ this.logger.warn(line);
12455
+ }
12456
+ }
12457
+ /**
12428
12458
  * Add a single replica region. Issues one `UpdateTableCommand` with
12429
12459
  * `ReplicaUpdates: [{ Create: { RegionName, ... } }]` and polls
12430
12460
  * `DescribeTable` until the replica's `ReplicaStatus` flips to ACTIVE.
@@ -12445,11 +12475,14 @@ var DynamoDBGlobalTableProvider = class {
12445
12475
  * is the granularity cdkd's pre-flight property-coverage check works at;
12446
12476
  * these nested keys are tracked as a known gap, not a supported surface.
12447
12477
  */
12448
- async addReplica(tableName, replica, region, logicalId) {
12478
+ async addReplica(tableName, replica, region, logicalId, billingMode, diagnostics) {
12449
12479
  const create = { RegionName: region };
12450
12480
  if (replica["KMSMasterKeyId"]) create.KMSMasterKeyId = replica["KMSMasterKeyId"];
12451
- const replicaIndexes = toSdkReplicaGlobalSecondaryIndexes(replica["GlobalSecondaryIndexes"]);
12481
+ const replicaIndexes = toSdkReplicaGlobalSecondaryIndexes(replica["GlobalSecondaryIndexes"], billingMode, diagnostics);
12452
12482
  if (replicaIndexes) create.GlobalSecondaryIndexes = replicaIndexes;
12483
+ const tableOverrides = toSdkReplicaThroughputOverrides(replica, billingMode, diagnostics, {});
12484
+ if (tableOverrides.ProvisionedThroughputOverride) create.ProvisionedThroughputOverride = tableOverrides.ProvisionedThroughputOverride;
12485
+ if (tableOverrides.OnDemandThroughputOverride) create.OnDemandThroughputOverride = tableOverrides.OnDemandThroughputOverride;
12453
12486
  if (replica["TableClassOverride"]) create.TableClassOverride = replica["TableClassOverride"];
12454
12487
  const replicaUpdates = [{ Create: create }];
12455
12488
  await this.dynamoDBClient.send(new UpdateTableCommand({
@@ -12494,8 +12527,9 @@ var DynamoDBGlobalTableProvider = class {
12494
12527
  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);
12495
12528
  const oldAttrs = previousProperties["AttributeDefinitions"] ?? [];
12496
12529
  const newAttrs = properties["AttributeDefinitions"] ?? [];
12497
- const removedAttrs = oldAttrs.filter((o) => !newAttrs.some((n) => n.AttributeName === o.AttributeName));
12498
- 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);
12530
+ const desiredKeyAttrNames = collectDesiredKeyAttributeNames(properties);
12531
+ const removedButStillReferenced = oldAttrs.filter((o) => !newAttrs.some((n) => n.AttributeName === o.AttributeName)).filter((a) => a.AttributeName !== void 0 && desiredKeyAttrNames.has(a.AttributeName));
12532
+ if (removedButStillReferenced.length > 0) throw new ProvisioningError(`AttributeDefinitions removals are immutable on AWS::DynamoDB::GlobalTable while a key schema still references them (offenders: ${removedButStillReferenced.map((a) => a.AttributeName).join(", ")}); replacement required`, resourceType, logicalId, physicalId);
12499
12533
  const currentRegion = await this.dynamoDBClient.config.region() ?? "";
12500
12534
  try {
12501
12535
  await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
@@ -12508,6 +12542,16 @@ var DynamoDBGlobalTableProvider = class {
12508
12542
  const oldBilling = previousProperties["BillingMode"] ?? "PAY_PER_REQUEST";
12509
12543
  const newBilling = properties["BillingMode"] ?? "PAY_PER_REQUEST";
12510
12544
  const onDemandCeilingLive = oldBilling === "PAY_PER_REQUEST" && newBilling === "PAY_PER_REQUEST";
12545
+ const diagnostics = [];
12546
+ const desiredCeilings = collectTableOnDemandCeilings(properties, currentRegion, diagnostics);
12547
+ const desiredSdkIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling, "min", diagnostics);
12548
+ for (const replica of properties["Replicas"] ?? []) {
12549
+ const replicaRegion = asRecord$1(replica)?.["Region"];
12550
+ if (typeof replicaRegion !== "string" || replicaRegion === currentRegion) continue;
12551
+ toSdkReplicaGlobalSecondaryIndexes(replica["GlobalSecondaryIndexes"], newBilling, diagnostics);
12552
+ toSdkReplicaThroughputOverrides(replica, newBilling, diagnostics, {});
12553
+ }
12554
+ this.reportThroughputDiagnostics(diagnostics, logicalId, physicalId);
12511
12555
  const newDpe = extractLocalDeletionProtection(properties, currentRegion);
12512
12556
  const oldDpe = extractLocalDeletionProtection(previousProperties, currentRegion);
12513
12557
  const awsDpe = describeResp.Table?.DeletionProtectionEnabled;
@@ -12537,17 +12581,23 @@ var DynamoDBGlobalTableProvider = class {
12537
12581
  };
12538
12582
  flatChanged = true;
12539
12583
  }
12540
- if (!deepEqual$1(properties["WriteOnDemandThroughputSettings"], previousProperties["WriteOnDemandThroughputSettings"])) {
12541
- const wodts = properties["WriteOnDemandThroughputSettings"];
12542
- const previousWodts = previousProperties["WriteOnDemandThroughputSettings"];
12543
- const rawMaxWrite = wodts?.["MaxWriteRequestUnits"];
12544
- if (rawMaxWrite !== void 0) {
12545
- flatUpdate.OnDemandThroughput = { MaxWriteRequestUnits: Number(rawMaxWrite) };
12546
- flatChanged = true;
12547
- } else if (onDemandCeilingLive && previousWodts?.["MaxWriteRequestUnits"] !== void 0) {
12548
- flatUpdate.OnDemandThroughput = { MaxWriteRequestUnits: ON_DEMAND_LIMIT_RESET };
12549
- flatChanged = true;
12584
+ const awsOnDemand = describeResp.Table?.OnDemandThroughput;
12585
+ const onDemandPayload = {};
12586
+ const resolveCeiling = (desired, live) => {
12587
+ if (desired.declared) {
12588
+ if (desired.value === void 0) return void 0;
12589
+ return desired.value !== live ? desired.value : void 0;
12550
12590
  }
12591
+ if (onDemandCeilingLive && live !== void 0) return ON_DEMAND_LIMIT_RESET;
12592
+ };
12593
+ const nextMaxRead = resolveCeiling(desiredCeilings.read, awsOnDemand?.MaxReadRequestUnits);
12594
+ const nextMaxWrite = resolveCeiling(desiredCeilings.write, awsOnDemand?.MaxWriteRequestUnits);
12595
+ if (nextMaxRead !== void 0) onDemandPayload.MaxReadRequestUnits = nextMaxRead;
12596
+ if (nextMaxWrite !== void 0) onDemandPayload.MaxWriteRequestUnits = nextMaxWrite;
12597
+ const deferCeilingsPastFlip = oldBilling === "PROVISIONED" && newBilling === "PAY_PER_REQUEST" && Object.keys(onDemandPayload).length > 0;
12598
+ if (newBilling !== "PROVISIONED" && !deferCeilingsPastFlip && Object.keys(onDemandPayload).length > 0) {
12599
+ flatUpdate.OnDemandThroughput = onDemandPayload;
12600
+ flatChanged = true;
12551
12601
  }
12552
12602
  if (flatChanged) {
12553
12603
  await this.dynamoDBClient.send(new UpdateTableCommand(flatUpdate));
@@ -12591,6 +12641,13 @@ var DynamoDBGlobalTableProvider = class {
12591
12641
  await this.dynamoDBClient.send(new UpdateTableCommand(billingUpdate));
12592
12642
  await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
12593
12643
  }
12644
+ if (deferCeilingsPastFlip) {
12645
+ await this.dynamoDBClient.send(new UpdateTableCommand({
12646
+ TableName: physicalId,
12647
+ OnDemandThroughput: onDemandPayload
12648
+ }));
12649
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
12650
+ }
12594
12651
  const writeAutoScalingOld = (previousProperties["WriteProvisionedThroughputSettings"] ?? {})["WriteCapacityAutoScalingSettings"];
12595
12652
  const writeAutoScalingNew = (properties["WriteProvisionedThroughputSettings"] ?? {})["WriteCapacityAutoScalingSettings"];
12596
12653
  const effectiveNewAutoScaling = oldBilling === "PROVISIONED" && newBilling === "PAY_PER_REQUEST" ? void 0 : writeAutoScalingNew;
@@ -12624,7 +12681,7 @@ var DynamoDBGlobalTableProvider = class {
12624
12681
  for (const replica of replicaDiff.added) {
12625
12682
  const region = replica["Region"];
12626
12683
  if (!region || region === currentRegion) continue;
12627
- await this.addReplica(physicalId, replica, region, logicalId);
12684
+ await this.addReplica(physicalId, replica, region, logicalId, newBilling);
12628
12685
  const newReplicaTags = replica["Tags"];
12629
12686
  await this.applyCrossRegionReplicaTagsDiff(tableArn, region, void 0, newReplicaTags, physicalId);
12630
12687
  const newReadAutoScaling = (replica["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
@@ -12657,10 +12714,20 @@ var DynamoDBGlobalTableProvider = class {
12657
12714
  }
12658
12715
  const updateAction = { RegionName: region };
12659
12716
  if (replica["KMSMasterKeyId"] !== void 0) updateAction.KMSMasterKeyId = replica["KMSMasterKeyId"];
12660
- const replicaIndexes = toSdkReplicaGlobalSecondaryIndexes(replica["GlobalSecondaryIndexes"]);
12717
+ const replicaIndexes = toSdkReplicaGlobalSecondaryIndexes(replica["GlobalSecondaryIndexes"], newBilling);
12661
12718
  if (replicaIndexes) updateAction.GlobalSecondaryIndexes = replicaIndexes;
12719
+ const replicaOverrides = toSdkReplicaThroughputOverrides(replica, newBilling, void 0, {});
12720
+ if (replicaOverrides.ProvisionedThroughputOverride) updateAction.ProvisionedThroughputOverride = replicaOverrides.ProvisionedThroughputOverride;
12721
+ if (replicaOverrides.OnDemandThroughputOverride) updateAction.OnDemandThroughputOverride = replicaOverrides.OnDemandThroughputOverride;
12722
+ if (oldBilling === newBilling) {
12723
+ const oldReplicaOverrides = toSdkReplicaThroughputOverrides(oldReplica ?? {}, oldBilling, void 0, {});
12724
+ const droppedOverrides = [];
12725
+ if (oldReplicaOverrides.OnDemandThroughputOverride && !replicaOverrides.OnDemandThroughputOverride) droppedOverrides.push("ReadOnDemandThroughputSettings");
12726
+ if (oldReplicaOverrides.ProvisionedThroughputOverride && !replicaOverrides.ProvisionedThroughputOverride) droppedOverrides.push("ReadProvisionedThroughputSettings");
12727
+ if (droppedOverrides.length > 0) this.logger.warn(`Cross-region replica ${region} of ${physicalId}: the template no longer declares ${droppedOverrides.join(" / ")}, but DynamoDB offers no way to CLEAR a replica-level throughput override — a -1 sentinel is stored literally, and an empty override block wedges the table in UPDATING (both live-probed, issue #1436). The previous override is STILL IN EFFECT on AWS. Set an explicit value, or remove and re-add the replica, to change it.`);
12728
+ }
12662
12729
  if (replica["TableClassOverride"]) updateAction.TableClassOverride = replica["TableClassOverride"];
12663
- if (!(updateAction.KMSMasterKeyId !== void 0 || updateAction.GlobalSecondaryIndexes !== void 0 || updateAction.TableClassOverride !== void 0)) {
12730
+ if (!(updateAction.KMSMasterKeyId !== void 0 || updateAction.GlobalSecondaryIndexes !== void 0 || updateAction.TableClassOverride !== void 0 || updateAction.ProvisionedThroughputOverride !== void 0 || updateAction.OnDemandThroughputOverride !== void 0)) {
12664
12731
  this.logger.debug(`Cross-region replica ${region} of ${physicalId}: only Tags-style changes detected; UpdateReplica skipped (AWS rejects empty Update actions). Tags propagation handled above via per-region client.`);
12665
12732
  continue;
12666
12733
  }
@@ -12671,7 +12738,7 @@ var DynamoDBGlobalTableProvider = class {
12671
12738
  await this.waitForReplicaActive(physicalId, region, logicalId);
12672
12739
  }
12673
12740
  const previousSdkIndexes = toSdkGlobalSecondaryIndexes(previousProperties, currentRegion, oldBilling);
12674
- const gsiDiff = diffGlobalSecondaryIndexes(previousSdkIndexes, toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling));
12741
+ const gsiDiff = diffGlobalSecondaryIndexes(previousSdkIndexes, desiredSdkIndexes);
12675
12742
  const rawOnDemandDeclarations = collectRawOnDemandDeclarations(properties, currentRegion);
12676
12743
  const previousSdkByName = /* @__PURE__ */ new Map();
12677
12744
  for (const prev of previousSdkIndexes) if (prev.IndexName) previousSdkByName.set(prev.IndexName, prev);
@@ -12716,7 +12783,6 @@ var DynamoDBGlobalTableProvider = class {
12716
12783
  if (previousOnDemand.MaxWriteRequestUnits !== void 0 && onDemand.MaxWriteRequestUnits === void 0) if (declared?.writeDeclared) unresolvedMembers.push("MaxWriteRequestUnits");
12717
12784
  else onDemand.MaxWriteRequestUnits = ON_DEMAND_LIMIT_RESET;
12718
12785
  }
12719
- if (unresolvedMembers.length > 0) this.logger.warn(`GSI '${gsi.IndexName}' on ${physicalId}: the template declares ${unresolvedMembers.join(" / ")} but the value did not resolve to a number, so the on-demand limit was left UNCHANGED on AWS rather than applied or cleared. This is usually an unresolved intrinsic in the template; resolve it to a literal to change the ceiling, or remove the property entirely to clear it.`);
12720
12786
  if (Object.keys(onDemand).length > 0) update.OnDemandThroughput = onDemand;
12721
12787
  if (!billingFlipped && gsi.ProvisionedThroughput) update.ProvisionedThroughput = gsi.ProvisionedThroughput;
12722
12788
  const previousSdk = previousSdkByName.get(gsi.IndexName);
@@ -13279,7 +13345,6 @@ var DynamoDBGlobalTableProvider = class {
13279
13345
  if (table.AttributeDefinitions) result["AttributeDefinitions"] = table.AttributeDefinitions;
13280
13346
  const billingMode = table.BillingModeSummary?.BillingMode;
13281
13347
  if (billingMode) result["BillingMode"] = billingMode;
13282
- if (table.GlobalSecondaryIndexes && table.GlobalSecondaryIndexes.length > 0) result["GlobalSecondaryIndexes"] = table.GlobalSecondaryIndexes;
13283
13348
  if (table.LocalSecondaryIndexes && table.LocalSecondaryIndexes.length > 0) result["LocalSecondaryIndexes"] = table.LocalSecondaryIndexes;
13284
13349
  if (table.StreamSpecification?.StreamEnabled && table.StreamSpecification.StreamViewType) result["StreamSpecification"] = {
13285
13350
  StreamEnabled: true,
@@ -13294,7 +13359,8 @@ var DynamoDBGlobalTableProvider = class {
13294
13359
  if (table.DeletionProtectionEnabled !== void 0) result["DeletionProtectionEnabled"] = table.DeletionProtectionEnabled;
13295
13360
  const currentRegion = await this.dynamoDBClient.config.region() ?? "";
13296
13361
  const tableNameForSubs = table.TableName ?? physicalId;
13297
- result["Replicas"] = await Promise.all((table.Replicas ?? []).map(async (r) => {
13362
+ const sdkReplicas = table.Replicas && table.Replicas.length > 0 ? table.Replicas : [{ RegionName: currentRegion }];
13363
+ const replicas = await Promise.all(sdkReplicas.map(async (r) => {
13298
13364
  const entry = { Region: r.RegionName };
13299
13365
  if (r.KMSMasterKeyId !== void 0) entry["KMSMasterKeyId"] = r.KMSMasterKeyId;
13300
13366
  if (!r.RegionName) return entry;
@@ -13324,8 +13390,38 @@ var DynamoDBGlobalTableProvider = class {
13324
13390
  }
13325
13391
  return entry;
13326
13392
  }));
13393
+ result["Replicas"] = replicas;
13394
+ const localReplicaEntry = replicas.find((r) => r["Region"] === currentRegion);
13395
+ if (table.GlobalSecondaryIndexes && table.GlobalSecondaryIndexes.length > 0) {
13396
+ const cfnIndexes = [];
13397
+ const replicaIndexEntries = [];
13398
+ for (const gsi of table.GlobalSecondaryIndexes) {
13399
+ if (!gsi.IndexName) continue;
13400
+ const entry = { IndexName: gsi.IndexName };
13401
+ if (gsi.KeySchema) entry["KeySchema"] = gsi.KeySchema;
13402
+ if (gsi.Projection) entry["Projection"] = gsi.Projection;
13403
+ if (gsi.WarmThroughput) entry["WarmThroughput"] = gsi.WarmThroughput;
13404
+ const replicaEntry = { IndexName: gsi.IndexName };
13405
+ if (billingMode === "PROVISIONED") {
13406
+ const writeAutoScaling = await this.readAutoScalingSettings(tableNameForSubs, "dynamodb:index:WriteCapacityUnits", void 0, gsi.IndexName);
13407
+ if (writeAutoScaling) entry["WriteProvisionedThroughputSettings"] = { WriteCapacityAutoScalingSettings: writeAutoScaling };
13408
+ else if (gsi.ProvisionedThroughput?.WriteCapacityUnits !== void 0) entry["WriteProvisionedThroughputSettings"] = { WriteCapacityUnits: gsi.ProvisionedThroughput.WriteCapacityUnits };
13409
+ const readAutoScaling = await this.readAutoScalingSettings(tableNameForSubs, "dynamodb:index:ReadCapacityUnits", void 0, gsi.IndexName);
13410
+ if (readAutoScaling) replicaEntry["ReadProvisionedThroughputSettings"] = { ReadCapacityAutoScalingSettings: readAutoScaling };
13411
+ else if (gsi.ProvisionedThroughput?.ReadCapacityUnits !== void 0) replicaEntry["ReadProvisionedThroughputSettings"] = { ReadCapacityUnits: gsi.ProvisionedThroughput.ReadCapacityUnits };
13412
+ } else {
13413
+ if (gsi.OnDemandThroughput?.MaxWriteRequestUnits !== void 0) entry["WriteOnDemandThroughputSettings"] = { MaxWriteRequestUnits: gsi.OnDemandThroughput.MaxWriteRequestUnits };
13414
+ if (gsi.OnDemandThroughput?.MaxReadRequestUnits !== void 0) replicaEntry["ReadOnDemandThroughputSettings"] = { MaxReadRequestUnits: gsi.OnDemandThroughput.MaxReadRequestUnits };
13415
+ }
13416
+ cfnIndexes.push(entry);
13417
+ if (Object.keys(replicaEntry).length > 1) replicaIndexEntries.push(replicaEntry);
13418
+ }
13419
+ result["GlobalSecondaryIndexes"] = cfnIndexes;
13420
+ if (localReplicaEntry && replicaIndexEntries.length > 0) localReplicaEntry["GlobalSecondaryIndexes"] = replicaIndexEntries;
13421
+ }
13327
13422
  if (table.OnDemandThroughput?.MaxWriteRequestUnits !== void 0) result["WriteOnDemandThroughputSettings"] = { MaxWriteRequestUnits: table.OnDemandThroughput.MaxWriteRequestUnits };
13328
13423
  else result["WriteOnDemandThroughputSettings"] = {};
13424
+ if (localReplicaEntry && table.OnDemandThroughput?.MaxReadRequestUnits !== void 0) localReplicaEntry["ReadOnDemandThroughputSettings"] = { MaxReadRequestUnits: table.OnDemandThroughput.MaxReadRequestUnits };
13329
13425
  if (billingMode === "PROVISIONED") {
13330
13426
  const autoScaling = await this.readAutoScalingSettings(tableNameForSubs, "dynamodb:table:WriteCapacityUnits");
13331
13427
  if (autoScaling) result["WriteProvisionedThroughputSettings"] = { WriteCapacityAutoScalingSettings: autoScaling };
@@ -13446,18 +13542,24 @@ var DynamoDBGlobalTableProvider = class {
13446
13542
  * under `--verbose`.
13447
13543
  *
13448
13544
  * @param tableName DynamoDB table name (the `physicalId`).
13449
- * @param scalableDimension `'dynamodb:table:WriteCapacityUnits'` or
13450
- * `'dynamodb:table:ReadCapacityUnits'`.
13545
+ * @param scalableDimension Any of the four dimensions a GlobalTable can
13546
+ * register — the two `dynamodb:table:*` ones and, since issue #1420's
13547
+ * GSI reverse map, the two `dynamodb:index:*` ones.
13451
13548
  * @param client Pre-built region-scoped autoscaling client. Defaults to
13452
13549
  * a fresh client in the local region — pass an explicit client when
13453
13550
  * reading from a cross-region replica.
13551
+ * @param indexName Required for a `dynamodb:index:*` dimension; the
13552
+ * `ResourceId` is built through the shared {@link autoScalingResourceId}
13553
+ * so the read cannot spell it differently from the register / deregister
13554
+ * calls.
13454
13555
  */
13455
- async readAutoScalingSettings(tableName, scalableDimension, client) {
13556
+ async readAutoScalingSettings(tableName, scalableDimension, client, indexName) {
13557
+ const resourceId = autoScalingResourceId(tableName, indexName);
13456
13558
  try {
13457
13559
  const asClient = client ?? new ApplicationAutoScalingClient({ region: await this.dynamoDBClient.config.region() ?? "" });
13458
13560
  const targets = (await asClient.send(new DescribeScalableTargetsCommand({
13459
13561
  ServiceNamespace: "dynamodb",
13460
- ResourceIds: [`table/${tableName}`],
13562
+ ResourceIds: [resourceId],
13461
13563
  ScalableDimension: scalableDimension
13462
13564
  }))).ScalableTargets ?? [];
13463
13565
  if (targets.length === 0) return null;
@@ -13465,7 +13567,7 @@ var DynamoDBGlobalTableProvider = class {
13465
13567
  if (target.MinCapacity === void 0 || target.MaxCapacity === void 0) return null;
13466
13568
  const targetTracking = ((await asClient.send(new DescribeScalingPoliciesCommand({
13467
13569
  ServiceNamespace: "dynamodb",
13468
- ResourceId: `table/${tableName}`,
13570
+ ResourceId: resourceId,
13469
13571
  ScalableDimension: scalableDimension
13470
13572
  }))).ScalingPolicies ?? []).find((p) => p.PolicyType === "TargetTrackingScaling");
13471
13573
  if (!targetTracking) return null;
@@ -13481,7 +13583,7 @@ var DynamoDBGlobalTableProvider = class {
13481
13583
  TargetTrackingScalingPolicyConfiguration: tttConfig
13482
13584
  };
13483
13585
  } catch (err) {
13484
- this.logger.debug(`Could not read application-autoscaling settings for ${tableName} (${scalableDimension}): ${err instanceof Error ? err.message : String(err)}`);
13586
+ this.logger.debug(`Could not read application-autoscaling settings for ${resourceId} (${scalableDimension}): ${err instanceof Error ? err.message : String(err)}`);
13485
13587
  return null;
13486
13588
  }
13487
13589
  }
@@ -13507,6 +13609,17 @@ var DynamoDBGlobalTableProvider = class {
13507
13609
  * - `TimeToLiveSpecification` is reverse-mapped via
13508
13610
  * `DescribeTimeToLive` in `readCurrentState`.
13509
13611
  *
13612
+ * Issue #1420 + #1436 kept it empty rather than growing it:
13613
+ * - `GlobalSecondaryIndexes` is a real CFn-shaped reverse map (SDK-only
13614
+ * members stripped, write throughput at the top level, read throughput
13615
+ * on the local replica's index entry, auto-scaling recovered per index
13616
+ * via `dynamodb:index:*`). The alternative #1420 offered — declaring the
13617
+ * subtree here — would have turned drift OFF for every GSI, which is a
13618
+ * strictly worse trade than the phantom drift it removes.
13619
+ * - The table-level on-demand READ ceiling is reverse-mapped onto
13620
+ * `Replicas[local].ReadOnDemandThroughputSettings`, the CFn location it
13621
+ * is authored at.
13622
+ *
13510
13623
  * Returning an empty list is the canonical "no known-unknown paths"
13511
13624
  * shape — keeping the method present (rather than removing it) for
13512
13625
  * future additions and for backward compat on test reflection.
@@ -13691,8 +13804,8 @@ function extractLocalDeletionProtection(props, region) {
13691
13804
  */
13692
13805
  function derivePerCallProvisionedThroughput(properties, region, source = "min") {
13693
13806
  return {
13694
- ReadCapacityUnits: deriveReadCapacityUnits(asRecord((properties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
13695
- WriteCapacityUnits: deriveWriteCapacityUnits(asRecord(properties["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
13807
+ ReadCapacityUnits: deriveReadCapacityUnits(asRecord$1((properties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
13808
+ WriteCapacityUnits: deriveWriteCapacityUnits(asRecord$1(properties["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
13696
13809
  };
13697
13810
  }
13698
13811
  /**
@@ -13757,7 +13870,7 @@ const DESCRIBE_SCALABLE_TARGETS_BATCH = 50;
13757
13870
  */
13758
13871
  function collectAutoScalingTargets(properties, localRegion) {
13759
13872
  const specs = [];
13760
- const tableWrite = asRecord(asRecord(properties["WriteProvisionedThroughputSettings"])?.["WriteCapacityAutoScalingSettings"]);
13873
+ const tableWrite = asRecord$1(asRecord$1(properties["WriteProvisionedThroughputSettings"])?.["WriteCapacityAutoScalingSettings"]);
13761
13874
  if (tableWrite) specs.push({
13762
13875
  dimension: "dynamodb:table:WriteCapacityUnits",
13763
13876
  region: localRegion,
@@ -13765,10 +13878,10 @@ function collectAutoScalingTargets(properties, localRegion) {
13765
13878
  });
13766
13879
  const cfnIndexes = properties["GlobalSecondaryIndexes"];
13767
13880
  if (Array.isArray(cfnIndexes)) for (const entry of cfnIndexes) {
13768
- const gsi = asRecord(entry);
13881
+ const gsi = asRecord$1(entry);
13769
13882
  const indexName = gsi?.["IndexName"];
13770
13883
  if (!gsi || typeof indexName !== "string") continue;
13771
- const indexWrite = asRecord(asRecord(gsi["WriteProvisionedThroughputSettings"])?.["WriteCapacityAutoScalingSettings"]);
13884
+ const indexWrite = asRecord$1(asRecord$1(gsi["WriteProvisionedThroughputSettings"])?.["WriteCapacityAutoScalingSettings"]);
13772
13885
  if (indexWrite) specs.push({
13773
13886
  dimension: "dynamodb:index:WriteCapacityUnits",
13774
13887
  indexName,
@@ -13778,10 +13891,10 @@ function collectAutoScalingTargets(properties, localRegion) {
13778
13891
  }
13779
13892
  const replicas = properties["Replicas"];
13780
13893
  if (Array.isArray(replicas)) for (const replicaEntry of replicas) {
13781
- const replica = asRecord(replicaEntry);
13894
+ const replica = asRecord$1(replicaEntry);
13782
13895
  const region = replica?.["Region"];
13783
13896
  if (!replica || typeof region !== "string") continue;
13784
- const replicaRead = asRecord(asRecord(replica["ReadProvisionedThroughputSettings"])?.["ReadCapacityAutoScalingSettings"]);
13897
+ const replicaRead = asRecord$1(asRecord$1(replica["ReadProvisionedThroughputSettings"])?.["ReadCapacityAutoScalingSettings"]);
13785
13898
  if (replicaRead) specs.push({
13786
13899
  dimension: "dynamodb:table:ReadCapacityUnits",
13787
13900
  region,
@@ -13790,10 +13903,10 @@ function collectAutoScalingTargets(properties, localRegion) {
13790
13903
  const replicaIndexes = replica["GlobalSecondaryIndexes"];
13791
13904
  if (!Array.isArray(replicaIndexes)) continue;
13792
13905
  for (const entry of replicaIndexes) {
13793
- const replicaIndex = asRecord(entry);
13906
+ const replicaIndex = asRecord$1(entry);
13794
13907
  const indexName = replicaIndex?.["IndexName"];
13795
13908
  if (!replicaIndex || typeof indexName !== "string") continue;
13796
- const indexRead = asRecord(asRecord(replicaIndex["ReadProvisionedThroughputSettings"])?.["ReadCapacityAutoScalingSettings"]);
13909
+ const indexRead = asRecord$1(asRecord$1(replicaIndex["ReadProvisionedThroughputSettings"])?.["ReadCapacityAutoScalingSettings"]);
13797
13910
  if (indexRead) specs.push({
13798
13911
  dimension: "dynamodb:index:ReadCapacityUnits",
13799
13912
  indexName,
@@ -13804,8 +13917,38 @@ function collectAutoScalingTargets(properties, localRegion) {
13804
13917
  }
13805
13918
  return specs;
13806
13919
  }
13920
+ /**
13921
+ * Every attribute name some KEY SCHEMA in the desired template references:
13922
+ * the table's own `KeySchema`, each GSI's, and each LSI's. This is the set
13923
+ * the `AttributeDefinitions`-removal guard in `update()` tests membership
13924
+ * against — a removed attribute still in this set is a genuine
13925
+ * redefine-in-place (replacement required); one outside it is the normal
13926
+ * bookkeeping of a GSI deletion and must NOT force a replacement.
13927
+ *
13928
+ * Malformed / intrinsic-valued entries contribute nothing, which errs on the
13929
+ * PERMISSIVE side of the guard — correct here, because the guard's failure
13930
+ * mode is a forced replacement of a live table, and AWS itself still rejects
13931
+ * a genuinely-bad `UpdateTable` loudly.
13932
+ */
13933
+ function collectDesiredKeyAttributeNames(properties) {
13934
+ const names = /* @__PURE__ */ new Set();
13935
+ const addKeySchema = (value) => {
13936
+ if (!Array.isArray(value)) return;
13937
+ for (const element of value) {
13938
+ const name = asRecord$1(element)?.["AttributeName"];
13939
+ if (typeof name === "string") names.add(name);
13940
+ }
13941
+ };
13942
+ addKeySchema(properties["KeySchema"]);
13943
+ for (const indexListKey of ["GlobalSecondaryIndexes", "LocalSecondaryIndexes"]) {
13944
+ const indexes = properties[indexListKey];
13945
+ if (!Array.isArray(indexes)) continue;
13946
+ for (const index of indexes) addKeySchema(asRecord$1(index)?.["KeySchema"]);
13947
+ }
13948
+ return names;
13949
+ }
13807
13950
  /** Narrow an unknown CFn sub-object to a record (or `undefined`). */
13808
- function asRecord(value) {
13951
+ function asRecord$1(value) {
13809
13952
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
13810
13953
  }
13811
13954
  /**
@@ -13820,10 +13963,93 @@ const ON_DEMAND_LIMIT_RESET = -1;
13820
13963
  /** Coerce a CFn numeric (CFn is stringly-typed) to a finite number. */
13821
13964
  function toFiniteNumber(value) {
13822
13965
  if (value === void 0 || value === null || value === "") return void 0;
13966
+ if (typeof value !== "number" && typeof value !== "string") return void 0;
13823
13967
  const n = Number(value);
13824
13968
  return Number.isFinite(n) ? n : void 0;
13825
13969
  }
13826
13970
  /**
13971
+ * Merge an explicitly-supplied, already-SDK-shaped throughput block over a
13972
+ * derived one, PER MEMBER (issue #1428).
13973
+ *
13974
+ * The CFn schema forbids `ProvisionedThroughput` / `OnDemandThroughput` /
13975
+ * `*ThroughputOverride` on `AWS::DynamoDB::GlobalTable`, so this only fires
13976
+ * for pre-#1387 cdkd state and hand-authored templates — but those ARE a
13977
+ * first-class cdkd surface (`cdkd import --migrate-from-cloudformation`,
13978
+ * `cdkd migrate --from-cfn-stack`), and the pre-fix code forwarded such a
13979
+ * block VERBATIM. Three defects followed, and fixing any subset leaves a
13980
+ * live one, which is why this helper does all three at once:
13981
+ *
13982
+ * 1. **Coercion.** The verbatim forward was the ONE path that skipped
13983
+ * {@link toFiniteNumber}, so a stringly-typed CFn `"5"` reached the SDK
13984
+ * unnormalized while every derived value was a number.
13985
+ * 2. **Per-member merge.** Whole-block replacement meant a PARTIAL explicit
13986
+ * block SUPPRESSED valid derived siblings — an explicit
13987
+ * `{MaxReadRequestUnits: 41}` discarded a perfectly good
13988
+ * `WriteOnDemandThroughputSettings.MaxWriteRequestUnits` from the same
13989
+ * template.
13990
+ * 3. **Billing gate.** That is the CALLER's job (it owns `billingMode`), but
13991
+ * it only works because this helper never emits a block the caller did
13992
+ * not ask for.
13993
+ *
13994
+ * WARN-AND-FALL-BACK, never throw. Three earlier designs are recorded on
13995
+ * issue #1428 and each was broken by the same lever: a present-but-
13996
+ * uncoercible member that THREW made a stack whose STATE carried the garbage
13997
+ * permanently undeployable (the helper also runs on `previousProperties`),
13998
+ * and throwing from the replica path threw AFTER `CreateTable` had already
13999
+ * committed a real table. So an uncoercible member is dropped, reported
14000
+ * through `diagnostics`, and the derived value stands in.
14001
+ *
14002
+ * Returns `undefined` when neither side contributes a single member — an
14003
+ * EMPTY block is not a safe stand-in, because AWS documents an empty
14004
+ * `ProvisionedThroughputOverride` / `OnDemandThroughputOverride` as "inherit
14005
+ * the source table's settings".
14006
+ */
14007
+ function mergeExplicitThroughputBlock(explicit, derived, members, diagnostics, context) {
14008
+ const out = {};
14009
+ for (const member of members) {
14010
+ if (explicit && explicit[member] !== void 0) {
14011
+ const coerced = toFiniteNumber(explicit[member]);
14012
+ if (coerced !== void 0) {
14013
+ out[member] = coerced;
14014
+ continue;
14015
+ }
14016
+ diagnostics?.push({
14017
+ kind: "unresolved-member",
14018
+ ...context.indexName !== void 0 && { indexName: context.indexName },
14019
+ member,
14020
+ message: `${context.blockName}.${member} is present but did not resolve to a number (${JSON.stringify(explicit[member])?.slice(0, 80)}), so it was ignored. This is usually an unresolved intrinsic; resolve it to a literal.`
14021
+ });
14022
+ }
14023
+ const derivedValue = derived?.[member];
14024
+ if (typeof derivedValue === "number") out[member] = derivedValue;
14025
+ }
14026
+ return Object.keys(out).length > 0 ? out : void 0;
14027
+ }
14028
+ /**
14029
+ * Report a member the template DECLARED whose value did not coerce (#1444 A).
14030
+ *
14031
+ * The diagnostic has to be raised WHERE THE VALUE IS LOST — inside the
14032
+ * translation — not from the per-GSI reset gate in `update()`. That gate only
14033
+ * runs for an index the diff already classified `modified` AND only when a
14034
+ * previous ceiling happened to exist AND only when the billing mode did not
14035
+ * flip, so the very same template defect was silent in the two most likely
14036
+ * cases: a FIRST-TIME set (both translated sides lack the member, so the index
14037
+ * is not `modified` at all and the loop never runs) and a BILLING FLIP (the
14038
+ * gate short-circuits). The defect is identical in all three; only the
14039
+ * diagnostic was conditional.
14040
+ */
14041
+ function reportUnresolvedRawMember(rawValue, diagnostics, context) {
14042
+ if (!diagnostics) return;
14043
+ if (rawValue === void 0) return;
14044
+ if (toFiniteNumber(rawValue) !== void 0) return;
14045
+ diagnostics.push({
14046
+ kind: "unresolved-member",
14047
+ ...context.indexName !== void 0 && { indexName: context.indexName },
14048
+ member: context.member,
14049
+ message: `${context.blockName}.${context.member} is declared but did not resolve to a number (${JSON.stringify(rawValue)?.slice(0, 80)}), so no throughput value was sent for it and the live AWS setting was left unchanged. This is usually an unresolved intrinsic; resolve it to a literal to apply the limit, or remove the property entirely to clear it.`
14050
+ });
14051
+ }
14052
+ /**
13827
14053
  * Derive a single WRITE capacity number from a CFn
13828
14054
  * `WriteProvisionedThroughputSettings` block.
13829
14055
  *
@@ -13849,7 +14075,7 @@ function deriveWriteCapacityUnits(settings, source = "min") {
13849
14075
  if (!settings) return void 0;
13850
14076
  const literal = toFiniteNumber(settings["WriteCapacityUnits"]);
13851
14077
  if (literal !== void 0) return literal;
13852
- const autoScaling = asRecord(settings["WriteCapacityAutoScalingSettings"]);
14078
+ const autoScaling = asRecord$1(settings["WriteCapacityAutoScalingSettings"]);
13853
14079
  if (!autoScaling) return void 0;
13854
14080
  return pickAutoScalingCapacity(autoScaling, source);
13855
14081
  }
@@ -13865,7 +14091,7 @@ function deriveReadCapacityUnits(settings, source = "min") {
13865
14091
  if (!settings) return void 0;
13866
14092
  const literal = toFiniteNumber(settings["ReadCapacityUnits"]);
13867
14093
  if (literal !== void 0) return literal;
13868
- const autoScaling = asRecord(settings["ReadCapacityAutoScalingSettings"]);
14094
+ const autoScaling = asRecord$1(settings["ReadCapacityAutoScalingSettings"]);
13869
14095
  if (!autoScaling) return void 0;
13870
14096
  return pickAutoScalingCapacity(autoScaling, source);
13871
14097
  }
@@ -13912,22 +14138,22 @@ function pickAutoScalingCapacity(autoScaling, source) {
13912
14138
  * written before this fix (and hand-authored templates) can carry them, and
13913
14139
  * silently re-deriving over an explicit value would be a regression.
13914
14140
  */
13915
- function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "min") {
14141
+ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "min", diagnostics) {
13916
14142
  const rawIndexes = properties["GlobalSecondaryIndexes"];
13917
14143
  if (rawIndexes !== void 0 && !Array.isArray(rawIndexes)) throw new Error(`AWS::DynamoDB::GlobalTable GlobalSecondaryIndexes must be an array, got ${typeof rawIndexes} (${JSON.stringify(rawIndexes)?.slice(0, 200)}). An unresolved intrinsic here would otherwise deploy a table with no indexes.`);
13918
14144
  const cfnIndexes = rawIndexes ?? [];
13919
14145
  if (cfnIndexes.length === 0) return [];
13920
14146
  const replicas = properties["Replicas"] ?? [];
13921
- const localReplicaIndexes = (Array.isArray(replicas) ? replicas.find((r) => asRecord(r)?.["Region"] === region) : void 0)?.["GlobalSecondaryIndexes"] ?? [];
14147
+ const localReplicaIndexes = (Array.isArray(replicas) ? replicas.find((r) => asRecord$1(r)?.["Region"] === region) : void 0)?.["GlobalSecondaryIndexes"] ?? [];
13922
14148
  const localByName = /* @__PURE__ */ new Map();
13923
14149
  if (Array.isArray(localReplicaIndexes)) for (const entry of localReplicaIndexes) {
13924
- const record = asRecord(entry);
14150
+ const record = asRecord$1(entry);
13925
14151
  const name = record?.["IndexName"];
13926
14152
  if (record && typeof name === "string") localByName.set(name, record);
13927
14153
  }
13928
14154
  const result = [];
13929
14155
  for (const entry of cfnIndexes) {
13930
- const gsi = asRecord(entry);
14156
+ const gsi = asRecord$1(entry);
13931
14157
  if (!gsi) {
13932
14158
  result.push(entry);
13933
14159
  continue;
@@ -13940,21 +14166,51 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "
13940
14166
  Projection: gsi["Projection"]
13941
14167
  };
13942
14168
  if (gsi["WarmThroughput"] !== void 0) sdk.WarmThroughput = gsi["WarmThroughput"];
13943
- const explicitProvisioned = asRecord(gsi["ProvisionedThroughput"]);
13944
- const explicitOnDemand = asRecord(gsi["OnDemandThroughput"]);
13945
- if (explicitProvisioned) sdk.ProvisionedThroughput = explicitProvisioned;
13946
- else if (billingMode === "PROVISIONED") sdk.ProvisionedThroughput = {
13947
- ReadCapacityUnits: deriveReadCapacityUnits(asRecord(localEntry?.["ReadProvisionedThroughputSettings"]), source) ?? deriveReadCapacityUnits(asRecord(gsi["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
13948
- WriteCapacityUnits: deriveWriteCapacityUnits(asRecord(gsi["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
13949
- };
13950
- if (explicitOnDemand) sdk.OnDemandThroughput = explicitOnDemand;
13951
- else if (billingMode !== "PROVISIONED") {
13952
- const maxWrite = toFiniteNumber(asRecord(gsi["WriteOnDemandThroughputSettings"])?.["MaxWriteRequestUnits"]);
13953
- const maxRead = toFiniteNumber(asRecord(localEntry?.["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"]) ?? toFiniteNumber(asRecord(gsi["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"]);
13954
- if (maxWrite !== void 0 || maxRead !== void 0) sdk.OnDemandThroughput = {
14169
+ const explicitProvisioned = asRecord$1(gsi["ProvisionedThroughput"]);
14170
+ const explicitOnDemand = asRecord$1(gsi["OnDemandThroughput"]);
14171
+ if (explicitProvisioned && billingMode !== "PROVISIONED") diagnostics?.push({
14172
+ kind: "billing-mode-mismatch",
14173
+ ...typeof indexName === "string" && { indexName },
14174
+ member: "ProvisionedThroughput",
14175
+ message: `an explicit ProvisionedThroughput block was dropped because the table's BillingMode is ${billingMode}; AWS rejects provisioned capacity on an on-demand table. Remove the block, or set BillingMode: PROVISIONED.`
14176
+ });
14177
+ if (explicitOnDemand && billingMode === "PROVISIONED") diagnostics?.push({
14178
+ kind: "billing-mode-mismatch",
14179
+ ...typeof indexName === "string" && { indexName },
14180
+ member: "OnDemandThroughput",
14181
+ message: "an explicit OnDemandThroughput block was dropped because the table's BillingMode is PROVISIONED; AWS rejects on-demand ceilings on a provisioned table. Remove the block, or set BillingMode: PAY_PER_REQUEST."
14182
+ });
14183
+ if (billingMode === "PROVISIONED") sdk.ProvisionedThroughput = mergeExplicitThroughputBlock(explicitProvisioned, {
14184
+ ReadCapacityUnits: deriveReadCapacityUnits(asRecord$1(localEntry?.["ReadProvisionedThroughputSettings"]), source) ?? deriveReadCapacityUnits(asRecord$1(gsi["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
14185
+ WriteCapacityUnits: deriveWriteCapacityUnits(asRecord$1(gsi["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
14186
+ }, ["ReadCapacityUnits", "WriteCapacityUnits"], diagnostics, {
14187
+ ...typeof indexName === "string" && { indexName },
14188
+ blockName: "ProvisionedThroughput"
14189
+ });
14190
+ else {
14191
+ const rawWrite = asRecord$1(gsi["WriteOnDemandThroughputSettings"])?.["MaxWriteRequestUnits"];
14192
+ const rawReadLocal = asRecord$1(localEntry?.["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"];
14193
+ const rawReadGsi = asRecord$1(gsi["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"];
14194
+ reportUnresolvedRawMember(rawWrite, diagnostics, {
14195
+ ...typeof indexName === "string" && { indexName },
14196
+ blockName: "WriteOnDemandThroughputSettings",
14197
+ member: "MaxWriteRequestUnits"
14198
+ });
14199
+ reportUnresolvedRawMember(rawReadLocal !== void 0 ? rawReadLocal : rawReadGsi, diagnostics, {
14200
+ ...typeof indexName === "string" && { indexName },
14201
+ blockName: rawReadLocal !== void 0 ? "Replicas[local].GlobalSecondaryIndexes[].ReadOnDemandThroughputSettings" : "ReadOnDemandThroughputSettings",
14202
+ member: "MaxReadRequestUnits"
14203
+ });
14204
+ const maxWrite = toFiniteNumber(rawWrite);
14205
+ const maxRead = toFiniteNumber(rawReadLocal) ?? toFiniteNumber(rawReadGsi);
14206
+ const merged = mergeExplicitThroughputBlock(explicitOnDemand, {
13955
14207
  ...maxRead !== void 0 && { MaxReadRequestUnits: maxRead },
13956
14208
  ...maxWrite !== void 0 && { MaxWriteRequestUnits: maxWrite }
13957
- };
14209
+ }, ["MaxReadRequestUnits", "MaxWriteRequestUnits"], diagnostics, {
14210
+ ...typeof indexName === "string" && { indexName },
14211
+ blockName: "OnDemandThroughput"
14212
+ });
14213
+ if (merged) sdk.OnDemandThroughput = merged;
13958
14214
  }
13959
14215
  result.push(sdk);
13960
14216
  }
@@ -13972,7 +14228,7 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "
13972
14228
  * is issue #1440 one level up from where the member test looks.
13973
14229
  */
13974
14230
  function isUnresolvedIntrinsicBlock(value) {
13975
- const record = asRecord(value);
14231
+ const record = asRecord$1(value);
13976
14232
  if (!record) return false;
13977
14233
  const keys = Object.keys(record);
13978
14234
  return keys.length > 0 && keys.every((k) => k === "Ref" || k.startsWith("Fn::"));
@@ -14014,31 +14270,25 @@ function collectRawOnDemandDeclarations(properties, region) {
14014
14270
  const rawIndexes = properties["GlobalSecondaryIndexes"];
14015
14271
  if (!Array.isArray(rawIndexes)) return out;
14016
14272
  const replicas = properties["Replicas"];
14017
- const localReplicaIndexes = asRecord(Array.isArray(replicas) ? replicas.find((r) => asRecord(r)?.["Region"] === region) : void 0)?.["GlobalSecondaryIndexes"];
14273
+ const localReplicaIndexes = asRecord$1(Array.isArray(replicas) ? replicas.find((r) => asRecord$1(r)?.["Region"] === region) : void 0)?.["GlobalSecondaryIndexes"];
14018
14274
  const localByName = /* @__PURE__ */ new Map();
14019
14275
  if (Array.isArray(localReplicaIndexes)) for (const entry of localReplicaIndexes) {
14020
- const record = asRecord(entry);
14276
+ const record = asRecord$1(entry);
14021
14277
  const name = record?.["IndexName"];
14022
14278
  if (record && typeof name === "string") localByName.set(name, record);
14023
14279
  }
14024
14280
  for (const entry of rawIndexes) {
14025
- const gsi = asRecord(entry);
14281
+ const gsi = asRecord$1(entry);
14026
14282
  const name = gsi?.["IndexName"];
14027
14283
  if (!gsi || typeof name !== "string") continue;
14028
14284
  const localEntry = localByName.get(name);
14029
- const explicitOnDemand = asRecord(gsi["OnDemandThroughput"]);
14030
- if (explicitOnDemand) {
14031
- out.set(name, {
14032
- readDeclared: explicitOnDemand["MaxReadRequestUnits"] !== void 0,
14033
- writeDeclared: explicitOnDemand["MaxWriteRequestUnits"] !== void 0
14034
- });
14035
- continue;
14036
- }
14285
+ const explicitOnDemand = asRecord$1(gsi["OnDemandThroughput"]);
14286
+ const explicitIsIntrinsic = isUnresolvedIntrinsicBlock(gsi["OnDemandThroughput"]);
14037
14287
  const readBlocks = [localEntry?.["ReadOnDemandThroughputSettings"], gsi["ReadOnDemandThroughputSettings"]];
14038
14288
  const writeBlock = gsi["WriteOnDemandThroughputSettings"];
14039
14289
  out.set(name, {
14040
- readDeclared: readBlocks.some((block) => asRecord(block)?.["MaxReadRequestUnits"] !== void 0 || isUnresolvedIntrinsicBlock(block)),
14041
- writeDeclared: asRecord(writeBlock)?.["MaxWriteRequestUnits"] !== void 0 || isUnresolvedIntrinsicBlock(writeBlock)
14290
+ readDeclared: explicitOnDemand?.["MaxReadRequestUnits"] !== void 0 || explicitIsIntrinsic || readBlocks.some((block) => asRecord$1(block)?.["MaxReadRequestUnits"] !== void 0 || isUnresolvedIntrinsicBlock(block)),
14291
+ writeDeclared: explicitOnDemand?.["MaxWriteRequestUnits"] !== void 0 || explicitIsIntrinsic || asRecord$1(writeBlock)?.["MaxWriteRequestUnits"] !== void 0 || isUnresolvedIntrinsicBlock(writeBlock)
14042
14292
  });
14043
14293
  }
14044
14294
  return out;
@@ -14063,23 +14313,113 @@ function collectRawOnDemandDeclarations(properties, region) {
14063
14313
  * status back in `readCurrentState`). Recorded here rather than silently
14064
14314
  * dropped; tracked with the other GlobalTable nested-key gaps.
14065
14315
  */
14066
- function toSdkReplicaGlobalSecondaryIndexes(replicaIndexes) {
14316
+ function toSdkReplicaGlobalSecondaryIndexes(replicaIndexes, billingMode, diagnostics) {
14067
14317
  if (!Array.isArray(replicaIndexes) || replicaIndexes.length === 0) return void 0;
14068
14318
  return replicaIndexes.map((entry) => {
14069
- const cfn = asRecord(entry);
14319
+ const cfn = asRecord$1(entry);
14070
14320
  if (!cfn) return entry;
14071
- const sdk = { IndexName: cfn["IndexName"] };
14072
- const explicitProvisioned = asRecord(cfn["ProvisionedThroughputOverride"]);
14073
- const explicitOnDemand = asRecord(cfn["OnDemandThroughputOverride"]);
14074
- const readCapacity = deriveReadCapacityUnits(asRecord(cfn["ReadProvisionedThroughputSettings"]));
14075
- const maxReadRequestUnits = toFiniteNumber(asRecord(cfn["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"]);
14076
- if (explicitProvisioned) sdk.ProvisionedThroughputOverride = explicitProvisioned;
14077
- else if (readCapacity !== void 0) sdk.ProvisionedThroughputOverride = { ReadCapacityUnits: readCapacity };
14078
- if (explicitOnDemand) sdk.OnDemandThroughputOverride = explicitOnDemand;
14079
- else if (maxReadRequestUnits !== void 0) sdk.OnDemandThroughputOverride = { MaxReadRequestUnits: maxReadRequestUnits };
14321
+ const indexName = cfn["IndexName"];
14322
+ const sdk = { IndexName: indexName };
14323
+ const overrides = toSdkReplicaThroughputOverrides(cfn, billingMode, diagnostics, { ...typeof indexName === "string" && { indexName } });
14324
+ if (overrides.ProvisionedThroughputOverride) sdk.ProvisionedThroughputOverride = overrides.ProvisionedThroughputOverride;
14325
+ if (overrides.OnDemandThroughputOverride) sdk.OnDemandThroughputOverride = overrides.OnDemandThroughputOverride;
14080
14326
  return sdk;
14081
14327
  });
14082
14328
  }
14329
+ /**
14330
+ * The `{Provisioned,OnDemand}ThroughputOverride` pair for ONE replica-scoped
14331
+ * CFn block — shared by the per-GSI replica entries and, since issue #1436,
14332
+ * by the replica's own TABLE-level entry, which carries the very same two CFn
14333
+ * spellings (`ReadProvisionedThroughputSettings` /
14334
+ * `ReadOnDemandThroughputSettings`) and maps to the very same two SDK members
14335
+ * on `{Create,Update}ReplicationGroupMemberAction`. One helper so the two
14336
+ * levels cannot disagree about the mapping.
14337
+ *
14338
+ * Same #1428 treatment as the GSI-level blocks: coerce, merge an explicit
14339
+ * SDK-shaped override PER MEMBER over the derived value, and gate on billing
14340
+ * mode. Both override members are READ-side only — a replica has no
14341
+ * independent write capacity, since writes replicate from the source table.
14342
+ */
14343
+ function toSdkReplicaThroughputOverrides(cfn, billingMode, diagnostics, context) {
14344
+ const explicitProvisioned = asRecord$1(cfn["ProvisionedThroughputOverride"]);
14345
+ const explicitOnDemand = asRecord$1(cfn["OnDemandThroughputOverride"]);
14346
+ if (explicitProvisioned && billingMode !== "PROVISIONED") diagnostics?.push({
14347
+ kind: "billing-mode-mismatch",
14348
+ ...context,
14349
+ member: "ProvisionedThroughputOverride",
14350
+ message: `an explicit ProvisionedThroughputOverride was dropped because the table's BillingMode is ${billingMode}; AWS rejects provisioned capacity on an on-demand table.`
14351
+ });
14352
+ if (explicitOnDemand && billingMode === "PROVISIONED") diagnostics?.push({
14353
+ kind: "billing-mode-mismatch",
14354
+ ...context,
14355
+ member: "OnDemandThroughputOverride",
14356
+ message: "an explicit OnDemandThroughputOverride was dropped because the table's BillingMode is PROVISIONED; AWS rejects on-demand ceilings on a provisioned table."
14357
+ });
14358
+ if (billingMode === "PROVISIONED") {
14359
+ const readCapacity = deriveReadCapacityUnits(asRecord$1(cfn["ReadProvisionedThroughputSettings"]));
14360
+ const merged = mergeExplicitThroughputBlock(explicitProvisioned, readCapacity !== void 0 ? { ReadCapacityUnits: readCapacity } : void 0, ["ReadCapacityUnits"], diagnostics, {
14361
+ ...context,
14362
+ blockName: "ProvisionedThroughputOverride"
14363
+ });
14364
+ return merged ? { ProvisionedThroughputOverride: merged } : {};
14365
+ }
14366
+ const rawMaxRead = asRecord$1(cfn["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"];
14367
+ reportUnresolvedRawMember(rawMaxRead, diagnostics, {
14368
+ ...context,
14369
+ blockName: "ReadOnDemandThroughputSettings",
14370
+ member: "MaxReadRequestUnits"
14371
+ });
14372
+ const maxReadRequestUnits = toFiniteNumber(rawMaxRead);
14373
+ const merged = mergeExplicitThroughputBlock(explicitOnDemand, maxReadRequestUnits !== void 0 ? { MaxReadRequestUnits: maxReadRequestUnits } : void 0, ["MaxReadRequestUnits"], diagnostics, {
14374
+ ...context,
14375
+ blockName: "OnDemandThroughputOverride"
14376
+ });
14377
+ return merged ? { OnDemandThroughputOverride: merged } : {};
14378
+ }
14379
+ /**
14380
+ * The TABLE-level on-demand ceilings, gathered from the two DIFFERENT places
14381
+ * the CFn `AWS::DynamoDB::GlobalTable` shape puts them (issue #1436).
14382
+ *
14383
+ * The write ceiling is top-level `WriteOnDemandThroughputSettings`, but the
14384
+ * READ ceiling lives on the LOCAL REPLICA as `ReadOnDemandThroughputSettings`
14385
+ * — the same write-here / read-on-the-replica split #1387 established one
14386
+ * level down for a GSI. The provider read only the write half, so the plain
14387
+ * CDK spelling
14388
+ *
14389
+ * billing: Billing.onDemand({ maxReadRequestUnits: 100, maxWriteRequestUnits: 200 })
14390
+ *
14391
+ * deployed a table with NO read ceiling and reported success. That is a
14392
+ * drop on the way IN, not the absent-field-reset class of #1423 / #1434.
14393
+ *
14394
+ * The registry schema (`cloudformation:DescribeType`, 2026-08-09) confirms the
14395
+ * member on `ReplicaSpecification`:
14396
+ * `ReadOnDemandThroughputSettings: { MaxReadRequestUnits: integer, minimum 1 }`.
14397
+ */
14398
+ function collectTableOnDemandCeilings(properties, region, diagnostics) {
14399
+ const writeBlock = properties["WriteOnDemandThroughputSettings"];
14400
+ const replicas = properties["Replicas"];
14401
+ const readBlock = asRecord$1(Array.isArray(replicas) ? replicas.find((r) => asRecord$1(r)?.["Region"] === region) : void 0)?.["ReadOnDemandThroughputSettings"];
14402
+ const rawRead = asRecord$1(readBlock)?.["MaxReadRequestUnits"];
14403
+ const rawWrite = asRecord$1(writeBlock)?.["MaxWriteRequestUnits"];
14404
+ reportUnresolvedRawMember(rawRead, diagnostics, {
14405
+ blockName: "Replicas[local].ReadOnDemandThroughputSettings",
14406
+ member: "MaxReadRequestUnits"
14407
+ });
14408
+ reportUnresolvedRawMember(rawWrite, diagnostics, {
14409
+ blockName: "WriteOnDemandThroughputSettings",
14410
+ member: "MaxWriteRequestUnits"
14411
+ });
14412
+ return {
14413
+ read: {
14414
+ declared: rawRead !== void 0 || isUnresolvedIntrinsicBlock(readBlock),
14415
+ value: toFiniteNumber(rawRead)
14416
+ },
14417
+ write: {
14418
+ declared: rawWrite !== void 0 || isUnresolvedIntrinsicBlock(writeBlock),
14419
+ value: toFiniteNumber(rawWrite)
14420
+ }
14421
+ };
14422
+ }
14083
14423
  function diffReplicas(oldReplicas, newReplicas) {
14084
14424
  const oldByRegion = /* @__PURE__ */ new Map();
14085
14425
  const newByRegion = /* @__PURE__ */ new Map();
@@ -33237,6 +33577,7 @@ var GlueProvider = class {
33237
33577
  try {
33238
33578
  const builtTableInput = this.buildTableInput(tableInput, tableName);
33239
33579
  this.preserveAwsManagedParameters(builtTableInput, tableInput["Parameters"], (previousProperties?.["TableInput"])?.["Parameters"], live.parameters);
33580
+ this.preserveAwsManagedStorageDescriptor(builtTableInput, tableInput, previousProperties?.["TableInput"], live.storageDescriptor);
33240
33581
  await this.getClient().send(new UpdateTableCommand$1({
33241
33582
  ...catalogId !== void 0 && { CatalogId: catalogId },
33242
33583
  DatabaseName: databaseName,
@@ -33253,7 +33594,7 @@ var GlueProvider = class {
33253
33594
  let message = `Failed to update Glue Table ${logicalId}: ${error instanceof Error ? error.message : String(error)}`;
33254
33595
  if (error instanceof ConcurrentModificationException) {
33255
33596
  message += ". Another writer changed the table between cdkd's pre-update read and its UpdateTable call.";
33256
- message += live.versionId !== void 0 ? ` cdkd sent the version it read (${live.versionId}) as a precondition and AWS rejected the write, rather than let it put back the AWS-managed Parameters read a moment earlier — which for an Apache Iceberg table would have pinned it to an older metadata_location (an effective snapshot rollback).` : " AWS returned no VersionId on the pre-update read, so cdkd could not attach a precondition; AWS rejected the write on its own.";
33597
+ message += live.versionId !== void 0 ? ` cdkd sent the version it read (${live.versionId}) as a precondition and AWS rejected the write, rather than let it put back the AWS-managed Parameters and StorageDescriptor members read a moment earlier — which for an Apache Iceberg table would have pinned it to an older metadata_location (an effective snapshot rollback).` : " AWS returned no VersionId on the pre-update read, so cdkd could not attach a precondition; AWS rejected the write on its own.";
33257
33598
  message += ` Re-run the deploy to pick up the current values.`;
33258
33599
  }
33259
33600
  throw new ProvisioningError(message, resourceType, logicalId, physicalId, cause);
@@ -33343,8 +33684,91 @@ var GlueProvider = class {
33343
33684
  };
33344
33685
  }
33345
33686
  /**
33346
- * The live table state {@link preserveAwsManagedParameters} needs: its
33347
- * current `Parameters` plus the `VersionId` that pins them.
33687
+ * Read-merge-write for out-of-band-authored `TableInput.StorageDescriptor`
33688
+ * members (issue #1479) — the subtree sibling of
33689
+ * {@link preserveAwsManagedParameters}, applied per SD MEMBER, with the two
33690
+ * nested bags (`StorageDescriptor.Parameters`, `SerdeInfo.Parameters`)
33691
+ * getting the same per-KEY rule one level down.
33692
+ *
33693
+ * Live probes (2026-08-10, us-east-1 — recorded on issue #1479) established
33694
+ * WHO authors what, which is why this is per-member rather than a blanket
33695
+ * merge:
33696
+ *
33697
+ * - A USER-authored `EXTERNAL_TABLE`: re-sending the template verbatim
33698
+ * reads back identical — the service itself authors only scalar defaults
33699
+ * (`Compressed: false`, `NumberOfBuckets: 0`, ...). Full-replace is
33700
+ * CORRECT for template-declared members, and `drift --revert` depends on
33701
+ * it to clear console-side edits.
33702
+ * - A CRAWLER-populated table: the crawler authors `Columns`,
33703
+ * `InputFormat` / `OutputFormat`, `SerdeInfo` (incl. its `Parameters`
33704
+ * bag), and `StorageDescriptor.Parameters`; an UpdateTable restating only
33705
+ * a minimal template wiped ALL of them (`Columns` -> `[]`, `SerdeInfo`
33706
+ * gone) while reporting success.
33707
+ * - An ICEBERG table: an UpdateTable omitting `Columns` was ACCEPTED and
33708
+ * wiped the catalog schema to `[]` (Glue re-derives Iceberg columns from
33709
+ * table metadata, so the catalog copy is metadata-authored between
33710
+ * engine commits).
33711
+ *
33712
+ * The preservation key is the SAME "present in NEITHER template side" test
33713
+ * as {@link preserveAwsManagedParameters}, per member:
33714
+ *
33715
+ * | member in desired | member in previous | outcome |
33716
+ * | ----------------- | ------------------ | ---------------------------------- |
33717
+ * | yes | (either) | template wins |
33718
+ * | no | yes | the USER REMOVED it -> stays removed |
33719
+ * | no | no | out-of-band authored -> carried forward from live |
33720
+ *
33721
+ * A template that never declared `StorageDescriptor` on EITHER side (a
33722
+ * crawler-managed table adopted into a minimal template) carries the whole
33723
+ * live block forward. Deliberately scoped to the `StorageDescriptor`
33724
+ * subtree per issue #1479 — the crawler also authors `PartitionKeys` /
33725
+ * `Owner`, but widening beyond the issue's member set is a separate
33726
+ * decision (recorded there).
33727
+ *
33728
+ * TOCTOU: the write rides the same unconditional `VersionId` precondition
33729
+ * as the Parameters merge (see {@link readLiveTableState}), so a concurrent
33730
+ * out-of-band write fails the update loudly instead of being clobbered.
33731
+ */
33732
+ preserveAwsManagedStorageDescriptor(built, desiredTableInput, previousTableInput, liveSd) {
33733
+ if (!liveSd || Object.keys(liveSd).length === 0) return;
33734
+ const desiredSdRaw = desiredTableInput["StorageDescriptor"];
33735
+ const previousSdRaw = asRecord(previousTableInput)?.["StorageDescriptor"];
33736
+ if (desiredSdRaw === void 0 && previousSdRaw === void 0) {
33737
+ built.StorageDescriptor = liveSd;
33738
+ return;
33739
+ }
33740
+ const desiredKeys = parameterKeySet(desiredSdRaw);
33741
+ const previousKeys = parameterKeySet(previousSdRaw);
33742
+ const sdOut = { ...built.StorageDescriptor ?? {} };
33743
+ for (const [member, liveValue] of Object.entries(liveSd)) {
33744
+ if (liveValue === void 0) continue;
33745
+ if (desiredKeys.has(member) || previousKeys.has(member)) continue;
33746
+ sdOut[member] = liveValue;
33747
+ }
33748
+ const desiredSd = asRecord(desiredSdRaw);
33749
+ const previousSd = asRecord(previousSdRaw);
33750
+ if (desiredKeys.has("Parameters") || previousKeys.has("Parameters")) this.preserveAwsManagedParameters(sdOut, desiredSd?.["Parameters"], previousSd?.["Parameters"], liveSd.Parameters);
33751
+ if (desiredKeys.has("SerdeInfo") && liveSd.SerdeInfo) {
33752
+ const desiredSerde = asRecord(desiredSd?.["SerdeInfo"]);
33753
+ const previousSerde = asRecord(previousSd?.["SerdeInfo"]);
33754
+ const desiredSerdeKeys = parameterKeySet(desiredSerde);
33755
+ const previousSerdeKeys = parameterKeySet(previousSerde);
33756
+ const serdeOut = { ...sdOut["SerdeInfo"] ?? {} };
33757
+ for (const [member, liveValue] of Object.entries(liveSd.SerdeInfo)) {
33758
+ if (liveValue === void 0) continue;
33759
+ if (desiredSerdeKeys.has(member) || previousSerdeKeys.has(member)) continue;
33760
+ serdeOut[member] = liveValue;
33761
+ }
33762
+ if (desiredSerdeKeys.has("Parameters") || previousSerdeKeys.has("Parameters")) this.preserveAwsManagedParameters(serdeOut, desiredSerde?.["Parameters"], previousSerde?.["Parameters"], liveSd.SerdeInfo.Parameters);
33763
+ if (Object.keys(serdeOut).length > 0) sdOut["SerdeInfo"] = serdeOut;
33764
+ }
33765
+ if (Object.keys(sdOut).length > 0) built.StorageDescriptor = sdOut;
33766
+ }
33767
+ /**
33768
+ * The live table state the two merges need: its current `Parameters` (for
33769
+ * {@link preserveAwsManagedParameters}), its `StorageDescriptor` (for
33770
+ * {@link preserveAwsManagedStorageDescriptor}, issue #1479), plus the
33771
+ * `VersionId` that pins both.
33348
33772
  *
33349
33773
  * A missing table returns empty rather than throwing: `UpdateTable` is about
33350
33774
  * to surface the real, more actionable error. Any OTHER failure is fatal by
@@ -33393,11 +33817,13 @@ var GlueProvider = class {
33393
33817
  }));
33394
33818
  return {
33395
33819
  parameters: resp.Table?.Parameters,
33820
+ storageDescriptor: resp.Table?.StorageDescriptor,
33396
33821
  versionId: resp.Table?.VersionId
33397
33822
  };
33398
33823
  } catch (error) {
33399
33824
  if (error instanceof EntityNotFoundException) return {
33400
33825
  parameters: void 0,
33826
+ storageDescriptor: void 0,
33401
33827
  versionId: void 0
33402
33828
  };
33403
33829
  throw new ProvisioningError(preUpdateReadFailureMessage("Table", "glue:GetTable", logicalId, error), resourceType, logicalId, physicalId, error instanceof Error ? error : void 0);
@@ -34433,6 +34859,19 @@ function parameterKeySet(value) {
34433
34859
  return new Set(Object.keys(value));
34434
34860
  }
34435
34861
  /**
34862
+ * Narrow an unknown template / state value to a plain object, or `undefined`.
34863
+ * A malformed side (string / array / null) yields `undefined` and therefore
34864
+ * an empty key set — the tolerant treatment `parameterKeySet` already gives
34865
+ * bags, extended to the StorageDescriptor merge's containers. (An unresolved
34866
+ * intrinsic is itself a plain object, so it passes through and contributes
34867
+ * its `Fn::*`/`Ref` key — harmless here, since providers only ever see
34868
+ * resolved values.)
34869
+ */
34870
+ function asRecord(value) {
34871
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return;
34872
+ return value;
34873
+ }
34874
+ /**
34436
34875
  * Message for a failed pre-update `Get*` read (issue #1461). Names the IAM
34437
34876
  * action so a permission gap is one line away from fixed, and says what the
34438
34877
  * read is FOR so the failure does not read as gratuitous.
@@ -66008,7 +66447,7 @@ function createMigrateCommand() {
66008
66447
  */
66009
66448
  function buildProgram() {
66010
66449
  const program = new Command();
66011
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.30");
66450
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.32");
66012
66451
  program.hook("preAction", (_thisCommand, actionCommand) => {
66013
66452
  const { profile } = actionCommand.optsWithGlobals();
66014
66453
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;