@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;
|
|
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
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
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))
|