@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,452 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { applyPostDefaults, computeIndexUpdates, stripInternalKeys } from './model-utils';
3
- import { ModelDefinition } from '../core/types';
4
-
5
- describe('applyPostDefaults - Timestamps', () => {
6
- const testModel: ModelDefinition = {
7
- key: {
8
- PK: { type: String, value: 'USER#${username}' },
9
- SK: { type: String, value: 'USER#${username}' },
10
- },
11
- attributes: {
12
- username: { type: String, required: true },
13
- name: { type: String },
14
- email: { type: String },
15
- },
16
- };
17
-
18
- beforeEach(() => {
19
- jest.useFakeTimers();
20
- jest.setSystemTime(new Date('2024-01-15T10:00:00.000Z'));
21
- });
22
-
23
- afterEach(() => {
24
- jest.useRealTimers();
25
- });
26
-
27
- describe('Timestamps disabled (default)', () => {
28
- test('should not add timestamps when disabled', () => {
29
- const input = { username: 'alice', name: 'Alice' };
30
- const result = applyPostDefaults(testModel, input, { timestamps: false });
31
-
32
- expect(result.createdAt).toBeUndefined();
33
- expect(result.updatedAt).toBeUndefined();
34
- });
35
-
36
- test('should not add timestamps when option not provided', () => {
37
- const input = { username: 'alice', name: 'Alice' };
38
- const result = applyPostDefaults(testModel, input);
39
-
40
- expect(result.createdAt).toBeUndefined();
41
- expect(result.updatedAt).toBeUndefined();
42
- });
43
- });
44
-
45
- describe('Timestamps enabled for new items', () => {
46
- test('should add createdAt and updatedAt for new items', () => {
47
- const input = { username: 'alice', name: 'Alice' };
48
- const result = applyPostDefaults(testModel, input, {
49
- timestamps: true,
50
- isUpdate: false,
51
- });
52
-
53
- expect(result.createdAt).toEqual('2024-01-15T10:00:00.000Z');
54
- expect(result.updatedAt).toEqual('2024-01-15T10:00:00.000Z');
55
- });
56
-
57
- test('should not override existing createdAt', () => {
58
- const existingCreatedAt = new Date('2024-01-01T00:00:00.000Z');
59
- const input = {
60
- username: 'alice',
61
- name: 'Alice',
62
- createdAt: existingCreatedAt,
63
- };
64
- const result = applyPostDefaults(testModel, input, {
65
- timestamps: true,
66
- isUpdate: false,
67
- });
68
-
69
- expect(result.createdAt).toEqual(existingCreatedAt);
70
- expect(result.updatedAt).toEqual('2024-01-15T10:00:00.000Z');
71
- });
72
-
73
- test('should work with other defaults', () => {
74
- const modelWithDefaults: ModelDefinition = {
75
- key: {
76
- PK: { type: String, value: 'USER#${username}' },
77
- SK: { type: String, value: 'USER#${username}' },
78
- },
79
- attributes: {
80
- username: { type: String, required: true },
81
- name: { type: String },
82
- status: { type: String, default: 'active' },
83
- score: { type: Number, default: 0 },
84
- },
85
- };
86
-
87
- const input = { username: 'alice', name: 'Alice' };
88
- const result = applyPostDefaults(modelWithDefaults, input, {
89
- timestamps: true,
90
- isUpdate: false,
91
- });
92
-
93
- expect(result.status).toBe('active');
94
- expect(result.score).toBe(0);
95
- expect(result.createdAt).toEqual('2024-01-15T10:00:00.000Z');
96
- expect(result.updatedAt).toEqual('2024-01-15T10:00:00.000Z');
97
- });
98
- });
99
-
100
- describe('Timestamps enabled for updates', () => {
101
- test('should only add updatedAt for updates', () => {
102
- const input = { username: 'alice', name: 'Alice Updated' };
103
- const result = applyPostDefaults(testModel, input, {
104
- timestamps: true,
105
- isUpdate: true,
106
- });
107
-
108
- expect(result.createdAt).toBeUndefined();
109
- expect(result.updatedAt).toEqual('2024-01-15T10:00:00.000Z');
110
- });
111
-
112
- test('should update updatedAt even if already present', () => {
113
- const oldUpdatedAt = new Date('2024-01-01T00:00:00.000Z');
114
- const input = {
115
- username: 'alice',
116
- name: 'Alice Updated',
117
- updatedAt: oldUpdatedAt,
118
- };
119
- const result = applyPostDefaults(testModel, input, {
120
- timestamps: true,
121
- isUpdate: true,
122
- });
123
-
124
- expect(result.updatedAt).toEqual('2024-01-15T10:00:00.000Z');
125
- expect(result.updatedAt).not.toEqual(oldUpdatedAt);
126
- });
127
-
128
- test('should preserve existing createdAt on update', () => {
129
- const existingCreatedAt = new Date('2024-01-01T00:00:00.000Z');
130
- const input = {
131
- username: 'alice',
132
- name: 'Alice Updated',
133
- createdAt: existingCreatedAt,
134
- };
135
- const result = applyPostDefaults(testModel, input, {
136
- timestamps: true,
137
- isUpdate: true,
138
- });
139
-
140
- expect(result.createdAt).toEqual(existingCreatedAt);
141
- expect(result.updatedAt).toEqual('2024-01-15T10:00:00.000Z');
142
- });
143
- });
144
-
145
- describe('Generated values with timestamps', () => {
146
- test('should work with ULID generation', () => {
147
- const modelWithUlid: ModelDefinition = {
148
- key: {
149
- PK: { type: String, value: 'USER#${id}' },
150
- SK: { type: String, value: 'USER#${id}' },
151
- },
152
- attributes: {
153
- id: { type: String, generate: 'ulid' },
154
- username: { type: String, required: true },
155
- },
156
- };
157
-
158
- const input = { username: 'alice' };
159
- const result = applyPostDefaults(modelWithUlid, input, {
160
- timestamps: true,
161
- isUpdate: false,
162
- });
163
-
164
- expect(result.id).toBeDefined();
165
- expect(typeof result.id).toBe('string');
166
- expect(result.id.length).toBeGreaterThan(0);
167
- expect(result.createdAt).toEqual('2024-01-15T10:00:00.000Z');
168
- expect(result.updatedAt).toEqual('2024-01-15T10:00:00.000Z');
169
- });
170
-
171
- test('should work with UUID generation', () => {
172
- const modelWithUuid: ModelDefinition = {
173
- key: {
174
- PK: { type: String, value: 'USER#${id}' },
175
- SK: { type: String, value: 'USER#${id}' },
176
- },
177
- attributes: {
178
- id: { type: String, generate: 'uuid' },
179
- username: { type: String, required: true },
180
- },
181
- };
182
-
183
- const input = { username: 'alice' };
184
- const result = applyPostDefaults(modelWithUuid, input, {
185
- timestamps: true,
186
- isUpdate: false,
187
- });
188
-
189
- expect(result.id).toBeDefined();
190
- expect(typeof result.id).toBe('string');
191
- expect(result.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
192
- expect(result.createdAt).toEqual('2024-01-15T10:00:00.000Z');
193
- expect(result.updatedAt).toEqual('2024-01-15T10:00:00.000Z');
194
- });
195
- });
196
-
197
- describe('Edge cases', () => {
198
- test('should handle empty input', () => {
199
- const input = {};
200
- const result = applyPostDefaults(testModel, input, {
201
- timestamps: true,
202
- isUpdate: false,
203
- });
204
-
205
- expect(result.createdAt).toEqual('2024-01-15T10:00:00.000Z');
206
- expect(result.updatedAt).toEqual('2024-01-15T10:00:00.000Z');
207
- });
208
-
209
- test('should handle multiple consecutive calls with different times', () => {
210
- const input1 = { username: 'alice' };
211
- const input2 = { username: 'bob' };
212
-
213
- jest.setSystemTime(new Date('2024-01-15T10:00:00.000Z'));
214
- const result1 = applyPostDefaults(testModel, input1, {
215
- timestamps: true,
216
- isUpdate: false,
217
- });
218
-
219
- jest.setSystemTime(new Date('2024-01-15T11:00:00.000Z'));
220
- const result2 = applyPostDefaults(testModel, input2, {
221
- timestamps: true,
222
- isUpdate: false,
223
- });
224
-
225
- expect(result1.createdAt).toEqual('2024-01-15T10:00:00.000Z');
226
- expect(result1.updatedAt).toEqual('2024-01-15T10:00:00.000Z');
227
-
228
- expect(result2.createdAt).toEqual('2024-01-15T11:00:00.000Z');
229
- expect(result2.updatedAt).toEqual('2024-01-15T11:00:00.000Z');
230
- });
231
- });
232
- });
233
-
234
- describe('stripInternalKeys', () => {
235
- test('should remove PK, SK, and _type from object', () => {
236
- const input = {
237
- PK: 'USER#alice',
238
- SK: 'USER#alice',
239
- _type: 'User',
240
- username: 'alice',
241
- name: 'Alice',
242
- email: 'alice@example.com',
243
- };
244
-
245
- const result = stripInternalKeys(input);
246
-
247
- expect(result).toEqual({
248
- username: 'alice',
249
- name: 'Alice',
250
- email: 'alice@example.com',
251
- });
252
- });
253
-
254
- test('should handle object without internal keys', () => {
255
- const input = {
256
- username: 'alice',
257
- name: 'Alice',
258
- email: 'alice@example.com',
259
- };
260
-
261
- const result = stripInternalKeys(input);
262
-
263
- expect(result).toEqual(input);
264
- });
265
-
266
- test('should handle array of objects', () => {
267
- const input = [
268
- {
269
- PK: 'USER#alice',
270
- SK: 'USER#alice',
271
- _type: 'User',
272
- username: 'alice',
273
- name: 'Alice',
274
- },
275
- {
276
- PK: 'USER#bob',
277
- SK: 'USER#bob',
278
- _type: 'User',
279
- username: 'bob',
280
- name: 'Bob',
281
- },
282
- ];
283
-
284
- const result = stripInternalKeys(input);
285
-
286
- expect(result).toEqual([
287
- { username: 'alice', name: 'Alice' },
288
- { username: 'bob', name: 'Bob' },
289
- ]);
290
- });
291
-
292
- test('should handle undefined', () => {
293
- const result = stripInternalKeys(undefined);
294
- expect(result).toBeUndefined();
295
- });
296
-
297
- test('should handle null', () => {
298
- const result = stripInternalKeys(null);
299
- expect(result).toBeNull();
300
- });
301
-
302
- test('should handle empty object', () => {
303
- const input = {};
304
- const result = stripInternalKeys(input);
305
- expect(result).toEqual({});
306
- });
307
-
308
- test('should handle empty array', () => {
309
- const input: any[] = [];
310
- const result = stripInternalKeys(input);
311
- expect(result).toEqual([]);
312
- });
313
-
314
- test('should preserve other fields with similar names', () => {
315
- const input = {
316
- PK: 'USER#alice',
317
- SK: 'USER#alice',
318
- _type: 'User',
319
- PKG: 'package-name',
320
- SKIP: 'skip-value',
321
- type: 'customer',
322
- username: 'alice',
323
- };
324
-
325
- const result = stripInternalKeys(input);
326
-
327
- expect(result).toEqual({
328
- PKG: 'package-name',
329
- SKIP: 'skip-value',
330
- type: 'customer',
331
- username: 'alice',
332
- });
333
- });
334
-
335
- test('should handle nested objects (does not recurse)', () => {
336
- const input = {
337
- PK: 'USER#alice',
338
- SK: 'USER#alice',
339
- _type: 'User',
340
- username: 'alice',
341
- metadata: {
342
- PK: 'METADATA#1',
343
- SK: 'METADATA#1',
344
- _type: 'Metadata',
345
- value: 'test',
346
- },
347
- };
348
-
349
- const result = stripInternalKeys(input);
350
-
351
- // stripInternalKeys does not recurse into nested objects
352
- expect(result).toEqual({
353
- username: 'alice',
354
- metadata: {
355
- PK: 'METADATA#1',
356
- SK: 'METADATA#1',
357
- _type: 'Metadata',
358
- value: 'test',
359
- },
360
- });
361
- });
362
- });
363
-
364
- describe('computeIndexUpdates', () => {
365
- const personnelModel: ModelDefinition = {
366
- key: {
367
- PK: { type: String, value: 'PERSON#${id}' },
368
- SK: { type: String, value: 'PROFILE' },
369
- },
370
- index: {
371
- GSI1PK: { type: String, value: 'AIRPORT#${airportId}' },
372
- GSI1SK: { type: String, value: 'PERSON#${lastName}#${firstName}' },
373
- },
374
- attributes: {
375
- id: { type: String, required: true },
376
- airportId: { type: String, required: true },
377
- firstName: { type: String, required: true },
378
- lastName: { type: String, required: true },
379
- },
380
- };
381
-
382
- test('returns empty result when model has no indexes', () => {
383
- const noIndexModel: ModelDefinition = {
384
- key: {
385
- PK: { type: String, value: 'USER#${id}' },
386
- SK: { type: String, value: 'USER#${id}' },
387
- },
388
- attributes: { id: { type: String, required: true } },
389
- };
390
- const result = computeIndexUpdates(noIndexModel, { id: '1' }, { name: 'X' });
391
- expect(result).toEqual({ actions: {}, missing: [] });
392
- });
393
-
394
- test('skips indexes whose template references no updated field', () => {
395
- // Updating only "role" — none of the index templates mention role.
396
- const result = computeIndexUpdates(
397
- personnelModel,
398
- { id: '1' },
399
- { role: 'pilot' }
400
- );
401
- expect(result).toEqual({ actions: {}, missing: [] });
402
- });
403
-
404
- test('recomputes an affected index when all template vars resolve', () => {
405
- const result = computeIndexUpdates(
406
- personnelModel,
407
- { id: '1' },
408
- { firstName: 'Ada', lastName: 'Lovelace' }
409
- );
410
- expect(result.actions).toEqual({ GSI1SK: 'PERSON#Lovelace#Ada' });
411
- expect(result.missing).toEqual([]);
412
- });
413
-
414
- test('reports missing template vars instead of throwing', () => {
415
- const result = computeIndexUpdates(
416
- personnelModel,
417
- { id: '1' },
418
- { lastName: 'Lovelace' }
419
- );
420
- expect(result.actions).toEqual({});
421
- expect(result.missing).toEqual([
422
- {
423
- index: 'GSI1SK',
424
- template: 'PERSON#${lastName}#${firstName}',
425
- missing: ['firstName'],
426
- },
427
- ]);
428
- });
429
-
430
- test('uses keyVars to resolve fields not present in updates', () => {
431
- // airportId is not in updates but in keyVars — GSI1PK should still resolve.
432
- const result = computeIndexUpdates(
433
- personnelModel,
434
- { id: '1', airportId: 'EZE' },
435
- { airportId: 'AEP' } // changing it
436
- );
437
- expect(result.actions).toEqual({ GSI1PK: 'AIRPORT#AEP' });
438
- });
439
-
440
- test('handles multiple affected indexes independently', () => {
441
- const result = computeIndexUpdates(
442
- personnelModel,
443
- { id: '1' },
444
- { airportId: 'AEP', firstName: 'Ada', lastName: 'Lovelace' }
445
- );
446
- expect(result.actions).toEqual({
447
- GSI1PK: 'AIRPORT#AEP',
448
- GSI1SK: 'PERSON#Lovelace#Ada',
449
- });
450
- expect(result.missing).toEqual([]);
451
- });
452
- });
@@ -1,188 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
-
3
- import { InferModel, KeyDefinition, ModelDefinition } from '@/core/types';
4
- import { ulid } from 'ulid';
5
-
6
- /**
7
- * Extracts all variable names from a template string
8
- * @param template - Template string with ${variable} syntax
9
- * @returns Array of variable names found in the template
10
- */
11
- export const extractTemplateVars = (template: string): string[] => {
12
- const matches = template.matchAll(/\${(.*?)}/g);
13
- return Array.from(matches, (m) => m[1]).filter((v): v is string => v !== undefined);
14
- };
15
-
16
- /**
17
- * Resolves a string template with provided data
18
- * @throws Error if any required template variable is missing from data
19
- */
20
- export const resolveTemplate = (template: string, data: Record<string, any>): string => {
21
- const missing: string[] = [];
22
-
23
- const result = template.replace(/\${(.*?)}/g, (_, key) => {
24
- const value = data[key];
25
- if (value === undefined) {
26
- missing.push(key);
27
- return `\${${key}}`; // Keep placeholder for error message
28
- }
29
- return String(value);
30
- });
31
-
32
- if (missing.length > 0) {
33
- throw new Error(
34
- `Missing required field(s) for key template: ${missing.join(', ')}. ` +
35
- `Template: "${template}"`
36
- );
37
- }
38
-
39
- return result;
40
- };
41
-
42
- /**
43
- * Resolves all keys or indexes for a model
44
- */
45
- export const resolveKeys = <M extends ModelDefinition>(
46
- model: M,
47
- input: Record<string, any>,
48
- type: 'key' | 'index' | 'both' = 'key'
49
- ): Record<string, string> => {
50
- const out: Record<string, string> = {};
51
- const process = (keys?: Record<string, KeyDefinition>) => {
52
- if (!keys) return;
53
- for (const [k, def] of Object.entries(keys)) {
54
- out[k] = resolveTemplate(def.value, input);
55
- }
56
- };
57
- if (type === 'key' || type === 'both') process(model.key);
58
- if (type === 'index' || type === 'both') process(model.index);
59
- return out;
60
- };
61
-
62
- /**
63
- * Result of analyzing how a partial update affects a model's secondary index keys.
64
- *
65
- * `actions` maps index-attribute name → freshly resolved template value, ready to SET.
66
- * `missing` lists indexes whose template references at least one updated field but
67
- * cannot be fully resolved from `keyVars + updates`; those entries identify the
68
- * variables the caller must additionally provide.
69
- */
70
- export type IndexUpdateAnalysis = {
71
- actions: Record<string, string>;
72
- missing: { index: string; template: string; missing: string[] }[];
73
- };
74
-
75
- /**
76
- * Determines which secondary-index keys must be recomputed given a partial update.
77
- *
78
- * An index is "affected" iff at least one of its template variables appears in
79
- * `updates`. For each affected index we attempt to resolve the template from the
80
- * union of `keyVars` (template vars carried in the primary key, e.g. `id`) and
81
- * `updates` (the user's `.set()` payload). If any template variable is missing
82
- * from that union, the index is reported in `missing` instead of `actions` —
83
- * recomputing it would require reading the existing item from DynamoDB, which
84
- * the builder intentionally does not do.
85
- */
86
- export const computeIndexUpdates = <M extends ModelDefinition>(
87
- model: M,
88
- keyVars: Record<string, any>,
89
- updates: Record<string, any>
90
- ): IndexUpdateAnalysis => {
91
- const actions: Record<string, string> = {};
92
- const missing: { index: string; template: string; missing: string[] }[] = [];
93
-
94
- if (!model.index) return { actions, missing };
95
-
96
- const updateKeys = new Set(Object.keys(updates));
97
- const combined: Record<string, any> = { ...keyVars, ...updates };
98
-
99
- for (const [indexName, indexDef] of Object.entries(model.index)) {
100
- const templateVars = extractTemplateVars(indexDef.value);
101
- const isAffected = templateVars.some((v) => updateKeys.has(v));
102
- if (!isAffected) continue;
103
-
104
- const missingVars = templateVars.filter((v) => combined[v] === undefined);
105
- if (missingVars.length > 0) {
106
- missing.push({ index: indexName, template: indexDef.value, missing: missingVars });
107
- continue;
108
- }
109
-
110
- actions[indexName] = resolveTemplate(indexDef.value, combined);
111
- }
112
-
113
- return { actions, missing };
114
- };
115
-
116
- /**
117
- * Applies default and generated values to validated input
118
- */
119
- export const applyPostDefaults = <M extends ModelDefinition>(
120
- model: M,
121
- validatedItem: Record<string, any>,
122
- options?: { isUpdate?: boolean; timestamps?: boolean }
123
- ): InferModel<M> => {
124
- const result: Record<string, any> = { ...validatedItem };
125
-
126
- for (const [key, attr] of Object.entries(model.attributes)) {
127
- if (result[key] === undefined) {
128
- const generate = 'generate' in attr ? attr.generate : undefined;
129
- if (generate === 'ulid') {
130
- result[key] = ulid();
131
- } else if (generate === 'uuid') {
132
- result[key] = crypto.randomUUID();
133
- } else if (attr.default !== undefined) {
134
- result[key] = typeof attr.default === 'function' ? attr.default() : attr.default;
135
- }
136
- }
137
- }
138
-
139
- // Apply timestamps if enabled
140
- const enableTimestamps = options?.timestamps ?? false;
141
- const isUpdate = options?.isUpdate ?? false;
142
-
143
- if (enableTimestamps) {
144
- const now = new Date().toISOString();
145
-
146
- // Only set createdAt on new items (not updates)
147
- if (!isUpdate && result.createdAt === undefined) {
148
- result.createdAt = now;
149
- }
150
-
151
- // Always set updatedAt
152
- result.updatedAt = now;
153
- }
154
-
155
- return result as InferModel<M>;
156
- };
157
-
158
- /**
159
- * Internal DynamoDB keys that should be stripped when cleanInternalKeys is enabled
160
- */
161
- const INTERNAL_KEYS = ['PK', 'SK', '_type'] as const;
162
-
163
- /**
164
- * Removes internal DynamoDB keys from an item or array of items
165
- * @param data - Single item or array of items from DynamoDB
166
- * @returns Data with internal keys removed
167
- */
168
- export const stripInternalKeys = <T>(data: T | T[] | undefined): T | T[] | undefined => {
169
- if (data === undefined || data === null) {
170
- return data;
171
- }
172
-
173
- if (Array.isArray(data)) {
174
- return data.map((item) => stripInternalKeys(item)) as T[];
175
- }
176
-
177
- if (typeof data === 'object') {
178
- const cleaned: any = {};
179
- for (const [key, value] of Object.entries(data)) {
180
- if (!INTERNAL_KEYS.includes(key as any)) {
181
- cleaned[key] = value;
182
- }
183
- }
184
- return cleaned as T;
185
- }
186
-
187
- return data;
188
- };