UNPKG

dynamoose

Version:

Dynamoose is a modeling tool for Amazon's DynamoDB (inspired by Mongoose)

358 lines (357 loc) 23.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Condition = void 0; const Item_1 = require("./Item"); const Error_1 = require("./Error"); const utils_1 = require("./utils"); const OR = Symbol("OR"); const Internal_1 = require("./Internal"); const InternalPropertiesClass_1 = require("./InternalPropertiesClass"); const { internalProperties } = Internal_1.default.General; const isRawConditionObject = (object) => Object.keys(object).length === 3 && ["ExpressionAttributeValues", "ExpressionAttributeNames"].every((item) => Boolean(object[item]) && typeof object[item] === "object"); var ConditionComparisonComparatorName; (function (ConditionComparisonComparatorName) { ConditionComparisonComparatorName["equals"] = "eq"; ConditionComparisonComparatorName["notEquals"] = "ne"; ConditionComparisonComparatorName["lessThan"] = "lt"; ConditionComparisonComparatorName["lessThanEquals"] = "le"; ConditionComparisonComparatorName["greaterThan"] = "gt"; ConditionComparisonComparatorName["greaterThanEquals"] = "ge"; ConditionComparisonComparatorName["beginsWith"] = "beginsWith"; ConditionComparisonComparatorName["contains"] = "contains"; ConditionComparisonComparatorName["exists"] = "exists"; ConditionComparisonComparatorName["in"] = "in"; ConditionComparisonComparatorName["between"] = "between"; })(ConditionComparisonComparatorName || (ConditionComparisonComparatorName = {})); var ConditionComparisonComparatorDynamoName; (function (ConditionComparisonComparatorDynamoName) { ConditionComparisonComparatorDynamoName["equals"] = "EQ"; ConditionComparisonComparatorDynamoName["notEquals"] = "NE"; ConditionComparisonComparatorDynamoName["lessThan"] = "LT"; ConditionComparisonComparatorDynamoName["lessThanEquals"] = "LE"; ConditionComparisonComparatorDynamoName["greaterThan"] = "GT"; ConditionComparisonComparatorDynamoName["greaterThanEquals"] = "GE"; ConditionComparisonComparatorDynamoName["beginsWith"] = "BEGINS_WITH"; ConditionComparisonComparatorDynamoName["contains"] = "CONTAINS"; ConditionComparisonComparatorDynamoName["notContains"] = "NOT_CONTAINS"; ConditionComparisonComparatorDynamoName["exists"] = "EXISTS"; ConditionComparisonComparatorDynamoName["notExists"] = "NOT_EXISTS"; ConditionComparisonComparatorDynamoName["in"] = "IN"; ConditionComparisonComparatorDynamoName["between"] = "BETWEEN"; })(ConditionComparisonComparatorDynamoName || (ConditionComparisonComparatorDynamoName = {})); const types = [ { "name": ConditionComparisonComparatorName.equals, "typeName": ConditionComparisonComparatorDynamoName.equals, "not": ConditionComparisonComparatorDynamoName.notEquals }, { "name": ConditionComparisonComparatorName.notEquals, "typeName": ConditionComparisonComparatorDynamoName.notEquals, "not": ConditionComparisonComparatorDynamoName.equals }, { "name": ConditionComparisonComparatorName.lessThan, "typeName": ConditionComparisonComparatorDynamoName.lessThan, "not": ConditionComparisonComparatorDynamoName.greaterThanEquals }, { "name": ConditionComparisonComparatorName.lessThanEquals, "typeName": ConditionComparisonComparatorDynamoName.lessThanEquals, "not": ConditionComparisonComparatorDynamoName.greaterThan }, { "name": ConditionComparisonComparatorName.greaterThan, "typeName": ConditionComparisonComparatorDynamoName.greaterThan, "not": ConditionComparisonComparatorDynamoName.lessThanEquals }, { "name": ConditionComparisonComparatorName.greaterThanEquals, "typeName": ConditionComparisonComparatorDynamoName.greaterThanEquals, "not": ConditionComparisonComparatorDynamoName.lessThan }, { "name": ConditionComparisonComparatorName.beginsWith, "typeName": ConditionComparisonComparatorDynamoName.beginsWith }, { "name": ConditionComparisonComparatorName.contains, "typeName": ConditionComparisonComparatorDynamoName.contains, "not": ConditionComparisonComparatorDynamoName.notContains }, { "name": ConditionComparisonComparatorName.exists, "typeName": ConditionComparisonComparatorDynamoName.exists, "not": ConditionComparisonComparatorDynamoName.notExists }, { "name": ConditionComparisonComparatorName.in, "typeName": ConditionComparisonComparatorDynamoName.in }, { "name": ConditionComparisonComparatorName.between, "typeName": ConditionComparisonComparatorDynamoName.between, "multipleArguments": true } ]; class Condition extends InternalPropertiesClass_1.InternalPropertiesClass { /** * TODO * @param object * @returns Condition */ constructor(object) { super(); this.setInternalProperties(internalProperties, { "requestObject": async (model, settings = { "conditionString": "ConditionExpression", "conditionStringType": "string" }) => { const toDynamo = async (key, value) => { const newObj = await Item_1.Item.objectFromSchema({ [key]: value }, model, { "type": "toDynamo", "modifiers": ["set"], "typeCheck": false, "mapAttributes": true }); const newObjKeys = Object.keys(newObj); // TODO: not quite sure how to unit test the error below. Need to figure this out. Maybe by mocking `Item.objectFromSchema`??? We don't currently have a system in place to do that easily tho. /* istanbul ignore next */ if (newObjKeys.length > 1) { /* istanbul ignore next */ throw new Error_1.default.OtherError("Error retrieving `requestObject` from Condition. Please submit an issue on the Dynamoose GitHub repository."); } const newValue = newObj[newObjKeys[0]]; return Item_1.Item.objectToDynamo(newValue, { "type": "value" }); }; if (this.getInternalProperties(internalProperties).settings.raw && utils_1.default.object.equals(Object.keys(this.getInternalProperties(internalProperties).settings.raw).sort(), [settings.conditionString, "ExpressionAttributeValues", "ExpressionAttributeNames"].sort())) { return utils_1.default.async_reduce(Object.entries(this.getInternalProperties(internalProperties).settings.raw.ExpressionAttributeValues), async (obj, entry) => { const [key, value] = entry; // TODO: we should fix this so that we can do `isDynamoItem(value)` if (!Item_1.Item.isDynamoObject({ "key": value })) { obj.ExpressionAttributeValues[key] = await toDynamo(key, value); } return obj; }, this.getInternalProperties(internalProperties).settings.raw); } else if (this.getInternalProperties(internalProperties).settings.conditions.length === 0) { return {}; } let index = (settings.index || {}).start || 0; const setIndex = (i) => { index = i; (settings.index || { "set": utils_1.default.empty_function }).set(i); }; async function main(input) { return utils_1.default.async_reduce(input, async (object, entry, i, arr) => { let expression = ""; if (Array.isArray(entry)) { const result = await main(entry); const newData = utils_1.default.merge_objects.main({ "combineMethod": "object_combine" })(Object.assign({}, result), Object.assign({}, object)); const returnObject = utils_1.default.object.pick(newData, ["ExpressionAttributeNames", "ExpressionAttributeValues"]); expression = settings.conditionStringType === "array" ? result[settings.conditionString] : `(${result[settings.conditionString]})`; object = Object.assign(Object.assign({}, object), returnObject); } else if (entry !== OR) { const keyConditionObj = Object.entries(entry)[0]; const key = await model.getInternalProperties(internalProperties).dynamoPropertyForAttribute(keyConditionObj[0]); const condition = keyConditionObj[1]; const { value } = condition; const keys = { "name": `#a${index}`, "value": `:v${index}` }; setIndex(++index); const keyParts = key.split("."); if (keyParts.length === 1) { object.ExpressionAttributeNames[keys.name] = key; } else { keys.name = keyParts.reduce((finalName, part, index) => { const name = `${keys.name}_${index}`; object.ExpressionAttributeNames[name] = part; finalName.push(name); return finalName; }, []).join("."); } object.ExpressionAttributeValues[keys.value] = await toDynamo(key, value); switch (condition.type) { case "EQ": case "NE": expression = `${keys.name} ${condition.type === "EQ" ? "=" : "<>"} ${keys.value}`; break; case "IN": delete object.ExpressionAttributeValues[keys.value]; expression = `${keys.name} IN (${value.map((_v, i) => `${keys.value}_${i + 1}`).join(", ")})`; await Promise.all(value.map(async (valueItem, i) => { object.ExpressionAttributeValues[`${keys.value}_${i + 1}`] = await toDynamo(key, valueItem); })); break; case "GT": case "GE": case "LT": case "LE": expression = `${keys.name} ${condition.type.startsWith("G") ? ">" : "<"}${condition.type.endsWith("E") ? "=" : ""} ${keys.value}`; break; case "BETWEEN": expression = `${keys.name} BETWEEN ${keys.value}_1 AND ${keys.value}_2`; object.ExpressionAttributeValues[`${keys.value}_1`] = await toDynamo(key, value[0]); object.ExpressionAttributeValues[`${keys.value}_2`] = await toDynamo(key, value[1]); delete object.ExpressionAttributeValues[keys.value]; break; case "CONTAINS": case "NOT_CONTAINS": expression = `${condition.type === "NOT_CONTAINS" ? "NOT " : ""}contains (${keys.name}, ${keys.value})`; break; case "EXISTS": case "NOT_EXISTS": expression = `attribute_${condition.type === "NOT_EXISTS" ? "not_" : ""}exists (${keys.name})`; delete object.ExpressionAttributeValues[keys.value]; break; case "BEGINS_WITH": expression = `begins_with (${keys.name}, ${keys.value})`; break; } } else { return object; } const conditionStringNewItems = [expression]; if (object[settings.conditionString].length > 0) { conditionStringNewItems.unshift(` ${arr[i - 1] === OR ? "OR" : "AND"} `); } conditionStringNewItems.forEach((item) => { if (typeof object[settings.conditionString] === "string") { object[settings.conditionString] = `${object[settings.conditionString]}${item}`; } else { object[settings.conditionString].push(Array.isArray(item) ? item : item.trim()); } }); return object; }, { [settings.conditionString]: settings.conditionStringType === "array" ? [] : "", "ExpressionAttributeNames": {}, "ExpressionAttributeValues": {} }); } return utils_1.default.object.clearEmpties(await main(this.getInternalProperties(internalProperties).settings.conditions)); }, "comparisonChart": (model) => { const comparisonChart = this.getInternalProperties(internalProperties).settings.conditions.reduce((res, item) => { const myItem = Object.entries(item)[0]; const key = myItem[0]; res[key] = { "type": myItem[1].type }; return res; }, {}); return Item_1.Item.objectFromSchema(comparisonChart, model, { "type": "toDynamo", "typeCheck": false, "mapAttributes": true }); } }); if (object instanceof Condition) { this.setInternalProperties(internalProperties, Object.assign(Object.assign({}, this.getInternalProperties(internalProperties)), { "settings": Object.assign({}, object.getInternalProperties(internalProperties).settings) })); } else { this.setInternalProperties(internalProperties, Object.assign(Object.assign({}, this.getInternalProperties(internalProperties)), { "settings": { "conditions": [], "pending": {} // represents the pending chain of filter data waiting to be attached to the `conditions` parameter. For example, storing the key before we know what the comparison operator is. } })); if (typeof object === "object") { if (!isRawConditionObject(object)) { Object.keys(object).forEach((key) => { const value = object[key]; const valueType = typeof value === "object" && Object.keys(value).length > 0 ? Object.keys(value)[0] : "eq"; const comparisonType = types.find((item) => item.name === valueType); if (!comparisonType) { throw new Error_1.default.InvalidFilterComparison(`The type: ${valueType} is invalid.`); } this.getInternalProperties(internalProperties).settings.conditions.push({ [key]: { "type": comparisonType.typeName, "value": typeof value[valueType] !== "undefined" && value[valueType] !== null ? value[valueType] : value } }); }); } } else if (object) { const internalPropertiesObject = this.getInternalProperties(internalProperties); internalPropertiesObject.settings.pending.key = object; this.setInternalProperties(internalProperties, internalPropertiesObject); } const internalPropertiesObject = this.getInternalProperties(internalProperties); internalPropertiesObject.settings.raw = object; this.setInternalProperties(internalProperties, internalPropertiesObject); } return this; } /** * This function specifies an `OR` join between two conditions, as opposed to the default `AND`. The condition will return `true` if either condition is met. * * ```js * new dynamoose.Condition().where("id").eq(1).or().where("name").eq("Bob"); // id = 1 OR name = Bob * ``` * @returns Condition */ or() { this.getInternalProperties(internalProperties).settings.conditions.push(OR); return this; } /** * This function has no behavior and is only used to increase readability of your conditional. This function can be omitted with no behavior change to your code. * * ```js * // The two condition objects below are identical * new dynamoose.Condition().where("id").eq(1).and().where("name").eq("Bob"); * new dynamoose.Condition().where("id").eq(1).where("name").eq("Bob"); * ``` * @returns Condition */ and() { return this; } /** * This function sets the condition to use the opposite comparison type for the given condition. You can find the list opposite comparison types below. * * | Original | Opposite | * |---|---| * | equals (EQ) | not equals (NE) | * | less than or equals (LE) | greater than (GT) | * | less than (LT) | greater than or equals (GE) | * | null (NULL) | not null (NOT_NULL) | * | contains (CONTAINS) | not contains (NOT_CONTAINS) | * | exists (EXISTS) | not exists (NOT_EXISTS) | * * The following comparisons do not have an opposite comparison type, and will throw an error if you try to use condition.not() with them. * * | Original | * |---| * | in (IN) | * | between (BETWEEN) | * | begins with (BEGINS_WITH) | * * ```js * new dynamoose.Condition().where("id").not().eq(1); // Retrieve all objects where id does NOT equal 1 * new dynamoose.Condition().where("id").not().between(1, 2); // Will throw error since between does not have an opposite comparison type * ``` * @returns Condition */ not() { this.getInternalProperties(internalProperties).settings.pending.not = !this.getInternalProperties(internalProperties).settings.pending.not; return this; } /** * This function takes in a `Condition` instance as a parameter and uses that as a group. This lets you specify the priority of the conditional. You can also pass a function into the `condition` parameter and Dynamoose will call your function with one argument which is a condition instance that you can return to specify the group. * * ```js * // The two condition objects below are identical * new dynamoose.Condition().where("id").eq(1).and().parenthesis(new dynamoose.Condition().where("name").eq("Bob")); // id = 1 AND (name = Bob) * new dynamoose.Condition().where("id").eq(1).and().parenthesis((condition) => condition.where("name").eq("Bob")); // id = 1 AND (name = Bob) * ``` * * `condition.group` is an alias to this method. * @param condition A new Condition instance or a function. If a function is passed, it will be called with one argument which is a condition instance that you can return to specify the group. * @returns Condition */ parenthesis(condition) { condition = typeof condition === "function" ? condition(new Condition()) : condition; const conditions = condition.getInternalProperties(internalProperties).settings.conditions; this.getInternalProperties(internalProperties).settings.conditions.push(conditions); return this; } /** * This function takes in a `Condition` instance as a parameter and uses that as a group. This lets you specify the priority of the conditional. You can also pass a function into the `condition` parameter and Dynamoose will call your function with one argument which is a condition instance that you can return to specify the group. * * ```js * // The two condition objects below are identical * new dynamoose.Condition().where("id").eq(1).and().group(new dynamoose.Condition().where("name").eq("Bob")); // id = 1 AND (name = Bob) * new dynamoose.Condition().where("id").eq(1).and().group((condition) => condition.where("name").eq("Bob")); // id = 1 AND (name = Bob) * ``` * * `condition.parenthesis` is an alias to this method. * @param condition A new Condition instance or a function. If a function is passed, it will be called with one argument which is a condition instance that you can return to specify the group. * @returns Condition */ group(condition) { return this.parenthesis(condition); } } exports.Condition = Condition; function finalizePending(instance) { const pending = instance.getInternalProperties(internalProperties).settings.pending; let dynamoNameType; if (pending.not === true) { if (!pending.type.not) { throw new Error_1.default.InvalidFilterComparison(`${pending.type.typeName} can not follow not()`); } dynamoNameType = pending.type.not; } else { dynamoNameType = pending.type.typeName; } instance.getInternalProperties(internalProperties).settings.conditions.push({ [pending.key]: { "type": dynamoNameType, "value": pending.value } }); instance.getInternalProperties(internalProperties).settings.pending = {}; } Condition.prototype.where = Condition.prototype.filter = Condition.prototype.attribute = function (key) { this.getInternalProperties(internalProperties).settings.pending = { key }; return this; }; // TODO: I don't think this prototypes are being exposed which is gonna cause a lot of problems with our type definition file. Need to figure out a better way to do this since they aren't defined and are dynamic. types.forEach((type) => { Condition.prototype[type.name] = function (...args) { if (args.includes(undefined)) { console.warn(`Dynamoose Warning: Passing \`undefined\` into a condition ${type.name} is not supported and can lead to behavior where DynamoDB returns an error related to your conditional. In a future version of Dynamoose this behavior will throw an error. If you believe your conditional is valid and you received this message in error, please submit an issue at https://github.com/dynamoose/dynamoose/issues/new/choose.`); // eslint-disable-line no-console } this.getInternalProperties(internalProperties).settings.pending.value = type.multipleArguments ? args : args[0]; this.getInternalProperties(internalProperties).settings.pending.type = type; finalizePending(this); return this; }; });