dyno-table 2.6.0 → 2.6.1

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