dyno-table 0.0.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +679 -239
  2. package/dist/index.d.ts +2836 -362
  3. package/dist/index.js +3075 -963
  4. package/package.json +6 -11
package/dist/index.js CHANGED
@@ -1,1177 +1,3289 @@
1
- // src/builders/expression-builder.ts
2
- var ExpressionBuilder = class {
3
- nameCount = 0;
4
- valueCount = 0;
5
- generateAlias(type, prefix = type === "name" ? "n" : "v") {
6
- const count = type === "name" ? this.nameCount++ : this.valueCount++;
7
- const symbol = type === "name" ? "#" : ":";
8
- return `${symbol}${prefix}${count}`;
9
- }
10
- reset() {
11
- this.nameCount = 0;
12
- this.valueCount = 0;
13
- }
14
- createAttributePath(path) {
15
- const parts = path.split(".");
16
- const aliases = parts.map(() => this.generateAlias("name"));
17
- return {
18
- path: aliases.join("."),
19
- names: Object.fromEntries(parts.map((part, i) => [aliases[i], part]))
20
- };
1
+ // src/conditions.ts
2
+ var createComparisonCondition = (type) => (attr, value) => ({
3
+ type,
4
+ attr,
5
+ value
6
+ });
7
+ var eq = createComparisonCondition("eq");
8
+ var ne = createComparisonCondition("ne");
9
+ var lt = createComparisonCondition("lt");
10
+ var lte = createComparisonCondition("lte");
11
+ var gt = createComparisonCondition("gt");
12
+ var gte = createComparisonCondition("gte");
13
+ var between = (attr, lower, upper) => ({
14
+ type: "between",
15
+ attr,
16
+ value: [lower, upper]
17
+ });
18
+ var beginsWith = createComparisonCondition("beginsWith");
19
+ var contains = createComparisonCondition("contains");
20
+ var attributeExists = (attr) => ({
21
+ type: "attributeExists",
22
+ attr
23
+ });
24
+ var attributeNotExists = (attr) => ({
25
+ type: "attributeNotExists",
26
+ attr
27
+ });
28
+ var and = (...conditions) => ({
29
+ type: "and",
30
+ conditions
31
+ });
32
+ var or = (...conditions) => ({
33
+ type: "or",
34
+ conditions
35
+ });
36
+ var not = (condition) => ({
37
+ type: "not",
38
+ condition
39
+ });
40
+
41
+ // src/expression.ts
42
+ var generateAttributeName = (params, attr) => {
43
+ for (const [existingName, existingAttr] of Object.entries(params.expressionAttributeNames)) {
44
+ if (existingAttr === attr) {
45
+ return existingName;
46
+ }
21
47
  }
22
- addValue(attributes, value, prefix) {
23
- const alias = this.generateAlias("value", prefix);
24
- attributes.values[alias] = value;
25
- return alias;
48
+ const attrName = `#${Object.keys(params.expressionAttributeNames).length}`;
49
+ params.expressionAttributeNames[attrName] = attr;
50
+ return attrName;
51
+ };
52
+ var generateValueName = (params, value) => {
53
+ const valueName = `:${params.valueCounter.count++}`;
54
+ params.expressionAttributeValues[valueName] = value;
55
+ return valueName;
56
+ };
57
+ var validateCondition = (condition, requiresAttr = true, requiresValue = true) => {
58
+ if (requiresAttr && !condition.attr) {
59
+ throw new Error(`Attribute is required for ${condition.type} condition`);
26
60
  }
27
- buildComparison(path, operator, value, attributes, prefix) {
28
- const simpleOperators = ["=", "<>", "<", "<=", ">", ">="];
29
- if (simpleOperators.includes(operator)) {
30
- const valueAlias = this.addValue(attributes, value, prefix);
31
- return `${path} ${operator} ${valueAlias}`;
32
- }
33
- switch (operator) {
34
- case "attribute_exists":
35
- case "attribute_not_exists":
36
- return `${operator}(${path})`;
37
- case "begins_with":
38
- case "contains":
39
- case "attribute_type":
40
- return `${operator}(${path}, ${this.addValue(attributes, value, prefix)})`;
41
- case "not_contains":
42
- return `NOT contains(${path}, ${this.addValue(attributes, value, prefix)})`;
43
- case "size": {
44
- const { compare, value: sizeValue } = value;
45
- return `size(${path}) ${compare} ${this.addValue(attributes, sizeValue, prefix)}`;
46
- }
47
- case "BETWEEN": {
48
- const valueAlias = this.addValue(attributes, value, prefix);
49
- return `${path} BETWEEN ${valueAlias}[0] AND ${valueAlias}[1]`;
61
+ if (requiresValue && condition.value === void 0) {
62
+ throw new Error(`Value is required for ${condition.type} condition`);
63
+ }
64
+ };
65
+ var buildComparisonExpression = (condition, operator, params) => {
66
+ validateCondition(condition);
67
+ if (!condition.attr) {
68
+ throw new Error(`Attribute is required for ${condition.type} condition`);
69
+ }
70
+ const attrName = generateAttributeName(params, condition.attr);
71
+ const valueName = generateValueName(params, condition.value);
72
+ return `${attrName} ${operator} ${valueName}`;
73
+ };
74
+ var buildBetweenExpression = (condition, params) => {
75
+ validateCondition(condition);
76
+ if (!condition.attr) {
77
+ throw new Error(`Attribute is required for ${condition.type} condition`);
78
+ }
79
+ if (!Array.isArray(condition.value) || condition.value.length !== 2) {
80
+ throw new Error("Between condition requires an array of two values");
81
+ }
82
+ const attrName = generateAttributeName(params, condition.attr);
83
+ const lowerName = generateValueName(params, condition.value[0]);
84
+ const upperName = generateValueName(params, condition.value[1]);
85
+ return `${attrName} BETWEEN ${lowerName} AND ${upperName}`;
86
+ };
87
+ var buildFunctionExpression = (functionName, condition, params) => {
88
+ validateCondition(condition);
89
+ if (!condition.attr) {
90
+ throw new Error(`Attribute is required for ${condition.type} condition`);
91
+ }
92
+ const attrName = generateAttributeName(params, condition.attr);
93
+ const valueName = generateValueName(params, condition.value);
94
+ return `${functionName}(${attrName}, ${valueName})`;
95
+ };
96
+ var buildAttributeFunction = (functionName, condition, params) => {
97
+ validateCondition(condition, true, false);
98
+ if (!condition.attr) {
99
+ throw new Error(`Attribute is required for ${condition.type} condition`);
100
+ }
101
+ const attrName = generateAttributeName(params, condition.attr);
102
+ return `${functionName}(${attrName})`;
103
+ };
104
+ var buildLogicalExpression = (operator, conditions, params) => {
105
+ if (!conditions || conditions.length === 0) {
106
+ throw new Error(`At least one condition is required for ${operator} expression`);
107
+ }
108
+ const expressions = conditions.map((c) => buildExpression(c, params));
109
+ return `(${expressions.join(` ${operator} `)})`;
110
+ };
111
+ var buildExpression = (condition, params) => {
112
+ if (!condition) return "";
113
+ try {
114
+ const expressionBuilders = {
115
+ eq: () => buildComparisonExpression(condition, "=", params),
116
+ ne: () => buildComparisonExpression(condition, "<>", params),
117
+ lt: () => buildComparisonExpression(condition, "<", params),
118
+ lte: () => buildComparisonExpression(condition, "<=", params),
119
+ gt: () => buildComparisonExpression(condition, ">", params),
120
+ gte: () => buildComparisonExpression(condition, ">=", params),
121
+ between: () => buildBetweenExpression(condition, params),
122
+ beginsWith: () => buildFunctionExpression("begins_with", condition, params),
123
+ contains: () => buildFunctionExpression("contains", condition, params),
124
+ attributeExists: () => buildAttributeFunction("attribute_exists", condition, params),
125
+ attributeNotExists: () => buildAttributeFunction("attribute_not_exists", condition, params),
126
+ and: () => {
127
+ if (!condition.conditions) {
128
+ throw new Error("Conditions array is required for AND operator");
129
+ }
130
+ return buildLogicalExpression("AND", condition.conditions, params);
131
+ },
132
+ or: () => {
133
+ if (!condition.conditions) {
134
+ throw new Error("Conditions array is required for OR operator");
135
+ }
136
+ return buildLogicalExpression("OR", condition.conditions, params);
137
+ },
138
+ not: () => {
139
+ if (!condition.condition) {
140
+ throw new Error("Condition is required for NOT operator");
141
+ }
142
+ return `NOT (${buildExpression(condition.condition, params)})`;
50
143
  }
51
- case "IN":
52
- return `${path} IN (${this.addValue(attributes, value, prefix)})`;
53
- default:
54
- throw new Error(`Unsupported operator: ${operator}`);
144
+ };
145
+ const builder = expressionBuilders[condition.type];
146
+ if (!builder) {
147
+ throw new Error(`Unknown condition type: ${condition.type}`);
148
+ }
149
+ return builder();
150
+ } catch (error) {
151
+ if (error instanceof Error) {
152
+ console.error(`Error building expression for condition type ${condition.type}:`, error.message);
153
+ } else {
154
+ console.error(`Error building expression for condition type ${condition.type}:`, error);
55
155
  }
156
+ throw error;
56
157
  }
57
- createExpression(conditions) {
58
- this.reset();
59
- const attributes = { names: {}, values: {} };
60
- const expressions = conditions.map(({ field, operator, value }) => {
61
- const { path, names } = this.createAttributePath(field);
62
- Object.assign(attributes.names, names);
63
- return this.buildComparison(path, operator, value, attributes);
64
- });
65
- return {
66
- expression: expressions.length ? expressions.join(" AND ") : void 0,
67
- attributes: this.formatAttributes(attributes)
68
- };
158
+ };
159
+ var prepareExpressionParams = (condition) => {
160
+ if (!condition) return {};
161
+ const params = {
162
+ expressionAttributeNames: {},
163
+ expressionAttributeValues: {},
164
+ valueCounter: { count: 0 }
165
+ };
166
+ const expression = buildExpression(condition, params);
167
+ return {
168
+ expression,
169
+ names: Object.keys(params.expressionAttributeNames).length > 0 ? params.expressionAttributeNames : void 0,
170
+ values: Object.keys(params.expressionAttributeValues).length > 0 ? params.expressionAttributeValues : void 0
171
+ };
172
+ };
173
+
174
+ // src/builders/paginator.ts
175
+ var Paginator = class {
176
+ queryBuilder;
177
+ pageSize;
178
+ currentPage = 0;
179
+ lastEvaluatedKey;
180
+ hasMorePages = true;
181
+ totalItemsRetrieved = 0;
182
+ overallLimit;
183
+ constructor(queryBuilder, pageSize) {
184
+ this.queryBuilder = queryBuilder;
185
+ this.pageSize = pageSize;
186
+ this.overallLimit = queryBuilder.getLimit();
69
187
  }
70
- formatAttributes({ names, values }) {
71
- return {
72
- ...Object.keys(names).length && { names },
73
- ...Object.keys(values).length && { values }
74
- };
188
+ /**
189
+ * Gets the current page number (1-indexed).
190
+ * Use this method when you need to:
191
+ * - Track progress through dinosaur lists
192
+ * - Display habitat inspection status
193
+ * - Monitor security sweep progress
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * const paginator = new QueryBuilder(executor, eq('species', 'Tyrannosaurus'))
198
+ * .paginate(5);
199
+ *
200
+ * await paginator.getNextPage();
201
+ * console.log(`Reviewing T-Rex group ${paginator.getCurrentPage()}`);
202
+ * ```
203
+ *
204
+ * @returns The current page number, starting from 1
205
+ */
206
+ getCurrentPage() {
207
+ return this.currentPage;
75
208
  }
76
- buildKeyCondition(key, indexConfig) {
77
- this.reset();
78
- const attributes = { names: {}, values: {} };
79
- const conditions = [];
80
- const pkName = this.generateAlias("name", "pk");
81
- attributes.names[pkName] = indexConfig.pkName;
82
- conditions.push(`${pkName} = ${this.addValue(attributes, key.pk, "pk")}`);
83
- if (key.sk && indexConfig.skName) {
84
- const skName = this.generateAlias("name", "sk");
85
- attributes.names[skName] = indexConfig.skName;
86
- if (typeof key.sk === "string") {
87
- conditions.push(`${skName} = ${this.addValue(attributes, key.sk, "sk")}`);
88
- } else {
89
- conditions.push(this.buildComparison(skName, key.sk.operator, key.sk.value, attributes));
90
- }
209
+ /**
210
+ * Checks if there are more pages of dinosaurs or habitats to process.
211
+ * Use this method when you need to:
212
+ * - Check for more dinosaurs to review
213
+ * - Continue habitat inspections
214
+ * - Process security incidents
215
+ * - Complete feeding schedules
216
+ *
217
+ * This method takes into account both:
218
+ * - DynamoDB's lastEvaluatedKey mechanism
219
+ * - Any overall limit set on the query
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * // Process all security incidents
224
+ * const paginator = new QueryBuilder(executor, eq('type', 'SECURITY_BREACH'))
225
+ * .sortDescending()
226
+ * .paginate(10);
227
+ *
228
+ * while (paginator.hasNextPage()) {
229
+ * const page = await paginator.getNextPage();
230
+ * for (const incident of page.items) {
231
+ * await processSecurityBreach(incident);
232
+ * }
233
+ * console.log(`Processed incidents page ${page.page}`);
234
+ * }
235
+ * ```
236
+ *
237
+ * @returns true if there are more pages available, false otherwise
238
+ */
239
+ hasNextPage() {
240
+ if (this.overallLimit !== void 0 && this.totalItemsRetrieved >= this.overallLimit) {
241
+ return false;
91
242
  }
92
- return {
93
- expression: conditions.join(" AND "),
94
- attributes: this.formatAttributes(attributes)
95
- };
243
+ return this.hasMorePages;
96
244
  }
97
- buildUpdateExpression(updates) {
98
- this.reset();
99
- const attributes = { names: {}, values: {} };
100
- const operations = { sets: [], removes: [] };
101
- for (const [key, value] of Object.entries(updates)) {
102
- if (key === "") {
103
- throw new Error("Empty key provided");
104
- }
105
- const nameAlias = this.generateAlias("name", "u");
106
- attributes.names[nameAlias] = key;
107
- if (value == null) {
108
- operations.removes.push(nameAlias);
109
- } else {
110
- const valueAlias = this.addValue(attributes, value, "u");
111
- operations.sets.push(`${nameAlias} = ${valueAlias}`);
245
+ /**
246
+ * Retrieves the next page of dinosaurs or habitats from DynamoDB.
247
+ * Use this method when you need to:
248
+ * - Process dinosaur groups systematically
249
+ * - Review habitat inspections in batches
250
+ * - Monitor security incidents in sequence
251
+ * - Schedule feeding rotations
252
+ *
253
+ * This method handles:
254
+ * - Automatic continuation between groups
255
+ * - Respect for park capacity limits
256
+ * - Group size adjustments for safety
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * const paginator = new QueryBuilder(executor, eq('species', 'Velociraptor'))
261
+ * .filter(op => op.eq('status', 'ACTIVE'))
262
+ * .paginate(5);
263
+ *
264
+ * // Check first raptor group
265
+ * const page1 = await paginator.getNextPage();
266
+ * console.log(`Found ${page1.items.length} active raptors`);
267
+ *
268
+ * // Continue inspection if more groups exist
269
+ * if (page1.hasNextPage) {
270
+ * const page2 = await paginator.getNextPage();
271
+ * console.log(`Inspecting raptor group ${page2.page}`);
272
+ *
273
+ * for (const raptor of page2.items) {
274
+ * await performHealthCheck(raptor);
275
+ * }
276
+ * }
277
+ * ```
278
+ *
279
+ * @returns A promise that resolves to a PaginationResult containing:
280
+ * - items: The dinosaurs/habitats for this page
281
+ * - hasNextPage: Whether more groups exist
282
+ * - page: The current group number
283
+ * - lastEvaluatedKey: DynamoDB's continuation token
284
+ */
285
+ async getNextPage() {
286
+ if (!this.hasNextPage()) {
287
+ return {
288
+ items: [],
289
+ hasNextPage: false,
290
+ page: this.currentPage
291
+ };
292
+ }
293
+ let effectivePageSize = this.pageSize;
294
+ if (this.overallLimit !== void 0) {
295
+ const remainingItems = this.overallLimit - this.totalItemsRetrieved;
296
+ if (remainingItems <= 0) {
297
+ return {
298
+ items: [],
299
+ hasNextPage: false,
300
+ page: this.currentPage
301
+ };
112
302
  }
303
+ effectivePageSize = Math.min(effectivePageSize, remainingItems);
113
304
  }
114
- const expression = [
115
- operations.sets.length && `SET ${operations.sets.join(", ")}`,
116
- operations.removes.length && `REMOVE ${operations.removes.join(", ")}`
117
- ].filter(Boolean).join(" ");
305
+ const query = this.queryBuilder.clone().limit(effectivePageSize);
306
+ if (this.lastEvaluatedKey) {
307
+ query.startFrom(this.lastEvaluatedKey);
308
+ }
309
+ const result = await query.execute();
310
+ this.currentPage += 1;
311
+ this.lastEvaluatedKey = result.lastEvaluatedKey;
312
+ this.totalItemsRetrieved += result.items.length;
313
+ this.hasMorePages = !!result.lastEvaluatedKey && (this.overallLimit === void 0 || this.totalItemsRetrieved < this.overallLimit);
118
314
  return {
119
- expression,
120
- attributes: this.formatAttributes(attributes)
315
+ items: result.items,
316
+ lastEvaluatedKey: result.lastEvaluatedKey,
317
+ hasNextPage: this.hasNextPage(),
318
+ page: this.currentPage
121
319
  };
122
320
  }
321
+ /**
322
+ * Gets all remaining dinosaurs or habitats and combines them into a single array.
323
+ * Use this method when you need to:
324
+ * - Generate complete park inventory
325
+ * - Perform full security audit
326
+ * - Create comprehensive feeding schedule
327
+ * - Run park-wide health checks
328
+ *
329
+ * Note: Use with caution! This method:
330
+ * - Could overwhelm systems with large dinosaur populations
331
+ * - Makes multiple database requests
332
+ * - May cause system strain during peak hours
333
+ *
334
+ * @example
335
+ * ```ts
336
+ * // Get complete carnivore inventory
337
+ * const paginator = new QueryBuilder(executor, eq('diet', 'CARNIVORE'))
338
+ * .filter(op => op.eq('status', 'ACTIVE'))
339
+ * .paginate(10);
340
+ *
341
+ * try {
342
+ * const allCarnivores = await paginator.getAllPages();
343
+ * console.log(`Park contains ${allCarnivores.length} active carnivores`);
344
+ *
345
+ * // Calculate total threat level
346
+ * const totalThreat = allCarnivores.reduce(
347
+ * (sum, dino) => sum + dino.stats.threatLevel,
348
+ * 0
349
+ * );
350
+ * console.log(`Total threat level: ${totalThreat}`);
351
+ * } catch (error) {
352
+ * console.error('Failed to complete carnivore census:', error);
353
+ * }
354
+ * ```
355
+ *
356
+ * @returns A promise that resolves to an array containing all remaining items
357
+ */
358
+ async getAllPages() {
359
+ const allItems = [];
360
+ while (this.hasNextPage()) {
361
+ const result = await this.getNextPage();
362
+ allItems.push(...result.items);
363
+ }
364
+ return allItems;
365
+ }
123
366
  };
124
367
 
125
- // src/builders/operation-builder.ts
126
- var OperationBuilder = class {
127
- constructor(expressionBuilder) {
128
- this.expressionBuilder = expressionBuilder;
129
- }
130
- conditions = [];
131
- where(field, operator, value) {
132
- this.conditions.push({ field, operator, value });
368
+ // src/builders/query-builder.ts
369
+ var QueryBuilder = class _QueryBuilder {
370
+ keyCondition;
371
+ options = {};
372
+ selectedFields = /* @__PURE__ */ new Set();
373
+ executor;
374
+ constructor(executor, keyCondition) {
375
+ this.executor = executor;
376
+ this.keyCondition = keyCondition;
377
+ }
378
+ /**
379
+ * Sets the maximum number of items to return from the query.
380
+ * Use this method when you need to:
381
+ * - Limit the result set size
382
+ * - Implement manual pagination
383
+ * - Control data transfer size
384
+ *
385
+ * Note: This limit applies to the items that match the key condition
386
+ * before any filter expressions are applied.
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * // Get first 10 orders for a user
391
+ * const result = await new QueryBuilder(executor, eq('userId', '123'))
392
+ * .limit(10)
393
+ * .execute();
394
+ * ```
395
+ *
396
+ * @param limit - Maximum number of items to return
397
+ * @returns The builder instance for method chaining
398
+ */
399
+ limit(limit) {
400
+ this.options.limit = limit;
133
401
  return this;
134
402
  }
135
- whereExists(field) {
136
- this.conditions.push({ field, operator: "attribute_exists" });
403
+ /**
404
+ * Gets the current limit set on the query.
405
+ * This is used internally by the paginator to manage result sets.
406
+ *
407
+ * @returns The current limit or undefined if no limit is set
408
+ */
409
+ getLimit() {
410
+ return this.options.limit;
411
+ }
412
+ /**
413
+ * Specifies a Global Secondary Index (GSI) to use for the query.
414
+ * Use this method when you need to:
415
+ * - Query data using non-primary key attributes
416
+ * - Access data through alternate access patterns
417
+ * - Optimize query performance for specific access patterns
418
+ *
419
+ * This method provides type safety by only allowing valid GSI names
420
+ * defined in your table configuration.
421
+ *
422
+ * @example
423
+ * ```ts
424
+ * // Query by status using a GSI
425
+ * const result = await new QueryBuilder(executor, eq('status', 'ACTIVE'))
426
+ * .useIndex('status-index')
427
+ * .execute();
428
+ *
429
+ * // Query by category and date range
430
+ * const result = await new QueryBuilder(executor, eq('category', 'books'))
431
+ * .useIndex('category-date-index')
432
+ * .filter(op => op.between('date', startDate, endDate))
433
+ * .execute();
434
+ * ```
435
+ *
436
+ * Note: Be aware that GSIs:
437
+ * - May have different projected attributes
438
+ * - Have eventually consistent reads only
439
+ * - May have different provisioned throughput
440
+ *
441
+ * @param indexName - The name of the GSI to use (type-safe based on table configuration)
442
+ * @returns The builder instance for method chaining
443
+ */
444
+ /**
445
+ * Specifies a Global Secondary Index (GSI) to use for the query.
446
+ * Use this method when you need to:
447
+ * - Query dinosaurs by species or status
448
+ * - Search habitats by security level
449
+ * - Find incidents by date
450
+ * - List feeding schedules by time
451
+ *
452
+ * @example
453
+ * ```typescript
454
+ * // Find all dinosaurs of a specific species
455
+ * builder
456
+ * .useIndex('species-status-index')
457
+ * .filter(op => op.eq('status', 'ACTIVE'));
458
+ *
459
+ * // Search high-security habitats
460
+ * builder
461
+ * .useIndex('security-level-index')
462
+ * .filter(op =>
463
+ * op.and([
464
+ * op.gt('securityLevel', 8),
465
+ * op.eq('status', 'OPERATIONAL')
466
+ * ])
467
+ * );
468
+ * ```
469
+ *
470
+ * @param indexName - The name of the GSI to use (type-safe based on table configuration)
471
+ * @returns The builder instance for method chaining
472
+ */
473
+ useIndex(indexName) {
474
+ this.options.indexName = indexName;
137
475
  return this;
138
476
  }
139
- whereNotExists(field) {
140
- this.conditions.push({ field, operator: "attribute_not_exists" });
477
+ /**
478
+ * Sets whether to use strongly consistent reads for the query.
479
+ * Use this method when you need to:
480
+ * - Get real-time dinosaur status updates
481
+ * - Monitor critical security systems
482
+ * - Track immediate habitat changes
483
+ * - Verify containment protocols
484
+ *
485
+ * Note:
486
+ * - Consistent reads are not available on GSIs
487
+ * - Consistent reads consume twice the throughput
488
+ * - Default is eventually consistent reads
489
+ *
490
+ * @example
491
+ * ```ts
492
+ * // Check immediate dinosaur status
493
+ * const result = await new QueryBuilder(executor, eq('species', 'Velociraptor'))
494
+ * .filter(op => op.eq('status', 'ACTIVE'))
495
+ * .consistentRead()
496
+ * .execute();
497
+ *
498
+ * // Monitor security breaches
499
+ * const result = await new QueryBuilder(executor, eq('type', 'SECURITY_ALERT'))
500
+ * .useIndex('primary-index')
501
+ * .consistentRead(isEmergencyMode)
502
+ * .execute();
503
+ * ```
504
+ *
505
+ * @param consistentRead - Whether to use consistent reads (defaults to true)
506
+ * @returns The builder instance for method chaining
507
+ */
508
+ consistentRead(consistentRead = true) {
509
+ this.options.consistentRead = consistentRead;
141
510
  return this;
142
511
  }
143
- whereEquals(field, value) {
144
- return this.where(field, "=", value);
512
+ /**
513
+ * Adds a filter expression to the query.
514
+ * Use this method when you need to:
515
+ * - Filter results based on non-key attributes
516
+ * - Apply complex filtering conditions
517
+ * - Combine multiple filter conditions
518
+ *
519
+ * Note: Filter expressions are applied after the key condition,
520
+ * so they don't reduce the amount of data read from DynamoDB.
521
+ *
522
+ * @example
523
+ * ```ts
524
+ * // Simple filter
525
+ * builder.filter(op => op.eq('status', 'ACTIVE'))
526
+ *
527
+ * // Complex filter with multiple conditions
528
+ * builder.filter(op =>
529
+ * op.and([
530
+ * op.gt('amount', 1000),
531
+ * op.beginsWith('category', 'ELECTRONICS'),
532
+ * op.attributeExists('reviewDate')
533
+ * ])
534
+ * )
535
+ * ```
536
+ *
537
+ * @param condition - Either a Condition object or a callback function that builds the condition
538
+ * @returns The builder instance for method chaining
539
+ */
540
+ /**
541
+ * Adds a filter expression to refine the query results.
542
+ * Use this method when you need to:
543
+ * - Filter dinosaurs by behavior patterns
544
+ * - Find habitats with specific conditions
545
+ * - Search for security incidents
546
+ * - Monitor feeding patterns
547
+ *
548
+ * @example
549
+ * ```typescript
550
+ * // Find aggressive carnivores
551
+ * builder.filter(op =>
552
+ * op.and([
553
+ * op.eq('diet', 'CARNIVORE'),
554
+ * op.gt('aggressionLevel', 7),
555
+ * op.eq('status', 'ACTIVE')
556
+ * ])
557
+ * );
558
+ *
559
+ * // Search suitable breeding habitats
560
+ * builder.filter(op =>
561
+ * op.and([
562
+ * op.between('temperature', 25, 30),
563
+ * op.lt('currentOccupants', 3),
564
+ * op.eq('quarantineStatus', 'CLEAR')
565
+ * ])
566
+ * );
567
+ * ```
568
+ *
569
+ * @param condition - Either a Condition object or a callback function that builds the condition
570
+ * @returns The builder instance for method chaining
571
+ */
572
+ filter(condition) {
573
+ if (typeof condition === "function") {
574
+ const conditionOperator = {
575
+ eq,
576
+ ne,
577
+ lt,
578
+ lte,
579
+ gt,
580
+ gte,
581
+ between,
582
+ beginsWith,
583
+ contains,
584
+ attributeExists,
585
+ attributeNotExists,
586
+ and,
587
+ or,
588
+ not
589
+ };
590
+ this.options.filter = condition(conditionOperator);
591
+ } else {
592
+ this.options.filter = condition;
593
+ }
594
+ return this;
145
595
  }
146
- whereBetween(field, start, end) {
147
- return this.where(field, "BETWEEN", [start, end]);
596
+ /**
597
+ * Specifies which attributes to return in the query results.
598
+ * Use this method when you need to:
599
+ * - Reduce data transfer by selecting specific attributes
600
+ * - Optimize response size
601
+ * - Focus on relevant attributes only
602
+ *
603
+ * Note: Using projection can significantly reduce the amount
604
+ * of data returned and lower your costs.
605
+ *
606
+ * @example
607
+ * ```ts
608
+ * // Select single attribute
609
+ * builder.select('email')
610
+ *
611
+ * // Select multiple attributes
612
+ * builder.select(['id', 'name', 'email'])
613
+ *
614
+ * // Chain multiple select calls
615
+ * builder
616
+ * .select('id')
617
+ * .select(['name', 'email'])
618
+ * ```
619
+ *
620
+ * @param fields - A single field name or an array of field names to return
621
+ * @returns The builder instance for method chaining
622
+ */
623
+ /**
624
+ * Specifies which attributes to return in the query results.
625
+ * Use this method when you need to:
626
+ * - Get specific dinosaur attributes
627
+ * - Retrieve habitat statistics
628
+ * - Monitor security metrics
629
+ * - Optimize response size
630
+ *
631
+ * @example
632
+ * ```typescript
633
+ * // Get basic dinosaur info
634
+ * builder.select([
635
+ * 'species',
636
+ * 'status',
637
+ * 'stats.health',
638
+ * 'stats.aggressionLevel'
639
+ * ]);
640
+ *
641
+ * // Monitor habitat conditions
642
+ * builder
643
+ * .select('securityStatus')
644
+ * .select([
645
+ * 'currentOccupants',
646
+ * 'temperature',
647
+ * 'lastInspectionDate'
648
+ * ]);
649
+ * ```
650
+ *
651
+ * @param fields - A single field name or an array of field names to return
652
+ * @returns The builder instance for method chaining
653
+ */
654
+ select(fields) {
655
+ if (typeof fields === "string") {
656
+ this.selectedFields.add(fields);
657
+ } else if (Array.isArray(fields)) {
658
+ for (const field of fields) {
659
+ this.selectedFields.add(field);
660
+ }
661
+ }
662
+ this.options.projection = Array.from(this.selectedFields);
663
+ return this;
148
664
  }
149
- whereIn(field, values) {
150
- return this.where(field, "IN", values);
665
+ /**
666
+ * Sets the query to return items in ascending order by sort key.
667
+ * Use this method when you need to:
668
+ * - Retrieve items in natural order (e.g., timestamps, versions)
669
+ * - Implement chronological ordering
670
+ * - Get oldest items first
671
+ *
672
+ * Note: This is the default behavior if no sort order is specified.
673
+ *
674
+ * @example
675
+ * ```typescript
676
+ * // Get orders in chronological order
677
+ * const result = await new QueryBuilder(executor, eq('userId', '123'))
678
+ * .sortAscending()
679
+ * .execute();
680
+ *
681
+ * // Get events from oldest to newest
682
+ * const result = await new QueryBuilder(executor, eq('entityId', 'order-123'))
683
+ * .useIndex('entity-timestamp-index')
684
+ * .sortAscending()
685
+ * .execute();
686
+ * ```
687
+ *
688
+ * @returns The builder instance for method chaining
689
+ */
690
+ /**
691
+ * Sets the query to return items in ascending order by sort key.
692
+ * Use this method when you need to:
693
+ * - List dinosaurs by age (youngest first)
694
+ * - View incidents chronologically
695
+ * - Track feeding schedule progression
696
+ * - Monitor habitat inspections
697
+ *
698
+ * @example
699
+ * ```typescript
700
+ * // List dinosaurs by age
701
+ * const result = await new QueryBuilder(executor, eq('species', 'Velociraptor'))
702
+ * .useIndex('age-index')
703
+ * .sortAscending()
704
+ * .execute();
705
+ *
706
+ * // View incidents chronologically
707
+ * const result = await new QueryBuilder(executor, eq('type', 'SECURITY_BREACH'))
708
+ * .useIndex('date-index')
709
+ * .sortAscending()
710
+ * .execute();
711
+ * ```
712
+ *
713
+ * @returns The builder instance for method chaining
714
+ */
715
+ sortAscending() {
716
+ this.options.scanIndexForward = true;
717
+ return this;
151
718
  }
152
- whereLessThan(field, value) {
153
- return this.where(field, "<", value);
719
+ /**
720
+ * Sets the query to return items in descending order by sort key.
721
+ * Use this method when you need to:
722
+ * - Get most recent security breaches
723
+ * - Find oldest dinosaurs first
724
+ * - Check latest habitat modifications
725
+ * - Monitor recent feeding events
726
+ *
727
+ * @example
728
+ * ```typescript
729
+ * // Get most recent security incidents
730
+ * const result = await new QueryBuilder(executor, eq('type', 'SECURITY_BREACH'))
731
+ * .useIndex('date-index')
732
+ * .sortDescending()
733
+ * .limit(10)
734
+ * .execute();
735
+ *
736
+ * // Check latest dinosaur activities
737
+ * const result = await new QueryBuilder(executor, eq('species', 'Velociraptor'))
738
+ * .useIndex('activity-time-index')
739
+ * .filter(op => op.eq('status', 'ACTIVE'))
740
+ * .sortDescending()
741
+ * .execute();
742
+ * ```
743
+ *
744
+ * @returns The builder instance for method chaining
745
+ */
746
+ sortDescending() {
747
+ this.options.scanIndexForward = false;
748
+ return this;
154
749
  }
155
- whereLessThanOrEqual(field, value) {
156
- return this.where(field, "<=", value);
750
+ /**
751
+ * Creates a paginator that handles DynamoDB pagination automatically.
752
+ * Use this method when you need to:
753
+ * - Browse large dinosaur collections
754
+ * - View habitat inspection history
755
+ * - Monitor security incidents
756
+ * - Track feeding patterns
757
+ *
758
+ * The paginator handles:
759
+ * - Tracking the last evaluated key
760
+ * - Managing page boundaries
761
+ * - Respecting overall query limits
762
+ *
763
+ * @example
764
+ * ```typescript
765
+ * // List dinosaurs by species
766
+ * const paginator = new QueryBuilder(executor, eq('species', 'Velociraptor'))
767
+ * .filter(op => op.eq('status', 'ACTIVE'))
768
+ * .useIndex('species-index')
769
+ * .paginate(10);
770
+ *
771
+ * // Process pages of security incidents
772
+ * const paginator = new QueryBuilder(executor, eq('type', 'SECURITY_BREACH'))
773
+ * .filter(op => op.gt('severityLevel', 7))
774
+ * .sortDescending()
775
+ * .paginate(25);
776
+ *
777
+ * while (paginator.hasNextPage()) {
778
+ * const page = await paginator.getNextPage();
779
+ * console.log(`Processing incidents page ${page.page}, count: ${page.items.length}`);
780
+ * // Handle security incidents
781
+ * }
782
+ * ```
783
+ *
784
+ * @param pageSize - The number of items to return per page
785
+ * @returns A Paginator instance that manages the pagination state
786
+ * @see Paginator for more pagination control options
787
+ */
788
+ paginate(pageSize) {
789
+ return new Paginator(this, pageSize);
157
790
  }
158
- whereGreaterThan(field, value) {
159
- return this.where(field, ">", value);
791
+ /**
792
+ * Sets the starting point for the query using a previous lastEvaluatedKey.
793
+ * Use this method when you need to:
794
+ * - Implement manual dinosaur list pagination
795
+ * - Resume habitat inspection reviews
796
+ * - Continue security incident analysis
797
+ * - Store query position between sessions
798
+ *
799
+ * Note: This method is typically used for manual pagination.
800
+ * For automatic pagination, use the paginate() method instead.
801
+ *
802
+ * @example
803
+ * ```typescript
804
+ * // First batch of dinosaurs
805
+ * const result1 = await new QueryBuilder(executor, eq('species', 'Velociraptor'))
806
+ * .filter(op => op.eq('status', 'ACTIVE'))
807
+ * .limit(5)
808
+ * .execute();
809
+ *
810
+ * if (result1.lastEvaluatedKey) {
811
+ * // Continue listing dinosaurs
812
+ * const result2 = await new QueryBuilder(executor, eq('species', 'Velociraptor'))
813
+ * .filter(op => op.eq('status', 'ACTIVE'))
814
+ * .startFrom(result1.lastEvaluatedKey)
815
+ * .limit(5)
816
+ * .execute();
817
+ *
818
+ * console.log('Additional dinosaurs:', result2.items);
819
+ * }
820
+ * ```
821
+ *
822
+ * @param lastEvaluatedKey - The exclusive start key from a previous query result
823
+ * @returns The builder instance for method chaining
824
+ */
825
+ startFrom(lastEvaluatedKey) {
826
+ this.options.lastEvaluatedKey = lastEvaluatedKey;
827
+ return this;
160
828
  }
161
- whereGreaterThanOrEqual(field, value) {
162
- return this.where(field, ">=", value);
829
+ /**
830
+ * Creates a deep clone of this QueryBuilder instance.
831
+ * Use this method when you need to:
832
+ * - Query different dinosaur statuses
833
+ * - Check multiple habitat conditions
834
+ * - Monitor various security levels
835
+ * - Create report templates
836
+ *
837
+ * This is particularly useful when:
838
+ * - Implementing pagination (used internally by paginate())
839
+ * - Creating query templates
840
+ * - Running multiple variations of a query
841
+ *
842
+ * @example
843
+ * ```typescript
844
+ * // Create base dinosaur query
845
+ * const baseQuery = new QueryBuilder(executor, eq('species', 'Velociraptor'))
846
+ * .useIndex('status-index')
847
+ * .select(['id', 'status', 'location']);
848
+ *
849
+ * // Check active dinosaurs
850
+ * const activeRaptors = baseQuery.clone()
851
+ * .filter(op => op.eq('status', 'HUNTING'))
852
+ * .execute();
853
+ *
854
+ * // Check contained dinosaurs
855
+ * const containedRaptors = baseQuery.clone()
856
+ * .filter(op => op.eq('status', 'CONTAINED'))
857
+ * .execute();
858
+ *
859
+ * // Check sedated dinosaurs
860
+ * const sedatedRaptors = baseQuery.clone()
861
+ * .filter(op => op.eq('status', 'SEDATED'))
862
+ * .execute();
863
+ * ```
864
+ *
865
+ * @returns A new QueryBuilder instance with the same configuration
866
+ */
867
+ clone() {
868
+ const clone = new _QueryBuilder(this.executor, this.keyCondition);
869
+ clone.options = { ...this.options };
870
+ clone.selectedFields = new Set(this.selectedFields);
871
+ return clone;
163
872
  }
164
- whereNotEqual(field, value) {
165
- return this.where(field, "<>", value);
873
+ /**
874
+ * Executes the query against DynamoDB.
875
+ * Use this method when you need to:
876
+ * - Find specific dinosaur groups
877
+ * - Check habitat conditions
878
+ * - Monitor security incidents
879
+ * - Track feeding patterns
880
+ *
881
+ * The method returns both the matched items and, if there are more results,
882
+ * a lastEvaluatedKey that can be used with startFrom() to continue the query.
883
+ *
884
+ * @example
885
+ * ```typescript
886
+ * try {
887
+ * // Find active carnivores in specific habitat
888
+ * const result = await new QueryBuilder(executor, eq('habitatId', 'PADDOCK-A'))
889
+ * .useIndex('species-status-index')
890
+ * .filter(op =>
891
+ * op.and([
892
+ * op.eq('diet', 'CARNIVORE'),
893
+ * op.eq('status', 'ACTIVE'),
894
+ * op.gt('aggressionLevel', 7)
895
+ * ])
896
+ * )
897
+ * .sortDescending()
898
+ * .limit(5)
899
+ * .execute();
900
+ *
901
+ * console.log(`Found ${result.items.length} dangerous dinosaurs`);
902
+ *
903
+ * if (result.lastEvaluatedKey) {
904
+ * console.log('Additional threats detected');
905
+ * }
906
+ * } catch (error) {
907
+ * console.error('Security scan failed:', error);
908
+ * }
909
+ * ```
910
+ *
911
+ * @returns A promise that resolves to an object containing:
912
+ * - items: Array of items matching the query
913
+ * - lastEvaluatedKey: Token for continuing the query, if more items exist
914
+ */
915
+ async execute() {
916
+ return this.executor(this.keyCondition, this.options);
166
917
  }
167
- whereBeginsWith(field, value) {
168
- return this.where(field, "begins_with", value);
918
+ };
919
+
920
+ // src/utils/debug-expression.ts
921
+ function debugCommand(command) {
922
+ const result = {};
923
+ function replaceAliases(expressionString) {
924
+ if (!expressionString) {
925
+ return expressionString;
926
+ }
927
+ let replacedString = expressionString;
928
+ for (const alias in command.expressionAttributeNames) {
929
+ const attributeName = command.expressionAttributeNames[alias];
930
+ const regex = new RegExp(alias, "g");
931
+ replacedString = replacedString.replace(regex, attributeName);
932
+ }
933
+ for (const alias in command.expressionAttributeValues) {
934
+ let attributeValue = command.expressionAttributeValues[alias];
935
+ if (attributeValue instanceof Set) {
936
+ const array = Array.from(attributeValue);
937
+ attributeValue = `Set(${array.length}){${array.map((v) => JSON.stringify(v)).join(", ")}}`;
938
+ } else {
939
+ attributeValue = JSON.stringify(attributeValue);
940
+ }
941
+ const regex = new RegExp(alias, "g");
942
+ replacedString = replacedString.replace(regex, attributeValue);
943
+ }
944
+ return replacedString;
169
945
  }
170
- whereContains(field, value) {
171
- return this.where(field, "contains", value);
946
+ if (command.updateExpression) {
947
+ result.updateExpression = replaceAliases(command.updateExpression);
172
948
  }
173
- whereNotContains(field, value) {
174
- this.conditions.push({ field, operator: "not_contains", value });
175
- return this;
949
+ if (command.conditionExpression) {
950
+ result.conditionExpression = replaceAliases(command.conditionExpression);
176
951
  }
177
- whereAttributeType(field, value) {
178
- this.conditions.push({ field, operator: "attribute_type", value });
179
- return this;
952
+ if (command.filterExpression) {
953
+ result.filterExpression = replaceAliases(command.filterExpression);
180
954
  }
181
- whereSize(field, value) {
182
- this.conditions.push({ field, operator: "size", value });
183
- return this;
955
+ if (command.keyConditionExpression) {
956
+ result.keyConditionExpression = replaceAliases(command.keyConditionExpression);
184
957
  }
185
- buildConditionExpression() {
186
- return this.expressionBuilder.createExpression(this.conditions);
958
+ if (command.projectionExpression) {
959
+ result.projectionExpression = replaceAliases(command.projectionExpression);
187
960
  }
188
- };
961
+ return {
962
+ raw: command,
963
+ readable: result
964
+ };
965
+ }
189
966
 
190
967
  // src/builders/put-builder.ts
191
- var PutBuilder = class extends OperationBuilder {
192
- constructor(item, expressionBuilder, onBuild) {
193
- super(expressionBuilder);
194
- this.onBuild = onBuild;
968
+ var PutBuilder = class {
969
+ item;
970
+ options = {};
971
+ executor;
972
+ tableName;
973
+ constructor(executor, item, tableName) {
974
+ this.executor = executor;
195
975
  this.item = item;
976
+ this.tableName = tableName;
196
977
  }
197
- item;
198
- set(field, value) {
199
- this.item[field] = value;
978
+ /**
979
+ * Adds a condition that must be satisfied for the put operation to succeed.
980
+ * Use this method when you need to:
981
+ * - Prevent overwriting existing items (optimistic locking)
982
+ * - Ensure items meet certain criteria before replacement
983
+ * - Implement complex business rules for item updates
984
+ *
985
+ * @example
986
+ * ```ts
987
+ * // Ensure item doesn't exist (insert only)
988
+ * builder.condition(op => op.attributeNotExists('id'))
989
+ *
990
+ * // Complex condition with version check
991
+ * builder.condition(op =>
992
+ * op.and([
993
+ * op.attributeExists('id'),
994
+ * op.eq('version', currentVersion),
995
+ * op.eq('status', 'ACTIVE')
996
+ * ])
997
+ * )
998
+ * ```
999
+ *
1000
+ * @param condition - Either a Condition object or a callback function that builds the condition
1001
+ * @returns The builder instance for method chaining
1002
+ */
1003
+ /**
1004
+ * Adds a condition that must be satisfied for the put operation to succeed.
1005
+ * Use this method when you need to:
1006
+ * - Prevent duplicate dinosaur entries
1007
+ * - Ensure habitat requirements
1008
+ * - Validate security protocols
1009
+ *
1010
+ * @example
1011
+ * ```typescript
1012
+ * // Ensure unique dinosaur ID
1013
+ * builder.condition(op =>
1014
+ * op.attributeNotExists('id')
1015
+ * );
1016
+ *
1017
+ * // Verify habitat requirements
1018
+ * builder.condition(op =>
1019
+ * op.and([
1020
+ * op.eq('securityStatus', 'READY'),
1021
+ * op.attributeExists('lastInspection'),
1022
+ * op.gt('securityLevel', 5)
1023
+ * ])
1024
+ * );
1025
+ *
1026
+ * // Check breeding facility conditions
1027
+ * builder.condition(op =>
1028
+ * op.and([
1029
+ * op.between('temperature', 25, 30),
1030
+ * op.between('humidity', 60, 80),
1031
+ * op.eq('quarantineStatus', 'CLEAR')
1032
+ * ])
1033
+ * );
1034
+ * ```
1035
+ *
1036
+ * @param condition - Either a Condition object or a callback function that builds the condition
1037
+ * @returns The builder instance for method chaining
1038
+ */
1039
+ condition(condition) {
1040
+ if (typeof condition === "function") {
1041
+ const conditionOperator = {
1042
+ eq,
1043
+ ne,
1044
+ lt,
1045
+ lte,
1046
+ gt,
1047
+ gte,
1048
+ between,
1049
+ beginsWith,
1050
+ contains,
1051
+ attributeExists,
1052
+ attributeNotExists,
1053
+ and,
1054
+ or,
1055
+ not
1056
+ };
1057
+ this.options.condition = condition(conditionOperator);
1058
+ } else {
1059
+ this.options.condition = condition;
1060
+ }
200
1061
  return this;
201
1062
  }
202
- setMany(attributes) {
203
- this.item = { ...this.item, ...attributes };
1063
+ /**
1064
+ * Sets whether to return the item's previous values (if it existed).
1065
+ * Use this method when you need to:
1066
+ * - Track dinosaur profile updates
1067
+ * - Monitor habitat modifications
1068
+ * - Maintain change history
1069
+ *
1070
+ * @example
1071
+ * ```ts
1072
+ * // Get previous dinosaur state
1073
+ * const result = await builder
1074
+ * .returnValues('ALL_OLD')
1075
+ * .execute();
1076
+ *
1077
+ * if (result) {
1078
+ * console.log('Previous profile:', {
1079
+ * species: result.species,
1080
+ * status: result.status,
1081
+ * stats: {
1082
+ * health: result.stats.health,
1083
+ * threatLevel: result.stats.threatLevel
1084
+ * }
1085
+ * });
1086
+ * }
1087
+ * ```
1088
+ *
1089
+ * @param returnValues - Use 'ALL_OLD' to return previous values, or 'NONE' (default)
1090
+ * @returns The builder instance for method chaining
1091
+ */
1092
+ returnValues(returnValues) {
1093
+ this.options.returnValues = returnValues;
204
1094
  return this;
205
1095
  }
206
- build() {
207
- const { expression, attributes } = this.buildConditionExpression();
1096
+ /**
1097
+ * Generate the DynamoDB command parameters
1098
+ */
1099
+ toDynamoCommand() {
1100
+ const { expression, names, values } = prepareExpressionParams(this.options.condition);
208
1101
  return {
209
- type: "put",
1102
+ tableName: this.tableName,
210
1103
  item: this.item,
211
- condition: expression ? {
212
- expression,
213
- names: attributes.names,
214
- values: attributes.values
215
- } : void 0
1104
+ conditionExpression: expression,
1105
+ expressionAttributeNames: names,
1106
+ expressionAttributeValues: values,
1107
+ returnValues: this.options.returnValues
216
1108
  };
217
1109
  }
218
1110
  /**
219
- * Runs the put operation to insert the provided attributes into the table.
1111
+ * Adds this put operation to a transaction.
1112
+ * Use this method when you need to:
1113
+ * - Transfer dinosaurs between habitats
1114
+ * - Initialize new breeding programs
1115
+ * - Update multiple facility records
1116
+ *
1117
+ * @example
1118
+ * ```ts
1119
+ * const transaction = new TransactionBuilder();
1120
+ *
1121
+ * // Add dinosaur to new habitat
1122
+ * new PutBuilder(executor, {
1123
+ * id: 'TREX-002',
1124
+ * location: 'PADDOCK-B',
1125
+ * status: 'ACTIVE',
1126
+ * transferDate: new Date().toISOString()
1127
+ * }, 'dinosaurs')
1128
+ * .withTransaction(transaction);
1129
+ *
1130
+ * // Update habitat records
1131
+ * new UpdateBuilder(executor, 'habitats', { id: 'PADDOCK-B' })
1132
+ * .add('occupants', 1)
1133
+ * .set('lastTransfer', new Date().toISOString())
1134
+ * .withTransaction(transaction);
1135
+ *
1136
+ * // Execute transfer atomically
1137
+ * await transaction.execute();
1138
+ * ```
1139
+ *
1140
+ * @param transaction - The transaction builder to add this operation to
1141
+ * @returns The builder instance for method chaining
1142
+ */
1143
+ withTransaction(transaction) {
1144
+ const command = this.toDynamoCommand();
1145
+ transaction.putWithCommand(command);
1146
+ return this;
1147
+ }
1148
+ /**
1149
+ * Executes the put operation against DynamoDB.
1150
+ *
1151
+ * @example
1152
+ * ```ts
1153
+ * try {
1154
+ * // Put with condition and return old values
1155
+ * const result = await new PutBuilder(executor, newItem, 'myTable')
1156
+ * .condition(op => op.eq('version', 1))
1157
+ * .returnValues('ALL_OLD')
1158
+ * .execute();
1159
+ *
1160
+ * console.log('Put successful, old item:', result);
1161
+ * } catch (error) {
1162
+ * // Handle condition check failure or other errors
1163
+ * console.error('Put failed:', error);
1164
+ * }
1165
+ * ```
220
1166
  *
221
- * @returns The provided attributes. This does not load the model from the DB after insert
1167
+ * @returns A promise that resolves to the operation result (type depends on returnValues setting)
1168
+ * @throws Will throw an error if the condition check fails or other DynamoDB errors occur
222
1169
  */
223
1170
  async execute() {
224
- return this.onBuild(this.build());
1171
+ const params = this.toDynamoCommand();
1172
+ return this.executor(params);
1173
+ }
1174
+ /**
1175
+ * Gets a human-readable representation of the put command
1176
+ * with all expression placeholders replaced by their actual values.
1177
+ * Use this method when you need to:
1178
+ * - Debug complex dinosaur transfers
1179
+ * - Verify habitat assignments
1180
+ * - Log security protocols
1181
+ * - Troubleshoot breeding program conditions
1182
+ *
1183
+ * @example
1184
+ * ```ts
1185
+ * const debugInfo = new PutBuilder(executor, {
1186
+ * id: 'RAPTOR-003',
1187
+ * species: 'Velociraptor',
1188
+ * status: 'QUARANTINE',
1189
+ * stats: {
1190
+ * health: 100,
1191
+ * aggressionLevel: 7,
1192
+ * age: 2
1193
+ * }
1194
+ * }, 'dinosaurs')
1195
+ * .condition(op =>
1196
+ * op.and([
1197
+ * op.attributeNotExists('id'),
1198
+ * op.eq('quarantineStatus', 'READY'),
1199
+ * op.gt('securityLevel', 8)
1200
+ * ])
1201
+ * )
1202
+ * .debug();
1203
+ *
1204
+ * console.log('Dinosaur transfer command:', debugInfo);
1205
+ * ```
1206
+ *
1207
+ * @returns A readable representation of the put command with resolved expressions
1208
+ */
1209
+ debug() {
1210
+ const command = this.toDynamoCommand();
1211
+ return debugCommand(command);
225
1212
  }
226
1213
  };
227
1214
 
228
- // src/builders/query-builder.ts
229
- var QueryBuilder = class extends OperationBuilder {
230
- constructor(key, indexConfig, expressionBuilder, onBuild) {
231
- super(expressionBuilder);
1215
+ // src/builders/delete-builder.ts
1216
+ var DeleteBuilder = class {
1217
+ options = {
1218
+ returnValues: "ALL_OLD"
1219
+ };
1220
+ executor;
1221
+ tableName;
1222
+ key;
1223
+ constructor(executor, tableName, key) {
1224
+ this.executor = executor;
1225
+ this.tableName = tableName;
232
1226
  this.key = key;
233
- this.indexConfig = indexConfig;
234
- this.onBuild = onBuild;
235
1227
  }
236
- limitValue;
237
- indexNameValue;
238
- limit(value) {
239
- this.limitValue = value;
1228
+ /**
1229
+ * Adds a condition that must be satisfied for the delete operation to succeed.
1230
+ * Use this method when you need to:
1231
+ * - Implement optimistic locking (e.g., version check)
1232
+ * - Ensure item exists before deletion
1233
+ * - Validate item state before deletion
1234
+ *
1235
+ * @example
1236
+ * ```ts
1237
+ * // Simple condition
1238
+ * builder.condition(op => op.attributeExists('id'))
1239
+ *
1240
+ * // Complex condition with version check
1241
+ * builder.condition(op =>
1242
+ * op.and([
1243
+ * op.eq('version', 1),
1244
+ * op.eq('status', 'ACTIVE')
1245
+ * ])
1246
+ * )
1247
+ * ```
1248
+ *
1249
+ * @param condition - Either a Condition object or a callback function that builds the condition
1250
+ * @returns The builder instance for method chaining
1251
+ */
1252
+ /**
1253
+ * Adds a condition that must be satisfied for the delete operation to succeed.
1254
+ * Use this method when you need to:
1255
+ * - Ensure safe removal conditions
1256
+ * - Verify habitat status before deletion
1257
+ * - Implement safety protocols
1258
+ *
1259
+ * @example
1260
+ * ```typescript
1261
+ * // Ensure dinosaur can be safely removed
1262
+ * builder.condition(op =>
1263
+ * op.and([
1264
+ * op.eq('status', 'SEDATED'),
1265
+ * op.eq('location', 'MEDICAL_BAY'),
1266
+ * op.attributeExists('lastCheckup')
1267
+ * ])
1268
+ * );
1269
+ *
1270
+ * // Verify habitat is empty
1271
+ * builder.condition(op =>
1272
+ * op.and([
1273
+ * op.eq('occupants', 0),
1274
+ * op.eq('maintenanceStatus', 'COMPLETE'),
1275
+ * op.not(op.attributeExists('activeAlerts'))
1276
+ * ])
1277
+ * );
1278
+ * ```
1279
+ *
1280
+ * @param condition - Either a Condition object or a callback function that builds the condition
1281
+ * @returns The builder instance for method chaining
1282
+ */
1283
+ condition(condition) {
1284
+ if (typeof condition === "function") {
1285
+ const conditionOperator = {
1286
+ eq,
1287
+ ne,
1288
+ lt,
1289
+ lte,
1290
+ gt,
1291
+ gte,
1292
+ between,
1293
+ beginsWith,
1294
+ contains,
1295
+ attributeExists,
1296
+ attributeNotExists,
1297
+ and,
1298
+ or,
1299
+ not
1300
+ };
1301
+ this.options.condition = condition(conditionOperator);
1302
+ } else {
1303
+ this.options.condition = condition;
1304
+ }
240
1305
  return this;
241
1306
  }
242
- useIndex(indexName) {
243
- this.indexNameValue = indexName;
1307
+ /**
1308
+ * Sets whether to return the item's attribute values before deletion.
1309
+ * Use this method when you need to:
1310
+ * - Archive removed dinosaur data
1311
+ * - Track habitat decommissioning history
1312
+ * - Maintain removal audit logs
1313
+ *
1314
+ * @example
1315
+ * ```ts
1316
+ * // Archive dinosaur data before removal
1317
+ * const result = await builder
1318
+ * .returnValues('ALL_OLD')
1319
+ * .execute();
1320
+ *
1321
+ * if (result.item) {
1322
+ * console.log('Removed dinosaur data:', {
1323
+ * species: result.item.species,
1324
+ * age: result.item.age,
1325
+ * lastLocation: result.item.location
1326
+ * });
1327
+ * }
1328
+ * ```
1329
+ *
1330
+ * @param returnValues - Use 'ALL_OLD' to return all attributes of the deleted item
1331
+ * @returns The builder instance for method chaining
1332
+ */
1333
+ returnValues(returnValues) {
1334
+ this.options.returnValues = returnValues;
244
1335
  return this;
245
1336
  }
246
- build() {
247
- const filter = this.buildConditionExpression();
248
- const keyCondition = this.expressionBuilder.buildKeyCondition(this.key, this.indexConfig);
1337
+ /**
1338
+ * Generate the DynamoDB command parameters
1339
+ */
1340
+ toDynamoCommand() {
1341
+ const { expression, names, values } = prepareExpressionParams(this.options.condition);
249
1342
  return {
250
- type: "query",
251
- keyCondition: {
252
- expression: keyCondition.expression,
253
- names: keyCondition.attributes.names,
254
- values: keyCondition.attributes.values
255
- },
256
- filter: filter.expression ? {
257
- expression: filter.expression,
258
- names: filter.attributes.names,
259
- values: filter.attributes.values
260
- } : void 0,
261
- limit: this.limitValue,
262
- indexName: this.indexNameValue
1343
+ tableName: this.tableName,
1344
+ key: this.key,
1345
+ conditionExpression: expression,
1346
+ expressionAttributeNames: names,
1347
+ expressionAttributeValues: values,
1348
+ returnValues: this.options.returnValues
263
1349
  };
264
1350
  }
1351
+ /**
1352
+ * Adds this delete operation to a transaction.
1353
+ * Use this method when you need to:
1354
+ * - Coordinate dinosaur transfers
1355
+ * - Manage habitat decommissioning
1356
+ * - Handle species relocations
1357
+ *
1358
+ * @example
1359
+ * ```ts
1360
+ * const transaction = new TransactionBuilder();
1361
+ *
1362
+ * // Remove dinosaur from old habitat
1363
+ * new DeleteBuilder(executor, 'dinosaurs', { id: 'RAPTOR-001' })
1364
+ * .condition(op => op.eq('status', 'SEDATED'))
1365
+ * .withTransaction(transaction);
1366
+ *
1367
+ * // Update old habitat occupancy
1368
+ * new UpdateBuilder(executor, 'habitats', { id: 'PADDOCK-A' })
1369
+ * .add('occupants', -1)
1370
+ * .withTransaction(transaction);
1371
+ *
1372
+ * // Execute transfer atomically
1373
+ * await transaction.execute();
1374
+ * ```
1375
+ *
1376
+ * @param transaction - The transaction builder to add this operation to
1377
+ */
1378
+ withTransaction(transaction) {
1379
+ const command = this.toDynamoCommand();
1380
+ transaction.deleteWithCommand(command);
1381
+ }
1382
+ /**
1383
+ * Executes the delete operation against DynamoDB.
1384
+ *
1385
+ * @example
1386
+ * ```ts
1387
+ * // Delete with condition and retrieve old values
1388
+ * const result = await new DeleteBuilder(executor, 'myTable', { id: '123' })
1389
+ * .condition(op => op.eq('status', 'INACTIVE'))
1390
+ * .returnValues('ALL_OLD')
1391
+ * .execute();
1392
+ *
1393
+ * if (result.item) {
1394
+ * console.log('Deleted item:', result.item);
1395
+ * }
1396
+ * ```
1397
+ *
1398
+ * @returns A promise that resolves to an object containing the deleted item's attributes (if returnValues is 'ALL_OLD')
1399
+ */
265
1400
  async execute() {
266
- return this.onBuild(this.build());
1401
+ const params = this.toDynamoCommand();
1402
+ return this.executor(params);
1403
+ }
1404
+ /**
1405
+ * Gets a human-readable representation of the delete command
1406
+ * with all expression placeholders replaced by their actual values.
1407
+ * Use this method when you need to:
1408
+ * - Debug complex deletion conditions
1409
+ * - Verify safety checks
1410
+ * - Log removal operations
1411
+ * - Troubleshoot failed deletions
1412
+ *
1413
+ * @example
1414
+ * ```ts
1415
+ * const debugInfo = new DeleteBuilder(executor, 'dinosaurs', { id: 'TREX-001' })
1416
+ * .condition(op => op.and([
1417
+ * op.eq('status', 'SEDATED'),
1418
+ * op.eq('location', 'MEDICAL_BAY'),
1419
+ * op.gt('sedationLevel', 8)
1420
+ * op.eq('version', 1),
1421
+ * op.attributeExists('status')
1422
+ * ]))
1423
+ * .debug();
1424
+ *
1425
+ * console.log('Delete command:', debugInfo);
1426
+ * ```
1427
+ *
1428
+ * @returns A readable representation of the delete command with resolved expressions
1429
+ */
1430
+ debug() {
1431
+ const command = this.toDynamoCommand();
1432
+ return debugCommand(command);
267
1433
  }
268
1434
  };
269
1435
 
270
1436
  // src/builders/update-builder.ts
271
- var UpdateBuilder = class extends OperationBuilder {
272
- constructor(key, expressionBuilder, onBuild) {
273
- super(expressionBuilder);
1437
+ var UpdateBuilder = class {
1438
+ updates = [];
1439
+ options = {
1440
+ returnValues: "ALL_NEW"
1441
+ };
1442
+ executor;
1443
+ tableName;
1444
+ key;
1445
+ constructor(executor, tableName, key) {
1446
+ this.executor = executor;
1447
+ this.tableName = tableName;
274
1448
  this.key = key;
275
- this.onBuild = onBuild;
276
1449
  }
277
- updates = {};
278
- set(field, value) {
279
- this.updates[field] = value;
1450
+ set(valuesOrPath, value) {
1451
+ if (typeof valuesOrPath === "object") {
1452
+ for (const [key, value2] of Object.entries(valuesOrPath)) {
1453
+ this.updates.push({
1454
+ type: "SET",
1455
+ path: key,
1456
+ value: value2
1457
+ });
1458
+ }
1459
+ } else {
1460
+ this.updates.push({
1461
+ type: "SET",
1462
+ path: valuesOrPath,
1463
+ value
1464
+ });
1465
+ }
280
1466
  return this;
281
1467
  }
282
- setMany(attributes) {
283
- this.updates = { ...this.updates, ...attributes };
1468
+ /**
1469
+ * Removes an attribute from the item.
1470
+ * Use this method when you need to:
1471
+ * - Delete attributes completely
1472
+ * - Remove nested attributes
1473
+ * - Clean up deprecated fields
1474
+ *
1475
+ * @example
1476
+ * ```typescript
1477
+ * // Remove simple attributes
1478
+ * builder
1479
+ * .remove('temporaryTag')
1480
+ * .remove('previousLocation');
1481
+ *
1482
+ * // Remove nested attributes
1483
+ * builder
1484
+ * .remove('metadata.testData')
1485
+ * .remove('stats.experimentalMetrics');
1486
+ * ```
1487
+ *
1488
+ * @param path - The path to the attribute to remove
1489
+ * @returns The builder instance for method chaining
1490
+ */
1491
+ remove(path) {
1492
+ this.updates.push({
1493
+ type: "REMOVE",
1494
+ path
1495
+ });
284
1496
  return this;
285
1497
  }
286
- remove(...fields) {
287
- for (const field of fields) {
288
- this.updates[field] = null;
289
- }
1498
+ /**
1499
+ * Adds a value to a number attribute or adds elements to a set.
1500
+ * Use this method when you need to:
1501
+ * - Increment counters
1502
+ * - Add elements to a set atomically
1503
+ * - Update numerical statistics
1504
+ *
1505
+ * @example
1506
+ * ```typescript
1507
+ * // Increment counters
1508
+ * builder
1509
+ * .add('escapeAttempts', 1)
1510
+ * .add('feedingCount', 1);
1511
+ *
1512
+ * // Add to sets
1513
+ * builder
1514
+ * .add('knownBehaviors', new Set(['PACK_HUNTING', 'AMBUSH_TACTICS']))
1515
+ * .add('visitedZones', new Set(['ZONE_A', 'ZONE_B']));
1516
+ * ```
1517
+ *
1518
+ * @param path - The path to the attribute to update
1519
+ * @param value - The value to add (number or set)
1520
+ * @returns The builder instance for method chaining
1521
+ */
1522
+ add(path, value) {
1523
+ this.updates.push({
1524
+ type: "ADD",
1525
+ path,
1526
+ value
1527
+ });
290
1528
  return this;
291
1529
  }
292
- increment(field, by = 1) {
293
- this.updates[field] = { $add: by };
1530
+ /**
1531
+ * Removes elements from a set attribute.
1532
+ * Use this method when you need to:
1533
+ * - Remove specific elements from a set
1534
+ * - Update set-based attributes atomically
1535
+ * - Maintain set membership
1536
+ *
1537
+ * @example
1538
+ * ```typescript
1539
+ * // Remove from sets using arrays
1540
+ * builder.deleteElementsFromSet(
1541
+ * 'allowedHabitats',
1542
+ * ['JUNGLE', 'COASTAL']
1543
+ * );
1544
+ *
1545
+ * // Remove from sets using Set objects
1546
+ * builder.deleteElementsFromSet(
1547
+ * 'knownBehaviors',
1548
+ * new Set(['NOCTURNAL', 'TERRITORIAL'])
1549
+ * );
1550
+ *
1551
+ * // Remove from nested sets
1552
+ * builder.deleteElementsFromSet(
1553
+ * 'stats.compatibleSpecies',
1554
+ * ['VELOCIRAPTOR', 'DILOPHOSAURUS']
1555
+ * );
1556
+ * ```
1557
+ *
1558
+ * @param path - The path to the set attribute
1559
+ * @param value - Elements to remove (array or Set)
1560
+ * @returns The builder instance for method chaining
1561
+ */
1562
+ deleteElementsFromSet(path, value) {
1563
+ let valuesToDelete;
1564
+ if (Array.isArray(value)) {
1565
+ valuesToDelete = new Set(value);
1566
+ } else {
1567
+ valuesToDelete = value;
1568
+ }
1569
+ this.updates.push({
1570
+ type: "DELETE",
1571
+ path,
1572
+ value: valuesToDelete
1573
+ });
294
1574
  return this;
295
1575
  }
296
- build() {
297
- const condition = this.buildConditionExpression();
298
- const update = this.expressionBuilder.buildUpdateExpression(this.updates);
299
- return {
300
- type: "update",
301
- key: this.key,
302
- update: {
303
- expression: update.expression,
304
- names: update.attributes.names,
305
- values: update.attributes.values
306
- },
307
- condition: condition.expression ? {
308
- expression: condition.expression,
309
- names: condition.attributes.names,
310
- values: condition.attributes.values
311
- } : void 0
312
- };
313
- }
314
- async execute() {
315
- return this.onBuild(this.build());
316
- }
317
- };
318
-
319
- // src/errors/dynamo-error.ts
320
- var DynamoError = class _DynamoError extends Error {
321
- constructor(message, originalError, context) {
322
- super(message);
323
- this.originalError = originalError;
324
- this.context = context;
325
- this.name = "DynamoError";
326
- if (Error.captureStackTrace) {
327
- Error.captureStackTrace(this, _DynamoError);
1576
+ /**
1577
+ * Adds a condition that must be satisfied for the update to succeed.
1578
+ * Use this method when you need to:
1579
+ * - Implement optimistic locking
1580
+ * - Ensure item state before update
1581
+ * - Validate business rules
1582
+ * - Prevent concurrent modifications
1583
+ *
1584
+ * @example
1585
+ * ```typescript
1586
+ * // Simple condition
1587
+ * builder.condition(op =>
1588
+ * op.eq('status', 'ACTIVE')
1589
+ * );
1590
+ *
1591
+ * // Health check condition
1592
+ * builder.condition(op =>
1593
+ * op.and([
1594
+ * op.gt('health', 50),
1595
+ * op.eq('status', 'HUNTING')
1596
+ * ])
1597
+ * );
1598
+ *
1599
+ * // Complex security condition
1600
+ * builder.condition(op =>
1601
+ * op.and([
1602
+ * op.attributeExists('securitySystem'),
1603
+ * op.eq('containmentStatus', 'SECURE'),
1604
+ * op.lt('aggressionLevel', 8)
1605
+ * ])
1606
+ * );
1607
+ *
1608
+ * // Version check (optimistic locking)
1609
+ * builder.condition(op =>
1610
+ * op.eq('version', currentVersion)
1611
+ * );
1612
+ * ```
1613
+ *
1614
+ * @param condition - Either a Condition object or a callback function that builds the condition
1615
+ * @returns The builder instance for method chaining
1616
+ */
1617
+ condition(condition) {
1618
+ if (typeof condition === "function") {
1619
+ const conditionOperator = {
1620
+ eq,
1621
+ ne,
1622
+ lt,
1623
+ lte,
1624
+ gt,
1625
+ gte,
1626
+ between,
1627
+ beginsWith,
1628
+ contains,
1629
+ attributeExists,
1630
+ attributeNotExists,
1631
+ and,
1632
+ or,
1633
+ not
1634
+ };
1635
+ this.options.condition = condition(conditionOperator);
1636
+ } else {
1637
+ this.options.condition = condition;
328
1638
  }
1639
+ return this;
329
1640
  }
330
- };
331
- var ConditionalCheckFailedError = class extends DynamoError {
332
- constructor(message, originalError, context) {
333
- super(message, originalError, context);
334
- this.name = "ConditionalCheckFailedError";
335
- }
336
- };
337
- var ResourceNotFoundError = class extends DynamoError {
338
- constructor(message, originalError, context) {
339
- super(message, originalError, context);
340
- this.name = "ResourceNotFoundError";
1641
+ /**
1642
+ * Sets which item attributes to include in the response.
1643
+ * Use this method when you need to:
1644
+ * - Get the complete updated item
1645
+ * - Track changes to specific attributes
1646
+ * - Compare old and new values
1647
+ * - Monitor attribute modifications
1648
+ *
1649
+ * Available options:
1650
+ * - ALL_NEW: All attributes after the update
1651
+ * - UPDATED_NEW: Only updated attributes, new values
1652
+ * - ALL_OLD: All attributes before the update
1653
+ * - UPDATED_OLD: Only updated attributes, old values
1654
+ * - NONE: No attributes returned (default)
1655
+ *
1656
+ * @example
1657
+ * ```typescript
1658
+ * // Get complete updated dinosaur
1659
+ * const result = await builder
1660
+ * .set('status', 'SLEEPING')
1661
+ * .returnValues('ALL_NEW')
1662
+ * .execute();
1663
+ *
1664
+ * // Track specific attribute changes
1665
+ * const result = await builder
1666
+ * .set({
1667
+ * 'stats.health': 100,
1668
+ * 'stats.energy': 95
1669
+ * })
1670
+ * .returnValues('UPDATED_OLD')
1671
+ * .execute();
1672
+ *
1673
+ * if (result.item) {
1674
+ * console.log('Previous health:', result.item.stats?.health);
1675
+ * }
1676
+ * ```
1677
+ *
1678
+ * @param returnValues - Which attributes to return in the response
1679
+ * @returns The builder instance for method chaining
1680
+ */
1681
+ returnValues(returnValues) {
1682
+ this.options.returnValues = returnValues;
1683
+ return this;
341
1684
  }
342
- };
343
-
344
- // src/errors/error-handler.ts
345
- function translateExpression(expression, attributes) {
346
- if (!expression || !attributes) return expression || "";
347
- let translated = expression;
348
- if (attributes.names) {
349
- for (const [alias, name] of Object.entries(attributes.names)) {
350
- translated = translated.replace(new RegExp(alias, "g"), name);
1685
+ /**
1686
+ * Generate the DynamoDB command parameters
1687
+ */
1688
+ toDynamoCommand() {
1689
+ if (this.updates.length === 0) {
1690
+ throw new Error("No update actions specified");
351
1691
  }
352
- }
353
- if (attributes.values) {
354
- for (const [alias, value] of Object.entries(attributes.values)) {
355
- translated = translated.replace(new RegExp(alias, "g"), typeof value === "string" ? `"${value}"` : String(value));
1692
+ const expressionParams = {
1693
+ expressionAttributeNames: {},
1694
+ expressionAttributeValues: {},
1695
+ valueCounter: { count: 0 }
1696
+ };
1697
+ let updateExpression = "";
1698
+ const setUpdates = [];
1699
+ const removeUpdates = [];
1700
+ const addUpdates = [];
1701
+ const deleteUpdates = [];
1702
+ for (const update of this.updates) {
1703
+ switch (update.type) {
1704
+ case "SET":
1705
+ setUpdates.push(update);
1706
+ break;
1707
+ case "REMOVE":
1708
+ removeUpdates.push(update);
1709
+ break;
1710
+ case "ADD":
1711
+ addUpdates.push(update);
1712
+ break;
1713
+ case "DELETE":
1714
+ deleteUpdates.push(update);
1715
+ break;
1716
+ }
356
1717
  }
357
- }
358
- return translated;
359
- }
360
- function buildErrorMessage(context, error) {
361
- const parts = [`DynamoDB ${context.operation} operation failed`];
362
- if (context.tableName) {
363
- parts.push(`
364
- Table: ${context.tableName}`);
365
- }
366
- if (context.key) {
367
- parts.push(`
368
- Key: ${JSON.stringify(context.key, null, 2)}`);
369
- }
370
- if (context.expression) {
371
- const { condition, update, filter, keyCondition } = context.expression;
372
- if (condition) {
373
- parts.push(`
374
- Condition: ${translateExpression(condition, context.attributes)}`);
1718
+ if (setUpdates.length > 0) {
1719
+ updateExpression += "SET ";
1720
+ updateExpression += setUpdates.map((update) => {
1721
+ const attrName = generateAttributeName(expressionParams, update.path);
1722
+ const valueName = generateValueName(expressionParams, update.value);
1723
+ expressionParams.expressionAttributeValues[valueName] = update.value;
1724
+ return `${attrName} = ${valueName}`;
1725
+ }).join(", ");
1726
+ }
1727
+ if (removeUpdates.length > 0) {
1728
+ if (updateExpression) {
1729
+ updateExpression += " ";
1730
+ }
1731
+ updateExpression += "REMOVE ";
1732
+ updateExpression += removeUpdates.map((update) => {
1733
+ return generateAttributeName(expressionParams, update.path);
1734
+ }).join(", ");
375
1735
  }
376
- if (update) {
377
- parts.push(`
378
- Update: ${translateExpression(update, context.attributes)}`);
1736
+ if (addUpdates.length > 0) {
1737
+ if (updateExpression) {
1738
+ updateExpression += " ";
1739
+ }
1740
+ updateExpression += "ADD ";
1741
+ updateExpression += addUpdates.map((update) => {
1742
+ const attrName = generateAttributeName(expressionParams, update.path);
1743
+ const valueName = generateValueName(expressionParams, update.value);
1744
+ return `${attrName} ${valueName}`;
1745
+ }).join(", ");
379
1746
  }
380
- if (filter) {
381
- parts.push(`
382
- Filter: ${translateExpression(filter, context.attributes)}`);
1747
+ if (deleteUpdates.length > 0) {
1748
+ if (updateExpression) {
1749
+ updateExpression += " ";
1750
+ }
1751
+ updateExpression += "DELETE ";
1752
+ updateExpression += deleteUpdates.map((update) => {
1753
+ const attrName = generateAttributeName(expressionParams, update.path);
1754
+ const valueName = generateValueName(expressionParams, update.value);
1755
+ return `${attrName} ${valueName}`;
1756
+ }).join(", ");
383
1757
  }
384
- if (keyCondition) {
385
- parts.push(`
386
- Key Condition: ${translateExpression(keyCondition, context.attributes)}`);
1758
+ let conditionExpression;
1759
+ if (this.options.condition) {
1760
+ conditionExpression = buildExpression(this.options.condition, expressionParams);
387
1761
  }
1762
+ const { expressionAttributeNames, expressionAttributeValues } = expressionParams;
1763
+ return {
1764
+ tableName: this.tableName,
1765
+ key: this.key,
1766
+ updateExpression,
1767
+ conditionExpression,
1768
+ expressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 ? expressionAttributeNames : void 0,
1769
+ expressionAttributeValues: Object.keys(expressionAttributeValues).length > 0 ? expressionAttributeValues : void 0,
1770
+ returnValues: this.options.returnValues
1771
+ };
388
1772
  }
389
- parts.push(`
390
- Original Error: ${error.message}`);
391
- return parts.join("");
392
- }
393
- function handleDynamoError(error, context) {
394
- if (!(error instanceof Error)) {
395
- throw error;
1773
+ /**
1774
+ * Adds this update operation to a transaction.
1775
+ * Use this method when you need to:
1776
+ * - Update items as part of a larger transaction
1777
+ * - Ensure multiple updates are atomic
1778
+ * - Coordinate updates across multiple items
1779
+ *
1780
+ * @example
1781
+ * ```typescript
1782
+ * const transaction = new TransactionBuilder(executor);
1783
+ *
1784
+ * // Update dinosaur status and habitat occupancy atomically
1785
+ * new UpdateBuilder(executor, 'dinosaurs', { id: 'TREX-001' })
1786
+ * .set('location', 'PADDOCK_A')
1787
+ * .set('status', 'CONTAINED')
1788
+ * .withTransaction(transaction);
1789
+ *
1790
+ * new UpdateBuilder(executor, 'habitats', { id: 'PADDOCK-A' })
1791
+ * .add('occupants', 1)
1792
+ * .set('lastOccupied', new Date().toISOString())
1793
+ * .withTransaction(transaction);
1794
+ *
1795
+ * // Execute all operations atomically
1796
+ * await transaction.execute();
1797
+ * ```
1798
+ *
1799
+ * @param transaction - The transaction builder to add this operation to
1800
+ * @returns The builder instance for method chaining
1801
+ */
1802
+ withTransaction(transaction) {
1803
+ const command = this.toDynamoCommand();
1804
+ transaction.updateWithCommand(command);
396
1805
  }
397
- const errorMessage = buildErrorMessage(context, error);
398
- switch (error.name) {
399
- case "ConditionalCheckFailedException":
400
- throw new ConditionalCheckFailedError(errorMessage, error, context);
401
- case "ResourceNotFoundException":
402
- throw new ResourceNotFoundError(errorMessage, error, context);
403
- default:
404
- throw new DynamoError(errorMessage, error, context);
1806
+ /**
1807
+ * Gets a human-readable representation of the update command.
1808
+ * Use this method when you need to:
1809
+ * - Debug complex update expressions
1810
+ * - Verify attribute names and values
1811
+ * - Log update operations
1812
+ * - Troubleshoot condition expressions
1813
+ *
1814
+ * @example
1815
+ * ```typescript
1816
+ * // Create complex update
1817
+ * const builder = new UpdateBuilder(executor, 'dinosaurs', { id: 'RAPTOR-001' })
1818
+ * .set({
1819
+ * status: 'HUNTING',
1820
+ * 'stats.health': 95,
1821
+ * 'behavior.lastObserved': new Date().toISOString()
1822
+ * })
1823
+ * .add('huntingSuccesses', 1)
1824
+ * .condition(op => op.gt('health', 50));
1825
+ *
1826
+ * // Debug the update
1827
+ * const debugInfo = builder.debug();
1828
+ * console.log('Update operation:', debugInfo);
1829
+ * ```
1830
+ *
1831
+ * @returns A readable representation of the update command with resolved expressions
1832
+ */
1833
+ debug() {
1834
+ const command = this.toDynamoCommand();
1835
+ return debugCommand(command);
405
1836
  }
406
- }
407
-
408
- // src/retry/retry-strategy.ts
409
- var RETRYABLE_ERRORS = /* @__PURE__ */ new Set([
410
- "ProvisionedThroughputExceededException",
411
- "ThrottlingException",
412
- "RequestLimitExceeded",
413
- "InternalServerError",
414
- "ServiceUnavailable"
415
- ]);
416
- var isRetryableError = (error) => {
417
- if (!error || typeof error !== "object") return false;
418
- return "name" in error && RETRYABLE_ERRORS.has(error.name);
419
- };
420
-
421
- // src/retry/exponential-backoff-strategy.ts
422
- var ExponentialBackoffStrategy = class {
423
- constructor(maxAttempts = 3, baseDelay = 100, maxDelay = 5e3, jitter = true) {
424
- this.maxAttempts = maxAttempts;
425
- this.baseDelay = baseDelay;
426
- this.maxDelay = maxDelay;
427
- this.jitter = jitter;
428
- }
429
- shouldRetry(error, attempt) {
430
- return attempt < this.maxAttempts && isRetryableError(error);
431
- }
432
- getDelay(attempt) {
433
- const delay = Math.min(this.baseDelay * attempt ** 2, this.maxDelay);
434
- if (!this.jitter) return delay;
435
- return delay * (0.5 + Math.random());
1837
+ /**
1838
+ * Executes the update operation against DynamoDB.
1839
+ * Use this method when you need to:
1840
+ * - Apply updates immediately
1841
+ * - Get the updated item values
1842
+ * - Handle conditional update failures
1843
+ *
1844
+ * @example
1845
+ * ```typescript
1846
+ * try {
1847
+ * // Update dinosaur status with conditions
1848
+ * const result = await new UpdateBuilder(executor, 'dinosaurs', { id: 'TREX-001' })
1849
+ * .set({
1850
+ * status: 'FEEDING',
1851
+ * lastMeal: new Date().toISOString(),
1852
+ * 'stats.hunger': 0
1853
+ * })
1854
+ * .add('feedingCount', 1)
1855
+ * .condition(op =>
1856
+ * op.and([
1857
+ * op.gt('stats.hunger', 80),
1858
+ * op.eq('status', 'HUNTING')
1859
+ * ])
1860
+ * )
1861
+ * .returnValues('ALL_NEW')
1862
+ * .execute();
1863
+ *
1864
+ * if (result.item) {
1865
+ * console.log('Updated dinosaur:', result.item);
1866
+ * }
1867
+ * } catch (error) {
1868
+ * // Handle condition check failure
1869
+ * console.error('Failed to update dinosaur:', error);
1870
+ * // Check if dinosaur wasn't hungry enough
1871
+ * if (error.name === 'ConditionalCheckFailedException') {
1872
+ * console.log('Dinosaur not ready for feeding');
1873
+ * }
1874
+ * }
1875
+ * ```
1876
+ *
1877
+ * @returns A promise that resolves to an object containing the updated item (if returnValues is set)
1878
+ * @throws {ConditionalCheckFailedException} If the condition check fails
1879
+ * @throws {Error} If the update operation fails for other reasons
1880
+ */
1881
+ async execute() {
1882
+ const params = this.toDynamoCommand();
1883
+ return this.executor(params);
436
1884
  }
437
1885
  };
438
1886
 
439
- // src/dynamo/dynamo-converter.ts
440
- var DynamoConverter = class {
441
- constructor(tableName) {
442
- this.tableName = tableName;
1887
+ // src/utils/debug-transaction.ts
1888
+ function debugTransactionItem(item) {
1889
+ const result = {
1890
+ type: item.type,
1891
+ tableName: item.params.tableName
1892
+ };
1893
+ if ("key" in item.params) {
1894
+ result.key = item.params.key;
1895
+ }
1896
+ if (item.type === "Put") {
1897
+ result.item = item.params.item;
1898
+ }
1899
+ switch (item.type) {
1900
+ case "Put":
1901
+ case "Delete":
1902
+ case "ConditionCheck":
1903
+ result.readable = debugCommand(item.params).readable;
1904
+ break;
1905
+ case "Update":
1906
+ result.readable = debugCommand(item.params).readable;
1907
+ break;
1908
+ }
1909
+ return result;
1910
+ }
1911
+ function debugTransaction(items) {
1912
+ return items.map((item) => debugTransactionItem(item));
1913
+ }
1914
+
1915
+ // src/builders/transaction-builder.ts
1916
+ var TransactionBuilder = class {
1917
+ items = [];
1918
+ options = {};
1919
+ indexConfig;
1920
+ executor;
1921
+ constructor(executor, indexConfig) {
1922
+ this.executor = executor;
1923
+ this.indexConfig = indexConfig;
443
1924
  }
444
1925
  /**
445
- * Converts our expression format to DynamoDB expression format
1926
+ * Checks if an item with the same primary key already exists in the transaction
1927
+ * @private
446
1928
  */
447
- convertExpression(expr) {
448
- if (!expr) return {};
449
- return {
450
- ...expr.expression && { Expression: expr.expression },
451
- ...expr.names && { ExpressionAttributeNames: expr.names },
452
- ...expr.values && { ExpressionAttributeValues: expr.values }
453
- };
1929
+ checkForDuplicateItem(tableName, newItem) {
1930
+ const pkName = this.indexConfig.partitionKey;
1931
+ const skName = this.indexConfig.sortKey || "";
1932
+ const pkValue = newItem[pkName];
1933
+ const skValue = skName ? newItem[skName] : void 0;
1934
+ if (!pkValue) {
1935
+ throw new Error(`Primary key value for '${pkName}' is missing`);
1936
+ }
1937
+ const duplicateItem = this.items.find((item) => {
1938
+ let itemKey;
1939
+ let itemTableName;
1940
+ switch (item.type) {
1941
+ case "Put":
1942
+ itemTableName = item.params.tableName;
1943
+ itemKey = item.params.item;
1944
+ break;
1945
+ case "Update":
1946
+ case "Delete":
1947
+ case "ConditionCheck":
1948
+ itemTableName = item.params.tableName;
1949
+ itemKey = item.params.key;
1950
+ break;
1951
+ }
1952
+ if (itemTableName === tableName && itemKey) {
1953
+ const itemPkValue = itemKey[pkName];
1954
+ const itemSkValue = skName ? itemKey[skName] : void 0;
1955
+ if (itemPkValue === pkValue) {
1956
+ if (skValue === void 0 && itemSkValue === void 0) {
1957
+ return true;
1958
+ }
1959
+ if (skValue !== void 0 && itemSkValue !== void 0 && skValue === itemSkValue) {
1960
+ return true;
1961
+ }
1962
+ }
1963
+ }
1964
+ return false;
1965
+ });
1966
+ if (duplicateItem) {
1967
+ throw new Error(
1968
+ `Duplicate item detected in transaction: Table=${tableName}, ${pkName}=${String(pkValue)}, ${skName}=${skValue !== void 0 ? String(skValue) : "undefined"}. DynamoDB transactions do not allow multiple operations on the same item.`
1969
+ );
1970
+ }
454
1971
  }
455
1972
  /**
456
- * Convert our format to DynamoDB put command input
1973
+ * Adds a put operation to the transaction.
1974
+ * Use this method when you need to:
1975
+ * - Insert new items as part of a transaction
1976
+ * - Replace existing items atomically
1977
+ * - Ensure items meet certain conditions before insertion
1978
+ *
1979
+ * The method automatically checks for duplicate items within the transaction
1980
+ * to prevent multiple operations on the same item.
1981
+ *
1982
+ * @example
1983
+ * ```typescript
1984
+ * // Simple put operation
1985
+ * transaction.put('orders', {
1986
+ * orderId: '123',
1987
+ * status: 'PENDING',
1988
+ * amount: 100
1989
+ * });
1990
+ *
1991
+ * // Conditional put operation
1992
+ * transaction.put(
1993
+ * 'inventory',
1994
+ * { productId: 'ABC', quantity: 50 },
1995
+ * op => op.attributeNotExists('productId')
1996
+ * );
1997
+ *
1998
+ * // Put with complex condition
1999
+ * transaction.put(
2000
+ * 'users',
2001
+ * { userId: '123', status: 'ACTIVE' },
2002
+ * op => op.and([
2003
+ * op.attributeNotExists('userId'),
2004
+ * op.beginsWith('status', 'ACTIVE')
2005
+ * ])
2006
+ * );
2007
+ * ```
2008
+ *
2009
+ * @param tableName - The name of the DynamoDB table
2010
+ * @param item - The item to put into the table
2011
+ * @param condition - Optional condition that must be satisfied
2012
+ * @returns The transaction builder for method chaining
2013
+ * @throws {Error} If a duplicate item is detected in the transaction
457
2014
  */
458
- toPutCommand(options) {
459
- return {
460
- TableName: this.tableName,
461
- Item: options.item,
462
- ...options.condition && {
463
- ConditionExpression: options.condition.expression,
464
- ExpressionAttributeNames: options.condition.names,
465
- ExpressionAttributeValues: options.condition.values
2015
+ put(tableName, item, condition) {
2016
+ this.checkForDuplicateItem(tableName, item);
2017
+ const transactionItem = {
2018
+ type: "Put",
2019
+ params: {
2020
+ tableName,
2021
+ item
466
2022
  }
467
2023
  };
2024
+ if (condition) {
2025
+ const { expression, names, values } = prepareExpressionParams(condition);
2026
+ transactionItem.params.conditionExpression = expression;
2027
+ transactionItem.params.expressionAttributeNames = names;
2028
+ transactionItem.params.expressionAttributeValues = values;
2029
+ }
2030
+ this.items.push(transactionItem);
2031
+ return this;
468
2032
  }
469
2033
  /**
470
- * Convert our format to DynamoDB get command input
2034
+ * Adds a pre-configured put operation to the transaction.
2035
+ * Use this method when you need to:
2036
+ * - Reuse put commands from PutBuilder
2037
+ * - Add complex put operations with pre-configured parameters
2038
+ * - Integrate with existing put command configurations
2039
+ *
2040
+ * This method is particularly useful when working with PutBuilder
2041
+ * to maintain consistency in put operations across your application.
2042
+ *
2043
+ * @example
2044
+ * ```typescript
2045
+ * // Create a put command with PutBuilder
2046
+ * const putCommand = new PutBuilder(executor, newItem, 'users')
2047
+ * .condition(op => op.attributeNotExists('userId'))
2048
+ * .toDynamoCommand();
2049
+ *
2050
+ * // Add the command to the transaction
2051
+ * transaction.putWithCommand(putCommand);
2052
+ * ```
2053
+ *
2054
+ * @param command - The complete put command configuration
2055
+ * @returns The transaction builder for method chaining
2056
+ * @throws {Error} If a duplicate item is detected in the transaction
2057
+ * @see PutBuilder for creating put commands
471
2058
  */
472
- toGetCommand(options) {
473
- return {
474
- TableName: this.tableName,
475
- Key: options.key,
476
- ...options.indexName && { IndexName: options.indexName }
2059
+ putWithCommand(command) {
2060
+ this.checkForDuplicateItem(command.tableName, command.item);
2061
+ const transactionItem = {
2062
+ type: "Put",
2063
+ params: command
477
2064
  };
2065
+ this.items.push(transactionItem);
2066
+ return this;
478
2067
  }
479
2068
  /**
480
- * Convert our format to DynamoDB update command input
2069
+ * Adds a delete operation to the transaction.
2070
+ * Use this method when you need to:
2071
+ * - Remove items as part of a transaction
2072
+ * - Conditionally delete items
2073
+ * - Ensure items exist before deletion
2074
+ *
2075
+ * The method automatically checks for duplicate items within the transaction
2076
+ * to prevent multiple operations on the same item.
2077
+ *
2078
+ * @example
2079
+ * ```typescript
2080
+ * // Simple delete operation
2081
+ * transaction.delete('orders', {
2082
+ * pk: 'ORDER#123',
2083
+ * sk: 'METADATA'
2084
+ * });
2085
+ *
2086
+ * // Conditional delete operation
2087
+ * transaction.delete(
2088
+ * 'users',
2089
+ * { pk: 'USER#123' },
2090
+ * op => op.eq('status', 'INACTIVE')
2091
+ * );
2092
+ *
2093
+ * // Delete with complex condition
2094
+ * transaction.delete(
2095
+ * 'products',
2096
+ * { pk: 'PROD#ABC' },
2097
+ * op => op.and([
2098
+ * op.eq('status', 'DRAFT'),
2099
+ * op.lt('version', 5)
2100
+ * ])
2101
+ * );
2102
+ * ```
2103
+ *
2104
+ * @param tableName - The name of the DynamoDB table
2105
+ * @param key - The primary key of the item to delete
2106
+ * @param condition - Optional condition that must be satisfied
2107
+ * @returns The transaction builder for method chaining
2108
+ * @throws {Error} If a duplicate item is detected in the transaction
481
2109
  */
482
- toUpdateCommand(options) {
483
- return {
484
- TableName: this.tableName,
485
- Key: options.key,
486
- UpdateExpression: options.update.expression,
487
- ExpressionAttributeNames: {
488
- ...options.update.names,
489
- ...options.condition?.names
490
- },
491
- ExpressionAttributeValues: {
492
- ...options.update.values,
493
- ...options.condition?.values
494
- },
495
- ...options.condition && {
496
- ConditionExpression: options.condition.expression
497
- },
498
- ...options.returnValues && {
499
- ReturnValues: options.returnValues
2110
+ delete(tableName, key, condition) {
2111
+ this.checkForDuplicateItem(tableName, key);
2112
+ const transactionItem = {
2113
+ type: "Delete",
2114
+ params: {
2115
+ tableName,
2116
+ key: {
2117
+ pk: key.pk,
2118
+ sk: key.sk
2119
+ }
500
2120
  }
501
2121
  };
2122
+ if (condition) {
2123
+ const { expression, names, values } = prepareExpressionParams(condition);
2124
+ transactionItem.params.conditionExpression = expression;
2125
+ transactionItem.params.expressionAttributeNames = names;
2126
+ transactionItem.params.expressionAttributeValues = values;
2127
+ }
2128
+ this.items.push(transactionItem);
2129
+ return this;
502
2130
  }
503
2131
  /**
504
- * Convert our format to DynamoDB delete command input
2132
+ * Adds a pre-configured delete operation to the transaction.
2133
+ * Use this method when you need to:
2134
+ * - Reuse delete commands from DeleteBuilder
2135
+ * - Add complex delete operations with pre-configured parameters
2136
+ * - Integrate with existing delete command configurations
2137
+ *
2138
+ * This method is particularly useful when working with DeleteBuilder
2139
+ * to maintain consistency in delete operations across your application.
2140
+ *
2141
+ * @example
2142
+ * ```typescript
2143
+ * // Create a delete command with DeleteBuilder
2144
+ * const deleteCommand = new DeleteBuilder(executor, 'users', { pk: 'USER#123' })
2145
+ * .condition(op => op.and([
2146
+ * op.attributeExists('pk'),
2147
+ * op.eq('status', 'INACTIVE')
2148
+ * ]))
2149
+ * .toDynamoCommand();
2150
+ *
2151
+ * // Add the command to the transaction
2152
+ * transaction.deleteWithCommand(deleteCommand);
2153
+ * ```
2154
+ *
2155
+ * @param command - The complete delete command configuration
2156
+ * @returns The transaction builder for method chaining
2157
+ * @throws {Error} If a duplicate item is detected in the transaction
2158
+ * @see DeleteBuilder for creating delete commands
505
2159
  */
506
- toDeleteCommand(options) {
507
- return {
508
- TableName: this.tableName,
509
- Key: options.key,
510
- ...options.condition && {
511
- ConditionExpression: options.condition.expression,
512
- ExpressionAttributeNames: options.condition.names,
513
- ExpressionAttributeValues: options.condition.values
514
- }
2160
+ deleteWithCommand(command) {
2161
+ this.checkForDuplicateItem(command.tableName, command.key);
2162
+ const transactionItem = {
2163
+ type: "Delete",
2164
+ params: command
515
2165
  };
2166
+ this.items.push(transactionItem);
2167
+ return this;
516
2168
  }
517
2169
  /**
518
- * Convert our format to DynamoDB query command input
2170
+ * Adds an update operation to the transaction.
2171
+ * Use this method when you need to:
2172
+ * - Modify existing items as part of a transaction
2173
+ * - Update multiple attributes atomically
2174
+ * - Apply conditional updates
2175
+ * - Perform complex attribute manipulations
2176
+ *
2177
+ * The method supports all DynamoDB update expressions:
2178
+ * - SET: Modify or add attributes
2179
+ * - REMOVE: Delete attributes
2180
+ * - ADD: Update numbers and sets
2181
+ * - DELETE: Remove elements from a set
2182
+ *
2183
+ * @example
2184
+ * ```typescript
2185
+ * // Simple update
2186
+ * transaction.update(
2187
+ * 'orders',
2188
+ * { pk: 'ORDER#123' },
2189
+ * 'SET #status = :status',
2190
+ * { '#status': 'status' },
2191
+ * { ':status': 'PROCESSING' }
2192
+ * );
2193
+ *
2194
+ * // Complex update with multiple operations
2195
+ * transaction.update(
2196
+ * 'products',
2197
+ * { pk: 'PROD#ABC' },
2198
+ * 'SET #qty = #qty - :amount, #status = :status REMOVE #oldAttr',
2199
+ * { '#qty': 'quantity', '#status': 'status', '#oldAttr': 'deprecated_field' },
2200
+ * { ':amount': 1, ':status': 'LOW_STOCK' }
2201
+ * );
2202
+ *
2203
+ * // Conditional update
2204
+ * transaction.update(
2205
+ * 'users',
2206
+ * { pk: 'USER#123' },
2207
+ * 'SET #lastLogin = :now',
2208
+ * { '#lastLogin': 'lastLoginDate' },
2209
+ * { ':now': new Date().toISOString() },
2210
+ * op => op.attributeExists('pk')
2211
+ * );
2212
+ * ```
2213
+ *
2214
+ * @param tableName - The name of the DynamoDB table
2215
+ * @param key - The primary key of the item to update
2216
+ * @param updateExpression - The update expression (SET, REMOVE, ADD, DELETE)
2217
+ * @param expressionAttributeNames - Map of attribute name placeholders to actual names
2218
+ * @param expressionAttributeValues - Map of value placeholders to actual values
2219
+ * @param condition - Optional condition that must be satisfied
2220
+ * @returns The transaction builder for method chaining
2221
+ * @throws {Error} If a duplicate item is detected in the transaction
519
2222
  */
520
- toQueryCommand(options) {
521
- return {
522
- TableName: this.tableName,
523
- ...options.keyCondition && {
524
- KeyConditionExpression: options.keyCondition.expression,
525
- ExpressionAttributeNames: {
526
- ...options.keyCondition.names,
527
- ...options.filter?.names
2223
+ update(tableName, key, updateExpression, expressionAttributeNames, expressionAttributeValues, condition) {
2224
+ this.checkForDuplicateItem(tableName, key);
2225
+ const transactionItem = {
2226
+ type: "Update",
2227
+ params: {
2228
+ tableName,
2229
+ key: {
2230
+ pk: key.pk,
2231
+ sk: key.sk
528
2232
  },
529
- ExpressionAttributeValues: {
530
- ...options.keyCondition.values,
531
- ...options.filter?.values
532
- }
533
- },
534
- ...options.filter && {
535
- FilterExpression: options.filter.expression
536
- },
537
- IndexName: options.indexName,
538
- Limit: options.limit,
539
- ExclusiveStartKey: options.pageKey,
540
- ConsistentRead: options.consistentRead
2233
+ updateExpression,
2234
+ expressionAttributeNames,
2235
+ expressionAttributeValues
2236
+ }
541
2237
  };
2238
+ if (condition) {
2239
+ const { expression, names, values } = prepareExpressionParams(condition);
2240
+ transactionItem.params.conditionExpression = expression;
2241
+ transactionItem.params.expressionAttributeNames = {
2242
+ ...transactionItem.params.expressionAttributeNames,
2243
+ ...names
2244
+ };
2245
+ transactionItem.params.expressionAttributeValues = {
2246
+ ...transactionItem.params.expressionAttributeValues,
2247
+ ...values
2248
+ };
2249
+ }
2250
+ this.items.push(transactionItem);
2251
+ return this;
542
2252
  }
543
2253
  /**
544
- * Convert our format to DynamoDB scan command input
2254
+ * Adds a pre-configured update operation to the transaction.
2255
+ * Use this method when you need to:
2256
+ * - Reuse update commands from UpdateBuilder
2257
+ * - Add complex update operations with pre-configured parameters
2258
+ * - Integrate with existing update command configurations
2259
+ *
2260
+ * This method is particularly useful when working with UpdateBuilder
2261
+ * to maintain consistency in update operations across your application.
2262
+ *
2263
+ * @example
2264
+ * ```typescript
2265
+ * // Create an update command with UpdateBuilder
2266
+ * const updateCommand = new UpdateBuilder(executor, 'inventory', { pk: 'PROD#ABC' })
2267
+ * .set('quantity', ':qty')
2268
+ * .set('lastUpdated', ':now')
2269
+ * .values({
2270
+ * ':qty': 100,
2271
+ * ':now': new Date().toISOString()
2272
+ * })
2273
+ * .condition(op => op.gt('quantity', 0))
2274
+ * .toDynamoCommand();
2275
+ *
2276
+ * // Add the command to the transaction
2277
+ * transaction.updateWithCommand(updateCommand);
2278
+ * ```
2279
+ *
2280
+ * @param command - The complete update command configuration
2281
+ * @returns The transaction builder for method chaining
2282
+ * @throws {Error} If a duplicate item is detected in the transaction
2283
+ * @see UpdateBuilder for creating update commands
545
2284
  */
546
- toScanCommand(options) {
547
- return {
548
- TableName: this.tableName,
549
- ...options.filter && {
550
- FilterExpression: options.filter.expression,
551
- ExpressionAttributeNames: options.filter.names,
552
- ExpressionAttributeValues: options.filter.values
553
- },
554
- IndexName: options.indexName,
555
- Limit: options.limit,
556
- ExclusiveStartKey: options.pageKey
2285
+ updateWithCommand(command) {
2286
+ this.checkForDuplicateItem(command.tableName, command.key);
2287
+ const transactionItem = {
2288
+ type: "Update",
2289
+ params: command
557
2290
  };
2291
+ this.items.push(transactionItem);
2292
+ return this;
558
2293
  }
559
2294
  /**
560
- * Convert our format to DynamoDB batch write command input
2295
+ * Adds a condition check operation to the transaction.
2296
+ * Use this method when you need to:
2297
+ * - Validate item state without modifying it
2298
+ * - Ensure data consistency across tables
2299
+ * - Implement complex business rules
2300
+ * - Verify preconditions for other operations
2301
+ *
2302
+ * Condition checks are particularly useful for:
2303
+ * - Implementing optimistic locking
2304
+ * - Ensuring referential integrity
2305
+ * - Validating business rules atomically
2306
+ *
2307
+ * @example
2308
+ * ```typescript
2309
+ * // Check if order is in correct state
2310
+ * transaction.conditionCheck(
2311
+ * 'orders',
2312
+ * { pk: 'ORDER#123' },
2313
+ * op => op.eq('status', 'PENDING')
2314
+ * );
2315
+ *
2316
+ * // Complex condition check
2317
+ * transaction.conditionCheck(
2318
+ * 'inventory',
2319
+ * { pk: 'PROD#ABC' },
2320
+ * op => op.and([
2321
+ * op.gt('quantity', 0),
2322
+ * op.eq('status', 'ACTIVE'),
2323
+ * op.attributeExists('lastRestockDate')
2324
+ * ])
2325
+ * );
2326
+ *
2327
+ * // Check with multiple attributes
2328
+ * transaction.conditionCheck(
2329
+ * 'users',
2330
+ * { pk: 'USER#123' },
2331
+ * op => op.or([
2332
+ * op.eq('status', 'PREMIUM'),
2333
+ * op.gte('credits', 100)
2334
+ * ])
2335
+ * );
2336
+ * ```
2337
+ *
2338
+ * @param tableName - The name of the DynamoDB table
2339
+ * @param key - The primary key of the item to check
2340
+ * @param condition - The condition that must be satisfied
2341
+ * @returns The transaction builder for method chaining
2342
+ * @throws {Error} If a duplicate item is detected in the transaction
2343
+ * @throws {Error} If condition expression generation fails
561
2344
  */
562
- toBatchWriteCommand(items) {
563
- const requests = items.map((item) => {
564
- if (item.put) {
565
- return {
566
- PutRequest: {
567
- Item: item.put
568
- }
569
- };
570
- }
571
- if (item.delete) {
572
- return {
573
- DeleteRequest: {
574
- Key: item.delete
575
- }
576
- };
577
- }
578
- throw new Error("Invalid batch write item");
579
- });
580
- return {
581
- RequestItems: {
582
- [this.tableName]: requests
2345
+ conditionCheck(tableName, key, condition) {
2346
+ this.checkForDuplicateItem(tableName, key);
2347
+ const { expression, names, values } = prepareExpressionParams(condition);
2348
+ if (!expression) {
2349
+ throw new Error("Failed to generate condition expression");
2350
+ }
2351
+ const transactionItem = {
2352
+ type: "ConditionCheck",
2353
+ params: {
2354
+ tableName,
2355
+ key: {
2356
+ pk: key.pk,
2357
+ sk: key.sk
2358
+ },
2359
+ conditionExpression: expression,
2360
+ expressionAttributeNames: names,
2361
+ expressionAttributeValues: values
583
2362
  }
584
2363
  };
2364
+ this.items.push(transactionItem);
2365
+ return this;
585
2366
  }
586
2367
  /**
587
- * Convert our format to DynamoDB transact write command input
2368
+ * Adds a pre-configured condition check operation to the transaction.
2369
+ * Use this method when you need to:
2370
+ * - Reuse condition checks from ConditionCheckBuilder
2371
+ * - Add complex condition checks with pre-configured parameters
2372
+ * - Integrate with existing condition check configurations
2373
+ *
2374
+ * This method is particularly useful when working with ConditionCheckBuilder
2375
+ * to maintain consistency in condition checks across your application.
2376
+ *
2377
+ * @example
2378
+ * ```typescript
2379
+ * // Create a condition check with ConditionCheckBuilder
2380
+ * const checkCommand = new ConditionCheckBuilder('inventory', { pk: 'PROD#ABC' })
2381
+ * .condition(op => op.and([
2382
+ * op.between('quantity', 10, 100),
2383
+ * op.beginsWith('category', 'ELECTRONICS'),
2384
+ * op.attributeExists('lastAuditDate')
2385
+ * ]))
2386
+ * .toDynamoCommand();
2387
+ *
2388
+ * // Add the command to the transaction
2389
+ * transaction.conditionCheckWithCommand(checkCommand);
2390
+ * ```
2391
+ *
2392
+ * @param command - The complete condition check command configuration
2393
+ * @returns The transaction builder for method chaining
2394
+ * @throws {Error} If a duplicate item is detected in the transaction
2395
+ * @see ConditionCheckBuilder for creating condition check commands
588
2396
  */
589
- toTransactWriteCommand(items) {
590
- return {
591
- TransactItems: items.map((item) => {
592
- if (item.put) {
2397
+ conditionCheckWithCommand(command) {
2398
+ this.checkForDuplicateItem(command.tableName, command.key);
2399
+ const transactionItem = {
2400
+ type: "ConditionCheck",
2401
+ params: command
2402
+ };
2403
+ this.items.push(transactionItem);
2404
+ return this;
2405
+ }
2406
+ /**
2407
+ * Sets options for the transaction execution.
2408
+ * Use this method when you need to:
2409
+ * - Enable idempotent transactions
2410
+ * - Track consumed capacity
2411
+ * - Monitor item collection metrics
2412
+ *
2413
+ * @example
2414
+ * ```typescript
2415
+ * // Enable idempotency and capacity tracking
2416
+ * transaction.withOptions({
2417
+ * clientRequestToken: 'unique-request-id-123',
2418
+ * returnConsumedCapacity: 'TOTAL'
2419
+ * });
2420
+ *
2421
+ * // Track item collection metrics
2422
+ * transaction.withOptions({
2423
+ * returnItemCollectionMetrics: 'SIZE'
2424
+ * });
2425
+ * ```
2426
+ *
2427
+ * Note: ClientRequestToken can be used to make transactions idempotent,
2428
+ * ensuring the same transaction is not executed multiple times.
2429
+ *
2430
+ * @param options - Configuration options for the transaction
2431
+ * @returns The transaction builder for method chaining
2432
+ */
2433
+ withOptions(options) {
2434
+ this.options = { ...this.options, ...options };
2435
+ return this;
2436
+ }
2437
+ /**
2438
+ * Gets a human-readable representation of the transaction items.
2439
+ * Use this method when you need to:
2440
+ * - Debug complex transactions
2441
+ * - Verify operation parameters
2442
+ * - Log transaction details
2443
+ * - Troubleshoot condition expressions
2444
+ *
2445
+ * The method resolves all expression placeholders with their actual values,
2446
+ * making it easier to understand the transaction's operations.
2447
+ *
2448
+ * @example
2449
+ * ```typescript
2450
+ * // Add multiple operations
2451
+ * transaction
2452
+ * .put('orders', { orderId: '123', status: 'PENDING' })
2453
+ * .update('inventory',
2454
+ * { productId: 'ABC' },
2455
+ * 'SET quantity = quantity - :amount',
2456
+ * undefined,
2457
+ * { ':amount': 1 }
2458
+ * );
2459
+ *
2460
+ * // Debug the transaction
2461
+ * const debugInfo = transaction.debug();
2462
+ * console.log('Transaction operations:', debugInfo);
2463
+ * ```
2464
+ *
2465
+ * @returns An array of readable representations of the transaction items
2466
+ */
2467
+ debug() {
2468
+ return debugTransaction(this.items);
2469
+ }
2470
+ /**
2471
+ * Executes all operations in the transaction atomically.
2472
+ * Use this method when you need to:
2473
+ * - Perform multiple operations atomically
2474
+ * - Ensure all-or-nothing execution
2475
+ * - Maintain data consistency across operations
2476
+ *
2477
+ * The transaction will only succeed if all operations succeed.
2478
+ * If any operation fails, the entire transaction is rolled back.
2479
+ *
2480
+ * @example
2481
+ * ```typescript
2482
+ * try {
2483
+ * // Build and execute transaction
2484
+ * await transaction
2485
+ * .put('orders', newOrder)
2486
+ * .update('inventory',
2487
+ * { productId: 'ABC' },
2488
+ * 'SET quantity = quantity - :qty',
2489
+ * undefined,
2490
+ * { ':qty': 1 }
2491
+ * )
2492
+ * .conditionCheck('products',
2493
+ * { productId: 'ABC' },
2494
+ * op => op.eq('status', 'ACTIVE')
2495
+ * )
2496
+ * .execute();
2497
+ *
2498
+ * console.log('Transaction completed successfully');
2499
+ * } catch (error) {
2500
+ * // Handle transaction failure
2501
+ * console.error('Transaction failed:', error);
2502
+ * }
2503
+ * ```
2504
+ *
2505
+ * @throws {Error} If no transaction items are specified
2506
+ * @throws {Error} If any operation in the transaction fails
2507
+ * @returns A promise that resolves when the transaction completes
2508
+ */
2509
+ async execute() {
2510
+ if (this.items.length === 0) {
2511
+ throw new Error("No transaction items specified");
2512
+ }
2513
+ const transactItems = this.items.map((item) => {
2514
+ switch (item.type) {
2515
+ case "Put":
593
2516
  return {
594
2517
  Put: {
595
- TableName: this.tableName,
596
- Item: item.put.item,
597
- ...item.put.condition && {
598
- ConditionExpression: item.put.condition.expression,
599
- ExpressionAttributeNames: item.put.condition.names,
600
- ExpressionAttributeValues: item.put.condition.values
601
- }
2518
+ TableName: item.params.tableName,
2519
+ Item: item.params.item,
2520
+ ConditionExpression: item.params.conditionExpression,
2521
+ ExpressionAttributeNames: item.params.expressionAttributeNames,
2522
+ ExpressionAttributeValues: item.params.expressionAttributeValues
602
2523
  }
603
2524
  };
604
- }
605
- if (item.delete) {
2525
+ case "Delete":
606
2526
  return {
607
2527
  Delete: {
608
- TableName: this.tableName,
609
- Key: item.delete.key,
610
- ...item.delete.condition && {
611
- ConditionExpression: item.delete.condition.expression,
612
- ExpressionAttributeNames: item.delete.condition.names,
613
- ExpressionAttributeValues: item.delete.condition.values
614
- }
2528
+ TableName: item.params.tableName,
2529
+ Key: item.params.key,
2530
+ ConditionExpression: item.params.conditionExpression,
2531
+ ExpressionAttributeNames: item.params.expressionAttributeNames,
2532
+ ExpressionAttributeValues: item.params.expressionAttributeValues
615
2533
  }
616
2534
  };
617
- }
618
- if (item.update) {
2535
+ case "Update":
619
2536
  return {
620
2537
  Update: {
621
- TableName: this.tableName,
622
- Key: item.update.key,
623
- UpdateExpression: item.update.update.expression,
624
- ...item.update.condition && {
625
- ConditionExpression: item.update.condition.expression,
626
- ExpressionAttributeNames: {
627
- ...item.update.update.names,
628
- ...item.update.condition.names
629
- },
630
- ExpressionAttributeValues: {
631
- ...item.update.update.values,
632
- ...item.update.condition.values
633
- }
634
- }
2538
+ TableName: item.params.tableName,
2539
+ Key: item.params.key,
2540
+ UpdateExpression: item.params.updateExpression,
2541
+ ConditionExpression: item.params.conditionExpression,
2542
+ ExpressionAttributeNames: item.params.expressionAttributeNames,
2543
+ ExpressionAttributeValues: item.params.expressionAttributeValues
2544
+ }
2545
+ };
2546
+ case "ConditionCheck":
2547
+ return {
2548
+ ConditionCheck: {
2549
+ TableName: item.params.tableName,
2550
+ Key: item.params.key,
2551
+ ConditionExpression: item.params.conditionExpression,
2552
+ ExpressionAttributeNames: item.params.expressionAttributeNames,
2553
+ ExpressionAttributeValues: item.params.expressionAttributeValues
635
2554
  }
636
2555
  };
2556
+ default: {
2557
+ const exhaustiveCheck = item;
2558
+ throw new Error(`Unsupported transaction item type: ${String(exhaustiveCheck)}`);
637
2559
  }
638
- throw new Error("Invalid transaction item");
639
- })
640
- };
641
- }
642
- /**
643
- * Convert DynamoDB batch write response to our format
644
- */
645
- fromBatchWriteResponse(response) {
646
- return response.map((item) => {
647
- if ("PutRequest" in item) {
648
- return {
649
- put: item.PutRequest.Item
650
- };
651
- }
652
- if ("DeleteRequest" in item) {
653
- return {
654
- delete: item.DeleteRequest.Key
655
- };
656
2560
  }
657
- throw new Error("Invalid batch write response item");
658
2561
  });
659
- }
660
- };
661
-
662
- // src/dynamo/dynamo-service.ts
663
- var BATCH_WRITE_LIMIT = 25;
664
- var TRANSACTION_LIMIT = 100;
665
- var DynamoService = class {
666
- constructor(client, tableName) {
667
- this.client = client;
668
- this.tableName = tableName;
669
- this.converter = new DynamoConverter(tableName);
670
- }
671
- converter;
672
- async put(options) {
673
- try {
674
- const params = this.converter.toPutCommand(options);
675
- return await this.withRetry(async () => {
676
- await this.client.put(params);
677
- return options.item;
678
- });
679
- } catch (error) {
680
- handleDynamoError(error, {
681
- operation: "PUT",
682
- tableName: this.tableName,
683
- key: options.item,
684
- expression: {
685
- condition: options.condition?.expression
686
- }
687
- });
688
- }
689
- }
690
- async update(options) {
691
- try {
692
- const params = this.converter.toUpdateCommand(options);
693
- return await this.withRetry(() => this.client.update(params));
694
- } catch (error) {
695
- handleDynamoError(error, {
696
- operation: "UPDATE",
697
- tableName: this.tableName,
698
- key: options.key,
699
- expression: {
700
- update: options.update.expression,
701
- condition: options.condition?.expression
702
- }
703
- });
704
- }
705
- }
706
- async delete(options) {
707
- const params = this.converter.toDeleteCommand(options);
2562
+ const params = {
2563
+ TransactItems: transactItems,
2564
+ ClientRequestToken: this.options.clientRequestToken,
2565
+ ReturnConsumedCapacity: this.options.returnConsumedCapacity,
2566
+ ReturnItemCollectionMetrics: this.options.returnItemCollectionMetrics
2567
+ };
708
2568
  try {
709
- return await this.withRetry(() => this.client.delete(params));
2569
+ await this.executor(params);
710
2570
  } catch (error) {
711
- handleDynamoError(error, {
712
- operation: "DELETE",
713
- tableName: this.tableName,
714
- key: options.key,
715
- expression: {
716
- condition: params.ConditionExpression
717
- }
718
- });
2571
+ console.log(this.debug());
2572
+ console.error("Error executing transaction:", error);
2573
+ throw error;
719
2574
  }
720
2575
  }
721
- async get(key, options) {
722
- try {
723
- const params = this.converter.toGetCommand({ key, ...options });
724
- return await this.withRetry(() => this.client.get(params));
725
- } catch (error) {
726
- handleDynamoError(error, {
727
- operation: "GET",
728
- tableName: this.tableName,
729
- key
730
- });
731
- }
2576
+ };
2577
+
2578
+ // src/utils/chunk-array.ts
2579
+ function* chunkArray(array, size) {
2580
+ if (size <= 0) {
2581
+ throw new Error("Chunk size must be greater than 0");
732
2582
  }
733
- async query(options) {
734
- try {
735
- if (options.autoPaginate) {
736
- return await this.executeWithAutoPagination(options);
737
- }
738
- const params = this.converter.toQueryCommand(options);
739
- return await this.withRetry(() => this.client.query(params));
740
- } catch (error) {
741
- handleDynamoError(error, {
742
- operation: "QUERY",
743
- tableName: this.tableName,
744
- expression: {
745
- keyCondition: options.keyCondition?.expression,
746
- filter: options.filter?.expression
747
- }
748
- });
749
- }
2583
+ for (let i = 0; i < array.length; i += size) {
2584
+ yield array.slice(i, i + size);
750
2585
  }
751
- async scan(options) {
752
- try {
753
- const params = this.converter.toScanCommand(options);
754
- return await this.withRetry(() => this.client.scan(params));
755
- } catch (error) {
756
- handleDynamoError(error, {
757
- operation: "SCAN",
758
- tableName: this.tableName,
759
- expression: {
760
- filter: options.filter?.expression
761
- }
762
- });
763
- }
2586
+ }
2587
+
2588
+ // src/builders/condition-check-builder.ts
2589
+ var ConditionCheckBuilder = class {
2590
+ key;
2591
+ tableName;
2592
+ conditionExpression;
2593
+ constructor(tableName, key) {
2594
+ this.tableName = tableName;
2595
+ this.key = key;
764
2596
  }
765
- async batchWrite(items) {
766
- try {
767
- const chunks = this.chunkArray(items, BATCH_WRITE_LIMIT);
768
- return await Promise.all(chunks.map((chunk) => this.processBatchWrite(chunk)));
769
- } catch (error) {
770
- handleDynamoError(error, {
771
- operation: "BATCH_WRITE",
772
- tableName: this.tableName
773
- });
2597
+ /**
2598
+ * Adds a condition that must be satisfied for the check to succeed.
2599
+ * Use this method when you need to:
2600
+ * - Validate complex item states
2601
+ * - Check multiple attributes together
2602
+ * - Ensure safety conditions are met
2603
+ *
2604
+ * @example
2605
+ * ```typescript
2606
+ * // Check dinosaur health and behavior
2607
+ * builder.condition(op =>
2608
+ * op.and([
2609
+ * op.gt('stats.health', 50),
2610
+ * op.not(op.eq('status', 'SEDATED')),
2611
+ * op.lt('aggressionLevel', 8)
2612
+ * ])
2613
+ * );
2614
+ *
2615
+ * // Verify habitat conditions
2616
+ * builder.condition(op =>
2617
+ * op.and([
2618
+ * op.eq('powerStatus', 'ONLINE'),
2619
+ * op.between('temperature', 20, 30),
2620
+ * op.attributeExists('lastMaintenance')
2621
+ * ])
2622
+ * );
2623
+ *
2624
+ * // Check breeding conditions
2625
+ * builder.condition(op =>
2626
+ * op.and([
2627
+ * op.eq('species', 'VELOCIRAPTOR'),
2628
+ * op.gte('age', 3),
2629
+ * op.eq('geneticPurity', 100)
2630
+ * ])
2631
+ * );
2632
+ * ```
2633
+ *
2634
+ * @param condition - Either a Condition object or a callback function that builds the condition
2635
+ * @returns The builder instance for method chaining
2636
+ */
2637
+ condition(condition) {
2638
+ if (typeof condition === "function") {
2639
+ const conditionOperator = {
2640
+ eq,
2641
+ ne,
2642
+ lt,
2643
+ lte,
2644
+ gt,
2645
+ gte,
2646
+ between,
2647
+ beginsWith,
2648
+ contains,
2649
+ attributeExists,
2650
+ attributeNotExists,
2651
+ and,
2652
+ or,
2653
+ not
2654
+ };
2655
+ this.conditionExpression = condition(conditionOperator);
2656
+ } else {
2657
+ this.conditionExpression = condition;
774
2658
  }
2659
+ return this;
775
2660
  }
776
- async transactWrite(items) {
777
- if (items.length > TRANSACTION_LIMIT) {
778
- throw new Error(`Transaction limit exceeded. Maximum is ${TRANSACTION_LIMIT} items, got ${items.length}`);
2661
+ /**
2662
+ * Generates the DynamoDB command parameters for direct execution.
2663
+ * Use this method when you want to:
2664
+ * - Execute the condition check as a standalone operation
2665
+ * - Get the raw DynamoDB command for custom execution
2666
+ * - Inspect the generated command parameters
2667
+ *
2668
+ * @example
2669
+ * ```ts
2670
+ * const command = new ConditionCheckBuilder('myTable', { id: '123' })
2671
+ * .condition(op => op.attributeExists('status'))
2672
+ * .toDynamoCommand();
2673
+ * // Use command with DynamoDB client
2674
+ * ```
2675
+ *
2676
+ * @throws {Error} If no condition has been set
2677
+ * @returns The DynamoDB command parameters
2678
+ */
2679
+ toDynamoCommand() {
2680
+ if (!this.conditionExpression) {
2681
+ throw new Error("Condition is required for condition check operations");
779
2682
  }
780
- try {
781
- const params = this.converter.toTransactWriteCommand(items);
782
- return await this.withRetry(() => this.client.transactWrite(params));
783
- } catch (error) {
784
- handleDynamoError(error, {
785
- operation: "TRANSACT_WRITE",
786
- tableName: this.tableName
787
- });
2683
+ const { expression, names, values } = prepareExpressionParams(this.conditionExpression);
2684
+ if (!expression) {
2685
+ throw new Error("Failed to generate condition expression");
788
2686
  }
789
- }
790
- async executeWithAutoPagination(options) {
791
- const allItems = [];
792
- let lastEvaluatedKey;
793
- do {
794
- const result = await this.query({
795
- ...options,
796
- pageKey: lastEvaluatedKey,
797
- autoPaginate: false
798
- });
799
- if (result.Items) {
800
- allItems.push(...result.Items);
801
- }
802
- lastEvaluatedKey = result.LastEvaluatedKey;
803
- } while (lastEvaluatedKey);
804
2687
  return {
805
- Items: allItems,
806
- Count: allItems.length,
807
- ScannedCount: allItems.length,
808
- LastEvaluatedKey: void 0
809
- };
810
- }
811
- async processBatchWrite(items) {
812
- const processUnprocessedItems = async (unprocessedItems2) => {
813
- const params2 = this.converter.toBatchWriteCommand(unprocessedItems2);
814
- const result = await this.client.batchWrite(params2);
815
- const outstandingItems = result.UnprocessedItems?.[this.tableName];
816
- if (outstandingItems && outstandingItems.length > 0) {
817
- const remainingItems = this.converter.fromBatchWriteResponse(outstandingItems);
818
- throw {
819
- name: "UnprocessedItemsError",
820
- unprocessedItems: remainingItems
821
- };
822
- }
823
- return result;
2688
+ tableName: this.tableName,
2689
+ key: this.key,
2690
+ conditionExpression: expression,
2691
+ expressionAttributeNames: names,
2692
+ expressionAttributeValues: values
824
2693
  };
825
- const params = this.converter.toBatchWriteCommand(items);
826
- const initialResult = await this.client.batchWrite(params);
827
- const rawUnprocessedItems = initialResult.UnprocessedItems?.[this.tableName];
828
- if (!rawUnprocessedItems || rawUnprocessedItems.length === 0) {
829
- return initialResult;
830
- }
831
- const unprocessedItems = this.converter.fromBatchWriteResponse(rawUnprocessedItems);
832
- return this.withRetry(() => processUnprocessedItems(unprocessedItems));
833
2694
  }
834
- async withRetry(operation, strategy = new ExponentialBackoffStrategy()) {
835
- let attempt = 0;
836
- while (true) {
837
- try {
838
- return await operation();
839
- } catch (error) {
840
- if (!strategy.shouldRetry(error, attempt)) {
841
- throw error;
842
- }
843
- await new Promise((resolve) => setTimeout(resolve, strategy.getDelay(attempt)));
844
- attempt++;
845
- }
2695
+ /**
2696
+ * Adds this condition check operation to a transaction.
2697
+ * Use this method when you need to:
2698
+ * - Verify habitat safety before transfers
2699
+ * - Ensure proper feeding conditions
2700
+ * - Validate security protocols
2701
+ *
2702
+ * @example
2703
+ * ```ts
2704
+ * const transaction = new TransactionBuilder();
2705
+ * new ConditionCheckBuilder('habitats', { id: 'PADDOCK-B' })
2706
+ * .condition(op => op.and([
2707
+ * op.eq('securityStatus', 'ACTIVE'),
2708
+ * op.lt('currentOccupants', 3),
2709
+ * op.eq('habitatType', 'CARNIVORE')
2710
+ * ]))
2711
+ * .withTransaction(transaction);
2712
+ * // Add dinosaur transfer operations
2713
+ * ```
2714
+ *
2715
+ * @param transaction - The transaction builder to add this operation to
2716
+ * @throws {Error} If no condition has been set
2717
+ * @returns The builder instance for method chaining
2718
+ */
2719
+ withTransaction(transaction) {
2720
+ if (!this.conditionExpression) {
2721
+ throw new Error("Condition is required for condition check operations");
846
2722
  }
847
- }
848
- chunkArray(array, size) {
849
- return Array.from(
850
- { length: Math.ceil(array.length / size) },
851
- (_, index) => array.slice(index * size, (index + 1) * size)
852
- );
853
- }
854
- };
855
-
856
- // src/builders/scan-builder.ts
857
- var ScanBuilder = class extends OperationBuilder {
858
- constructor(expressionBuilder, onBuild) {
859
- super(expressionBuilder);
860
- this.onBuild = onBuild;
861
- }
862
- limitValue;
863
- indexNameValue;
864
- pageKeyValue;
865
- limit(value) {
866
- this.limitValue = value;
2723
+ const command = this.toDynamoCommand();
2724
+ transaction.conditionCheckWithCommand(command);
867
2725
  return this;
868
2726
  }
869
- useIndex(indexName) {
870
- this.indexNameValue = indexName;
871
- return this;
872
- }
873
- startKey(key) {
874
- this.pageKeyValue = key;
875
- return this;
876
- }
877
- build() {
878
- const filter = this.buildConditionExpression();
879
- return {
880
- type: "scan",
881
- filter: filter.expression ? {
882
- expression: filter.expression,
883
- names: filter.attributes.names,
884
- values: filter.attributes.values
885
- } : void 0,
886
- limit: this.limitValue,
887
- pageKey: this.pageKeyValue,
888
- indexName: this.indexNameValue
889
- };
890
- }
891
- async execute() {
892
- return this.onBuild(this.build());
2727
+ /**
2728
+ * Gets a human-readable representation of the condition check command
2729
+ * with all expression placeholders replaced by their actual values.
2730
+ * Use this method when you need to:
2731
+ * - Debug complex condition expressions
2732
+ * - Verify condition parameters
2733
+ * - Log safety checks
2734
+ * - Troubleshoot condition failures
2735
+ *
2736
+ * @example
2737
+ * ```ts
2738
+ * const debugInfo = new ConditionCheckBuilder('dinosaurs', { id: 'TREX-001' })
2739
+ * .condition(op => op.and([
2740
+ * op.between('stats.health', 50, 100),
2741
+ * op.not(op.eq('status', 'SEDATED')),
2742
+ * op.attributeExists('lastFeedingTime')
2743
+ * op.eq('version', 1)
2744
+ * ]))
2745
+ * .debug();
2746
+ * console.log(debugInfo);
2747
+ * ```
2748
+ *
2749
+ * @returns A readable representation of the condition check command with resolved expressions
2750
+ */
2751
+ debug() {
2752
+ const command = this.toDynamoCommand();
2753
+ return debugCommand(command);
893
2754
  }
894
2755
  };
895
2756
 
896
- // src/table.ts
897
- var Table = class {
898
- dynamoService;
899
- expressionBuilder;
900
- indexes;
901
- constructor({
902
- client,
903
- tableName,
904
- tableIndexes,
905
- expressionBuilder
906
- }) {
907
- this.dynamoService = new DynamoService(client, tableName);
908
- this.expressionBuilder = expressionBuilder ?? new ExpressionBuilder();
909
- this.indexes = tableIndexes;
910
- }
911
- getIndexConfig(indexName) {
912
- if (!indexName) {
913
- return this.indexes.primary;
914
- }
915
- if (this.indexes[indexName]) {
916
- return this.indexes[indexName];
917
- }
918
- throw new Error(`Index ${indexName} does not exist`);
919
- }
920
- put(item) {
921
- return new PutBuilder(item, this.expressionBuilder, (operation) => this.executeOperation(operation));
922
- }
923
- update(key, data) {
924
- const builder = new UpdateBuilder(key, this.expressionBuilder, (operation) => this.executeOperation(operation));
925
- if (data) {
926
- builder.setMany(data);
927
- }
928
- return builder;
929
- }
930
- query(key) {
931
- return new QueryBuilder(
932
- key,
933
- this.getIndexConfig(),
934
- this.expressionBuilder,
935
- (operation) => this.executeOperation(operation)
936
- );
937
- }
938
- async get(key, options) {
939
- const indexConfig = this.getIndexConfig(options?.indexName);
940
- const keyObject = this.buildKeyFromIndex(key, indexConfig);
941
- const result = await this.dynamoService.get(keyObject, options);
942
- return result.Item;
943
- }
944
- async delete(key) {
945
- const operation = {
946
- type: "delete",
2757
+ // src/builders/get-builder.ts
2758
+ var GetBuilder = class {
2759
+ /**
2760
+ * Creates a new GetBuilder instance.
2761
+ *
2762
+ * @param executor - Function that executes the get operation
2763
+ * @param key - Primary key of the item to retrieve
2764
+ * @param tableName - Name of the DynamoDB table
2765
+ */
2766
+ constructor(executor, key, tableName) {
2767
+ this.executor = executor;
2768
+ this.params = {
2769
+ tableName,
947
2770
  key
948
2771
  };
949
- return this.executeOperation(operation);
950
2772
  }
951
- scan() {
952
- return new ScanBuilder(this.expressionBuilder, (operation) => this.executeOperation(operation));
953
- }
954
- async batchWrite(operations) {
955
- const batchOperation = {
956
- type: "batchWrite",
957
- operations: operations.map((op) => {
958
- if (op.type === "put") {
959
- return { put: op.item };
960
- }
961
- return { delete: op.key };
962
- })
963
- };
964
- return this.executeOperation(batchOperation);
965
- }
966
- async transactWrite(operations) {
967
- const transactOperation = {
968
- type: "transactWrite",
969
- operations
970
- };
971
- return this.executeOperation(transactOperation);
972
- }
973
- async executeOperation(operation) {
974
- switch (operation.type) {
975
- case "put":
976
- return this.dynamoService.put({
977
- item: operation.item,
978
- condition: operation.condition
979
- });
980
- case "update":
981
- return this.dynamoService.update({
982
- key: operation.key,
983
- update: operation.update,
984
- condition: operation.condition,
985
- returnValues: "ALL_NEW"
986
- });
987
- case "query":
988
- return this.dynamoService.query({
989
- keyCondition: operation.keyCondition,
990
- filter: operation.filter,
991
- limit: operation.limit,
992
- indexName: operation.indexName
993
- });
994
- case "delete":
995
- return this.dynamoService.delete({
996
- key: operation.key
997
- });
998
- case "batchWrite":
999
- return this.dynamoService.batchWrite(operation.operations);
1000
- case "transactWrite":
1001
- return this.dynamoService.transactWrite(operation.operations);
1002
- case "scan":
1003
- return this.dynamoService.scan({
1004
- filter: operation.filter,
1005
- limit: operation.limit,
1006
- pageKey: operation.pageKey,
1007
- indexName: operation.indexName
1008
- });
1009
- default:
1010
- throw new Error("Unknown operation type");
2773
+ params;
2774
+ options = {};
2775
+ selectedFields = /* @__PURE__ */ new Set();
2776
+ /**
2777
+ * Specifies which attributes to return in the get results.
2778
+ * Use this method when you need to:
2779
+ * - Reduce data transfer by selecting specific dinosaur attributes
2780
+ * - Optimize response size for dinosaur records
2781
+ * - Focus on relevant dinosaur characteristics only
2782
+ *
2783
+ * @example
2784
+ * ```typescript
2785
+ * // Select single attribute
2786
+ * builder.select('species')
2787
+ *
2788
+ * // Select multiple attributes
2789
+ * builder.select(['id', 'species', 'diet'])
2790
+ *
2791
+ * // Chain multiple select calls
2792
+ * builder
2793
+ * .select('id')
2794
+ * .select(['species', 'diet'])
2795
+ * ```
2796
+ *
2797
+ * @param fields - A single field name or an array of field names to return
2798
+ * @returns The builder instance for method chaining
2799
+ */
2800
+ select(fields) {
2801
+ if (typeof fields === "string") {
2802
+ this.selectedFields.add(fields);
2803
+ } else if (Array.isArray(fields)) {
2804
+ for (const field of fields) {
2805
+ this.selectedFields.add(field);
2806
+ }
1011
2807
  }
2808
+ this.options.projection = Array.from(this.selectedFields);
2809
+ return this;
1012
2810
  }
1013
- buildKeyFromIndex(key, indexConfig) {
1014
- this.validateKey(key, indexConfig);
1015
- const keyObject = {
1016
- [indexConfig.pkName]: key.pk
1017
- };
1018
- if (indexConfig.skName && key.sk) {
1019
- keyObject[indexConfig.skName] = key.sk;
1020
- }
1021
- return keyObject;
2811
+ /**
2812
+ * Sets whether to use strongly consistent reads for the get operation.
2813
+ * Use this method when you need:
2814
+ * - The most up-to-date dinosaur data
2815
+ * - To ensure you're reading the latest dinosaur status
2816
+ * - Critical safety information about dangerous species
2817
+ *
2818
+ * Note: Consistent reads consume twice the throughput
2819
+ *
2820
+ * @example
2821
+ * ```typescript
2822
+ * // Get the latest T-Rex data
2823
+ * const result = await new GetBuilder(executor, { pk: 'dinosaur#123', sk: 'profile' })
2824
+ * .consistentRead()
2825
+ * .execute();
2826
+ * ```
2827
+ *
2828
+ * @param consistentRead - Whether to use consistent reads (defaults to true)
2829
+ * @returns The builder instance for method chaining
2830
+ */
2831
+ consistentRead(consistentRead = true) {
2832
+ this.params.consistentRead = consistentRead;
2833
+ return this;
1022
2834
  }
1023
- validateKey(key, indexConfig) {
1024
- if (!key.pk) {
1025
- throw new Error("Partition key is required");
1026
- }
1027
- if (key.sk && !indexConfig.skName) {
1028
- throw new Error("Sort key provided but index does not support sort keys");
1029
- }
1030
- if (!key.sk && indexConfig.skName) {
1031
- throw new Error("Index requires a sort key but none was provided");
2835
+ /**
2836
+ * Executes the get operation against DynamoDB.
2837
+ *
2838
+ * @example
2839
+ * ```typescript
2840
+ * try {
2841
+ * const result = await new GetBuilder(executor, { pk: 'dinosaur#123', sk: 'profile' })
2842
+ * .select(['species', 'name', 'diet'])
2843
+ * .consistentRead()
2844
+ * .execute();
2845
+ *
2846
+ * if (result.item) {
2847
+ * console.log('Dinosaur found:', result.item);
2848
+ * } else {
2849
+ * console.log('Dinosaur not found');
2850
+ * }
2851
+ * } catch (error) {
2852
+ * console.error('Error getting dinosaur:', error);
2853
+ * }
2854
+ * ```
2855
+ *
2856
+ * @returns A promise that resolves to an object containing:
2857
+ * - item: The retrieved dinosaur or undefined if not found
2858
+ */
2859
+ async execute() {
2860
+ if (this.selectedFields.size > 0) {
2861
+ const expressionAttributeNames = {};
2862
+ const projectionParts = [];
2863
+ for (const path of this.selectedFields) {
2864
+ const attrName = `#attr${projectionParts.length}`;
2865
+ expressionAttributeNames[attrName] = path;
2866
+ projectionParts.push(attrName);
2867
+ }
2868
+ this.params.projectionExpression = projectionParts.join(", ");
2869
+ this.params.expressionAttributeNames = expressionAttributeNames;
1032
2870
  }
2871
+ return this.executor(this.params);
1033
2872
  }
1034
2873
  };
1035
2874
 
1036
- // src/repository/base-repository.ts
1037
- var BaseRepository = class {
1038
- constructor(table) {
1039
- this.table = table;
2875
+ // src/table.ts
2876
+ var DDB_BATCH_WRITE_LIMIT = 25;
2877
+ var DDB_BATCH_GET_LIMIT = 100;
2878
+ var Table = class {
2879
+ dynamoClient;
2880
+ tableName;
2881
+ partitionKey;
2882
+ sortKey;
2883
+ gsis;
2884
+ constructor(config) {
2885
+ this.dynamoClient = config.client;
2886
+ this.tableName = config.tableName;
2887
+ this.partitionKey = config.indexes.partitionKey;
2888
+ this.sortKey = config.indexes.sortKey;
2889
+ this.gsis = config.indexes.gsis || {};
1040
2890
  }
1041
2891
  /**
1042
- * Hook method called before inserting a record.
1043
- * Subclasses can override this method to modify the data before insertion.
1044
- * @param data - The record data.
1045
- * @returns The modified record data.
2892
+ * Creates a new item in the table, it will fail if the item already exists
2893
+ *
2894
+ * @param item The item to create
2895
+ * @returns A PutBuilder instance for chaining conditions and executing the put operation
1046
2896
  */
1047
- beforeInsert(data) {
1048
- return data;
2897
+ create(item) {
2898
+ return this.put(item).condition((op) => op.attributeNotExists(this.partitionKey));
1049
2899
  }
1050
- /**
1051
- * Hook method called before updating a record.
1052
- * Subclasses can override this method to modify the data before updating.
1053
- * @param data - The partial record data to be updated.
1054
- * @returns The modified partial record data.
1055
- */
1056
- beforeUpdate(data) {
1057
- return data;
2900
+ get(keyCondition) {
2901
+ const executor = async (params) => {
2902
+ try {
2903
+ const result = await this.dynamoClient.get({
2904
+ TableName: params.tableName,
2905
+ Key: params.key,
2906
+ ProjectionExpression: params.projectionExpression,
2907
+ ExpressionAttributeNames: params.expressionAttributeNames,
2908
+ ConsistentRead: params.consistentRead
2909
+ });
2910
+ return {
2911
+ item: result.Item ? result.Item : void 0
2912
+ };
2913
+ } catch (error) {
2914
+ console.error("Error getting item:", error);
2915
+ throw error;
2916
+ }
2917
+ };
2918
+ return new GetBuilder(executor, keyCondition, this.tableName);
1058
2919
  }
1059
2920
  /**
1060
- * Checks if a record exists in the table.
1061
- * @param key - The primary key of the record.
1062
- * @returns A promise that resolves to true if the record exists, false otherwise.
2921
+ * Updates an item in the table
2922
+ *
2923
+ * @param item The item to update
2924
+ * @returns A PutBuilder instance for chaining conditions and executing the put operation
1063
2925
  */
1064
- async exists(key) {
1065
- const item = await this.table.get(key);
1066
- return item !== null;
2926
+ put(item) {
2927
+ const executor = async (params) => {
2928
+ try {
2929
+ await this.dynamoClient.put({
2930
+ TableName: params.tableName,
2931
+ Item: params.item,
2932
+ ConditionExpression: params.conditionExpression,
2933
+ ExpressionAttributeNames: params.expressionAttributeNames,
2934
+ ExpressionAttributeValues: params.expressionAttributeValues,
2935
+ ReturnValues: params.returnValues
2936
+ });
2937
+ return params.item;
2938
+ } catch (error) {
2939
+ console.error("Error creating item:", error);
2940
+ throw error;
2941
+ }
2942
+ };
2943
+ return new PutBuilder(executor, item, this.tableName);
1067
2944
  }
1068
2945
  /**
1069
- * Type guard to check if the value is a primary key.
1070
- * @param value - The value to check.
1071
- * @returns True if the value is a primary key, false otherwise.
2946
+ * Creates a query builder for complex queries
2947
+ * If useIndex is called on the returned QueryBuilder, it will use the GSI configuration
1072
2948
  */
1073
- isPrimaryKey(value) {
1074
- return "pk" in value && typeof value.pk === "string";
2949
+ query(keyCondition) {
2950
+ const pkAttributeName = "pk";
2951
+ const skAttributeName = "sk";
2952
+ let keyConditionExpression = eq(pkAttributeName, keyCondition.pk);
2953
+ if (keyCondition.sk) {
2954
+ const keyConditionOperator = {
2955
+ eq: (value) => eq(skAttributeName, value),
2956
+ lt: (value) => lt(skAttributeName, value),
2957
+ lte: (value) => lte(skAttributeName, value),
2958
+ gt: (value) => gt(skAttributeName, value),
2959
+ gte: (value) => gte(skAttributeName, value),
2960
+ between: (lower, upper) => between(skAttributeName, lower, upper),
2961
+ beginsWith: (value) => beginsWith(skAttributeName, value),
2962
+ and: (...conditions) => and(...conditions)
2963
+ };
2964
+ const skCondition = keyCondition.sk(keyConditionOperator);
2965
+ keyConditionExpression = and(eq(pkAttributeName, keyCondition.pk), skCondition);
2966
+ }
2967
+ const executor = async (originalKeyCondition, options) => {
2968
+ let finalKeyCondition = originalKeyCondition;
2969
+ if (options.indexName) {
2970
+ const gsiName = String(options.indexName);
2971
+ const gsi = this.gsis[gsiName];
2972
+ if (!gsi) {
2973
+ throw new Error(`GSI with name "${gsiName}" does not exist on table "${this.tableName}"`);
2974
+ }
2975
+ const gsiPkAttributeName = gsi.partitionKey;
2976
+ const gsiSkAttributeName = gsi.sortKey;
2977
+ let pkValue;
2978
+ let skValue;
2979
+ let extractedSkCondition;
2980
+ if (originalKeyCondition.type === "eq") {
2981
+ pkValue = originalKeyCondition.value;
2982
+ } else if (originalKeyCondition.type === "and" && originalKeyCondition.conditions) {
2983
+ const pkCondition = originalKeyCondition.conditions.find(
2984
+ (c) => c.type === "eq" && c.attr === pkAttributeName
2985
+ );
2986
+ if (pkCondition && pkCondition.type === "eq") {
2987
+ pkValue = pkCondition.value;
2988
+ }
2989
+ const skConditions = originalKeyCondition.conditions.filter((c) => c.attr === skAttributeName);
2990
+ if (skConditions.length > 0) {
2991
+ if (skConditions.length === 1) {
2992
+ extractedSkCondition = skConditions[0];
2993
+ if (extractedSkCondition && extractedSkCondition.type === "eq") {
2994
+ skValue = extractedSkCondition.value;
2995
+ }
2996
+ } else if (skConditions.length > 1) {
2997
+ extractedSkCondition = and(...skConditions);
2998
+ }
2999
+ }
3000
+ }
3001
+ if (!pkValue) {
3002
+ throw new Error("Could not extract partition key value from key condition");
3003
+ }
3004
+ let gsiKeyCondition = eq(gsiPkAttributeName, pkValue);
3005
+ if (skValue && gsiSkAttributeName) {
3006
+ gsiKeyCondition = and(gsiKeyCondition, eq(gsiSkAttributeName, skValue));
3007
+ } else if (extractedSkCondition && gsiSkAttributeName) {
3008
+ if (extractedSkCondition.attr === skAttributeName) {
3009
+ const updatedSkCondition = {
3010
+ ...extractedSkCondition,
3011
+ attr: gsiSkAttributeName
3012
+ };
3013
+ gsiKeyCondition = and(gsiKeyCondition, updatedSkCondition);
3014
+ } else {
3015
+ gsiKeyCondition = and(gsiKeyCondition, extractedSkCondition);
3016
+ }
3017
+ }
3018
+ finalKeyCondition = gsiKeyCondition;
3019
+ }
3020
+ const expressionParams = {
3021
+ expressionAttributeNames: {},
3022
+ expressionAttributeValues: {},
3023
+ valueCounter: { count: 0 }
3024
+ };
3025
+ const keyConditionExpression2 = buildExpression(finalKeyCondition, expressionParams);
3026
+ let filterExpression;
3027
+ if (options.filter) {
3028
+ filterExpression = buildExpression(options.filter, expressionParams);
3029
+ }
3030
+ const projectionExpression = options.projection?.map((p) => generateAttributeName(expressionParams, p)).join(", ");
3031
+ const { expressionAttributeNames, expressionAttributeValues } = expressionParams;
3032
+ const { indexName, limit, consistentRead, scanIndexForward, lastEvaluatedKey } = options;
3033
+ const params = {
3034
+ TableName: this.tableName,
3035
+ KeyConditionExpression: keyConditionExpression2,
3036
+ FilterExpression: filterExpression,
3037
+ ExpressionAttributeNames: expressionAttributeNames,
3038
+ ExpressionAttributeValues: expressionAttributeValues,
3039
+ IndexName: indexName,
3040
+ Limit: limit,
3041
+ ConsistentRead: consistentRead,
3042
+ ScanIndexForward: scanIndexForward,
3043
+ ProjectionExpression: projectionExpression,
3044
+ ExclusiveStartKey: lastEvaluatedKey
3045
+ };
3046
+ try {
3047
+ const result = await this.dynamoClient.query(params);
3048
+ return {
3049
+ items: result.Items,
3050
+ lastEvaluatedKey: result.LastEvaluatedKey
3051
+ };
3052
+ } catch (error) {
3053
+ console.log(debugCommand(params));
3054
+ console.error("Error querying items:", error);
3055
+ throw error;
3056
+ }
3057
+ };
3058
+ return new QueryBuilder(executor, keyConditionExpression);
1075
3059
  }
1076
- /**
1077
- * Creates a new record in the table.
1078
- * @param data - The record data.
1079
- * @returns A PutBuilder instance to execute the put operation.
1080
- */
1081
- create(data) {
1082
- const key = this.createPrimaryKey(data);
1083
- const item = {
1084
- ...data,
1085
- ...key
3060
+ delete(keyCondition) {
3061
+ const executor = async (params) => {
3062
+ try {
3063
+ const result = await this.dynamoClient.delete({
3064
+ TableName: params.tableName,
3065
+ Key: params.key,
3066
+ ConditionExpression: params.conditionExpression,
3067
+ ExpressionAttributeNames: params.expressionAttributeNames,
3068
+ ExpressionAttributeValues: params.expressionAttributeValues,
3069
+ ReturnValues: params.returnValues
3070
+ });
3071
+ return {
3072
+ item: result.Attributes
3073
+ };
3074
+ } catch (error) {
3075
+ console.error("Error deleting item:", error);
3076
+ throw error;
3077
+ }
1086
3078
  };
1087
- const indexConfig = this.table.getIndexConfig();
1088
- const builder = this.table.put(item).set(this.getTypeAttributeName(), this.getType()).whereNotExists(indexConfig.pkName);
1089
- if (indexConfig.skName) {
1090
- builder.whereNotExists(indexConfig.skName);
1091
- }
1092
- return builder;
3079
+ return new DeleteBuilder(executor, this.tableName, keyCondition);
1093
3080
  }
1094
3081
  /**
1095
- * Updates an existing record in the table.
1096
- * @param key - The primary key of the record.
1097
- * @param updates - The partial record data to be updated.
1098
- * @returns A promise that resolves to the updated record or null if the record does not exist.
3082
+ * Updates an item in the table
3083
+ *
3084
+ * @param keyCondition The primary key of the item to update
3085
+ * @returns An UpdateBuilder instance for chaining update operations and conditions
1099
3086
  */
1100
- async update(key, updates) {
1101
- const processed = this.beforeUpdate(updates);
1102
- const updateData = {
1103
- ...processed,
1104
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3087
+ update(keyCondition) {
3088
+ const executor = async (params) => {
3089
+ try {
3090
+ const result = await this.dynamoClient.update({
3091
+ TableName: params.tableName,
3092
+ Key: params.key,
3093
+ UpdateExpression: params.updateExpression,
3094
+ ConditionExpression: params.conditionExpression,
3095
+ ExpressionAttributeNames: params.expressionAttributeNames,
3096
+ ExpressionAttributeValues: params.expressionAttributeValues,
3097
+ ReturnValues: params.returnValues
3098
+ });
3099
+ return {
3100
+ item: result.Attributes
3101
+ };
3102
+ } catch (error) {
3103
+ console.error("Error updating item:", error);
3104
+ throw error;
3105
+ }
1105
3106
  };
1106
- const result = await this.table.update(key).setMany(updateData).execute();
1107
- if (!result.Attributes) return null;
1108
- return this.findOne(key);
3107
+ return new UpdateBuilder(executor, this.tableName, keyCondition);
1109
3108
  }
1110
3109
  /**
1111
- * Upserts (inserts or updates) a record in the table.
1112
- * @param data - The record data.
1113
- * @returns A PutBuilder instance to execute the put operation.
3110
+ * Creates a transaction builder for performing multiple operations atomically
1114
3111
  */
1115
- upsert(data) {
1116
- const key = this.createPrimaryKey(data);
1117
- return this.table.put({
1118
- ...data,
1119
- ...key
3112
+ transactionBuilder() {
3113
+ const executor = async (params) => {
3114
+ await this.dynamoClient.transactWrite(params);
3115
+ };
3116
+ return new TransactionBuilder(executor, {
3117
+ partitionKey: this.partitionKey,
3118
+ sortKey: this.sortKey
1120
3119
  });
1121
3120
  }
1122
3121
  /**
1123
- * Deletes a record from the table.
1124
- * @param keyOrDTO - The primary key or the record data.
1125
- * @returns A promise that resolves when the record is deleted.
3122
+ * Executes a transaction using a callback function
3123
+ *
3124
+ * @param callback A function that receives a transaction context and performs operations on it
3125
+ * @param options Optional transaction options
3126
+ * @returns A promise that resolves when the transaction is complete
1126
3127
  */
1127
- async delete(keyOrDTO) {
1128
- if (this.isPrimaryKey(keyOrDTO)) {
1129
- this.table.delete(keyOrDTO);
1130
- return;
1131
- }
1132
- const key = this.createPrimaryKey(keyOrDTO);
1133
- await this.table.delete(key);
3128
+ transaction(callback, options) {
3129
+ const executor = async () => {
3130
+ const transactionExecutor = async (params) => {
3131
+ await this.dynamoClient.transactWrite(params);
3132
+ };
3133
+ const transaction = new TransactionBuilder(transactionExecutor, {
3134
+ partitionKey: this.partitionKey,
3135
+ sortKey: this.sortKey
3136
+ });
3137
+ if (options) {
3138
+ transaction.withOptions(options);
3139
+ }
3140
+ const result = await callback(transaction);
3141
+ await transaction.execute();
3142
+ return result;
3143
+ };
3144
+ return executor();
1134
3145
  }
1135
3146
  /**
1136
- * Finds a single record by its primary key.
1137
- * @param key - The primary key of the record.
1138
- * @returns A promise that resolves to the record or null if the record does not exist.
3147
+ * Creates a condition check operation for use in transactions
3148
+ *
3149
+ * This is useful for when you require a transaction to succeed only when a specific condition is met on a
3150
+ * a record within the database that you are not directly updating.
3151
+ *
3152
+ * For example, you are updating a record and you want to ensure that another record exists and/or has a specific value before proceeding.
1139
3153
  */
1140
- async findOne(key) {
1141
- const results = await this.table.query(key).whereEquals(this.getTypeAttributeName(), this.getType()).limit(1).execute();
1142
- const item = results.Items?.[0];
1143
- if (!item) {
1144
- return null;
1145
- }
1146
- return item;
3154
+ conditionCheck(keyCondition) {
3155
+ return new ConditionCheckBuilder(this.tableName, keyCondition);
1147
3156
  }
1148
3157
  /**
1149
- * Finds a single record by its primary key or throws an error if the record does not exist.
1150
- * @param key - The primary key of the record.
1151
- * @returns A promise that resolves to the record.
1152
- * @throws An error if the record does not exist.
3158
+ * Performs a batch get operation to retrieve multiple items at once
3159
+ *
3160
+ * @param keys Array of primary keys to retrieve
3161
+ * @returns A promise that resolves to the retrieved items
1153
3162
  */
1154
- async findOrFail(key) {
1155
- const result = await this.findOne(key);
1156
- if (!result) {
1157
- throw new Error("Item not found");
3163
+ async batchGet(keys) {
3164
+ const allItems = [];
3165
+ const allUnprocessedKeys = [];
3166
+ for (const chunk of chunkArray(keys, DDB_BATCH_GET_LIMIT)) {
3167
+ const formattedKeys = chunk.map((key) => ({
3168
+ pk: key.pk,
3169
+ sk: key.sk
3170
+ }));
3171
+ const params = {
3172
+ RequestItems: {
3173
+ [this.tableName]: {
3174
+ Keys: formattedKeys
3175
+ }
3176
+ }
3177
+ };
3178
+ try {
3179
+ const result = await this.dynamoClient.batchGet(params);
3180
+ if (result.Responses?.[this.tableName]) {
3181
+ allItems.push(...result.Responses[this.tableName]);
3182
+ }
3183
+ const unprocessedKeysArray = result.UnprocessedKeys?.[this.tableName]?.Keys || [];
3184
+ const unprocessedKeys = unprocessedKeysArray.map((key) => ({
3185
+ pk: key.pk,
3186
+ sk: key.sk
3187
+ }));
3188
+ if (unprocessedKeys.length > 0) {
3189
+ allUnprocessedKeys.push(...unprocessedKeys);
3190
+ }
3191
+ } catch (error) {
3192
+ console.error("Error in batch get operation:", error);
3193
+ throw error;
3194
+ }
1158
3195
  }
1159
- return result;
3196
+ return {
3197
+ items: allItems,
3198
+ unprocessedKeys: allUnprocessedKeys
3199
+ };
1160
3200
  }
1161
3201
  /**
1162
- * Creates a query builder for querying records by their primary key.
1163
- * @param key - The primary key of the record.
1164
- * @returns A QueryBuilder instance to build and execute the query.
3202
+ * Performs a batch write operation to put or delete multiple items at once
3203
+ *
3204
+ * @param operations Array of put or delete operations
3205
+ * @returns A promise that resolves to any unprocessed operations
1165
3206
  */
1166
- query(key) {
1167
- return this.table.query(key).whereEquals(this.getTypeAttributeName(), this.getType());
3207
+ async batchWrite(operations) {
3208
+ const allUnprocessedItems = [];
3209
+ for (const chunk of chunkArray(operations, DDB_BATCH_WRITE_LIMIT)) {
3210
+ const writeRequests = chunk.map((operation) => {
3211
+ if (operation.type === "put") {
3212
+ return {
3213
+ PutRequest: {
3214
+ Item: operation.item
3215
+ }
3216
+ };
3217
+ }
3218
+ return {
3219
+ DeleteRequest: {
3220
+ Key: {
3221
+ pk: operation.key.pk,
3222
+ sk: operation.key.sk
3223
+ }
3224
+ }
3225
+ };
3226
+ });
3227
+ const params = {
3228
+ RequestItems: {
3229
+ [this.tableName]: writeRequests
3230
+ }
3231
+ };
3232
+ try {
3233
+ const result = await this.dynamoClient.batchWrite(params);
3234
+ const unprocessedRequestsArray = result.UnprocessedItems?.[this.tableName] || [];
3235
+ if (unprocessedRequestsArray.length > 0) {
3236
+ const unprocessedItems = unprocessedRequestsArray.map((request) => {
3237
+ if (request?.PutRequest?.Item) {
3238
+ return {
3239
+ type: "put",
3240
+ item: request.PutRequest.Item
3241
+ };
3242
+ }
3243
+ if (request?.DeleteRequest?.Key) {
3244
+ return {
3245
+ type: "delete",
3246
+ key: {
3247
+ pk: request.DeleteRequest.Key.pk,
3248
+ sk: request.DeleteRequest.Key.sk
3249
+ }
3250
+ };
3251
+ }
3252
+ throw new Error("Invalid unprocessed item format returned from DynamoDB");
3253
+ });
3254
+ allUnprocessedItems.push(...unprocessedItems);
3255
+ }
3256
+ } catch (error) {
3257
+ console.error("Error in batch write operation:", error);
3258
+ throw error;
3259
+ }
3260
+ }
3261
+ return {
3262
+ unprocessedItems: allUnprocessedItems
3263
+ };
1168
3264
  }
1169
3265
  };
1170
3266
  export {
1171
- BaseRepository,
1172
- ConditionalCheckFailedError,
1173
- DynamoError,
1174
- ExponentialBackoffStrategy,
1175
- ResourceNotFoundError,
1176
- Table
3267
+ ConditionCheckBuilder,
3268
+ DeleteBuilder,
3269
+ Paginator,
3270
+ PutBuilder,
3271
+ QueryBuilder,
3272
+ Table,
3273
+ TransactionBuilder,
3274
+ UpdateBuilder,
3275
+ and,
3276
+ attributeExists,
3277
+ attributeNotExists,
3278
+ beginsWith,
3279
+ between,
3280
+ contains,
3281
+ eq,
3282
+ gt,
3283
+ gte,
3284
+ lt,
3285
+ lte,
3286
+ ne,
3287
+ not,
3288
+ or
1177
3289
  };