@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.
- package/CHANGELOG.md +7 -0
- package/dist/builders/query/create-query-builder.d.ts.map +1 -1
- package/dist/builders/query/create-query-builder.js +19 -2
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/package.json +11 -2
- package/eslint.config.mjs +0 -4
- package/jest.config.js +0 -11
- package/src/builders/README.md +0 -339
- package/src/builders/batch-get/README.md +0 -43
- package/src/builders/batch-get/create-batch-get-builder.test.ts +0 -193
- package/src/builders/batch-get/create-batch-get-builder.ts +0 -108
- package/src/builders/batch-get/index.ts +0 -2
- package/src/builders/batch-get/types.ts +0 -28
- package/src/builders/batch-write/README.md +0 -204
- package/src/builders/batch-write/create-batch-write-builder.test.ts +0 -173
- package/src/builders/batch-write/create-batch-write-builder.ts +0 -49
- package/src/builders/batch-write/index.ts +0 -2
- package/src/builders/batch-write/types.ts +0 -33
- package/src/builders/delete/create-delete-builder.test.ts +0 -294
- package/src/builders/delete/create-delete-builder.ts +0 -100
- package/src/builders/delete/index.ts +0 -2
- package/src/builders/delete/types.ts +0 -17
- package/src/builders/get/create-get-builder.test.ts +0 -272
- package/src/builders/get/create-get-builder.ts +0 -117
- package/src/builders/get/index.ts +0 -2
- package/src/builders/get/types.ts +0 -39
- package/src/builders/index.ts +0 -14
- package/src/builders/put/create-put-builder.test.ts +0 -213
- package/src/builders/put/create-put-builder.ts +0 -160
- package/src/builders/put/index.ts +0 -2
- package/src/builders/put/types.ts +0 -24
- package/src/builders/query/create-query-builder.test.ts +0 -770
- package/src/builders/query/create-query-builder.ts +0 -455
- package/src/builders/query/index.ts +0 -2
- package/src/builders/query/types.ts +0 -109
- package/src/builders/scan/create-scan-builder.test.ts +0 -416
- package/src/builders/scan/create-scan-builder.ts +0 -265
- package/src/builders/scan/index.ts +0 -2
- package/src/builders/scan/types.ts +0 -88
- package/src/builders/shared/conditions.ts +0 -58
- package/src/builders/shared/index.ts +0 -4
- package/src/builders/shared/operators.test.ts +0 -270
- package/src/builders/shared/operators.ts +0 -208
- package/src/builders/shared/projection.ts +0 -28
- package/src/builders/shared/types.ts +0 -101
- package/src/builders/transact-get/README.md +0 -167
- package/src/builders/transact-get/create-transact-get-builder.test.ts +0 -239
- package/src/builders/transact-get/create-transact-get-builder.ts +0 -67
- package/src/builders/transact-get/index.ts +0 -2
- package/src/builders/transact-get/types.ts +0 -31
- package/src/builders/transact-write/README.md +0 -202
- package/src/builders/transact-write/create-transact-write-builder.test.ts +0 -288
- package/src/builders/transact-write/create-transact-write-builder.ts +0 -124
- package/src/builders/transact-write/index.ts +0 -2
- package/src/builders/transact-write/types.ts +0 -71
- package/src/builders/update/create-update-builder.test.ts +0 -573
- package/src/builders/update/create-update-builder.ts +0 -375
- package/src/builders/update/index.ts +0 -2
- package/src/builders/update/types.ts +0 -63
- package/src/core/types.test.ts +0 -641
- package/src/core/types.ts +0 -366
- package/src/entity/create-entity-api.ts +0 -224
- package/src/entity/index.ts +0 -19
- package/src/entity/middleware/factories.ts +0 -15
- package/src/entity/middleware/types.ts +0 -7
- package/src/entity/middleware/with-middleware.ts +0 -37
- package/src/entity/types.ts +0 -79
- package/src/entity/validation/key-validation.ts +0 -34
- package/src/index.ts +0 -23
- package/src/table.ts +0 -116
- package/src/utils/dynamodb-logger.test.ts +0 -246
- package/src/utils/dynamodb-logger.ts +0 -175
- package/src/utils/model-utils.test.ts +0 -452
- package/src/utils/model-utils.ts +0 -188
- package/src/utils/zod-utils.test.ts +0 -363
- package/src/utils/zod-utils.ts +0 -56
- package/tests/integration/instagram-clone.integration.test.ts +0 -1019
- package/tests/integration/pagination-timestamps.integration.test.ts +0 -374
- package/tests/integration/transactions.integration.test.ts +0 -528
- 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
|
-
});
|
package/src/utils/model-utils.ts
DELETED
|
@@ -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
|
-
};
|