@ftschopp/dynatable-core 1.2.4 → 1.3.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -3
  3. package/dist/builders/batch-get/create-batch-get-builder.d.ts.map +1 -1
  4. package/dist/builders/batch-get/create-batch-get-builder.js +4 -8
  5. package/dist/builders/batch-get/types.d.ts +2 -3
  6. package/dist/builders/batch-get/types.d.ts.map +1 -1
  7. package/dist/builders/query/create-query-builder.d.ts +1 -1
  8. package/dist/builders/query/create-query-builder.d.ts.map +1 -1
  9. package/dist/builders/query/create-query-builder.js +35 -10
  10. package/dist/builders/shared/operators.d.ts.map +1 -1
  11. package/dist/builders/shared/operators.js +8 -0
  12. package/dist/builders/shared/types.d.ts +1 -0
  13. package/dist/builders/shared/types.d.ts.map +1 -1
  14. package/dist/builders/update/create-update-builder.d.ts +8 -2
  15. package/dist/builders/update/create-update-builder.d.ts.map +1 -1
  16. package/dist/builders/update/create-update-builder.js +45 -16
  17. package/dist/builders/update/types.d.ts +13 -0
  18. package/dist/builders/update/types.d.ts.map +1 -1
  19. package/dist/core/types.d.ts +0 -1
  20. package/dist/core/types.d.ts.map +1 -1
  21. package/dist/entity/create-entity-api.d.ts.map +1 -1
  22. package/dist/entity/create-entity-api.js +8 -4
  23. package/dist/utils/model-utils.d.ts +28 -0
  24. package/dist/utils/model-utils.d.ts.map +1 -1
  25. package/dist/utils/model-utils.js +34 -1
  26. package/package.json +1 -1
  27. package/src/builders/batch-get/README.md +9 -64
  28. package/src/builders/batch-get/create-batch-get-builder.ts +5 -10
  29. package/src/builders/batch-get/types.ts +2 -3
  30. package/src/builders/query/create-query-builder.test.ts +204 -0
  31. package/src/builders/query/create-query-builder.ts +51 -10
  32. package/src/builders/shared/operators.ts +8 -0
  33. package/src/builders/shared/types.ts +1 -0
  34. package/src/builders/update/create-update-builder.test.ts +197 -0
  35. package/src/builders/update/create-update-builder.ts +72 -19
  36. package/src/builders/update/types.ts +14 -0
  37. package/src/core/types.test.ts +0 -2
  38. package/src/core/types.ts +0 -1
  39. package/src/entity/create-entity-api.ts +8 -4
  40. package/src/utils/model-utils.test.ts +91 -1
  41. package/src/utils/model-utils.ts +54 -0
  42. package/tests/integration/instagram-clone.integration.test.ts +23 -4
  43. package/tests/integration/pagination-timestamps.integration.test.ts +1 -2
  44. package/tests/integration/transactions.integration.test.ts +0 -1
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.stripInternalKeys = exports.applyPostDefaults = exports.resolveKeys = exports.resolveTemplate = exports.extractTemplateVars = void 0;
4
+ exports.stripInternalKeys = exports.applyPostDefaults = exports.computeIndexUpdates = exports.resolveKeys = exports.resolveTemplate = exports.extractTemplateVars = void 0;
5
5
  const ulid_1 = require("ulid");
6
6
  /**
7
7
  * Extracts all variable names from a template string
@@ -53,6 +53,39 @@ const resolveKeys = (model, input, type = 'key') => {
53
53
  return out;
54
54
  };
55
55
  exports.resolveKeys = resolveKeys;
56
+ /**
57
+ * Determines which secondary-index keys must be recomputed given a partial update.
58
+ *
59
+ * An index is "affected" iff at least one of its template variables appears in
60
+ * `updates`. For each affected index we attempt to resolve the template from the
61
+ * union of `keyVars` (template vars carried in the primary key, e.g. `id`) and
62
+ * `updates` (the user's `.set()` payload). If any template variable is missing
63
+ * from that union, the index is reported in `missing` instead of `actions` —
64
+ * recomputing it would require reading the existing item from DynamoDB, which
65
+ * the builder intentionally does not do.
66
+ */
67
+ const computeIndexUpdates = (model, keyVars, updates) => {
68
+ const actions = {};
69
+ const missing = [];
70
+ if (!model.index)
71
+ return { actions, missing };
72
+ const updateKeys = new Set(Object.keys(updates));
73
+ const combined = { ...keyVars, ...updates };
74
+ for (const [indexName, indexDef] of Object.entries(model.index)) {
75
+ const templateVars = (0, exports.extractTemplateVars)(indexDef.value);
76
+ const isAffected = templateVars.some((v) => updateKeys.has(v));
77
+ if (!isAffected)
78
+ continue;
79
+ const missingVars = templateVars.filter((v) => combined[v] === undefined);
80
+ if (missingVars.length > 0) {
81
+ missing.push({ index: indexName, template: indexDef.value, missing: missingVars });
82
+ continue;
83
+ }
84
+ actions[indexName] = (0, exports.resolveTemplate)(indexDef.value, combined);
85
+ }
86
+ return { actions, missing };
87
+ };
88
+ exports.computeIndexUpdates = computeIndexUpdates;
56
89
  /**
57
90
  * Applies default and generated values to validated input
58
91
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ftschopp/dynatable-core",
3
- "version": "1.2.4",
3
+ "version": "1.3.0",
4
4
  "description": "Core library for DynamoDB single table design",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,57 +1,29 @@
1
1
  # BatchGet Builder
2
2
 
3
- The `BatchGetBuilder` allows retrieving multiple items from one or more tables in a single request to DynamoDB.
3
+ The `BatchGetBuilder` retrieves multiple items by key in a single DynamoDB `BatchGetItem` request. It is consumed via the entity API (`table.entities.<Entity>.batchGet([...])`), and `execute()` returns a flat `Model[]`.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - Retrieve multiple items by their keys
8
8
  - Supports attribute projection (select only specific fields)
9
9
  - Supports consistent read
10
- - Can retrieve items from multiple tables in a single request
11
10
 
12
11
  ## Basic Usage
13
12
 
14
13
  ```typescript
15
- import { createBatchGetBuilder } from './builders/batch-get';
16
- import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
14
+ const users = await table.entities.User.batchGet([
15
+ { username: 'alice' },
16
+ { username: 'bob' },
17
+ { username: 'charlie' },
18
+ ]).execute();
17
19
 
18
- const client = new DynamoDBClient({});
19
-
20
- // Retrieve multiple users
21
- const result = await createBatchGetBuilder(
22
- {
23
- Users: {
24
- Keys: [
25
- { pk: 'USER#alice', sk: 'USER#alice' },
26
- { pk: 'USER#bob', sk: 'USER#bob' },
27
- { pk: 'USER#charlie', sk: 'USER#charlie' },
28
- ],
29
- },
30
- },
31
- client
32
- ).execute();
20
+ users.forEach((user) => console.log(user.username));
33
21
  ```
34
22
 
35
23
  ## With Attribute Projection
36
24
 
37
25
  ```typescript
38
- interface User {
39
- username: string;
40
- name: string;
41
- followerCount: number;
42
- }
43
-
44
- const result = await createBatchGetBuilder<User>(
45
- {
46
- Users: {
47
- Keys: [
48
- { pk: 'USER#alice', sk: 'USER#alice' },
49
- { pk: 'USER#bob', sk: 'USER#bob' },
50
- ],
51
- },
52
- },
53
- client
54
- )
26
+ const users = await table.entities.User.batchGet([{ username: 'alice' }, { username: 'bob' }])
55
27
  .select(['username', 'name', 'followerCount'])
56
28
  .execute();
57
29
  ```
@@ -59,38 +31,11 @@ const result = await createBatchGetBuilder<User>(
59
31
  ## With Consistent Read
60
32
 
61
33
  ```typescript
62
- const result = await createBatchGetBuilder(
63
- {
64
- Users: {
65
- Keys: [
66
- { pk: 'USER#alice', sk: 'USER#alice' },
67
- { pk: 'USER#bob', sk: 'USER#bob' },
68
- ],
69
- },
70
- },
71
- client
72
- )
34
+ const users = await table.entities.User.batchGet([{ username: 'alice' }, { username: 'bob' }])
73
35
  .consistentRead()
74
36
  .execute();
75
37
  ```
76
38
 
77
- ## Using the Table Class
78
-
79
- ```typescript
80
- // Retrieve multiple users using the Table class
81
- const result = await table
82
- .batchGet({
83
- InstagramClone: {
84
- Keys: [
85
- { pk: 'USER#alice', sk: 'USER#alice' },
86
- { pk: 'USER#bob', sk: 'USER#bob' },
87
- ],
88
- },
89
- })
90
- .select(['username', 'name'])
91
- .execute();
92
- ```
93
-
94
39
  ## DynamoDB Limitations
95
40
 
96
41
  - Maximum 100 items per request
@@ -84,23 +84,18 @@ export function createBatchGetBuilder<Model>(
84
84
  },
85
85
 
86
86
  /**
87
- * Execute the BatchGetItem command and return the items.
87
+ * Execute the BatchGetItem command and return the items as a flat array.
88
88
  */
89
- async execute(): Promise<Record<string, Model[]>> {
89
+ async execute(): Promise<Model[]> {
90
90
  const params = this.dbParams();
91
91
  const result: BatchGetCommandOutput = await client.send(new BatchGetCommand(params));
92
92
  logger?.log('BatchGetCommand', params, result);
93
93
 
94
- // Transform the response into a more usable format
95
- const responses: Record<string, Model[]> = {};
96
-
97
- if (result.Responses) {
98
- for (const [tableName, items] of Object.entries(result.Responses)) {
99
- responses[tableName] = items as unknown as Model[];
100
- }
94
+ if (!result.Responses) {
95
+ return [];
101
96
  }
102
97
 
103
- return responses;
98
+ return Object.values(result.Responses).flat() as unknown as Model[];
104
99
  },
105
100
  };
106
101
  }
@@ -22,8 +22,7 @@ export type BatchGetBuilder<Model> = {
22
22
  dbParams(): BatchGetItemCommandInput;
23
23
 
24
24
  /**
25
- * Execute the BatchGetItem command and return the items.
26
- * Returns an object mapping table names to arrays of items.
25
+ * Execute the BatchGetItem command and return the items as a flat array.
27
26
  */
28
- execute(): Promise<Record<string, Model[]>>;
27
+ execute(): Promise<Model[]>;
29
28
  };
@@ -298,3 +298,207 @@ describe('QueryBuilder - GSI key recognition', () => {
298
298
  expect(params.FilterExpression).toBeUndefined();
299
299
  });
300
300
  });
301
+
302
+ describe('QueryBuilder - multi-variable key templates', () => {
303
+ const client = new DynamoDBClient({});
304
+ const tableName = 'TestTable';
305
+
306
+ interface AirportResource {
307
+ id: string;
308
+ airport: string;
309
+ category: string;
310
+ code: string;
311
+ status: string;
312
+ }
313
+
314
+ const airportResourceModel: ModelDefinition = {
315
+ key: {
316
+ PK: { type: String, value: 'AIRPORT_RESOURCE#${id}' },
317
+ SK: { type: String, value: 'AIRPORT_RESOURCE#${id}' },
318
+ },
319
+ index: {
320
+ GSI1PK: { type: String, value: 'AIRPORT#${airport}' },
321
+ GSI1SK: { type: String, value: 'RES#${category}#${code}' },
322
+ },
323
+ attributes: {
324
+ id: { type: String, required: true },
325
+ airport: { type: String, required: true },
326
+ category: { type: String, required: true },
327
+ code: { type: String, required: true },
328
+ status: { type: String, required: true },
329
+ },
330
+ };
331
+
332
+ test('beginsWith on attribute that is part of multi-var template should truncate at the first unfilled variable', () => {
333
+ const params = createQueryBuilder<AirportResource>(tableName, client, airportResourceModel)
334
+ .where((attr, op) =>
335
+ op.and(op.eq(attr.airport, 'EZE'), op.beginsWith(attr.category, 'GPU'))
336
+ )
337
+ .useIndex('GSI1')
338
+ .dbParams();
339
+
340
+ expect(params.KeyConditionExpression).toBeDefined();
341
+ expect(params.KeyConditionExpression).toContain('#GSI1PK');
342
+ expect(params.KeyConditionExpression).toContain('begins_with(#GSI1SK');
343
+ expect(params.IndexName).toBe('GSI1');
344
+
345
+ const values = params.ExpressionAttributeValues ?? {};
346
+ expect(Object.values(values)).toContain('AIRPORT#EZE');
347
+ // Should be the prefix up to the next unfilled var, NOT 'RES#GPU#${code}'
348
+ expect(Object.values(values)).toContain('RES#GPU#');
349
+ expect(Object.values(values).every((v) => !String(v).includes('${'))).toBe(true);
350
+ });
351
+
352
+ test('eq on attribute with multi-var template should throw a clear error', () => {
353
+ expect(() =>
354
+ createQueryBuilder<AirportResource>(tableName, client, airportResourceModel)
355
+ .where((attr, op) => op.eq(attr.category, 'GPU'))
356
+ .useIndex('GSI1')
357
+ .dbParams()
358
+ ).toThrow(/template/i);
359
+ });
360
+
361
+ test('beginsWith on a single-var template still works (no truncation needed)', () => {
362
+ const params = createQueryBuilder<AirportResource>(tableName, client, airportResourceModel)
363
+ .where((attr, op) => op.beginsWith(attr.airport, 'EZ'))
364
+ .useIndex('GSI1')
365
+ .dbParams();
366
+
367
+ const values = params.ExpressionAttributeValues ?? {};
368
+ expect(Object.values(values)).toContain('AIRPORT#EZ');
369
+ });
370
+ });
371
+
372
+ describe('QueryBuilder - entity type auto filter', () => {
373
+ const client = new DynamoDBClient({});
374
+ const tableName = 'TestTable';
375
+
376
+ interface AirportPersonnel {
377
+ id: string;
378
+ airport: string;
379
+ role: string;
380
+ }
381
+
382
+ // Mirrors the reported scenario: PK is entity-specific, GSI1 is shared
383
+ // across multiple entities (Airport, AirportPersonnel, etc.)
384
+ const airportPersonnelModel: ModelDefinition = {
385
+ key: {
386
+ PK: { type: String, value: 'AIRPORT_PERSONNEL#${id}' },
387
+ SK: { type: String, value: 'AIRPORT_PERSONNEL#${id}' },
388
+ },
389
+ index: {
390
+ GSI1PK: { type: String, value: 'AIRPORT#${airport}' },
391
+ GSI1SK: { type: String, value: 'PERSONNEL#${role}' },
392
+ },
393
+ attributes: {
394
+ id: { type: String, required: true },
395
+ airport: { type: String, required: true },
396
+ role: { type: String, required: true },
397
+ },
398
+ };
399
+
400
+ test('does NOT add _type filter when entityType is not provided (back-compat)', () => {
401
+ const params = createQueryBuilder<AirportPersonnel>(
402
+ tableName,
403
+ client,
404
+ airportPersonnelModel
405
+ )
406
+ .where((attr, op) => op.eq(attr.airport, 'EZE'))
407
+ .useIndex('GSI1')
408
+ .dbParams();
409
+
410
+ expect(params.FilterExpression).toBeUndefined();
411
+ const names = params.ExpressionAttributeNames ?? {};
412
+ expect(names['#_type']).toBeUndefined();
413
+ });
414
+
415
+ test('adds _type filter when entityType is provided and there are no other filters', () => {
416
+ const params = createQueryBuilder<AirportPersonnel>(
417
+ tableName,
418
+ client,
419
+ airportPersonnelModel,
420
+ undefined,
421
+ 'AirportPersonnel'
422
+ )
423
+ .where((attr, op) => op.eq(attr.airport, 'EZE'))
424
+ .useIndex('GSI1')
425
+ .dbParams();
426
+
427
+ // Key cond goes to KeyConditionExpression, _type goes to FilterExpression
428
+ expect(params.KeyConditionExpression).toBeDefined();
429
+ expect(params.KeyConditionExpression).toContain('#GSI1PK');
430
+ expect(params.FilterExpression).toBe('#_type = :_type');
431
+ expect(params.ExpressionAttributeNames?.['#_type']).toBe('_type');
432
+ expect(params.ExpressionAttributeValues?.[':_type']).toBe('AirportPersonnel');
433
+ });
434
+
435
+ test('combines _type filter with a user-provided non-key filter via AND', () => {
436
+ const params = createQueryBuilder<AirportPersonnel & { status?: string }>(
437
+ tableName,
438
+ client,
439
+ airportPersonnelModel,
440
+ undefined,
441
+ 'AirportPersonnel'
442
+ )
443
+ .where((attr, op) => op.and(op.eq(attr.airport, 'EZE'), op.eq(attr.status, 'active')))
444
+ .useIndex('GSI1')
445
+ .dbParams();
446
+
447
+ expect(params.KeyConditionExpression).toContain('#GSI1PK');
448
+ expect(params.FilterExpression).toBeDefined();
449
+ expect(params.FilterExpression).toContain('#status');
450
+ expect(params.FilterExpression).toContain('#_type = :_type');
451
+ expect(params.ExpressionAttributeValues?.[':_type']).toBe('AirportPersonnel');
452
+ });
453
+
454
+ test('repro of reported bug: GSI1 query with conditional beginsWith still filters by _type', () => {
455
+ // The exact shape from the user report:
456
+ // .where((attr, op) =>
457
+ // role
458
+ // ? op.and(op.eq(attr.airport, airport), op.beginsWith(attr.role, role))
459
+ // : op.eq(attr.airport, airport)
460
+ // )
461
+ const airport = 'EZE';
462
+ const role = 'PILOT';
463
+
464
+ const params = createQueryBuilder<AirportPersonnel>(
465
+ tableName,
466
+ client,
467
+ airportPersonnelModel,
468
+ undefined,
469
+ 'AirportPersonnel'
470
+ )
471
+ .where((attr, op) =>
472
+ role
473
+ ? op.and(op.eq(attr.airport, airport), op.beginsWith(attr.role, role))
474
+ : op.eq(attr.airport, airport)
475
+ )
476
+ .useIndex('GSI1')
477
+ .dbParams();
478
+
479
+ // Both airport (GSI1PK) and role (GSI1SK) become key conditions
480
+ expect(params.KeyConditionExpression).toContain('#GSI1PK');
481
+ expect(params.KeyConditionExpression).toContain('begins_with(#GSI1SK');
482
+ // _type still gets enforced as a filter — this is the fix
483
+ expect(params.FilterExpression).toBe('#_type = :_type');
484
+ expect(params.ExpressionAttributeValues?.[':_type']).toBe('AirportPersonnel');
485
+ });
486
+
487
+ test('does not collide with user attribute placeholders', () => {
488
+ // Confirm the auto-injected names/values don't clash with user-supplied ones
489
+ const params = createQueryBuilder<AirportPersonnel & { status?: string }>(
490
+ tableName,
491
+ client,
492
+ airportPersonnelModel,
493
+ undefined,
494
+ 'AirportPersonnel'
495
+ )
496
+ .where((attr, op) => op.and(op.eq(attr.airport, 'EZE'), op.eq(attr.status, 'active')))
497
+ .useIndex('GSI1')
498
+ .dbParams();
499
+
500
+ expect(params.ExpressionAttributeNames?.['#_type']).toBe('_type');
501
+ expect(params.ExpressionAttributeNames?.['#status']).toBe('status');
502
+ expect(params.ExpressionAttributeValues?.[':_type']).toBe('AirportPersonnel');
503
+ });
504
+ });
@@ -24,6 +24,7 @@ type QueryState<Model> = {
24
24
  consistentReadEnabled?: boolean;
25
25
  exclusiveStartKey?: Record<string, any>;
26
26
  logger?: DynamoDBLogger;
27
+ entityType?: string;
27
28
  };
28
29
 
29
30
  /**
@@ -60,11 +61,32 @@ function getKeyNameForAttribute(
60
61
  }
61
62
 
62
63
  /**
63
- * Apply key template to transform attribute value to key value
64
- * e.g., applyKeyTemplate("UP#${username}", "username", "johndoe") -> "UP#johndoe"
64
+ * Apply key template to transform an attribute value to its key value.
65
+ *
66
+ * For multi-variable templates (e.g. "RES#${category}#${code}") we can only fill
67
+ * the variable matching `fieldName`. For `beginsWith` queries we truncate the
68
+ * result at the first remaining `${...}` placeholder so the prefix is usable.
69
+ * For other operators an exact match is required, so we throw a clear error.
65
70
  */
66
- function applyKeyTemplate(template: string, fieldName: string, fieldValue: any): string {
67
- return template.replace(`\${${fieldName}}`, fieldValue);
71
+ function applyKeyTemplate(
72
+ template: string,
73
+ fieldName: string,
74
+ fieldValue: any,
75
+ keyOperator?: string
76
+ ): string {
77
+ const substituted = template.replace(`\${${fieldName}}`, String(fieldValue));
78
+ const nextPlaceholder = substituted.indexOf('${');
79
+ if (nextPlaceholder === -1) return substituted;
80
+
81
+ if (keyOperator === 'beginsWith') {
82
+ return substituted.slice(0, nextPlaceholder);
83
+ }
84
+
85
+ throw new Error(
86
+ `Cannot use operator "${keyOperator ?? 'unknown'}" on key template "${template}" ` +
87
+ `with only "${fieldName}" provided — the template still contains unfilled ` +
88
+ `variable(s). Use beginsWith for prefix queries on composite keys.`
89
+ );
68
90
  }
69
91
 
70
92
  /**
@@ -116,7 +138,12 @@ function separateConditions(
116
138
  if (cond.values) {
117
139
  for (const [valuePlaceholder, value] of Object.entries(cond.values)) {
118
140
  // Apply the key template to transform the value
119
- const transformedValue = applyKeyTemplate(keyInfo.template, fieldName, value);
141
+ const transformedValue = applyKeyTemplate(
142
+ keyInfo.template,
143
+ fieldName,
144
+ value,
145
+ cond.keyOperator
146
+ );
120
147
  newValues[valuePlaceholder] = transformedValue;
121
148
  }
122
149
  }
@@ -239,17 +266,29 @@ function createQueryExecutor<Model>(state: QueryState<Model>): QueryExecutor<Mod
239
266
  // Build KeyConditionExpression (simple AND)
240
267
  const keyExpr = buildSimpleAndExpression(keyConditions);
241
268
 
269
+ // Auto-inject an entity-type filter so query() only returns items
270
+ // belonging to this entity (single-table designs share PKs across
271
+ // entities, especially on GSIs).
272
+ const allFilters = [...filterConditions];
273
+ if (state.entityType) {
274
+ allFilters.push({
275
+ expression: '#_type = :_type',
276
+ names: { '#_type': '_type' },
277
+ values: { ':_type': state.entityType },
278
+ });
279
+ }
280
+
242
281
  // Build FilterExpression (can be complex with OR, NOT, etc.)
243
282
  let filterExpr = { expression: '', names: {}, values: {} };
244
- if (filterConditions.length > 0) {
245
- if (filterConditions.length === 1 && filterConditions[0]) {
246
- filterExpr = buildExpression(filterConditions[0]);
283
+ if (allFilters.length > 0) {
284
+ if (allFilters.length === 1 && allFilters[0]) {
285
+ filterExpr = buildExpression(allFilters[0]);
247
286
  } else {
248
287
  // Multiple filter conditions - combine with AND
249
288
  filterExpr = buildExpression({
250
289
  expression: '',
251
290
  operator: 'AND',
252
- children: filterConditions,
291
+ children: allFilters,
253
292
  });
254
293
  }
255
294
  }
@@ -322,7 +361,8 @@ export function createQueryBuilder<Model>(
322
361
  tableName: string,
323
362
  client: DynamoDBClient,
324
363
  model?: ModelDefinition,
325
- logger?: DynamoDBLogger
364
+ logger?: DynamoDBLogger,
365
+ entityType?: string
326
366
  ): QueryBuilder<Model> {
327
367
  return {
328
368
  where(fn) {
@@ -346,6 +386,7 @@ export function createQueryBuilder<Model>(
346
386
  model,
347
387
  condition,
348
388
  logger,
389
+ entityType,
349
390
  };
350
391
 
351
392
  return createQueryExecutor(initialState);
@@ -21,6 +21,7 @@ export function createOpBuilder(): OpBuilder {
21
21
  expression: `#${attr.name} = :${valueName}`,
22
22
  names: { [`#${attr.name}`]: attr.name },
23
23
  values: { [`:${valueName}`]: value },
24
+ keyOperator: 'eq',
24
25
  };
25
26
  },
26
27
  ne: (attr, value) => {
@@ -29,6 +30,7 @@ export function createOpBuilder(): OpBuilder {
29
30
  expression: `#${attr.name} <> :${valueName}`,
30
31
  names: { [`#${attr.name}`]: attr.name },
31
32
  values: { [`:${valueName}`]: value },
33
+ keyOperator: 'ne',
32
34
  };
33
35
  },
34
36
  lt: (attr, value) => {
@@ -37,6 +39,7 @@ export function createOpBuilder(): OpBuilder {
37
39
  expression: `#${attr.name} < :${valueName}`,
38
40
  names: { [`#${attr.name}`]: attr.name },
39
41
  values: { [`:${valueName}`]: value },
42
+ keyOperator: 'lt',
40
43
  };
41
44
  },
42
45
  lte: (attr, value) => {
@@ -45,6 +48,7 @@ export function createOpBuilder(): OpBuilder {
45
48
  expression: `#${attr.name} <= :${valueName}`,
46
49
  names: { [`#${attr.name}`]: attr.name },
47
50
  values: { [`:${valueName}`]: value },
51
+ keyOperator: 'lte',
48
52
  };
49
53
  },
50
54
  gt: (attr, value) => {
@@ -53,6 +57,7 @@ export function createOpBuilder(): OpBuilder {
53
57
  expression: `#${attr.name} > :${valueName}`,
54
58
  names: { [`#${attr.name}`]: attr.name },
55
59
  values: { [`:${valueName}`]: value },
60
+ keyOperator: 'gt',
56
61
  };
57
62
  },
58
63
  gte: (attr, value) => {
@@ -61,6 +66,7 @@ export function createOpBuilder(): OpBuilder {
61
66
  expression: `#${attr.name} >= :${valueName}`,
62
67
  names: { [`#${attr.name}`]: attr.name },
63
68
  values: { [`:${valueName}`]: value },
69
+ keyOperator: 'gte',
64
70
  };
65
71
  },
66
72
  between: (attr, low, high) => {
@@ -73,6 +79,7 @@ export function createOpBuilder(): OpBuilder {
73
79
  [`:${lowName}`]: low,
74
80
  [`:${highName}`]: high,
75
81
  },
82
+ keyOperator: 'between',
76
83
  };
77
84
  },
78
85
  beginsWith: (attr, value) => {
@@ -81,6 +88,7 @@ export function createOpBuilder(): OpBuilder {
81
88
  expression: `begins_with(#${attr.name}, :${valueName})`,
82
89
  names: { [`#${attr.name}`]: attr.name },
83
90
  values: { [`:${valueName}`]: value },
91
+ keyOperator: 'beginsWith',
84
92
  };
85
93
  },
86
94
  contains: (attr, value) => {
@@ -11,6 +11,7 @@ export type Condition = {
11
11
  names?: Record<string, string>;
12
12
  values?: Record<string, any>;
13
13
  operator?: 'AND' | 'OR';
14
+ keyOperator?: 'eq' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte' | 'between' | 'beginsWith';
14
15
  isNegated?: boolean;
15
16
  children?: Condition[];
16
17
  };