dynoexpr-fixed
Version:
Expression builder for AWS.DynamoDB.DocumentClient
616 lines (602 loc) • 20.2 kB
JavaScript
// src/document-client.ts
var AwsSdk = null;
var AwsSdkDocumentClient = class {
static setDocumentClient(clientAwsSdk) {
AwsSdk = clientAwsSdk;
}
createSet(list, options) {
if (!AwsSdk) {
throw Error(
"dynoexpr: When working with Sets, please provide the AWS DocumentClient (v2)."
);
}
return AwsSdk.prototype.createSet(list, options);
}
};
// src/utils.ts
import crypto from "node:crypto";
function toString(data) {
if (data instanceof Set) {
return `Set(${JSON.stringify(Array.from(data))}))`;
}
return typeof data === "object" ? JSON.stringify(data) : `${data}:${typeof data}`;
}
function md5(data) {
return crypto.createHash("md5").update(toString(data).trim()).digest("hex");
}
function md5hash(data) {
return md5(data).slice(24);
}
function unquote(input) {
return input.replace(/^"/, "").replace(/"$/, "");
}
function splitByDot(input) {
const parts = input.match(/"[^"]+"|[^.]+/g) ?? [];
return parts.map(unquote);
}
function getSingleAttrName(attr) {
return `#n${md5hash(attr)}`;
}
function getAttrName(attribute) {
if (/^#/.test(attribute)) return attribute;
return splitByDot(attribute).map(getSingleAttrName).join(".");
}
function getAttrValue(value) {
if (typeof value === "string" && /^:/.test(value)) {
return value;
}
return `:v${md5hash(value)}`;
}
function splitByOperator(operator, input) {
const rg = new RegExp(` [${operator}] `, "g");
return input.split(rg).filter((m) => m !== operator).map((m) => m.trim()).map(unquote);
}
// src/expressions/helpers.ts
function convertValue(value) {
const v = value.trim();
if (v === "null") return null;
if (/^true$|^false$/i.test(v)) return v === "true";
if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
;
return v;
}
var REGEX_NOT = /^not\s(.+)/i;
function parseNotCondition(exp) {
const [, v] = REGEX_NOT.exec(exp) || [];
return v.trim();
}
var REGEX_ATTRIBUTE_TYPE = /^attribute_type\s*\(([^)]+)/i;
function parseAttributeTypeValue(exp) {
const [, v] = REGEX_ATTRIBUTE_TYPE.exec(exp) || [];
return convertValue(v);
}
var REGEX_BEGINS_WITH = /^begins_with[ |(]+([^)]+)/i;
function parseBeginsWithValue(exp) {
const [, v] = REGEX_BEGINS_WITH.exec(exp) || [];
return convertValue(v);
}
var REGEX_BETWEEN = /^between\s+(.+)\s+and\s+(.+)/i;
function parseBetweenValue(exp) {
const vs = REGEX_BETWEEN.exec(exp) || [];
return vs.slice(1, 3).map(convertValue);
}
var REGEX_COMPARISON = /^[>=<]+\s*(.+)/;
function parseComparisonValue(exp) {
const [, v] = REGEX_COMPARISON.exec(exp) || [];
const sv = v.trim();
return convertValue(sv);
}
var REGEX_PARSE_IN = /^in\s*\(([^)]+)/i;
var parseInValue = (exp) => {
const [, list] = REGEX_PARSE_IN.exec(exp) || [];
return list.split(",").map(convertValue);
};
var REGEX_SIZE = /^size\s*[<=>]+\s*(\d+)/i;
function parseSizeValue(exp) {
const [, v] = REGEX_SIZE.exec(exp) || [];
return convertValue(v);
}
var REGEX_CONTAINS = /^contains\s*\(([^)]+)\)/i;
function parseContainsValue(exp) {
const [, v] = REGEX_CONTAINS.exec(exp) || [];
return convertValue(v);
}
var REGEX_ATTRIBUTE_EXISTS = /^attribute_exists$/i;
var REGEX_ATTRIBUTE_NOT_EXISTS = /^attribute_not_exists$/i;
function flattenExpressions(Condition) {
return Object.entries(Condition).flatMap(([key, value]) => {
if (Array.isArray(value)) {
return value.map((v) => [key, v]);
}
return [[key, value]];
});
}
function buildConditionExpression(args) {
const { Condition = {}, LogicalOperator = "AND" } = args;
return flattenExpressions(Condition).map(([key, value]) => {
let expr;
if (typeof value === "string") {
let strValue = value.trim();
const hasNotCondition = REGEX_NOT.test(strValue);
if (hasNotCondition) {
strValue = parseNotCondition(strValue);
}
if (REGEX_COMPARISON.test(strValue)) {
const [, operator] = /([<=>]+)/.exec(strValue) || [];
const v = parseComparisonValue(strValue);
expr = `${getAttrName(key)} ${operator} ${getAttrValue(v)}`;
} else if (REGEX_BETWEEN.test(strValue)) {
const v = parseBetweenValue(strValue);
const exp = `between ${getAttrValue(v[0])} and ${getAttrValue(v[1])}`;
expr = `${getAttrName(key)} ${exp}`;
} else if (REGEX_PARSE_IN.test(strValue)) {
const v = parseInValue(strValue);
expr = `${getAttrName(key)} in (${v.map(getAttrValue).join(",")})`;
} else if (REGEX_ATTRIBUTE_EXISTS.test(strValue)) {
expr = `attribute_exists(${getAttrName(key)})`;
} else if (REGEX_ATTRIBUTE_NOT_EXISTS.test(strValue)) {
expr = `attribute_not_exists(${getAttrName(key)})`;
} else if (REGEX_ATTRIBUTE_TYPE.test(strValue)) {
const v = parseAttributeTypeValue(strValue);
expr = `attribute_type(${getAttrName(key)},${getAttrValue(v)})`;
} else if (REGEX_BEGINS_WITH.test(strValue)) {
const v = parseBeginsWithValue(strValue);
expr = `begins_with(${getAttrName(key)},${getAttrValue(v)})`;
} else if (REGEX_CONTAINS.test(strValue)) {
const v = parseContainsValue(strValue);
expr = `contains(${getAttrName(key)},${getAttrValue(v)})`;
} else if (REGEX_SIZE.test(strValue)) {
const [, operator] = /([<=>]+)/.exec(strValue) || [];
const v = parseSizeValue(strValue);
expr = `size(${getAttrName(key)}) ${operator} ${getAttrValue(v)}`;
} else {
expr = `${getAttrName(key)} = ${getAttrValue(strValue)}`;
}
expr = [hasNotCondition && "not", expr].filter(Boolean).join(" ");
} else {
expr = `${getAttrName(key)} = ${getAttrValue(value)}`;
}
return expr;
}).map((expr) => `(${expr})`).join(` ${LogicalOperator} `);
}
function buildConditionAttributeNames(condition, params = {}) {
return Object.keys(condition).reduce(
(acc, key) => {
splitByDot(key).forEach((k) => {
acc[getSingleAttrName(k)] = k;
});
return acc;
},
params.ExpressionAttributeNames || {}
);
}
function buildConditionAttributeValues(condition, params = {}) {
return flattenExpressions(condition).reduce(
(acc, [, value]) => {
let v;
if (typeof value === "string") {
let strValue = value.trim();
const hasNotCondition = REGEX_NOT.test(strValue);
if (hasNotCondition) {
strValue = parseNotCondition(strValue);
}
if (REGEX_COMPARISON.test(strValue)) {
v = parseComparisonValue(strValue);
} else if (REGEX_BETWEEN.test(strValue)) {
v = parseBetweenValue(strValue);
} else if (REGEX_PARSE_IN.test(strValue)) {
v = parseInValue(strValue);
} else if (REGEX_ATTRIBUTE_TYPE.test(strValue)) {
v = parseAttributeTypeValue(strValue);
} else if (REGEX_BEGINS_WITH.test(strValue)) {
v = parseBeginsWithValue(strValue);
} else if (REGEX_CONTAINS.test(strValue)) {
v = parseContainsValue(strValue);
} else if (REGEX_SIZE.test(strValue)) {
v = parseSizeValue(strValue);
} else if (!REGEX_ATTRIBUTE_EXISTS.test(strValue) && !REGEX_ATTRIBUTE_NOT_EXISTS.test(strValue)) {
v = strValue;
}
} else {
v = value;
}
if (typeof v === "undefined") {
return acc;
}
if (Array.isArray(v)) {
v.forEach((val) => {
acc[getAttrValue(val)] = val;
});
} else {
acc[getAttrValue(v)] = v;
}
return acc;
},
params.ExpressionAttributeValues || {}
);
}
// src/expressions/condition.ts
function getConditionExpression(params = {}) {
if (!params.Condition) {
return params;
}
const { Condition, ConditionLogicalOperator, ...restOfParams } = params;
const ConditionExpression = buildConditionExpression({
Condition,
LogicalOperator: ConditionLogicalOperator
});
const paramsWithConditions = {
...restOfParams,
ConditionExpression,
ExpressionAttributeNames: buildConditionAttributeNames(Condition, params),
ExpressionAttributeValues: buildConditionAttributeValues(Condition, params)
};
const { ExpressionAttributeNames, ExpressionAttributeValues } = paramsWithConditions;
if (Object.keys(ExpressionAttributeNames || {}).length === 0) {
delete paramsWithConditions.ExpressionAttributeNames;
}
if (Object.keys(ExpressionAttributeValues || {}).length === 0) {
delete paramsWithConditions.ExpressionAttributeValues;
}
return paramsWithConditions;
}
// src/expressions/filter.ts
function getFilterExpression(params = {}) {
if (!params.Filter) {
return params;
}
const { Filter, FilterLogicalOperator, ...restOfParams } = params;
const FilterExpression = buildConditionExpression({
Condition: Filter,
LogicalOperator: FilterLogicalOperator
});
const ExpressionAttributeNames = buildConditionAttributeNames(Filter, params);
const ExpressionAttributeValues = buildConditionAttributeValues(
Filter,
params
);
return {
...restOfParams,
FilterExpression,
ExpressionAttributeNames,
ExpressionAttributeValues
};
}
// src/expressions/key-condition.ts
function getKeyConditionExpression(params = {}) {
if (!params.KeyCondition) {
return params;
}
const { KeyCondition, KeyConditionLogicalOperator, ...restOfParams } = params;
const KeyConditionExpression = buildConditionExpression({
Condition: KeyCondition,
LogicalOperator: KeyConditionLogicalOperator
});
const ExpressionAttributeNames = buildConditionAttributeNames(
KeyCondition,
params
);
const ExpressionAttributeValues = buildConditionAttributeValues(
KeyCondition,
params
);
return {
...restOfParams,
KeyConditionExpression,
ExpressionAttributeNames,
ExpressionAttributeValues
};
}
// src/expressions/projection.ts
function getProjectionExpression(params = {}) {
if (!params.Projection) {
return params;
}
const { Projection, ...restOfParams } = params;
const fields = Projection.map((field) => field.trim());
const ProjectionExpression = fields.map(getAttrName).join(",");
const ExpressionAttributeNames = fields.reduce((acc, field) => {
const attrName = getAttrName(field);
if (attrName in acc) return acc;
acc[attrName] = field;
return acc;
}, params.ExpressionAttributeNames || {});
return {
...restOfParams,
ProjectionExpression,
ExpressionAttributeNames
};
}
// src/expressions/update.ts
function parseOperationValue(expr, key) {
const v = expr.replace(key, "").replace(/[+-]/, "");
return Number(v.trim());
}
function isMathExpression(name, value) {
if (typeof name !== "string") {
return false;
}
const rgLh = new RegExp(`^${name}\\s*[+-]\\s*\\d+$`);
const rgRh = new RegExp(`^\\d+\\s*[+-]\\s*${name}$`);
return rgLh.test(`${value}`) || rgRh.test(`${value}`);
}
function fromStrListToArray(strList) {
const [, inner] = /^\[([^\]]+)\]$/.exec(strList) || [];
return inner.split(",").map((v) => JSON.parse(v));
}
function getListAppendExpressionAttributes(key, value) {
const [, listAppendValues] = /list_append\((.+)\)/.exec(`${value}`) || [];
const rg = /(\[[^\]]+\])/g;
return Array.from(listAppendValues.matchAll(rg)).map((m) => m[0]).filter((v) => v !== key).flatMap((list) => fromStrListToArray(list));
}
function getListAppendExpression(key, value) {
const attr = getAttrName(key);
const [, listAppendValues] = /list_append\((.+)\)/.exec(`${value}`) || [];
const rg = /(\[[^\]]+\])/g;
const lists = Array.from(listAppendValues.matchAll(rg)).map((m) => m[0]);
const attrValues = {};
const newValue = lists.reduce((acc, list) => {
const listValues = fromStrListToArray(list);
attrValues[list] = getAttrValue(listValues);
return acc.replace(list, attrValues[list]);
}, listAppendValues);
const vv = newValue.split(/,/).map((v) => v.trim()).map((v) => v === key ? attr : v);
return `${attr} = list_append(${vv.join(", ")})`;
}
function getExpressionAttributes(params) {
const { Update = {}, UpdateAction = "SET" } = params;
return Object.entries(Update).reduce((acc, [key, value]) => {
if (!acc.ExpressionAttributeNames) acc.ExpressionAttributeNames = {};
if (!acc.ExpressionAttributeValues) acc.ExpressionAttributeValues = {};
splitByDot(key).forEach((k) => {
acc.ExpressionAttributeNames[getSingleAttrName(k)] = k;
});
if (UpdateAction !== "REMOVE") {
let v = value;
if (isMathExpression(key, value)) {
v = parseOperationValue(value, key);
}
if (/^if_not_exists/.test(`${value}`)) {
const [, vv] = /if_not_exists\((.+)\)/.exec(`${value}`) || [];
v = vv;
}
if (/^list_append/.test(`${value}`)) {
v = getListAppendExpressionAttributes(key, value);
}
if (Array.isArray(v) && /ADD|DELETE/.test(UpdateAction)) {
const s = new Set(v);
acc.ExpressionAttributeValues[getAttrValue(s)] = s;
} else {
acc.ExpressionAttributeValues[getAttrValue(v)] = v;
}
}
return acc;
}, params);
}
function getUpdateExpression(params = {}) {
if (!params.Update) return params;
const { Update, UpdateAction = "SET", ...restOfParams } = params;
const { ExpressionAttributeNames = {}, ExpressionAttributeValues = {} } = getExpressionAttributes(params);
let entries = "";
switch (UpdateAction) {
case "SET":
entries = Object.entries(Update).map(([name, value]) => {
if (/^if_not_exists/.test(`${value}`)) {
const attr = getAttrName(name);
const [, v] = /if_not_exists\((.+)\)/.exec(`${value}`) || [];
return `${attr} = if_not_exists(${attr}, ${getAttrValue(v)})`;
}
if (/^list_append/.test(`${value}`)) {
return getListAppendExpression(name, value);
}
if (isMathExpression(name, value)) {
const [, operator] = /(\s-|-\s|[+])/.exec(value) || [];
const val = value?.toString() || "unknown";
const operands = [];
if (/\+/.test(val)) {
operands.push(...splitByOperator("+", val));
} else if (/-/.test(val)) {
operands.push(...splitByOperator("-", val));
}
const expr = operands.map((operand) => operand.trim()).map((operand) => {
if (operand === name) return getAttrName(name);
const v = parseOperationValue(operand, name);
return getAttrValue(v);
}).join(` ${operator?.trim()} `);
return `${getAttrName(name)} = ${expr}`;
}
return `${getAttrName(name)} = ${getAttrValue(value)}`;
}).join(", ");
break;
case "ADD":
case "DELETE":
entries = Object.entries(Update).map(
([name, value]) => [
name,
Array.isArray(value) ? new Set(value) : value
]
).map(([name, value]) => [getAttrName(name), getAttrValue(value)]).map(([exprName, exprValue]) => `${exprName} ${exprValue}`).join(", ");
break;
case "REMOVE":
entries = Object.entries(Update).map(([name]) => [getAttrName(name)]).join(", ");
break;
default:
break;
}
const parameters = {
...restOfParams,
UpdateExpression: [UpdateAction, entries].join(" "),
ExpressionAttributeNames,
ExpressionAttributeValues
};
return parameters;
}
// src/expressions/update-ops.ts
function getUpdateSetExpression(params) {
const { UpdateSet, ...restOfParams } = params || {};
return getUpdateExpression({
...restOfParams,
Update: UpdateSet,
UpdateAction: "SET"
});
}
function getUpdateRemoveExpression(params) {
const { UpdateRemove, ...restOfParams } = params || {};
return getUpdateExpression({
...restOfParams,
Update: UpdateRemove,
UpdateAction: "REMOVE"
});
}
function getUpdateAddExpression(params) {
const { UpdateAdd, ...restOfParams } = params || {};
return getUpdateExpression({
...restOfParams,
Update: UpdateAdd,
UpdateAction: "ADD"
});
}
function getUpdateDeleteExpression(params) {
const { UpdateDelete, ...restOfParams } = params || {};
return getUpdateExpression({
...restOfParams,
Update: UpdateDelete,
UpdateAction: "DELETE"
});
}
function getUpdateOperationsExpression(params = {}) {
const updateExpressions = [];
const outputParams = [
getUpdateSetExpression,
getUpdateRemoveExpression,
getUpdateAddExpression,
getUpdateDeleteExpression
].reduce((acc, getExpressionFn) => {
const expr = getExpressionFn(acc);
const { UpdateExpression = "" } = expr;
updateExpressions.push(UpdateExpression);
return expr;
}, params);
const aggUpdateExpression = updateExpressions.filter(Boolean).filter((e, i, a) => a.indexOf(e) === i).join(" ");
if (aggUpdateExpression) {
outputParams.UpdateExpression = aggUpdateExpression;
}
return outputParams;
}
// src/operations/helpers.ts
function trimEmptyExpressionAttributes(expression) {
const trimmed = { ...expression };
const { ExpressionAttributeNames, ExpressionAttributeValues } = expression;
if (Object.keys(ExpressionAttributeNames || {}).length === 0) {
delete trimmed.ExpressionAttributeNames;
}
if (Object.keys(ExpressionAttributeValues || {}).length === 0) {
delete trimmed.ExpressionAttributeValues;
}
return trimmed;
}
// src/operations/single.ts
function convertValuesToDynamoDbSet(attributeValues) {
return Object.entries(attributeValues).reduce(
(acc, [key, value]) => {
if (value instanceof Set) {
const sdk = new AwsSdkDocumentClient();
acc[key] = sdk.createSet(Array.from(value));
} else {
acc[key] = value;
}
return acc;
},
{}
);
}
function getSingleTableExpressions(params = {}) {
const expression = [
getKeyConditionExpression,
getConditionExpression,
getFilterExpression,
getProjectionExpression,
getUpdateExpression,
getUpdateOperationsExpression
].reduce((acc, getExpressionFn) => getExpressionFn(acc), params);
delete expression.Update;
delete expression.UpdateAction;
const { ExpressionAttributeValues = {} } = expression;
if (Object.keys(ExpressionAttributeValues).length > 0) {
expression.ExpressionAttributeValues = convertValuesToDynamoDbSet(
ExpressionAttributeValues
);
}
return trimEmptyExpressionAttributes(expression);
}
// src/operations/batch.ts
function isBatchRequest(params) {
return "RequestItems" in params;
}
function isBatchGetRequest(tableParams) {
return !Array.isArray(tableParams);
}
function isBatchWriteRequest(tableParams) {
if (!Array.isArray(tableParams)) {
return false;
}
const [firstTable] = tableParams;
return "DeleteRequest" in firstTable || "PutRequest" in firstTable;
}
function getBatchExpressions(params) {
const RequestItems = Object.entries(params.RequestItems).reduce(
(accParams, [tableName, tableParams]) => {
if (isBatchGetRequest(tableParams)) {
accParams[tableName] = getSingleTableExpressions(tableParams);
}
if (isBatchWriteRequest(tableParams)) {
accParams[tableName] = tableParams;
}
return accParams;
},
{}
);
return {
...params,
RequestItems
};
}
// src/operations/transact.ts
function isTransactRequest(params) {
return "TransactItems" in params;
}
function getTransactExpressions(params) {
const TransactItems = params.TransactItems.map((tableItems) => {
const [key] = Object.keys(tableItems);
return {
[key]: getSingleTableExpressions(tableItems[key])
};
});
return {
...params,
TransactItems
};
}
// src/index.ts
function cleanOutput(output) {
const { DocumentClient, ...restOfOutput } = output || {};
return restOfOutput;
}
function dynoexpr(args) {
if (args.DocumentClient) {
AwsSdkDocumentClient.setDocumentClient(args.DocumentClient);
}
let returns;
if (isBatchRequest(args)) {
returns = getBatchExpressions(args);
}
if (isTransactRequest(args)) {
returns = getTransactExpressions(args);
}
returns = getSingleTableExpressions(args);
return cleanOutput(returns);
}
var src_default = dynoexpr;
export {
src_default as default
};