@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;
|
|
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
|