dynamodb-toolbox
Version:
A simple set of tools for working with Amazon DynamoDB and the DocumentClient.
272 lines (271 loc) • 10.6 kB
JavaScript
;
/**
* DynamoDB Toolbox: A simple set of tools for working with Amazon DynamoDB
* @author Jeremy Daly <jeremy@jeremydaly.com>
* @license MIT
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SUPPORTED_FILTER_EXP_ATTR_REF_OPERATORS = void 0;
const checkAttribute_js_1 = __importDefault(require("./checkAttribute.js"));
const utils_js_1 = require("./utils.js");
exports.SUPPORTED_FILTER_EXP_ATTR_REF_OPERATORS = ['eq', 'ne', 'lt', 'lte', 'gt', 'gte'];
const toDynamoValue = (value) => {
if (typeof value === 'bigint') {
return (0, utils_js_1.toDynamoBigInt)(value);
}
return value;
};
const buildExpression = (exp, table, entity, group = 0, level = 0) => {
// Coerce to array if not already
const clauses = Array.isArray(exp) ? exp : [exp];
let expression = '';
let names = {};
let values = {};
let logic; // init undefined at each level
// group logic tracker - need to mark the first clause
// Loop through the clauses
clauses.forEach((_, id) => {
// If clause is nested in an array
if (Array.isArray(clauses[id])) {
// Build the sub clause
const sub = buildExpression(clauses[id], table, entity, group, level);
// If no logic at this level, capture from sub clause
logic = logic ? logic : sub.logic;
// Concat to expression and merge names and values
expression += `${id > 0 ? ` ${sub.logic} ` : ''}(${sub.expression})`;
names = Object.assign(names, sub.names);
values = Object.assign(values, sub.values);
group = sub.group;
// Else process the clause
}
else {
// Make sure TS knows this is a FilterExpression; copy to avoid mutation
const exp = { ...clauses[id] };
// Increment group counter for each clause
group++; // guarantees uniqueness
// Default entity reference
if (entity && !exp.entity)
exp.entity = entity;
// Parse the clause
const clause = parseClause(exp, group, table);
// Concat to expression and merge names and values
expression += `${id > 0 ? ` ${clause.logic} ` : ''}${clause.clause}`;
names = Object.assign(names, clause.names);
values = Object.assign(values, clause.values);
// Capture the first logic indicator at this level
logic = logic ? logic : clause.logic;
}
});
return {
logic,
expression,
names,
values,
group
};
};
// Export buildExpression
exports.default = buildExpression;
// Define condition expression error
const conditionError = (op) => (0, utils_js_1.error)(`You can only supply one filter condition per query. Already using '${op}'`);
// Parses expression clause and returns structured clause object
const parseClause = (_clause, grp, table) => {
if (!table) {
throw new Error(`'table' should be defined`);
}
// Init clause, names, and values
let clause = '';
const names = {};
const values = {};
// Deconstruct valid expression options
const { attr, // attribute
size, // wraps attr in size()
negate, // negates expression
or, // change logical evaluator to OR
eq, // =
ne, // <>
in: _in, // IN
lt, // <
lte, // <=
gt, // >
gte, // >=
between, // BETWEEN
exists, // NULL and NOT_NULL
contains, // CONTAINS
beginsWith, // BEGINS_WITH
type, // checks attribute_type
entity, // optional entity reference for aliasing
...args } = _clause;
// Error on extraneous arguments
if (Object.keys(args).length > 0)
(0, utils_js_1.error)(`Invalid expression options: ${Object.keys(args).join(', ')}`);
// Verify entity
if (entity !== undefined &&
(typeof entity !== 'string' ||
!table[entity] ||
!table[entity].schema ||
!table[entity].schema.attributes))
(0, utils_js_1.error)(`'entity' value of '${entity}' must be a string and a valid table Entity name`);
const path = typeof attr === 'string'
? (0, checkAttribute_js_1.default)(attr, entity ? table[entity].schema.attributes : table.Table.attributes)
: typeof size === 'string'
? (0, checkAttribute_js_1.default)(size, entity ? table[entity].schema.attributes : table.Table.attributes)
: (0, utils_js_1.error)(`A string for 'attr' or 'size' is required for condition expressions`);
const pathParts = path.split('.');
if (pathParts.length === 1) {
names[`#attr${grp}`] = pathParts[0];
}
else {
pathParts.forEach((part, i) => (names[`#attr${grp}_${i}`] = part));
}
const operand = Object.keys(names).join('.');
// Parse clause operator and value
let operator, value, filterType;
if (eq !== undefined) {
value = eq;
filterType = 'eq';
operator = '=';
}
if (ne !== undefined) {
value = value ? conditionError(filterType) : ne;
filterType = 'ne';
operator = '<>';
}
if (_in) {
value = value ? conditionError(filterType) : _in;
filterType = 'in';
operator = 'IN';
}
if (lt !== undefined) {
value = value ? conditionError(filterType) : lt;
filterType = 'lt';
operator = '<';
}
if (lte !== undefined) {
value = value ? conditionError(filterType) : lte;
filterType = 'lte';
operator = '<=';
}
if (gt !== undefined) {
value = value ? conditionError(filterType) : gt;
filterType = 'gt';
operator = '>';
}
if (gte !== undefined) {
value = value ? conditionError(filterType) : gte;
filterType = 'gte';
operator = '>=';
}
if (between) {
value = value ? conditionError(filterType) : between;
filterType = 'between';
operator = 'BETWEEN';
}
if (exists !== undefined) {
value = value ? conditionError(filterType) : exists;
filterType = 'exists';
operator = 'EXISTS';
}
if (contains) {
value = value ? conditionError(filterType) : contains;
filterType = 'contains';
operator = 'CONTAINS';
}
if (beginsWith) {
value = value ? conditionError(filterType) : beginsWith;
filterType = 'beginsWith';
operator = 'BEGINS_WITH';
}
if (type) {
value = value ? conditionError(filterType) : type;
filterType = 'type';
operator = 'ATTRIBUTE_TYPE';
}
// If a operator was set
if (operator && filterType) {
// If begins_with
if (operator === 'BETWEEN') {
// Verify array input
if (Array.isArray(value) && value.length === 2) {
// Add values and special key condition
values[`:attr${grp}_0`] = toDynamoValue(value[0]);
values[`:attr${grp}_1`] = toDynamoValue(value[1]);
clause = `${size ? `size(${operand})` : operand} between :attr${grp}_0 and :attr${grp}_1`;
}
else {
(0, utils_js_1.error)(`'between' conditions require an array with two values.`);
}
}
else if (operator === 'IN') {
if (!attr)
(0, utils_js_1.error)(`'in' conditions require an 'attr'.`);
// Verify array input
if (Array.isArray(value)) {
// Add values and special key condition
clause = `${operand} IN (${value
.map((x, i) => {
values[`:attr${grp}_${i}`] = toDynamoValue(x);
return `:attr${grp}_${i}`;
})
.join(',')})`;
}
else {
(0, utils_js_1.error)(`'in' conditions require an array.`);
}
}
else if (operator === 'EXISTS') {
if (!attr)
(0, utils_js_1.error)(`'exists' conditions require an 'attr'.`);
clause = value ? `attribute_exists(${operand})` : `attribute_not_exists(${operand})`;
}
else if (value && typeof value === 'object') {
const ref = value;
if (!exports.SUPPORTED_FILTER_EXP_ATTR_REF_OPERATORS.includes(filterType))
(0, utils_js_1.error)(`AttrRef is only supported for the following operators: ${exports.SUPPORTED_FILTER_EXP_ATTR_REF_OPERATORS.join(', ')}.`);
if (typeof (ref === null || ref === void 0 ? void 0 : ref.attr) !== 'string' || !(ref === null || ref === void 0 ? void 0 : ref.attr))
(0, utils_js_1.error)(`AttrRef must have an attr field which references another attribute in the same entity.`);
names[`#attr${grp}_ref`] = (0, checkAttribute_js_1.default)(ref.attr, entity ? table[entity].schema.attributes : table.Table.attributes);
clause = `${operand} ${operator} #attr${grp}_ref`;
}
else {
// Add value
values[`:attr${grp}`] = toDynamoValue(value);
// If begins_with, add special key condition
if (operator === 'BEGINS_WITH') {
if (!attr)
(0, utils_js_1.error)(`'beginsWith' conditions require an 'attr'.`);
clause = `begins_with(${operand},:attr${grp})`;
}
else if (operator === 'CONTAINS') {
if (!attr)
(0, utils_js_1.error)(`'contains' conditions require an 'attr'.`);
clause = `contains(${operand},:attr${grp})`;
}
else if (operator === 'ATTRIBUTE_TYPE') {
if (!attr)
(0, utils_js_1.error)(`'type' conditions require an 'attr'.`);
// TODO: validate/convert types
clause = `attribute_type(${operand},:attr${grp})`;
}
else {
clause = `${size ? `size(${operand})` : operand} ${operator} :attr${grp}`;
}
}
// Negate the clause
if (negate) {
clause = `(NOT ${clause})`;
}
}
else {
(0, utils_js_1.error)('A condition is required');
}
return {
logic: or ? 'OR' : 'AND',
clause,
names,
values
};
};