@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,167 @@
1
+ # TransactGet - Atomic Multi-Item Reads
2
+
3
+ TransactGet provides atomic multi-item read operations in DynamoDB. Get multiple items with snapshot isolation.
4
+
5
+ ## Features
6
+
7
+ - **Functional API**: Immutable builders using pure functions
8
+ - **Type-safe**: Full TypeScript support
9
+ - **Atomic**: Snapshot isolation across items
10
+ - **Composable**: Chain operations fluently
11
+
12
+ ## Usage
13
+
14
+ ### Basic Example
15
+
16
+ ```typescript
17
+ import { createTransactGetBuilder } from '@repo/core';
18
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
19
+
20
+ const client = new DynamoDBClient({});
21
+
22
+ const [item1, item2] = await createTransactGetBuilder(client)
23
+ .addGet(getParams1)
24
+ .addGet(getParams2)
25
+ .execute();
26
+ ```
27
+
28
+ ### With Table API
29
+
30
+ ```typescript
31
+ const table = new Table({
32
+ name: 'MyTable',
33
+ client: new DynamoDBClient({}),
34
+ schema: MySchema,
35
+ });
36
+
37
+ // Get user and photo atomically
38
+ const [user, photo] = await table
39
+ .transactGet()
40
+ .addGet(table.entities.User.get({ username: 'alice' }).dbParams())
41
+ .addGet(
42
+ table.entities.Photo.get({
43
+ username: 'alice',
44
+ photoId: '123',
45
+ }).dbParams()
46
+ )
47
+ .execute();
48
+ ```
49
+
50
+ ## Real-World Examples
51
+
52
+ ### Get User + Photo + Comment
53
+
54
+ ```typescript
55
+ const [user, photo, comment] = await table
56
+ .transactGet()
57
+ .addGet(table.entities.User.get({ username: 'alice' }).dbParams())
58
+ .addGet(
59
+ table.entities.Photo.get({
60
+ username: 'alice',
61
+ photoId: '123',
62
+ }).dbParams()
63
+ )
64
+ .addGet(
65
+ table.entities.Comment.get({
66
+ photoId: '123',
67
+ commentId: '456',
68
+ }).dbParams()
69
+ )
70
+ .execute();
71
+ ```
72
+
73
+ ### Verify Follow Relationship
74
+
75
+ ```typescript
76
+ // Get follow relationship and both users atomically
77
+ const [follow, follower, followed] = await table
78
+ .transactGet()
79
+ .addGet(
80
+ table.entities.Follow.get({
81
+ followedUsername: 'alice',
82
+ followingUsername: 'bob',
83
+ }).dbParams()
84
+ )
85
+ .addGet(table.entities.User.get({ username: 'bob' }).dbParams())
86
+ .addGet(table.entities.User.get({ username: 'alice' }).dbParams())
87
+ .execute();
88
+
89
+ // Verify counters match
90
+ console.log(follower.followingCount); // Includes alice
91
+ console.log(followed.followerCount); // Includes bob
92
+ ```
93
+
94
+ ### Get Photo with Engagement
95
+
96
+ ```typescript
97
+ const [photo, like, comment] = await table
98
+ .transactGet()
99
+ .addGet(
100
+ table.entities.Photo.get({
101
+ username: 'alice',
102
+ photoId: '123',
103
+ }).dbParams()
104
+ )
105
+ .addGet(
106
+ table.entities.Like.get({
107
+ photoId: '123',
108
+ likingUsername: 'bob',
109
+ }).dbParams()
110
+ )
111
+ .addGet(
112
+ table.entities.Comment.get({
113
+ photoId: '123',
114
+ commentId: '456',
115
+ }).dbParams()
116
+ )
117
+ .execute();
118
+ ```
119
+
120
+ ## With Projections
121
+
122
+ ```typescript
123
+ const [user] = await table
124
+ .transactGet()
125
+ .addGet({
126
+ TableName: 'MyTable',
127
+ Key: { pk: 'USER#alice', sk: 'USER#alice' },
128
+ ProjectionExpression: 'username, name, followerCount',
129
+ ExpressionAttributeNames: {
130
+ '#username': 'username',
131
+ '#name': 'name',
132
+ '#followerCount': 'followerCount',
133
+ },
134
+ })
135
+ .execute();
136
+ ```
137
+
138
+ ## Functional Design
139
+
140
+ The builder is immutable - each method returns a new instance:
141
+
142
+ ```typescript
143
+ const builder1 = table.transactGet();
144
+ const builder2 = builder1.addGet(params1);
145
+ const builder3 = builder2.addGet(params2);
146
+
147
+ builder1.dbParams().TransactItems.length; // 0
148
+ builder2.dbParams().TransactItems.length; // 1
149
+ builder3.dbParams().TransactItems.length; // 2
150
+ ```
151
+
152
+ ## Return Value
153
+
154
+ `execute()` returns an array of items in the same order as the `addGet()` calls:
155
+
156
+ ```typescript
157
+ const [user, photo, comment] = await builder.execute();
158
+ ```
159
+
160
+ If an item doesn't exist, the corresponding array element will be `undefined`.
161
+
162
+ ## Limitations
163
+
164
+ - Maximum **25 items** per transaction
165
+ - Maximum **4 MB** total size
166
+ - Single table only
167
+ - Provides snapshot isolation (consistent reads across items)
@@ -0,0 +1,239 @@
1
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2
+ import { createTransactGetBuilder } from './create-transact-get-builder';
3
+
4
+ const client = new DynamoDBClient({});
5
+
6
+ describe('TransactGet Builder - Functional API', () => {
7
+ test('should create empty transaction', () => {
8
+ const params = createTransactGetBuilder(client).dbParams();
9
+
10
+ expect(params).toEqual({
11
+ TransactItems: [],
12
+ });
13
+ });
14
+
15
+ test('should add single Get operation', () => {
16
+ const getParams = {
17
+ TableName: 'InstagramClone',
18
+ Key: {
19
+ pk: 'USER#alice',
20
+ sk: 'USER#alice',
21
+ },
22
+ };
23
+
24
+ const params = createTransactGetBuilder(client).addGet(getParams).dbParams();
25
+
26
+ expect(params).toEqual({
27
+ TransactItems: [{ Get: getParams }],
28
+ });
29
+ });
30
+
31
+ test('should chain multiple Get operations', () => {
32
+ const getUserParams = {
33
+ TableName: 'InstagramClone',
34
+ Key: { pk: 'USER#alice', sk: 'USER#alice' },
35
+ };
36
+
37
+ const getPhotoParams = {
38
+ TableName: 'InstagramClone',
39
+ Key: { pk: 'UP#alice', sk: 'PHOTO#photo123' },
40
+ };
41
+
42
+ const params = createTransactGetBuilder(client)
43
+ .addGet(getUserParams)
44
+ .addGet(getPhotoParams)
45
+ .dbParams();
46
+
47
+ expect(params).toEqual({
48
+ TransactItems: [{ Get: getUserParams }, { Get: getPhotoParams }],
49
+ });
50
+ expect(params.TransactItems).toHaveLength(2);
51
+ });
52
+
53
+ test('should get User + Photo + Comment atomically', () => {
54
+ const getUserParams = {
55
+ TableName: 'InstagramClone',
56
+ Key: { pk: 'USER#alice', sk: 'USER#alice' },
57
+ };
58
+
59
+ const getPhotoParams = {
60
+ TableName: 'InstagramClone',
61
+ Key: { pk: 'UP#alice', sk: 'PHOTO#photo123' },
62
+ };
63
+
64
+ const getCommentParams = {
65
+ TableName: 'InstagramClone',
66
+ Key: { pk: 'PC#photo123', sk: 'COMMENT#comment456' },
67
+ };
68
+
69
+ const params = createTransactGetBuilder(client)
70
+ .addGet(getUserParams)
71
+ .addGet(getPhotoParams)
72
+ .addGet(getCommentParams)
73
+ .dbParams();
74
+
75
+ expect(params.TransactItems).toHaveLength(3);
76
+ expect(params.TransactItems[0]).toEqual({ Get: getUserParams });
77
+ expect(params.TransactItems[1]).toEqual({ Get: getPhotoParams });
78
+ expect(params.TransactItems[2]).toEqual({ Get: getCommentParams });
79
+ });
80
+
81
+ test('should support projection expressions', () => {
82
+ const getParams = {
83
+ TableName: 'InstagramClone',
84
+ Key: { pk: 'USER#alice', sk: 'USER#alice' },
85
+ ProjectionExpression: 'username, name, followerCount',
86
+ ExpressionAttributeNames: {
87
+ '#username': 'username',
88
+ '#name': 'name',
89
+ '#followerCount': 'followerCount',
90
+ },
91
+ };
92
+
93
+ const params = createTransactGetBuilder(client).addGet(getParams).dbParams();
94
+
95
+ expect(params.TransactItems[0]?.Get).toEqual(getParams);
96
+ expect(params.TransactItems[0]?.Get.ProjectionExpression).toBe('username, name, followerCount');
97
+ });
98
+
99
+ test('should get Follow relationship + both Users', () => {
100
+ const getFollowParams = {
101
+ TableName: 'InstagramClone',
102
+ Key: { pk: 'FOLLOW#alice', sk: 'FOLLOW#bob' },
103
+ };
104
+
105
+ const getFollowerParams = {
106
+ TableName: 'InstagramClone',
107
+ Key: { pk: 'USER#bob', sk: 'USER#bob' },
108
+ };
109
+
110
+ const getFollowedParams = {
111
+ TableName: 'InstagramClone',
112
+ Key: { pk: 'USER#alice', sk: 'USER#alice' },
113
+ };
114
+
115
+ const params = createTransactGetBuilder(client)
116
+ .addGet(getFollowParams)
117
+ .addGet(getFollowerParams)
118
+ .addGet(getFollowedParams)
119
+ .dbParams();
120
+
121
+ expect(params.TransactItems).toHaveLength(3);
122
+ });
123
+
124
+ test('should preserve immutability - original builder unchanged', () => {
125
+ const builder1 = createTransactGetBuilder(client);
126
+ const getParams1 = {
127
+ TableName: 'InstagramClone',
128
+ Key: { pk: 'USER#alice', sk: 'USER#alice' },
129
+ };
130
+ const getParams2 = {
131
+ TableName: 'InstagramClone',
132
+ Key: { pk: 'USER#bob', sk: 'USER#bob' },
133
+ };
134
+
135
+ const builder2 = builder1.addGet(getParams1);
136
+ const builder3 = builder2.addGet(getParams2);
137
+
138
+ // Each builder should be independent
139
+ expect(builder1.dbParams().TransactItems).toHaveLength(0);
140
+ expect(builder2.dbParams().TransactItems).toHaveLength(1);
141
+ expect(builder3.dbParams().TransactItems).toHaveLength(2);
142
+ });
143
+
144
+ test('should handle max operations (25 items limit)', () => {
145
+ let builder = createTransactGetBuilder(client);
146
+
147
+ // Add 25 Get operations (DynamoDB limit for TransactGet)
148
+ for (let i = 0; i < 25; i++) {
149
+ builder = builder.addGet({
150
+ TableName: 'InstagramClone',
151
+ Key: { pk: `USER#user${i}`, sk: `USER#user${i}` },
152
+ });
153
+ }
154
+
155
+ const params = builder.dbParams();
156
+ expect((params.TransactItems as any).length).toBe(25);
157
+ });
158
+
159
+ test('should get multiple Likes for a Photo', () => {
160
+ const params = createTransactGetBuilder(client)
161
+ .addGet({
162
+ TableName: 'InstagramClone',
163
+ Key: { pk: 'PL#photo123', sk: 'LIKE#alice' },
164
+ })
165
+ .addGet({
166
+ TableName: 'InstagramClone',
167
+ Key: { pk: 'PL#photo123', sk: 'LIKE#bob' },
168
+ })
169
+ .addGet({
170
+ TableName: 'InstagramClone',
171
+ Key: { pk: 'PL#photo123', sk: 'LIKE#charlie' },
172
+ })
173
+ .dbParams();
174
+
175
+ expect(params.TransactItems).toHaveLength(3);
176
+ expect(params.TransactItems.every((item: any) => item.Get.Key.pk === 'PL#photo123')).toBe(true);
177
+ });
178
+
179
+ test('should get Photo + its Comments', () => {
180
+ const params = createTransactGetBuilder(client)
181
+ .addGet({
182
+ TableName: 'InstagramClone',
183
+ Key: { pk: 'UP#alice', sk: 'PHOTO#photo123' },
184
+ })
185
+ .addGet({
186
+ TableName: 'InstagramClone',
187
+ Key: { pk: 'PC#photo123', sk: 'COMMENT#comment1' },
188
+ })
189
+ .addGet({
190
+ TableName: 'InstagramClone',
191
+ Key: { pk: 'PC#photo123', sk: 'COMMENT#comment2' },
192
+ })
193
+ .dbParams();
194
+
195
+ expect(params.TransactItems).toHaveLength(3);
196
+ });
197
+
198
+ test('should build complex cross-entity read', () => {
199
+ // Get User + their Photo + Like on that photo + Comment on that photo
200
+ const params = createTransactGetBuilder(client)
201
+ .addGet({
202
+ TableName: 'InstagramClone',
203
+ Key: { pk: 'USER#alice', sk: 'USER#alice' },
204
+ })
205
+ .addGet({
206
+ TableName: 'InstagramClone',
207
+ Key: { pk: 'UP#alice', sk: 'PHOTO#photo123' },
208
+ })
209
+ .addGet({
210
+ TableName: 'InstagramClone',
211
+ Key: { pk: 'PL#photo123', sk: 'LIKE#bob' },
212
+ })
213
+ .addGet({
214
+ TableName: 'InstagramClone',
215
+ Key: { pk: 'PC#photo123', sk: 'COMMENT#comment456' },
216
+ })
217
+ .dbParams();
218
+
219
+ expect(params.TransactItems).toHaveLength(4);
220
+
221
+ // Verify each entity type
222
+ expect(params.TransactItems[0]?.Get.Key).toEqual({
223
+ pk: 'USER#alice',
224
+ sk: 'USER#alice',
225
+ });
226
+ expect(params.TransactItems[1]?.Get.Key).toEqual({
227
+ pk: 'UP#alice',
228
+ sk: 'PHOTO#photo123',
229
+ });
230
+ expect(params.TransactItems[2]?.Get.Key).toEqual({
231
+ pk: 'PL#photo123',
232
+ sk: 'LIKE#bob',
233
+ });
234
+ expect(params.TransactItems[3]?.Get.Key).toEqual({
235
+ pk: 'PC#photo123',
236
+ sk: 'COMMENT#comment456',
237
+ });
238
+ });
239
+ });
@@ -0,0 +1,67 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
+ import { TransactGetCommand } from '@aws-sdk/lib-dynamodb';
4
+ import { TransactGetBuilder, TransactGetState } from './types';
5
+
6
+ /**
7
+ * Creates the initial state for a TransactGet builder
8
+ */
9
+ const createInitialState = (client: DynamoDBClient): TransactGetState => ({
10
+ client,
11
+ items: [],
12
+ });
13
+
14
+ /**
15
+ * Adds a Get operation to the transaction
16
+ */
17
+ const addGetItem =
18
+ (state: TransactGetState) =>
19
+ (params: any): TransactGetState => ({
20
+ ...state,
21
+ items: [...state.items, { Get: params }],
22
+ });
23
+
24
+ /**
25
+ * Converts the builder state to DynamoDB parameters
26
+ */
27
+ const toDbParams = (state: TransactGetState) => ({
28
+ TransactItems: [...state.items] as any,
29
+ });
30
+
31
+ /**
32
+ * Executes the transaction and returns the items
33
+ */
34
+ const execute = async (state: TransactGetState): Promise<any[]> => {
35
+ const params = toDbParams(state);
36
+ const command = new TransactGetCommand(params);
37
+ const response = await state.client.send(command);
38
+ return response.Responses?.map((r: any) => r.Item) || [];
39
+ };
40
+
41
+ /**
42
+ * Creates a builder from the current state
43
+ */
44
+ const createBuilder = (state: TransactGetState): TransactGetBuilder => ({
45
+ addGet: (params: any) => createBuilder(addGetItem(state)(params)),
46
+ dbParams: () => toDbParams(state),
47
+ execute: () => execute(state),
48
+ });
49
+
50
+ /**
51
+ * Creates a new TransactGet builder
52
+ *
53
+ * @param client - DynamoDB client instance
54
+ * @returns A TransactGetBuilder
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const [user, photo] = await createTransactGetBuilder(client)
59
+ * .addGet(getUserParams)
60
+ * .addGet(getPhotoParams)
61
+ * .execute();
62
+ * ```
63
+ */
64
+ export const createTransactGetBuilder = (client: DynamoDBClient): TransactGetBuilder => {
65
+ const initialState = createInitialState(client);
66
+ return createBuilder(initialState);
67
+ };
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export * from './create-transact-get-builder';
@@ -0,0 +1,31 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
+
4
+ /**
5
+ * Represents a single Get operation in a TransactGet
6
+ */
7
+ export type TransactGetItem = {
8
+ Get: {
9
+ TableName: string;
10
+ Key: Record<string, any>;
11
+ ProjectionExpression?: string;
12
+ ExpressionAttributeNames?: Record<string, string>;
13
+ };
14
+ };
15
+
16
+ /**
17
+ * State for the TransactGet builder
18
+ */
19
+ export type TransactGetState = {
20
+ readonly client: DynamoDBClient;
21
+ readonly items: readonly TransactGetItem[];
22
+ };
23
+
24
+ /**
25
+ * TransactGet builder interface
26
+ */
27
+ export type TransactGetBuilder = {
28
+ readonly addGet: (params: any) => TransactGetBuilder;
29
+ readonly dbParams: () => any;
30
+ readonly execute: () => Promise<any[]>;
31
+ };
@@ -0,0 +1,166 @@
1
+ # TransactWrite - Atomic Multi-Item Writes
2
+
3
+ TransactWrite provides atomic multi-item write operations in DynamoDB. All operations in a transaction either succeed together or fail together.
4
+
5
+ ## Features
6
+
7
+ - **Functional API**: Immutable builders using pure functions
8
+ - **Type-safe**: Full TypeScript support
9
+ - **Composable**: Chain operations fluently
10
+ - **Idempotent**: Support for client request tokens
11
+
12
+ ## Usage
13
+
14
+ ### Basic Example
15
+
16
+ ```typescript
17
+ import { createTransactWriteBuilder } from '@repo/core';
18
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
19
+
20
+ const client = new DynamoDBClient({});
21
+
22
+ await createTransactWriteBuilder(client).addPut(putParams).addUpdate(updateParams).execute();
23
+ ```
24
+
25
+ ### With Table API
26
+
27
+ ```typescript
28
+ const table = new Table({
29
+ name: 'MyTable',
30
+ client: new DynamoDBClient({}),
31
+ schema: MySchema,
32
+ });
33
+
34
+ // Like a photo atomically
35
+ await table
36
+ .transactWrite()
37
+ .addPut(
38
+ table.entities.Like.put({
39
+ photoId: '123',
40
+ likingUsername: 'alice',
41
+ })
42
+ .ifNotExists()
43
+ .dbParams()
44
+ )
45
+ .addUpdate(
46
+ table.entities.Photo.update({
47
+ username: 'bob',
48
+ photoId: '123',
49
+ })
50
+ .add('likesCount', 1)
51
+ .dbParams()
52
+ )
53
+ .execute();
54
+ ```
55
+
56
+ ## Real-World Examples
57
+
58
+ ### Follow a User
59
+
60
+ ```typescript
61
+ await table
62
+ .transactWrite()
63
+ .addPut(
64
+ table.entities.Follow.put({
65
+ followedUsername: 'alice',
66
+ followingUsername: 'bob',
67
+ })
68
+ .ifNotExists()
69
+ .dbParams()
70
+ )
71
+ .addUpdate(table.entities.User.update({ username: 'alice' }).add('followerCount', 1).dbParams())
72
+ .addUpdate(table.entities.User.update({ username: 'bob' }).add('followingCount', 1).dbParams())
73
+ .execute();
74
+ ```
75
+
76
+ ### Comment on Photo
77
+
78
+ ```typescript
79
+ await table
80
+ .transactWrite()
81
+ .addPut(
82
+ table.entities.Comment.put({
83
+ photoId: '123',
84
+ commentingUsername: 'charlie',
85
+ content: 'Great photo!',
86
+ }).dbParams()
87
+ )
88
+ .addUpdate(
89
+ table.entities.Photo.update({
90
+ username: 'alice',
91
+ photoId: '123',
92
+ })
93
+ .add('commentCount', 1)
94
+ .dbParams()
95
+ )
96
+ .execute();
97
+ ```
98
+
99
+ ### Delete with Safety Check
100
+
101
+ ```typescript
102
+ // Only delete if photo has no engagement
103
+ await table
104
+ .transactWrite()
105
+ .addDelete(
106
+ table.entities.Photo.delete({
107
+ username: 'alice',
108
+ photoId: '123',
109
+ })
110
+ .where((attr, op) => op.and(op.eq(attr.likesCount, 0), op.eq(attr.commentCount, 0)))
111
+ .dbParams()
112
+ )
113
+ .execute();
114
+ ```
115
+
116
+ ### Condition Check
117
+
118
+ ```typescript
119
+ // Create user only if admin exists
120
+ await table
121
+ .transactWrite()
122
+ .addPut(
123
+ table.entities.User.put({
124
+ username: 'newuser',
125
+ name: 'New User',
126
+ })
127
+ .ifNotExists()
128
+ .dbParams()
129
+ )
130
+ .addConditionCheck({
131
+ TableName: 'MyTable',
132
+ Key: { pk: 'USER#admin', sk: 'USER#admin' },
133
+ ConditionExpression: 'attribute_exists(#pk)',
134
+ ExpressionAttributeNames: { '#pk': 'pk' },
135
+ })
136
+ .execute();
137
+ ```
138
+
139
+ ## Idempotency
140
+
141
+ Use client request tokens for idempotent operations:
142
+
143
+ ```typescript
144
+ await table.transactWrite().addPut(params).withClientRequestToken('unique-id-12345').execute();
145
+ ```
146
+
147
+ ## Functional Design
148
+
149
+ The builder is immutable - each method returns a new instance:
150
+
151
+ ```typescript
152
+ const builder1 = table.transactWrite();
153
+ const builder2 = builder1.addPut(params1);
154
+ const builder3 = builder2.addPut(params2);
155
+
156
+ builder1.dbParams().TransactItems.length; // 0
157
+ builder2.dbParams().TransactItems.length; // 1
158
+ builder3.dbParams().TransactItems.length; // 2
159
+ ```
160
+
161
+ ## Limitations
162
+
163
+ - Maximum **100 items** per transaction
164
+ - Maximum **4 MB** total size
165
+ - Single table only
166
+ - May fail with `TransactionCanceledException` if conditions fail