@ftschopp/dynatable-core 2.0.0 → 2.1.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,10 @@
1
+ # @ftschopp/dynatable-core [2.1.0](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@2.0.0...@ftschopp/dynatable-core@2.1.0) (2026-06-01)
2
+
3
+
4
+ ### Features
5
+
6
+ * **core:** auto-inject equality conditions for literal-template keys in query builder ([5ecb781](https://github.com/ftschopp/dynatable/commit/5ecb78130a393a63eb255a49116177bff1caa496))
7
+
1
8
  # @ftschopp/dynatable-core [2.0.0](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.6.1...@ftschopp/dynatable-core@2.0.0) (2026-05-11)
2
9
 
3
10
 
@@ -1 +1 @@
1
- {"version":3,"file":"create-query-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/query/create-query-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAM1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,EAAiB,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAElE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAid7D;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EACtC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,KAAK,CAAC,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,cAAc,EACvB,UAAU,CAAC,EAAE,MAAM,GAClB,YAAY,CAAC,KAAK,CAAC,CA0BrB"}
1
+ {"version":3,"file":"create-query-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/query/create-query-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAM1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,EAAiB,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAElE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AA4hB7D;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EACtC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,KAAK,CAAC,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,cAAc,EACvB,UAAU,CAAC,EAAE,MAAM,GAClB,YAAY,CAAC,KAAK,CAAC,CA0BrB"}
@@ -85,6 +85,43 @@ function applyKeyTemplate(template, fieldName, fieldValue, keyOperator) {
85
85
  `with only "${fieldName}" provided — the template still contains unfilled ` +
86
86
  `variable(s). Use beginsWith for prefix queries on composite keys.`);
87
87
  }
88
+ /**
89
+ * Heuristic for identifying a hash (partition) key by name. Used only to
90
+ * decide whether a literal-template auto-injection covers DynamoDB's
91
+ * "Query needs a partition key" requirement — for actual key resolution
92
+ * we go through {@link keyBelongsToIndex} instead.
93
+ *
94
+ * Convention: primary is literally `PK`; index keys are `${indexName}PK`
95
+ * (conventional) or any `…PK`-suffixed name (non-conventional, opted in
96
+ * via `KeyDefinition.indexName`). The legitimate non-conventional case
97
+ * `BySpotifyId` / `lookupPK` is captured by the `endsWith('PK')` check.
98
+ */
99
+ function isHashKeyName(keyName, indexName) {
100
+ return indexName ? keyName.endsWith('PK') : keyName === 'PK';
101
+ }
102
+ /**
103
+ * Collects every key on the active index (or primary) whose template is
104
+ * fully literal — no `${...}` variables. These keys can't be referenced
105
+ * by attribute name in `where()` (there is no template var to bind to),
106
+ * so the query builder synthesizes the equality condition itself.
107
+ *
108
+ * Two real-world patterns this enables:
109
+ * 1. Entity-type partition: `GSI1PK: 'AIRPORT'` with a variable SK —
110
+ * "give me every Airport, filtered/range-scoped by SK".
111
+ * 2. Singleton sort-key marker: `SK: 'PROFILE'` paired with a variable
112
+ * PK like `'USER#${userId}'` — auto-injecting SK turns a wasteful
113
+ * "scan the whole user partition then filter" into a direct lookup.
114
+ */
115
+ function getLiteralKeys(model, indexName) {
116
+ if (!model)
117
+ return [];
118
+ const entries = indexName
119
+ ? Object.entries(model.index ?? {}).filter(([keyName, keyDef]) => keyBelongsToIndex(keyName, keyDef, indexName))
120
+ : Object.entries(model.key);
121
+ return entries
122
+ .filter(([, keyDef]) => (0, model_utils_1.extractTemplateVars)(keyDef.value).length === 0)
123
+ .map(([keyName, keyDef]) => ({ keyName, value: keyDef.value }));
124
+ }
88
125
  /**
89
126
  * Separates a condition tree into key conditions and filter conditions
90
127
  * Key conditions are rewritten to use actual DynamoDB key names (PK/SK)
@@ -236,12 +273,38 @@ function createQueryExecutor(state) {
236
273
  }
237
274
  // Separate key conditions from filter conditions
238
275
  const { keyConditions, filterConditions } = separateConditions(state.condition, state.model, state.indexName);
239
- // DynamoDB Query requires at least a partition-key condition. If
240
- // separation didn't pick anything as a key condition, the caller is
241
- // either filtering on non-key attributes (which means they want
242
- // scan()) or referencing the wrong attribute name. Fail loudly here
243
- // instead of letting the SDK reject the request at execute time.
244
- if (keyConditions.length === 0) {
276
+ // Auto-inject equality conditions for keys whose template is fully
277
+ // literal they have no `${vars}`, so the user can't reference
278
+ // them via `attr.*` in `where()`. Two real patterns this enables:
279
+ // 1. Entity-type GSI partition: `GSI1PK: 'AIRPORT'` without
280
+ // injection DynamoDB rejects the Query for missing the PK.
281
+ // 2. Sort-key marker: `SK: 'PROFILE'` paired with a variable PK
282
+ // like `'USER#${userId}'` — without injection the query reads
283
+ // every item under that PK and post-filters by `_type`.
284
+ // The `covered` check is defensive — a literal-template key can't
285
+ // appear in user conditions (no template var to bind) — but cheap
286
+ // protection against future schema shapes.
287
+ const literalKeys = getLiteralKeys(state.model, state.indexName);
288
+ const userKeyConditionCount = keyConditions.length;
289
+ for (const { keyName, value } of literalKeys) {
290
+ const covered = keyConditions.some((cond) => Object.values(cond.names ?? {}).includes(keyName));
291
+ if (covered)
292
+ continue;
293
+ keyConditions.push({
294
+ expression: `#${keyName} = :${keyName}_literal`,
295
+ names: { [`#${keyName}`]: keyName },
296
+ values: { [`:${keyName}_literal`]: value },
297
+ });
298
+ }
299
+ // DynamoDB Query requires at least a partition-key condition. The
300
+ // friendly error fires when:
301
+ // - the user's where() contributed zero key conditions, AND
302
+ // - no auto-injected literal covers the hash key.
303
+ // We can't blindly check `keyConditions.length === 0` after
304
+ // injection because a literal SK alone would silently slip past
305
+ // and produce a less helpful DynamoDB error.
306
+ const literalHashInjected = literalKeys.some(({ keyName }) => isHashKeyName(keyName, state.indexName));
307
+ if (userKeyConditionCount === 0 && !literalHashInjected) {
245
308
  const keyTemplates = state.indexName
246
309
  ? Object.entries(state.model?.index ?? {})
247
310
  .filter(([keyName, def]) => keyBelongsToIndex(keyName, def, state.indexName))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ftschopp/dynatable-core",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Core library for DynamoDB single table design",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",