1 | const Document = require("./Document");
|
2 | const Error = require("./Error");
|
3 | const utils = require("./utils");
|
4 | const OR = Symbol("OR");
|
5 |
|
6 | const isRawConditionObject = (object) => Object.keys(object).length === 3 && ["ExpressionAttributeValues", "ExpressionAttributeNames"].every((item) => Boolean(object[item]) && typeof object[item] === "object");
|
7 |
|
8 | class Condition {
|
9 | constructor(object) {
|
10 | if (object instanceof Condition) {
|
11 | Object.entries(object).forEach((entry) => {
|
12 | const [key, value] = entry;
|
13 | this[key] = value;
|
14 | });
|
15 | } else {
|
16 | this.settings = {};
|
17 | this.settings.conditions = [];
|
18 | this.settings.pending = {};
|
19 |
|
20 | if (typeof object === "object") {
|
21 | if (!isRawConditionObject(object)) {
|
22 | Object.keys(object).forEach((key) => {
|
23 | const value = object[key];
|
24 | const valueType = typeof value === "object" && Object.keys(value).length > 0 ? Object.keys(value)[0] : "eq";
|
25 | const comparisonType = types.find((item) => item.name === valueType);
|
26 |
|
27 | if (!comparisonType) {
|
28 | throw new Error.InvalidFilterComparison(`The type: ${valueType} is invalid.`);
|
29 | }
|
30 |
|
31 | this.settings.conditions.push([key, {"type": comparisonType.typeName, "value": typeof value[valueType] !== "undefined" && value[valueType] !== null ? value[valueType] : value}]);
|
32 | });
|
33 | }
|
34 | } else if (object) {
|
35 | this.settings.pending.key = object;
|
36 | }
|
37 | }
|
38 | this.settings.raw = object;
|
39 |
|
40 | return this;
|
41 | }
|
42 | }
|
43 |
|
44 |
|
45 | function finalizePending(instance) {
|
46 | const pending = instance.settings.pending;
|
47 |
|
48 | if (pending.not === true) {
|
49 | if (!pending.type.not) {
|
50 | throw new Error.InvalidFilterComparison(`${pending.type.typeName} can not follow not()`);
|
51 | }
|
52 | pending.type = pending.type.not;
|
53 | } else {
|
54 | pending.type = pending.type.typeName;
|
55 | }
|
56 |
|
57 | instance.settings.conditions.push([pending.key, {
|
58 | "type": pending.type,
|
59 | "value": pending.value
|
60 | }]);
|
61 |
|
62 | instance.settings.pending = {};
|
63 | }
|
64 |
|
65 | Condition.prototype.parenthesis = Condition.prototype.group = function (value) {
|
66 | value = typeof value === "function" ? value(new Condition()) : value;
|
67 | this.settings.conditions.push(value.settings.conditions);
|
68 | return this;
|
69 | };
|
70 | Condition.prototype.or = function() {
|
71 | this.settings.conditions.push(OR);
|
72 | return this;
|
73 | };
|
74 | Condition.prototype.and = function() { return this; };
|
75 | Condition.prototype.not = function() {
|
76 | this.settings.pending.not = !this.settings.pending.not;
|
77 | return this;
|
78 | };
|
79 | Condition.prototype.where = Condition.prototype.filter = Condition.prototype.attribute = function(key) {
|
80 | this.settings.pending = {key};
|
81 | return this;
|
82 | };
|
83 | const types = [
|
84 | {"name": "eq", "typeName": "EQ", "not": "NE"},
|
85 | {"name": "lt", "typeName": "LT", "not": "GE"},
|
86 | {"name": "le", "typeName": "LE", "not": "GT"},
|
87 | {"name": "gt", "typeName": "GT", "not": "LE"},
|
88 | {"name": "ge", "typeName": "GE", "not": "LT"},
|
89 | {"name": "beginsWith", "typeName": "BEGINS_WITH"},
|
90 | {"name": "contains", "typeName": "CONTAINS", "not": "NOT_CONTAINS"},
|
91 | {"name": "exists", "typeName": "EXISTS", "not": "NOT_EXISTS"},
|
92 | {"name": "in", "typeName": "IN"},
|
93 | {"name": "between", "typeName": "BETWEEN", "multipleArguments": true}
|
94 | ];
|
95 | types.forEach((type) => {
|
96 | Condition.prototype[type.name] = function(value) {
|
97 | this.settings.pending.value = type.value || (type.multipleArguments ? [...arguments] : value);
|
98 | this.settings.pending.type = type;
|
99 | finalizePending(this);
|
100 | return this;
|
101 | };
|
102 | });
|
103 |
|
104 | Condition.prototype.requestObject = function(settings = {"conditionString": "ConditionExpression"}) {
|
105 | if (this.settings.raw && utils.object.equals(Object.keys(this.settings.raw).sort(), [settings.conditionString, "ExpressionAttributeValues", "ExpressionAttributeNames"].sort())) {
|
106 | return Object.entries(this.settings.raw.ExpressionAttributeValues).reduce((obj, entry) => {
|
107 | const [key, value] = entry;
|
108 |
|
109 | if (!Document.isDynamoObject({"key": value})) {
|
110 | obj.ExpressionAttributeValues[key] = Document.toDynamo(value, {"type": "value"});
|
111 | }
|
112 | return obj;
|
113 | }, this.settings.raw);
|
114 | } else if (this.settings.conditions.length === 0) {
|
115 | return {};
|
116 | }
|
117 |
|
118 | let index = (settings.index || {}).starting || 0;
|
119 | const setIndex = (i) => {index = i; (settings.index || {"set": utils.empty_function}).set(i);};
|
120 | function main(input) {
|
121 | return input.reduce((object, entry, i, arr) => {
|
122 | let expression = "";
|
123 | if (Array.isArray(entry[0])) {
|
124 | const result = main(entry);
|
125 | const newData = utils.merge_objects.main({"combineMethod": "object_combine"})({...result}, {...object});
|
126 | const returnObject = utils.object.pick(newData, ["ExpressionAttributeNames", "ExpressionAttributeValues"]);
|
127 |
|
128 | expression = settings.conditionStringType === "array" ? result[settings.conditionString] : `(${result[settings.conditionString]})`;
|
129 | object = {...object, ...returnObject};
|
130 | } else if (entry !== OR) {
|
131 | const [key, condition] = entry;
|
132 | const {value} = condition;
|
133 | const keys = {"name": `#a${index}`, "value": `:v${index}`};
|
134 | setIndex(++index);
|
135 |
|
136 | const keyParts = key.split(".");
|
137 | if (keyParts.length === 1) {
|
138 | object.ExpressionAttributeNames[keys.name] = key;
|
139 | } else {
|
140 | keys.name = keyParts.reduce((finalName, part, index) => {
|
141 | const name = `${keys.name}_${index}`;
|
142 | object.ExpressionAttributeNames[name] = part;
|
143 | finalName.push(name);
|
144 | return finalName;
|
145 | }, []).join(".");
|
146 | }
|
147 | object.ExpressionAttributeValues[keys.value] = Document.toDynamo(value, {"type": "value"});
|
148 |
|
149 | switch (condition.type) {
|
150 | case "EQ":
|
151 | case "NE":
|
152 | expression = `${keys.name} ${condition.type === "EQ" ? "=" : "<>"} ${keys.value}`;
|
153 | break;
|
154 | case "IN":
|
155 | delete object.ExpressionAttributeValues[keys.value];
|
156 | expression = `${keys.name} IN (${value.map((v, i) => `${keys.value}_${i + 1}`).join(", ")})`;
|
157 | value.forEach((valueItem, i) => {
|
158 | object.ExpressionAttributeValues[`${keys.value}_${i + 1}`] = Document.toDynamo(valueItem, {"type": "value"});
|
159 | });
|
160 | break;
|
161 | case "GT":
|
162 | case "GE":
|
163 | case "LT":
|
164 | case "LE":
|
165 | expression = `${keys.name} ${condition.type.startsWith("G") ? ">" : "<"}${condition.type.endsWith("E") ? "=" : ""} ${keys.value}`;
|
166 | break;
|
167 | case "BETWEEN":
|
168 | expression = `${keys.name} BETWEEN ${keys.value}_1 AND ${keys.value}_2`;
|
169 | object.ExpressionAttributeValues[`${keys.value}_1`] = Document.toDynamo(value[0], {"type": "value"});
|
170 | object.ExpressionAttributeValues[`${keys.value}_2`] = Document.toDynamo(value[1], {"type": "value"});
|
171 | delete object.ExpressionAttributeValues[keys.value];
|
172 | break;
|
173 | case "CONTAINS":
|
174 | case "NOT_CONTAINS":
|
175 | expression = `${condition.type === "NOT_CONTAINS" ? "NOT " : ""}contains (${keys.name}, ${keys.value})`;
|
176 | break;
|
177 | case "EXISTS":
|
178 | case "NOT_EXISTS":
|
179 | expression = `attribute_${condition.type === "NOT_EXISTS" ? "not_" : ""}exists (${keys.name})`;
|
180 | delete object.ExpressionAttributeValues[keys.value];
|
181 | break;
|
182 | case "BEGINS_WITH":
|
183 | expression = `begins_with (${keys.name}, ${keys.value})`;
|
184 | break;
|
185 | }
|
186 | } else {
|
187 | return object;
|
188 | }
|
189 |
|
190 | const conditionStringNewItems = [expression];
|
191 | if (object[settings.conditionString].length > 0) {
|
192 | conditionStringNewItems.unshift(` ${arr[i - 1] === OR ? "OR" : "AND"} `);
|
193 | }
|
194 | conditionStringNewItems.forEach((item) => {
|
195 | if (typeof object[settings.conditionString] === "string") {
|
196 | object[settings.conditionString] = `${object[settings.conditionString]}${item}`;
|
197 | } else {
|
198 | object[settings.conditionString].push(Array.isArray(item) ? item : item.trim());
|
199 | }
|
200 | });
|
201 |
|
202 | return object;
|
203 | }, {[settings.conditionString]: settings.conditionStringType === "array" ? [] : "", "ExpressionAttributeNames": {}, "ExpressionAttributeValues": {}});
|
204 | }
|
205 | return main(this.settings.conditions);
|
206 | };
|
207 |
|
208 | module.exports = Condition;
|