• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

silvermine / cloudformation-custom-resources / 25378473900

05 May 2026 01:12PM UTC coverage: 0.0%. Remained the same
25378473900

push

github

web-flow
Merge pull request #29 from MrMarCode/MrMarCode/upgrade-node-24

Mr mar code/upgrade node 24

0 of 117 branches covered (0.0%)

Branch coverage included in aggregate %.

0 of 270 new or added lines in 6 files covered. (0.0%)

6 existing lines in 3 files now uncovered.

0 of 356 relevant lines covered (0.0%)

0.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/DynamoDBGlobalTable.js
1
'use strict';
2

NEW
3
const _ = require('underscore'),
×
NEW
4
      util = require('util'),
×
NEW
5
      BaseResource = require('./BaseResource');
×
6

7
const {
8
   DynamoDBClient,
9
   CreateGlobalTableCommand,
10
   UpdateGlobalTableCommand,
11
   DescribeGlobalTableCommand,
12
   CreateTableCommand,
13
   UpdateTableCommand,
14
   DescribeTableCommand,
15
   DeleteTableCommand,
16
   ListTagsOfResourceCommand,
17
   TagResourceCommand,
18
   GlobalTableNotFoundException,
19
   ResourceNotFoundException,
NEW
20
} = require('@aws-sdk/client-dynamodb');
×
21

NEW
22
const AWS_REGION = process.env.AWS_REGION,
×
NEW
23
      dynamo = new DynamoDBClient({});
×
24

NEW
25
const clientCache = { [AWS_REGION]: dynamo };
×
26

27
function clientFor(region) {
NEW
28
   if (!clientCache[region]) {
×
NEW
29
      clientCache[region] = new DynamoDBClient({ region });
×
30
   }
NEW
31
   return clientCache[region];
×
32
}
33

34
function delay(ms) {
NEW
35
   return new Promise((resolve) => {
×
NEW
36
      setTimeout(resolve, ms);
×
37
   });
38
}
39

40
class DynamoDBGlobalTable extends BaseResource {
41

42
   normalizeResourceProperties(props, allowErrors) {
43
      if (props.DeleteUnneededTables && props.DeleteUnneededTables === 'true') {
×
44
         props.DeleteUnneededTables = true;
×
45
      } else {
46
         props.DeleteUnneededTables = false;
×
47
      }
48

49
      if (props.DeploymentRegions) {
×
NEW
50
         props.ReplicationGroup = _.map(props.DeploymentRegions, (dr) => {
×
51
            return { RegionName: dr.region };
×
52
         });
53
      }
54

55
      if (allowErrors && !props.LastStackUpdate) {
×
56
         throw new Error('You must supply the LastStackUpdate property for global table resources. See docs.');
×
57
      }
58

59
      return props;
×
60
   }
61

62
   // In doCreate and doUpdate we delay ten seconds before starting any operations that
63
   // will describe tables because while tables are being created or updated, our describe
64
   // table operation may either (a) not return the table, or (b) return an old
65
   // description of the table. Note that we are assuming (b) based on the documentation
66
   // that clearly states (a) for DescribeTable after CreateTable [1]. It only seems
67
   // logical that describing the table immediately after it was updated would yield the
68
   // same problem because of the eventually consistent query. Thus, this is a safety
69
   // measure to try to avoid getting tables out of sync between regions. While that might
70
   // seem like it would only need to happen in doUpdate, because doCreate is creating the
71
   // global table, we actually don't know in doCreate if the DynamoDB table was also just
72
   // created, or if it has existed for some time and now our global table is being
73
   // created; thus, the actual DynamoDB table could have just been updated. For example,
74
   // perhaps it was created earlier, and just now an index or stream specification is
75
   // being added to it, at the same time our global table was added to the stack.
76
   //
77
   // [1] https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#describeTable-property
78
   //
79
   // Says: Note: If you issue a DescribeTable request immediately after a CreateTable
80
   // request, DynamoDB might return a ResourceNotFoundException. This is because
81
   // DescribeTable uses an eventually consistent query, and the metadata for your table
82
   // might not be available at that moment. Wait for a few seconds, and then try the
83
   // DescribeTable request again.
84

85
   async doCreate(props) {
NEW
86
      const tableName = props.GlobalTableName,
×
NEW
87
            allRegions = _.pluck(props.ReplicationGroup, 'RegionName'),
×
NEW
88
            copyTableRegions = _.chain(props.ReplicationGroup).pluck('RegionName').without(AWS_REGION).value();
×
89

90
      console.log('Pausing ten seconds before starting create for global table %s in regions %s', tableName, allRegions);
×
NEW
91
      await delay(10000);
×
NEW
92
      await this._printDescriptionsOfTables(tableName, [ AWS_REGION ]);
×
NEW
93
      await this._ensureTableCopiedToRegions(tableName, copyTableRegions);
×
NEW
94
      await this._printDescriptionsOfTables(tableName, allRegions);
×
95

NEW
96
      return this._ensureGlobalTableConsistent(props);
×
97
   }
98

99
   async doUpdate(resourceID, props, oldProps) {
NEW
100
      const tableName = props.GlobalTableName,
×
NEW
101
            allRegions = _.pluck(props.ReplicationGroup, 'RegionName'),
×
NEW
102
            oldRegions = _.pluck(oldProps.ReplicationGroup, 'RegionName'),
×
NEW
103
            copyTableRegions = _.without(allRegions, AWS_REGION),
×
NEW
104
            oldCopyTableRegions = _.without(oldRegions, AWS_REGION);
×
105

106
      console.log('Pausing ten seconds before starting update for global table %s in regions %s', tableName, allRegions);
×
NEW
107
      await delay(10000);
×
NEW
108
      await this._printDescriptionsOfTables(tableName, [ AWS_REGION ]);
×
NEW
109
      await this._ensureTableCopiedToRegions(tableName, copyTableRegions);
×
NEW
110
      await this._printDescriptionsOfTables(tableName, _.uniq(allRegions.concat(oldRegions)));
×
111

NEW
112
      const globalTableCloudFormationResp = await this._ensureGlobalTableConsistent(props),
×
NEW
113
            regionsToDelete = _.difference(oldCopyTableRegions, copyTableRegions);
×
114

NEW
115
      if (props.DeleteUnneededTables) {
×
NEW
116
         await this._removeTableFromRegions(tableName, regionsToDelete);
×
NEW
117
         return globalTableCloudFormationResp;
×
118
      }
119

NEW
120
      console.log('Not deleting table %s from regions %s because DeleteUnneededTables was not truthy', tableName, regionsToDelete);
×
NEW
121
      return globalTableCloudFormationResp;
×
122
   }
123

124
   async doDelete(resourceID, props) {
NEW
125
      const tableName = props.GlobalTableName,
×
NEW
126
            copyTableRegions = _.chain(props.ReplicationGroup).pluck('RegionName').without(AWS_REGION).value();
×
127

128
      if (props.DeleteUnneededTables) {
×
NEW
129
         await this._removeTableFromRegions(tableName, copyTableRegions);
×
NEW
130
         return { PhysicalResourceId: props.GlobalTableName };
×
131
      }
132

133
      console.log('Not deleting replica %s tables in %s because DeleteUnneededTables was not truthy', tableName, copyTableRegions);
×
NEW
134
      return { PhysicalResourceId: props.GlobalTableName };
×
135
   }
136

137
   async _ensureTableCopiedToRegions(tableName, regions) {
138
      // Wait for the table to be in any state but DELETING:
NEW
139
      const masterDesc = await this._describeTableUntilState(tableName, AWS_REGION, [ 'CREATING', 'ACTIVE', 'UPDATING' ]);
×
140

NEW
141
      if (!this._hasRequiredStreamSpec(masterDesc)) {
×
NEW
142
         throw new Error('The master table ' + tableName + ' does not have the required NEW_AND_OLD_IMAGES stream enabled');
×
143
      }
144

NEW
145
      const tags = await this._listTags(AWS_REGION, masterDesc.TableArn);
×
146

NEW
147
      return Promise.all(_.map(regions, (region) => {
×
NEW
148
         return this._ensureTableCopiedToRegion(tableName, masterDesc, tags, region);
×
149
      }));
150
   }
151

152
   async _ensureTableCopiedToRegion(tableName, masterDesc, masterTags, region) {
NEW
153
      const dyn = clientFor(region),
×
NEW
154
            copyDesc = await this._describeTable(tableName, region);
×
155

156
      let createOrUpdateResp;
157

NEW
158
      if (copyDesc) {
×
NEW
159
         const params = this._makeUpdateTableParams(tableName, region, masterDesc, copyDesc);
×
160

NEW
161
         if (params) {
×
NEW
162
            console.log('Updating a copy of DynamoDB table %s in %s: %j', tableName, region, params);
×
NEW
163
            createOrUpdateResp = await dyn.send(new UpdateTableCommand(params));
×
164
         } else {
NEW
165
            createOrUpdateResp = { TableDescription: copyDesc };
×
166
         }
167
      } else {
NEW
168
         const params = this._makeCreateTableParamsFromDescription(masterDesc);
×
169

NEW
170
         console.log('Creating a copy of DynamoDB table %s in %s: %j', tableName, region, params);
×
NEW
171
         createOrUpdateResp = await dyn.send(new CreateTableCommand(params));
×
172
      }
173

NEW
174
      const arn = createOrUpdateResp.TableDescription.TableArn,
×
NEW
175
            copyTags = await this._listTags(region, arn);
×
176

NEW
177
      if (_.isEqual(masterTags, copyTags)) {
×
NEW
178
         console.log('No change needed for tags on %s in %s: %j', tableName, region, copyTags);
×
NEW
179
         return;
×
180
      }
181

NEW
182
      console.log('Tagging table %s in %s with tags %j', tableName, region, masterTags);
×
NEW
183
      await dyn.send(new TagResourceCommand({ ResourceArn: arn, Tags: masterTags }));
×
184
   }
185

186
   async _listTags(region, arn) {
NEW
187
      const dyn = clientFor(region);
×
188

NEW
189
      let attempts = 0,
×
UNCOV
190
          timeout = 2000;
×
191

192
      // We allow 15 attempts here (as opposed to 10 when waiting on tables in certain
193
      // states) because it seems to take longer for the list-tags-of-resource operation
194
      // to start showing a new table.
NEW
195
      while (attempts < 15) {
×
UNCOV
196
         attempts = attempts + 1;
×
197

198
         let tagsResp;
199

NEW
200
         try {
×
NEW
201
            tagsResp = await dyn.send(new ListTagsOfResourceCommand({ ResourceArn: arn }));
×
202
         } catch(err) {
NEW
203
            if (err instanceof ResourceNotFoundException) {
×
NEW
204
               console.log('Could not list tags for %s because of ResourceNotFoundException', arn);
×
NEW
205
               tagsResp = null;
×
206
            } else {
UNCOV
207
               throw err;
×
208
            }
209
         }
210

NEW
211
         if (tagsResp) {
×
NEW
212
            if (tagsResp.NextToken) {
×
NEW
213
               throw new Error('Too many tags on table ' + arn + ' for this simplistic tag replication');
×
214
            }
215

NEW
216
            return tagsResp.Tags;
×
217
         }
218

NEW
219
         console.log('Will try listing tags for %s again in %s seconds', arn, (timeout / 1000));
×
NEW
220
         await delay(timeout);
×
NEW
221
         timeout = Math.min(10000, timeout * 1.5);
×
222
      }
223

NEW
224
      throw new Error(util.format('ERROR: Exhausted all %d attempts waiting for %s to have tags', attempts, arn));
×
225
   }
226

227
   async _removeTableFromRegions(tableName, regions) {
228
      if (_.contains(regions, AWS_REGION)) {
×
229
         throw new Error('Should not delete table %s from master region %s', tableName, AWS_REGION);
×
230
      }
231

NEW
232
      return Promise.all(_.map(regions, async (region) => {
×
NEW
233
         const dyn = clientFor(region),
×
NEW
234
               desc = await this._describeTable(tableName, region);
×
235

NEW
236
         if (desc) {
×
NEW
237
            console.log('Deleting table %s in region %s', tableName, region);
×
NEW
238
            await dyn.send(new DeleteTableCommand({ TableName: tableName }));
×
NEW
239
            console.log('Done deleting table %s in region %s', tableName, region);
×
240
         }
241
      }));
242
   }
243

244
   async _describeTable(tableName, region) {
NEW
245
      const dyn = clientFor(region);
×
246

NEW
247
      try {
×
NEW
248
         const resp = await dyn.send(new DescribeTableCommand({ TableName: tableName }));
×
249

NEW
250
         return resp.Table;
×
251
      } catch(err) {
NEW
252
         if (err instanceof ResourceNotFoundException) {
×
NEW
253
            console.log('Table %s does not exist in %s', tableName, region);
×
NEW
254
            return false;
×
255
         }
256

NEW
257
         throw err;
×
258
      }
259
   }
260

261
   async _describeTableUntilState(tableName, region, desiredStates) {
NEW
262
      let attempts = 0,
×
UNCOV
263
          timeout = 2000;
×
264

NEW
265
      while (attempts < 10) {
×
266
         attempts = attempts + 1;
×
267

NEW
268
         const desc = await this._describeTable(tableName, region);
×
269

NEW
270
         if (desc && _.contains(desiredStates, desc.TableStatus)) {
×
NEW
271
            return desc;
×
NEW
272
         } else if (desc) {
×
NEW
273
            console.log('Table %s in %s currently %s (waiting for %s)', tableName, region, desc.TableStatus, desiredStates);
×
274
         } else {
NEW
275
            console.log('Table %s in %s does not yet exist (waiting for it in %s state)', tableName, region, desiredStates);
×
276
         }
277

NEW
278
         console.log('Will try describing %s in %s again in %s seconds', tableName, region, (timeout / 1000));
×
NEW
279
         await delay(timeout);
×
NEW
280
         timeout = Math.min(10000, timeout * 1.5);
×
281
      }
282

283
      // eslint-disable-next-line max-len
NEW
284
      throw new Error(util.format('ERROR: Exhausted all %d attempts waiting for %s:%s to be %s', attempts, tableName, region, desiredStates));
×
285
   }
286

287
   async _printDescriptionsOfTables(tableName, regions) {
NEW
288
      return Promise.all(_.map(regions, async (region) => {
×
NEW
289
         const resp = await this._describeTable(tableName, region);
×
290

NEW
291
         console.log('Table description for %s:%s: %j', tableName, region, resp);
×
292
      }));
293
   }
294

295
   _hasRequiredStreamSpec(desc) {
296
      return desc.StreamSpecification &&
×
297
         desc.StreamSpecification.StreamEnabled &&
298
         desc.StreamSpecification.StreamViewType === 'NEW_AND_OLD_IMAGES';
299
   }
300

301
   _makeCreateTableParamsFromDescription(desc) {
NEW
302
      const params = _.pick(desc, 'AttributeDefinitions', 'KeySchema', 'TableName', 'StreamSpecification'),
×
NEW
303
            srcBillingMode = (desc.BillingModeSummary ? desc.BillingModeSummary.BillingMode : null);
×
304

305
      if (srcBillingMode) {
×
306
         params.BillingMode = srcBillingMode;
×
307
      }
308
      if (srcBillingMode !== 'PAY_PER_REQUEST') {
×
309
         params.ProvisionedThroughput = _.pick(desc.ProvisionedThroughput, 'ReadCapacityUnits', 'WriteCapacityUnits');
×
310
      }
311

312
      if (!_.isEmpty(desc.LocalSecondaryIndexes)) {
×
NEW
313
         params.LocalSecondaryIndexes = _.map(desc.LocalSecondaryIndexes, (lsi) => {
×
314
            return _.pick(lsi, 'IndexName', 'KeySchema', 'Projection');
×
315
         });
316
      }
317

318
      if (!_.isEmpty(desc.GlobalSecondaryIndexes)) {
×
NEW
319
         params.GlobalSecondaryIndexes = _.map(desc.GlobalSecondaryIndexes, (gsi) => {
×
NEW
320
            const newGSI = _.pick(gsi, 'IndexName', 'KeySchema', 'Projection');
×
321

322
            if (srcBillingMode !== 'PAY_PER_REQUEST') {
×
323
               newGSI.ProvisionedThroughput = _.pick(gsi.ProvisionedThroughput, 'ReadCapacityUnits', 'WriteCapacityUnits');
×
324
            }
325
            return newGSI;
×
326
         });
327
      }
328

329
      return params;
×
330
   }
331

332
   _makeUpdateTableParams(tableName, destRegion, master, dest) {
NEW
333
      const params = _.pick(master, 'AttributeDefinitions', 'TableName'),
×
NEW
334
            destParams = _.pick(dest, 'AttributeDefinitions', 'TableName'),
×
NEW
335
            srcBillingMode = (master.BillingModeSummary ? master.BillingModeSummary.BillingMode : null),
×
NEW
336
            destBillingMode = (dest.BillingModeSummary ? dest.BillingModeSummary.BillingMode : null),
×
NEW
337
            baseParamsAreEqual = _.isEqual(params, destParams) && (srcBillingMode === destBillingMode),
×
NEW
338
            indexesBeingUpdated = [];
×
339

340
      // NOTE: on updates we do not copy the provisioned throughput from the master table
341
      // because we never manage throughput through CloudFormation ... we always intend to
342
      // either manage it with our own DynamoDB Capacity Manager (via the
343
      // core:dynamo-provisioning service), or through AWS' own auto-scaling. We would not
344
      // want to compare the current provisioned capacity of the master and dest table and
345
      // copy them here because we could cause errors.
346

347
      // Similarly, we do not update the stream status because it should never change
348
      // after the initial creation since global tables require a specific type of stream.
349

350
      if (srcBillingMode && srcBillingMode !== destBillingMode) {
×
351
         params.BillingMode = srcBillingMode;
×
352
      }
353

354
      // The provisioned throughput setting should only be copied when switching a table
355
      // from on-demand to provisioned. In this case, the table needs an "initial"
356
      // throughput set. However, in all other cases we don't want to copy this value (see
357
      // the note above)
358
      if (srcBillingMode !== 'PAY_PER_REQUEST' && destBillingMode === 'PAY_PER_REQUEST') {
×
359
         params.ProvisionedThroughput = _.pick(master.ProvisionedThroughput, 'ReadCapacityUnits', 'WriteCapacityUnits');
×
360
      }
361

362
      params.GlobalSecondaryIndexUpdates = [];
×
363

364
      // Find indexes on the master table that are deleting (and need to be deleted on the
365
      // destination table), or are missing on the destination and thus need to be
366
      // created.
NEW
367
      _.each(master.GlobalSecondaryIndexes, (masterGSI) => {
×
NEW
368
         const destGSI = _.findWhere(dest.GlobalSecondaryIndexes, { IndexName: masterGSI.IndexName });
×
369

370
         let gsiUpdate;
371

372
         if (destGSI && masterGSI.IndexStatus === 'DELETING') {
×
373
            console.log(
×
374
               'Need to delete index %s:%s in %s because it exists on dest table and is DELETING on the master table',
375
               tableName,
376
               masterGSI.IndexName,
377
               destRegion
378
            );
379

380
            params.GlobalSecondaryIndexUpdates.push({ Delete: _.pick(masterGSI, 'IndexName') });
×
381
            indexesBeingUpdated.push(masterGSI.IndexName);
×
382
         } else if (!destGSI) {
×
383
            console.log('Need to create index %s:%s in %s', tableName, masterGSI.IndexName, destRegion);
×
384
            gsiUpdate = { Create: _.pick(masterGSI, 'IndexName', 'KeySchema', 'Projection') };
×
385
            if (srcBillingMode !== 'PAY_PER_REQUEST') {
×
386
               gsiUpdate.Create.ProvisionedThroughput = _.pick(masterGSI.ProvisionedThroughput, 'ReadCapacityUnits', 'WriteCapacityUnits');
×
387
            }
388
            params.GlobalSecondaryIndexUpdates.push(gsiUpdate);
×
389
            indexesBeingUpdated.push(masterGSI.IndexName);
×
390
         }
391
      });
392

393
      // If the source table's billing mode is 'PROVISIONED', but the destination table's
394
      // mode is 'PAY_PER_REQUEST', then we will be changing it to PROVISIONED, and thus
395
      // need to update all the indexes to include the provisioned capacity.
396
      // Note that there's some oddness here: when the table's billing mode is
397
      // 'PROVISIONED', you may not actually get back the BillingModeSummary in the table
398
      // description. That's why we use `srcBillingMode !== 'PAY_PER_REQUEST'` everywhere
399
      // in this class - because if it's pay per request, you'll always get the billing
400
      // mode back.
401
      if (srcBillingMode !== 'PAY_PER_REQUEST' && destBillingMode === 'PAY_PER_REQUEST') {
×
NEW
402
         _.each(master.GlobalSecondaryIndexes, (masterGSI) => {
×
403
            if (_.contains(indexesBeingUpdated, masterGSI.IndexName) || masterGSI.IndexStatus === 'DELETING') {
×
404
               // This index is already in our call params, or it's being deleted.
405
               return;
×
406
            }
407

408
            params.GlobalSecondaryIndexUpdates.push({
×
409
               Update: {
410
                  IndexName: masterGSI.IndexName,
411
                  ProvisionedThroughput: _.pick(masterGSI.ProvisionedThroughput, 'ReadCapacityUnits', 'WriteCapacityUnits'),
412
               },
413
            });
414
         });
415
      }
416

417
      // Now find indexes that only the destination table has, since they must have been
418
      // deleted from the master table.
NEW
419
      _.each(dest.GlobalSecondaryIndexes, (destGSI) => {
×
NEW
420
         const masterGSI = _.findWhere(master.GlobalSecondaryIndexes, { IndexName: destGSI.IndexName });
×
421

422
         if (!masterGSI) {
×
423
            console.log(
×
424
               'Need to delete index %s:%s in %s because it exists on dest table and does not exist on master table',
425
               tableName,
426
               destGSI.IndexName,
427
               destRegion
428
            );
429

430
            params.GlobalSecondaryIndexUpdates.push({ Delete: _.pick(destGSI, 'IndexName') });
×
431
         }
432
      });
433

434

435
      if (baseParamsAreEqual && _.isEmpty(params.GlobalSecondaryIndexUpdates)) {
×
436
         // There are no updates to be made
437
         console.log('There are no updates to be made to %s in %s', tableName, destRegion);
×
438
         return false;
×
439
      } else if (_.isEmpty(params.GlobalSecondaryIndexUpdates)) {
×
440
         console.log('There are no GlobalSecondaryIndexUpdates to be made to %s in %s', tableName, destRegion);
×
441
         delete params.GlobalSecondaryIndexUpdates;
×
442
      }
443

444
      return params;
×
445
   }
446

447
   async _ensureGlobalTableConsistent(props) {
NEW
448
      const tableName = props.GlobalTableName,
×
NEW
449
            desc = await this._describeGlobalTable(tableName);
×
450

NEW
451
      if (desc) {
×
NEW
452
         return this._updateGlobalTable(props, desc);
×
453
      }
454

NEW
455
      return this._createGlobalTable(props);
×
456
   }
457

458
   async _createGlobalTable(props) {
NEW
459
      await this._waitForTablesCreatingOrActive(props.GlobalTableName, _.pluck(props.ReplicationGroup, 'RegionName'));
×
460

NEW
461
      const params = _.pick(props, 'GlobalTableName', 'ReplicationGroup');
×
462

NEW
463
      console.log('Creating global table: %j', params);
×
NEW
464
      const resp = await dynamo.send(new CreateGlobalTableCommand(params));
×
465

NEW
466
      console.log('createGlobalTable response: %j', resp);
×
NEW
467
      return { PhysicalResourceId: props.GlobalTableName, Arn: resp.GlobalTableDescription.GlobalTableArn };
×
468
   }
469

470
   async _updateGlobalTable(props, desc) {
NEW
471
      const tableName = props.GlobalTableName,
×
NEW
472
            desiredRegions = _.pluck(props.ReplicationGroup, 'RegionName'),
×
NEW
473
            existingRegions = _.pluck(desc.ReplicationGroup, 'RegionName'),
×
NEW
474
            params = { GlobalTableName: tableName, ReplicaUpdates: [] };
×
475

476
      console.log('Updating global table %s to match props %j', tableName, props);
×
477
      console.log('The description of the current global table %s is: %j', tableName, desc);
×
478

479
      // add missing regions:
NEW
480
      _.each(_.difference(desiredRegions, existingRegions), (region) => {
×
481
         params.ReplicaUpdates.push({ Create: { RegionName: region } });
×
482
      });
483

484
      // remove extra regions:
NEW
485
      _.each(_.difference(existingRegions, desiredRegions), (region) => {
×
486
         params.ReplicaUpdates.push({ Delete: { RegionName: region } });
×
487
      });
488

489
      if (_.isEmpty(params.ReplicaUpdates)) {
×
490
         console.log('No update needed for global table %s', tableName);
×
NEW
491
         return { PhysicalResourceId: props.GlobalTableName, Arn: desc.GlobalTableArn };
×
492
      }
493

NEW
494
      await this._waitForTablesCreatingOrActive(tableName, desiredRegions.concat(existingRegions));
×
495

NEW
496
      console.log('Updating global table %s with params: %j', tableName, params);
×
NEW
497
      await dynamo.send(new UpdateGlobalTableCommand(params));
×
498

NEW
499
      return { PhysicalResourceId: props.GlobalTableName, Arn: desc.GlobalTableArn };
×
500
   }
501

502
   async _waitForTablesCreatingOrActive(tableName, regions) {
503
      // Whenever you modify a global table, all of the tables in the global table
504
      // replication group must be in either CREATING or ACTIVE state. Often when a table
505
      // is first created it will temporarily change CREATING -> ACTIVE -> UPDATING, and
506
      // then back to ACTIVE. If we happen to try to updateGlobalTable before the table is
507
      // ACTIVE, we will get an error.
508
      console.log('Waiting for %s in %s to be CREATING or ACTIVE', tableName, regions);
×
NEW
509
      return Promise.all(_.map(regions, (region) => {
×
510
         return this._describeTableUntilState(tableName, region, [ 'CREATING', 'ACTIVE' ]);
×
511
      }));
512
   }
513

514
   async _describeGlobalTable(tableName) {
NEW
515
      try {
×
NEW
516
         const resp = await dynamo.send(new DescribeGlobalTableCommand({ GlobalTableName: tableName }));
×
517

NEW
518
         return resp.GlobalTableDescription;
×
519
      } catch(err) {
NEW
520
         if (err instanceof GlobalTableNotFoundException) {
×
NEW
521
            return false;
×
522
         }
523

NEW
524
         throw err;
×
525
      }
526
   }
527

528
}
529

NEW
530
module.exports = DynamoDBGlobalTable;
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc