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 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: () => uuidv4(),
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 indendently undefined because either attribute could have triggered this callback
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 indendently undefined because either attribute could have triggered this callback.
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 indendently undefined because either attribute could have triggered this callback
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 indendently undefined because either attribute could have triggered this callback
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 genericly named index fields that are separate from your business attributes._
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 "JExpotic"
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
- colleciton: "assignments"
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
- colleciton: ["assignments"]
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 Feburary 2021
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.0",
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.1",
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.0",
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
- 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.
1890
1891
  let facets = this.model.facets.bySlot;
1891
- let match;
1892
- let keys = {};
1893
- for (let i = 0; i < facets.length; i++) {
1894
- let currentMatches = [];
1895
- let nextMatches = [];
1896
- for (let j = 0; j < candidates.length; j++) {
1897
- let slot = candidates[j];
1898
- if (!facets[i][slot]) {
1899
- continue;
1900
- }
1901
- let name = facets[i][slot].name;
1902
- let next = facets[i][slot].next;
1903
- let index = facets[i][slot].index;
1904
- let type = facets[i][slot].type;
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
- if (currentMatches.length) {
1917
- if (nextMatches.length) {
1918
- 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;
1919
1921
  continue;
1920
- } else {
1921
- match = facets[i][currentMatches[0]].index;
1922
- break;
1923
1922
  }
1924
- } else if (i === 0) {
1925
- break;
1926
- } else {
1927
- match = (candidates[0] !== undefined && facets[i][candidates[0]].index) || TableIndex;
1928
- 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
+ });
1929
1941
  }
1930
1942
  }
1931
- return {
1932
- keys: keys[match] || [],
1933
- index: match || TableIndex,
1934
- shouldScan: match === undefined
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 = "";