@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;AAiT7D;;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"}
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 }; // Returns "PK" and "UP#${username}"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ftschopp/dynatable-core",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Core library for DynamoDB single table design",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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 }; // Returns "PK" and "UP#${username}"
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);