@ftschopp/dynatable-core 1.4.2 → 1.4.3

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 (81) hide show
  1. package/CHANGELOG.md +7 -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/core/types.d.ts +5 -0
  5. package/dist/core/types.d.ts.map +1 -1
  6. package/package.json +11 -2
  7. package/eslint.config.mjs +0 -4
  8. package/jest.config.js +0 -11
  9. package/src/builders/README.md +0 -339
  10. package/src/builders/batch-get/README.md +0 -43
  11. package/src/builders/batch-get/create-batch-get-builder.test.ts +0 -193
  12. package/src/builders/batch-get/create-batch-get-builder.ts +0 -108
  13. package/src/builders/batch-get/index.ts +0 -2
  14. package/src/builders/batch-get/types.ts +0 -28
  15. package/src/builders/batch-write/README.md +0 -204
  16. package/src/builders/batch-write/create-batch-write-builder.test.ts +0 -173
  17. package/src/builders/batch-write/create-batch-write-builder.ts +0 -49
  18. package/src/builders/batch-write/index.ts +0 -2
  19. package/src/builders/batch-write/types.ts +0 -33
  20. package/src/builders/delete/create-delete-builder.test.ts +0 -294
  21. package/src/builders/delete/create-delete-builder.ts +0 -100
  22. package/src/builders/delete/index.ts +0 -2
  23. package/src/builders/delete/types.ts +0 -17
  24. package/src/builders/get/create-get-builder.test.ts +0 -272
  25. package/src/builders/get/create-get-builder.ts +0 -117
  26. package/src/builders/get/index.ts +0 -2
  27. package/src/builders/get/types.ts +0 -39
  28. package/src/builders/index.ts +0 -14
  29. package/src/builders/put/create-put-builder.test.ts +0 -213
  30. package/src/builders/put/create-put-builder.ts +0 -160
  31. package/src/builders/put/index.ts +0 -2
  32. package/src/builders/put/types.ts +0 -24
  33. package/src/builders/query/create-query-builder.test.ts +0 -770
  34. package/src/builders/query/create-query-builder.ts +0 -455
  35. package/src/builders/query/index.ts +0 -2
  36. package/src/builders/query/types.ts +0 -109
  37. package/src/builders/scan/create-scan-builder.test.ts +0 -416
  38. package/src/builders/scan/create-scan-builder.ts +0 -265
  39. package/src/builders/scan/index.ts +0 -2
  40. package/src/builders/scan/types.ts +0 -88
  41. package/src/builders/shared/conditions.ts +0 -58
  42. package/src/builders/shared/index.ts +0 -4
  43. package/src/builders/shared/operators.test.ts +0 -270
  44. package/src/builders/shared/operators.ts +0 -208
  45. package/src/builders/shared/projection.ts +0 -28
  46. package/src/builders/shared/types.ts +0 -101
  47. package/src/builders/transact-get/README.md +0 -167
  48. package/src/builders/transact-get/create-transact-get-builder.test.ts +0 -239
  49. package/src/builders/transact-get/create-transact-get-builder.ts +0 -67
  50. package/src/builders/transact-get/index.ts +0 -2
  51. package/src/builders/transact-get/types.ts +0 -31
  52. package/src/builders/transact-write/README.md +0 -202
  53. package/src/builders/transact-write/create-transact-write-builder.test.ts +0 -288
  54. package/src/builders/transact-write/create-transact-write-builder.ts +0 -124
  55. package/src/builders/transact-write/index.ts +0 -2
  56. package/src/builders/transact-write/types.ts +0 -71
  57. package/src/builders/update/create-update-builder.test.ts +0 -573
  58. package/src/builders/update/create-update-builder.ts +0 -375
  59. package/src/builders/update/index.ts +0 -2
  60. package/src/builders/update/types.ts +0 -63
  61. package/src/core/types.test.ts +0 -641
  62. package/src/core/types.ts +0 -366
  63. package/src/entity/create-entity-api.ts +0 -224
  64. package/src/entity/index.ts +0 -19
  65. package/src/entity/middleware/factories.ts +0 -15
  66. package/src/entity/middleware/types.ts +0 -7
  67. package/src/entity/middleware/with-middleware.ts +0 -37
  68. package/src/entity/types.ts +0 -79
  69. package/src/entity/validation/key-validation.ts +0 -34
  70. package/src/index.ts +0 -23
  71. package/src/table.ts +0 -116
  72. package/src/utils/dynamodb-logger.test.ts +0 -246
  73. package/src/utils/dynamodb-logger.ts +0 -175
  74. package/src/utils/model-utils.test.ts +0 -452
  75. package/src/utils/model-utils.ts +0 -188
  76. package/src/utils/zod-utils.test.ts +0 -363
  77. package/src/utils/zod-utils.ts +0 -56
  78. package/tests/integration/instagram-clone.integration.test.ts +0 -1019
  79. package/tests/integration/pagination-timestamps.integration.test.ts +0 -374
  80. package/tests/integration/transactions.integration.test.ts +0 -528
  81. 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
- };