@mcma/aws-dynamodb
Version:
Node module with code for using DynamoDB as the backing data storage for MCMA API handlers and workers.
190 lines (189 loc) • 8.15 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DynamoDbTable = void 0;
exports.keyFromBase64Json = keyFromBase64Json;
exports.base64JsonFromKey = base64JsonFromKey;
const util_1 = require("util");
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
const core_1 = require("@mcma/core");
const data_1 = require("@mcma/data");
const build_filter_expression_1 = require("./build-filter-expression");
const dynamo_db_mutex_1 = require("./dynamo-db-mutex");
function parsePartitionAndSortKeys(id) {
const lastSlashIndex = id.lastIndexOf("/");
return lastSlashIndex > 0
? { partitionKey: id.substring(0, lastSlashIndex), sortKey: id.substring(lastSlashIndex + 1) }
: { partitionKey: id, sortKey: id };
}
function keyFromBase64Json(str) {
if (!str) {
return undefined;
}
try {
return JSON.parse(core_1.Utils.fromBase64(str));
}
catch (e) {
throw new core_1.McmaException(`Invalid key '${str}'.`);
}
}
function base64JsonFromKey(key) {
return key ? core_1.Utils.toBase64(JSON.stringify(key)) : undefined;
}
class DynamoDbTable {
tableDescription;
options;
docClient;
constructor(dynamoDBClient, tableDescription, options) {
this.tableDescription = tableDescription;
this.options = options;
this.docClient = lib_dynamodb_1.DynamoDBDocumentClient.from(dynamoDBClient, { marshallOptions: { removeUndefinedValues: true } });
}
serialize(object) {
let copy;
if (object) {
copy = Array.isArray(object) ? [] : {};
for (const key of Object.keys(object)) {
const value = object[key];
if (util_1.types.isDate(value) && !isNaN(value.getTime())) {
copy[key] = value.toISOString();
}
else if (typeof value === "object") {
copy[key] = this.serialize(value);
}
else {
copy[key] = value;
}
}
}
return copy;
}
deserialize(object) {
let copy;
if (object) {
copy = Array.isArray(object) ? [] : {};
for (const key of Object.keys(object)) {
const value = object[key];
if (core_1.Utils.isValidDateString(value)) {
copy[key] = new Date(value);
}
else if (typeof value === "object") {
copy[key] = this.deserialize(value);
}
else {
copy[key] = value;
}
}
}
return copy;
}
async query(query) {
let keyNames = this.tableDescription.keyNames;
let keyConditionExpression = "#partitionKey = :partitionKey";
let expressionAttributeNames = { "#partitionKey": keyNames.partition };
let expressionAttributeValues = { ":partitionKey": query.path };
let filterExpression;
if ((0, data_1.hasFilterCriteria)(query.filterExpression)) {
const dynamoDbExpression = (0, build_filter_expression_1.buildFilterExpression)(query.filterExpression);
filterExpression = dynamoDbExpression.expressionStatement;
expressionAttributeNames = Object.assign(expressionAttributeNames, dynamoDbExpression.expressionAttributeNames);
expressionAttributeValues = Object.assign(expressionAttributeValues, dynamoDbExpression.expressionAttributeValues);
}
let indexName;
if (query.sortBy) {
const matchingIndex = this.tableDescription.localSecondaryIndexes.find(x => x.sortKeyName?.toLowerCase() === query.sortBy.toLowerCase());
if (!matchingIndex) {
throw new core_1.McmaException(`No matching local secondary index found for sorting by '${query.sortBy}'`);
}
indexName = matchingIndex.name;
}
const params = {
TableName: this.tableDescription.tableName,
KeyConditionExpression: keyConditionExpression,
FilterExpression: filterExpression,
ExpressionAttributeNames: expressionAttributeNames,
ExpressionAttributeValues: expressionAttributeValues,
IndexName: indexName,
ScanIndexForward: query.sortOrder === data_1.QuerySortOrder.Ascending,
ConsistentRead: this.options?.consistentQuery,
Limit: query.pageSize,
ExclusiveStartKey: keyFromBase64Json(query.pageStartToken)
};
const data = await this.docClient.send(new lib_dynamodb_1.QueryCommand(params));
return {
results: data.Items.map(i => this.deserialize(i.resource)),
nextPageStartToken: base64JsonFromKey(data.LastEvaluatedKey)
};
}
async customQuery(query) {
const getQueryInputFromCustomQuery = this.options.customQueryRegistry[query.name];
if (!getQueryInputFromCustomQuery) {
throw new core_1.McmaException(`Custom query with name '${query.name}' has not been configured.`);
}
const params = getQueryInputFromCustomQuery(query);
params.TableName = this.tableDescription.tableName;
params.ExclusiveStartKey = keyFromBase64Json(query.pageStartToken);
const data = await this.docClient.send(new lib_dynamodb_1.QueryCommand(params));
return {
results: data.Items.map(i => this.deserialize(i.resource)),
nextPageStartToken: base64JsonFromKey(data.LastEvaluatedKey)
};
}
async get(id) {
const { partitionKey, sortKey } = parsePartitionAndSortKeys(id);
const params = {
TableName: this.tableDescription.tableName,
Key: {
[this.tableDescription.keyNames.partition]: partitionKey,
[this.tableDescription.keyNames.sort]: sortKey
},
ConsistentRead: this.options?.consistentGet
};
const data = await this.docClient.send(new lib_dynamodb_1.GetCommand(params));
return data?.Item?.resource ? this.deserialize(data.Item.resource) : null;
}
async put(id, resource) {
const { partitionKey, sortKey } = parsePartitionAndSortKeys(id);
const serializedResource = this.serialize(resource);
let item = {
[this.tableDescription.keyNames.partition]: partitionKey,
[this.tableDescription.keyNames.sort]: sortKey,
resource: serializedResource
};
if (this.options?.topLevelAttributeMappings) {
for (let topLevelAttributeMappingKey of Object.keys(this.options.topLevelAttributeMappings)) {
item[topLevelAttributeMappingKey] = this.options.topLevelAttributeMappings[topLevelAttributeMappingKey](partitionKey, sortKey, resource);
}
}
const params = {
TableName: this.tableDescription.tableName,
Item: item
};
try {
await this.docClient.send(new lib_dynamodb_1.PutCommand(params));
}
catch (error) {
throw new core_1.McmaException("Failed to put resource in DynamoDB table", error);
}
return resource;
}
async delete(id) {
const { partitionKey, sortKey } = parsePartitionAndSortKeys(id);
const params = {
TableName: this.tableDescription.tableName,
Key: {
[this.tableDescription.keyNames.partition]: partitionKey,
[this.tableDescription.keyNames.sort]: sortKey
}
};
try {
await this.docClient.send(new lib_dynamodb_1.DeleteCommand(params));
}
catch (error) {
throw new core_1.McmaException("Failed to delete resource in DynamoDB table", error);
}
}
createMutex(mutexProperties) {
return new dynamo_db_mutex_1.DynamoDbMutex(this.docClient, this.tableDescription, mutexProperties.name, mutexProperties.holder, mutexProperties.lockTimeout, mutexProperties.logger);
}
}
exports.DynamoDbTable = DynamoDbTable;