@cpmech/az-dynamo
Version:
Auxiliary Tools for DynamoDB
508 lines (485 loc) • 16.8 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var AWS = require('aws-sdk');
var basic = require('@cpmech/basic');
var util = require('@cpmech/util');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var AWS__default = /*#__PURE__*/_interopDefaultLegacy(AWS);
// create creates data in DB
// NOTE: this function will call get first to check if the item exists already;
// So, this is NOT very efficient
const create = async (table, primaryKey, data) => {
const ddb = new AWS.DynamoDB.DocumentClient();
// check if item exists already
const exists = await ddb
.get({
TableName: table,
Key: primaryKey,
AttributesToGet: Object.keys(primaryKey),
})
.promise();
if (exists.Item) {
throw new Error(`item with key = ${JSON.stringify(primaryKey)} exists already`);
}
// put item (cannot use ReturnValues)
await ddb
.put({
TableName: table,
Item: {
...primaryKey,
...data,
},
})
.promise();
};
// exists checks if item exists in table or not
const exists = async (table, primaryKey) => {
const ddb = new AWS.DynamoDB.DocumentClient();
const data = await ddb
.get({
TableName: table,
Key: primaryKey,
AttributesToGet: Object.keys(primaryKey),
})
.promise();
return !!data.Item;
};
// get gets a single item from DB
// NOTE: this function will return null if the item does not exist
const get = async (table, primaryKey) => {
const ddb = new AWS.DynamoDB.DocumentClient();
const data = await ddb
.get({
TableName: table,
Key: primaryKey,
})
.promise();
if (data.Item) {
return data.Item;
}
return null;
};
// getT gets data in a single TRANSACTION
// (even from different tables in the same region)
//
// From the AWS: It returns an ordered array of up to 25 ItemResponse objects,
// each of which corresponds to the TransactGetItem object in the same position
// in the TransactItems array
//
// i.e. It returns in the SAME ORDER as the input
//
// Reference: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#transactGetItems-property
//
const getT = async (items) => {
// params
const TransactItems = items.map(({ table, primaryKey }) => ({
Get: {
TableName: table,
Key: primaryKey,
},
}));
// transaction
const ddb = new AWS.DynamoDB.DocumentClient();
const res = await ddb.transactGet({ TransactItems }).promise();
if (res.Responses) {
return res.Responses.map(r => r.Item || {});
}
return [];
};
// getBatch gets many items from the DB
// NOTE: this function will return null if there are no items
const getBatch = async (table, keys, // must be less than 100 items
filterAttributes) => {
// dynamodb
const ddb = new AWS.DynamoDB.DocumentClient();
// request
const res = await ddb
.batchGet({
RequestItems: {
[table]: {
Keys: keys,
ProjectionExpression: filterAttributes,
},
},
})
.promise();
// check for unprocessed keys
if (res.UnprocessedKeys && basic.hasProp(res.UnprocessedKeys, table)) {
throw new Error('getBatch: cannot handle unprocessed keys at this time');
}
// extract results
if (res.Responses && basic.hasProp(res.Responses, table)) {
return res.Responses[table];
}
// there is nothing
return [];
};
// key2ddbKey converts (primary) Ikey to (raw) DynamoDB.Key
const key2ddbKey = (keyObject) => {
return Object.keys(keyObject).reduce((acc, curr) => ({
...acc,
[curr]: { S: keyObject[curr] },
}), {});
};
// increment increments attribute by 1 (or incVal)
const increment = async (table, primaryKey, attributeName, incVal = 1) => {
const ddb = new AWS.DynamoDB();
await ddb
.updateItem({
TableName: table,
Key: key2ddbKey(primaryKey),
UpdateExpression: `ADD ${attributeName} :incval`,
ExpressionAttributeValues: { ':incval': { N: `${incVal}` } },
})
.promise();
};
// put puts data in DB and returns the new values
// NOTE: (1) this function will return null if data is empty like {}
// (2) the primaryKey (partition or sort) MAY be present in 'data'
// (3) the partitiona/sort values in data will override the values in primaryKey;
// thus they should NOT be different that the ones in primaryKey
const put = async (table, primaryKey, data) => {
const input = { ...primaryKey, ...data };
if (Object.keys(input).length === 0) {
return null;
}
const ddb = new AWS.DynamoDB.DocumentClient();
await ddb
.put({
TableName: table,
Item: input,
ReturnValues: 'ALL_OLD',
})
.promise();
};
// query returns one or more items from the DB
// NOTE: the hashKey is essential and the rangeKey is optional
const query = async ({ table, index, pkName: pkName, pkValue: pkValue, skName: skName, skValue: skValue, skValue2: skValue2, op, }) => {
// set default op
if (!op) {
op = '=';
}
// params
const params = {
TableName: table,
IndexName: index,
};
// with primary and sort keys and a second value for the sort condition (BETWEEN)
if (skName && skValue2) {
params.ExpressionAttributeNames = {
[`#${pkName}`]: pkName,
[`#${skName}`]: skName,
};
params.ExpressionAttributeValues = {
':hval': pkValue,
':rval': skValue,
':rval2': skValue2,
};
params.KeyConditionExpression = `#${pkName} = :hval and #${skName} BETWEEN :rval AND :rval2`;
}
// with hash (primary) and range (sort) keys
else if (skName) {
params.ExpressionAttributeNames = {
[`#${pkName}`]: pkName,
[`#${skName}`]: skName,
};
params.ExpressionAttributeValues = {
':hval': pkValue,
':rval': skValue,
};
params.KeyConditionExpression =
op === 'prefix'
? `#${pkName} = :hval and begins_with(#${skName}, :rval)`
: `#${pkName} = :hval and #${skName} ${op} :rval`;
}
// just hash key (primary key)
else {
params.ExpressionAttributeNames = { [`#${pkName}`]: pkName };
params.ExpressionAttributeValues = { ':hval': pkValue };
params.KeyConditionExpression = `#${pkName} = :hval`;
}
// perform query
const ddb = new AWS.DynamoDB.DocumentClient();
const data = await ddb.query(params).promise();
// check
if (data.LastEvaluatedKey) {
throw new Error('cannot handle partial results just yet');
}
// results
return data.Items ? data.Items : [];
};
// removeAttributes deletes attributes in item (keeps item)
const removeAttributes = async (table, primaryKey, attributeNames) => {
const ddb = new AWS.DynamoDB.DocumentClient();
const atts = attributeNames.join(', ');
await ddb
.update({
TableName: table,
Key: primaryKey,
UpdateExpression: `REMOVE ${atts}`,
})
.promise();
};
// removeItem deletes the whole item, including attributes
const removeItem = async (table, primaryKey) => {
const ddb = new AWS.DynamoDB.DocumentClient();
await ddb
.delete({
TableName: table,
Key: primaryKey,
})
.promise();
};
// scan returns all items from the table
// NOTE: the hashKey is not needed here, but the rangeKey is essential
//
// NOTE from the AWS:
//
// In general, Scan operations are less efficient than other operations in DynamoDB.
// A Scan operation always scans the entire table or secondary index.
// It then filters out values to provide the result you want,
// essentially adding the extra step of removing data from the result set.
//
// If possible, you should avoid using a Scan operation on a large table or index
// with a filter that removes many results.
//
// Also, as a table or index grows, the Scan operation slows.
// The Scan operation examines every item for the requested values and
// can use up the provisioned throughput for a large table or index in a single operation.
//
// For faster response times, design your tables and indexes so that your
// applications can use Query instead of Scan.
// (For tables, you can also consider using the GetItem and BatchGetItem APIs.)
//
const scan = async ({ table, index, skName, skValue, skValue2, op, }) => {
// set default op
if (!op) {
op = '=';
}
// params
const params = {
TableName: table,
IndexName: index,
};
// 'between' case
if (skValue2) {
params.ExpressionAttributeNames = {
[`#${skName}`]: skName,
};
params.ExpressionAttributeValues = {
':rval': skValue,
':rval2': skValue2,
};
params.FilterExpression = `#${skName} BETWEEN :rval AND :rval2`;
}
// default
else {
params.ExpressionAttributeNames = {
[`#${skName}`]: skName,
};
params.ExpressionAttributeValues = {
':rval': skValue,
};
params.FilterExpression =
op === 'prefix' ? `begins_with(#${skName}, :rval)` : `#${skName} ${op} :rval`;
}
// perform scan
const ddb = new AWS.DynamoDB.DocumentClient();
const data = await ddb.scan(params).promise();
// check
if (data.LastEvaluatedKey) {
throw new Error('cannot handle partial results just yet');
}
// results
return data.Items ? data.Items : [];
};
// any2updateData converts object of 'Iany' to IUpdateData
const any2updateData = (update, primaryKeyNames) => {
const keys = Object.keys(update).filter((k) => !primaryKeyNames.includes(k) && update[k] !== undefined);
const UpdateExpression = 'SET ' + [...keys.map((_, i) => `#y${i} = :x${i}`)].join(', ');
const ExpressionAttributeNames = keys.reduce((acc, curr, i) => ({ ...acc, [`#y${i}`]: curr }), {});
const ExpressionAttributeValues = keys.reduce((acc, curr, i) => ({ ...acc, [`:x${i}`]: update[curr] }), {});
return {
UpdateExpression,
ExpressionAttributeNames,
ExpressionAttributeValues,
};
};
// update updates data in DB and returns the new values
// NOTE: (1) this function will return null if data is empty like {}
// (2) the primaryKey (partition or sort) MAY be present in 'data'
const update = async (table, primaryKey, data) => {
if (Object.keys(data).length === 0) {
return null;
}
const ddb = new AWS.DynamoDB.DocumentClient();
const upData = any2updateData(data, Object.keys(primaryKey));
const updatedData = await ddb
.update({
TableName: table,
Key: primaryKey,
ReturnValues: 'ALL_NEW',
...upData,
})
.promise();
if (updatedData.Attributes) {
return updatedData.Attributes;
}
return null;
};
// updateAndDeleteT updates some data and delete other in a single TRANSACTION
// (even from different tables in the same region)
// NOTE: (1) the total number of items (update, delete) must be less than 10
// (2) the updated values are returned by another call to the DB
// (3) the primaryKey (partition or sort) MAY be present in 'data'
// ex: ConditionExpression: "attribute_not_exists(username)"
const updateAndDeleteT = async (itemsUpdate, itemsDelete) => {
// check
const sum = itemsUpdate.length + itemsDelete.length;
if (sum < 1 || sum > 10) {
throw new Error('the total number of items must be in [1, 10]');
}
// params: update
const pUpdate = itemsUpdate.map(({ table, primaryKey, data, put }) => put
? {
Put: {
TableName: table,
Item: { ...primaryKey, ...data },
},
}
: {
Update: {
TableName: table,
Key: primaryKey,
...any2updateData(data, Object.keys(primaryKey)),
},
});
// params: delete
const pDelete = itemsDelete.map(({ table, primaryKey }) => ({
Delete: {
TableName: table,
Key: primaryKey,
},
}));
// transaction
const ddb = new AWS.DynamoDB.DocumentClient();
const params = pUpdate.concat(pDelete);
await ddb.transactWrite({ TransactItems: params }).promise();
};
// updateT updates data in a single TRANSACTION
// (even from different tables in the same region)
// NOTE: (1) the max number of items to update is 10
// (2) the updated values are returned by another call to the DB
// (3) the primaryKey (partition or sort) MAY be present in 'data'
// ex: ConditionExpression: "attribute_not_exists(username)"
//
// The return data is retrieved using the transactGet method
// as in the getT of this library. In this case, the order is
// the SAME as the input.
//
//
const updateT = async (items, returnItems = false) => {
// check
if (items.length < 1 || items.length > 10) {
throw new Error('the number of items to update must be in [1, 10]');
}
// params
const TransactItems = items.map(({ table, primaryKey, data, put }) => put
? {
Put: {
TableName: table,
Item: { ...primaryKey, ...data },
},
}
: {
Update: {
TableName: table,
Key: primaryKey,
...any2updateData(data, Object.keys(primaryKey)),
},
});
// transaction
const ddb = new AWS.DynamoDB.DocumentClient();
await ddb.transactWrite({ TransactItems }).promise();
// get results
if (returnItems) {
const params = items.map(({ table, primaryKey }) => ({
Get: {
TableName: table,
Key: primaryKey,
},
}));
const res = await ddb.transactGet({ TransactItems: params }).promise();
if (res.Responses) {
return res.Responses.map((r) => r.Item || {});
}
return [];
}
// nothing to return
return null;
};
const tableExists = async (table) => {
const ddb = new AWS__default["default"].DynamoDB();
const params = { TableName: table };
try {
const res = await ddb.describeTable(params).promise();
if (res.Table) {
return res.Table.TableName === table;
}
}
catch (_) { }
return false;
};
const tableIsActive = async (table) => {
const ddb = new AWS__default["default"].DynamoDB();
const params = { TableName: table };
try {
const res = await ddb.describeTable(params).promise();
if (res.Table) {
return res.Table.TableStatus === 'ACTIVE';
}
}
catch (_) { }
return false;
};
const tableDelete = async (table) => {
const ddb = new AWS__default["default"].DynamoDB();
const params = { TableName: table };
try {
await ddb.deleteTable(params).promise();
}
catch (_) { }
};
const tableDeleteAndWait = async (table) => {
await tableDelete(table);
await util.exponentialBackoff(async () => {
const hasTable = await tableExists(table);
return !hasTable;
});
};
const tableWaitActive = async (table) => {
await util.exponentialBackoff(async () => {
return await tableIsActive(table);
});
};
exports.any2updateData = any2updateData;
exports.create = create;
exports.exists = exists;
exports.get = get;
exports.getBatch = getBatch;
exports.getT = getT;
exports.increment = increment;
exports.key2ddbKey = key2ddbKey;
exports.put = put;
exports.query = query;
exports.removeAttributes = removeAttributes;
exports.removeItem = removeItem;
exports.scan = scan;
exports.tableDelete = tableDelete;
exports.tableDeleteAndWait = tableDeleteAndWait;
exports.tableExists = tableExists;
exports.tableIsActive = tableIsActive;
exports.tableWaitActive = tableWaitActive;
exports.update = update;
exports.updateAndDeleteT = updateAndDeleteT;
exports.updateT = updateT;