electrodb 1.4.8 → 1.6.2

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];
@@ -197,11 +198,7 @@ class Entity {
197
198
 
198
199
  create(attributes = {}) {
199
200
  let index = TableIndex;
200
- let options = {
201
- params: {
202
- ConditionExpression: this._makeItemDoesntExistConditions(index)
203
- }
204
- };
201
+ let options = {};
205
202
  return this._makeChain(index, this._clausesWithFilters, clauses.index, options).create(attributes);
206
203
  }
207
204
 
@@ -212,21 +209,13 @@ class Entity {
212
209
 
213
210
  patch(facets = {}) {
214
211
  let index = TableIndex;
215
- let options = {
216
- params: {
217
- ConditionExpression: this._makeItemExistsConditions(index)
218
- }
219
- };
212
+ let options = {};
220
213
  return this._makeChain(index, this._clausesWithFilters, clauses.index, options).patch(facets);
221
214
  }
222
215
 
223
216
  remove(facets = {}) {
224
217
  let index = TableIndex;
225
- let options = {
226
- params: {
227
- ConditionExpression: this._makeItemExistsConditions(index)
228
- }
229
- };
218
+ let options = {};
230
219
  return this._makeChain(index, this._clausesWithFilters, clauses.index, options).remove(facets);
231
220
  }
232
221
 
@@ -241,8 +230,10 @@ class Entity {
241
230
  return await this.executeBulkWrite(parameters, config);
242
231
  case MethodTypes.batchGet:
243
232
  return await this.executeBulkGet(parameters, config);
233
+ case MethodTypes.query:
234
+ return await this.executeQuery(parameters, config)
244
235
  default:
245
- return await this.executeQuery(method, parameters, config);
236
+ return await this.executeOperation(method, parameters, config);
246
237
  }
247
238
  } catch (err) {
248
239
  if (config.originalErr || stackTrace === undefined) {
@@ -324,7 +315,54 @@ class Entity {
324
315
  return [resultsAll, unprocessedAll];
325
316
  }
326
317
 
327
- async executeQuery(method, parameters, config) {
318
+ async executeQuery(parameters, config = {}) {
319
+ let results = config._isCollectionQuery
320
+ ? {}
321
+ : [];
322
+ let ExclusiveStartKey;
323
+ let pages = this._normalizePagesValue(config.pages);
324
+ let max = this._normalizeLimitValue(config.limit);
325
+ let iterations = 0;
326
+ let count = 0;
327
+ do {
328
+ let limit = max === undefined
329
+ ? parameters.Limit
330
+ : max - count;
331
+ let response = await this._exec("query", {ExclusiveStartKey, ...parameters, Limit: limit});
332
+
333
+ ExclusiveStartKey = response.LastEvaluatedKey;
334
+
335
+ if (validations.isFunction(config.parse)) {
336
+ response = config.parse(config, response);
337
+ } else {
338
+ response = this.formatResponse(response, parameters.IndexName, config);
339
+ }
340
+
341
+ if (config.raw || config._isPagination) {
342
+ return response;
343
+ } else if (config._isCollectionQuery) {
344
+ for (const entity in response) {
345
+ if (max) {
346
+ count += response[entity].length;
347
+ }
348
+ results[entity] = results[entity] || [];
349
+ results[entity] = [...results[entity], ...response[entity]];
350
+ }
351
+ } else if (Array.isArray(response)) {
352
+ if (max) {
353
+ count += response.length;
354
+ }
355
+ results = [...results, ...response];
356
+ } else {
357
+ return response;
358
+ }
359
+
360
+ iterations++;
361
+ } while(ExclusiveStartKey && iterations < pages && (max === undefined || count < max));
362
+ return results;
363
+ }
364
+
365
+ async executeOperation(method, parameters, config) {
328
366
  let response = await this._exec(method, parameters);
329
367
  if (validations.isFunction(config.parse)) {
330
368
  return config.parse(config, response);
@@ -566,6 +604,24 @@ class Entity {
566
604
  return value;
567
605
  }
568
606
 
607
+ _normalizePagesValue(value = Number.MAX_SAFE_INTEGER) {
608
+ value = parseInt(value);
609
+ if (isNaN(value) || value < 1) {
610
+ throw new e.ElectroError(e.ErrorCodes.InvalidPagesOption, "Query option 'pages' must be of type 'number' and greater than zero.");
611
+ }
612
+ return value;
613
+ }
614
+
615
+ _normalizeLimitValue(value) {
616
+ if (value !== undefined) {
617
+ value = parseInt(value);
618
+ if (isNaN(value) || value < 1) {
619
+ throw new e.ElectroError(e.ErrorCodes.InvalidLimitOption, "Query option 'limit' must be of type 'number' and greater than zero.");
620
+ }
621
+ }
622
+ return value;
623
+ }
624
+
569
625
  _deconstructKeys(index, keyType, key, backupFacets = {}) {
570
626
  if (typeof key !== "string" || key.length === 0) {
571
627
  return null;
@@ -693,6 +749,8 @@ class Entity {
693
749
  response: 'default',
694
750
  ignoreOwnership: false,
695
751
  _isPagination: false,
752
+ _isCollectionQuery: false,
753
+ pages: undefined,
696
754
  };
697
755
 
698
756
  config = options.reduce((config, option) => {
@@ -705,6 +763,14 @@ class Entity {
705
763
  config.params.ReturnValues = FormatToReturnValues[format];
706
764
  }
707
765
 
766
+ if (option.pages !== undefined) {
767
+ config.pages = option.pages;
768
+ }
769
+
770
+ if (option._isCollectionQuery === true) {
771
+ config._isCollectionQuery = true;
772
+ }
773
+
708
774
  if (option.includeKeys === true) {
709
775
  config.includeKeys = true;
710
776
  }
@@ -727,7 +793,8 @@ class Entity {
727
793
  config.unprocessed = UnprocessedTypes.raw;
728
794
  }
729
795
 
730
- if (!isNaN(option.limit)) {
796
+ if (option.limit !== undefined) {
797
+ config.limit = option.limit;
731
798
  config.params.Limit = option.limit;
732
799
  }
733
800
 
@@ -787,29 +854,18 @@ class Entity {
787
854
  return {parameters, config};
788
855
  }
789
856
 
790
- _makeItemDoesntExistConditions(index) {
791
- let hasSortKey = this.model.lookup.indexHasSortKeys[index];
792
- let accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[index];
857
+ _getPrimaryIndexFieldNames() {
858
+ let hasSortKey = this.model.lookup.indexHasSortKeys[TableIndex];
859
+ let accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[TableIndex];
793
860
  let pkField = this.model.indexes[accessPattern].pk.field;
794
- let filter = [`attribute_not_exists(${pkField})`];
861
+ let skField;
795
862
  if (hasSortKey) {
796
- let skField = this.model.indexes[accessPattern].sk.field;
797
- filter.push(`attribute_not_exists(${skField})`);
863
+ skField = this.model.indexes[accessPattern].sk.field;
798
864
  }
799
- return filter.join(" AND ");
800
- }
801
-
802
- _makeItemExistsConditions(index) {
803
- let hasSortKey = this.model.lookup.indexHasSortKeys[index];
804
- let accessPattern = this.model.translations.indexes.fromIndexToAccessPattern[index];
805
- let pkField = this.model.indexes[accessPattern].pk.field;
806
-
807
- let filter = [`attribute_exists(${pkField})`];
808
- if (hasSortKey) {
809
- let skField = this.model.indexes[accessPattern].sk.field;
810
- filter.push(`attribute_exists(${skField})`);
865
+ return {
866
+ pk: pkField,
867
+ sk: skField
811
868
  }
812
- return filter.join(" AND ");
813
869
  }
814
870
 
815
871
  _applyParameterExpressionTypes(params, filter) {
@@ -1806,54 +1862,105 @@ class Entity {
1806
1862
  return utilities.formatKeyCasing(key, casing);
1807
1863
  }
1808
1864
 
1809
- _findBestIndexKeyMatch(attributes) {
1810
- let candidates = this.model.facets.bySlot.map((val, i) => i);
1865
+ _findBestIndexKeyMatch(attributes = {}) {
1866
+ // an array of arrays, representing the order of pk and sk composites specified for each index, and then an
1867
+ // array with each access pattern occupying the same array index.
1811
1868
  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
- }
1869
+ // a flat array containing the match results of each access pattern, in the same array index they occur within
1870
+ // bySlot above
1871
+ let matches = [];
1872
+ for (let f = 0; f < facets.length; f++) {
1873
+ const slots = facets[f] || [];
1874
+ for (let s = 0; s < slots.length; s++) {
1875
+ const accessPatternSlot = slots[s];
1876
+ matches[s] = matches[s] || {
1877
+ index: accessPatternSlot.index,
1878
+ allKeys: false,
1879
+ hasSk: false,
1880
+ count: 0,
1881
+ done: false,
1882
+ keys: []
1835
1883
  }
1836
- }
1837
- if (currentMatches.length) {
1838
- if (nextMatches.length) {
1839
- candidates = [...nextMatches];
1884
+ // already determined to be out of contention on prior iteration
1885
+ const indexOutOfContention = matches[s].done;
1886
+ // composite shorter than other indexes
1887
+ const lacksAttributeAtSlot = !accessPatternSlot;
1888
+ // attribute at this slot is not in the object provided
1889
+ const attributeNotProvided = accessPatternSlot && attributes[accessPatternSlot.name] === undefined;
1890
+ // if the next attribute is a sort key then all partition keys were provided
1891
+ const nextAttributeIsSortKey = accessPatternSlot && accessPatternSlot.next && facets[f+1][s].type === "sk";
1892
+ // if no keys are left then all attribute requirements were met (remember indexes don't require a sort key)
1893
+ const hasAllKeys = accessPatternSlot && !accessPatternSlot.next;
1894
+
1895
+ // no sense iterating on items we know to be "done"
1896
+ if (indexOutOfContention || lacksAttributeAtSlot || attributeNotProvided) {
1897
+ matches[s].done = true;
1840
1898
  continue;
1841
- } else {
1842
- match = facets[i][currentMatches[0]].index;
1843
- break;
1844
1899
  }
1845
- } else if (i === 0) {
1846
- break;
1847
- } else {
1848
- match = (candidates[0] !== undefined && facets[i][candidates[0]].index) || TableIndex;
1849
- break;
1900
+
1901
+ // if the next attribute is a sort key (and you reached this line) then you have fulfilled all the
1902
+ // partition key requirements for this index
1903
+ if (nextAttributeIsSortKey) {
1904
+ matches[s].hasSk = true;
1905
+ // if you reached this step and there are no more attributes, then you fulfilled the index
1906
+ } else if (hasAllKeys) {
1907
+ matches[s].allKeys = true;
1908
+ }
1909
+
1910
+ // number of successfully fulfilled attributes plays into the ranking heuristic
1911
+ matches[s].count++;
1912
+
1913
+ // note the names/types of fulfilled attributes
1914
+ matches[s].keys.push({
1915
+ name: accessPatternSlot.name,
1916
+ type: accessPatternSlot.type
1917
+ });
1850
1918
  }
1851
1919
  }
1852
- return {
1853
- keys: keys[match] || [],
1854
- index: match || TableIndex,
1855
- shouldScan: match === undefined
1856
- };
1920
+ // the highest count of matched attributes among all access patterns
1921
+ let max = 0;
1922
+ matches = matches
1923
+ // remove incomplete indexes
1924
+ .filter(match => match.hasSk || match.allKeys)
1925
+ // calculate max attribute match
1926
+ .map(match => {
1927
+ max = Math.max(max, match.count);
1928
+ return match;
1929
+ });
1930
+
1931
+ // matched contains the ranked attributes. The closer an element is to zero the "higher" the rank.
1932
+ const matched = [];
1933
+ for (let m = 0; m < matches.length; m++) {
1934
+ const match = matches[m];
1935
+ // a finished primary index is most ideal (could be a get)
1936
+ const primaryIndexIsFinished = match.index === "" && match.allKeys;
1937
+ // if there is a tie for matched index attributes, primary index should win
1938
+ const primaryIndexIsMostMatched = match.index === "" && match.count === max;
1939
+ // composite attributes are complete
1940
+ const indexRequirementsFulfilled = match.allKeys;
1941
+ // having the most matches is important
1942
+ const hasTheMostAttributeMatches = match.count === max;
1943
+ if (primaryIndexIsFinished) {
1944
+ matched[0] = match;
1945
+ } else if (primaryIndexIsMostMatched) {
1946
+ matched[1] = match;
1947
+ } else if (indexRequirementsFulfilled) {
1948
+ matched[2] = match;
1949
+ } else if (hasTheMostAttributeMatches) {
1950
+ matched[3] = match;
1951
+ }
1952
+ }
1953
+ // find the first non-undefined element (best ranked) -- if possible
1954
+ const match = matched.find(value => !!value);
1955
+ let keys = [];
1956
+ let index = "";
1957
+ let shouldScan = true;
1958
+ if (match) {
1959
+ keys = match.keys;
1960
+ index = match.index;
1961
+ shouldScan = false;
1962
+ }
1963
+ return { keys, index, shouldScan };
1857
1964
  }
1858
1965
 
1859
1966
  /* istanbul ignore next */
@@ -1890,7 +1997,7 @@ class Entity {
1890
1997
  type = "name";
1891
1998
  } else if (char === "}" && type === "name") {
1892
1999
  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.`);
2000
+ throw new e.ElectroError(e.ErrorCodes.InvalidKeyCompositeAttributeTemplate, `Invalid key composite attribute template. Empty expression "\${${current.name}}" provided. Expected attribute name.`);
1894
2001
  }
1895
2002
  attributes.push({name: current.name, label: current.label});
1896
2003
  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
  };
package/src/operations.js CHANGED
@@ -453,4 +453,9 @@ class AttributeOperationProxy {
453
453
  }
454
454
  }
455
455
 
456
- module.exports = {UpdateOperations, FilterOperations, ExpressionState, AttributeOperationProxy};
456
+ const FilterOperationNames = Object.keys(FilterOperations).reduce((ops, name) => {
457
+ ops[name] = name;
458
+ return ops;
459
+ }, {});
460
+
461
+ module.exports = {UpdateOperations, FilterOperations, FilterOperationNames, ExpressionState, AttributeOperationProxy};