UNPKG

14.4 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.Query = exports.Scan = void 0;
4const ddb = require("./aws/ddb/internal");
5const CustomError = require("./Error");
6const utils = require("./utils");
7const Condition_1 = require("./Condition");
8const Document_1 = require("./Document");
9const General_1 = require("./General");
10const Populate_1 = require("./Populate");
11var DocumentRetrieverTypes;
12(function (DocumentRetrieverTypes) {
13 DocumentRetrieverTypes["scan"] = "scan";
14 DocumentRetrieverTypes["query"] = "query";
15})(DocumentRetrieverTypes || (DocumentRetrieverTypes = {}));
16// DocumentRetriever is used for both Scan and Query since a lot of the code is shared between the two
17// type DocumentRetriever = BasicOperators;
18class DocumentRetriever {
19 constructor(model, typeInformation, object) {
20 this.internalSettings = { model, typeInformation };
21 let condition;
22 try {
23 condition = new Condition_1.Condition(object);
24 }
25 catch (e) {
26 e.message = `${e.message.replace(" is invalid.", "")} is invalid for the ${this.internalSettings.typeInformation.type} operation.`;
27 throw e;
28 }
29 this.settings = {
30 "condition": condition
31 };
32 }
33 exec(callback) {
34 let timesRequested = 0;
35 const prepareForReturn = async (result) => {
36 if (Array.isArray(result)) {
37 result = utils.merge_objects(...result);
38 }
39 if (this.settings.count) {
40 return {
41 "count": result.Count,
42 [`${this.internalSettings.typeInformation.pastTense}Count`]: result[`${utils.capitalize_first_letter(this.internalSettings.typeInformation.pastTense)}Count`]
43 };
44 }
45 const array = (await Promise.all(result.Items.map(async (item) => await new this.internalSettings.model.Document(item, { "type": "fromDynamo" }).conformToSchema({ "customTypesDynamo": true, "checkExpiredItem": true, "saveUnknown": true, "modifiers": ["get"], "type": "fromDynamo" })))).filter((a) => Boolean(a));
46 array.lastKey = result.LastEvaluatedKey ? Array.isArray(result.LastEvaluatedKey) ? result.LastEvaluatedKey.map((key) => this.internalSettings.model.Document.fromDynamo(key)) : this.internalSettings.model.Document.fromDynamo(result.LastEvaluatedKey) : undefined;
47 array.count = result.Count;
48 array[`${this.internalSettings.typeInformation.pastTense}Count`] = result[`${utils.capitalize_first_letter(this.internalSettings.typeInformation.pastTense)}Count`];
49 array[`times${utils.capitalize_first_letter(this.internalSettings.typeInformation.pastTense)}`] = timesRequested;
50 array["populate"] = Populate_1.PopulateDocuments;
51 array["toJSON"] = utils.dynamoose.documentToJSON;
52 return array;
53 };
54 const promise = this.internalSettings.model.pendingTaskPromise().then(() => this.getRequest()).then((request) => {
55 const allRequest = (extraParameters = {}) => {
56 let promise = ddb(this.internalSettings.typeInformation.type, Object.assign(Object.assign({}, request), extraParameters));
57 timesRequested++;
58 if (this.settings.all) {
59 promise = promise.then(async (result) => {
60 if (this.settings.all.delay && this.settings.all.delay > 0) {
61 await utils.timeout(this.settings.all.delay);
62 }
63 let lastKey = result.LastEvaluatedKey;
64 let requestedTimes = 1;
65 while (lastKey && (this.settings.all.max === 0 || requestedTimes < this.settings.all.max)) {
66 if (this.settings.all.delay && this.settings.all.delay > 0) {
67 await utils.timeout(this.settings.all.delay);
68 }
69 const nextRequest = await ddb(this.internalSettings.typeInformation.type, Object.assign(Object.assign(Object.assign({}, request), extraParameters), { "ExclusiveStartKey": lastKey }));
70 timesRequested++;
71 result = utils.merge_objects(result, nextRequest);
72 // The operation below is safe because right above we are overwriting the entire `result` variable, so there is no chance it'll be reassigned based on an outdated value since it's already been overwritten. There might be a better way to do this than ignoring the rule on the line below.
73 result.LastEvaluatedKey = nextRequest.LastEvaluatedKey; // eslint-disable-line require-atomic-updates
74 lastKey = nextRequest.LastEvaluatedKey;
75 requestedTimes++;
76 }
77 return result;
78 });
79 }
80 return promise;
81 };
82 if (this.settings.parallel) {
83 return Promise.all(new Array(this.settings.parallel).fill(0).map((a, index) => allRequest({ "Segment": index })));
84 }
85 else {
86 return allRequest();
87 }
88 });
89 // TODO: we do something similar to do this below in other functions as well (ex. get, save), where we allow a callback or a promise, we should figure out a way to make this code more DRY and have a standard way of doing this throughout Dynamoose
90 if (callback) {
91 promise.then((result) => prepareForReturn(result)).then((result) => callback(null, result)).catch((error) => callback(error));
92 }
93 else {
94 return (async () => {
95 const result = await promise;
96 const finalResult = await prepareForReturn(result);
97 return finalResult;
98 })();
99 }
100 }
101}
102Object.entries(Condition_1.Condition.prototype).forEach((prototype) => {
103 const [key, func] = prototype;
104 if (key !== "requestObject") {
105 DocumentRetriever.prototype[key] = function (...args) {
106 func.bind(this.settings.condition)(...args);
107 return this;
108 };
109 }
110});
111DocumentRetriever.prototype.getRequest = async function () {
112 const object = Object.assign(Object.assign({}, this.settings.condition.requestObject({ "conditionString": "FilterExpression", "conditionStringType": "array" })), { "TableName": this.internalSettings.model.name });
113 if (this.settings.limit) {
114 object.Limit = this.settings.limit;
115 }
116 if (this.settings.startAt) {
117 object.ExclusiveStartKey = Document_1.Document.isDynamoObject(this.settings.startAt) ? this.settings.startAt : this.internalSettings.model.Document.objectToDynamo(this.settings.startAt);
118 }
119 const indexes = await this.internalSettings.model.getIndexes();
120 function canUseIndexOfTable(hashKeyOfTable, rangeKeyOfTable, chart) {
121 const hashKeyInQuery = Object.entries(chart).find(([fieldName, { type }]) => type === "EQ" && fieldName === hashKeyOfTable);
122 if (!hashKeyInQuery) {
123 return false;
124 }
125 const isOneKeyQuery = Object.keys(chart).length === 1;
126 if (isOneKeyQuery && hashKeyInQuery) {
127 return true;
128 }
129 else if (rangeKeyOfTable) {
130 return Object.entries(chart).some(([fieldName]) => fieldName !== hashKeyInQuery[0]);
131 }
132 return false;
133 }
134 if (this.settings.index) {
135 object.IndexName = this.settings.index;
136 }
137 else if (this.internalSettings.typeInformation.type === "query") {
138 const comparisonChart = this.settings.condition.settings.conditions.reduce((res, item) => {
139 const myItem = Object.entries(item)[0];
140 res[myItem[0]] = { "type": myItem[1].type };
141 return res;
142 }, {});
143 if (!canUseIndexOfTable(this.internalSettings.model.getHashKey(), this.internalSettings.model.getRangeKey(), comparisonChart)) {
144 const validIndexes = utils.array_flatten(Object.values(indexes))
145 .map((index) => {
146 const { hash, range } = index.KeySchema.reduce((res, item) => {
147 res[item.KeyType.toLowerCase()] = item.AttributeName;
148 return res;
149 }, {});
150 index._hashKey = hash;
151 index._rangeKey = range;
152 return index;
153 })
154 .filter((index) => { var _a; return ((_a = comparisonChart[index._hashKey]) === null || _a === void 0 ? void 0 : _a.type) === "EQ"; });
155 const index = validIndexes.find((index) => comparisonChart[index._rangeKey]) || validIndexes[0];
156 if (!index) {
157 throw new CustomError.InvalidParameter("Index can't be found for query.");
158 }
159 else {
160 object.IndexName = index.IndexName;
161 }
162 }
163 }
164 function moveParameterNames(val, prefix) {
165 const entry = Object.entries(object.ExpressionAttributeNames).find((entry) => entry[1] === val);
166 if (!entry) {
167 return;
168 }
169 const [key, value] = entry;
170 const filterExpressionIndex = object.FilterExpression.findIndex((item) => item.includes(key));
171 const filterExpression = object.FilterExpression[filterExpressionIndex];
172 if (filterExpression.includes("attribute_exists") || filterExpression.includes("contains")) {
173 return;
174 }
175 object.ExpressionAttributeNames[`#${prefix}a`] = value;
176 delete object.ExpressionAttributeNames[key];
177 const valueKey = key.replace("#a", ":v");
178 Object.keys(object.ExpressionAttributeValues).filter((key) => key.startsWith(valueKey)).forEach((key) => {
179 object.ExpressionAttributeValues[key.replace(new RegExp(":v\\d"), `:${prefix}v`)] = object.ExpressionAttributeValues[key];
180 delete object.ExpressionAttributeValues[key];
181 });
182 const newExpression = filterExpression.replace(key, `#${prefix}a`).replace(new RegExp(valueKey, "g"), `:${prefix}v`);
183 object.KeyConditionExpression = `${object.KeyConditionExpression || ""}${object.KeyConditionExpression ? " AND " : ""}${newExpression}`;
184 utils.object.delete(object.FilterExpression, filterExpressionIndex);
185 const previousElementIndex = filterExpressionIndex === 0 ? 0 : filterExpressionIndex - 1;
186 if (object.FilterExpression[previousElementIndex] === "AND") {
187 utils.object.delete(object.FilterExpression, previousElementIndex);
188 }
189 }
190 if (this.internalSettings.typeInformation.type === "query") {
191 const index = utils.array_flatten(Object.values(indexes)).find((index) => index.IndexName === object.IndexName);
192 if (index) {
193 const { hash, range } = index.KeySchema.reduce((res, item) => {
194 res[item.KeyType.toLowerCase()] = item.AttributeName;
195 return res;
196 }, {});
197 moveParameterNames(hash, "qh");
198 if (range) {
199 moveParameterNames(range, "qr");
200 }
201 }
202 else {
203 moveParameterNames(this.internalSettings.model.getHashKey(), "qh");
204 if (this.internalSettings.model.getRangeKey()) {
205 moveParameterNames(this.internalSettings.model.getRangeKey(), "qr");
206 }
207 }
208 }
209 if (this.settings.consistent) {
210 object.ConsistentRead = this.settings.consistent;
211 }
212 if (this.settings.count) {
213 object.Select = "COUNT";
214 }
215 if (this.settings.parallel) {
216 object.TotalSegments = this.settings.parallel;
217 }
218 if (this.settings.sort === General_1.SortOrder.descending) {
219 object.ScanIndexForward = false;
220 }
221 if (this.settings.attributes) {
222 if (!object.ExpressionAttributeNames) {
223 object.ExpressionAttributeNames = {};
224 }
225 object.ProjectionExpression = this.settings.attributes.map((attribute) => {
226 let expressionAttributeName = "";
227 expressionAttributeName = (Object.entries(object.ExpressionAttributeNames).find((entry) => entry[1] === attribute) || [])[0];
228 if (!expressionAttributeName) {
229 const nextIndex = (Object.keys(object.ExpressionAttributeNames).map((item) => parseInt(item.replace("#a", ""))).filter((item) => !isNaN(item)).reduce((existing, item) => Math.max(item, existing), 0) || 0) + 1;
230 expressionAttributeName = `#a${nextIndex}`;
231 object.ExpressionAttributeNames[expressionAttributeName] = attribute;
232 }
233 return expressionAttributeName;
234 }).sort().join(", ");
235 }
236 if (object.FilterExpression) {
237 object.FilterExpression = utils.dynamoose.convertConditionArrayRequestObjectToString(object.FilterExpression);
238 }
239 if (object.FilterExpression === "") {
240 delete object.FilterExpression;
241 }
242 return object;
243};
244const settings = [
245 "limit",
246 "startAt",
247 "attributes",
248 { "name": "count", "boolean": true },
249 { "name": "consistent", "boolean": true },
250 { "name": "using", "settingsName": "index" }
251];
252settings.forEach((item) => {
253 DocumentRetriever.prototype[item.name || item] = function (value) {
254 const key = item.settingsName || item.name || item;
255 this.settings[key] = item.boolean ? !this.settings[key] : value;
256 return this;
257 };
258});
259DocumentRetriever.prototype.all = function (delay = 0, max = 0) {
260 this.settings.all = { delay, max };
261 return this;
262};
263class Scan extends DocumentRetriever {
264 exec(callback) {
265 return super.exec(callback);
266 }
267 parallel(value) {
268 this.settings.parallel = value;
269 return this;
270 }
271 constructor(model, object) {
272 super(model, { "type": DocumentRetrieverTypes.scan, "pastTense": "scanned" }, object);
273 }
274}
275exports.Scan = Scan;
276class Query extends DocumentRetriever {
277 exec(callback) {
278 return super.exec(callback);
279 }
280 sort(order) {
281 this.settings.sort = order;
282 return this;
283 }
284 constructor(model, object) {
285 super(model, { "type": DocumentRetrieverTypes.query, "pastTense": "queried" }, object);
286 }
287}
288exports.Query = Query;
289//# sourceMappingURL=DocumentRetriever.js.map
\No newline at end of file