UNPKG

8 kBJavaScriptView Raw
1const Document = require("./Document");
2const Error = require("./Error");
3const utils = require("./utils");
4const OR = Symbol("OR");
5
6const isRawConditionObject = (object) => Object.keys(object).length === 3 && ["ExpressionAttributeValues", "ExpressionAttributeNames"].every((item) => Boolean(object[item]) && typeof object[item] === "object");
7
8class 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 = {}; // 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.
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
45function 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
65Condition.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};
70Condition.prototype.or = function() {
71 this.settings.conditions.push(OR);
72 return this;
73};
74Condition.prototype.and = function() { return this; };
75Condition.prototype.not = function() {
76 this.settings.pending.not = !this.settings.pending.not;
77 return this;
78};
79Condition.prototype.where = Condition.prototype.filter = Condition.prototype.attribute = function(key) {
80 this.settings.pending = {key};
81 return this;
82};
83const 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];
95types.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
104Condition.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 // TODO: we should fix this so that we can do `isDynamoItem(value)`
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
208module.exports = Condition;