@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,294 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
+ import { createDeleteBuilder } from './create-delete-builder';
4
+
5
+ describe('DeleteBuilder', () => {
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
+ followerCount?: number;
16
+ status?: string;
17
+ }
18
+
19
+ describe('Basic delete operations', () => {
20
+ test('should build params with just key', () => {
21
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
22
+ const params = createDeleteBuilder<TestModel>(tableName, key, client).dbParams();
23
+
24
+ expect(params.TableName).toBe(tableName);
25
+ expect(params.Key).toEqual(key);
26
+ expect(params.ConditionExpression).toBeUndefined();
27
+ expect(params.ExpressionAttributeNames).toBeUndefined();
28
+ expect(params.ExpressionAttributeValues).toBeUndefined();
29
+ expect(params.ReturnValues).toBeUndefined();
30
+ });
31
+
32
+ test('should build params with partial key', () => {
33
+ const key: Partial<TestModel> = { pk: 'USER#1' };
34
+ const params = createDeleteBuilder<TestModel>(tableName, key, client).dbParams();
35
+
36
+ expect(params.Key).toEqual({ pk: 'USER#1' });
37
+ });
38
+ });
39
+
40
+ describe('Condition expressions', () => {
41
+ test('should build params with single where condition', () => {
42
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
43
+ const params = createDeleteBuilder<TestModel>(tableName, key, client)
44
+ .where((attr, op) => op.eq(attr.status, 'inactive'))
45
+ .dbParams();
46
+
47
+ expect(params.TableName).toBe(tableName);
48
+ expect(params.Key).toEqual(key);
49
+ expect(params.ConditionExpression).toMatch(/#status = :status_\d+/);
50
+ expect(params.ExpressionAttributeNames).toEqual({
51
+ '#status': 'status',
52
+ });
53
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain('inactive');
54
+ });
55
+
56
+ test('should build params with AND condition', () => {
57
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
58
+ const params = createDeleteBuilder<TestModel>(tableName, key, client)
59
+ .where((attr, op) => op.and(op.eq(attr.status, 'inactive'), op.lt(attr.followerCount, 10)))
60
+ .dbParams();
61
+
62
+ expect(params.ConditionExpression).toMatch(
63
+ /\(#status = :status_\d+\) AND \(#followerCount < :followerCount_\d+\)/
64
+ );
65
+ expect(params.ExpressionAttributeNames).toEqual({
66
+ '#status': 'status',
67
+ '#followerCount': 'followerCount',
68
+ });
69
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain('inactive');
70
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain(10);
71
+ });
72
+
73
+ test('should build params with OR condition', () => {
74
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
75
+ const params = createDeleteBuilder<TestModel>(tableName, key, client)
76
+ .where((attr, op) => op.or(op.eq(attr.status, 'inactive'), op.eq(attr.status, 'deleted')))
77
+ .dbParams();
78
+
79
+ expect(params.ConditionExpression).toMatch(
80
+ /\(#status = :status_\d+\) OR \(#status = :status_\d+\)/
81
+ );
82
+ expect(params.ExpressionAttributeNames).toEqual({
83
+ '#status': 'status',
84
+ });
85
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain('inactive');
86
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain('deleted');
87
+ });
88
+
89
+ test('should build params with multiple where conditions', () => {
90
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
91
+ const params = createDeleteBuilder<TestModel>(tableName, key, client)
92
+ .where((attr, op) => op.eq(attr.status, 'inactive'))
93
+ .where((attr, op) => op.lt(attr.followerCount, 10))
94
+ .dbParams();
95
+
96
+ expect(params.ConditionExpression).toMatch(
97
+ /\(#status = :status_\d+\) AND \(#followerCount < :followerCount_\d+\)/
98
+ );
99
+ expect(params.ExpressionAttributeNames).toEqual({
100
+ '#status': 'status',
101
+ '#followerCount': 'followerCount',
102
+ });
103
+ });
104
+
105
+ test('should build params with complex nested conditions', () => {
106
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
107
+ const params = createDeleteBuilder<TestModel>(tableName, key, client)
108
+ .where((attr, op) =>
109
+ op.and(
110
+ op.or(op.eq(attr.status, 'inactive'), op.eq(attr.status, 'banned')),
111
+ op.lt(attr.followerCount, 5)
112
+ )
113
+ )
114
+ .dbParams();
115
+
116
+ expect(params.ConditionExpression).toMatch(/\(/);
117
+ expect(params.ConditionExpression).toMatch(/AND/);
118
+ expect(params.ConditionExpression).toMatch(/OR/);
119
+ expect(params.ExpressionAttributeNames).toEqual({
120
+ '#status': 'status',
121
+ '#followerCount': 'followerCount',
122
+ });
123
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain('inactive');
124
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain('banned');
125
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain(5);
126
+ });
127
+
128
+ test('should support comparison operators', () => {
129
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
130
+
131
+ // Greater than
132
+ const gtParams = createDeleteBuilder<TestModel>(tableName, key, client)
133
+ .where((attr, op) => op.gt(attr.age, 18))
134
+ .dbParams();
135
+ expect(gtParams.ConditionExpression).toMatch(/#age > :age_\d+/);
136
+
137
+ // Greater than or equal
138
+ const gteParams = createDeleteBuilder<TestModel>(tableName, key, client)
139
+ .where((attr, op) => op.gte(attr.age, 18))
140
+ .dbParams();
141
+ expect(gteParams.ConditionExpression).toMatch(/#age >= :age_\d+/);
142
+
143
+ // Less than
144
+ const ltParams = createDeleteBuilder<TestModel>(tableName, key, client)
145
+ .where((attr, op) => op.lt(attr.age, 65))
146
+ .dbParams();
147
+ expect(ltParams.ConditionExpression).toMatch(/#age < :age_\d+/);
148
+
149
+ // Less than or equal
150
+ const lteParams = createDeleteBuilder<TestModel>(tableName, key, client)
151
+ .where((attr, op) => op.lte(attr.age, 65))
152
+ .dbParams();
153
+ expect(lteParams.ConditionExpression).toMatch(/#age <= :age_\d+/);
154
+
155
+ // Not equal
156
+ const neParams = createDeleteBuilder<TestModel>(tableName, key, client)
157
+ .where((attr, op) => op.ne(attr.status, 'active'))
158
+ .dbParams();
159
+ expect(neParams.ConditionExpression).toMatch(/#status <> :status_\d+/);
160
+ });
161
+
162
+ test('should support BETWEEN operator', () => {
163
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
164
+ const params = createDeleteBuilder<TestModel>(tableName, key, client)
165
+ .where((attr, op) => op.between(attr.age, 18, 65))
166
+ .dbParams();
167
+
168
+ expect(params.ConditionExpression).toMatch(/#age BETWEEN :age_low_\d+ AND :age_high_\d+/);
169
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain(18);
170
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain(65);
171
+ });
172
+
173
+ test('should support beginsWith operator', () => {
174
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
175
+ const params = createDeleteBuilder<TestModel>(tableName, key, client)
176
+ .where((attr, op) => op.beginsWith(attr.name, 'John'))
177
+ .dbParams();
178
+
179
+ expect(params.ConditionExpression).toMatch(/begins_with\(#name, :name_\d+\)/);
180
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain('John');
181
+ });
182
+ });
183
+
184
+ describe('ReturnValues', () => {
185
+ test('should default to NONE', () => {
186
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
187
+ const params = createDeleteBuilder<TestModel>(tableName, key, client).dbParams();
188
+
189
+ expect(params.ReturnValues).toBeUndefined();
190
+ });
191
+
192
+ test('should support ALL_OLD', () => {
193
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
194
+ const params = createDeleteBuilder<TestModel>(tableName, key, client)
195
+ .returning('ALL_OLD')
196
+ .dbParams();
197
+
198
+ expect(params.ReturnValues).toBe('ALL_OLD');
199
+ });
200
+
201
+ test('should support NONE explicitly', () => {
202
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
203
+ const params = createDeleteBuilder<TestModel>(tableName, key, client)
204
+ .returning('NONE')
205
+ .dbParams();
206
+
207
+ expect(params.ReturnValues).toBeUndefined();
208
+ });
209
+ });
210
+
211
+ describe('Immutability', () => {
212
+ test('should create new builder instance on each method call', () => {
213
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
214
+ const builder1 = createDeleteBuilder<TestModel>(tableName, key, client);
215
+ const builder2 = builder1.where((attr, op) => op.eq(attr.status, 'inactive'));
216
+ const builder3 = builder2.returning('ALL_OLD');
217
+
218
+ expect(builder1.dbParams().ConditionExpression).toBeUndefined();
219
+ expect(builder1.dbParams().ReturnValues).toBeUndefined();
220
+
221
+ expect(builder2.dbParams().ConditionExpression).toMatch(/#status = :status_\d+/);
222
+ expect(builder2.dbParams().ReturnValues).toBeUndefined();
223
+
224
+ expect(builder3.dbParams().ConditionExpression).toMatch(/#status = :status_\d+/);
225
+ expect(builder3.dbParams().ReturnValues).toBe('ALL_OLD');
226
+ });
227
+ });
228
+
229
+ describe('Complex scenarios', () => {
230
+ test('should handle delete with all features', () => {
231
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
232
+ const params = createDeleteBuilder<TestModel>(tableName, key, client)
233
+ .where((attr, op) =>
234
+ op.and(
235
+ op.eq(attr.status, 'inactive'),
236
+ op.or(op.lt(attr.followerCount, 5), op.gt(attr.age, 365))
237
+ )
238
+ )
239
+ .returning('ALL_OLD')
240
+ .dbParams();
241
+
242
+ expect(params.TableName).toBe(tableName);
243
+ expect(params.Key).toEqual(key);
244
+ expect(params.ConditionExpression).toBeTruthy();
245
+ expect(params.ConditionExpression).toMatch(/AND/);
246
+ expect(params.ConditionExpression).toMatch(/OR/);
247
+ expect(params.ReturnValues).toBe('ALL_OLD');
248
+ expect(params.ExpressionAttributeNames).toEqual({
249
+ '#status': 'status',
250
+ '#followerCount': 'followerCount',
251
+ '#age': 'age',
252
+ });
253
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain('inactive');
254
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain(5);
255
+ expect(Object.values(params.ExpressionAttributeValues || {})).toContain(365);
256
+ });
257
+
258
+ test('should handle conditional delete based on multiple attributes', () => {
259
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
260
+ const params = createDeleteBuilder<TestModel>(tableName, key, client)
261
+ .where((attr, op) => op.eq(attr.status, 'inactive'))
262
+ .where((attr, op) => op.lt(attr.followerCount, 10))
263
+ .where((attr, op) => op.between(attr.age, 0, 30))
264
+ .returning('ALL_OLD')
265
+ .dbParams();
266
+
267
+ expect(params.ConditionExpression).toBeTruthy();
268
+ expect(params.ConditionExpression).toMatch(/AND/);
269
+ expect(params.ExpressionAttributeNames).toEqual({
270
+ '#status': 'status',
271
+ '#followerCount': 'followerCount',
272
+ '#age': 'age',
273
+ });
274
+ });
275
+ });
276
+
277
+ describe('Edge cases', () => {
278
+ test('should handle empty conditions gracefully', () => {
279
+ const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
280
+ const params = createDeleteBuilder<TestModel>(tableName, key, client).dbParams();
281
+
282
+ expect(params.ConditionExpression).toBeUndefined();
283
+ expect(params.ExpressionAttributeNames).toBeUndefined();
284
+ expect(params.ExpressionAttributeValues).toBeUndefined();
285
+ });
286
+
287
+ test('should handle key with only pk', () => {
288
+ const key: Partial<TestModel> = { pk: 'USER#1' };
289
+ const params = createDeleteBuilder<TestModel>(tableName, key, client).dbParams();
290
+
291
+ expect(params.Key).toEqual({ pk: 'USER#1' });
292
+ });
293
+ });
294
+ });
@@ -0,0 +1,100 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
+ import { DeleteCommand } from '@aws-sdk/lib-dynamodb';
4
+ import { buildExpression, AttrBuilder, Condition, createOpBuilder } from '../shared';
5
+ import { DeleteBuilder } from './types';
6
+ import { DynamoDBLogger } from '../../utils/dynamodb-logger';
7
+
8
+ /**
9
+ * Creates a DeleteBuilder for an item key and table.
10
+ */
11
+ export function createDeleteBuilder<Model>(
12
+ tableName: string,
13
+ key: Partial<Model>,
14
+ client: DynamoDBClient,
15
+ prevConditions: Condition[] = [],
16
+ returnMode: 'NONE' | 'ALL_OLD' = 'NONE',
17
+ logger?: DynamoDBLogger
18
+ ): DeleteBuilder<Model> {
19
+ const conditions = [...prevConditions];
20
+
21
+ const build = (): DeleteBuilder<Model> => ({
22
+ where(fn) {
23
+ const attrs = new Proxy({} as AttrBuilder<Model>, {
24
+ get(_, prop: string) {
25
+ return { name: prop };
26
+ },
27
+ });
28
+ const opBuilder = createOpBuilder();
29
+ const condition = fn(attrs, opBuilder);
30
+ return createDeleteBuilder(
31
+ tableName,
32
+ key,
33
+ client,
34
+ [...conditions, condition],
35
+ returnMode,
36
+ logger
37
+ );
38
+ },
39
+
40
+ returning(mode) {
41
+ return createDeleteBuilder(tableName, key, client, conditions, mode, logger);
42
+ },
43
+
44
+ dbParams() {
45
+ // Build ConditionExpression from condition tree
46
+ let conditionExpression = '';
47
+ let expressionAttributeNames = {};
48
+ let expressionAttributeValues = {};
49
+
50
+ if (conditions.length > 0) {
51
+ const combinedCondition =
52
+ conditions.length === 1 && conditions[0]
53
+ ? conditions[0]
54
+ : {
55
+ expression: '',
56
+ operator: 'AND' as const,
57
+ children: conditions,
58
+ };
59
+
60
+ const result = buildExpression(combinedCondition);
61
+ conditionExpression = result.expression;
62
+ expressionAttributeNames = result.names;
63
+ expressionAttributeValues = result.values;
64
+ }
65
+
66
+ const extra: any = returnMode !== 'NONE' ? { ReturnValues: returnMode } : {};
67
+
68
+ return {
69
+ TableName: tableName,
70
+ Key: key,
71
+ ...(conditionExpression && {
72
+ ConditionExpression: conditionExpression,
73
+ }),
74
+ ...(Object.keys(expressionAttributeNames).length && {
75
+ ExpressionAttributeNames: expressionAttributeNames,
76
+ }),
77
+ ...(Object.keys(expressionAttributeValues).length && {
78
+ ExpressionAttributeValues: expressionAttributeValues,
79
+ }),
80
+ ...extra,
81
+ };
82
+ },
83
+
84
+ async execute() {
85
+ const params = build().dbParams();
86
+ const response = await client.send(new DeleteCommand(params));
87
+ logger?.log('DeleteCommand', params, response);
88
+
89
+ // Return the deleted item if returnMode is ALL_OLD
90
+ if (returnMode === 'ALL_OLD' && response.Attributes) {
91
+ return response.Attributes as Model;
92
+ }
93
+
94
+ // For NONE mode, return the key as a fallback
95
+ return key as Model;
96
+ },
97
+ });
98
+
99
+ return build();
100
+ }
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export * from './create-delete-builder';
@@ -0,0 +1,11 @@
1
+ import { OperationBuilder } from '../shared';
2
+
3
+ /**
4
+ * Builder interface for DynamoDB DeleteItem operations
5
+ */
6
+ export interface DeleteBuilder<Model> extends OperationBuilder<Model> {
7
+ /**
8
+ * Configures what values should be returned after the delete operation
9
+ */
10
+ returning(mode: 'NONE' | 'ALL_OLD'): DeleteBuilder<Model>;
11
+ }
@@ -0,0 +1,272 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
+ import { GetCommand } from '@aws-sdk/lib-dynamodb';
4
+ import { createGetBuilder } from './create-get-builder';
5
+ import { mockClient } from 'aws-sdk-client-mock';
6
+
7
+ const ddbMock = mockClient(DynamoDBClient);
8
+
9
+ type TestUser = {
10
+ username: string;
11
+ email: string;
12
+ name: string;
13
+ status: string;
14
+ age: number;
15
+ };
16
+
17
+ describe('GetBuilder', () => {
18
+ let client: DynamoDBClient;
19
+
20
+ beforeEach(() => {
21
+ ddbMock.reset();
22
+ client = new DynamoDBClient({});
23
+ });
24
+
25
+ describe('ProjectionExpression with ExpressionAttributeNames', () => {
26
+ it('should use ExpressionAttributeNames for simple attributes', () => {
27
+ const builder = createGetBuilder<any, TestUser>(
28
+ 'TestTable',
29
+ { PK: 'USER#alice', SK: 'USER#alice' },
30
+ client
31
+ );
32
+
33
+ const params = builder.select(['username', 'email']).dbParams();
34
+
35
+ expect(params.ProjectionExpression).toBe('#username, #email');
36
+ expect(params.ExpressionAttributeNames).toEqual({
37
+ '#username': 'username',
38
+ '#email': 'email',
39
+ });
40
+ });
41
+
42
+ it('should handle reserved words correctly', () => {
43
+ const builder = createGetBuilder<any, TestUser>(
44
+ 'TestTable',
45
+ { PK: 'USER#alice', SK: 'USER#alice' },
46
+ client
47
+ );
48
+
49
+ // 'name' and 'status' are DynamoDB reserved words
50
+ const params = builder.select(['name', 'status']).dbParams();
51
+
52
+ expect(params.ProjectionExpression).toBe('#name, #status');
53
+ expect(params.ExpressionAttributeNames).toEqual({
54
+ '#name': 'name',
55
+ '#status': 'status',
56
+ });
57
+ });
58
+
59
+ it('should work with mixed reserved and non-reserved words', () => {
60
+ const builder = createGetBuilder<any, TestUser>(
61
+ 'TestTable',
62
+ { PK: 'USER#alice', SK: 'USER#alice' },
63
+ client
64
+ );
65
+
66
+ const params = builder.select(['username', 'name', 'status', 'email']).dbParams();
67
+
68
+ expect(params.ProjectionExpression).toBe('#username, #name, #status, #email');
69
+ expect(params.ExpressionAttributeNames).toEqual({
70
+ '#username': 'username',
71
+ '#name': 'name',
72
+ '#status': 'status',
73
+ '#email': 'email',
74
+ });
75
+ });
76
+
77
+ it('should not add ExpressionAttributeNames when no projection', () => {
78
+ const builder = createGetBuilder<any, TestUser>(
79
+ 'TestTable',
80
+ { PK: 'USER#alice', SK: 'USER#alice' },
81
+ client
82
+ );
83
+
84
+ const params = builder.dbParams();
85
+
86
+ expect(params.ProjectionExpression).toBeUndefined();
87
+ expect(params.ExpressionAttributeNames).toBeUndefined();
88
+ });
89
+ });
90
+
91
+ describe('returnConsumedCapacity()', () => {
92
+ it('should add ReturnConsumedCapacity with TOTAL', () => {
93
+ const builder = createGetBuilder<any, TestUser>(
94
+ 'TestTable',
95
+ { PK: 'USER#alice', SK: 'USER#alice' },
96
+ client
97
+ );
98
+
99
+ const params = builder.returnConsumedCapacity('TOTAL').dbParams();
100
+
101
+ expect(params.ReturnConsumedCapacity).toBe('TOTAL');
102
+ });
103
+
104
+ it('should add ReturnConsumedCapacity with INDEXES', () => {
105
+ const builder = createGetBuilder<any, TestUser>(
106
+ 'TestTable',
107
+ { PK: 'USER#alice', SK: 'USER#alice' },
108
+ client
109
+ );
110
+
111
+ const params = builder.returnConsumedCapacity('INDEXES').dbParams();
112
+
113
+ expect(params.ReturnConsumedCapacity).toBe('INDEXES');
114
+ });
115
+
116
+ it('should add ReturnConsumedCapacity with NONE', () => {
117
+ const builder = createGetBuilder<any, TestUser>(
118
+ 'TestTable',
119
+ { PK: 'USER#alice', SK: 'USER#alice' },
120
+ client
121
+ );
122
+
123
+ const params = builder.returnConsumedCapacity('NONE').dbParams();
124
+
125
+ expect(params.ReturnConsumedCapacity).toBe('NONE');
126
+ });
127
+
128
+ it('should not add ReturnConsumedCapacity when not specified', () => {
129
+ const builder = createGetBuilder<any, TestUser>(
130
+ 'TestTable',
131
+ { PK: 'USER#alice', SK: 'USER#alice' },
132
+ client
133
+ );
134
+
135
+ const params = builder.dbParams();
136
+
137
+ expect(params.ReturnConsumedCapacity).toBeUndefined();
138
+ });
139
+ });
140
+
141
+ describe('Method chaining with immutability', () => {
142
+ it('should chain select() and returnConsumedCapacity()', () => {
143
+ const builder = createGetBuilder<any, TestUser>(
144
+ 'TestTable',
145
+ { PK: 'USER#alice', SK: 'USER#alice' },
146
+ client
147
+ );
148
+
149
+ const params = builder
150
+ .select(['username', 'email'])
151
+ .returnConsumedCapacity('TOTAL')
152
+ .dbParams();
153
+
154
+ expect(params.ProjectionExpression).toBe('#username, #email');
155
+ expect(params.ExpressionAttributeNames).toEqual({
156
+ '#username': 'username',
157
+ '#email': 'email',
158
+ });
159
+ expect(params.ReturnConsumedCapacity).toBe('TOTAL');
160
+ });
161
+
162
+ it('should chain consistentRead() and returnConsumedCapacity()', () => {
163
+ const builder = createGetBuilder<any, TestUser>(
164
+ 'TestTable',
165
+ { PK: 'USER#alice', SK: 'USER#alice' },
166
+ client
167
+ );
168
+
169
+ const params = builder.consistentRead().returnConsumedCapacity('INDEXES').dbParams();
170
+
171
+ expect(params.ConsistentRead).toBe(true);
172
+ expect(params.ReturnConsumedCapacity).toBe('INDEXES');
173
+ });
174
+
175
+ it('should chain all methods together', () => {
176
+ const builder = createGetBuilder<any, TestUser>(
177
+ 'TestTable',
178
+ { PK: 'USER#alice', SK: 'USER#alice' },
179
+ client
180
+ );
181
+
182
+ const params = builder
183
+ .select(['name', 'status'])
184
+ .consistentRead()
185
+ .returnConsumedCapacity('TOTAL')
186
+ .dbParams();
187
+
188
+ expect(params.ProjectionExpression).toBe('#name, #status');
189
+ expect(params.ExpressionAttributeNames).toEqual({
190
+ '#name': 'name',
191
+ '#status': 'status',
192
+ });
193
+ expect(params.ConsistentRead).toBe(true);
194
+ expect(params.ReturnConsumedCapacity).toBe('TOTAL');
195
+ });
196
+
197
+ it('should maintain immutability - original builder unchanged', () => {
198
+ const builder = createGetBuilder<any, TestUser>(
199
+ 'TestTable',
200
+ { PK: 'USER#alice', SK: 'USER#alice' },
201
+ client
202
+ );
203
+
204
+ const builder2 = builder.select(['username']);
205
+ const builder3 = builder2.returnConsumedCapacity('TOTAL');
206
+
207
+ // Original builder should not have projection
208
+ const params1 = builder.dbParams();
209
+ expect(params1.ProjectionExpression).toBeUndefined();
210
+ expect(params1.ReturnConsumedCapacity).toBeUndefined();
211
+
212
+ // Builder2 should have projection but not returnConsumedCapacity
213
+ const params2 = builder2.dbParams();
214
+ expect(params2.ProjectionExpression).toBe('#username');
215
+ expect(params2.ReturnConsumedCapacity).toBeUndefined();
216
+
217
+ // Builder3 should have both
218
+ const params3 = builder3.dbParams();
219
+ expect(params3.ProjectionExpression).toBe('#username');
220
+ expect(params3.ReturnConsumedCapacity).toBe('TOTAL');
221
+ });
222
+ });
223
+
224
+ describe('execute()', () => {
225
+ it('should send GetCommand with correct parameters', async () => {
226
+ ddbMock.on(GetCommand).resolves({
227
+ Item: { username: 'alice', email: 'alice@example.com' },
228
+ });
229
+
230
+ const builder = createGetBuilder<any, TestUser>(
231
+ 'TestTable',
232
+ { PK: 'USER#alice', SK: 'USER#alice' },
233
+ client
234
+ );
235
+
236
+ const result = await builder
237
+ .select(['username', 'email'])
238
+ .returnConsumedCapacity('TOTAL')
239
+ .execute();
240
+
241
+ expect(result).toEqual({ username: 'alice', email: 'alice@example.com' });
242
+
243
+ // Verify the command was called with correct params
244
+ const calls = ddbMock.commandCalls(GetCommand);
245
+ expect(calls).toHaveLength(1);
246
+ expect(calls[0]?.args[0]?.input).toMatchObject({
247
+ TableName: 'TestTable',
248
+ Key: { PK: 'USER#alice', SK: 'USER#alice' },
249
+ ProjectionExpression: '#username, #email',
250
+ ExpressionAttributeNames: {
251
+ '#username': 'username',
252
+ '#email': 'email',
253
+ },
254
+ ReturnConsumedCapacity: 'TOTAL',
255
+ });
256
+ });
257
+
258
+ it('should return undefined when item not found', async () => {
259
+ ddbMock.on(GetCommand).resolves({});
260
+
261
+ const builder = createGetBuilder<any, TestUser>(
262
+ 'TestTable',
263
+ { PK: 'USER#notfound', SK: 'USER#notfound' },
264
+ client
265
+ );
266
+
267
+ const result = await builder.execute();
268
+
269
+ expect(result).toBeUndefined();
270
+ });
271
+ });
272
+ });