dynamoose
Version:
Dynamoose is a modeling tool for Amazon's DynamoDB (inspired by Mongoose)
273 lines (272 loc) • 14.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateTable = exports.waitForActive = exports.updateTimeToLive = exports.createTable = exports.createTableRequest = exports.getTagDetails = exports.getTableDetails = void 0;
const _1 = require(".");
const Internal_1 = require("../Internal");
const { internalProperties } = Internal_1.default.General;
const DynamoDB = require("@aws-sdk/client-dynamodb");
const internal_1 = require("../aws/ddb/internal");
const utils_1 = require("../utils");
const Error_1 = require("../Error");
const index_changes_1 = require("../utils/dynamoose/index_changes");
const types_1 = require("./types");
const defaults = require("./defaults");
// Utility functions
async function getTableDetails(table, settings = {}) {
const func = async () => {
const tableDetails = await (0, internal_1.default)(table.getInternalProperties(internalProperties).instance, "describeTable", { "TableName": table.getInternalProperties(internalProperties).name });
table.getInternalProperties(internalProperties).latestTableDetails = tableDetails; // eslint-disable-line require-atomic-updates
};
if (settings.forceRefresh || !table.getInternalProperties(internalProperties).latestTableDetails) {
if (settings.allowError) {
try {
await func();
}
catch (e) { } // eslint-disable-line no-empty
}
else {
await func();
}
}
return table.getInternalProperties(internalProperties).latestTableDetails;
}
exports.getTableDetails = getTableDetails;
function getExpectedTags(table) {
const tagEntries = Object.entries(table.getInternalProperties(internalProperties).options.tags);
if (tagEntries.length === 0) {
return undefined;
}
else {
return tagEntries.map(([Key, Value]) => ({
Key,
Value
}));
}
}
async function getTagDetails(table) {
const tableDetails = await getTableDetails(table);
const instance = table.getInternalProperties(internalProperties).instance;
const tags = await (0, internal_1.default)(instance, "listTagsOfResource", {
"ResourceArn": tableDetails.Table.TableArn
});
while (tags.NextToken) {
// TODO: The timeout below causes tests to fail, so we disable it for now. We should also probably only run a timeout if we get an error from AWS.
// await timeout(100); // You can call ListTagsOfResource up to 10 times per second, per account.
const nextTags = await (0, internal_1.default)(instance, "listTagsOfResource", {
"ResourceArn": tableDetails.Table.TableArn,
"NextToken": tags.NextToken
});
tags.NextToken = nextTags.NextToken;
tags.Tags = [...tags.Tags, ...nextTags.Tags];
}
return tags;
}
exports.getTagDetails = getTagDetails;
async function createTableRequest(table) {
const object = Object.assign(Object.assign({ "TableName": table.getInternalProperties(internalProperties).name }, utils_1.default.dynamoose.get_provisioned_throughput(table.getInternalProperties(internalProperties).options)), await table.getInternalProperties(internalProperties).getCreateTableAttributeParams());
if (table.getInternalProperties(internalProperties).options.tableClass === types_1.TableClass.infrequentAccess) {
object.TableClass = DynamoDB.TableClass.STANDARD_INFREQUENT_ACCESS;
}
const tags = getExpectedTags(table);
if (tags) {
object.Tags = tags;
}
// Add stream specification if enabled
if (table.getInternalProperties(internalProperties).options.streamOptions.enabled) {
const streamOptions = table.getInternalProperties(internalProperties).options.streamOptions;
object.StreamSpecification = {
"StreamEnabled": true,
"StreamViewType": streamOptions.type
};
}
return object;
}
exports.createTableRequest = createTableRequest;
async function createTable(table, force = false) {
var _a, _b;
const tableStatus = (_b = (_a = (await getTableDetails(table, { "allowError": true }))) === null || _a === void 0 ? void 0 : _a.Table) === null || _b === void 0 ? void 0 : _b.TableStatus;
if (!force && tableStatus === "ACTIVE") {
table.getInternalProperties(internalProperties).alreadyCreated = true;
return () => Promise.resolve.bind(Promise)();
}
await (0, internal_1.default)(table.getInternalProperties(internalProperties).instance, "createTable", await createTableRequest(table));
}
exports.createTable = createTable;
async function updateTimeToLive(table) {
let ttlDetails;
const instance = table.getInternalProperties(internalProperties).instance;
async function updateDetails() {
ttlDetails = await (0, internal_1.default)(instance, "describeTimeToLive", {
"TableName": table.getInternalProperties(internalProperties).name
});
}
await updateDetails();
function updateTTL() {
return (0, internal_1.default)(instance, "updateTimeToLive", {
"TableName": table.getInternalProperties(internalProperties).name,
"TimeToLiveSpecification": {
"AttributeName": table.getInternalProperties(internalProperties).options.expires.attribute,
"Enabled": true
}
});
}
switch (ttlDetails.TimeToLiveDescription.TimeToLiveStatus) {
case "DISABLING":
while (ttlDetails.TimeToLiveDescription.TimeToLiveStatus === "DISABLING") {
await utils_1.default.timeout(1000);
await updateDetails();
}
// fallthrough
case "DISABLED":
await updateTTL();
break;
/* istanbul ignore next */
default:
break;
}
}
exports.updateTimeToLive = updateTimeToLive;
function waitForActive(table, forceRefreshOnFirstAttempt = true) {
return () => new Promise((resolve, reject) => {
const start = Date.now();
async function check(count) {
var _a;
const waitForActiveSettingValue = table.getInternalProperties(internalProperties).options.waitForActive;
if (typeof waitForActiveSettingValue !== "boolean" || waitForActiveSettingValue === true) {
const waitForActiveSetting = typeof waitForActiveSettingValue === "boolean" ? defaults.original.waitForActive : waitForActiveSettingValue;
try {
// Normally we'd want to do `dynamodb.waitFor` here, but since it doesn't work with tables that are being updated we can't use it in this case
const tableDetails = (await getTableDetails(table, { "forceRefresh": forceRefreshOnFirstAttempt === true ? forceRefreshOnFirstAttempt : count > 0 })).Table;
if (tableDetails.TableStatus === "ACTIVE" && ((_a = tableDetails.GlobalSecondaryIndexes) !== null && _a !== void 0 ? _a : []).every((val) => val.IndexStatus === "ACTIVE")) {
return resolve();
}
}
catch (e) {
return reject(e);
}
if (count > 0) {
waitForActiveSetting.check.frequency === 0 ? await utils_1.default.set_immediate_promise() : await utils_1.default.timeout(waitForActiveSetting.check.frequency);
}
if (Date.now() - start >= waitForActiveSetting.check.timeout) {
return reject(new Error_1.default.WaitForActiveTimeout(`Wait for active timed out after ${Date.now() - start} milliseconds.`));
}
else {
check(++count);
}
}
}
check(0);
});
}
exports.waitForActive = waitForActive;
async function updateTable(table) {
var _a, _b;
const updateAll = typeof table.getInternalProperties(internalProperties).options.update === "boolean" && table.getInternalProperties(internalProperties).options.update;
const instance = table.getInternalProperties(internalProperties).instance;
// Throughput
if (updateAll || table.getInternalProperties(internalProperties).options.update.includes(_1.TableUpdateOptions.throughput)) {
const currentThroughput = (await getTableDetails(table)).Table;
const expectedThroughput = utils_1.default.dynamoose.get_provisioned_throughput(table.getInternalProperties(internalProperties).options);
const isThroughputUpToDate = expectedThroughput.BillingMode === (currentThroughput.BillingModeSummary || {}).BillingMode && expectedThroughput.BillingMode || (currentThroughput.ProvisionedThroughput || {}).ReadCapacityUnits === (expectedThroughput.ProvisionedThroughput || {}).ReadCapacityUnits && currentThroughput.ProvisionedThroughput.WriteCapacityUnits === expectedThroughput.ProvisionedThroughput.WriteCapacityUnits;
if (!isThroughputUpToDate) {
const object = Object.assign({ "TableName": table.getInternalProperties(internalProperties).name }, expectedThroughput);
await (0, internal_1.default)(instance, "updateTable", object);
await waitForActive(table)();
}
}
// Indexes
if (updateAll || table.getInternalProperties(internalProperties).options.update.includes(_1.TableUpdateOptions.indexes)) {
const tableDetails = await getTableDetails(table);
const existingIndexes = tableDetails.Table.GlobalSecondaryIndexes;
const updateIndexes = await utils_1.default.dynamoose.index_changes(table, existingIndexes);
await updateIndexes.reduce(async (existingFlow, index) => {
await existingFlow;
const params = {
"TableName": table.getInternalProperties(internalProperties).name
};
if (index.type === index_changes_1.TableIndexChangeType.add) {
params.AttributeDefinitions = (await table.getInternalProperties(internalProperties).getCreateTableAttributeParams()).AttributeDefinitions;
params.GlobalSecondaryIndexUpdates = [{ "Create": index.spec }];
}
else {
params.GlobalSecondaryIndexUpdates = [{ "Delete": { "IndexName": index.name } }];
}
await (0, internal_1.default)(instance, "updateTable", params);
await waitForActive(table)();
}, Promise.resolve());
}
// Tags
if (updateAll || table.getInternalProperties(internalProperties).options.update.includes(_1.TableUpdateOptions.tags)) {
try {
const currentTags = (await getTagDetails(table)).Tags;
const expectedTags = table.getInternalProperties(internalProperties).options.tags;
let tableDetails;
const tagsToDelete = currentTags.filter((tag) => expectedTags[tag.Key] !== tag.Value).map((tag) => tag.Key);
if (tagsToDelete.length > 0) {
tableDetails = await getTableDetails(table);
await (0, internal_1.default)(instance, "untagResource", {
"ResourceArn": tableDetails.Table.TableArn,
"TagKeys": tagsToDelete
});
}
const tagsToAdd = Object.keys(expectedTags).filter((key) => tagsToDelete.includes(key) || !currentTags.some((tag) => tag.Key === key));
if (tagsToAdd.length > 0) {
tableDetails = tableDetails || await getTableDetails(table);
await (0, internal_1.default)(instance, "tagResource", {
"ResourceArn": tableDetails.Table.TableArn,
"Tags": tagsToAdd.map((key) => ({
"Key": key,
"Value": expectedTags[key]
}))
});
}
}
catch (error) {
if (error.name === "UnknownOperationException" && error.message === "Tagging is not currently supported in DynamoDB Local.") {
console.warn(`Tagging is not currently supported in DynamoDB Local. Skipping tag update for table: ${table.name}`); // eslint-disable-line no-console
}
else {
throw error;
}
}
}
// Table Class
if (updateAll || table.getInternalProperties(internalProperties).options.update.includes(_1.TableUpdateOptions.tableClass)) {
const tableDetails = (await getTableDetails(table)).Table;
const expectedDynamoDBTableClass = table.getInternalProperties(internalProperties).options.tableClass === types_1.TableClass.infrequentAccess ? DynamoDB.TableClass.STANDARD_INFREQUENT_ACCESS : DynamoDB.TableClass.STANDARD;
if (!tableDetails.TableClassSummary && expectedDynamoDBTableClass !== DynamoDB.TableClass.STANDARD || tableDetails.TableClassSummary && tableDetails.TableClassSummary.TableClass !== expectedDynamoDBTableClass) {
const object = {
"TableName": table.getInternalProperties(internalProperties).name,
"TableClass": expectedDynamoDBTableClass
};
await (0, internal_1.default)(instance, "updateTable", object);
await waitForActive(table)();
}
}
// Streams
if (updateAll || table.getInternalProperties(internalProperties).options.update.includes(_1.TableUpdateOptions.streams)) {
const tableDetails = (await getTableDetails(table)).Table;
const streamOptions = table.getInternalProperties(internalProperties).options.streamOptions;
if (streamOptions) {
const currentStreamEnabled = !!((_a = tableDetails.StreamSpecification) === null || _a === void 0 ? void 0 : _a.StreamEnabled);
const currentStreamViewType = (_b = tableDetails.StreamSpecification) === null || _b === void 0 ? void 0 : _b.StreamViewType;
const updateStreamEnabled = streamOptions.enabled;
const updateStreamViewType = streamOptions.type || undefined;
// Only update if stream settings are different
if (currentStreamEnabled !== updateStreamEnabled || updateStreamEnabled && currentStreamViewType !== updateStreamViewType) {
const object = {
"TableName": table.getInternalProperties(internalProperties).name,
"StreamSpecification": {
"StreamEnabled": updateStreamEnabled
}
};
if (updateStreamEnabled && updateStreamViewType) {
object.StreamSpecification.StreamViewType = updateStreamViewType;
}
await (0, internal_1.default)(instance, "updateTable", object);
await waitForActive(table)();
}
}
}
}
exports.updateTable = updateTable;