dynamodb-toolbox
Version:
A simple set of tools for working with Amazon DynamoDB and the DocumentClient.
892 lines (891 loc) • 46.3 kB
JavaScript
import { DeleteCommand, GetCommand, PutCommand, UpdateCommand, } from '@aws-sdk/lib-dynamodb';
import * as cloneDeep from 'deep-copy';
import parseEntity from '../../lib/parseEntity.js';
import validateTypes from '../../lib/validateTypes.js';
import normalizeData from '../../lib/normalizeData.js';
import formatItem from '../../lib/formatItem.js';
import getKey from '../../lib/getKey.js';
import parseConditions from '../../lib/expressionBuilder.js';
import parseProjections from '../../lib/projectionBuilder.js';
import { error, transformAttr, isEmpty } from '../../lib/utils.js';
import { ATTRIBUTE_VALUES_LIST_DEFAULT_KEY, ATTRIBUTE_VALUES_LIST_DEFAULT_VALUE, } from '../../constants.js';
class Entity {
// Declare constructor (entity config)
constructor(entity) {
// Sanity check the entity object
if ((entity === null || entity === void 0 ? void 0 : entity.constructor) !== Object) {
error('Please provide a valid entity definition');
}
const { table, ...entitySchemaWithoutTable } = entity;
// we want to prevent mutation of the original entity configuration input but still be able
// to mutate the original table instance
entity = {
...cloneDeep.default(entitySchemaWithoutTable),
...(table ? { table } : {}),
};
const { attributes, timestamps = true, createdAlias = 'created', modifiedAlias = 'modified', typeAlias = 'entity', typeHidden = false, } = entity;
this.attributes = attributes;
this.timestamps = timestamps;
this.createdAlias = createdAlias;
this.modifiedAlias = modifiedAlias;
this.typeAlias = typeAlias;
this.typeHidden = typeHidden;
Object.assign(this, parseEntity(entity));
}
get table() {
return this._table;
}
/*
* @internal
*/
set table(table) {
this.setTable(table);
}
get DocumentClient() {
var _a;
if ((_a = this.table) === null || _a === void 0 ? void 0 : _a.DocumentClient) {
return this.table.DocumentClient;
}
else {
return error('DocumentClient required for this operation');
}
}
// Sets the auto execute mode (default to true)
set autoExecute(val) {
this._execute = typeof val === 'boolean' ? val : undefined;
}
// Gets the current auto execute mode
get autoExecute() {
var _a;
return typeof this._execute === 'boolean'
? this._execute
: typeof ((_a = this.table) === null || _a === void 0 ? void 0 : _a.autoExecute) === 'boolean'
? this.table.autoExecute
: true;
}
// Sets the auto parse mode (default to true)
set autoParse(val) {
this._parse = typeof val === 'boolean' ? val : undefined;
}
// Gets the current auto execute mode
get autoParse() {
var _a;
return typeof this._parse === 'boolean'
? this._parse
: typeof ((_a = this.table) === null || _a === void 0 ? void 0 : _a.autoParse) === 'boolean'
? this.table.autoParse
: true;
}
// Primary key getters
get partitionKey() {
return this.schema.keys.partitionKey
? this.attribute(this.schema.keys.partitionKey)
: error(`No partitionKey defined`);
}
// 🔨 TOIMPROVE: We could hardly type sortKey here
get sortKey() {
return this.schema.keys.sortKey ? this.attribute(this.schema.keys.sortKey) : null;
}
// Get mapped attribute name
attribute(attr) {
return this.schema.attributes[attr] && this.schema.attributes[attr].map
? this.schema.attributes[attr].map
: this.schema.attributes[attr]
? attr
: error(`'${attr}' does not exist or is an invalid alias`);
}
parse(input, include = []) {
// TODO: 'include' needs to handle nested maps?
// Convert include to roots and de-alias
include = include.map(attr => {
const _attr = attr.split('.')[0].split('[')[0];
return (this.schema.attributes[_attr] && this.schema.attributes[_attr].map) || _attr;
});
// Load the schema
const { schema, linked } = this;
// Assume standard response from DynamoDB
const data = input.Item || input.Items || input;
if (Array.isArray(data)) {
return data.map(item => formatItem()(schema.attributes, linked, item, include));
}
else {
return formatItem()(schema.attributes, linked, data, include);
}
}
/**
* Generate GET parameters and execute operation
* @param {object} item - The keys from item you wish to get.
* @param {object} [options] - Additional get options.
* @param {object} [params] - Additional DynamoDB parameters you wish to pass to the get request.
*/
async get(item, options = {}, params = {}) {
const getParams = this.getParams(item, options, params);
if (!shouldExecute(options.execute, this.autoExecute)) {
return getParams;
}
const output = await this.DocumentClient.send(new GetCommand(getParams));
if (!shouldParse(options.parse, this.autoParse)) {
return output;
}
const { Item, ...restOutput } = output;
if (!Item) {
return restOutput;
}
const parsedItem = this.parse(Item, options.include);
return { Item: parsedItem, ...restOutput };
}
/**
* Generate parameters for GET batch operation
* @param {object} item - The keys from item you wish to get.
*/
getBatch(item) {
return {
Table: this.table,
Key: this.getParams(item).Key,
};
}
/**
* Generate parameters for GET transaction operation
* @param {object} item - The keys from item you wish to get.
* @param {object} [options] - Additional get options
*/
getTransaction(item, options = {}) {
// Destructure options to check for extraneous arguments
const { attributes, // ProjectionExpression
...args } = options;
// Error on extraneous arguments
if (Object.keys(args).length > 0) {
error(`Invalid get transaction options: ${Object.keys(args).join(', ')}`);
}
// Generate the get parameters
const payload = this.getParams(item, options);
// Return in transaction format
return {
Entity: this,
Get: payload,
};
}
/**
* Generate GET parameters
* @param {object} item - The keys from item you wish to get.
* @param {object} [options] - Additional get options.
* @param {object} [params] - Additional DynamoDB parameters you wish to pass to the get request.
*/
getParams(item, options = {}, params = {}) {
// Extract schema and merge defaults
const { schema, defaults, linked, _table } = this;
const data = normalizeData()(schema.attributes, linked, Object.assign({}, defaults, item), true, false);
const { consistent, // ConsistentRead (boolean)
capacity, // ReturnConsumedCapacity (none, total, or indexes)
attributes, // Projections
..._args } = options;
// Remove other valid options from options
const args = Object.keys(_args).filter(x => !['execute', 'parse'].includes(x));
// Error on extraneous arguments
if (args.length > 0)
error(`Invalid get options: ${args.join(', ')}`);
// Verify consistent read
if (consistent !== undefined && typeof consistent !== 'boolean') {
error(`'consistent' requires a boolean`);
}
// Verify capacity
if (capacity !== undefined &&
(typeof capacity !== 'string' ||
!['NONE', 'TOTAL', 'INDEXES'].includes(capacity.toUpperCase()))) {
error(`'capacity' must be one of 'NONE','TOTAL', OR 'INDEXES'`);
}
let ExpressionAttributeNames; // init ExpressionAttributeNames
let ProjectionExpression; // init ProjectionExpression
// If projections
if (attributes) {
const { names, projections } = parseProjections(attributes, this.table, this.name);
if (Object.keys(names).length > 0) {
// Merge names and add projection expression
ExpressionAttributeNames = names;
ProjectionExpression = projections;
}
}
// Generate the payload
const payload = Object.assign({
TableName: _table.name,
Key: getKey()(data, schema.attributes, schema.keys.partitionKey, schema.keys.sortKey),
}, ExpressionAttributeNames ? { ExpressionAttributeNames } : null, ProjectionExpression ? { ProjectionExpression } : null, consistent ? { ConsistentRead: consistent } : null, capacity ? { ReturnConsumedCapacity: capacity.toUpperCase() } : null, typeof params === 'object' ? params : {});
return payload;
}
/**
* Generate DELETE parameters and execute operation
* @param {object} item - The keys from item you wish to delete.
* @param {object} [options] - Additional delete options.
* @param {object} [params] - Additional DynamoDB parameters you wish to pass to the delete request.
*/
async delete(item, options = {}, params = {}) {
const deleteParams = this.deleteParams(item, options, params);
if (!shouldExecute(options.execute, this.autoExecute)) {
return deleteParams;
}
const output = await this.DocumentClient.send(new DeleteCommand(deleteParams));
if (!shouldParse(options.parse, this.autoParse)) {
return output;
}
const { Attributes, ...restOutput } = output;
if (!Attributes) {
return restOutput;
}
const parsedAttributes = this.parse(Attributes, options.include);
return { Attributes: parsedAttributes, ...restOutput };
}
/**
* Generate parameters for DELETE batch operation
* @param {object} item - The keys from item you wish to delete.
*
* Only Key is supported (e.g. no conditions)
* https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
*/
deleteBatch(item) {
const payload = this.deleteParams(item);
return { [payload.TableName]: { DeleteRequest: { Key: payload.Key } } };
}
/**
* Generate parameters for DELETE transaction operation
* @param {object} item - The keys from item you wish to delete.
* @param {object} [options] - Additional delete options
* @param {object} [params] - Additional DynamoDB parameters you wish to pass to the delete request.
*
* Creates a Delete object: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Delete.html
*/
deleteTransaction(item, options = {}, params) {
// Destructure options to check for extraneous arguments
const { conditions, // ConditionExpression
returnValues, // ReturnValuesOnConditionCheckFailure (none, all_old)
...args } = options;
// Error on extraneous arguments
if (Object.keys(args).length > 0) {
error(`Invalid delete transaction options: ${Object.keys(args).join(', ')}`);
}
// Generate the delete parameters
let payload = this.deleteParams(item, options, params);
// If ReturnValues exists, replace with ReturnValuesOnConditionCheckFailure
if ('ReturnValues' in payload) {
const { ReturnValues, ..._payload } = payload;
payload = Object.assign({}, _payload, { ReturnValuesOnConditionCheckFailure: ReturnValues });
}
// Return in transaction format
return { Delete: payload };
}
/**
* Generate DELETE parameters
* @param {object} item - The keys from item you wish to delete.
* @param {object} [options] - Additional delete options.
* @param {object} [params] - Additional DynamoDB parameters you wish to pass to the delete request.
*/
deleteParams(item, options = {}, params = {}) {
// Extract schema and merge defaults
const { schema, defaults, linked, _table } = this;
const data = normalizeData()(schema.attributes, linked, Object.assign({}, defaults, item), true);
const { conditions, // ConditionExpression
capacity, // ReturnConsumedCapacity (none, total, or indexes)
metrics, // ReturnItemCollectionMetrics: (size or none)
returnValues, // Return Values (none, all_old)
..._args } = options;
// Remove other valid options from options
const args = Object.keys(_args).filter(x => !['execute', 'parse'].includes(x));
// Error on extraneous arguments
if (args.length > 0)
error(`Invalid delete options: ${args.join(', ')}`);
// Verify metrics
if (metrics !== undefined &&
(typeof metrics !== 'string' || !['NONE', 'SIZE'].includes(metrics.toUpperCase()))) {
error(`'metrics' must be one of 'NONE' OR 'SIZE'`);
}
// Verify capacity
if (capacity !== undefined &&
(typeof capacity !== 'string' ||
!['NONE', 'TOTAL', 'INDEXES'].includes(capacity.toUpperCase()))) {
error(`'capacity' must be one of 'NONE','TOTAL', OR 'INDEXES'`);
}
// Verify returnValues
if (returnValues !== undefined &&
(typeof returnValues !== 'string' ||
!['NONE', 'ALL_OLD'].includes(returnValues.toUpperCase()))) {
error(`'returnValues' must be one of 'NONE' OR 'ALL_OLD'`);
}
let ExpressionAttributeNames; // init ExpressionAttributeNames
let ExpressionAttributeValues; // init ExpressionAttributeValues
let ConditionExpression; // init ConditionExpression
// If conditions
if (conditions) {
// Parse the conditions
const { expression, names, values } = parseConditions(conditions, this.table, this.name);
if (Object.keys(names).length > 0) {
// TODO: alias attribute field names
// Merge names and values and add condition expression
ExpressionAttributeNames = names;
ExpressionAttributeValues = values;
ConditionExpression = expression;
}
}
// Generate the payload
const payload = Object.assign({
TableName: _table.name,
Key: getKey()(data, schema.attributes, schema.keys.partitionKey, schema.keys.sortKey),
}, ExpressionAttributeNames ? { ExpressionAttributeNames } : null, !isEmpty(ExpressionAttributeValues) ? { ExpressionAttributeValues } : null, ConditionExpression ? { ConditionExpression } : null, capacity ? { ReturnConsumedCapacity: capacity.toUpperCase() } : null, metrics ? { ReturnItemCollectionMetrics: metrics.toUpperCase() } : null, returnValues ? { ReturnValues: returnValues.toUpperCase() } : null, typeof params === 'object' ? params : {});
return payload;
}
/**
* Generate UPDATE parameters and execute operations
* @param {object} item - The keys from item you wish to update.
* @param {object} [options] - Additional update options.
* @param {object} [params] - Additional DynamoDB parameters you wish to pass to the update request.
*/
async update(item, options = {}, params = {}) {
// Generate the payload
const updateParams = this.updateParams(item, options, params);
if (!shouldExecute(options.execute, this.autoExecute)) {
return updateParams;
}
const output = await this.DocumentClient.send(new UpdateCommand(updateParams));
if (!shouldParse(options.parse, this.autoParse)) {
return output;
}
const { Attributes, ...restOutput } = output;
if (!Attributes) {
return restOutput;
}
const parsedAttributes = this.parse(Attributes, options.include);
return { Attributes: parsedAttributes, ...restOutput };
}
/**
* Generate parameters for UPDATE transaction operation
* @param {object} item - The item you wish to update.
* @param {object} [options] - Additional update options.
* @param {object} [params] - Additional DynamoDB parameters you wish to pass to the update request.
*
* Creates an Update object: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Update.html
*/
updateTransaction(item, options = {}, params) {
// Destructure options to check for extraneous arguments
const { conditions, // ConditionExpression
returnValues, // ReturnValuesOnConditionCheckFailure (none, all_old)
strictSchemaCheck, // StrictSchemaCheck
...args } = options;
// Error on extraneous arguments
if (Object.keys(args).length > 0) {
error(`Invalid update transaction options: ${Object.keys(args).join(', ')}`);
}
// Generate the update parameters
let payload = this.updateParams(item, options, params);
// If ReturnValues exists, replace with ReturnValuesOnConditionCheckFailure
if ('ReturnValues' in payload) {
const { ReturnValues, ..._payload } = payload;
payload = Object.assign({}, _payload, { ReturnValuesOnConditionCheckFailure: ReturnValues });
}
// Return in transaction format (cast as Update since UpdateExpression can't be undefined)
return { Update: payload };
}
// Generate UPDATE Parameters
updateParams(item, options = {}, { SET = [], REMOVE = [], ADD = [], DELETE = [], ExpressionAttributeNames = {}, ExpressionAttributeValues = {}, ...params } = {}) {
// Validate operation types
if (!Array.isArray(SET))
error('SET must be an array');
if (!Array.isArray(REMOVE))
error('REMOVE must be an array');
if (!Array.isArray(ADD))
error('ADD must be an array');
if (!Array.isArray(DELETE))
error('DELETE must be an array');
// Validate attribute names and values
if ((ExpressionAttributeNames === null || ExpressionAttributeNames === void 0 ? void 0 : ExpressionAttributeNames.constructor) !== Object) {
error('ExpressionAttributeNames must be an object');
}
if ((ExpressionAttributeValues === null || ExpressionAttributeValues === void 0 ? void 0 : ExpressionAttributeValues.constructor) !== Object) {
error('ExpressionAttributeValues must be an object');
}
// if (ConditionExpression && typeof ConditionExpression !== 'string')
// error(`ConditionExpression must be a string`)
// Extract schema and defaults
const { schema, defaults, required, linked, _table } = this;
// Initialize validateType
const validateType = validateTypes();
const shouldFilterUnmappedFields = options.strictSchemaCheck === false;
// Merge defaults
const data = normalizeData()(schema.attributes, linked, Object.assign({}, defaults, item), shouldFilterUnmappedFields);
// Extract valid options
const { conditions, // ConditionExpression
capacity, // ReturnConsumedCapacity (none, total, or indexes)
metrics, // ReturnItemCollectionMetrics: (size or none)
returnValues, // Return Values (none, all_old, updated_old, all_new, updated_new)
strictSchemaCheck, // Strict Schema Check (true or false)
..._args } = options;
// Remove other valid options from options
const args = Object.keys(_args).filter(x => !['execute', 'parse'].includes(x));
// Error on extraneous arguments
if (args.length > 0)
error(`Invalid update options: ${args.join(', ')}`);
// Verify metrics
if (metrics !== undefined &&
(typeof metrics !== 'string' || !['NONE', 'SIZE'].includes(metrics.toUpperCase()))) {
error(`'metrics' must be one of 'NONE' OR 'SIZE'`);
}
// Verify capacity
if (capacity !== undefined &&
(typeof capacity !== 'string' ||
!['NONE', 'TOTAL', 'INDEXES'].includes(capacity.toUpperCase()))) {
error(`'capacity' must be one of 'NONE','TOTAL', OR 'INDEXES'`);
}
// Verify returnValues
if (returnValues !== undefined &&
(typeof returnValues !== 'string' ||
!['NONE', 'ALL_OLD', 'UPDATED_OLD', 'ALL_NEW', 'UPDATED_NEW'].includes(returnValues.toUpperCase()))) {
error(`'returnValues' must be one of 'NONE', 'ALL_OLD', 'UPDATED_OLD', 'ALL_NEW', OR 'UPDATED_NEW'`);
}
let ConditionExpression; // init ConditionExpression
// If conditions
if (conditions) {
// Parse the conditions
const { expression, names, values } = parseConditions(conditions, this.table, this.name);
if (Object.keys(names).length > 0) {
// TODO: alias attribute field names
// Add names, values and condition expression
ExpressionAttributeNames = Object.assign(ExpressionAttributeNames, names);
ExpressionAttributeValues = Object.assign(ExpressionAttributeValues, values);
ConditionExpression = expression;
}
}
// Check for required fields
Object.keys(required).forEach(field => required[field] &&
(data[field] === undefined || data[field] === null) &&
error(`'${field}${this.schema.attributes[field].alias ? `/${this.schema.attributes[field].alias}` : ''}' is a required field`));
// Get partition and sort keys
const Key = getKey()(data, schema.attributes, schema.keys.partitionKey, schema.keys.sortKey);
// Init names and values
const names = {};
const values = {};
// Loop through valid fields and add appropriate action
Object.keys(data).forEach(field => {
var _a, _b, _c, _d, _e, _f, _g, _h;
const mapping = schema.attributes[field];
// Remove attributes
if (field === '$remove') {
const attrs = Array.isArray(data[field]) ? data[field] : [data[field]];
for (const i in attrs) {
// Verify attribute
if (!schema.attributes[attrs[i]]) {
error(`'${attrs[i]}' is not a valid attribute and cannot be removed`);
}
// Verify attribute is not a pk/sk
if (schema.attributes[attrs[i]].partitionKey === true ||
schema.attributes[attrs[i]].sortKey === true) {
error(`'${attrs[i]}' is the ${schema.attributes[attrs[i]].partitionKey === true ? 'partitionKey' : 'sortKey'} and cannot be removed`);
}
// Verify attribute is not required
if (schema.attributes[attrs[i]].required) {
error(`'${attrs[i]}' is required and cannot be removed`);
}
const attributeHasDefaultValue = schema.attributes[attrs[i]].default !== undefined;
if (attributeHasDefaultValue) {
error(`'${attrs[i]}' has a default value and cannot be removed`);
}
// Grab the attribute name and add to REMOVE and names
const attr = schema.attributes[attrs[i]].map || attrs[i];
REMOVE.push(`#${attr}`);
names[`#${attr}`] = attr;
}
}
else if (this._table._removeNulls === true &&
(data[field] === null || String(data[field]).trim() === '') &&
!Array.isArray(data[field]) &&
(!mapping.link || mapping.save)) {
// Verify attribute is not required
if (schema.attributes[field].required)
error(`'${field}' is required and cannot be removed`);
REMOVE.push(`#${field}`);
names[`#${field}`] = field;
}
else if (
// !mapping.partitionKey
// && !mapping.sortKey
mapping.partitionKey !== true &&
mapping.sortKey !== true &&
(mapping.save === undefined || mapping.save === true) &&
(!mapping.link || (mapping.link && mapping.save === true))) {
// If a number or a set and adding
if (['bigint', 'number', 'set'].includes(mapping.type) &&
((_a = data[field]) === null || _a === void 0 ? void 0 : _a.$add) !== undefined &&
((_b = data[field]) === null || _b === void 0 ? void 0 : _b.$add) !== null) {
ADD.push(`#${field} :${field}`);
values[`:${field}`] = validateType(mapping, field, data[field].$add);
// Add field to names
names[`#${field}`] = field;
// if a set and deleting items
}
else if (mapping.type === 'set' && ((_c = data[field]) === null || _c === void 0 ? void 0 : _c.$delete)) {
DELETE.push(`#${field} :${field}`);
values[`:${field}`] = validateType(mapping, field, data[field].$delete);
// Add field to names
names[`#${field}`] = field;
// if a list and removing items by index
}
else if (mapping.type === 'list' && Array.isArray((_d = data[field]) === null || _d === void 0 ? void 0 : _d.$remove)) {
data[field].$remove.forEach((i) => {
if (typeof i !== 'number') {
error(`Remove array for '${field}' must only contain numeric indexes`);
}
REMOVE.push(`#${field}[${i}]`);
});
// Add field to names
names[`#${field}`] = field;
// if list and appending or prepending
}
else if (mapping.type === 'list' && (((_e = data[field]) === null || _e === void 0 ? void 0 : _e.$append) || ((_f = data[field]) === null || _f === void 0 ? void 0 : _f.$prepend))) {
if (data[field].$append) {
SET.push(`#${field} = list_append(if_not_exists(#${field}, :${ATTRIBUTE_VALUES_LIST_DEFAULT_KEY}) ,:${field})`);
values[`:${field}`] = validateType(mapping, field, data[field].$append);
}
else {
SET.push(`#${field} = list_append(:${field}, if_not_exists(#${field}, :${ATTRIBUTE_VALUES_LIST_DEFAULT_KEY}))`);
values[`:${field}`] = validateType(mapping, field, data[field].$prepend);
}
// Add field to names
names[`#${field}`] = field;
values[`:${ATTRIBUTE_VALUES_LIST_DEFAULT_KEY}`] = ATTRIBUTE_VALUES_LIST_DEFAULT_VALUE;
// if a list and updating by index
}
else if (mapping.type === 'list' && ((_g = data[field]) === null || _g === void 0 ? void 0 : _g.constructor) === Object) {
Object.keys(data[field]).forEach(i => {
if (String(parseInt(i)) !== i) {
error(`Properties must be numeric to update specific list items in '${field}'`);
}
SET.push(`#${field}[${i}] = :${field}_${i}`);
values[`:${field}_${i}`] = data[field][i];
});
// Add field to names
names[`#${field}`] = field;
// if a map and updating by nested attribute/index
}
else if (mapping.type === 'map' && ((_h = data[field]) === null || _h === void 0 ? void 0 : _h.$set)) {
Object.keys(data[field].$set).forEach(f => {
const props = f.split('.');
const acc = [`#${field}`];
props.forEach((prop, i) => {
const id = `${field}_${props.slice(0, i + 1).join('_')}`;
// Add names and values
names[`#${id.replace(/\[(\d+)\]/, '')}`] = prop.replace(/\[(\d+)\]/, '');
// if the final prop, add the SET and values
if (i === props.length - 1) {
const input = data[field].$set[f];
const path = `${acc.join('.')}.#${id}`;
const value = `${id.replace(/\[(\d+)\]/, '_$1')}`;
if (input === undefined || input === null) {
REMOVE.push(`${path}`);
}
else if (input.$add) {
ADD.push(`${path} :${value}`);
values[`:${value}`] = input.$add;
}
else if (input.$append) {
SET.push(`${path} = list_append(if_not_exists(${path}, :${ATTRIBUTE_VALUES_LIST_DEFAULT_KEY}), :${value})`);
values[`:${value}`] = input.$append;
// add default list value
values[`:${ATTRIBUTE_VALUES_LIST_DEFAULT_KEY}`] = ATTRIBUTE_VALUES_LIST_DEFAULT_VALUE;
}
else if (input.$prepend) {
SET.push(`${path} = list_append(:${value}, if_not_exists(${path}, :${ATTRIBUTE_VALUES_LIST_DEFAULT_KEY}))`);
values[`:${value}`] = input.$prepend;
// add default list value
values[`:${ATTRIBUTE_VALUES_LIST_DEFAULT_KEY}`] = ATTRIBUTE_VALUES_LIST_DEFAULT_VALUE;
}
else if (input.$remove) {
// console.log('REMOVE:',input.$remove);
input.$remove.forEach((i) => {
if (typeof i !== 'number') {
error(`Remove array for '${field}' must only contain numeric indexes`);
}
REMOVE.push(`${path}[${i}]`);
});
}
else {
SET.push(`${path} = :${value}`);
values[`:${value}`] = input;
}
if (input === null || input === void 0 ? void 0 : input.$set) {
Object.keys(input.$set).forEach(i => {
if (String(parseInt(i)) !== i) {
error(`Properties must be numeric to update specific list items in '${field}'`);
}
SET.push(`${path}[${i}] = :${value}_${i}`);
values[`:${value}_${i}`] = input.$set[i];
});
}
}
else {
acc.push(`#${id.replace(/\[(\d+)\]/, '')}`);
}
});
});
const shouldAppendFieldToExpressionNames = Object.keys(data[field].$set).length > 0;
if (shouldAppendFieldToExpressionNames) {
names[`#${field}`] = field;
}
// else add to SET
}
else {
const value = transformAttr(mapping, validateType(mapping, field, data[field]), data);
// It's possible that defaults can purposely return undefined values
// if (hasValue(value)) {
if (value !== undefined) {
// Push the update to SET
SET.push(
// @ts-ignore
mapping.default !== undefined && item[mapping.alias || field] === undefined && !mapping.onUpdate
? `#${field} = if_not_exists(#${field},:${field})`
: `#${field} = :${field}`);
// Add names and values
names[`#${field}`] = field;
values[`:${field}`] = value;
}
}
}
});
// Create the update expression
const expression = ((SET.length > 0 ? 'SET ' + SET.join(', ') : '') +
(REMOVE.length > 0 ? ' REMOVE ' + REMOVE.join(', ') : '') +
(ADD.length > 0 ? ' ADD ' + ADD.join(', ') : '') +
(DELETE.length > 0 ? ' DELETE ' + DELETE.join(', ') : '')).trim();
// Merge attribute values
ExpressionAttributeValues = Object.assign(values, ExpressionAttributeValues);
// Generate the payload
const payload = Object.assign({
TableName: _table.name,
Key,
UpdateExpression: expression,
ExpressionAttributeNames: Object.assign(names, ExpressionAttributeNames),
}, typeof params === 'object' ? params : {}, !isEmpty(ExpressionAttributeValues) ? { ExpressionAttributeValues } : {}, ConditionExpression ? { ConditionExpression } : {}, capacity ? { ReturnConsumedCapacity: capacity.toUpperCase() } : null, metrics ? { ReturnItemCollectionMetrics: metrics.toUpperCase() } : null, returnValues ? { ReturnValues: returnValues.toUpperCase() } : null);
return payload;
// TODO: Check why primary/secondary GSIs are using if_not_exists
}
// PUT - put item
async put(item, options = {}, params = {}) {
const putParams = this.putParams(item, options, params);
if (!shouldExecute(options.execute, this.autoExecute)) {
return putParams;
}
const output = await this.DocumentClient.send(new PutCommand(putParams));
if (!shouldParse(options.parse, this.autoParse)) {
return output;
}
const { Attributes, ...restOutput } = output;
if (!Attributes) {
return output;
}
const parsedAttributes = this.parse(Attributes, options.include);
return { Attributes: parsedAttributes, ...restOutput };
}
/**
* Generate parameters for PUT batch operation
* @param {object} item - The item you wish to put.
* @param {object} options - Options for the operation.
*
* Only Item is supported (e.g. no conditions)
* https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
*/
putBatch(item, options = {}) {
const payload = this.putParams(item, options);
return { [payload.TableName]: { PutRequest: { Item: payload.Item } } };
}
/**
* Generate parameters for PUT transaction operation
* @param {object} item - The item you wish to put.
* @param {object} [options] - Additional put options
* @param {object} [params] - Additional DynamoDB parameters you wish to pass to the put request.
*
* Creates a Put object: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Put.html
*/
putTransaction(item, options = {}, params) {
// Destructure options to check for extraneous arguments
const { conditions, // ConditionExpression
returnValues, // ReturnValuesOnConditionCheckFailure (none, all_old)
strictSchemaCheck, // StrictSchemaCheck
...args } = options;
// Error on extraneous arguments
if (Object.keys(args).length > 0) {
error(`Invalid put transaction options: ${Object.keys(args).join(', ')}`);
}
// Generate the put parameters
let payload = this.putParams(item, options, params);
// If ReturnValues exists, replace with ReturnValuesOnConditionCheckFailure
if ('ReturnValues' in payload) {
const { ReturnValues, ..._payload } = payload;
payload = Object.assign({}, _payload, { ReturnValuesOnConditionCheckFailure: ReturnValues });
}
// Return in transaction format
return { Put: payload };
}
// Generate PUT Parameters
putParams(item, options = {}, params = {}) {
// Extract schema and defaults
const { schema, defaults, required, linked, _table } = this;
// Initialize validateType
const validateType = validateTypes();
const shouldFilterUnmappedFields = options.strictSchemaCheck === false;
// Merge defaults
const data = normalizeData()(schema.attributes, linked, Object.assign({}, defaults, item), shouldFilterUnmappedFields);
// Extract valid options
const { conditions, // ConditionExpression
capacity, // ReturnConsumedCapacity (none, total, or indexes)
metrics, // ReturnItemCollectionMetrics: (size or none)
returnValues, // Return Values (none, all_old, updated_old, all_new, updated_new)
strictSchemaCheck, // Strict Schema Check (true or false)
..._args } = options;
// Remove other valid options from options
const args = Object.keys(_args).filter(x => !['execute', 'parse'].includes(x));
// Error on extraneous arguments
if (args.length > 0)
error(`Invalid put options: ${args.join(', ')}`);
// Verify metrics
if (metrics !== undefined &&
(typeof metrics !== 'string' || !['NONE', 'SIZE'].includes(metrics.toUpperCase()))) {
error(`'metrics' must be one of 'NONE' OR 'SIZE'`);
}
// Verify capacity
if (capacity !== undefined &&
(typeof capacity !== 'string' ||
!['NONE', 'TOTAL', 'INDEXES'].includes(capacity.toUpperCase()))) {
error(`'capacity' must be one of 'NONE','TOTAL', OR 'INDEXES'`);
}
// Verify returnValues
if (returnValues !== undefined &&
(typeof returnValues !== 'string' ||
!['NONE', 'ALL_OLD', 'UPDATED_OLD', 'ALL_NEW', 'UPDATED_NEW'].includes(returnValues.toUpperCase()))) {
error(`'returnValues' must be one of 'NONE', 'ALL_OLD', 'UPDATED_OLD', 'ALL_NEW', or 'UPDATED_NEW'`);
}
let ExpressionAttributeNames; // init ExpressionAttributeNames
let ExpressionAttributeValues; // init ExpressionAttributeValues
let ConditionExpression; // init ConditionExpression
// If conditions
if (conditions) {
// Parse the conditions
const { expression, names, values } = parseConditions(conditions, this.table, this.name);
if (Object.keys(names).length > 0) {
// TODO: alias attribute field names
// Add names, values and condition expression
ExpressionAttributeNames = names;
ExpressionAttributeValues = values;
ConditionExpression = expression;
}
}
// Check for required fields
Object.keys(required).forEach(field => required[field] !== undefined &&
(data[field] === undefined || data[field] === null) &&
error(`'${field}${this.schema.attributes[field].alias ? `/${this.schema.attributes[field].alias}` : ''}' is a required field`));
// Checks for partition and sort keys
getKey()(data, schema.attributes, schema.keys.partitionKey, schema.keys.sortKey);
// Generate the payload
const payload = Object.assign({
TableName: _table.name,
// Loop through valid fields and add appropriate action
Item: Object.keys(data).reduce((acc, field) => {
const mapping = schema.attributes[field];
let value = data[field];
if (value !== undefined &&
(mapping.save === undefined || mapping.save === true) &&
(!mapping.link || (mapping.link && mapping.save === true)) &&
(!_table._removeNulls || (_table._removeNulls && value !== null))) {
// Transform before validation as user can transform entity into
// invalid value which will be thrown by DynamoDB Document client
value = transformAttr(mapping, value, data);
value = validateType(mapping, field, value);
return Object.assign(acc, { [field]: value });
}
return acc;
}, {}),
}, ExpressionAttributeNames ? { ExpressionAttributeNames } : null, !isEmpty(ExpressionAttributeValues) ? { ExpressionAttributeValues } : null, ConditionExpression ? { ConditionExpression } : null, capacity ? { ReturnConsumedCapacity: capacity.toUpperCase() } : null, metrics ? { ReturnItemCollectionMetrics: metrics.toUpperCase() } : null, returnValues ? { ReturnValues: returnValues.toUpperCase() } : null, typeof params === 'object' ? params : {});
return payload;
}
/**
* Generate parameters for ConditionCheck transaction operation
* @param {object} item - The keys from item you wish to check.
* @param {object} [options] - Additional condition check options
*
* Creates a ConditionCheck object:
* https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_ConditionCheck.html
*/
conditionCheck(item, options = {}) {
// Destructure options to check for extraneous arguments
const { conditions, // ConditionExpression
returnValues, // ReturnValuesOnConditionCheckFailure (none, all_old)
...args } = options;
// Error on extraneous arguments
if (Object.keys(args).length > 0) {
error(`Invalid conditionCheck options: ${Object.keys(args).join(', ')}`);
}
// Generate the condition parameters (same params as delete)
let payload = this.deleteParams(item, options);
// Error on missing conditions
if (!('ConditionExpression' in payload))
error(`'conditions' are required in a conditionCheck`);
// If ReturnValues exists, replace with ReturnValuesOnConditionCheckFailure
if ('ReturnValues' in payload) {
const { ReturnValues, ..._payload } = payload;
payload = Object.assign({}, _payload, { ReturnValuesOnConditionCheckFailure: ReturnValues });
}
// Return in transaction format
return { ConditionCheck: payload };
}
// Query pass-through (default entity)
query(pk, options = {}, params = {}) {
if (!this.table) {
throw new Error('Entity table is not defined');
}
options.entity = this.name;
return this.table.query(pk, options, params);
}
// Scan pass-through (default entity)
scan(options = {}, params = {}) {
if (!this.table) {
throw new Error('Entity table is not defined');
}
options.entity = this.name;
return this.table.scan(options, params);
}
setTable(table) {
var _a, _b, _c, _d, _e, _f, _g;
if (table == null && !this._table) {
return this;
}
if ((table === null || table === void 0 ? void 0 : table.name) === ((_a = this === null || this === void 0 ? void 0 : this._table) === null || _a === void 0 ? void 0 : _a.name)) {
return this;
}
if (table != null && !((_b = table === null || table === void 0 ? void 0 : table.Table) === null || _b === void 0 ? void 0 : _b.attributes)) {
error(`Entity ${this.name} was assigned an invalid table`);
}
if ((_d = (_c = this._table) === null || _c === void 0 ? void 0 : _c.Table) === null || _d === void 0 ? void 0 : _d.entityField) {
delete this.schema.attributes[this._table.Table.entityField];
delete this.defaults[this._table.Table.entityField];
delete this.schema.attributes[this._etAlias];
delete this.defaults[this._etAlias];
}
(_f = (_e = this._table) === null || _e === void 0 ? void 0 : _e.removeEntity) === null || _f === void 0 ? void 0 : _f.call(_e, this);
this._table = table;
table === null || table === void 0 ? void 0 : table.addEntity(this);
// If an entity tracking field is enabled, add the attributes, alias and the default
if ((_g = table === null || table === void 0 ? void 0 : table.Table) === null || _g === void 0 ? void 0 : _g.entityField) {
this.schema.attributes[table.Table.entityField] = {
type: 'string',
hidden: this.typeHidden,
alias: this._etAlias,
default: this.name,
};
this.defaults[table.Table.entityField] = this.name;
this.schema.attributes[this._etAlias] = {
type: 'string',
map: table.Table.entityField,
default: this.name,
};
this.defaults[this._etAlias] = this.name;
}
return this;
}
}
export default Entity;
export const shouldExecute = (execute, autoExecute) => execute === true || (execute === undefined && autoExecute);
export const shouldParse = (parse, autoParse) => parse === true || (parse === undefined && autoParse);