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/CHANGELOG.md +31 -4
- package/README.md +151 -61
- package/index.d.ts +41 -1
- package/package.json +6 -3
- package/src/entity.js +177 -47
- package/src/errors.js +104 -8
- package/src/schema.js +101 -50
- package/tsconfig.json +1 -1
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.
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
for (let
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
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
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
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(
|
|
215
|
+
constructor(code, message) {
|
|
200
216
|
super(message);
|
|
201
217
|
let detail = ErrorCodes.UnknownError;
|
|
202
|
-
if (
|
|
203
|
-
detail =
|
|
218
|
+
if (code && code.sym === ErrorCode) {
|
|
219
|
+
detail = code
|
|
204
220
|
}
|
|
205
|
-
this.
|
|
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 =
|
|
229
|
+
this.ref = code;
|
|
213
230
|
this.code = detail.code;
|
|
214
|
-
this.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
|
-
|
|
315
|
+
ElectroValidationError,
|
|
316
|
+
ElectroUserValidationError,
|
|
317
|
+
ElectroAttributeValidationError
|
|
222
318
|
};
|