@@ -83,12 +83,12 @@ import type {
8383 * - The serialization is load-bearing: AWS's `UpdateTable` does not accept
8484 * `ReplicaUpdates` alongside `BillingMode` / `GlobalSecondaryIndexUpdates`,
8585 * so each category is its own SDK round-trip with a wait-for-ACTIVE in
86- * between. The ONE documented exception is the `PAY_PER_REQUEST ->
87- * PROVISIONED` flip, where AWS REQUIRES per-GSI `ProvisionedThroughput` in
88- * the SAME call as `BillingMode` ("you must specify read and write capacity
89- * unit values for the table and for each global secondary index") — so that
90- * path deliberately sends both together (Issue #1387). Immutable property
91- * changes (TableName, KeySchema,
86+ * between. `BillingMode` and `GlobalSecondaryIndexUpdates` DO combine, and
87+ * only in the `PAY_PER_REQUEST -> PROVISIONED` flip, where AWS REQUIRES
88+ * per-GSI `ProvisionedThroughput` in the SAME call as `BillingMode` ("you
89+ * must specify read and write capacity unit values for the table and for
90+ * each global secondary index") — so that path deliberately sends both
91+ * together (Issue #1387). Immutable property changes (TableName, KeySchema,
9292 * AttributeDefinitions removal, LocalSecondaryIndexes) throw
9393 * `ProvisioningError` naming the offending field — the deploy engine's
9494 * diff classification should catch these as REPLACEMENT before ever
@@ -654,9 +654,10 @@ export class DynamoDBGlobalTableProvider implements ResourceProvider {
654654 * AWS-side state-machine constraint: `UpdateTable` does not accept
655655 * `ReplicaUpdates` alongside `BillingMode` / `GlobalSecondaryIndexUpdates`,
656656 * so each category must serialize into its own SDK round-trip with a
657- * `waitForTableActiveAfterUpdate` between every step. The ONE exception is
658- * step 4's `PAY_PER_REQUEST -> PROVISIONED` flip, which AWS requires to
659- * carry per-GSI `ProvisionedThroughput` in the same call (Issue #1387).
657+ * `waitForTableActiveAfterUpdate` between every step. `BillingMode` and
658+ * `GlobalSecondaryIndexUpdates` DO combine, and only in step 4's
659+ * `PAY_PER_REQUEST -> PROVISIONED` flip, which AWS requires to carry
660+ * per-GSI `ProvisionedThroughput` in the same call (Issue #1387).
660661 * Order:
661662 * 1. Wait for current ACTIVE (defensive).
662663 * 2. Tags diff (TagResource / UntagResource — no wait needed).
@@ -2785,7 +2786,6 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
27852786 : undefined ;
27862787}
27872788
2788- /** Coerce a CFn numeric (CFn is stringly-typed) to a finite number. */
27892789/**
27902790 * Normalize an explicitly-supplied, already-SDK-shaped throughput block.
27912791 *
@@ -2796,19 +2796,32 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
27962796 * coerced — an inconsistency that only shows up on the rarest input. Picking
27972797 * the known members explicitly also drops junk keys the SDK serializer would
27982798 * discard silently anyway.
2799+ *
2800+ * Returns `undefined` — NOT an empty object — when no member survives
2801+ * coercion. That distinction is load-bearing on the replica overrides: AWS
2802+ * documents an EMPTY `{Provisioned,OnDemand}ThroughputOverride` as "inherit
2803+ * the source table's settings", so assigning `{}` for an unparseable value
2804+ * would turn a loud serialization failure into a silent inherit, and on the
2805+ * GSI side would additionally suppress the derived fallback. Callers treat
2806+ * `undefined` as "no explicit block" and fall through.
27992807 */
28002808function coerceThroughputNumbers < K extends string > (
28012809 block : Record < string , unknown > ,
28022810 members : readonly K [ ]
2803- ) : Record < K , number | undefined > {
2804- const out = { } as Record < K , number | undefined > ;
2811+ ) : Record < K , number > | undefined {
2812+ const out = { } as Record < K , number > ;
2813+ let any = false ;
28052814 for ( const member of members ) {
28062815 const n = toFiniteNumber ( block [ member ] ) ;
2807- if ( n !== undefined ) out [ member ] = n ;
2816+ if ( n !== undefined ) {
2817+ out [ member ] = n ;
2818+ any = true ;
2819+ }
28082820 }
2809- return out ;
2821+ return any ? out : undefined ;
28102822}
28112823
2824+ /** Coerce a CFn numeric (CFn is stringly-typed) to a finite number. */
28122825function toFiniteNumber ( value : unknown ) : number | undefined {
28132826 if ( value === undefined || value === null || value === '' ) return undefined ;
28142827 const n = Number ( value ) ;
@@ -2963,13 +2976,20 @@ export function toSdkGlobalSecondaryIndexes(
29632976 sdk . WarmThroughput = gsi [ 'WarmThroughput' ] as GlobalSecondaryIndex [ 'WarmThroughput' ] ;
29642977 }
29652978
2966- const explicitProvisioned = asRecord ( gsi [ 'ProvisionedThroughput' ] ) ;
2967- const explicitOnDemand = asRecord ( gsi [ 'OnDemandThroughput' ] ) ;
2979+ const explicitProvisioned = asRecord ( gsi [ 'ProvisionedThroughput' ] )
2980+ ? coerceThroughputNumbers ( asRecord ( gsi [ 'ProvisionedThroughput' ] ) ! , [
2981+ 'ReadCapacityUnits' ,
2982+ 'WriteCapacityUnits' ,
2983+ ] )
2984+ : undefined ;
2985+ const explicitOnDemand = asRecord ( gsi [ 'OnDemandThroughput' ] )
2986+ ? coerceThroughputNumbers ( asRecord ( gsi [ 'OnDemandThroughput' ] ) ! , [
2987+ 'MaxReadRequestUnits' ,
2988+ 'MaxWriteRequestUnits' ,
2989+ ] )
2990+ : undefined ;
29682991 if ( explicitProvisioned ) {
2969- sdk . ProvisionedThroughput = coerceThroughputNumbers ( explicitProvisioned , [
2970- 'ReadCapacityUnits' ,
2971- 'WriteCapacityUnits' ,
2972- ] ) as ProvisionedThroughput ;
2992+ sdk . ProvisionedThroughput = explicitProvisioned ;
29732993 } else if ( billingMode === 'PROVISIONED' ) {
29742994 sdk . ProvisionedThroughput = {
29752995 ReadCapacityUnits :
@@ -2983,10 +3003,7 @@ export function toSdkGlobalSecondaryIndexes(
29833003 }
29843004
29853005 if ( explicitOnDemand ) {
2986- sdk . OnDemandThroughput = coerceThroughputNumbers ( explicitOnDemand , [
2987- 'MaxReadRequestUnits' ,
2988- 'MaxWriteRequestUnits' ,
2989- ] ) as OnDemandThroughput ;
3006+ sdk . OnDemandThroughput = explicitOnDemand ;
29903007 } else if ( billingMode !== 'PROVISIONED' ) {
29913008 const maxWrite = toFiniteNumber (
29923009 asRecord ( gsi [ 'WriteOnDemandThroughputSettings' ] ) ?. [ 'MaxWriteRequestUnits' ]
@@ -3039,25 +3056,30 @@ export function toSdkReplicaGlobalSecondaryIndexes(
30393056 const sdk : ReplicaGlobalSecondaryIndex = {
30403057 IndexName : cfn [ 'IndexName' ] as string | undefined ,
30413058 } ;
3042- const explicitProvisioned = asRecord ( cfn [ 'ProvisionedThroughputOverride' ] ) ;
3043- const explicitOnDemand = asRecord ( cfn [ 'OnDemandThroughputOverride' ] ) ;
3059+ // Coerced to `undefined` when nothing parses: AWS reads an EMPTY override
3060+ // as "inherit the source table's settings", so assigning `{}` here would
3061+ // silently change replica behavior instead of failing loudly.
3062+ const explicitProvisionedRaw = asRecord ( cfn [ 'ProvisionedThroughputOverride' ] ) ;
3063+ const explicitOnDemandRaw = asRecord ( cfn [ 'OnDemandThroughputOverride' ] ) ;
3064+ const explicitProvisioned = explicitProvisionedRaw
3065+ ? coerceThroughputNumbers ( explicitProvisionedRaw , [ 'ReadCapacityUnits' ] )
3066+ : undefined ;
3067+ const explicitOnDemand = explicitOnDemandRaw
3068+ ? coerceThroughputNumbers ( explicitOnDemandRaw , [ 'MaxReadRequestUnits' ] )
3069+ : undefined ;
30443070 const readCapacity = deriveReadCapacityUnits (
30453071 asRecord ( cfn [ 'ReadProvisionedThroughputSettings' ] )
30463072 ) ;
30473073 const maxReadRequestUnits = toFiniteNumber (
30483074 asRecord ( cfn [ 'ReadOnDemandThroughputSettings' ] ) ?. [ 'MaxReadRequestUnits' ]
30493075 ) ;
30503076 if ( explicitProvisioned ) {
3051- sdk . ProvisionedThroughputOverride = coerceThroughputNumbers ( explicitProvisioned , [
3052- 'ReadCapacityUnits' ,
3053- ] ) ;
3077+ sdk . ProvisionedThroughputOverride = explicitProvisioned ;
30543078 } else if ( readCapacity !== undefined ) {
30553079 sdk . ProvisionedThroughputOverride = { ReadCapacityUnits : readCapacity } ;
30563080 }
30573081 if ( explicitOnDemand ) {
3058- sdk . OnDemandThroughputOverride = coerceThroughputNumbers ( explicitOnDemand , [
3059- 'MaxReadRequestUnits' ,
3060- ] ) ;
3082+ sdk . OnDemandThroughputOverride = explicitOnDemand ;
30613083 } else if ( maxReadRequestUnits !== undefined ) {
30623084 sdk . OnDemandThroughputOverride = { MaxReadRequestUnits : maxReadRequestUnits } ;
30633085 }
0 commit comments