@ftschopp/dynatable-core 2.2.0 → 2.2.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
@@ -1,3 +1,10 @@
1
+ ## @ftschopp/dynatable-core [2.2.1](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@2.2.0...@ftschopp/dynatable-core@2.2.1) (2026-06-03)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **core:** reject undefined in update actions with actionable error ([#56](https://github.com/ftschopp/dynatable/issues/56)) ([5b8bee9](https://github.com/ftschopp/dynatable/commit/5b8bee9e9c041abdbff1b2b97d1eaba16730f257)), closes [#55](https://github.com/ftschopp/dynatable/issues/55)
7
+
1
8
  # @ftschopp/dynatable-core [2.2.0](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@2.1.0...@ftschopp/dynatable-core@2.2.0) (2026-06-02)
2
9
 
3
10
 
package/README.md CHANGED
@@ -265,6 +265,13 @@ await table.entities.User.update({ username: 'alice' })
265
265
  .where((attr, op) => op.gt(attr.followerCount, 0))
266
266
  .execute();
267
267
 
268
+ // Note: `.set()`, `.setIfNotExists()`, `.add()` and `.delete()` reject
269
+ // `undefined` values up front. DynamoDB can't encode `undefined` in
270
+ // `ExpressionAttributeValues`, so the builder throws with the offending
271
+ // keys instead of letting the request fail server-side. Use `.remove(attr)`
272
+ // to clear an attribute, or filter `undefined` out of your payload before
273
+ // calling `.set()`. `null` is allowed — it writes the DynamoDB `NULL` type.
274
+
268
275
  // DELETE - Remove item
269
276
  await table.entities.User.delete({ username: 'alice' })
270
277
  .returning('ALL_OLD')
@@ -1 +1 @@
1
- {"version":3,"file":"create-update-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/update/create-update-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,EAAgC,SAAS,EAA4B,MAAM,WAAW,CAAC;AAC9F,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAyB7D;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EACvC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,EACnB,MAAM,EAAE,cAAc,EACtB,cAAc,GAAE,SAAS,EAAO,EAChC,aAAa,GAAE;IACb,GAAG,EAAE,YAAY,EAAE,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,GAAG,EAAE,YAAY,EAAE,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;CACuB,EAChD,UAAU,GAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,aAAsB,EACnF,YAAY,SAAI,EAChB,gBAAgB,UAAQ,EACxB,MAAM,CAAC,EAAE,cAAc,EACvB,YAAY,CAAC,EAAE,YAAY,EAC3B,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EACnC,gBAAgB,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,EAO/C,oBAAoB,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC7C,aAAa,CAAC,KAAK,CAAC,CA4jBtB"}
1
+ {"version":3,"file":"create-update-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/update/create-update-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,EAAgC,SAAS,EAA4B,MAAM,WAAW,CAAC;AAC9F,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAkE7D;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EACvC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,EACnB,MAAM,EAAE,cAAc,EACtB,cAAc,GAAE,SAAS,EAAO,EAChC,aAAa,GAAE;IACb,GAAG,EAAE,YAAY,EAAE,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,GAAG,EAAE,YAAY,EAAE,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;CACuB,EAChD,UAAU,GAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,aAAsB,EACnF,YAAY,SAAI,EAChB,gBAAgB,UAAQ,EACxB,MAAM,CAAC,EAAE,cAAc,EACvB,YAAY,CAAC,EAAE,YAAY,EAC3B,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EACnC,gBAAgB,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,EAO/C,oBAAoB,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC7C,aAAa,CAAC,KAAK,CAAC,CAkkBtB"}
@@ -25,6 +25,37 @@ function actionAttrName(action) {
25
25
  const values = Object.values(action.names);
26
26
  return values[0];
27
27
  }
28
+ /**
29
+ * Reject `undefined` before it reaches DynamoDB. SET / ADD / DELETE /
30
+ * setIfNotExists all encode the value into `ExpressionAttributeValues` — an
31
+ * `undefined` there is either silently dropped by the marshaller (when
32
+ * `removeUndefinedValues` is on, which then fails server-side with an unused
33
+ * expression value error) or rejected outright. Catching it at the call site
34
+ * gives a useful error and a pointer to the right primitive.
35
+ *
36
+ * `null` is intentionally allowed — DynamoDB has a NULL attribute type and
37
+ * the caller may legitimately want to write it.
38
+ */
39
+ function assertNoUndefined(method, payload) {
40
+ const undefinedKeys = 'attr' in payload
41
+ ? payload.value === undefined
42
+ ? [payload.attr]
43
+ : []
44
+ : Object.entries(payload)
45
+ .filter(([, v]) => v === undefined)
46
+ .map(([k]) => k);
47
+ if (undefinedKeys.length === 0)
48
+ return;
49
+ const guidance = method === 'set'
50
+ ? `Use .remove(attr) to clear an attribute, or filter undefined out ` +
51
+ `before calling .set().`
52
+ : method === 'setIfNotExists'
53
+ ? `setIfNotExists requires a concrete value; omit the key or filter ` +
54
+ `it out.`
55
+ : `.${method}() requires a concrete value for each attribute.`;
56
+ throw new Error(`.${method}() received undefined for key(s) [${undefinedKeys.join(', ')}]. ` +
57
+ `DynamoDB cannot encode undefined in ExpressionAttributeValues. ${guidance}`);
58
+ }
28
59
  /**
29
60
  * Creates an UpdateBuilder for an item key and table.
30
61
  *
@@ -72,6 +103,7 @@ setIfNotExistsInputs = {}) {
72
103
  attrOrUpdates !== null) {
73
104
  // Multiple updates case
74
105
  const updates = attrOrUpdates;
106
+ assertNoUndefined('set', updates);
75
107
  const newActions = [];
76
108
  const newSetInputs = { ...setInputs };
77
109
  for (const [attr, val] of Object.entries(updates)) {
@@ -88,6 +120,7 @@ setIfNotExistsInputs = {}) {
88
120
  }
89
121
  // Single update case
90
122
  const attrName = normalizeAttr(attrOrUpdates);
123
+ assertNoUndefined('set', { attr: attrName, value });
91
124
  const valueName = getUniqueValueName(attrName);
92
125
  const action = {
93
126
  expression: `#${attrName} = :${valueName}`,
@@ -103,6 +136,7 @@ setIfNotExistsInputs = {}) {
103
136
  typeof attrOrUpdates === 'object' &&
104
137
  attrOrUpdates !== null) {
105
138
  const updates = attrOrUpdates;
139
+ assertNoUndefined('setIfNotExists', updates);
106
140
  const newActions = [];
107
141
  const newSetIfNotExistsInputs = { ...setIfNotExistsInputs };
108
142
  for (const [attr, val] of Object.entries(updates)) {
@@ -119,6 +153,7 @@ setIfNotExistsInputs = {}) {
119
153
  }
120
154
  // Single update case
121
155
  const attrName = normalizeAttr(attrOrUpdates);
156
+ assertNoUndefined('setIfNotExists', { attr: attrName, value });
122
157
  const valueName = getUniqueValueName(attrName);
123
158
  const action = {
124
159
  expression: `#${attrName} = if_not_exists(#${attrName}, :${valueName})`,
@@ -137,6 +172,7 @@ setIfNotExistsInputs = {}) {
137
172
  },
138
173
  add(attr, value) {
139
174
  const attrName = normalizeAttr(attr);
175
+ assertNoUndefined('add', { attr: attrName, value });
140
176
  const valueName = getUniqueValueName(attrName);
141
177
  const action = {
142
178
  expression: `#${attrName} :${valueName}`,
@@ -147,6 +183,7 @@ setIfNotExistsInputs = {}) {
147
183
  },
148
184
  delete(attr, value) {
149
185
  const attrName = normalizeAttr(attr);
186
+ assertNoUndefined('delete', { attr: attrName, value });
150
187
  const valueName = getUniqueValueName(attrName);
151
188
  const action = {
152
189
  expression: `#${attrName} :${valueName}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ftschopp/dynatable-core",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Core library for DynamoDB single table design",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",