UNPKG

@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
"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;