UNPKG

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