@ftschopp/dynatable-core 1.0.0

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