UNPKG

@cpmech/az-dynamo

Version:

Auxiliary Tools for DynamoDB

508 lines (485 loc) 16.8 kB
'use strict'; 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;