@setho/dynamodb-repository
Version:
DynamoDB repository for hash-key and hash-key/range indexed tables. Designed for Lambda use. Handles nice-to-haves like created and updated timestamps and default id creation.
166 lines • 6.67 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const http_errors_1 = require("http-errors");
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
const validator_1 = __importDefault(require("./validator"));
const utils_1 = require("./utils");
const updateExpressionBuilder_1 = require("./updateExpressionBuilder");
class KeyValueRepository {
tableName;
keyName;
idOptions;
docClient;
updateExpressionsBuilder;
/**
* Create a HashKey Repository
* @param {Object} param - The constructor parameter
* @param {string} param.tableName - The name of your DynamoDB table
* @param {string} param.keyName - The name of your Hash/Partition Key property
* @param {Object} param.documentClient - Injectable DynamoDBDocumentClient (v3) from @aws-sdk/lib-dynamodb
* @param {Object} [param.idOptions] - The idOptions parameter
* @param {string} [param.idOptions.prefix=] - The prefix of your id
*/
constructor(input) {
(0, validator_1.default)(input);
const { tableName, keyName, idOptions, documentClient } = input;
this.tableName = tableName;
this.keyName = keyName;
this.idOptions = idOptions;
this.docClient = documentClient;
this.updateExpressionsBuilder = new updateExpressionBuilder_1.UpdateExpressionsBuilder(keyName);
}
async get(hashKey) {
const key = (0, utils_1.createDynamoDbKey)({ keyName: this.keyName, keyValue: hashKey });
const getParams = {
TableName: this.tableName,
Key: key,
};
const { Item } = await this.docClient.send(new lib_dynamodb_1.GetCommand(getParams));
if (!Item) {
const message = `No ${this.keyName} found for ${hashKey}`;
throw (0, http_errors_1.NotFound)(message);
}
return Item;
}
async getMany(input = {}) {
const { limit = 100, cursor } = input;
const scanParams = {
TableName: this.tableName,
Limit: limit,
};
if (cursor) {
scanParams.ExclusiveStartKey = (0, utils_1.parseCursor)(cursor);
}
let result;
try {
result = await this.docClient.send(new lib_dynamodb_1.ScanCommand(scanParams));
}
catch (err) {
if (err.code === 'ValidationException') {
throw new http_errors_1.BadRequest('cursor is not valid');
}
throw err;
}
const returnCursor = result.LastEvaluatedKey
? (0, utils_1.createCursor)(result.LastEvaluatedKey)
: undefined;
return {
items: result.Items || [],
cursor: returnCursor,
};
}
async remove(hashKey) {
const key = (0, utils_1.createDynamoDbKey)({ keyName: this.keyName, keyValue: hashKey });
const deleteParams = {
TableName: this.tableName,
Key: key,
};
await this.docClient.send(new lib_dynamodb_1.DeleteCommand(deleteParams));
}
async create(item) {
const itemToSave = { ...item };
itemToSave[this.keyName] = await (0, utils_1.createId)(this.idOptions);
const isoString = new Date().toISOString();
itemToSave.createdAt = isoString;
itemToSave.updatedAt = isoString;
itemToSave.revision = 1;
const putParams = {
TableName: this.tableName,
Item: itemToSave,
};
await this.docClient.send(new lib_dynamodb_1.PutCommand(putParams));
return itemToSave;
}
async update(item) {
this.validateRequiredProperties(item);
const updateInput = this.buildUpdateCommandInput(item);
let result;
try {
result = await this.docClient.send(new lib_dynamodb_1.UpdateCommand(updateInput));
}
catch (err) {
if (err.name === 'ConditionalCheckFailedException') {
const { Item } = err;
if (isNotFoundConflict(Item)) {
throw (0, http_errors_1.NotFound)();
}
if (isRevisionConflict({
expectedRevision: item.revision,
actualRevision: Item?.revision?.N,
})) {
throw (0, http_errors_1.Conflict)(`Conflict: Item in DB has revision [${Item?.revision?.N}]. You are using revision [${item.revision}]`);
}
}
throw err;
}
return result.Attributes || {};
}
buildUpdateCommandInput(item) {
const { revision: previousRevision } = item;
const itemToSave = setRepositoryModifiedPropertiesForUpdate(item);
const key = (0, utils_1.createDynamoDbKey)({ keyName: this.keyName, keyValue: itemToSave[this.keyName] });
const updateInput = {
TableName: this.tableName,
Key: key,
ConditionExpression: 'attribute_exists(#key) AND #revision = :prevRev',
UpdateExpression: this.updateExpressionsBuilder.buildUpdateExpression(itemToSave),
ExpressionAttributeNames: {
'#key': this.keyName,
'#revision': 'revision',
...this.updateExpressionsBuilder.buildExpressionNames(itemToSave),
},
ExpressionAttributeValues: {
':prevRev': previousRevision,
...this.updateExpressionsBuilder.buildExpressionValues(itemToSave),
},
ReturnValues: 'ALL_NEW',
ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
};
return updateInput;
}
validateRequiredProperties(item) {
if (!item[this.keyName]) {
throw new http_errors_1.BadRequest(`Bad Request: Item has no key named "${this.keyName}"`);
}
if (!item.revision) {
throw new http_errors_1.BadRequest('Bad Request: Item has no revision');
}
}
}
const isNotFoundConflict = (itemFromError) => !itemFromError;
const isRevisionConflict = (input) => {
const { expectedRevision, actualRevision } = input;
return expectedRevision !== actualRevision;
};
const setRepositoryModifiedPropertiesForUpdate = (item) => {
const returnItem = { ...item };
delete returnItem.createdAt;
returnItem.updatedAt = new Date().toISOString();
returnItem.revision = item.revision + 1;
return returnItem;
};
exports.default = KeyValueRepository;
//# sourceMappingURL=keyValueRepository.js.map