@ftschopp/dynatable-core 1.2.0 → 1.2.1
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,17 @@
|
|
|
1
|
+
## @ftschopp/dynatable-core [1.2.1](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.2.0...@ftschopp/dynatable-core@1.2.1) (2026-04-15)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* resolve GSI key attributes to KeyConditionExpression when using useIndex ([8b2ee35](https://github.com/ftschopp/dynatable/commit/8b2ee35ca56bae9a32c8f1fec764365e8496674d))
|
|
7
|
+
|
|
8
|
+
## @ftschopp/dynatable-core [1.2.1](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.2.0...@ftschopp/dynatable-core@1.2.1) (2026-04-15)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* resolve GSI key attributes to KeyConditionExpression when using useIndex ([8b2ee35](https://github.com/ftschopp/dynatable/commit/8b2ee35ca56bae9a32c8f1fec764365e8496674d))
|
|
14
|
+
|
|
1
15
|
# @ftschopp/dynatable-core [1.2.0](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.1.0...@ftschopp/dynatable-core@1.2.0) (2026-03-20)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -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;AAK1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,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;AAK1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAuU7D;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EACtC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,KAAK,CAAC,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,cAAc,GACtB,YAAY,CAAC,KAAK,CAAC,CA4BrB"}
|
|
@@ -6,11 +6,20 @@ const operators_1 = require("../shared/operators");
|
|
|
6
6
|
const conditions_1 = require("../shared/conditions");
|
|
7
7
|
const model_utils_1 = require("../../utils/model-utils");
|
|
8
8
|
/**
|
|
9
|
-
* Determines if a field is a key field (used in pk/sk templates)
|
|
9
|
+
* Determines if a field is a key field (used in pk/sk or index key templates)
|
|
10
10
|
*/
|
|
11
|
-
function isKeyField(fieldName, model) {
|
|
11
|
+
function isKeyField(fieldName, model, indexName) {
|
|
12
12
|
if (!model?.key)
|
|
13
13
|
return false;
|
|
14
|
+
// When an index is specified, also check model.index for key templates
|
|
15
|
+
if (indexName && model.index) {
|
|
16
|
+
for (const [, keyDef] of Object.entries(model.index)) {
|
|
17
|
+
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
18
|
+
if (templateVars.includes(fieldName)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
const keyEntries = Object.entries(model.key);
|
|
15
24
|
for (const [, keyDef] of keyEntries) {
|
|
16
25
|
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
@@ -24,13 +33,22 @@ function isKeyField(fieldName, model) {
|
|
|
24
33
|
* Maps a model attribute name to its corresponding DynamoDB key name (PK/SK)
|
|
25
34
|
* Also returns the key template for value transformation
|
|
26
35
|
*/
|
|
27
|
-
function getKeyNameForAttribute(fieldName, model) {
|
|
36
|
+
function getKeyNameForAttribute(fieldName, model, indexName) {
|
|
28
37
|
if (!model?.key)
|
|
29
38
|
return null;
|
|
39
|
+
// When an index is specified, check model.index first for key templates
|
|
40
|
+
if (indexName && model.index) {
|
|
41
|
+
for (const [keyName, keyDef] of Object.entries(model.index)) {
|
|
42
|
+
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
43
|
+
if (templateVars.includes(fieldName)) {
|
|
44
|
+
return { keyName, template: keyDef.value };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
30
48
|
for (const [keyName, keyDef] of Object.entries(model.key)) {
|
|
31
49
|
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
32
50
|
if (templateVars.includes(fieldName)) {
|
|
33
|
-
return { keyName, template: keyDef.value };
|
|
51
|
+
return { keyName, template: keyDef.value };
|
|
34
52
|
}
|
|
35
53
|
}
|
|
36
54
|
return null;
|
|
@@ -46,7 +64,7 @@ function applyKeyTemplate(template, fieldName, fieldValue) {
|
|
|
46
64
|
* Separates a condition tree into key conditions and filter conditions
|
|
47
65
|
* Key conditions are rewritten to use actual DynamoDB key names (PK/SK)
|
|
48
66
|
*/
|
|
49
|
-
function separateConditions(condition, model) {
|
|
67
|
+
function separateConditions(condition, model, indexName) {
|
|
50
68
|
const keyConditions = [];
|
|
51
69
|
const filterConditions = [];
|
|
52
70
|
function traverse(cond) {
|
|
@@ -76,7 +94,7 @@ function separateConditions(condition, model) {
|
|
|
76
94
|
const fieldMatch = cond.expression.match(/#(\w+)/);
|
|
77
95
|
if (fieldMatch && fieldMatch[1]) {
|
|
78
96
|
const fieldName = fieldMatch[1];
|
|
79
|
-
const keyInfo = getKeyNameForAttribute(fieldName, model);
|
|
97
|
+
const keyInfo = getKeyNameForAttribute(fieldName, model, indexName);
|
|
80
98
|
if (keyInfo) {
|
|
81
99
|
// This is a key field - rewrite the condition to use the actual key name
|
|
82
100
|
// and apply the template to the value
|
|
@@ -178,7 +196,7 @@ function createQueryExecutor(state) {
|
|
|
178
196
|
throw new Error('No where condition specified');
|
|
179
197
|
}
|
|
180
198
|
// Separate key conditions from filter conditions
|
|
181
|
-
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model);
|
|
199
|
+
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model, state.indexName);
|
|
182
200
|
// Build KeyConditionExpression (simple AND)
|
|
183
201
|
const keyExpr = buildSimpleAndExpression(keyConditions);
|
|
184
202
|
// Build FilterExpression (can be complex with OR, NOT, etc.)
|
package/package.json
CHANGED
|
@@ -228,3 +228,73 @@ describe('QueryBuilder - Pagination', () => {
|
|
|
228
228
|
});
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
|
+
|
|
232
|
+
describe('QueryBuilder - GSI key recognition', () => {
|
|
233
|
+
const client = new DynamoDBClient({});
|
|
234
|
+
const tableName = 'TestTable';
|
|
235
|
+
|
|
236
|
+
interface UserModel {
|
|
237
|
+
id: string;
|
|
238
|
+
email: string;
|
|
239
|
+
name?: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const modelWithIndex: ModelDefinition = {
|
|
243
|
+
key: {
|
|
244
|
+
PK: { type: String, value: 'USER#${id}' },
|
|
245
|
+
SK: { type: String, value: 'USER#${id}' },
|
|
246
|
+
},
|
|
247
|
+
index: {
|
|
248
|
+
GSI1PK: { type: String, value: 'EMAIL#${email}' },
|
|
249
|
+
GSI1SK: { type: String, value: 'EMAIL#${email}' },
|
|
250
|
+
},
|
|
251
|
+
attributes: {
|
|
252
|
+
id: { type: String, required: true },
|
|
253
|
+
email: { type: String, required: true },
|
|
254
|
+
name: { type: String },
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
test('should place GSI index attribute into KeyConditionExpression when useIndex is set', () => {
|
|
259
|
+
const params = createQueryBuilder<UserModel>(tableName, client, modelWithIndex)
|
|
260
|
+
.where((attr, op) => op.eq(attr.email, 'alice@example.com'))
|
|
261
|
+
.useIndex('GSI1')
|
|
262
|
+
.dbParams();
|
|
263
|
+
|
|
264
|
+
// email should be recognized as a key field via model.index and placed in KeyConditionExpression
|
|
265
|
+
expect(params.KeyConditionExpression).toBeDefined();
|
|
266
|
+
expect(params.KeyConditionExpression).toContain('#GSI1PK');
|
|
267
|
+
expect(params.ExpressionAttributeValues).toBeDefined();
|
|
268
|
+
// The value should have the template applied
|
|
269
|
+
expect(Object.values(params.ExpressionAttributeValues!)).toContain('EMAIL#alice@example.com');
|
|
270
|
+
// It should NOT be in FilterExpression
|
|
271
|
+
expect(params.FilterExpression).toBeUndefined();
|
|
272
|
+
expect(params.IndexName).toBe('GSI1');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('should place GSI attribute into FilterExpression when NO index is specified', () => {
|
|
276
|
+
const params = createQueryBuilder<UserModel>(tableName, client, modelWithIndex)
|
|
277
|
+
.where((attr, op) => op.and(op.eq(attr.id, '123'), op.eq(attr.email, 'alice@example.com')))
|
|
278
|
+
.dbParams();
|
|
279
|
+
|
|
280
|
+
// id maps to PK -> KeyConditionExpression
|
|
281
|
+
expect(params.KeyConditionExpression).toBeDefined();
|
|
282
|
+
expect(params.KeyConditionExpression).toContain('#PK');
|
|
283
|
+
// email is NOT a primary key field, so without useIndex it goes to filter
|
|
284
|
+
expect(params.FilterExpression).toBeDefined();
|
|
285
|
+
expect(params.FilterExpression).toContain('#email');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('should handle both primary key and GSI key fields together with useIndex', () => {
|
|
289
|
+
const params = createQueryBuilder<UserModel>(tableName, client, modelWithIndex)
|
|
290
|
+
.where((attr, op) => op.and(op.eq(attr.email, 'alice@example.com'), op.eq(attr.id, '123')))
|
|
291
|
+
.useIndex('GSI1')
|
|
292
|
+
.dbParams();
|
|
293
|
+
|
|
294
|
+
// Both should be key conditions: email via index, id via primary key
|
|
295
|
+
expect(params.KeyConditionExpression).toBeDefined();
|
|
296
|
+
expect(params.KeyConditionExpression).toContain('#GSI1PK');
|
|
297
|
+
expect(params.KeyConditionExpression).toContain('#PK');
|
|
298
|
+
expect(params.FilterExpression).toBeUndefined();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -27,11 +27,21 @@ type QueryState<Model> = {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Determines if a field is a key field (used in pk/sk templates)
|
|
30
|
+
* Determines if a field is a key field (used in pk/sk or index key templates)
|
|
31
31
|
*/
|
|
32
|
-
function isKeyField(fieldName: string, model?: ModelDefinition): boolean {
|
|
32
|
+
function isKeyField(fieldName: string, model?: ModelDefinition, indexName?: string): boolean {
|
|
33
33
|
if (!model?.key) return false;
|
|
34
34
|
|
|
35
|
+
// When an index is specified, also check model.index for key templates
|
|
36
|
+
if (indexName && model.index) {
|
|
37
|
+
for (const [, keyDef] of Object.entries(model.index)) {
|
|
38
|
+
const templateVars = extractTemplateVars(keyDef.value);
|
|
39
|
+
if (templateVars.includes(fieldName)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
const keyEntries = Object.entries(model.key);
|
|
36
46
|
for (const [, keyDef] of keyEntries) {
|
|
37
47
|
const templateVars = extractTemplateVars(keyDef.value);
|
|
@@ -48,14 +58,25 @@ function isKeyField(fieldName: string, model?: ModelDefinition): boolean {
|
|
|
48
58
|
*/
|
|
49
59
|
function getKeyNameForAttribute(
|
|
50
60
|
fieldName: string,
|
|
51
|
-
model?: ModelDefinition
|
|
61
|
+
model?: ModelDefinition,
|
|
62
|
+
indexName?: string
|
|
52
63
|
): { keyName: string; template: string } | null {
|
|
53
64
|
if (!model?.key) return null;
|
|
54
65
|
|
|
66
|
+
// When an index is specified, check model.index first for key templates
|
|
67
|
+
if (indexName && model.index) {
|
|
68
|
+
for (const [keyName, keyDef] of Object.entries(model.index)) {
|
|
69
|
+
const templateVars = extractTemplateVars(keyDef.value);
|
|
70
|
+
if (templateVars.includes(fieldName)) {
|
|
71
|
+
return { keyName, template: keyDef.value };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
for (const [keyName, keyDef] of Object.entries(model.key)) {
|
|
56
77
|
const templateVars = extractTemplateVars(keyDef.value);
|
|
57
78
|
if (templateVars.includes(fieldName)) {
|
|
58
|
-
return { keyName, template: keyDef.value };
|
|
79
|
+
return { keyName, template: keyDef.value };
|
|
59
80
|
}
|
|
60
81
|
}
|
|
61
82
|
return null;
|
|
@@ -75,7 +96,8 @@ function applyKeyTemplate(template: string, fieldName: string, fieldValue: any):
|
|
|
75
96
|
*/
|
|
76
97
|
function separateConditions(
|
|
77
98
|
condition: Condition,
|
|
78
|
-
model?: ModelDefinition
|
|
99
|
+
model?: ModelDefinition,
|
|
100
|
+
indexName?: string
|
|
79
101
|
): { keyConditions: Condition[]; filterConditions: Condition[] } {
|
|
80
102
|
const keyConditions: Condition[] = [];
|
|
81
103
|
const filterConditions: Condition[] = [];
|
|
@@ -106,7 +128,7 @@ function separateConditions(
|
|
|
106
128
|
const fieldMatch = cond.expression.match(/#(\w+)/);
|
|
107
129
|
if (fieldMatch && fieldMatch[1]) {
|
|
108
130
|
const fieldName = fieldMatch[1];
|
|
109
|
-
const keyInfo = getKeyNameForAttribute(fieldName, model);
|
|
131
|
+
const keyInfo = getKeyNameForAttribute(fieldName, model, indexName);
|
|
110
132
|
|
|
111
133
|
if (keyInfo) {
|
|
112
134
|
// This is a key field - rewrite the condition to use the actual key name
|
|
@@ -231,7 +253,7 @@ function createQueryExecutor<Model>(state: QueryState<Model>): QueryExecutor<Mod
|
|
|
231
253
|
}
|
|
232
254
|
|
|
233
255
|
// Separate key conditions from filter conditions
|
|
234
|
-
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model);
|
|
256
|
+
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model, state.indexName);
|
|
235
257
|
|
|
236
258
|
// Build KeyConditionExpression (simple AND)
|
|
237
259
|
const keyExpr = buildSimpleAndExpression(keyConditions);
|