@ftschopp/dynatable-core 1.5.1 → 1.5.2

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 [1.5.2](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.5.1...@ftschopp/dynatable-core@1.5.2) (2026-05-10)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **core:** guard primary-key template fields and non-set ops on GSI templates in update builder ([#42](https://github.com/ftschopp/dynatable/issues/42)) ([4487ff2](https://github.com/ftschopp/dynatable/commit/4487ff22ad507b37aa0e2e279a602a4813afddba))
7
+
1
8
  ## @ftschopp/dynatable-core [1.5.1](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.5.0...@ftschopp/dynatable-core@1.5.1) (2026-05-10)
2
9
 
3
10
 
@@ -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;AAa7D;;;;;;;;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,GAClC,aAAa,CAAC,KAAK,CAAC,CAmXtB"}
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,GAClC,aAAa,CAAC,KAAK,CAAC,CAwbtB"}
@@ -13,6 +13,18 @@ function extractAttrName(action) {
13
13
  const m = action.expression.match(/^\s*#([A-Za-z0-9_]+)\s*=/);
14
14
  return m?.[1];
15
15
  }
16
+ /**
17
+ * Pull the attribute name from any update action (set / remove / add / delete).
18
+ * Each action emitted by this builder carries exactly one entry in `names`
19
+ * mapping `#<attr>` → `<attr>`, so the value side of that map is the source
20
+ * of truth regardless of the surrounding expression syntax.
21
+ */
22
+ function actionAttrName(action) {
23
+ if (!action.names)
24
+ return undefined;
25
+ const values = Object.values(action.names);
26
+ return values[0];
27
+ }
16
28
  /**
17
29
  * Creates an UpdateBuilder for an item key and table.
18
30
  *
@@ -120,6 +132,68 @@ function createUpdateBuilder(tableName, key, client, prevConditions = [], update
120
132
  // key vars + the .set() payload, we throw — recomputing from the
121
133
  // existing item would require an extra read the builder won't do.
122
134
  if (indexContext) {
135
+ // Collect the attribute name targeted by every update action,
136
+ // grouped by op. `setInputs` carries the .set() payload (already
137
+ // structured) — for remove/add/delete we read from the action's
138
+ // attribute-name map.
139
+ const setFields = Object.keys(setInputs);
140
+ const removeFields = updateActions.remove
141
+ .map(actionAttrName)
142
+ .filter((n) => !!n);
143
+ const addFields = updateActions.add
144
+ .map(actionAttrName)
145
+ .filter((n) => !!n);
146
+ const deleteFields = updateActions.delete
147
+ .map(actionAttrName)
148
+ .filter((n) => !!n);
149
+ // Guard 1: primary-key template fields are immutable in DynamoDB.
150
+ // The Key on the UpdateItem request fixes the row to operate on; if
151
+ // the user changes a field that participates in the PK/SK template,
152
+ // the row's PK doesn't move (DynamoDB doesn't allow that) but the
153
+ // attribute does — leaving an inconsistent row whose PK encodes the
154
+ // old value. Catch this on every op (.set / .remove / .add / .delete)
155
+ // before it leaves the process.
156
+ const primaryKeyTemplateVars = new Set();
157
+ for (const keyDef of Object.values(indexContext.model.key)) {
158
+ for (const v of (0, model_utils_1.extractTemplateVars)(keyDef.value)) {
159
+ primaryKeyTemplateVars.add(v);
160
+ }
161
+ }
162
+ const allTouched = [...setFields, ...removeFields, ...addFields, ...deleteFields];
163
+ const pkConflicts = [
164
+ ...new Set(allTouched.filter((f) => primaryKeyTemplateVars.has(f))),
165
+ ];
166
+ if (pkConflicts.length > 0) {
167
+ throw new Error(`Cannot update field(s) [${pkConflicts.join(', ')}] — they participate ` +
168
+ `in the primary key template, which is immutable in DynamoDB. To ` +
169
+ `"rename" a primary-key value, delete the old item and put a new one ` +
170
+ `(ideally inside a transactWrite for atomicity).`);
171
+ }
172
+ // Guard 2: .add() / .remove() / .delete() against a field used in a
173
+ // SECONDARY-index template. The recompute path needs an explicit new
174
+ // value to resolve the template — ADD increments without exposing
175
+ // the new value, REMOVE strips the field entirely, and DELETE
176
+ // mutates a Set without naming a scalar. Switching to .set(field,
177
+ // newValue) lets the recompute path handle it correctly.
178
+ if (indexContext.model.index) {
179
+ const indexTemplateVars = new Set();
180
+ for (const indexDef of Object.values(indexContext.model.index)) {
181
+ for (const v of (0, model_utils_1.extractTemplateVars)(indexDef.value)) {
182
+ indexTemplateVars.add(v);
183
+ }
184
+ }
185
+ const nonSetTouches = [...removeFields, ...addFields, ...deleteFields];
186
+ const gsiConflicts = [
187
+ ...new Set(nonSetTouches.filter((f) => indexTemplateVars.has(f))),
188
+ ];
189
+ if (gsiConflicts.length > 0) {
190
+ throw new Error(`Cannot use .add() / .remove() / .delete() on field(s) ` +
191
+ `[${gsiConflicts.join(', ')}] — they participate in a ` +
192
+ `secondary-index template, and the affected index key cannot be ` +
193
+ `recomputed without an explicit new value. Use .set(field, ` +
194
+ `newValue) instead so the index key is recomputed atomically.`);
195
+ }
196
+ }
123
197
  const { actions: idxActions, missing } = (0, model_utils_1.computeIndexUpdates)(indexContext.model, indexContext.keyVars, setInputs);
124
198
  if (missing.length > 0) {
125
199
  const details = missing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ftschopp/dynatable-core",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "Core library for DynamoDB single table design",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",