@ftschopp/dynatable-core 1.4.2 → 1.4.4

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 (86) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/builders/query/create-query-builder.d.ts.map +1 -1
  3. package/dist/builders/query/create-query-builder.js +19 -2
  4. package/dist/builders/update/create-update-builder.d.ts.map +1 -1
  5. package/dist/builders/update/create-update-builder.js +25 -0
  6. package/dist/core/types.d.ts +5 -0
  7. package/dist/core/types.d.ts.map +1 -1
  8. package/dist/utils/model-utils.d.ts +9 -3
  9. package/dist/utils/model-utils.d.ts.map +1 -1
  10. package/dist/utils/model-utils.js +15 -7
  11. package/package.json +19 -2
  12. package/eslint.config.mjs +0 -4
  13. package/jest.config.js +0 -11
  14. package/src/builders/README.md +0 -339
  15. package/src/builders/batch-get/README.md +0 -43
  16. package/src/builders/batch-get/create-batch-get-builder.test.ts +0 -193
  17. package/src/builders/batch-get/create-batch-get-builder.ts +0 -108
  18. package/src/builders/batch-get/index.ts +0 -2
  19. package/src/builders/batch-get/types.ts +0 -28
  20. package/src/builders/batch-write/README.md +0 -204
  21. package/src/builders/batch-write/create-batch-write-builder.test.ts +0 -173
  22. package/src/builders/batch-write/create-batch-write-builder.ts +0 -49
  23. package/src/builders/batch-write/index.ts +0 -2
  24. package/src/builders/batch-write/types.ts +0 -33
  25. package/src/builders/delete/create-delete-builder.test.ts +0 -294
  26. package/src/builders/delete/create-delete-builder.ts +0 -100
  27. package/src/builders/delete/index.ts +0 -2
  28. package/src/builders/delete/types.ts +0 -17
  29. package/src/builders/get/create-get-builder.test.ts +0 -272
  30. package/src/builders/get/create-get-builder.ts +0 -117
  31. package/src/builders/get/index.ts +0 -2
  32. package/src/builders/get/types.ts +0 -39
  33. package/src/builders/index.ts +0 -14
  34. package/src/builders/put/create-put-builder.test.ts +0 -213
  35. package/src/builders/put/create-put-builder.ts +0 -160
  36. package/src/builders/put/index.ts +0 -2
  37. package/src/builders/put/types.ts +0 -24
  38. package/src/builders/query/create-query-builder.test.ts +0 -770
  39. package/src/builders/query/create-query-builder.ts +0 -455
  40. package/src/builders/query/index.ts +0 -2
  41. package/src/builders/query/types.ts +0 -109
  42. package/src/builders/scan/create-scan-builder.test.ts +0 -416
  43. package/src/builders/scan/create-scan-builder.ts +0 -265
  44. package/src/builders/scan/index.ts +0 -2
  45. package/src/builders/scan/types.ts +0 -88
  46. package/src/builders/shared/conditions.ts +0 -58
  47. package/src/builders/shared/index.ts +0 -4
  48. package/src/builders/shared/operators.test.ts +0 -270
  49. package/src/builders/shared/operators.ts +0 -208
  50. package/src/builders/shared/projection.ts +0 -28
  51. package/src/builders/shared/types.ts +0 -101
  52. package/src/builders/transact-get/README.md +0 -167
  53. package/src/builders/transact-get/create-transact-get-builder.test.ts +0 -239
  54. package/src/builders/transact-get/create-transact-get-builder.ts +0 -67
  55. package/src/builders/transact-get/index.ts +0 -2
  56. package/src/builders/transact-get/types.ts +0 -31
  57. package/src/builders/transact-write/README.md +0 -202
  58. package/src/builders/transact-write/create-transact-write-builder.test.ts +0 -288
  59. package/src/builders/transact-write/create-transact-write-builder.ts +0 -124
  60. package/src/builders/transact-write/index.ts +0 -2
  61. package/src/builders/transact-write/types.ts +0 -71
  62. package/src/builders/update/create-update-builder.test.ts +0 -573
  63. package/src/builders/update/create-update-builder.ts +0 -375
  64. package/src/builders/update/index.ts +0 -2
  65. package/src/builders/update/types.ts +0 -63
  66. package/src/core/types.test.ts +0 -641
  67. package/src/core/types.ts +0 -366
  68. package/src/entity/create-entity-api.ts +0 -224
  69. package/src/entity/index.ts +0 -19
  70. package/src/entity/middleware/factories.ts +0 -15
  71. package/src/entity/middleware/types.ts +0 -7
  72. package/src/entity/middleware/with-middleware.ts +0 -37
  73. package/src/entity/types.ts +0 -79
  74. package/src/entity/validation/key-validation.ts +0 -34
  75. package/src/index.ts +0 -23
  76. package/src/table.ts +0 -116
  77. package/src/utils/dynamodb-logger.test.ts +0 -246
  78. package/src/utils/dynamodb-logger.ts +0 -175
  79. package/src/utils/model-utils.test.ts +0 -452
  80. package/src/utils/model-utils.ts +0 -188
  81. package/src/utils/zod-utils.test.ts +0 -363
  82. package/src/utils/zod-utils.ts +0 -56
  83. package/tests/integration/instagram-clone.integration.test.ts +0 -1019
  84. package/tests/integration/pagination-timestamps.integration.test.ts +0 -374
  85. package/tests/integration/transactions.integration.test.ts +0 -528
  86. package/tsconfig.json +0 -12
@@ -1,573 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
- import { createUpdateBuilder } from './create-update-builder';
4
-
5
- describe('UpdateBuilder', () => {
6
- const client = new DynamoDBClient({});
7
- const tableName = 'TestTable';
8
-
9
- interface TestModel {
10
- pk: string;
11
- sk: string;
12
- name?: string;
13
- age?: number;
14
- score?: number;
15
- tags?: string[];
16
- followerCount?: number;
17
- }
18
-
19
- describe('SET operations', () => {
20
- test('should build params with single SET operation', () => {
21
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
22
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
23
- .set('name', 'John Doe')
24
- .dbParams();
25
-
26
- expect(params.TableName).toBe(tableName);
27
- expect(params.Key).toEqual(key);
28
- expect(params.UpdateExpression).toBe('SET #name = :name_0');
29
- expect(params.ExpressionAttributeNames).toEqual({ '#name': 'name' });
30
- expect(params.ExpressionAttributeValues).toEqual({
31
- ':name_0': 'John Doe',
32
- });
33
- });
34
-
35
- test('should build params with multiple SET operations', () => {
36
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
37
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
38
- .set('name', 'John Doe')
39
- .set('age', 30)
40
- .set('score', 100)
41
- .dbParams();
42
-
43
- expect(params.UpdateExpression).toBe('SET #name = :name_0, #age = :age_1, #score = :score_2');
44
- expect(params.ExpressionAttributeNames).toEqual({
45
- '#name': 'name',
46
- '#age': 'age',
47
- '#score': 'score',
48
- });
49
- expect(params.ExpressionAttributeValues).toEqual({
50
- ':name_0': 'John Doe',
51
- ':age_1': 30,
52
- ':score_2': 100,
53
- });
54
- });
55
-
56
- test('should build params with multiple SET operations using object', () => {
57
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
58
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
59
- .set({
60
- name: 'John Doe',
61
- age: 30,
62
- score: 100,
63
- })
64
- .dbParams();
65
-
66
- expect(params.UpdateExpression).toBe('SET #name = :name_0, #age = :age_1, #score = :score_2');
67
- expect(params.ExpressionAttributeNames).toEqual({
68
- '#name': 'name',
69
- '#age': 'age',
70
- '#score': 'score',
71
- });
72
- expect(params.ExpressionAttributeValues).toEqual({
73
- ':name_0': 'John Doe',
74
- ':age_1': 30,
75
- ':score_2': 100,
76
- });
77
- });
78
-
79
- test('should build params with single-property object whose key is "name"', () => {
80
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
81
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
82
- .set({ name: 'Ministro Pistarini' })
83
- .dbParams();
84
-
85
- expect(params.UpdateExpression).toBe('SET #name = :name_0');
86
- expect(params.ExpressionAttributeNames).toEqual({ '#name': 'name' });
87
- expect(params.ExpressionAttributeValues).toEqual({
88
- ':name_0': 'Ministro Pistarini',
89
- });
90
- });
91
-
92
- test('should build params with single-property object whose key is not "name"', () => {
93
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
94
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
95
- .set({ age: 30 })
96
- .dbParams();
97
-
98
- expect(params.UpdateExpression).toBe('SET #age = :age_0');
99
- expect(params.ExpressionAttributeNames).toEqual({ '#age': 'age' });
100
- expect(params.ExpressionAttributeValues).toEqual({ ':age_0': 30 });
101
- });
102
-
103
- test('should combine single and multiple SET operations', () => {
104
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
105
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
106
- .set('name', 'John Doe')
107
- .set({ age: 30, score: 100 })
108
- .dbParams();
109
-
110
- expect(params.UpdateExpression).toBe('SET #name = :name_0, #age = :age_1, #score = :score_2');
111
- expect(params.ExpressionAttributeNames).toEqual({
112
- '#name': 'name',
113
- '#age': 'age',
114
- '#score': 'score',
115
- });
116
- expect(params.ExpressionAttributeValues).toEqual({
117
- ':name_0': 'John Doe',
118
- ':age_1': 30,
119
- ':score_2': 100,
120
- });
121
- });
122
- });
123
-
124
- describe('REMOVE operations', () => {
125
- test('should build params with single REMOVE operation', () => {
126
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
127
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
128
- .remove('age')
129
- .dbParams();
130
-
131
- expect(params.UpdateExpression).toBe('REMOVE #age');
132
- expect(params.ExpressionAttributeNames).toEqual({ '#age': 'age' });
133
- expect(params.ExpressionAttributeValues).toBeUndefined();
134
- });
135
-
136
- test('should build params with multiple REMOVE operations', () => {
137
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
138
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
139
- .remove('age')
140
- .remove('score')
141
- .dbParams();
142
-
143
- expect(params.UpdateExpression).toBe('REMOVE #age, #score');
144
- expect(params.ExpressionAttributeNames).toEqual({
145
- '#age': 'age',
146
- '#score': 'score',
147
- });
148
- });
149
- });
150
-
151
- describe('ADD operations', () => {
152
- test('should build params with single ADD operation', () => {
153
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
154
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
155
- .add('followerCount', 1)
156
- .dbParams();
157
-
158
- expect(params.UpdateExpression).toBe('ADD #followerCount :followerCount_0');
159
- expect(params.ExpressionAttributeNames).toEqual({
160
- '#followerCount': 'followerCount',
161
- });
162
- expect(params.ExpressionAttributeValues).toEqual({
163
- ':followerCount_0': 1,
164
- });
165
- });
166
-
167
- test('should build params with multiple ADD operations', () => {
168
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
169
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
170
- .add('followerCount', 1)
171
- .add('score', 10)
172
- .dbParams();
173
-
174
- expect(params.UpdateExpression).toBe('ADD #followerCount :followerCount_0, #score :score_1');
175
- expect(params.ExpressionAttributeNames).toEqual({
176
- '#followerCount': 'followerCount',
177
- '#score': 'score',
178
- });
179
- expect(params.ExpressionAttributeValues).toEqual({
180
- ':followerCount_0': 1,
181
- ':score_1': 10,
182
- });
183
- });
184
- });
185
-
186
- describe('DELETE operations', () => {
187
- test('should build params with single DELETE operation', () => {
188
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
189
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
190
- .delete('tags', ['inactive'])
191
- .dbParams();
192
-
193
- expect(params.UpdateExpression).toBe('DELETE #tags :tags_0');
194
- expect(params.ExpressionAttributeNames).toEqual({ '#tags': 'tags' });
195
- expect(params.ExpressionAttributeValues).toEqual({
196
- ':tags_0': ['inactive'],
197
- });
198
- });
199
-
200
- test('should build params with multiple DELETE operations', () => {
201
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
202
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
203
- .delete('tags', ['inactive'])
204
- .delete('tags', ['old'])
205
- .dbParams();
206
-
207
- expect(params.UpdateExpression).toBe('DELETE #tags :tags_0, #tags :tags_1');
208
- expect(params.ExpressionAttributeNames).toEqual({ '#tags': 'tags' });
209
- expect(params.ExpressionAttributeValues).toEqual({
210
- ':tags_0': ['inactive'],
211
- ':tags_1': ['old'],
212
- });
213
- });
214
- });
215
-
216
- describe('Mixed operations', () => {
217
- test('should build params with SET and REMOVE', () => {
218
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
219
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
220
- .set('name', 'John Doe')
221
- .remove('age')
222
- .dbParams();
223
-
224
- expect(params.UpdateExpression).toBe('SET #name = :name_0 REMOVE #age');
225
- expect(params.ExpressionAttributeNames).toEqual({
226
- '#name': 'name',
227
- '#age': 'age',
228
- });
229
- expect(params.ExpressionAttributeValues).toEqual({
230
- ':name_0': 'John Doe',
231
- });
232
- });
233
-
234
- test('should build params with all operation types', () => {
235
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
236
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
237
- .set('name', 'John Doe')
238
- .remove('age')
239
- .add('followerCount', 1)
240
- .delete('tags', ['old'])
241
- .dbParams();
242
-
243
- expect(params.UpdateExpression).toBe(
244
- 'SET #name = :name_0 REMOVE #age ADD #followerCount :followerCount_1 DELETE #tags :tags_2'
245
- );
246
- expect(params.ExpressionAttributeNames).toEqual({
247
- '#name': 'name',
248
- '#age': 'age',
249
- '#followerCount': 'followerCount',
250
- '#tags': 'tags',
251
- });
252
- expect(params.ExpressionAttributeValues).toEqual({
253
- ':name_0': 'John Doe',
254
- ':followerCount_1': 1,
255
- ':tags_2': ['old'],
256
- });
257
- });
258
- });
259
-
260
- describe('Condition expressions', () => {
261
- test('should build params with where condition', () => {
262
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
263
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
264
- .set('name', 'John Doe')
265
- .where((attr, op) => op.eq(attr.age, 25))
266
- .dbParams();
267
-
268
- expect(params.UpdateExpression).toBe('SET #name = :name_0');
269
- expect(params.ConditionExpression).toMatch(/#age = :age_\d+/);
270
- expect(params.ExpressionAttributeNames).toEqual({
271
- '#name': 'name',
272
- '#age': 'age',
273
- });
274
- expect(Object.values(params.ExpressionAttributeValues || {})).toContain('John Doe');
275
- expect(Object.values(params.ExpressionAttributeValues || {})).toContain(25);
276
- });
277
-
278
- test('should build params with multiple where conditions using AND', () => {
279
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
280
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
281
- .set('name', 'John Doe')
282
- .where((attr, op) => op.and(op.gt(attr.age, 18), op.lt(attr.age, 65)))
283
- .dbParams();
284
-
285
- expect(params.ConditionExpression).toMatch(/\(#age > :age_\d+\) AND \(#age < :age_\d+\)/);
286
- expect(params.ExpressionAttributeNames).toEqual({
287
- '#name': 'name',
288
- '#age': 'age',
289
- });
290
- expect(Object.values(params.ExpressionAttributeValues || {})).toContain(18);
291
- expect(Object.values(params.ExpressionAttributeValues || {})).toContain(65);
292
- });
293
-
294
- test('should build params with OR conditions', () => {
295
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
296
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
297
- .set('name', 'John Doe')
298
- .where((attr, op) => op.or(op.eq(attr.age, 25), op.eq(attr.age, 30)))
299
- .dbParams();
300
-
301
- expect(params.ConditionExpression).toMatch(/\(#age = :age_\d+\) OR \(#age = :age_\d+\)/);
302
- expect(Object.values(params.ExpressionAttributeValues || {})).toContain(25);
303
- expect(Object.values(params.ExpressionAttributeValues || {})).toContain(30);
304
- });
305
- });
306
-
307
- describe('ReturnValues', () => {
308
- test('should default to NONE', () => {
309
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
310
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
311
- .set('name', 'John Doe')
312
- .dbParams();
313
-
314
- expect(params.ReturnValues).toBeUndefined();
315
- });
316
-
317
- test('should support ALL_OLD', () => {
318
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
319
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
320
- .set('name', 'John Doe')
321
- .returning('ALL_OLD')
322
- .dbParams();
323
-
324
- expect(params.ReturnValues).toBe('ALL_OLD');
325
- });
326
-
327
- test('should support ALL_NEW', () => {
328
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
329
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
330
- .set('name', 'John Doe')
331
- .returning('ALL_NEW')
332
- .dbParams();
333
-
334
- expect(params.ReturnValues).toBe('ALL_NEW');
335
- });
336
-
337
- test('should support UPDATED_OLD', () => {
338
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
339
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
340
- .set('name', 'John Doe')
341
- .returning('UPDATED_OLD')
342
- .dbParams();
343
-
344
- expect(params.ReturnValues).toBe('UPDATED_OLD');
345
- });
346
-
347
- test('should support UPDATED_NEW', () => {
348
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
349
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
350
- .set('name', 'John Doe')
351
- .returning('UPDATED_NEW')
352
- .dbParams();
353
-
354
- expect(params.ReturnValues).toBe('UPDATED_NEW');
355
- });
356
- });
357
-
358
- describe('Immutability', () => {
359
- test('should create new builder instance on each method call', () => {
360
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
361
- const builder1 = createUpdateBuilder<TestModel>(tableName, key, client);
362
- const builder2 = builder1.set('name', 'John');
363
- const builder3 = builder2.set('age', 30);
364
-
365
- expect(builder1.dbParams().UpdateExpression).toBeUndefined();
366
- expect(builder2.dbParams().UpdateExpression).toBe('SET #name = :name_0');
367
- expect(builder3.dbParams().UpdateExpression).toBe('SET #name = :name_0, #age = :age_1');
368
- });
369
- });
370
-
371
- describe('Secondary-index recomputation (indexContext)', () => {
372
- interface PersonnelModel {
373
- id: string;
374
- airportId: string;
375
- firstName: string;
376
- lastName: string;
377
- role: string;
378
- }
379
-
380
- const personnelModel = {
381
- key: {
382
- PK: { type: String, value: 'PERSON#${id}' },
383
- SK: { type: String, value: 'PROFILE' },
384
- },
385
- index: {
386
- GSI1PK: { type: String, value: 'AIRPORT#${airportId}' },
387
- GSI1SK: { type: String, value: 'PERSON#${lastName}#${firstName}' },
388
- },
389
- attributes: {
390
- id: { type: String, required: true },
391
- airportId: { type: String, required: true },
392
- firstName: { type: String, required: true },
393
- lastName: { type: String, required: true },
394
- role: { type: String },
395
- },
396
- } as const;
397
-
398
- test('does nothing when no .set() field appears in any index template', () => {
399
- const params = createUpdateBuilder<PersonnelModel>(
400
- tableName,
401
- { id: '1' } as Partial<PersonnelModel>,
402
- client,
403
- [],
404
- { set: [], remove: [], add: [], delete: [] },
405
- 'NONE',
406
- 0,
407
- false,
408
- undefined,
409
- { model: personnelModel as any, keyVars: { id: '1' } }
410
- )
411
- .set('role', 'pilot')
412
- .dbParams();
413
-
414
- expect(params.UpdateExpression).toBe('SET #role = :role_0');
415
- expect(params.ExpressionAttributeNames).toEqual({ '#role': 'role' });
416
- });
417
-
418
- test('recomputes affected index when all template vars are present in updates', () => {
419
- const params = createUpdateBuilder<PersonnelModel>(
420
- tableName,
421
- { id: '1' } as Partial<PersonnelModel>,
422
- client,
423
- [],
424
- { set: [], remove: [], add: [], delete: [] },
425
- 'NONE',
426
- 0,
427
- false,
428
- undefined,
429
- { model: personnelModel as any, keyVars: { id: '1' } }
430
- )
431
- .set({ firstName: 'Ada', lastName: 'Lovelace' })
432
- .dbParams();
433
-
434
- // GSI1SK depends on lastName + firstName — both supplied → recomputed.
435
- // GSI1PK depends on airportId — untouched, no recompute.
436
- expect(params.UpdateExpression).toBe(
437
- 'SET #firstName = :firstName_0, #lastName = :lastName_1, #GSI1SK = :GSI1SK_2'
438
- );
439
- expect(params.ExpressionAttributeValues).toMatchObject({
440
- ':firstName_0': 'Ada',
441
- ':lastName_1': 'Lovelace',
442
- ':GSI1SK_2': 'PERSON#Lovelace#Ada',
443
- });
444
- });
445
-
446
- test('throws when an affected index template references a field not in updates or key', () => {
447
- const builder = createUpdateBuilder<PersonnelModel>(
448
- tableName,
449
- { id: '1' } as Partial<PersonnelModel>,
450
- client,
451
- [],
452
- { set: [], remove: [], add: [], delete: [] },
453
- 'NONE',
454
- 0,
455
- false,
456
- undefined,
457
- { model: personnelModel as any, keyVars: { id: '1' } }
458
- ).set({ lastName: 'Lovelace' });
459
-
460
- // GSI1SK template = "PERSON#${lastName}#${firstName}" — firstName is missing.
461
- expect(() => builder.dbParams()).toThrow(/firstName/);
462
- expect(() => builder.dbParams()).toThrow(/GSI1SK/);
463
- });
464
-
465
- test('uses primary-key template vars when resolving index templates', () => {
466
- const userModel = {
467
- key: {
468
- PK: { type: String, value: 'USER#${username}' },
469
- SK: { type: String, value: 'USER#${username}' },
470
- },
471
- index: {
472
- GSI1PK: { type: String, value: 'USER#${username}#STATUS#${status}' },
473
- },
474
- attributes: {
475
- username: { type: String, required: true },
476
- status: { type: String, required: true },
477
- },
478
- } as const;
479
-
480
- const params = createUpdateBuilder<{ username: string; status: string }>(
481
- tableName,
482
- { username: 'jane' } as any,
483
- client,
484
- [],
485
- { set: [], remove: [], add: [], delete: [] },
486
- 'NONE',
487
- 0,
488
- false,
489
- undefined,
490
- { model: userModel as any, keyVars: { username: 'jane' } }
491
- )
492
- .set('status', 'active')
493
- .dbParams();
494
-
495
- expect(params.UpdateExpression).toBe(
496
- 'SET #status = :status_0, #GSI1PK = :GSI1PK_1'
497
- );
498
- expect(params.ExpressionAttributeValues).toMatchObject({
499
- ':status_0': 'active',
500
- ':GSI1PK_1': 'USER#jane#STATUS#active',
501
- });
502
- });
503
-
504
- test('accumulates set inputs across chained calls before resolving', () => {
505
- const params = createUpdateBuilder<PersonnelModel>(
506
- tableName,
507
- { id: '1' } as Partial<PersonnelModel>,
508
- client,
509
- [],
510
- { set: [], remove: [], add: [], delete: [] },
511
- 'NONE',
512
- 0,
513
- false,
514
- undefined,
515
- { model: personnelModel as any, keyVars: { id: '1' } }
516
- )
517
- .set('lastName', 'Lovelace')
518
- .set('firstName', 'Ada')
519
- .dbParams();
520
-
521
- // Each individual .set() was missing one var, but the accumulator has both
522
- // by the time dbParams() runs.
523
- expect(params.UpdateExpression).toBe(
524
- 'SET #lastName = :lastName_0, #firstName = :firstName_1, #GSI1SK = :GSI1SK_2'
525
- );
526
- expect(params.ExpressionAttributeValues).toMatchObject({
527
- ':GSI1SK_2': 'PERSON#Lovelace#Ada',
528
- });
529
- });
530
-
531
- test('does nothing when indexContext is omitted (backwards-compatible default)', () => {
532
- const params = createUpdateBuilder<PersonnelModel>(
533
- tableName,
534
- { id: '1' } as Partial<PersonnelModel>,
535
- client
536
- )
537
- .set({ lastName: 'Lovelace' })
538
- .dbParams();
539
-
540
- expect(params.UpdateExpression).toBe('SET #lastName = :lastName_0');
541
- });
542
- });
543
-
544
- describe('Complex scenarios', () => {
545
- test('should handle complex update with all features', () => {
546
- const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
547
- const params = createUpdateBuilder<TestModel>(tableName, key, client)
548
- .set('name', 'John Doe')
549
- .set('score', 100)
550
- .add('followerCount', 5)
551
- .remove('age')
552
- .where((attr, op) => op.and(op.gt(attr.followerCount, 0), op.lt(attr.followerCount, 1000)))
553
- .returning('ALL_NEW')
554
- .dbParams();
555
-
556
- expect(params.TableName).toBe(tableName);
557
- expect(params.Key).toEqual(key);
558
- expect(params.UpdateExpression).toContain('SET');
559
- expect(params.UpdateExpression).toContain('ADD');
560
- expect(params.UpdateExpression).toContain('REMOVE');
561
- expect(params.ConditionExpression).toMatch(
562
- /\(#followerCount > :followerCount_\d+\) AND \(#followerCount < :followerCount_\d+\)/
563
- );
564
- expect(params.ReturnValues).toBe('ALL_NEW');
565
- expect(params.ExpressionAttributeNames).toEqual({
566
- '#name': 'name',
567
- '#score': 'score',
568
- '#followerCount': 'followerCount',
569
- '#age': 'age',
570
- });
571
- });
572
- });
573
- });