electrodb 1.6.0 → 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 +5 -1
- package/README.md +24 -10
- package/package.json +6 -3
- package/src/entity.js +94 -43
package/CHANGELOG.md
CHANGED
|
@@ -134,4 +134,8 @@ All notable changes to this project will be documented in this file. Breaking ch
|
|
|
134
134
|
- As a byproduct of enhancing validation errors, the format of message text on a validation error has changed. This could be breaking if your app had a hardcoded dependency on the exact text of a thrown validation error.
|
|
135
135
|
|
|
136
136
|
### Fixed
|
|
137
|
-
- For Set attributes, the callback functions `get`, `set`, and `validate` are now consistently given an Array of values. These functions would sometimes (incorrectly) be called with a DynamoDB DocClient Set.
|
|
137
|
+
- For Set attributes, the callback functions `get`, `set`, and `validate` are now consistently given an Array of values. These functions would sometimes (incorrectly) be called with a DynamoDB DocClient Set.
|
|
138
|
+
|
|
139
|
+
## [1.6.1] - 2021-12-05
|
|
140
|
+
### Fixed
|
|
141
|
+
- In some cases the `find()` and `match()` methods would incorrectly select an index without a complete partition key. This would result in validation exceptions preventing the user from querying if an index definition and provided attribute object aligned improperly. This was fixed and a slightly more robust mechanism for ranking indexes was made.
|
package/README.md
CHANGED
|
@@ -569,7 +569,7 @@ const TasksModel = {
|
|
|
569
569
|
attributes: {
|
|
570
570
|
task: {
|
|
571
571
|
type: "string",
|
|
572
|
-
default: () =>
|
|
572
|
+
default: () => uuid(),
|
|
573
573
|
},
|
|
574
574
|
project: {
|
|
575
575
|
type: "string",
|
|
@@ -819,11 +819,11 @@ myAttr: {
|
|
|
819
819
|
watch: ["otherAttr"],
|
|
820
820
|
set: (myAttr, {otherAttr}) => {
|
|
821
821
|
// Whenever "myAttr" or "otherAttr" are updated from an `update` or `patch` operation, this callback will be fired.
|
|
822
|
-
// Note: myAttr or otherAttr could be
|
|
822
|
+
// Note: myAttr or otherAttr could be independently undefined because either attribute could have triggered this callback
|
|
823
823
|
},
|
|
824
824
|
get: (myAttr, {otherAttr}) => {
|
|
825
825
|
// Whenever "myAttr" or "otherAttr" are retrieved from a `query` or `get` operation, this callback will be fired.
|
|
826
|
-
// Note: myAttr or otherAttr could be
|
|
826
|
+
// Note: myAttr or otherAttr could be independently undefined because either attribute could have triggered this callback.
|
|
827
827
|
}
|
|
828
828
|
}
|
|
829
829
|
```
|
|
@@ -837,11 +837,11 @@ myAttr: {
|
|
|
837
837
|
watch: "*", // "watch all"
|
|
838
838
|
set: (myAttr, allAttributes) => {
|
|
839
839
|
// Whenever an `update` or `patch` operation is performed, this callback will be fired.
|
|
840
|
-
// Note: myAttr or the attributes under `allAttributes` could be
|
|
840
|
+
// Note: myAttr or the attributes under `allAttributes` could be independently undefined because either attribute could have triggered this callback
|
|
841
841
|
},
|
|
842
842
|
get: (myAttr, allAttributes) => {
|
|
843
843
|
// Whenever a `query` or `get` operation is performed, this callback will be fired.
|
|
844
|
-
// Note: myAttr or the attributes under `allAttributes` could be
|
|
844
|
+
// Note: myAttr or the attributes under `allAttributes` could be independently undefined because either attribute could have triggered this callback
|
|
845
845
|
}
|
|
846
846
|
}
|
|
847
847
|
```
|
|
@@ -1421,7 +1421,7 @@ As described in the above two sections ([Composite Attributes](#composite-attrib
|
|
|
1421
1421
|
|
|
1422
1422
|
It may be the case that an index field is also an attribute. For example, if a table was created with a Primary Index partition key of `accountId`, and that same field is used to store the `accountId` value used by the application. The following are a few examples of how to model that schema with ElectroDB:
|
|
1423
1423
|
|
|
1424
|
-
> _NOTE: If you have the unique opportunity to use ElectroDB with a new project, it is strongly recommended to use
|
|
1424
|
+
> _NOTE: If you have the unique opportunity to use ElectroDB with a new project, it is strongly recommended to use generically named index fields that are separate from your business attributes._
|
|
1425
1425
|
|
|
1426
1426
|
**Using `composite`**
|
|
1427
1427
|
|
|
@@ -1762,7 +1762,7 @@ let results = await TaskApp.collections
|
|
|
1762
1762
|
|
|
1763
1763
|
{
|
|
1764
1764
|
tasks: [...], // tasks for employeeId "JExotic"
|
|
1765
|
-
employees: [...] // employee record(s) with employeeId "
|
|
1765
|
+
employees: [...] // employee record(s) with employeeId "JExotic"
|
|
1766
1766
|
}
|
|
1767
1767
|
```
|
|
1768
1768
|
|
|
@@ -1777,7 +1777,7 @@ The following is an example of functionally identical collections, implemented a
|
|
|
1777
1777
|
**As a string (collection):**
|
|
1778
1778
|
```typescript
|
|
1779
1779
|
{
|
|
1780
|
-
|
|
1780
|
+
collection: "assignments"
|
|
1781
1781
|
pk: {
|
|
1782
1782
|
field: "pk",
|
|
1783
1783
|
composite: ["employeeId"]
|
|
@@ -1792,7 +1792,7 @@ The following is an example of functionally identical collections, implemented a
|
|
|
1792
1792
|
**As a string array (sub-collections):**
|
|
1793
1793
|
```typescript
|
|
1794
1794
|
{
|
|
1795
|
-
|
|
1795
|
+
collection: ["assignments"]
|
|
1796
1796
|
pk: {
|
|
1797
1797
|
field: "pk",
|
|
1798
1798
|
composite: ["employeeId"]
|
|
@@ -3720,6 +3720,8 @@ DynamoDB offers three methods to query records: `get`, `query`, and `scan`. In *
|
|
|
3720
3720
|
|
|
3721
3721
|
> _NOTE: The Find method is similar to the Match method with one exception: The attributes you supply directly to the `.find()` method will only be used to identify and fulfill your index access patterns. Any values supplied that do not contribute to a composite key will not be applied as query filters. Furthermore, if the values you provide do not resolve to an index access pattern, then a table scan will be performed. Use the `where()` chain method to further filter beyond keys, or use [Match](#match-records) for the convenience of automatic filtering based on the values given directly to that method._
|
|
3722
3722
|
|
|
3723
|
+
The Find method is useful when the index chosen does not matter or is not known. If your secondary indexes do not contain all attributes then this method might not be right for you. The mechanism that picks the best index for a given payload is subject to improvement and change without triggering a breaking change release version.
|
|
3724
|
+
|
|
3723
3725
|
```javascript
|
|
3724
3726
|
await StoreLocations.find({
|
|
3725
3727
|
mallId: "EastPointe",
|
|
@@ -3750,8 +3752,11 @@ await StoreLocations.find({
|
|
|
3750
3752
|
|
|
3751
3753
|
Match is a convenience method based off of ElectroDB's [find](#find-records) method. Similar to Find, Match does not require you to provide keys, but under the covers it will leverage the attributes provided to choose the best index to query on.
|
|
3752
3754
|
|
|
3755
|
+
> _NOTE: The Math method is useful when the index chosen does not matter or is not known. If your secondary indexes do not contain all attributes then this method might not be right for you. The mechanism that picks the best index for a given payload is subject to improvement and change without triggering a breaking change release version.
|
|
3756
|
+
|
|
3753
3757
|
Match differs from [Find](#find-records) in that it will also include all supplied values into a query filter.
|
|
3754
3758
|
|
|
3759
|
+
|
|
3755
3760
|
```javascript
|
|
3756
3761
|
await StoreLocations.find({
|
|
3757
3762
|
mallId: "EastPointe",
|
|
@@ -4228,7 +4233,7 @@ await StoreLocations.query.leases({storeId}).gte({leaseEndDate: "2020-03"}).go()
|
|
|
4228
4233
|
// Lease Agreements by StoreId before 2021
|
|
4229
4234
|
await StoreLocations.query.leases({storeId}).lt({leaseEndDate: "2021-01"}).go()
|
|
4230
4235
|
|
|
4231
|
-
// Lease Agreements by StoreId before
|
|
4236
|
+
// Lease Agreements by StoreId before February 2021
|
|
4232
4237
|
await StoreLocations.query.leases({storeId}).lte({leaseEndDate: "2021-02"}).go()
|
|
4233
4238
|
|
|
4234
4239
|
// Lease Agreements by StoreId between 2010 and 2020
|
|
@@ -4589,6 +4594,15 @@ Below is the type definition for an ElectroValidationError:
|
|
|
4589
4594
|
```typescript
|
|
4590
4595
|
ElectroValidationError<T extends Error = Error> extends ElectroError {
|
|
4591
4596
|
readonly name: "ElectroValidationError"
|
|
4597
|
+
readonly code: number;
|
|
4598
|
+
readonly date: number;
|
|
4599
|
+
readonly isElectroError: boolean;
|
|
4600
|
+
ref: {
|
|
4601
|
+
readonly code: number;
|
|
4602
|
+
readonly section: string;
|
|
4603
|
+
readonly name: string;
|
|
4604
|
+
readonly sym: unique symbol;
|
|
4605
|
+
}
|
|
4592
4606
|
readonly fields: ReadonlyArray<{
|
|
4593
4607
|
/**
|
|
4594
4608
|
* The json path to the attribute that had a validation error
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "electrodb",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://github.com/tywalch/electrodb#readme",
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@istanbuljs/nyc-config-typescript": "^1.0.
|
|
28
|
+
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
|
29
29
|
"@types/chai": "^4.2.12",
|
|
30
30
|
"@types/mocha": "^8.0.3",
|
|
31
31
|
"@types/node": "^15.6.0",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"aws-sdk": "2.630.0",
|
|
34
34
|
"browserify": "^17.0.0",
|
|
35
35
|
"chai": "4.2.0",
|
|
36
|
-
"coveralls": "^3.1.
|
|
36
|
+
"coveralls": "^3.1.1",
|
|
37
37
|
"istanbul": "0.4.5",
|
|
38
38
|
"jest": "25.4.0",
|
|
39
39
|
"mocha": "7.1.1",
|
|
@@ -50,6 +50,9 @@
|
|
|
50
50
|
"electrodb",
|
|
51
51
|
"dynamo",
|
|
52
52
|
"dynamodb",
|
|
53
|
+
"nosql",
|
|
54
|
+
"single table design",
|
|
55
|
+
"typescript",
|
|
53
56
|
"aws"
|
|
54
57
|
],
|
|
55
58
|
"tsd": {
|
package/src/entity.js
CHANGED
|
@@ -1885,54 +1885,105 @@ class Entity {
|
|
|
1885
1885
|
return utilities.formatKeyCasing(key, casing);
|
|
1886
1886
|
}
|
|
1887
1887
|
|
|
1888
|
-
_findBestIndexKeyMatch(attributes) {
|
|
1889
|
-
|
|
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.
|
|
1890
1891
|
let facets = this.model.facets.bySlot;
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
for (let
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
let match = !!attributes[name];
|
|
1906
|
-
let matchNext = !!attributes[next];
|
|
1907
|
-
if (match) {
|
|
1908
|
-
keys[index] = keys[index] || [];
|
|
1909
|
-
keys[index].push({ name, type });
|
|
1910
|
-
currentMatches.push(slot);
|
|
1911
|
-
if (matchNext) {
|
|
1912
|
-
nextMatches.push(slot);
|
|
1913
|
-
}
|
|
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: []
|
|
1914
1906
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
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;
|
|
1919
1921
|
continue;
|
|
1920
|
-
} else {
|
|
1921
|
-
match = facets[i][currentMatches[0]].index;
|
|
1922
|
-
break;
|
|
1923
1922
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
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
|
+
});
|
|
1929
1941
|
}
|
|
1930
1942
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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 };
|
|
1936
1987
|
}
|
|
1937
1988
|
|
|
1938
1989
|
/* istanbul ignore next */
|
|
@@ -1969,7 +2020,7 @@ class Entity {
|
|
|
1969
2020
|
type = "name";
|
|
1970
2021
|
} else if (char === "}" && type === "name") {
|
|
1971
2022
|
if (current.name.match(/^\s*$/)) {
|
|
1972
|
-
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.`);
|
|
1973
2024
|
}
|
|
1974
2025
|
attributes.push({name: current.name, label: current.label});
|
|
1975
2026
|
current.name = "";
|