@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,333 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
+ import { createUpdateBuilder } from './create-update-builder';
4
+
5
+ describe('UpdateBuilder', () => {
6
+ const client = new DynamoDBClient({});
7
+ const tableName = 'TestTable';
8
+
9
+ interface TestModel {
10
+ pk: string;
11
+ sk: string;
12
+ name?: string;
13
+ age?: number;
14
+ score?: number;
15
+ tags?: string[];
16
+ followerCount?: number;
17
+ }
18
+
19
+ describe('SET operations', () => {
20
+ test('should build params with single SET operation', () => {
21
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
22
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
23
+ .set('name', 'John Doe')
24
+ .dbParams();
25
+
26
+ expect(params.TableName).toBe(tableName);
27
+ expect(params.Key).toEqual(key);
28
+ expect(params.UpdateExpression).toBe('SET #name = :name_0');
29
+ expect(params.ExpressionAttributeNames).toEqual({ '#name': 'name' });
30
+ expect(params.ExpressionAttributeValues).toEqual({
31
+ ':name_0': 'John Doe',
32
+ });
33
+ });
34
+
35
+ test('should build params with multiple SET operations', () => {
36
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
37
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
38
+ .set('name', 'John Doe')
39
+ .set('age', 30)
40
+ .set('score', 100)
41
+ .dbParams();
42
+
43
+ expect(params.UpdateExpression).toBe('SET #name = :name_0, #age = :age_1, #score = :score_2');
44
+ expect(params.ExpressionAttributeNames).toEqual({
45
+ '#name': 'name',
46
+ '#age': 'age',
47
+ '#score': 'score',
48
+ });
49
+ expect(params.ExpressionAttributeValues).toEqual({
50
+ ':name_0': 'John Doe',
51
+ ':age_1': 30,
52
+ ':score_2': 100,
53
+ });
54
+ });
55
+ });
56
+
57
+ describe('REMOVE operations', () => {
58
+ test('should build params with single REMOVE operation', () => {
59
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
60
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
61
+ .remove('age')
62
+ .dbParams();
63
+
64
+ expect(params.UpdateExpression).toBe('REMOVE #age');
65
+ expect(params.ExpressionAttributeNames).toEqual({ '#age': 'age' });
66
+ expect(params.ExpressionAttributeValues).toBeUndefined();
67
+ });
68
+
69
+ test('should build params with multiple REMOVE operations', () => {
70
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
71
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
72
+ .remove('age')
73
+ .remove('score')
74
+ .dbParams();
75
+
76
+ expect(params.UpdateExpression).toBe('REMOVE #age, #score');
77
+ expect(params.ExpressionAttributeNames).toEqual({
78
+ '#age': 'age',
79
+ '#score': 'score',
80
+ });
81
+ });
82
+ });
83
+
84
+ describe('ADD operations', () => {
85
+ test('should build params with single ADD operation', () => {
86
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
87
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
88
+ .add('followerCount', 1)
89
+ .dbParams();
90
+
91
+ expect(params.UpdateExpression).toBe('ADD #followerCount :followerCount_0');
92
+ expect(params.ExpressionAttributeNames).toEqual({
93
+ '#followerCount': 'followerCount',
94
+ });
95
+ expect(params.ExpressionAttributeValues).toEqual({
96
+ ':followerCount_0': 1,
97
+ });
98
+ });
99
+
100
+ test('should build params with multiple ADD operations', () => {
101
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
102
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
103
+ .add('followerCount', 1)
104
+ .add('score', 10)
105
+ .dbParams();
106
+
107
+ expect(params.UpdateExpression).toBe('ADD #followerCount :followerCount_0, #score :score_1');
108
+ expect(params.ExpressionAttributeNames).toEqual({
109
+ '#followerCount': 'followerCount',
110
+ '#score': 'score',
111
+ });
112
+ expect(params.ExpressionAttributeValues).toEqual({
113
+ ':followerCount_0': 1,
114
+ ':score_1': 10,
115
+ });
116
+ });
117
+ });
118
+
119
+ describe('DELETE operations', () => {
120
+ test('should build params with single DELETE operation', () => {
121
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
122
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
123
+ .delete('tags', ['inactive'])
124
+ .dbParams();
125
+
126
+ expect(params.UpdateExpression).toBe('DELETE #tags :tags_0');
127
+ expect(params.ExpressionAttributeNames).toEqual({ '#tags': 'tags' });
128
+ expect(params.ExpressionAttributeValues).toEqual({
129
+ ':tags_0': ['inactive'],
130
+ });
131
+ });
132
+
133
+ test('should build params with multiple DELETE operations', () => {
134
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
135
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
136
+ .delete('tags', ['inactive'])
137
+ .delete('tags', ['old'])
138
+ .dbParams();
139
+
140
+ expect(params.UpdateExpression).toBe('DELETE #tags :tags_0, #tags :tags_1');
141
+ expect(params.ExpressionAttributeNames).toEqual({ '#tags': 'tags' });
142
+ expect(params.ExpressionAttributeValues).toEqual({
143
+ ':tags_0': ['inactive'],
144
+ ':tags_1': ['old'],
145
+ });
146
+ });
147
+ });
148
+
149
+ describe('Mixed operations', () => {
150
+ test('should build params with SET and REMOVE', () => {
151
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
152
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
153
+ .set('name', 'John Doe')
154
+ .remove('age')
155
+ .dbParams();
156
+
157
+ expect(params.UpdateExpression).toBe('SET #name = :name_0 REMOVE #age');
158
+ expect(params.ExpressionAttributeNames).toEqual({
159
+ '#name': 'name',
160
+ '#age': 'age',
161
+ });
162
+ expect(params.ExpressionAttributeValues).toEqual({
163
+ ':name_0': 'John Doe',
164
+ });
165
+ });
166
+
167
+ test('should build params with all operation types', () => {
168
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
169
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
170
+ .set('name', 'John Doe')
171
+ .remove('age')
172
+ .add('followerCount', 1)
173
+ .delete('tags', ['old'])
174
+ .dbParams();
175
+
176
+ expect(params.UpdateExpression).toBe(
177
+ 'SET #name = :name_0 REMOVE #age ADD #followerCount :followerCount_1 DELETE #tags :tags_2'
178
+ );
179
+ expect(params.ExpressionAttributeNames).toEqual({
180
+ '#name': 'name',
181
+ '#age': 'age',
182
+ '#followerCount': 'followerCount',
183
+ '#tags': 'tags',
184
+ });
185
+ expect(params.ExpressionAttributeValues).toEqual({
186
+ ':name_0': 'John Doe',
187
+ ':followerCount_1': 1,
188
+ ':tags_2': ['old'],
189
+ });
190
+ });
191
+ });
192
+
193
+ describe('Condition expressions', () => {
194
+ test('should build params with where condition', () => {
195
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
196
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
197
+ .set('name', 'John Doe')
198
+ .where((attr, op) => op.eq(attr.age, 25))
199
+ .dbParams();
200
+
201
+ expect(params.UpdateExpression).toBe('SET #name = :name_0');
202
+ expect(params.ConditionExpression).toMatch(/#age = :age_\d+/);
203
+ expect(params.ExpressionAttributeNames).toEqual({
204
+ '#name': 'name',
205
+ '#age': 'age',
206
+ });
207
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain('John Doe');
208
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain(25);
209
+ });
210
+
211
+ test('should build params with multiple where conditions using AND', () => {
212
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
213
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
214
+ .set('name', 'John Doe')
215
+ .where((attr, op) => op.and(op.gt(attr.age, 18), op.lt(attr.age, 65)))
216
+ .dbParams();
217
+
218
+ expect(params.ConditionExpression).toMatch(/\(#age > :age_\d+\) AND \(#age < :age_\d+\)/);
219
+ expect(params.ExpressionAttributeNames).toEqual({
220
+ '#name': 'name',
221
+ '#age': 'age',
222
+ });
223
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain(18);
224
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain(65);
225
+ });
226
+
227
+ test('should build params with OR conditions', () => {
228
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
229
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
230
+ .set('name', 'John Doe')
231
+ .where((attr, op) => op.or(op.eq(attr.age, 25), op.eq(attr.age, 30)))
232
+ .dbParams();
233
+
234
+ expect(params.ConditionExpression).toMatch(/\(#age = :age_\d+\) OR \(#age = :age_\d+\)/);
235
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain(25);
236
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain(30);
237
+ });
238
+ });
239
+
240
+ describe('ReturnValues', () => {
241
+ test('should default to NONE', () => {
242
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
243
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
244
+ .set('name', 'John Doe')
245
+ .dbParams();
246
+
247
+ expect(params.ReturnValues).toBeUndefined();
248
+ });
249
+
250
+ test('should support ALL_OLD', () => {
251
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
252
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
253
+ .set('name', 'John Doe')
254
+ .returning('ALL_OLD')
255
+ .dbParams();
256
+
257
+ expect(params.ReturnValues).toBe('ALL_OLD');
258
+ });
259
+
260
+ test('should support ALL_NEW', () => {
261
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
262
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
263
+ .set('name', 'John Doe')
264
+ .returning('ALL_NEW')
265
+ .dbParams();
266
+
267
+ expect(params.ReturnValues).toBe('ALL_NEW');
268
+ });
269
+
270
+ test('should support UPDATED_OLD', () => {
271
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
272
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
273
+ .set('name', 'John Doe')
274
+ .returning('UPDATED_OLD')
275
+ .dbParams();
276
+
277
+ expect(params.ReturnValues).toBe('UPDATED_OLD');
278
+ });
279
+
280
+ test('should support UPDATED_NEW', () => {
281
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
282
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
283
+ .set('name', 'John Doe')
284
+ .returning('UPDATED_NEW')
285
+ .dbParams();
286
+
287
+ expect(params.ReturnValues).toBe('UPDATED_NEW');
288
+ });
289
+ });
290
+
291
+ describe('Immutability', () => {
292
+ test('should create new builder instance on each method call', () => {
293
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
294
+ const builder1 = createUpdateBuilder<TestModel>(tableName, key, client);
295
+ const builder2 = builder1.set('name', 'John');
296
+ const builder3 = builder2.set('age', 30);
297
+
298
+ expect(builder1.dbParams().UpdateExpression).toBeUndefined();
299
+ expect(builder2.dbParams().UpdateExpression).toBe('SET #name = :name_0');
300
+ expect(builder3.dbParams().UpdateExpression).toBe('SET #name = :name_0, #age = :age_1');
301
+ });
302
+ });
303
+
304
+ describe('Complex scenarios', () => {
305
+ test('should handle complex update with all features', () => {
306
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
307
+ const params = createUpdateBuilder<TestModel>(tableName, key, client)
308
+ .set('name', 'John Doe')
309
+ .set('score', 100)
310
+ .add('followerCount', 5)
311
+ .remove('age')
312
+ .where((attr, op) => op.and(op.gt(attr.followerCount, 0), op.lt(attr.followerCount, 1000)))
313
+ .returning('ALL_NEW')
314
+ .dbParams();
315
+
316
+ expect(params.TableName).toBe(tableName);
317
+ expect(params.Key).toEqual(key);
318
+ expect(params.UpdateExpression).toContain('SET');
319
+ expect(params.UpdateExpression).toContain('ADD');
320
+ expect(params.UpdateExpression).toContain('REMOVE');
321
+ expect(params.ConditionExpression).toMatch(
322
+ /\(#followerCount > :followerCount_\d+\) AND \(#followerCount < :followerCount_\d+\)/
323
+ );
324
+ expect(params.ReturnValues).toBe('ALL_NEW');
325
+ expect(params.ExpressionAttributeNames).toEqual({
326
+ '#name': 'name',
327
+ '#score': 'score',
328
+ '#followerCount': 'followerCount',
329
+ '#age': 'age',
330
+ });
331
+ });
332
+ });
333
+ });
@@ -0,0 +1,284 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
+ import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
4
+ import { buildExpression, AttrBuilder, Condition, createOpBuilder, AttrRef } from '../shared';
5
+ import { UpdateBuilder, UpdateAction } from './types';
6
+ import { DynamoDBLogger } from '../../utils/dynamodb-logger';
7
+
8
+ /**
9
+ * Creates an UpdateBuilder for an item key and table.
10
+ */
11
+ export function createUpdateBuilder<Model>(
12
+ tableName: string,
13
+ key: Partial<Model>,
14
+ client: DynamoDBClient,
15
+ prevConditions: Condition[] = [],
16
+ updateActions: {
17
+ set: UpdateAction[];
18
+ remove: UpdateAction[];
19
+ add: UpdateAction[];
20
+ delete: UpdateAction[];
21
+ } = { set: [], remove: [], add: [], delete: [] },
22
+ returnMode: 'NONE' | 'ALL_OLD' | 'ALL_NEW' | 'UPDATED_OLD' | 'UPDATED_NEW' = 'NONE',
23
+ valueCounter = 0,
24
+ enableTimestamps = false,
25
+ logger?: DynamoDBLogger
26
+ ): UpdateBuilder<Model> {
27
+ const conditions = [...prevConditions];
28
+
29
+ const getUniqueValueName = (baseName: string): string => {
30
+ return `${baseName}_${valueCounter++}`;
31
+ };
32
+
33
+ const normalizeAttr = (attr: keyof Model | AttrRef): string => {
34
+ if (typeof attr === 'string') {
35
+ return attr;
36
+ }
37
+ return (attr as AttrRef).name;
38
+ };
39
+
40
+ const build = (): UpdateBuilder<Model> => ({
41
+ where(fn) {
42
+ const attrs = new Proxy({} as AttrBuilder<Model>, {
43
+ get(_, prop: string) {
44
+ return { name: prop };
45
+ },
46
+ });
47
+ const opBuilder = createOpBuilder();
48
+ const condition = fn(attrs, opBuilder);
49
+ return createUpdateBuilder(
50
+ tableName,
51
+ key,
52
+ client,
53
+ [...conditions, condition],
54
+ updateActions,
55
+ returnMode,
56
+ valueCounter,
57
+ enableTimestamps,
58
+ logger
59
+ );
60
+ },
61
+
62
+ set(attr, value) {
63
+ const attrName = normalizeAttr(attr);
64
+ const valueName = getUniqueValueName(attrName);
65
+ const action: UpdateAction = {
66
+ expression: `#${attrName} = :${valueName}`,
67
+ names: { [`#${attrName}`]: attrName },
68
+ values: { [`:${valueName}`]: value },
69
+ };
70
+ return createUpdateBuilder(
71
+ tableName,
72
+ key,
73
+ client,
74
+ conditions,
75
+ { ...updateActions, set: [...updateActions.set, action] },
76
+ returnMode,
77
+ valueCounter,
78
+ enableTimestamps,
79
+ logger
80
+ );
81
+ },
82
+
83
+ remove(attr) {
84
+ const attrName = normalizeAttr(attr);
85
+ const action: UpdateAction = {
86
+ expression: `#${attrName}`,
87
+ names: { [`#${attrName}`]: attrName },
88
+ };
89
+ return createUpdateBuilder(
90
+ tableName,
91
+ key,
92
+ client,
93
+ conditions,
94
+ { ...updateActions, remove: [...updateActions.remove, action] },
95
+ returnMode,
96
+ valueCounter,
97
+ enableTimestamps,
98
+ logger
99
+ );
100
+ },
101
+
102
+ add(attr, value) {
103
+ const attrName = normalizeAttr(attr);
104
+ const valueName = getUniqueValueName(attrName);
105
+ const action: UpdateAction = {
106
+ expression: `#${attrName} :${valueName}`,
107
+ names: { [`#${attrName}`]: attrName },
108
+ values: { [`:${valueName}`]: value },
109
+ };
110
+ return createUpdateBuilder(
111
+ tableName,
112
+ key,
113
+ client,
114
+ conditions,
115
+ { ...updateActions, add: [...updateActions.add, action] },
116
+ returnMode,
117
+ valueCounter,
118
+ enableTimestamps,
119
+ logger
120
+ );
121
+ },
122
+
123
+ delete(attr, value) {
124
+ const attrName = normalizeAttr(attr);
125
+ const valueName = getUniqueValueName(attrName);
126
+ const action: UpdateAction = {
127
+ expression: `#${attrName} :${valueName}`,
128
+ names: { [`#${attrName}`]: attrName },
129
+ values: { [`:${valueName}`]: value },
130
+ };
131
+ return createUpdateBuilder(
132
+ tableName,
133
+ key,
134
+ client,
135
+ conditions,
136
+ { ...updateActions, delete: [...updateActions.delete, action] },
137
+ returnMode,
138
+ valueCounter,
139
+ enableTimestamps,
140
+ logger
141
+ );
142
+ },
143
+
144
+ returning(mode) {
145
+ return createUpdateBuilder(
146
+ tableName,
147
+ key,
148
+ client,
149
+ conditions,
150
+ updateActions,
151
+ mode,
152
+ valueCounter,
153
+ enableTimestamps,
154
+ logger
155
+ );
156
+ },
157
+
158
+ dbParams() {
159
+ // Build UpdateExpression from actions
160
+ const updateParts: string[] = [];
161
+ const allNames: Record<string, string> = {};
162
+ const allValues: Record<string, any> = {};
163
+
164
+ // Clone updateActions to avoid mutation
165
+ const actionsToProcess = { ...updateActions };
166
+
167
+ // Add updatedAt timestamp if enabled
168
+ if (enableTimestamps) {
169
+ const now = new Date().toISOString();
170
+ const timestampAction: UpdateAction = {
171
+ expression: `#updatedAt = :updatedAt_ts`,
172
+ names: { '#updatedAt': 'updatedAt' },
173
+ values: { ':updatedAt_ts': now },
174
+ };
175
+ actionsToProcess.set = [...actionsToProcess.set, timestampAction];
176
+ }
177
+
178
+ if (actionsToProcess.set.length > 0) {
179
+ const setExpressions = actionsToProcess.set.map((action) => {
180
+ Object.assign(allNames, action.names || {});
181
+ Object.assign(allValues, action.values || {});
182
+ return action.expression;
183
+ });
184
+ updateParts.push(`SET ${setExpressions.join(', ')}`);
185
+ }
186
+
187
+ if (updateActions.remove.length > 0) {
188
+ const removeExpressions = updateActions.remove.map((action) => {
189
+ Object.assign(allNames, action.names || {});
190
+ return action.expression;
191
+ });
192
+ updateParts.push(`REMOVE ${removeExpressions.join(', ')}`);
193
+ }
194
+
195
+ if (updateActions.add.length > 0) {
196
+ const addExpressions = updateActions.add.map((action) => {
197
+ Object.assign(allNames, action.names || {});
198
+ Object.assign(allValues, action.values || {});
199
+ return action.expression;
200
+ });
201
+ updateParts.push(`ADD ${addExpressions.join(', ')}`);
202
+ }
203
+
204
+ if (updateActions.delete.length > 0) {
205
+ const deleteExpressions = updateActions.delete.map((action) => {
206
+ Object.assign(allNames, action.names || {});
207
+ Object.assign(allValues, action.values || {});
208
+ return action.expression;
209
+ });
210
+ updateParts.push(`DELETE ${deleteExpressions.join(', ')}`);
211
+ }
212
+
213
+ const updateExpression = updateParts.join(' ');
214
+
215
+ // Build ConditionExpression from conditions
216
+ let conditionExpression = '';
217
+ let conditionNames = {};
218
+ let conditionValues = {};
219
+
220
+ if (conditions.length > 0) {
221
+ const combinedCondition =
222
+ conditions.length === 1 && conditions[0]
223
+ ? conditions[0]
224
+ : {
225
+ expression: '',
226
+ operator: 'AND' as const,
227
+ children: conditions,
228
+ };
229
+
230
+ const result = buildExpression(combinedCondition);
231
+ conditionExpression = result.expression;
232
+ conditionNames = result.names;
233
+ conditionValues = result.values;
234
+ }
235
+
236
+ // Merge names and values
237
+ const expressionAttributeNames = {
238
+ ...allNames,
239
+ ...conditionNames,
240
+ };
241
+ const expressionAttributeValues = {
242
+ ...allValues,
243
+ ...conditionValues,
244
+ };
245
+
246
+ const extra: any = returnMode !== 'NONE' ? { ReturnValues: returnMode } : {};
247
+
248
+ return {
249
+ TableName: tableName,
250
+ Key: key,
251
+ ...(updateExpression && { UpdateExpression: updateExpression }),
252
+ ...(conditionExpression && {
253
+ ConditionExpression: conditionExpression,
254
+ }),
255
+ ...(Object.keys(expressionAttributeNames).length && {
256
+ ExpressionAttributeNames: expressionAttributeNames,
257
+ }),
258
+ ...(Object.keys(expressionAttributeValues).length && {
259
+ ExpressionAttributeValues: expressionAttributeValues,
260
+ }),
261
+ ...extra,
262
+ };
263
+ },
264
+
265
+ async execute() {
266
+ const params = build().dbParams();
267
+ const response = await client.send(new UpdateCommand(params));
268
+ logger?.log('UpdateCommand', params, response);
269
+
270
+ // Return the updated item based on returnMode
271
+ if (returnMode === 'ALL_NEW' && response.Attributes) {
272
+ return response.Attributes as Model;
273
+ }
274
+ if (returnMode === 'UPDATED_NEW' && response.Attributes) {
275
+ return response.Attributes as Model;
276
+ }
277
+
278
+ // For other modes, return the key as a fallback
279
+ return key as Model;
280
+ },
281
+ });
282
+
283
+ return build();
284
+ }
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export * from './create-update-builder';
@@ -0,0 +1,42 @@
1
+ import { OperationBuilder, AttrRef } from '../shared';
2
+
3
+ /**
4
+ * Update actions that can be performed on attributes
5
+ */
6
+ export type UpdateAction = {
7
+ expression: string;
8
+ names?: Record<string, string>;
9
+ values?: Record<string, any>;
10
+ };
11
+
12
+ /**
13
+ * Builder interface for DynamoDB UpdateItem operations
14
+ */
15
+ export interface UpdateBuilder<Model> extends OperationBuilder<Model> {
16
+ /**
17
+ * Sets an attribute to a specific value
18
+ */
19
+ set(attr: keyof Model | AttrRef, value: any): UpdateBuilder<Model>;
20
+
21
+ /**
22
+ * Removes an attribute from the item
23
+ */
24
+ remove(attr: keyof Model | AttrRef): UpdateBuilder<Model>;
25
+
26
+ /**
27
+ * Adds a value to a number attribute or adds elements to a set
28
+ */
29
+ add(attr: keyof Model | AttrRef, value: any): UpdateBuilder<Model>;
30
+
31
+ /**
32
+ * Deletes elements from a set attribute
33
+ */
34
+ delete(attr: keyof Model | AttrRef, value: any): UpdateBuilder<Model>;
35
+
36
+ /**
37
+ * Configures what values should be returned after the update operation
38
+ */
39
+ returning(
40
+ mode: 'NONE' | 'ALL_OLD' | 'ALL_NEW' | 'UPDATED_OLD' | 'UPDATED_NEW'
41
+ ): UpdateBuilder<Model>;
42
+ }