UNPKG

dynoexpr-fixed

Version:

Expression builder for AWS.DynamoDB.DocumentClient

616 lines (602 loc) 20.2 kB
// 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 };