diff --git a/index.js b/index.js index eff5033..e6a50c6 100644 --- a/index.js +++ b/index.js @@ -310,11 +310,11 @@ class Table { } while (!created); if (!tableHasRequiredIndexes) { - await this.#updateIndexes({differentIndexes, missingIndexes, existingIndexes:response.Table.GlobalSecondaryIndexes}); + await this.#updateIndexes({differentIndexes, missingIndexes, existingIndexes: response.Table.GlobalSecondaryIndexes, createAll: waitForIndexes}); } if (waitForIndexes) { - await this.#waitForIndexes(); + await this.#waitForIndexesActive(); } this[kTableIsReady] = true; @@ -499,52 +499,48 @@ class Table { return {uniqueRequiredAttributes}; } - async #updateIndexes({differentIndexes, missingIndexes, existingIndexes}) { + async #updateIndexes({differentIndexes, missingIndexes, existingIndexes, createAll}) { if (differentIndexes.length) { this.#logger.warn({existingIndexes, differentIndexes}, `WARNING: indexes "${differentIndexes.map(i => i.index.IndexName).join(',')}" differ from the current specifications, but these will not be automatically updated.`); } - // FIXME: we can only add one missing index at a time, so just try to - // add the first one. Need a createIndexes option, as this could be a - // long wait if we need to create many? (Subscriber limit exceeded: - // Only 1 online index can be created or deleted simultaneously per - // table -> each update table command can only create one index.... - // "You can create or delete only one global secondary index per - // UpdateTable operation.") if (missingIndexes.length) { - const updates = { - TableName: this.name, - GlobalSecondaryIndexUpdates: [], - AttributeDefinitions: [] - }; - // we only need to include the attribute definitions required by - // the indexes being created, existing attribute definitions used - // by other indexes do not need to be repeated: - // FIXME see above, only adding the first missing one: - //for (const missingIndex of missingIndexes) { - // updates.GlobalSecondaryIndexUpdates.push({Create: missingIndex.index}); - // updates.AttributeDefinitions = updates.AttributeDefinitions.concat(missingIndex.requiredAttributes); - //} - const missingIndex = missingIndexes.shift(); - updates.GlobalSecondaryIndexUpdates.push({Create: missingIndex.index}); - updates.AttributeDefinitions = updates.AttributeDefinitions.concat(missingIndex.requiredAttributes); - - this.#logger.info({updates}, 'Updating table %s.', this.name); - await this[kTableDDBClient].send(new UpdateTableCommand(updates)); + // Only one index can be added at a time: + for (const missingIndex of missingIndexes) { + const updates = { + TableName: this.name, + GlobalSecondaryIndexUpdates: [], + AttributeDefinitions: [] + }; + // we only need to include the attribute definitions required by + // the indexes being created, existing attribute definitions used + // by other indexes do not need to be repeated: + updates.GlobalSecondaryIndexUpdates.push({Create: missingIndex.index}); + updates.AttributeDefinitions = updates.AttributeDefinitions.concat(missingIndex.requiredAttributes); + + this.#logger.info({updates}, 'Updating table %s.', this.name); + await this[kTableDDBClient].send(new UpdateTableCommand(updates)); + + if (createAll) { + await this.#waitForIndexesActive(); + } else { + break; + } + } } } - async #waitForIndexes() { - let response; + async #waitForIndexesActive() { while (true) { - response = await this[kTableDDBClient].send(new DescribeTableCommand({TableName: this.name})); - // TODO: should be able to cover this actually - /* c8 ignore else */ - if (response.Table.TableStatus === 'ACTIVE') { + const response = await this[kTableDDBClient].send(new DescribeTableCommand({TableName: this.name})); + if (response.Table.TableStatus === 'ACTIVE' && + (response.Table.GlobalSecondaryIndexes || []).every(gsi => gsi.IndexStatus === 'ACTIVE')) { break; - } else if (response.Table.TableStatus === 'UPDATING'){ - await new Promise(resolve => setTimeout(resolve, 500)); + } else if (['UPDATING', 'ACTIVE'].includes(response.Table.TableStatus) || + (response.Table.GlobalSecondaryIndexes || []).every(gsi => ['CREATING', 'UPDATING', 'ACTIVE'].includes(gsi.IndexStatus))) { + // TODO: probably want exponential backoff here too... + await delayMs(500); } else { - throw new Error(`Table ${this.name} status is ${response.Table.TableStatus}.`); + throw new Error(`Table ${this.name} status is ${response.Table.TableStatus}, index statuses are ${(response.Table.GlobalSecondaryIndexes || []).map(gsi => gsi.IndexStatus).join(', ')}.`); } } } diff --git a/package.json b/package.json index b0df553..9ce8c8a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ ], "files": [ "test" - ] + ], + "timeout": 60000 }, "files": [ "index.js" diff --git a/test/index.js b/test/index.js index 36b7242..19f3455 100644 --- a/test/index.js +++ b/test/index.js @@ -321,4 +321,56 @@ t.test('crud:', async t => { t.end(); }); +t.test('wait for index creation', async t => { + const Schema1_noIndexes = DynamoDM.Schema('1', { + properties: { + aString: {type:'string'}, + bNum: {type:'number'}, + } + }); + const Schema1_withIndexes = DynamoDM.Schema('1', { + properties: { + aString: {type:'string'}, + bNum: {type:'number'}, + } + }, { + index: { + aString:1, + indexWithHashAndSortKey: { + sortKey:'aString', + hashKey:'bNum' + } + } + }); + + // first create a table, and wait for its built-in indexes to be created + const table1 = DynamoDM.Table({ name: 'test-index-creation'}, {clientOptions}); + const Model1a = table1.model(Schema1_noIndexes); + await table1.ready({waitForIndexes:true}); + + // and delete it when the test is finished + t.teardown(async () => { + await table1.deleteTable(); + }); + + // add lots of models, so that creating the indexes for this table takes some amount of time: + for (let i = 0; i < 400; i++) { + await (new Model1a({aString:`value ${i}`, bNum:i})).save(); + } + t.pass('created models ok'); + + // now create another reference to the existing table, which requires more indexes, and wait for them: + const table2 = DynamoDM.Table({ name: 'test-index-creation'}, {clientOptions}); + const Model1b = table2.model(Schema1_withIndexes); + + // wait for the indexes to be created + await table2.ready({waitForIndexes:true}); + + // and check that we can query using the indexes immediately: + t.equal((await Model1b.queryMany({bNum:4})).length, 1); + t.equal((await Model1b.queryMany({aString:{$gt: 'value 50'}, bNum:99})).length, 1); + + t.end(); +}); + t.end(); diff --git a/test/schemas.js b/test/schemas.js index 2422143..dfce9e2 100644 --- a/test/schemas.js +++ b/test/schemas.js @@ -298,6 +298,7 @@ t.test('basic schemas:', async t => { t.resolves(FooModel.testAsyncStatic(), 'async statics work'); }); }); + t.test('virtuals:', async t => { t.test('string aliases', async t => { const table = DynamoDM.Table({ name: 'test-table-2'}); @@ -538,7 +539,6 @@ t.test('custom field names', async t => { t.hasOwnProps(doc, ['myId', 'myType', 'myCreatedAt', 'myUpdatedAt']); }); - t.test('schema errors', async t => { const table = DynamoDM.Table({ name: 'test-table-schemas'});