@ftschopp/dynatable-core 2.3.2 → 2.4.0

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,18 @@
1
+ # @ftschopp/dynatable-core [2.4.0](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@2.3.3...@ftschopp/dynatable-core@2.4.0) (2026-07-22)
2
+
3
+
4
+ ### Features
5
+
6
+ * update dependencies to latest across the monorepo ([#65](https://github.com/ftschopp/dynatable/issues/65)) ([43b9fa2](https://github.com/ftschopp/dynatable/commit/43b9fa23f46c9301ea2cd5a34487ecca1c1a45b2))
7
+
8
+ ## @ftschopp/dynatable-core [2.3.3](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@2.3.2...@ftschopp/dynatable-core@2.3.3) (2026-07-22)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **core:** clone array/object schema defaults so items don't share a reference ([#63](https://github.com/ftschopp/dynatable/issues/63)) ([dd62fe1](https://github.com/ftschopp/dynatable/commit/dd62fe1b90b0836f500ad4ade243bc0c2c300cb8))
14
+ * **core:** prevent placeholder collision between update .where() and .set() ([#61](https://github.com/ftschopp/dynatable/issues/61)) ([396eef3](https://github.com/ftschopp/dynatable/commit/396eef33b78ed966ffafb1f9669dfc789d93b40d))
15
+
1
16
  ## @ftschopp/dynatable-core [2.3.2](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@2.3.1...@ftschopp/dynatable-core@2.3.2) (2026-06-08)
2
17
 
3
18
 
@@ -2,8 +2,14 @@ import { OpBuilder } from './types';
2
2
  /**
3
3
  * Creates a scoped OpBuilder with its own counter for unique value placeholders.
4
4
  * Each builder instance maintains isolated state to prevent naming conflicts.
5
+ *
6
+ * @param startCounter - Initial value for the placeholder counter. Defaults to
7
+ * 0 for standalone use (query/scan/get, where conditions are the only source
8
+ * of value placeholders). The update builder passes its own `valueCounter`
9
+ * here so condition placeholders (`:status_0`) share one numbering space with
10
+ * the SET/ADD/DELETE action placeholders and can never collide with them.
5
11
  */
6
- export declare function createOpBuilder(): OpBuilder;
12
+ export declare function createOpBuilder(startCounter?: number): OpBuilder;
7
13
  /**
8
14
  * @deprecated Use createOpBuilder() instead to avoid global state.
9
15
  * This export is maintained for backward compatibility.
@@ -1 +1 @@
1
- {"version":3,"file":"operators.d.ts","sourceRoot":"","sources":["../../../src/builders/shared/operators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC;;;GAGG;AACH,wBAAgB,eAAe,IAAI,SAAS,CA6L3C;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,EAAE,SAA6B,CAAC"}
1
+ {"version":3,"file":"operators.d.ts","sourceRoot":"","sources":["../../../src/builders/shared/operators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,YAAY,SAAI,GAAG,SAAS,CA6L3D;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,EAAE,SAA6B,CAAC"}
@@ -5,9 +5,15 @@ exports.createOpBuilder = createOpBuilder;
5
5
  /**
6
6
  * Creates a scoped OpBuilder with its own counter for unique value placeholders.
7
7
  * Each builder instance maintains isolated state to prevent naming conflicts.
8
+ *
9
+ * @param startCounter - Initial value for the placeholder counter. Defaults to
10
+ * 0 for standalone use (query/scan/get, where conditions are the only source
11
+ * of value placeholders). The update builder passes its own `valueCounter`
12
+ * here so condition placeholders (`:status_0`) share one numbering space with
13
+ * the SET/ADD/DELETE action placeholders and can never collide with them.
8
14
  */
9
- function createOpBuilder() {
10
- let valueCounter = 0;
15
+ function createOpBuilder(startCounter = 0) {
16
+ let valueCounter = startCounter;
11
17
  /**
12
18
  * Generates a unique value placeholder name within this builder's scope
13
19
  */
@@ -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;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,CA6lBtB"}
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;AAkF7D;;;;;;;;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,CAsmBtB"}
@@ -25,6 +25,21 @@ function actionAttrName(action) {
25
25
  const values = Object.values(action.names);
26
26
  return values[0];
27
27
  }
28
+ /**
29
+ * Counts how many ExpressionAttributeValues placeholders a condition tree
30
+ * introduces. Each leaf operator advances the opBuilder's counter once per
31
+ * value it emits (`eq` → 1, `between` → 2, `in` → N, `exists`/`notExists` → 0);
32
+ * combinators (AND/OR/NOT) emit none of their own. The total therefore equals
33
+ * how far the shared placeholder counter advanced while building the tree —
34
+ * exactly how much the update builder must advance its own `valueCounter` so a
35
+ * later `.set()` on the same attribute never reuses a condition placeholder.
36
+ */
37
+ function countConditionValues(cond) {
38
+ if (cond.children && cond.children.length > 0) {
39
+ return cond.children.reduce((n, child) => n + countConditionValues(child), 0);
40
+ }
41
+ return cond.values ? Object.keys(cond.values).length : 0;
42
+ }
28
43
  /**
29
44
  * Reject `undefined` before it reaches DynamoDB. SET / ADD / DELETE /
30
45
  * setIfNotExists all encode the value into `ExpressionAttributeValues` — an
@@ -90,9 +105,18 @@ setIfNotExistsInputs = {}) {
90
105
  return { name: prop };
91
106
  },
92
107
  });
93
- const opBuilder = (0, shared_1.createOpBuilder)();
108
+ // Seed the condition's placeholder counter from THIS builder's
109
+ // valueCounter so condition values (`:status_0`) share one numbering
110
+ // space with the SET/ADD/DELETE action values that draw from the same
111
+ // counter. Then advance valueCounter past the values this condition
112
+ // consumed so a subsequent `.set()` on the same attribute gets a fresh
113
+ // placeholder. Without this, `.where(eq(status,'pending')).set('status',
114
+ // 'active')` emitted `:status_0` twice and the merge in dbParams()
115
+ // silently kept one value — writing the condition's (old) value.
116
+ const opBuilder = (0, shared_1.createOpBuilder)(valueCounter);
94
117
  const condition = fn(attrs, opBuilder);
95
- return createUpdateBuilder(tableName, key, client, [...conditions, condition], updateActions, returnMode, valueCounter, enableTimestamps, logger, indexContext, setInputs, consumedCapacity, setIfNotExistsInputs);
118
+ const consumed = countConditionValues(condition);
119
+ return createUpdateBuilder(tableName, key, client, [...conditions, condition], updateActions, returnMode, valueCounter + consumed, enableTimestamps, logger, indexContext, setInputs, consumedCapacity, setIfNotExistsInputs);
96
120
  },
97
121
  set(attrOrUpdates, value) {
98
122
  // When no value is provided and the first arg is a plain object,
@@ -1 +1 @@
1
- {"version":3,"file":"model-utils.d.ts","sourceRoot":"","sources":["../../src/utils/model-utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAiB,eAAe,EAAE,MAAM,cAAc,CAAC;AAG7F;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,MAAM,KAAG,MAAM,EAG5D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAG,MAoB7E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,eAAe,EACnD,OAAO,CAAC,EACR,OAAO,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC1B,OAAM,KAAK,GAAG,OAAO,GAAG,MAAc,KACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAQvB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CACnE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,eAAe,EAC3D,OAAO,CAAC,EACR,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAC3B,mBAwBF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,eAAe,EACzD,OAAO,CAAC,EACR,eAAe,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAClC,UAAU;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,KACrD,UAAU,CAAC,CAAC,CAiCd,CAAC;AAWF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,GACpC,SAAS,iBAAiB,GAAG,SAAS,KACrC,MAAM,EAUR,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,EACjC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,SAAS,EACzB,eAAc,SAAS,MAAM,EAA0B,KACtD,CAAC,GAAG,CAAC,EAAE,GAAG,SAqBZ,CAAC"}
1
+ {"version":3,"file":"model-utils.d.ts","sourceRoot":"","sources":["../../src/utils/model-utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAiB,eAAe,EAAE,MAAM,cAAc,CAAC;AAG7F;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,MAAM,KAAG,MAAM,EAG5D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAG,MAoB7E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,eAAe,EACnD,OAAO,CAAC,EACR,OAAO,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC1B,OAAM,KAAK,GAAG,OAAO,GAAG,MAAc,KACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAQvB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CACnE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,eAAe,EAC3D,OAAO,CAAC,EACR,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAC3B,mBAwBF,CAAC;AAgBF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,eAAe,EACzD,OAAO,CAAC,EACR,eAAe,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAClC,UAAU;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,KACrD,UAAU,CAAC,CAAC,CAiCd,CAAC;AAWF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,GACpC,SAAS,iBAAiB,GAAG,SAAS,KACrC,MAAM,EAUR,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,EACjC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,SAAS,EACzB,eAAc,SAAS,MAAM,EAA0B,KACtD,CAAC,GAAG,CAAC,EAAE,GAAG,SAqBZ,CAAC"}
@@ -78,6 +78,18 @@ const computeIndexUpdates = (model, keyVars, updates) => {
78
78
  return { actions, missing };
79
79
  };
80
80
  exports.computeIndexUpdates = computeIndexUpdates;
81
+ /**
82
+ * Produces a fresh copy of a schema default so two items created without that
83
+ * field never share the same array/object instance.
84
+ *
85
+ * A non-function `default` declared in the schema (e.g. `{ type: Array,
86
+ * default: [] }`) is a single value that lives on the schema object. Assigning
87
+ * it by reference means every item defaulted from it aliases that one instance
88
+ * — mutating one item's field (`item.history.push(...)`) then poisons the
89
+ * schema default and every later create. Primitives are immutable and returned
90
+ * as-is; arrays/objects/Dates are structurally cloned.
91
+ */
92
+ const cloneDefault = (value) => value !== null && typeof value === 'object' ? structuredClone(value) : value;
81
93
  /**
82
94
  * Applies default and generated values to validated input
83
95
  */
@@ -93,7 +105,7 @@ const applyPostDefaults = (model, validatedItem, options) => {
93
105
  result[key] = crypto.randomUUID();
94
106
  }
95
107
  else if (attr.default !== undefined) {
96
- result[key] = typeof attr.default === 'function' ? attr.default() : attr.default;
108
+ result[key] = typeof attr.default === 'function' ? attr.default() : cloneDefault(attr.default);
97
109
  }
98
110
  }
99
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ftschopp/dynatable-core",
3
- "version": "2.3.2",
3
+ "version": "2.4.0",
4
4
  "description": "Core library for DynamoDB single table design",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -31,23 +31,20 @@
31
31
  "@repo/eslint-config": "*",
32
32
  "@repo/typescript-config": "*",
33
33
  "@types/jest": "^30.0.0",
34
- "@types/node": "^25.0.3",
34
+ "@types/node": "^26.1.1",
35
35
  "aws-sdk-client-mock": "^4.1.0",
36
- "eslint": "^9.39.2",
37
- "jest": "^30.2.0",
38
- "ts-jest": "^29.4.6",
36
+ "eslint": "^10.7.0",
37
+ "jest": "^30.4.2",
38
+ "ts-jest": "^29.4.12",
39
39
  "ts-node": "^10.9.2",
40
- "tsc-alias": "^1.8.16",
40
+ "tsc-alias": "^1.9.1",
41
41
  "typescript": "5.9.3"
42
42
  },
43
43
  "dependencies": {
44
- "@aws-sdk/client-dynamodb": "^3.965.0",
45
- "@aws-sdk/lib-dynamodb": "^3.965.0",
46
- "@aws-sdk/util-dynamodb": "^3.965.0",
47
- "@types/ramda": "^0.31.1",
48
- "ramda": "^0.32.0",
44
+ "@aws-sdk/client-dynamodb": "^3.1092.0",
45
+ "@aws-sdk/lib-dynamodb": "^3.1092.0",
49
46
  "ulid": "^3.0.2",
50
- "zod": "^4.3.5"
47
+ "zod": "^4.4.3"
51
48
  },
52
49
  "engines": {
53
50
  "node": ">=22"