@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,528 +0,0 @@
1
- import { Table } from '../../src/table';
2
- import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
-
4
- /**
5
- * Instagram Clone Schema - same as demo.instagram.test.ts
6
- */
7
- const InstagramSchema = {
8
- format: 'dynatable:1.0.0',
9
- version: '1.0.0',
10
- indexes: {
11
- primary: { hash: 'PK', sort: 'SK' },
12
- gs1: { hash: 'GSI1PK', sort: 'GSI1SK' },
13
- },
14
- models: {
15
- User: {
16
- key: {
17
- PK: { type: String, value: 'USER#${username}' },
18
- SK: { type: String, value: 'USER#${username}' },
19
- },
20
- attributes: {
21
- username: { type: String, required: true },
22
- name: { type: String, required: true },
23
- followerCount: { type: Number, default: 0 },
24
- followingCount: { type: Number, default: 0 },
25
- },
26
- },
27
-
28
- Photo: {
29
- key: {
30
- PK: { type: String, value: 'UP#${username}' },
31
- SK: { type: String, value: 'PHOTO#${photoId}' },
32
- },
33
- attributes: {
34
- username: { type: String, required: true },
35
- photoId: { type: String, generate: 'ulid' },
36
- url: { type: String, required: true },
37
- likesCount: { type: Number, default: 0 },
38
- commentCount: { type: Number, default: 0 },
39
- },
40
- },
41
-
42
- Like: {
43
- key: {
44
- PK: { type: String, value: 'PL#${photoId}' },
45
- SK: { type: String, value: 'LIKE#${likingUsername}' },
46
- },
47
- index: {
48
- gs1PK: { type: String, value: 'PL#${photoId}' },
49
- gs1SK: { type: String, value: 'LIKE#${likeId}' },
50
- },
51
- attributes: {
52
- photoId: { type: String, required: true },
53
- likingUsername: { type: String, required: true },
54
- likeId: { type: String, generate: 'ulid' },
55
- },
56
- },
57
-
58
- Comment: {
59
- key: {
60
- PK: { type: String, value: 'PC#${photoId}' },
61
- SK: { type: String, value: 'COMMENT#${commentId}' },
62
- },
63
- attributes: {
64
- photoId: { type: String, required: true },
65
- commentId: { type: String, generate: 'ulid' },
66
- commentingUsername: { type: String, required: true },
67
- content: { type: String, required: true },
68
- },
69
- },
70
-
71
- Follow: {
72
- key: {
73
- PK: { type: String, value: 'FOLLOW#${followedUsername}' },
74
- SK: { type: String, value: 'FOLLOW#${followingUsername}' },
75
- },
76
- index: {
77
- gs1PK: { type: String, value: 'FOLLOW#${followingUsername}' },
78
- gs1SK: { type: String, value: 'FOLLOW#${followedUsername}' },
79
- },
80
- attributes: {
81
- followedUsername: { type: String, required: true },
82
- followingUsername: { type: String, required: true },
83
- },
84
- },
85
- },
86
- params: {
87
- timestamps: true,
88
- },
89
- } as const;
90
-
91
- const table = new Table({
92
- name: 'InstagramClone',
93
- client: new DynamoDBClient({}),
94
- schema: InstagramSchema,
95
- });
96
-
97
- jest.mock('ulid', () => ({
98
- ulid: jest.fn(() => '01K16ZP43BRX67DG50SHGZ11DS'),
99
- }));
100
-
101
- describe('Transactions Integration Tests - Instagram Schema', () => {
102
- describe('TransactWrite - Real World Use Cases', () => {
103
- test('Like a Photo (Put Like + Update Photo.likesCount)', () => {
104
- // This is an atomic operation: both must succeed or both fail
105
- const params = table
106
- .transactWrite()
107
- .addPut(
108
- table.entities.Like.put({
109
- photoId: 'photo123',
110
- likingUsername: 'juanca',
111
- })
112
- .ifNotExists()
113
- .dbParams()
114
- )
115
- .addUpdate(
116
- table.entities.Photo.update({
117
- username: 'alice',
118
- photoId: 'photo123',
119
- })
120
- .add('likesCount', 1)
121
- .dbParams()
122
- )
123
- .dbParams();
124
-
125
- expect(params.TransactItems).toHaveLength(2);
126
-
127
- // Verify Put operation
128
- expect(params.TransactItems[0]).toHaveProperty('Put');
129
- expect(params.TransactItems[0]?.Put.Item).toMatchObject({
130
- photoId: 'photo123',
131
- likingUsername: 'juanca',
132
- PK: 'PL#photo123',
133
- SK: 'LIKE#juanca',
134
- });
135
- expect(params.TransactItems[0]?.Put.ConditionExpression).toContain('attribute_not_exists');
136
-
137
- // Verify Update operation
138
- expect(params.TransactItems[1]).toHaveProperty('Update');
139
- expect(params.TransactItems[1]?.Update.Key).toEqual({
140
- PK: 'UP#alice',
141
- SK: 'PHOTO#photo123',
142
- });
143
- expect(params.TransactItems[1]?.Update.UpdateExpression).toContain(
144
- 'ADD #likesCount :likesCount_0'
145
- );
146
- // Should also include updatedAt timestamp
147
- expect(params.TransactItems[1]?.Update.UpdateExpression).toContain(
148
- 'SET #updatedAt = :updatedAt_ts'
149
- );
150
- });
151
-
152
- test('Follow a User (Put Follow + Update both user counts)', () => {
153
- const params = table
154
- .transactWrite()
155
- .addPut(
156
- table.entities.Follow.put({
157
- followedUsername: 'alice',
158
- followingUsername: 'bob',
159
- })
160
- .ifNotExists()
161
- .dbParams()
162
- )
163
- .addUpdate(
164
- table.entities.User.update({ username: 'alice' }).add('followerCount', 1).dbParams()
165
- )
166
- .addUpdate(
167
- table.entities.User.update({ username: 'bob' }).add('followingCount', 1).dbParams()
168
- )
169
- .dbParams();
170
-
171
- expect(params.TransactItems).toHaveLength(3);
172
-
173
- // Put Follow
174
- expect(params.TransactItems[0]?.Put.Item).toMatchObject({
175
- followedUsername: 'alice',
176
- followingUsername: 'bob',
177
- PK: 'FOLLOW#alice',
178
- SK: 'FOLLOW#bob',
179
- });
180
-
181
- // Update alice's followerCount
182
- expect(params.TransactItems[1]?.Update.Key).toEqual({
183
- PK: 'USER#alice',
184
- SK: 'USER#alice',
185
- });
186
- expect(params.TransactItems[1]?.Update.UpdateExpression).toContain('followerCount');
187
-
188
- // Update bob's followingCount
189
- expect(params.TransactItems[2]?.Update.Key).toEqual({
190
- PK: 'USER#bob',
191
- SK: 'USER#bob',
192
- });
193
- expect(params.TransactItems[2]?.Update.UpdateExpression).toContain('followingCount');
194
- });
195
-
196
- test('Comment on Photo (Put Comment + Update Photo.commentCount)', () => {
197
- const params = table
198
- .transactWrite()
199
- .addPut(
200
- table.entities.Comment.put({
201
- photoId: 'photo123',
202
- commentingUsername: 'bob',
203
- content: 'Great photo!',
204
- }).dbParams()
205
- )
206
- .addUpdate(
207
- table.entities.Photo.update({
208
- username: 'alice',
209
- photoId: 'photo123',
210
- })
211
- .add('commentCount', 1)
212
- .dbParams()
213
- )
214
- .dbParams();
215
-
216
- expect(params.TransactItems).toHaveLength(2);
217
-
218
- // Put Comment
219
- expect(params.TransactItems[0]?.Put.Item).toMatchObject({
220
- photoId: 'photo123',
221
- commentingUsername: 'bob',
222
- content: 'Great photo!',
223
- PK: 'PC#photo123',
224
- });
225
-
226
- // Update Photo commentCount
227
- expect(params.TransactItems[1]?.Update.UpdateExpression).toContain(
228
- 'ADD #commentCount :commentCount_0'
229
- );
230
- // Should also include updatedAt timestamp
231
- expect(params.TransactItems[1]?.Update.UpdateExpression).toContain(
232
- 'SET #updatedAt = :updatedAt_ts'
233
- );
234
- });
235
-
236
- test('Delete Photo with safety check (only if no likes or comments)', () => {
237
- const params = table
238
- .transactWrite()
239
- .addDelete(
240
- table.entities.Photo.delete({
241
- username: 'alice',
242
- photoId: 'photo123',
243
- })
244
- .where((attr, op) => op.and(op.eq(attr.likesCount, 0), op.eq(attr.commentCount, 0)))
245
- .dbParams()
246
- )
247
- .dbParams();
248
-
249
- expect(params.TransactItems).toHaveLength(1);
250
- expect(params.TransactItems[0]?.Delete.Key).toEqual({
251
- PK: 'UP#alice',
252
- SK: 'PHOTO#photo123',
253
- });
254
- expect(params.TransactItems[0]?.Delete.ConditionExpression).toMatch(
255
- /likesCount.*commentCount/
256
- );
257
- });
258
-
259
- test('Create User with admin existence check', () => {
260
- // Only create user if admin exists (business rule)
261
- const params = table
262
- .transactWrite()
263
- .addPut(
264
- table.entities.User.put({
265
- username: 'newuser',
266
- name: 'New User',
267
- })
268
- .ifNotExists()
269
- .dbParams()
270
- )
271
- .addConditionCheck({
272
- TableName: 'InstagramClone',
273
- Key: { PK: 'USER#admin', SK: 'USER#admin' },
274
- ConditionExpression: 'attribute_exists(#PK)',
275
- ExpressionAttributeNames: { '#PK': 'PK' },
276
- })
277
- .dbParams();
278
-
279
- expect(params.TransactItems).toHaveLength(2);
280
- expect(params.TransactItems[0]).toHaveProperty('Put');
281
- expect(params.TransactItems[1]).toHaveProperty('ConditionCheck');
282
- });
283
-
284
- test('Unlike Photo (Delete Like + Decrement Photo.likesCount)', () => {
285
- const params = table
286
- .transactWrite()
287
- .addDelete(
288
- table.entities.Like.delete({
289
- photoId: 'photo123',
290
- likingUsername: 'juanca',
291
- }).dbParams()
292
- )
293
- .addUpdate(
294
- table.entities.Photo.update({
295
- username: 'alice',
296
- photoId: 'photo123',
297
- })
298
- .add('likesCount', -1)
299
- .dbParams()
300
- )
301
- .dbParams();
302
-
303
- expect(params.TransactItems).toHaveLength(2);
304
-
305
- // Delete Like
306
- expect(params.TransactItems[0]?.Delete.Key).toEqual({
307
- PK: 'PL#photo123',
308
- SK: 'LIKE#juanca',
309
- });
310
-
311
- // Update Photo (decrement)
312
- expect(params.TransactItems[1]?.Update.Key).toEqual({
313
- PK: 'UP#alice',
314
- SK: 'PHOTO#photo123',
315
- });
316
- expect(params.TransactItems[1]?.Update.UpdateExpression).toContain(
317
- 'ADD #likesCount :likesCount_0'
318
- );
319
- expect(params.TransactItems[1]?.Update.UpdateExpression).toContain(
320
- 'SET #updatedAt = :updatedAt_ts'
321
- );
322
- expect(params.TransactItems[1]?.Update.ExpressionAttributeValues).toHaveProperty(
323
- ':likesCount_0'
324
- );
325
- });
326
-
327
- test('Unfollow User (Delete Follow + Update both counts)', () => {
328
- const params = table
329
- .transactWrite()
330
- .addDelete(
331
- table.entities.Follow.delete({
332
- followedUsername: 'alice',
333
- followingUsername: 'bob',
334
- }).dbParams()
335
- )
336
- .addUpdate(
337
- table.entities.User.update({ username: 'alice' })
338
- .add('followerCount', -1)
339
- .where((attr, op) => op.gte(attr.followerCount, 1))
340
- .dbParams()
341
- )
342
- .addUpdate(
343
- table.entities.User.update({ username: 'bob' })
344
- .add('followingCount', -1)
345
- .where((attr, op) => op.gte(attr.followingCount, 1))
346
- .dbParams()
347
- )
348
- .dbParams();
349
-
350
- expect(params.TransactItems).toHaveLength(3);
351
- });
352
-
353
- test('Builder immutability - chaining creates new instances', () => {
354
- const builder1 = table.transactWrite();
355
- const builder2 = builder1.addPut(
356
- table.entities.User.put({
357
- username: 'alice',
358
- name: 'Alice',
359
- }).dbParams()
360
- );
361
- const builder3 = builder2.addUpdate(
362
- table.entities.User.update({ username: 'bob' }).add('followerCount', 1).dbParams()
363
- );
364
-
365
- // Original builder unchanged
366
- expect(builder1.dbParams().TransactItems).toHaveLength(0);
367
- expect(builder2.dbParams().TransactItems).toHaveLength(1);
368
- expect(builder3.dbParams().TransactItems).toHaveLength(2);
369
- });
370
- });
371
-
372
- describe('TransactGet - Real World Use Cases', () => {
373
- test('Get User + Photo atomically', () => {
374
- const params = table
375
- .transactGet()
376
- .addGet(table.entities.User.get({ username: 'alice' }).dbParams())
377
- .addGet(
378
- table.entities.Photo.get({
379
- username: 'alice',
380
- photoId: 'photo123',
381
- }).dbParams()
382
- )
383
- .dbParams();
384
-
385
- expect(params.TransactItems).toHaveLength(2);
386
-
387
- // Get User
388
- expect(params.TransactItems[0]?.Get.Key).toEqual({
389
- PK: 'USER#alice',
390
- SK: 'USER#alice',
391
- });
392
-
393
- // Get Photo
394
- expect(params.TransactItems[1]?.Get.Key).toEqual({
395
- PK: 'UP#alice',
396
- SK: 'PHOTO#photo123',
397
- });
398
- });
399
-
400
- test('Get Follow relationship + both Users', () => {
401
- const params = table
402
- .transactGet()
403
- .addGet(
404
- table.entities.Follow.get({
405
- followedUsername: 'alice',
406
- followingUsername: 'bob',
407
- }).dbParams()
408
- )
409
- .addGet(table.entities.User.get({ username: 'alice' }).dbParams())
410
- .addGet(table.entities.User.get({ username: 'bob' }).dbParams())
411
- .dbParams();
412
-
413
- expect(params.TransactItems).toHaveLength(3);
414
-
415
- // Follow
416
- expect(params.TransactItems[0]?.Get.Key).toEqual({
417
- PK: 'FOLLOW#alice',
418
- SK: 'FOLLOW#bob',
419
- });
420
-
421
- // Users
422
- expect(params.TransactItems[1]?.Get.Key.PK).toBe('USER#alice');
423
- expect(params.TransactItems[2]?.Get.Key.PK).toBe('USER#bob');
424
- });
425
-
426
- test('Get Photo + Like + Comment atomically', () => {
427
- const params = table
428
- .transactGet()
429
- .addGet(
430
- table.entities.Photo.get({
431
- username: 'alice',
432
- photoId: 'photo123',
433
- }).dbParams()
434
- )
435
- .addGet(
436
- table.entities.Like.get({
437
- photoId: 'photo123',
438
- likingUsername: 'bob',
439
- }).dbParams()
440
- )
441
- .addGet(
442
- table.entities.Comment.get({
443
- photoId: 'photo123',
444
- commentId: 'comment456',
445
- }).dbParams()
446
- )
447
- .dbParams();
448
-
449
- expect(params.TransactItems).toHaveLength(3);
450
-
451
- // Photo
452
- expect(params.TransactItems[0]?.Get.Key).toEqual({
453
- PK: 'UP#alice',
454
- SK: 'PHOTO#photo123',
455
- });
456
-
457
- // Like
458
- expect(params.TransactItems[1]?.Get.Key).toEqual({
459
- PK: 'PL#photo123',
460
- SK: 'LIKE#bob',
461
- });
462
-
463
- // Comment
464
- expect(params.TransactItems[2]?.Get.Key).toEqual({
465
- PK: 'PC#photo123',
466
- SK: 'COMMENT#comment456',
467
- });
468
- });
469
-
470
- test('Builder immutability - chaining creates new instances', () => {
471
- const builder1 = table.transactGet();
472
- const builder2 = builder1.addGet(table.entities.User.get({ username: 'alice' }).dbParams());
473
- const builder3 = builder2.addGet(table.entities.User.get({ username: 'bob' }).dbParams());
474
-
475
- // Original builder unchanged
476
- expect(builder1.dbParams().TransactItems).toHaveLength(0);
477
- expect(builder2.dbParams().TransactItems).toHaveLength(1);
478
- expect(builder3.dbParams().TransactItems).toHaveLength(2);
479
- });
480
- });
481
-
482
- describe('Mixed Transaction Scenarios', () => {
483
- test('Complex social interaction - Like with notification', () => {
484
- // Like photo + increment count + update follower count
485
- const params = table
486
- .transactWrite()
487
- .addPut(
488
- table.entities.Like.put({
489
- photoId: 'photo123',
490
- likingUsername: 'charlie',
491
- })
492
- .ifNotExists()
493
- .dbParams()
494
- )
495
- .addUpdate(
496
- table.entities.Photo.update({
497
- username: 'alice',
498
- photoId: 'photo123',
499
- })
500
- .add('likesCount', 1)
501
- .dbParams()
502
- )
503
- .addUpdate(
504
- table.entities.User.update({ username: 'alice' })
505
- .add('followerCount', 0) // Just to demonstrate multiple updates
506
- .dbParams()
507
- )
508
- .dbParams();
509
-
510
- expect(params.TransactItems).toHaveLength(3);
511
- });
512
-
513
- test('Client request token for idempotency', () => {
514
- const params = table
515
- .transactWrite()
516
- .addPut(
517
- table.entities.User.put({
518
- username: 'alice',
519
- name: 'Alice',
520
- }).dbParams()
521
- )
522
- .withClientRequestToken('unique-request-id-12345')
523
- .dbParams();
524
-
525
- expect(params.ClientRequestToken).toBe('unique-request-id-12345');
526
- });
527
- });
528
- });
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "extends": "../typescript-config/base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "baseUrl": ".",
6
- "paths": {
7
- "@/*": ["src/*"]
8
- }
9
- },
10
- "include": ["src", "eslint.config.mjs"],
11
- "exclude": ["**/*.test.ts", "node_modules", "dist"]
12
- }