electrodb 1.4.3 → 1.4.7

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
@@ -88,4 +88,27 @@ All notable changes to this project will be documented in this file. Breaking ch
88
88
 
89
89
  ## [1.4.3] - 2021-10-03
90
90
  ### Fixed
91
- - ElectroDB would throw when an `undefined` property was passed to query. This has been changed to not throw if a partial query on that index can be accomplished with the data provided.
91
+ - ElectroDB would throw when an `undefined` property was passed to query. This has been changed to not throw if a partial query on that index can be accomplished with the data provided.
92
+
93
+ ## [1.4.4] - 2021-10-16
94
+ ### Added
95
+ - Updates did not include composite attributes involved in primary index. Though these values cannot be changed, they should be `set` on update method calls in case the update results in an item insert. [[read more]](./README.md#updates-to-composite-attributes)
96
+
97
+ ## [0.11.1] - 2021-10-17
98
+ ### Patched
99
+ - Updates did not include composite attributes involved in primary index. Though these values cannot be changed, they should be `set` on update method calls in case the update results in an item insert. [[read more]](./README.md#updates-to-composite-attributes)
100
+
101
+ ## [1.4.5] = 2021-10-17
102
+ ### Fixed
103
+ - Improved .npmignore to remove playground oriented files, and created official directory to keep playground in sync with library changes.
104
+
105
+ ## [1.4.6] = 2021-10-20
106
+ ### Added, Fixed
107
+ - Adding Entity identifiers to all update operations. When primary index composite attributes were added in 1.4.4, entities were written properly but did not include the identifiers. This resulted in entities being written but not being readable without the query option `ignoreOwnership` being used.
108
+
109
+ ## [1.4.7] = 2021-10-20
110
+ ### Changed
111
+ - Using `add()` update mutation now resolves to `ADD #prop :prop` update expression instead of a `SET #prop = #prop + :prop`
112
+
113
+ ### Fixed
114
+ - Fixed param naming conflict during updates, when map attribute shares a name with another (separate) attribute.
package/README.md CHANGED
@@ -176,6 +176,7 @@ tasks
176
176
  + [Put Record](#put-record)
177
177
  + [Batch Write Put Records](#batch-write-put-records)
178
178
  + [Update Record](#update-record)
179
+ - [Updates to Composite Attributes](#updates-to-composite-attributes)
179
180
  - [Update Method: Set](#update-method-set)
180
181
  - [Update Method: Remove](#update-method-remove)
181
182
  - [Update Method: Add](#update-method-add)
@@ -3129,6 +3130,99 @@ Update Method | Attribute Types
3129
3130
  [delete](#update-method-delete) | `any` `set` | `object`
3130
3131
  [data](#update-method-data) | `*` | `callback`
3131
3132
 
3133
+ #### Updates to Composite Attributes
3134
+
3135
+ ElectroDB adds some constraints to update calls to prevent the accidental loss of data. If an access pattern is defined with multiple composite attributes, then ElectroDB ensure the attributes cannot be updated individually. If an attribute involved in an index composite is updated, then the index key also must be updated, and if the whole key cannot be formed by the attributes supplied to the update, then it cannot create a composite key without overwriting the old data.
3136
+
3137
+ This example shows why a partial update to a composite key is prevented by ElectroDB:
3138
+
3139
+ ```json
3140
+ {
3141
+ "index": "my-gsi",
3142
+ "pk": {
3143
+ "field": "gsi1pk",
3144
+ "composite": ["attr1"]
3145
+ },
3146
+ "sk": {
3147
+ "field": "gsi1sk",
3148
+ "composite": ["attr2", "attr3"]
3149
+ }
3150
+ }
3151
+ ```
3152
+
3153
+ The above secondary index definition would generate the following index keys:
3154
+
3155
+ ```json
3156
+ {
3157
+ "gsi1pk": "$service#attr1_value1",
3158
+ "gsi1sk": "$entity_version#attr2_value2#attr3_value6"
3159
+ }
3160
+ ```
3161
+
3162
+ If a user attempts to update the attribute `attr2`, then ElectroDB has no way of knowing value of the attribute `attr3` or if forming the composite key without it would overwrite its value. The same problem exists if a user were to update `attr3`, ElectroDB cannot update the key without knowing each composite attribute's value.
3163
+
3164
+ In the event that a secondary index includes composite values from the table's primary index, ElectroDB will draw from the values supplied for the update key to address index gaps in the secondary index. For example:
3165
+
3166
+ For the defined indexes:
3167
+
3168
+ ```json
3169
+ {
3170
+ "accessPattern1": {
3171
+ "pk": {
3172
+ "field": "pk",
3173
+ "composite": ["attr1"]
3174
+ },
3175
+ "sk": {
3176
+ "field": "sk",
3177
+ "composite": ["attr2"]
3178
+ }
3179
+ },
3180
+ "accessPattern2": {
3181
+ "index": "my-gsi",
3182
+ "pk": {
3183
+ "field": "gsi1pk",
3184
+ "composite": ["attr3"]
3185
+ },
3186
+ "sk": {
3187
+ "field": "gsi1sk",
3188
+ "composite": ["attr2", "attr4"]
3189
+ }
3190
+ }
3191
+ }
3192
+ ```
3193
+
3194
+ A user could update `attr4` alone because ElectroDB is able to leverage the value for `attr2` from values supplied to the `update()` method:
3195
+
3196
+ ```typescript
3197
+ entity.update({ attr1: "value1", attr2: "value2" })
3198
+ .set({ attr4: "value4" })
3199
+ .go();
3200
+
3201
+ {
3202
+ "UpdateExpression": "SET #attr4 = :attr4_u0, #gsi1sk = :gsi1sk_u0, #attr1 = :attr1_u0, #attr2 = :attr2_u0",
3203
+ "ExpressionAttributeNames": {
3204
+ "#attr4": "attr4",
3205
+ "#gsi1sk": "gsi1sk",
3206
+ "#attr1": "attr1",
3207
+ "#attr2": "attr2"
3208
+ },
3209
+ "ExpressionAttributeValues": {
3210
+ ":attr4_u0": "value6",
3211
+ // This index was successfully built
3212
+ ":gsi1sk_u0": "$update-edgecases_1#attr2_value2#attr4_value6",
3213
+ ":attr1_u0": "value1",
3214
+ ":attr2_u0": "value2"
3215
+ },
3216
+ "TableName": "test_table",
3217
+ "Key": {
3218
+ "pk": "$service#attr1_value1",
3219
+ "sk": "$entity_version#attr2_value2"
3220
+ }
3221
+ }
3222
+ ```
3223
+
3224
+ > Note: Included in the update are all attributes from the table's primary index. These values are automatically included on all updates in the event an update results in an insert.
3225
+
3132
3226
  #### Update Method: Set
3133
3227
 
3134
3228
  The `set()` method will accept all attributes defined on the model. Provide a value to apply or replace onto the item.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrodb",
3
- "version": "1.4.3",
3
+ "version": "1.4.7",
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": {
@@ -11,7 +11,8 @@
11
11
  "test-types": "node ./node_modules/tsd/dist/cli.js",
12
12
  "coverage": "nyc npm run test-all && nyc report --reporter=text-lcov | coveralls",
13
13
  "coverage-coveralls-local": "nyc npm run test-all-local && nyc report --reporter=text-lcov | coveralls",
14
- "coverage-html-local": "nyc npm run test-all-local && nyc report --reporter=html"
14
+ "coverage-html-local": "nyc npm run test-all-local && nyc report --reporter=html",
15
+ "build:browser": "browserify playground/browser.js -o playground/bundle.js"
15
16
  },
16
17
  "repository": {
17
18
  "type": "git",
@@ -30,6 +31,7 @@
30
31
  "@types/node": "^15.6.0",
31
32
  "@types/uuid": "^8.3.0",
32
33
  "aws-sdk": "2.630.0",
34
+ "browserify": "^17.0.0",
33
35
  "chai": "4.2.0",
34
36
  "coveralls": "^3.1.0",
35
37
  "istanbul": "0.4.5",
package/src/entity.js CHANGED
@@ -1003,6 +1003,7 @@ class Entity {
1003
1003
  }
1004
1004
 
1005
1005
  _makeUpdateParams(update = {}, pk = {}, sk = {}) {
1006
+ let primaryIndexAttributes = {...pk, ...sk};
1006
1007
  let modifiedAttributeValues = {};
1007
1008
  let modifiedAttributeNames = {};
1008
1009
  for (const path of Object.keys(update.paths)) {
@@ -1038,6 +1039,7 @@ class Entity {
1038
1039
  const wasNotAlreadyModified = modifiedAttributeNames[indexKey] === undefined;
1039
1040
  if (isNotTablePK && isNotTableSK && wasNotAlreadyModified) {
1040
1041
  update.set(indexKey, updatedKeys[indexKey]);
1042
+
1041
1043
  }
1042
1044
  }
1043
1045
 
@@ -1050,6 +1052,27 @@ class Entity {
1050
1052
  }
1051
1053
  }
1052
1054
 
1055
+ // This loop adds the composite attributes to the Primary Index. This is important
1056
+ // in the case an update results in an "upsert". We want to add the Primary Index
1057
+ // composite attributes to the update so they will be included on the item when it
1058
+ // is created. It is done after all of the above because it is not a true "update"
1059
+ // so it should not be subject to the above "rules".
1060
+ for (const primaryIndexAttribute of Object.keys(primaryIndexAttributes)) {
1061
+ // isNotTablePK and isNotTableSK is important to check in case these properties
1062
+ // are not also the name of the index (you cannot modify the PK or SK of an item
1063
+ // after its creation)
1064
+ const attribute = this.model.schema.attributes[primaryIndexAttribute];
1065
+ const isNotTablePK = !!(attribute && attribute.field !== this.model.indexes[accessPattern].pk.field);
1066
+ const isNotTableSK = !!(attribute && attribute.field !== this.model.indexes[accessPattern].sk.field);
1067
+ const wasNotAlreadyModified = modifiedAttributeNames[primaryIndexAttribute] === undefined;
1068
+ if (isNotTablePK && isNotTableSK && wasNotAlreadyModified) {
1069
+ update.set(primaryIndexAttribute, primaryIndexAttributes[primaryIndexAttribute]);
1070
+ }
1071
+ }
1072
+
1073
+ update.set(this.identifiers.entity, this.getName());
1074
+ update.set(this.identifiers.version, this.getVersion());
1075
+
1053
1076
  return {
1054
1077
  UpdateExpression: update.build(),
1055
1078
  ExpressionAttributeNames: update.getNames(),
@@ -1429,7 +1452,7 @@ class Entity {
1429
1452
  { ...keyAttributes },
1430
1453
  );
1431
1454
  const removedKeyImpact = this._expectIndexFacets(
1432
- {...removed},
1455
+ { ...removed },
1433
1456
  {...keyAttributes}
1434
1457
  )
1435
1458
 
package/src/filters.js CHANGED
@@ -39,7 +39,7 @@ class FilterFactory {
39
39
  );
40
40
  }
41
41
  }
42
- let expression = template(attribute, prop, ...attrValues);
42
+ let expression = template({}, attribute, prop, ...attrValues);
43
43
  return expression.trim();
44
44
  };
45
45
  },
package/src/operations.js CHANGED
@@ -4,7 +4,7 @@ const v = require("./util");
4
4
 
5
5
  const deleteOperations = {
6
6
  canNest: false,
7
- template: function del(attr, path, value) {
7
+ template: function del(options, attr, path, value) {
8
8
  let operation = "";
9
9
  let expression = "";
10
10
  switch(attr.type) {
@@ -23,19 +23,19 @@ const deleteOperations = {
23
23
  const UpdateOperations = {
24
24
  name: {
25
25
  canNest: true,
26
- template: function name(attr, path) {
26
+ template: function name(options, attr, path) {
27
27
  return path;
28
28
  }
29
29
  },
30
30
  value: {
31
31
  canNest: true,
32
- template: function value(attr, path, value) {
32
+ template: function value(options, attr, path, value) {
33
33
  return value;
34
34
  }
35
35
  },
36
36
  append: {
37
37
  canNest: false,
38
- template: function append(attr, path, value) {
38
+ template: function append(options, attr, path, value) {
39
39
  let operation = "";
40
40
  let expression = "";
41
41
  switch(attr.type) {
@@ -52,7 +52,7 @@ const UpdateOperations = {
52
52
  },
53
53
  add: {
54
54
  canNest: false,
55
- template: function add(attr, path, value) {
55
+ template: function add(options, attr, path, value) {
56
56
  let operation = "";
57
57
  let expression = "";
58
58
  switch(attr.type) {
@@ -62,8 +62,13 @@ const UpdateOperations = {
62
62
  expression = `${path} ${value}`;
63
63
  break;
64
64
  case AttributeTypes.number:
65
- operation = ItemOperations.set;
66
- expression = `${path} = ${path} + ${value}`;
65
+ if (options.nestedValue) {
66
+ operation = ItemOperations.set;
67
+ expression = `${path} = ${path} + ${value}`;
68
+ } else {
69
+ operation = ItemOperations.add;
70
+ expression = `${path} ${value}`;
71
+ }
67
72
  break;
68
73
  default:
69
74
  throw new Error(`Invalid Update Attribute Operation: "ADD" Operation can only be performed on attributes with type "number", "set", or "any".`);
@@ -73,7 +78,7 @@ const UpdateOperations = {
73
78
  },
74
79
  subtract: {
75
80
  canNest: false,
76
- template: function subtract(attr, path, value) {
81
+ template: function subtract(options, attr, path, value) {
77
82
  let operation = "";
78
83
  let expression = "";
79
84
  switch(attr.type) {
@@ -91,7 +96,7 @@ const UpdateOperations = {
91
96
  },
92
97
  set: {
93
98
  canNest: false,
94
- template: function set(attr, path, value) {
99
+ template: function set(options, attr, path, value) {
95
100
  let operation = "";
96
101
  let expression = "";
97
102
  switch(attr.type) {
@@ -114,7 +119,7 @@ const UpdateOperations = {
114
119
  },
115
120
  remove: {
116
121
  canNest: false,
117
- template: function remove(attr, ...paths) {
122
+ template: function remove(options, attr, ...paths) {
118
123
  let operation = "";
119
124
  let expression = "";
120
125
  switch(attr.type) {
@@ -142,86 +147,86 @@ const UpdateOperations = {
142
147
 
143
148
  const FilterOperations = {
144
149
  ne: {
145
- template: function eq(attr, name, value) {
150
+ template: function eq(options, attr, name, value) {
146
151
  return `${name} <> ${value}`;
147
152
  },
148
153
  strict: false,
149
154
  },
150
155
  eq: {
151
- template: function eq(attr, name, value) {
156
+ template: function eq(options, attr, name, value) {
152
157
  return `${name} = ${value}`;
153
158
  },
154
159
  strict: false,
155
160
  },
156
161
  gt: {
157
- template: function gt(attr, name, value) {
162
+ template: function gt(options, attr, name, value) {
158
163
  return `${name} > ${value}`;
159
164
  },
160
165
  strict: false
161
166
  },
162
167
  lt: {
163
- template: function lt(attr, name, value) {
168
+ template: function lt(options, attr, name, value) {
164
169
  return `${name} < ${value}`;
165
170
  },
166
171
  strict: false
167
172
  },
168
173
  gte: {
169
- template: function gte(attr, name, value) {
174
+ template: function gte(options, attr, name, value) {
170
175
  return `${name} >= ${value}`;
171
176
  },
172
177
  strict: false
173
178
  },
174
179
  lte: {
175
- template: function lte(attr, name, value) {
180
+ template: function lte(options, attr, name, value) {
176
181
  return `${name} <= ${value}`;
177
182
  },
178
183
  strict: false
179
184
  },
180
185
  between: {
181
- template: function between(attr, name, value1, value2) {
186
+ template: function between(options, attr, name, value1, value2) {
182
187
  return `(${name} between ${value1} and ${value2})`;
183
188
  },
184
189
  strict: false
185
190
  },
186
191
  begins: {
187
- template: function begins(attr, name, value) {
192
+ template: function begins(options, attr, name, value) {
188
193
  return `begins_with(${name}, ${value})`;
189
194
  },
190
195
  strict: false
191
196
  },
192
197
  exists: {
193
- template: function exists(attr, name) {
198
+ template: function exists(options, attr, name) {
194
199
  return `attribute_exists(${name})`;
195
200
  },
196
201
  strict: false
197
202
  },
198
203
  notExists: {
199
- template: function notExists(attr, name) {
204
+ template: function notExists(options, attr, name) {
200
205
  return `attribute_not_exists(${name})`;
201
206
  },
202
207
  strict: false
203
208
  },
204
209
  contains: {
205
- template: function contains(attr, name, value) {
210
+ template: function contains(options, attr, name, value) {
206
211
  return `contains(${name}, ${value})`;
207
212
  },
208
213
  strict: false
209
214
  },
210
215
  notContains: {
211
- template: function notContains(attr, name, value) {
216
+ template: function notContains(options, attr, name, value) {
212
217
  return `not contains(${name}, ${value})`;
213
218
  },
214
219
  strict: false
215
220
  },
216
221
  value: {
217
- template: function(attr, name, value) {
222
+ template: function(options, attr, name, value) {
218
223
  return value;
219
224
  },
220
225
  strict: false,
221
226
  canNest: true,
222
227
  },
223
228
  name: {
224
- template: function(attr, name) {
229
+ template: function(options, attr, name) {
225
230
  return name;
226
231
  },
227
232
  strict: false,
@@ -230,7 +235,7 @@ const FilterOperations = {
230
235
  };
231
236
 
232
237
  class ExpressionState {
233
- constructor({prefix, singleOccurrence} = {}) {
238
+ constructor({prefix} = {}) {
234
239
  this.names = {};
235
240
  this.values = {};
236
241
  this.paths = {};
@@ -238,13 +243,9 @@ class ExpressionState {
238
243
  this.impacted = {};
239
244
  this.expression = "";
240
245
  this.prefix = prefix || "";
241
- this.singleOccurrence = singleOccurrence;
242
246
  }
243
247
 
244
248
  incrementName(name) {
245
- if (this.singleOccurrence) {
246
- return `${this.prefix}${0}`
247
- }
248
249
  if (this.counts[name] === undefined) {
249
250
  this.counts[name] = 0;
250
251
  }
@@ -372,12 +373,14 @@ class AttributeOperationProxy {
372
373
  if (property.__is_clause__ === AttributeProxySymbol) {
373
374
  const {paths, root, target} = property();
374
375
  const attributeValues = [];
376
+ let hasNestedValue = false;
375
377
  for (let value of values) {
376
378
  value = target.format(value);
377
379
  // template.length is to see if function takes value argument
378
- if (template.length > 2) {
380
+ if (template.length > 3) {
379
381
  if (seen.has(value)) {
380
382
  attributeValues.push(value);
383
+ hasNestedValue = true;
381
384
  } else {
382
385
  let attributeValueName = builder.setValue(target.name, value);
383
386
  builder.setPath(paths.json, {value, name: attributeValueName});
@@ -386,7 +389,11 @@ class AttributeOperationProxy {
386
389
  }
387
390
  }
388
391
 
389
- const formatted = template(target, paths.expression, ...attributeValues);
392
+ const options = {
393
+ nestedValue: hasNestedValue
394
+ }
395
+
396
+ const formatted = template(options, target, paths.expression, ...attributeValues);
390
397
  builder.setImpacted(operation, paths.json);
391
398
  if (canNest) {
392
399
  seen.add(paths.expression);
package/src/update.js CHANGED
@@ -3,7 +3,7 @@ const {ItemOperations, BuilderTypes} = require("./types");
3
3
 
4
4
  class UpdateExpression extends ExpressionState {
5
5
  constructor(props = {}) {
6
- super({...props, singleOccurrence: true});
6
+ super({...props});
7
7
  this.operations = {
8
8
  set: new Set(),
9
9
  remove: new Set(),
package/.idea/aws.xml DELETED
@@ -1,17 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="accountSettings">
4
- <option name="activeProfile" value="profile:default" />
5
- <option name="activeRegion" value="us-east-1" />
6
- <option name="recentlyUsedProfiles">
7
- <list>
8
- <option value="profile:default" />
9
- </list>
10
- </option>
11
- <option name="recentlyUsedRegions">
12
- <list>
13
- <option value="us-east-1" />
14
- </list>
15
- </option>
16
- </component>
17
- </project>
@@ -1,12 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="WEB_MODULE" version="4">
3
- <component name="NewModuleRootManager">
4
- <content url="file://$MODULE_DIR$">
5
- <excludeFolder url="file://$MODULE_DIR$/.tmp" />
6
- <excludeFolder url="file://$MODULE_DIR$/temp" />
7
- <excludeFolder url="file://$MODULE_DIR$/tmp" />
8
- </content>
9
- <orderEntry type="inheritedJdk" />
10
- <orderEntry type="sourceFolder" forTests="false" />
11
- </component>
12
- </module>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="GoogleJavaFormatSettings">
4
- <option name="enabled" value="false" />
5
- </component>
6
- </project>
package/.idea/misc.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="JavaScriptSettings">
4
- <option name="languageLevel" value="ES6" />
5
- </component>
6
- </project>
package/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/electrodb.iml" filepath="$PROJECT_DIR$/.idea/electrodb.iml" />
6
- </modules>
7
- </component>
8
- </project>
package/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
- </component>
6
- </project>
package/browser.js DELETED
@@ -1,53 +0,0 @@
1
- const ElectroDB = require("./index");
2
- window.Prism = window.Prism || {};
3
- const appDiv = document.getElementById('param-container');
4
-
5
- function printToScreen(val) {
6
- const innerHtml = appDiv.innerHTML;
7
- // if (window.Prism) {
8
- // console.log("fn", window.Prism.highlight, window.Prism.highlight.toString());
9
- // console.log("window.Prism.languages.json", Object.keys(window.Prism.languages), window.Prism.languages.JSON);
10
- // // appDiv.innerHTML = innerHtml + window.Prism.highlight(val, window.Prism.languages.json, 'json');
11
- // } else {
12
- // console.log("FUUUU", window, window.Prism);
13
- // }
14
- // appDiv.innerHTML = innerHtml + Prism.highlight(val, Prism.languages.json, 'json');
15
- appDiv.innerHTML = innerHtml + `<hr><pre><code class="language-json">${val}</code></pre>`;
16
- }
17
-
18
- function clearScreen() {
19
- appDiv.innerHTML = '';
20
- }
21
-
22
- class Entity extends ElectroDB.Entity {
23
- constructor(...params) {
24
- super(...params);
25
- this.client = {};
26
- }
27
- _queryParams(state, config) {
28
- const params = super._queryParams(state, config);
29
- printToScreen(JSON.stringify(params, null, 4));
30
- return params;
31
- }
32
-
33
- _params(state, config) {
34
- // @ts-ignore
35
- const params = super._params(state, config);
36
- printToScreen(JSON.stringify(params, null, 4));
37
- return params;
38
- }
39
-
40
- go(type, params) {
41
- printToScreen(JSON.stringify(params, null, 4));
42
- }
43
- }
44
-
45
- class Service extends ElectroDB.Service {}
46
-
47
-
48
- window.ElectroDB = {
49
- Entity,
50
- Service,
51
- printToScreen,
52
- clearScreen
53
- };