electrodb 1.4.7 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/entity.js CHANGED
@@ -150,7 +150,8 @@ class Entity {
150
150
  names: expressions.names || {},
151
151
  values: expressions.values || {},
152
152
  expression: expressions.expression || ""
153
- }
153
+ },
154
+ _isCollectionQuery: true,
154
155
  };
155
156
 
156
157
  let index = this.model.translations.collections.fromCollectionToIndex[collection];
@@ -241,8 +242,10 @@ class Entity {
241
242
  return await this.executeBulkWrite(parameters, config);
242
243
  case MethodTypes.batchGet:
243
244
  return await this.executeBulkGet(parameters, config);
245
+ case MethodTypes.query:
246
+ return await this.executeQuery(parameters, config)
244
247
  default:
245
- return await this.executeQuery(method, parameters, config);
248
+ return await this.executeOperation(method, parameters, config);
246
249
  }
247
250
  } catch (err) {
248
251
  if (config.originalErr || stackTrace === undefined) {
@@ -324,7 +327,54 @@ class Entity {
324
327
  return [resultsAll, unprocessedAll];
325
328
  }
326
329
 
327
- async executeQuery(method, parameters, config) {
330
+ async executeQuery(parameters, config = {}) {
331
+ let results = config._isCollectionQuery
332
+ ? {}
333
+ : [];
334
+ let ExclusiveStartKey;
335
+ let pages = this._normalizePagesValue(config.pages);
336
+ let max = this._normalizeLimitValue(config.limit);
337
+ let iterations = 0;
338
+ let count = 0;
339
+ do {
340
+ let limit = max === undefined
341
+ ? parameters.Limit
342
+ : max - count;
343
+ let response = await this._exec("query", {ExclusiveStartKey, ...parameters, Limit: limit});
344
+
345
+ ExclusiveStartKey = response.LastEvaluatedKey;
346
+
347
+ if (validations.isFunction(config.parse)) {
348
+ response = config.parse(config, response);
349
+ } else {
350
+ response = this.formatResponse(response, parameters.IndexName, config);
351
+ }
352
+
353
+ if (config.raw || config._isPagination) {
354
+ return response;
355
+ } else if (config._isCollectionQuery) {
356
+ for (const entity in response) {
357
+ if (max) {
358
+ count += response[entity].length;
359
+ }
360
+ results[entity] = results[entity] || [];
361
+ results[entity] = [...results[entity], ...response[entity]];
362
+ }
363
+ } else if (Array.isArray(response)) {
364
+ if (max) {
365
+ count += response.length;
366
+ }
367
+ results = [...results, ...response];
368
+ } else {
369
+ return response;
370
+ }
371
+
372
+ iterations++;
373
+ } while(ExclusiveStartKey && iterations < pages && (max === undefined || count < max));
374
+ return results;
375
+ }
376
+
377
+ async executeOperation(method, parameters, config) {
328
378
  let response = await this._exec(method, parameters);
329
379
  if (validations.isFunction(config.parse)) {
330
380
  return config.parse(config, response);
@@ -566,6 +616,24 @@ class Entity {
566
616
  return value;
567
617
  }
568
618
 
619
+ _normalizePagesValue(value = Number.MAX_SAFE_INTEGER) {
620
+ value = parseInt(value);
621
+ if (isNaN(value) || value < 1) {
622
+ throw new e.ElectroError(e.ErrorCodes.InvalidPagesOption, "Query option 'pages' must be of type 'number' and greater than zero.");
623
+ }
624
+ return value;
625
+ }
626
+
627
+ _normalizeLimitValue(value) {
628
+ if (value !== undefined) {
629
+ value = parseInt(value);
630
+ if (isNaN(value) || value < 1) {
631
+ throw new e.ElectroError(e.ErrorCodes.InvalidLimitOption, "Query option 'limit' must be of type 'number' and greater than zero.");
632
+ }
633
+ }
634
+ return value;
635
+ }
636
+
569
637
  _deconstructKeys(index, keyType, key, backupFacets = {}) {
570
638
  if (typeof key !== "string" || key.length === 0) {
571
639
  return null;
@@ -693,6 +761,8 @@ class Entity {
693
761
  response: 'default',
694
762
  ignoreOwnership: false,
695
763
  _isPagination: false,
764
+ _isCollectionQuery: false,
765
+ pages: undefined,
696
766
  };
697
767
 
698
768
  config = options.reduce((config, option) => {
@@ -705,6 +775,14 @@ class Entity {
705
775
  config.params.ReturnValues = FormatToReturnValues[format];
706
776
  }
707
777
 
778
+ if (option.pages !== undefined) {
779
+ config.pages = option.pages;
780
+ }
781
+
782
+ if (option._isCollectionQuery === true) {
783
+ config._isCollectionQuery = true;
784
+ }
785
+
708
786
  if (option.includeKeys === true) {
709
787
  config.includeKeys = true;
710
788
  }
@@ -727,7 +805,8 @@ class Entity {
727
805
  config.unprocessed = UnprocessedTypes.raw;
728
806
  }
729
807
 
730
- if (!isNaN(option.limit)) {
808
+ if (option.limit !== undefined) {
809
+ config.limit = option.limit;
731
810
  config.params.Limit = option.limit;
732
811
  }
733
812
 
@@ -1806,54 +1885,105 @@ class Entity {
1806
1885
  return utilities.formatKeyCasing(key, casing);
1807
1886
  }
1808
1887
 
1809
- _findBestIndexKeyMatch(attributes) {
1810
- let candidates = this.model.facets.bySlot.map((val, i) => i);
1888
+ _findBestIndexKeyMatch(attributes = {}) {
1889
+ // an array of arrays, representing the order of pk and sk composites specified for each index, and then an
1890
+ // array with each access pattern occupying the same array index.
1811
1891
  let facets = this.model.facets.bySlot;
1812
- let match;
1813
- let keys = {};
1814
- for (let i = 0; i < facets.length; i++) {
1815
- let currentMatches = [];
1816
- let nextMatches = [];
1817
- for (let j = 0; j < candidates.length; j++) {
1818
- let slot = candidates[j];
1819
- if (!facets[i][slot]) {
1820
- continue;
1821
- }
1822
- let name = facets[i][slot].name;
1823
- let next = facets[i][slot].next;
1824
- let index = facets[i][slot].index;
1825
- let type = facets[i][slot].type;
1826
- let match = !!attributes[name];
1827
- let matchNext = !!attributes[next];
1828
- if (match) {
1829
- keys[index] = keys[index] || [];
1830
- keys[index].push({ name, type });
1831
- currentMatches.push(slot);
1832
- if (matchNext) {
1833
- nextMatches.push(slot);
1834
- }
1892
+ // a flat array containing the match results of each access pattern, in the same array index they occur within
1893
+ // bySlot above
1894
+ let matches = [];
1895
+ for (let f = 0; f < facets.length; f++) {
1896
+ const slots = facets[f] || [];
1897
+ for (let s = 0; s < slots.length; s++) {
1898
+ const accessPatternSlot = slots[s];
1899
+ matches[s] = matches[s] || {
1900
+ index: accessPatternSlot.index,
1901
+ allKeys: false,
1902
+ hasSk: false,
1903
+ count: 0,
1904
+ done: false,
1905
+ keys: []
1835
1906
  }
1836
- }
1837
- if (currentMatches.length) {
1838
- if (nextMatches.length) {
1839
- candidates = [...nextMatches];
1907
+ // already determined to be out of contention on prior iteration
1908
+ const indexOutOfContention = matches[s].done;
1909
+ // composite shorter than other indexes
1910
+ const lacksAttributeAtSlot = !accessPatternSlot;
1911
+ // attribute at this slot is not in the object provided
1912
+ const attributeNotProvided = accessPatternSlot && attributes[accessPatternSlot.name] === undefined;
1913
+ // if the next attribute is a sort key then all partition keys were provided
1914
+ const nextAttributeIsSortKey = accessPatternSlot && accessPatternSlot.next && facets[f+1][s].type === "sk";
1915
+ // if no keys are left then all attribute requirements were met (remember indexes don't require a sort key)
1916
+ const hasAllKeys = accessPatternSlot && !accessPatternSlot.next;
1917
+
1918
+ // no sense iterating on items we know to be "done"
1919
+ if (indexOutOfContention || lacksAttributeAtSlot || attributeNotProvided) {
1920
+ matches[s].done = true;
1840
1921
  continue;
1841
- } else {
1842
- match = facets[i][currentMatches[0]].index;
1843
- break;
1844
1922
  }
1845
- } else if (i === 0) {
1846
- break;
1847
- } else {
1848
- match = (candidates[0] !== undefined && facets[i][candidates[0]].index) || TableIndex;
1849
- break;
1923
+
1924
+ // if the next attribute is a sort key (and you reached this line) then you have fulfilled all the
1925
+ // partition key requirements for this index
1926
+ if (nextAttributeIsSortKey) {
1927
+ matches[s].hasSk = true;
1928
+ // if you reached this step and there are no more attributes, then you fulfilled the index
1929
+ } else if (hasAllKeys) {
1930
+ matches[s].allKeys = true;
1931
+ }
1932
+
1933
+ // number of successfully fulfilled attributes plays into the ranking heuristic
1934
+ matches[s].count++;
1935
+
1936
+ // note the names/types of fulfilled attributes
1937
+ matches[s].keys.push({
1938
+ name: accessPatternSlot.name,
1939
+ type: accessPatternSlot.type
1940
+ });
1850
1941
  }
1851
1942
  }
1852
- return {
1853
- keys: keys[match] || [],
1854
- index: match || TableIndex,
1855
- shouldScan: match === undefined
1856
- };
1943
+ // the highest count of matched attributes among all access patterns
1944
+ let max = 0;
1945
+ matches = matches
1946
+ // remove incomplete indexes
1947
+ .filter(match => match.hasSk || match.allKeys)
1948
+ // calculate max attribute match
1949
+ .map(match => {
1950
+ max = Math.max(max, match.count);
1951
+ return match;
1952
+ });
1953
+
1954
+ // matched contains the ranked attributes. The closer an element is to zero the "higher" the rank.
1955
+ const matched = [];
1956
+ for (let m = 0; m < matches.length; m++) {
1957
+ const match = matches[m];
1958
+ // a finished primary index is most ideal (could be a get)
1959
+ const primaryIndexIsFinished = match.index === "" && match.allKeys;
1960
+ // if there is a tie for matched index attributes, primary index should win
1961
+ const primaryIndexIsMostMatched = match.index === "" && match.count === max;
1962
+ // composite attributes are complete
1963
+ const indexRequirementsFulfilled = match.allKeys;
1964
+ // having the most matches is important
1965
+ const hasTheMostAttributeMatches = match.count === max;
1966
+ if (primaryIndexIsFinished) {
1967
+ matched[0] = match;
1968
+ } else if (primaryIndexIsMostMatched) {
1969
+ matched[1] = match;
1970
+ } else if (indexRequirementsFulfilled) {
1971
+ matched[2] = match;
1972
+ } else if (hasTheMostAttributeMatches) {
1973
+ matched[3] = match;
1974
+ }
1975
+ }
1976
+ // find the first non-undefined element (best ranked) -- if possible
1977
+ const match = matched.find(value => !!value);
1978
+ let keys = [];
1979
+ let index = "";
1980
+ let shouldScan = true;
1981
+ if (match) {
1982
+ keys = match.keys;
1983
+ index = match.index;
1984
+ shouldScan = false;
1985
+ }
1986
+ return { keys, index, shouldScan };
1857
1987
  }
1858
1988
 
1859
1989
  /* istanbul ignore next */
@@ -1890,7 +2020,7 @@ class Entity {
1890
2020
  type = "name";
1891
2021
  } else if (char === "}" && type === "name") {
1892
2022
  if (current.name.match(/^\s*$/)) {
1893
- throw new e.ElectroError(e.ErrorCodes.InvalidKeyCompositeAttributeTemplate, `Invalid key composite attribute template. Empty expression "\${${current.name}" provided. Expected attribute name.`);
2023
+ throw new e.ElectroError(e.ErrorCodes.InvalidKeyCompositeAttributeTemplate, `Invalid key composite attribute template. Empty expression "\${${current.name}}" provided. Expected attribute name.`);
1894
2024
  }
1895
2025
  attributes.push({name: current.name, label: current.label});
1896
2026
  current.name = "";
package/src/errors.js CHANGED
@@ -151,6 +151,18 @@ const ErrorCodes = {
151
151
  name: "InvalidConcurrencyOption",
152
152
  sym: ErrorCode
153
153
  },
154
+ InvalidPagesOption: {
155
+ code: 2005,
156
+ section: "invalid-pages-option",
157
+ name: "InvalidPagesOption",
158
+ sym: ErrorCode,
159
+ },
160
+ InvalidLimitOption: {
161
+ code: 2006,
162
+ section: "invalid-limit-option",
163
+ name: "InvalidLimitOption",
164
+ sym: ErrorCode,
165
+ },
154
166
  InvalidAttribute: {
155
167
  code: 3001,
156
168
  section: "invalid-attribute",
@@ -195,28 +207,112 @@ const ErrorCodes = {
195
207
  },
196
208
  };
197
209
 
210
+ function makeMessage(message, section) {
211
+ return `${message} - For more detail on this error reference: ${getHelpLink(section)}`
212
+ }
213
+
198
214
  class ElectroError extends Error {
199
- constructor(err, message) {
215
+ constructor(code, message) {
200
216
  super(message);
201
217
  let detail = ErrorCodes.UnknownError;
202
- if (err && err.sym === ErrorCode) {
203
- detail = err
218
+ if (code && code.sym === ErrorCode) {
219
+ detail = code
204
220
  }
205
- this.message = `${message} - For more detail on this error reference: ${getHelpLink(detail.section)}`;
206
-
221
+ this._message = message;
222
+ // this.message = `${message} - For more detail on this error reference: ${getHelpLink(detail.section)}`;
223
+ this.message = makeMessage(message, detail.section);
207
224
  if (Error.captureStackTrace) {
208
225
  Error.captureStackTrace(this, ElectroError);
209
226
  }
210
227
 
211
228
  this.name = 'ElectroError';
212
- this.ref = err;
229
+ this.ref = code;
213
230
  this.code = detail.code;
214
- this.date = new Date();
231
+ this.date = Date.now();
215
232
  this.isElectroError = true;
216
233
  }
217
234
  }
218
235
 
236
+ class ElectroValidationError extends ElectroError {
237
+ constructor(errors = []) {
238
+ const fields = [];
239
+ const messages = [];
240
+ for (let i = 0; i < errors.length; i++) {
241
+ const error = errors[i];
242
+ const message = error ? (error._message || error.message) : undefined;
243
+ messages.push(message);
244
+ if (error instanceof ElectroUserValidationError) {
245
+ fields.push({
246
+ field: error.field,
247
+ index: error.index,
248
+ reason: message,
249
+ cause: error.cause,
250
+ type: 'validation'
251
+ });
252
+ } else if (error instanceof ElectroAttributeValidationError) {
253
+ fields.push({
254
+ field: error.field,
255
+ index: error.index,
256
+ reason: message,
257
+ cause: error.cause || error, // error | undefined
258
+ type: 'validation'
259
+ });
260
+ } else if (message) {
261
+ fields.push({
262
+ field: '',
263
+ index: error.index,
264
+ reason: message,
265
+ cause: error !== undefined ? error.cause || error : undefined,
266
+ type: 'fatal'
267
+ });
268
+ }
269
+ }
270
+
271
+ const message = messages
272
+ .filter(message => typeof message === "string" && message.length)
273
+ .join(', ') || `Invalid value(s) provided`;
274
+
275
+ super(ErrorCodes.InvalidAttribute, message);
276
+ this.fields = fields;
277
+ this.name = "ElectroValidationError";
278
+ }
279
+ }
280
+
281
+ class ElectroUserValidationError extends ElectroError {
282
+ constructor(field, cause) {
283
+ let message;
284
+ let hasCause = false;
285
+ if (typeof cause === "string") {
286
+ message = cause;
287
+ } else if (cause !== undefined && typeof cause._message === "string" && cause._message.length) {
288
+ message = cause._message;
289
+ hasCause = true;
290
+ } else if (cause !== undefined && typeof cause.message === "string" && cause.message.length) {
291
+ message = cause.message;
292
+ hasCause = true;
293
+ } else {
294
+ message = "Invalid value provided";
295
+ }
296
+ super(ErrorCodes.InvalidAttribute, message);
297
+ this.field = field;
298
+ this.name = "ElectroUserValidationError";
299
+ if (hasCause) {
300
+ this.cause = cause;
301
+ }
302
+ }
303
+ }
304
+
305
+ class ElectroAttributeValidationError extends ElectroError {
306
+ constructor(field, reason) {
307
+ super(ErrorCodes.InvalidAttribute, reason);
308
+ this.field = field;
309
+ }
310
+ }
311
+
219
312
  module.exports = {
313
+ ErrorCodes,
220
314
  ElectroError,
221
- ErrorCodes
315
+ ElectroValidationError,
316
+ ElectroUserValidationError,
317
+ ElectroAttributeValidationError
222
318
  };