dyno-table 2.2.0 → 2.2.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.
Files changed (53) hide show
  1. package/dist/{batch-builder-BPoHyN_Q.d.cts → batch-builder-BiQDIZ7p.d.cts} +1 -1
  2. package/dist/{batch-builder-Cdo49C2r.d.ts → batch-builder-CNsLS6sR.d.ts} +1 -1
  3. package/dist/builders/condition-check-builder.cjs.map +1 -1
  4. package/dist/builders/condition-check-builder.d.cts +3 -3
  5. package/dist/builders/condition-check-builder.d.ts +3 -3
  6. package/dist/builders/condition-check-builder.js.map +1 -1
  7. package/dist/builders/delete-builder.cjs.map +1 -1
  8. package/dist/builders/delete-builder.d.cts +4 -4
  9. package/dist/builders/delete-builder.d.ts +4 -4
  10. package/dist/builders/delete-builder.js.map +1 -1
  11. package/dist/builders/put-builder.cjs.map +1 -1
  12. package/dist/builders/put-builder.d.cts +4 -4
  13. package/dist/builders/put-builder.d.ts +4 -4
  14. package/dist/builders/put-builder.js.map +1 -1
  15. package/dist/builders/query-builder.cjs +19 -5
  16. package/dist/builders/query-builder.cjs.map +1 -1
  17. package/dist/builders/query-builder.d.cts +3 -3
  18. package/dist/builders/query-builder.d.ts +3 -3
  19. package/dist/builders/query-builder.js +19 -5
  20. package/dist/builders/query-builder.js.map +1 -1
  21. package/dist/builders/transaction-builder.cjs.map +1 -1
  22. package/dist/builders/transaction-builder.d.cts +2 -2
  23. package/dist/builders/transaction-builder.d.ts +2 -2
  24. package/dist/builders/transaction-builder.js.map +1 -1
  25. package/dist/builders/update-builder.cjs.map +1 -1
  26. package/dist/builders/update-builder.d.cts +3 -3
  27. package/dist/builders/update-builder.d.ts +3 -3
  28. package/dist/builders/update-builder.js.map +1 -1
  29. package/dist/{conditions-CC3NDfUU.d.cts → conditions-CcZL0sR2.d.cts} +1 -1
  30. package/dist/{conditions-DD0bvyHm.d.ts → conditions-D_w7vVYG.d.ts} +1 -1
  31. package/dist/conditions.d.cts +1 -1
  32. package/dist/conditions.d.ts +1 -1
  33. package/dist/entity.cjs.map +1 -1
  34. package/dist/entity.d.cts +10 -10
  35. package/dist/entity.d.ts +10 -10
  36. package/dist/entity.js.map +1 -1
  37. package/dist/index.cjs +1489 -1475
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +10 -10
  40. package/dist/index.d.ts +10 -10
  41. package/dist/index.js +1489 -1475
  42. package/dist/index.js.map +1 -1
  43. package/dist/{query-builder-DoZzZz_c.d.cts → query-builder-D3URwK9k.d.cts} +2 -2
  44. package/dist/{query-builder-CUWdavZw.d.ts → query-builder-cfEkU0_w.d.ts} +2 -2
  45. package/dist/{table-f-3wsT7K.d.cts → table-ClST8nkR.d.cts} +3 -3
  46. package/dist/{table-CZBMkW2Z.d.ts → table-vE3cGoDy.d.ts} +3 -3
  47. package/dist/table.cjs +19 -5
  48. package/dist/table.cjs.map +1 -1
  49. package/dist/table.d.cts +4 -4
  50. package/dist/table.d.ts +4 -4
  51. package/dist/table.js +19 -5
  52. package/dist/table.js.map +1 -1
  53. package/package.json +7 -2
package/dist/index.js CHANGED
@@ -535,163 +535,6 @@ function debugCommand(command) {
535
535
  };
536
536
  }
537
537
 
538
- // src/builders/condition-check-builder.ts
539
- var ConditionCheckBuilder = class {
540
- key;
541
- tableName;
542
- conditionExpression;
543
- constructor(tableName, key) {
544
- this.tableName = tableName;
545
- this.key = key;
546
- }
547
- /**
548
- * Adds a condition that must be satisfied for the check to succeed.
549
- *
550
- * @example
551
- * ```typescript
552
- * // Check dinosaur health and behavior
553
- * builder.condition(op =>
554
- * op.and([
555
- * op.gt('stats.health', 50),
556
- * op.not(op.eq('status', 'SEDATED')),
557
- * op.lt('aggressionLevel', 8)
558
- * ])
559
- * );
560
- *
561
- * // Verify habitat conditions
562
- * builder.condition(op =>
563
- * op.and([
564
- * op.eq('powerStatus', 'ONLINE'),
565
- * op.between('temperature', 20, 30),
566
- * op.attributeExists('lastMaintenance')
567
- * ])
568
- * );
569
- *
570
- * // Check breeding conditions
571
- * builder.condition(op =>
572
- * op.and([
573
- * op.eq('species', 'VELOCIRAPTOR'),
574
- * op.gte('age', 3),
575
- * op.eq('geneticPurity', 100)
576
- * ])
577
- * );
578
- * ```
579
- *
580
- * @param condition - Either a Condition DynamoItem or a callback function that builds the condition
581
- * @returns The builder instance for method chaining
582
- */
583
- condition(condition) {
584
- if (typeof condition === "function") {
585
- const conditionOperator = {
586
- eq,
587
- ne,
588
- lt,
589
- lte,
590
- gt,
591
- gte,
592
- between,
593
- inArray,
594
- beginsWith,
595
- contains,
596
- attributeExists,
597
- attributeNotExists,
598
- and,
599
- or,
600
- not
601
- };
602
- this.conditionExpression = condition(conditionOperator);
603
- } else {
604
- this.conditionExpression = condition;
605
- }
606
- return this;
607
- }
608
- /**
609
- * Generates the DynamoDB command parameters for direct execution.
610
- * Use this method when you want to:
611
- * - Execute the condition check as a standalone operation
612
- * - Get the raw DynamoDB command for custom execution
613
- * - Inspect the generated command parameters
614
- *
615
- * @example
616
- * ```ts
617
- * const command = new ConditionCheckBuilder('myTable', { id: '123' })
618
- * .condition(op => op.attributeExists('status'))
619
- * .toDynamoCommand();
620
- * // Use command with DynamoDB client
621
- * ```
622
- *
623
- * @throws {Error} If no condition has been set
624
- * @returns The DynamoDB command parameters
625
- */
626
- toDynamoCommand() {
627
- if (!this.conditionExpression) {
628
- throw new Error("Condition is required for condition check operations");
629
- }
630
- const { expression, names, values } = prepareExpressionParams(this.conditionExpression);
631
- if (!expression) {
632
- throw new Error("Failed to generate condition expression");
633
- }
634
- return {
635
- tableName: this.tableName,
636
- key: this.key,
637
- conditionExpression: expression,
638
- expressionAttributeNames: names,
639
- expressionAttributeValues: values
640
- };
641
- }
642
- /**
643
- * Adds this condition check operation to a transaction.
644
- *
645
- * @example
646
- * ```ts
647
- * const transaction = new TransactionBuilder();
648
- * new ConditionCheckBuilder('habitats', { id: 'PADDOCK-B' })
649
- * .condition(op => op.and([
650
- * op.eq('securityStatus', 'ACTIVE'),
651
- * op.lt('currentOccupants', 3),
652
- * op.eq('habitatType', 'CARNIVORE')
653
- * ]))
654
- * .withTransaction(transaction);
655
- * // Add dinosaur transfer operations
656
- * ```
657
- *
658
- * @param transaction - The transaction builder to add this operation to
659
- * @throws {Error} If no condition has been set
660
- * @returns The builder instance for method chaining
661
- */
662
- withTransaction(transaction) {
663
- if (!this.conditionExpression) {
664
- throw new Error("Condition is required for condition check operations");
665
- }
666
- const command = this.toDynamoCommand();
667
- transaction.conditionCheckWithCommand(command);
668
- return this;
669
- }
670
- /**
671
- * Gets a human-readable representation of the condition check command
672
- * with all expression placeholders replaced by their actual values.
673
- *
674
- * @example
675
- * ```ts
676
- * const debugInfo = new ConditionCheckBuilder('dinosaurs', { id: 'TREX-001' })
677
- * .condition(op => op.and([
678
- * op.between('stats.health', 50, 100),
679
- * op.not(op.eq('status', 'SEDATED')),
680
- * op.attributeExists('lastFeedingTime')
681
- * op.eq('version', 1)
682
- * ]))
683
- * .debug();
684
- * console.log(debugInfo);
685
- * ```
686
- *
687
- * @returns A readable representation of the condition check command with resolved expressions
688
- */
689
- debug() {
690
- const command = this.toDynamoCommand();
691
- return debugCommand(command);
692
- }
693
- };
694
-
695
538
  // src/builders/delete-builder.ts
696
539
  var DeleteBuilder = class {
697
540
  options = {
@@ -911,216 +754,58 @@ var DeleteBuilder = class {
911
754
  }
912
755
  };
913
756
 
914
- // src/builders/get-builder.ts
915
- var GetBuilder = class {
916
- /**
917
- * Creates a new GetBuilder instance.
918
- *
919
- * @param executor - Function that executes the get operation
920
- * @param key - Primary key of the item to retrieve
921
- * @param tableName - Name of the DynamoDB table
922
- */
923
- constructor(executor, key, tableName) {
757
+ // src/builders/put-builder.ts
758
+ var PutBuilder = class {
759
+ item;
760
+ options;
761
+ executor;
762
+ tableName;
763
+ constructor(executor, item, tableName) {
924
764
  this.executor = executor;
925
- this.params = {
926
- tableName,
927
- key
765
+ this.item = item;
766
+ this.tableName = tableName;
767
+ this.options = {
768
+ returnValues: "NONE"
928
769
  };
929
770
  }
930
- params;
931
- options = {};
932
- selectedFields = /* @__PURE__ */ new Set();
771
+ set(valuesOrPath, value) {
772
+ if (typeof valuesOrPath === "object") {
773
+ Object.assign(this.item, valuesOrPath);
774
+ } else {
775
+ this.item[valuesOrPath] = value;
776
+ }
777
+ return this;
778
+ }
933
779
  /**
934
- * Specifies which attributes to return in the get results.
780
+ * Adds a condition that must be satisfied for the put operation to succeed.
935
781
  *
936
782
  * @example
937
- * ```typescript
938
- * // Select single attribute
939
- * builder.select('species')
940
- *
941
- * // Select multiple attributes
942
- * builder.select(['id', 'species', 'diet'])
783
+ * ```ts
784
+ * // Ensure item doesn't exist (insert only)
785
+ * builder.condition(op => op.attributeNotExists('id'))
943
786
  *
944
- * // Chain multiple select calls
945
- * builder
946
- * .select('id')
947
- * .select(['species', 'diet'])
787
+ * // Complex condition with version check
788
+ * builder.condition(op =>
789
+ * op.and([
790
+ * op.attributeExists('id'),
791
+ * op.eq('version', currentVersion),
792
+ * op.eq('status', 'ACTIVE')
793
+ * ])
794
+ * )
948
795
  * ```
949
796
  *
950
- * @param fields - A single field name or an array of field names to return
797
+ * @param condition - Either a Condition object or a callback function that builds the condition
951
798
  * @returns The builder instance for method chaining
952
799
  */
953
- select(fields) {
954
- if (typeof fields === "string") {
955
- this.selectedFields.add(fields);
956
- } else if (Array.isArray(fields)) {
957
- for (const field of fields) {
958
- this.selectedFields.add(field);
959
- }
960
- }
961
- this.options.projection = Array.from(this.selectedFields);
962
- return this;
963
- }
964
800
  /**
965
- * Sets whether to use strongly consistent reads for the get operation.
966
- * Use this method when you need:
967
- * - The most up-to-date dinosaur data
968
- * - To ensure you're reading the latest dinosaur status
969
- * - Critical safety information about dangerous species
970
- *
971
- * Note: Consistent reads consume twice the throughput
801
+ * Adds a condition that must be satisfied for the put operation to succeed.
972
802
  *
973
803
  * @example
974
804
  * ```typescript
975
- * // Get the latest T-Rex data
976
- * const result = await new GetBuilder(executor, { pk: 'dinosaur#123', sk: 'profile' })
977
- * .consistentRead()
978
- * .execute();
979
- * ```
980
- *
981
- * @param consistentRead - Whether to use consistent reads (defaults to true)
982
- * @returns The builder instance for method chaining
983
- */
984
- consistentRead(consistentRead = true) {
985
- this.params.consistentRead = consistentRead;
986
- return this;
987
- }
988
- /**
989
- * Adds this get operation to a batch with optional entity type information.
990
- *
991
- * @example Basic Usage
992
- * ```ts
993
- * const batch = table.batchBuilder();
994
- *
995
- * // Add multiple get operations to batch
996
- * dinosaurRepo.get({ id: 'dino-1' }).withBatch(batch);
997
- * dinosaurRepo.get({ id: 'dino-2' }).withBatch(batch);
998
- * dinosaurRepo.get({ id: 'dino-3' }).withBatch(batch);
999
- *
1000
- * // Execute all gets efficiently
1001
- * const results = await batch.execute();
1002
- * ```
1003
- *
1004
- * @example Typed Usage
1005
- * ```ts
1006
- * const batch = table.batchBuilder<{
1007
- * User: UserEntity;
1008
- * Order: OrderEntity;
1009
- * }>();
1010
- *
1011
- * // Add operations with type information
1012
- * userRepo.get({ id: 'user-1' }).withBatch(batch, 'User');
1013
- * orderRepo.get({ id: 'order-1' }).withBatch(batch, 'Order');
1014
- *
1015
- * // Execute and get typed results
1016
- * const result = await batch.execute();
1017
- * const users: UserEntity[] = result.reads.itemsByType.User;
1018
- * const orders: OrderEntity[] = result.reads.itemsByType.Order;
1019
- * ```
1020
- *
1021
- * @param batch - The batch builder to add this operation to
1022
- * @param entityType - Optional entity type key for type tracking
1023
- */
1024
- withBatch(batch, entityType) {
1025
- const command = this.toDynamoCommand();
1026
- batch.getWithCommand(command, entityType);
1027
- }
1028
- /**
1029
- * Converts the builder configuration to a DynamoDB command
1030
- */
1031
- toDynamoCommand() {
1032
- const expressionParams = {
1033
- expressionAttributeNames: {}};
1034
- const projectionExpression = Array.from(this.selectedFields).map((p) => generateAttributeName(expressionParams, p)).join(", ");
1035
- const { expressionAttributeNames } = expressionParams;
1036
- return {
1037
- ...this.params,
1038
- projectionExpression: projectionExpression.length > 0 ? projectionExpression : void 0,
1039
- expressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 ? expressionAttributeNames : void 0
1040
- };
1041
- }
1042
- /**
1043
- * Executes the get operation against DynamoDB.
1044
- *
1045
- * @example
1046
- * ```typescript
1047
- * try {
1048
- * const result = await new GetBuilder(executor, { pk: 'dinosaur#123', sk: 'profile' })
1049
- * .select(['species', 'name', 'diet'])
1050
- * .consistentRead()
1051
- * .execute();
1052
- *
1053
- * if (result.item) {
1054
- * console.log('Dinosaur found:', result.item);
1055
- * } else {
1056
- * console.log('Dinosaur not found');
1057
- * }
1058
- * } catch (error) {
1059
- * console.error('Error getting dinosaur:', error);
1060
- * }
1061
- * ```
1062
- *
1063
- * @returns A promise that resolves to an object containing:
1064
- * - item: The retrieved dinosaur or undefined if not found
1065
- */
1066
- async execute() {
1067
- const command = this.toDynamoCommand();
1068
- return this.executor(command);
1069
- }
1070
- };
1071
-
1072
- // src/builders/put-builder.ts
1073
- var PutBuilder = class {
1074
- item;
1075
- options;
1076
- executor;
1077
- tableName;
1078
- constructor(executor, item, tableName) {
1079
- this.executor = executor;
1080
- this.item = item;
1081
- this.tableName = tableName;
1082
- this.options = {
1083
- returnValues: "NONE"
1084
- };
1085
- }
1086
- set(valuesOrPath, value) {
1087
- if (typeof valuesOrPath === "object") {
1088
- Object.assign(this.item, valuesOrPath);
1089
- } else {
1090
- this.item[valuesOrPath] = value;
1091
- }
1092
- return this;
1093
- }
1094
- /**
1095
- * Adds a condition that must be satisfied for the put operation to succeed.
1096
- *
1097
- * @example
1098
- * ```ts
1099
- * // Ensure item doesn't exist (insert only)
1100
- * builder.condition(op => op.attributeNotExists('id'))
1101
- *
1102
- * // Complex condition with version check
1103
- * builder.condition(op =>
1104
- * op.and([
1105
- * op.attributeExists('id'),
1106
- * op.eq('version', currentVersion),
1107
- * op.eq('status', 'ACTIVE')
1108
- * ])
1109
- * )
1110
- * ```
1111
- *
1112
- * @param condition - Either a Condition object or a callback function that builds the condition
1113
- * @returns The builder instance for method chaining
1114
- */
1115
- /**
1116
- * Adds a condition that must be satisfied for the put operation to succeed.
1117
- *
1118
- * @example
1119
- * ```typescript
1120
- * // Ensure unique dinosaur ID
1121
- * builder.condition(op =>
1122
- * op.attributeNotExists('id')
1123
- * );
805
+ * // Ensure unique dinosaur ID
806
+ * builder.condition(op =>
807
+ * op.attributeNotExists('id')
808
+ * );
1124
809
  *
1125
810
  * // Verify habitat requirements
1126
811
  * builder.condition(op =>
@@ -1669,12 +1354,26 @@ var FilterBuilder = class {
1669
1354
  const newCondition = typeof condition === "function" ? condition(this.getConditionOperator()) : condition;
1670
1355
  if (this.options.filter) {
1671
1356
  if (this.options.filter.type === "and" && this.options.filter.conditions) {
1672
- this.options.filter = {
1673
- type: "and",
1674
- conditions: [...this.options.filter.conditions, newCondition]
1675
- };
1357
+ if (newCondition.type === "and" && newCondition.conditions) {
1358
+ this.options.filter = {
1359
+ type: "and",
1360
+ conditions: [...this.options.filter.conditions, ...newCondition.conditions]
1361
+ };
1362
+ } else {
1363
+ this.options.filter = {
1364
+ type: "and",
1365
+ conditions: [...this.options.filter.conditions, newCondition]
1366
+ };
1367
+ }
1676
1368
  } else {
1677
- this.options.filter = and(this.options.filter, newCondition);
1369
+ if (newCondition.type === "and" && newCondition.conditions) {
1370
+ this.options.filter = {
1371
+ type: "and",
1372
+ conditions: [this.options.filter, ...newCondition.conditions]
1373
+ };
1374
+ } else {
1375
+ this.options.filter = and(this.options.filter, newCondition);
1376
+ }
1678
1377
  }
1679
1378
  } else {
1680
1379
  this.options.filter = newCondition;
@@ -2051,78 +1750,6 @@ var QueryBuilder = class _QueryBuilder extends FilterBuilder {
2051
1750
  }
2052
1751
  };
2053
1752
 
2054
- // src/builders/scan-builder.ts
2055
- var ScanBuilder = class _ScanBuilder extends FilterBuilder {
2056
- executor;
2057
- constructor(executor) {
2058
- super();
2059
- this.executor = executor;
2060
- }
2061
- /**
2062
- * Creates a deep clone of this ScanBuilder instance.
2063
- *
2064
- * @returns A new ScanBuilder instance with the same configuration
2065
- */
2066
- clone() {
2067
- const clone = new _ScanBuilder(this.executor);
2068
- clone.options = {
2069
- ...this.options,
2070
- filter: this.deepCloneFilter(this.options.filter)
2071
- };
2072
- clone.selectedFields = new Set(this.selectedFields);
2073
- return clone;
2074
- }
2075
- deepCloneFilter(filter) {
2076
- if (!filter) return filter;
2077
- if (filter.type === "and" || filter.type === "or") {
2078
- return {
2079
- ...filter,
2080
- conditions: filter.conditions?.map((condition) => this.deepCloneFilter(condition)).filter((c) => c !== void 0)
2081
- };
2082
- }
2083
- return { ...filter };
2084
- }
2085
- /**
2086
- * Executes the scan against DynamoDB and returns a generator that behaves like an array.
2087
- *
2088
- * The generator automatically handles pagination and provides array-like methods
2089
- * for processing results efficiently without loading everything into memory at once.
2090
- *
2091
- * @example
2092
- * ```typescript
2093
- * try {
2094
- * // Find all dinosaurs with high aggression levels with automatic pagination
2095
- * const results = await new ScanBuilder(executor)
2096
- * .filter(op =>
2097
- * op.and([
2098
- * op.eq('status', 'ACTIVE'),
2099
- * op.gt('aggressionLevel', 7)
2100
- * ])
2101
- * )
2102
- * .execute();
2103
- *
2104
- * // Use like an array with automatic pagination
2105
- * for await (const dinosaur of results) {
2106
- * console.log(`Processing dangerous dinosaur: ${dinosaur.name}`);
2107
- * }
2108
- *
2109
- * // Or convert to array and use array methods
2110
- * const allItems = await results.toArray();
2111
- * const criticalThreats = allItems.filter(dino => dino.aggressionLevel > 9);
2112
- * const totalCount = allItems.length;
2113
- * } catch (error) {
2114
- * console.error('Security scan failed:', error);
2115
- * }
2116
- * ```
2117
- *
2118
- * @returns A promise that resolves to a ResultGenerator that behaves like an array
2119
- */
2120
- async execute() {
2121
- const directExecutor = () => this.executor(this.options);
2122
- return new ResultIterator(this, directExecutor);
2123
- }
2124
- };
2125
-
2126
1753
  // src/utils/debug-transaction.ts
2127
1754
  function debugTransactionItem(item) {
2128
1755
  const result = {
@@ -3226,1174 +2853,1561 @@ var UpdateBuilder = class {
3226
2853
  }
3227
2854
  };
3228
2855
 
3229
- // src/utils/chunk-array.ts
3230
- function* chunkArray(array, size) {
3231
- if (size <= 0) {
3232
- throw new Error("Chunk size must be greater than 0");
3233
- }
3234
- for (let i = 0; i < array.length; i += size) {
3235
- yield array.slice(i, i + size);
3236
- }
2856
+ // src/builders/entity-aware-builders.ts
2857
+ function createEntityAwareBuilder(builder, entityName) {
2858
+ return new Proxy(builder, {
2859
+ get(target, prop, receiver) {
2860
+ if (prop === "entityName") {
2861
+ return entityName;
2862
+ }
2863
+ if (prop === "withBatch" && typeof target[prop] === "function") {
2864
+ return (batch, entityType) => {
2865
+ const typeToUse = entityType ?? entityName;
2866
+ const fn = target[prop];
2867
+ return fn.call(target, batch, typeToUse);
2868
+ };
2869
+ }
2870
+ return Reflect.get(target, prop, receiver);
2871
+ }
2872
+ });
3237
2873
  }
3238
-
3239
- // src/table.ts
3240
- var DDB_BATCH_WRITE_LIMIT = 25;
3241
- var DDB_BATCH_GET_LIMIT = 100;
3242
- var Table = class {
3243
- dynamoClient;
3244
- tableName;
3245
- /**
3246
- * The column name of the partitionKey for the Table
3247
- */
3248
- partitionKey;
3249
- /**
3250
- * The column name of the sortKey for the Table
3251
- */
3252
- sortKey;
3253
- /**
3254
- * The Global Secondary Indexes that are configured on this table
3255
- */
3256
- gsis;
3257
- constructor(config) {
3258
- this.dynamoClient = config.client;
3259
- this.tableName = config.tableName;
3260
- this.partitionKey = config.indexes.partitionKey;
3261
- this.sortKey = config.indexes.sortKey;
3262
- this.gsis = config.indexes.gsis || {};
2874
+ function createEntityAwarePutBuilder(builder, entityName) {
2875
+ return createEntityAwareBuilder(builder, entityName);
2876
+ }
2877
+ function createEntityAwareGetBuilder(builder, entityName) {
2878
+ return createEntityAwareBuilder(builder, entityName);
2879
+ }
2880
+ function createEntityAwareDeleteBuilder(builder, entityName) {
2881
+ return createEntityAwareBuilder(builder, entityName);
2882
+ }
2883
+ var EntityAwareUpdateBuilder = class {
2884
+ forceRebuildIndexes = [];
2885
+ entityName;
2886
+ builder;
2887
+ entityConfig;
2888
+ updateDataApplied = false;
2889
+ constructor(builder, entityName) {
2890
+ this.builder = builder;
2891
+ this.entityName = entityName;
3263
2892
  }
3264
- createKeyForPrimaryIndex(keyCondition) {
3265
- const primaryCondition = { [this.partitionKey]: keyCondition.pk };
3266
- if (this.sortKey) {
3267
- if (!keyCondition.sk) {
3268
- throw new Error("Sort key has not been provided but the Table has a sort key");
3269
- }
3270
- primaryCondition[this.sortKey] = keyCondition.sk;
3271
- }
3272
- return primaryCondition;
2893
+ /**
2894
+ * Configure entity-specific logic for automatic timestamp generation and index updates
2895
+ */
2896
+ configureEntityLogic(config) {
2897
+ this.entityConfig = config;
3273
2898
  }
3274
2899
  /**
3275
- * Creates a new item in the table, it will fail if the item already exists.
3276
- *
3277
- * By default, this method returns the input values passed to the create operation
3278
- * upon successful creation.
3279
- *
3280
- * You can customise the return behaviour by chaining the `.returnValues()` method:
2900
+ * Forces a rebuild of one or more readonly indexes during the update operation.
3281
2901
  *
3282
- * @param item The item to create
3283
- * @returns A PutBuilder instance for chaining additional conditions and executing the create operation
2902
+ * By default, readonly indexes are not updated during entity updates to prevent
2903
+ * errors when required index attributes are missing. This method allows you to
2904
+ * override that behavior and force specific indexes to be rebuilt.
3284
2905
  *
3285
2906
  * @example
3286
- * ```ts
3287
- * // Create with default behavior (returns input values)
3288
- * const result = await table.create({
3289
- * id: 'user-123',
3290
- * name: 'John Doe',
3291
- * email: 'john@example.com'
3292
- * }).execute();
3293
- * console.log(result); // Returns the input object
3294
- *
3295
- * // Create with no return value for better performance
3296
- * await table.create(userData).returnValues('NONE').execute();
2907
+ * ```typescript
2908
+ * // Force rebuild a single readonly index
2909
+ * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
2910
+ * .forceIndexRebuild('gsi1')
2911
+ * .execute();
3297
2912
  *
3298
- * // Create and get fresh data from dynamodb using a strongly consistent read
3299
- * const freshData = await table.create(userData).returnValues('CONSISTENT').execute();
2913
+ * // Force rebuild multiple readonly indexes
2914
+ * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
2915
+ * .forceIndexRebuild(['gsi1', 'gsi2'])
2916
+ * .execute();
3300
2917
  *
3301
- * // Create and get previous values (if the item was overwritten)
3302
- * const oldData = await table.create(userData).returnValues('ALL_OLD').execute();
2918
+ * // Chain with other update operations
2919
+ * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
2920
+ * .set('lastUpdated', new Date().toISOString())
2921
+ * .forceIndexRebuild('gsi1')
2922
+ * .condition(op => op.eq('status', 'INACTIVE'))
2923
+ * .execute();
3303
2924
  * ```
2925
+ *
2926
+ * @param indexes - A single index name or array of index names to force rebuild
2927
+ * @returns The builder instance for method chaining
3304
2928
  */
3305
- create(item) {
3306
- return this.put(item).condition((op) => op.attributeNotExists(this.partitionKey)).returnValues("INPUT");
2929
+ forceIndexRebuild(indexes) {
2930
+ if (Array.isArray(indexes)) {
2931
+ this.forceRebuildIndexes = [...this.forceRebuildIndexes, ...indexes];
2932
+ } else {
2933
+ this.forceRebuildIndexes.push(indexes);
2934
+ }
2935
+ return this;
3307
2936
  }
3308
- get(keyCondition) {
3309
- const executor = async (params) => {
3310
- try {
3311
- const result = await this.dynamoClient.get({
3312
- TableName: params.tableName,
3313
- Key: this.createKeyForPrimaryIndex(keyCondition),
3314
- ProjectionExpression: params.projectionExpression,
3315
- ExpressionAttributeNames: params.expressionAttributeNames,
3316
- ConsistentRead: params.consistentRead
3317
- });
3318
- return {
3319
- item: result.Item ? result.Item : void 0
3320
- };
3321
- } catch (error) {
3322
- console.error("Error getting item:", error);
3323
- throw error;
2937
+ /**
2938
+ * Gets the list of indexes that should be force rebuilt.
2939
+ * This is used internally by entity update logic.
2940
+ *
2941
+ * @returns Array of index names to force rebuild
2942
+ */
2943
+ getForceRebuildIndexes() {
2944
+ return [...this.forceRebuildIndexes];
2945
+ }
2946
+ /**
2947
+ * Apply entity-specific update data (timestamps and index updates)
2948
+ * This is called automatically when needed
2949
+ */
2950
+ applyEntityUpdates() {
2951
+ if (!this.entityConfig || this.updateDataApplied) return;
2952
+ const timestamps = this.entityConfig.generateTimestamps();
2953
+ const updatedItem = { ...this.entityConfig.key, ...this.entityConfig.data, ...timestamps };
2954
+ const indexUpdates = this.entityConfig.buildIndexUpdates(
2955
+ this.entityConfig.key,
2956
+ updatedItem,
2957
+ this.entityConfig.table,
2958
+ this.entityConfig.indexes,
2959
+ this.forceRebuildIndexes
2960
+ );
2961
+ this.builder.set({ ...this.entityConfig.data, ...timestamps, ...indexUpdates });
2962
+ this.updateDataApplied = true;
2963
+ }
2964
+ set(valuesOrPath, value) {
2965
+ if (typeof valuesOrPath === "object") {
2966
+ this.builder.set(valuesOrPath);
2967
+ } else {
2968
+ if (value === void 0) {
2969
+ throw new Error("Value is required when setting a single path");
3324
2970
  }
3325
- };
3326
- return new GetBuilder(executor, keyCondition, this.tableName);
2971
+ this.builder.set(valuesOrPath, value);
2972
+ }
2973
+ return this;
2974
+ }
2975
+ remove(path) {
2976
+ this.builder.remove(path);
2977
+ return this;
2978
+ }
2979
+ add(path, value) {
2980
+ this.builder.add(path, value);
2981
+ return this;
2982
+ }
2983
+ deleteElementsFromSet(path, value) {
2984
+ this.builder.deleteElementsFromSet(path, value);
2985
+ return this;
2986
+ }
2987
+ condition(condition) {
2988
+ this.builder.condition(condition);
2989
+ return this;
2990
+ }
2991
+ returnValues(returnValues) {
2992
+ this.builder.returnValues(returnValues);
2993
+ return this;
2994
+ }
2995
+ toDynamoCommand() {
2996
+ return this.builder.toDynamoCommand();
2997
+ }
2998
+ withTransaction(transaction) {
2999
+ this.applyEntityUpdates();
3000
+ this.builder.withTransaction(transaction);
3001
+ }
3002
+ debug() {
3003
+ return this.builder.debug();
3004
+ }
3005
+ async execute() {
3006
+ this.updateDataApplied = false;
3007
+ this.applyEntityUpdates();
3008
+ return this.builder.execute();
3327
3009
  }
3010
+ };
3011
+ function createEntityAwareUpdateBuilder(builder, entityName) {
3012
+ return new EntityAwareUpdateBuilder(builder, entityName);
3013
+ }
3014
+
3015
+ // src/entity/ddb-indexing.ts
3016
+ var IndexBuilder = class {
3328
3017
  /**
3329
- * Updates an item in the table
3018
+ * Creates a new IndexBuilder instance
3330
3019
  *
3331
- * @param item The item to update
3332
- * @returns A PutBuilder instance for chaining conditions and executing the put operation
3020
+ * @param table - The DynamoDB table instance
3021
+ * @param indexes - The index definitions
3333
3022
  */
3334
- put(item) {
3335
- const executor = async (params) => {
3336
- try {
3337
- const result = await this.dynamoClient.put({
3338
- TableName: params.tableName,
3339
- Item: params.item,
3340
- ConditionExpression: params.conditionExpression,
3341
- ExpressionAttributeNames: params.expressionAttributeNames,
3342
- ExpressionAttributeValues: params.expressionAttributeValues,
3343
- // CONSISTENT and INPUT are not valid ReturnValues for DDB, so we set NONE as we are not interested in its
3344
- // response and will be handling these cases separately
3345
- ReturnValues: params.returnValues === "CONSISTENT" || params.returnValues === "INPUT" ? "NONE" : params.returnValues
3346
- });
3347
- if (params.returnValues === "INPUT") {
3348
- return params.item;
3349
- }
3350
- if (params.returnValues === "CONSISTENT") {
3351
- const getResult = await this.dynamoClient.get({
3352
- TableName: params.tableName,
3353
- Key: this.createKeyForPrimaryIndex({
3354
- pk: params.item[this.partitionKey],
3355
- ...this.sortKey && { sk: params.item[this.sortKey] }
3356
- }),
3357
- ConsistentRead: true
3358
- });
3359
- return getResult.Item;
3360
- }
3361
- return result.Attributes;
3362
- } catch (error) {
3363
- console.error("Error creating item:", error);
3364
- throw error;
3365
- }
3366
- };
3367
- return new PutBuilder(executor, item, this.tableName);
3023
+ constructor(table, indexes = {}) {
3024
+ this.table = table;
3025
+ this.indexes = indexes;
3368
3026
  }
3369
3027
  /**
3370
- * Creates a query builder for complex queries
3371
- * If useIndex is called on the returned QueryBuilder, it will use the GSI configuration
3028
+ * Build index attributes for item creation
3029
+ *
3030
+ * @param item - The item to generate indexes for
3031
+ * @param options - Options for building indexes
3032
+ * @returns Record of GSI attribute names to their values
3372
3033
  */
3373
- query(keyCondition) {
3374
- const pkAttributeName = this.partitionKey;
3375
- const skAttributeName = this.sortKey;
3376
- let keyConditionExpression = eq(pkAttributeName, keyCondition.pk);
3377
- if (keyCondition.sk) {
3378
- if (!skAttributeName) {
3379
- throw new Error("Sort key is not defined for Index");
3034
+ buildForCreate(item, options = {}) {
3035
+ const attributes = {};
3036
+ for (const [indexName, indexDef] of Object.entries(this.indexes)) {
3037
+ if (options.excludeReadOnly && indexDef.isReadOnly) {
3038
+ continue;
3380
3039
  }
3381
- const keyConditionOperator = {
3382
- eq: (value) => eq(skAttributeName, value),
3383
- lt: (value) => lt(skAttributeName, value),
3384
- lte: (value) => lte(skAttributeName, value),
3385
- gt: (value) => gt(skAttributeName, value),
3386
- gte: (value) => gte(skAttributeName, value),
3387
- between: (lower, upper) => between(skAttributeName, lower, upper),
3388
- beginsWith: (value) => beginsWith(skAttributeName, value),
3389
- and: (...conditions) => and(...conditions)
3390
- };
3391
- const skCondition = keyCondition.sk(keyConditionOperator);
3392
- keyConditionExpression = and(eq(pkAttributeName, keyCondition.pk), skCondition);
3393
- }
3394
- const executor = async (originalKeyCondition, options) => {
3395
- let finalKeyCondition = originalKeyCondition;
3396
- if (options.indexName) {
3397
- const gsiName = String(options.indexName);
3398
- const gsi = this.gsis[gsiName];
3399
- if (!gsi) {
3400
- throw new Error(`GSI with name "${gsiName}" does not exist on table "${this.tableName}"`);
3401
- }
3402
- const gsiPkAttributeName = gsi.partitionKey;
3403
- const gsiSkAttributeName = gsi.sortKey;
3404
- let pkValue;
3405
- let skValue;
3406
- let extractedSkCondition;
3407
- if (originalKeyCondition.type === "eq") {
3408
- pkValue = originalKeyCondition.value;
3409
- } else if (originalKeyCondition.type === "and" && originalKeyCondition.conditions) {
3410
- const pkCondition = originalKeyCondition.conditions.find(
3411
- (c) => c.type === "eq" && c.attr === pkAttributeName
3412
- );
3413
- if (pkCondition && pkCondition.type === "eq") {
3414
- pkValue = pkCondition.value;
3415
- }
3416
- const skConditions = originalKeyCondition.conditions.filter((c) => c.attr === skAttributeName);
3417
- if (skConditions.length > 0) {
3418
- if (skConditions.length === 1) {
3419
- extractedSkCondition = skConditions[0];
3420
- if (extractedSkCondition && extractedSkCondition.type === "eq") {
3421
- skValue = extractedSkCondition.value;
3422
- }
3423
- } else if (skConditions.length > 1) {
3424
- extractedSkCondition = and(...skConditions);
3425
- }
3426
- }
3427
- }
3428
- if (!pkValue) {
3429
- throw new Error("Could not extract partition key value from key condition");
3430
- }
3431
- let gsiKeyCondition = eq(gsiPkAttributeName, pkValue);
3432
- if (skValue && gsiSkAttributeName) {
3433
- gsiKeyCondition = and(gsiKeyCondition, eq(gsiSkAttributeName, skValue));
3434
- } else if (extractedSkCondition && gsiSkAttributeName) {
3435
- if (extractedSkCondition.attr === skAttributeName) {
3436
- const updatedSkCondition = {
3437
- ...extractedSkCondition,
3438
- attr: gsiSkAttributeName
3439
- };
3440
- gsiKeyCondition = and(gsiKeyCondition, updatedSkCondition);
3441
- } else {
3442
- gsiKeyCondition = and(gsiKeyCondition, extractedSkCondition);
3443
- }
3444
- }
3445
- finalKeyCondition = gsiKeyCondition;
3040
+ const key = indexDef.generateKey(item);
3041
+ const gsiConfig = this.table.gsis[indexName];
3042
+ if (!gsiConfig) {
3043
+ throw new Error(`GSI configuration not found for index: ${indexName}`);
3446
3044
  }
3447
- const expressionParams = {
3448
- expressionAttributeNames: {},
3449
- expressionAttributeValues: {},
3450
- valueCounter: { count: 0 }
3451
- };
3452
- const keyConditionExpression2 = buildExpression(finalKeyCondition, expressionParams);
3453
- let filterExpression;
3454
- if (options.filter) {
3455
- filterExpression = buildExpression(options.filter, expressionParams);
3045
+ if (key.pk) {
3046
+ attributes[gsiConfig.partitionKey] = key.pk;
3456
3047
  }
3457
- const projectionExpression = options.projection?.map((p) => generateAttributeName(expressionParams, p)).join(", ");
3458
- const { expressionAttributeNames, expressionAttributeValues } = expressionParams;
3459
- const { indexName, limit, consistentRead, scanIndexForward, lastEvaluatedKey } = options;
3460
- const params = {
3461
- TableName: this.tableName,
3462
- KeyConditionExpression: keyConditionExpression2,
3463
- FilterExpression: filterExpression,
3464
- ExpressionAttributeNames: expressionAttributeNames,
3465
- ExpressionAttributeValues: expressionAttributeValues,
3466
- IndexName: indexName,
3467
- Limit: limit,
3468
- ConsistentRead: consistentRead,
3469
- ScanIndexForward: scanIndexForward,
3470
- ProjectionExpression: projectionExpression,
3471
- ExclusiveStartKey: lastEvaluatedKey
3472
- };
3473
- try {
3474
- const result = await this.dynamoClient.query(params);
3475
- return {
3476
- items: result.Items,
3477
- lastEvaluatedKey: result.LastEvaluatedKey
3478
- };
3479
- } catch (error) {
3480
- console.log(debugCommand(params));
3481
- console.error("Error querying items:", error);
3482
- throw error;
3048
+ if (key.sk && gsiConfig.sortKey) {
3049
+ attributes[gsiConfig.sortKey] = key.sk;
3483
3050
  }
3484
- };
3485
- return new QueryBuilder(executor, keyConditionExpression);
3051
+ }
3052
+ return attributes;
3486
3053
  }
3487
3054
  /**
3488
- * Creates a scan builder for scanning the entire table
3489
- * Use this when you need to:
3490
- * - Process all items in a table
3491
- * - Apply filters to a large dataset
3492
- * - Use a GSI for scanning
3055
+ * Build index attributes for item updates
3493
3056
  *
3494
- * @returns A ScanBuilder instance for chaining operations
3057
+ * @param currentData - The current data before update
3058
+ * @param updates - The update data
3059
+ * @param options - Options for building indexes
3060
+ * @returns Record of GSI attribute names to their updated values
3495
3061
  */
3496
- scan() {
3497
- const executor = async (options) => {
3498
- const expressionParams = {
3499
- expressionAttributeNames: {},
3500
- expressionAttributeValues: {},
3501
- valueCounter: { count: 0 }
3502
- };
3503
- let filterExpression;
3504
- if (options.filter) {
3505
- filterExpression = buildExpression(options.filter, expressionParams);
3062
+ buildForUpdate(currentData, updates, options = {}) {
3063
+ const attributes = {};
3064
+ const updatedItem = { ...currentData, ...updates };
3065
+ if (options.forceRebuildIndexes && options.forceRebuildIndexes.length > 0) {
3066
+ const invalidIndexes = options.forceRebuildIndexes.filter((indexName) => !this.indexes[indexName]);
3067
+ if (invalidIndexes.length > 0) {
3068
+ throw new Error(
3069
+ `Cannot force rebuild unknown indexes: ${invalidIndexes.join(", ")}. Available indexes: ${Object.keys(this.indexes).join(", ")}`
3070
+ );
3506
3071
  }
3507
- const projectionExpression = options.projection?.map((p) => generateAttributeName(expressionParams, p)).join(", ");
3508
- const { expressionAttributeNames, expressionAttributeValues } = expressionParams;
3509
- const { indexName, limit, consistentRead, lastEvaluatedKey } = options;
3510
- const params = {
3511
- TableName: this.tableName,
3512
- FilterExpression: filterExpression,
3513
- ExpressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 ? expressionAttributeNames : void 0,
3514
- ExpressionAttributeValues: Object.keys(expressionAttributeValues).length > 0 ? expressionAttributeValues : void 0,
3515
- IndexName: indexName,
3516
- Limit: limit,
3517
- ConsistentRead: consistentRead,
3518
- ProjectionExpression: projectionExpression,
3519
- ExclusiveStartKey: lastEvaluatedKey
3520
- };
3521
- try {
3522
- const result = await this.dynamoClient.scan(params);
3523
- return {
3524
- items: result.Items,
3525
- lastEvaluatedKey: result.LastEvaluatedKey
3526
- };
3527
- } catch (error) {
3528
- console.log(debugCommand(params));
3529
- console.error("Error scanning items:", error);
3530
- throw error;
3072
+ }
3073
+ for (const [indexName, indexDef] of Object.entries(this.indexes)) {
3074
+ const isForced = options.forceRebuildIndexes?.includes(indexName);
3075
+ if (indexDef.isReadOnly && !isForced) {
3076
+ continue;
3531
3077
  }
3532
- };
3533
- return new ScanBuilder(executor);
3534
- }
3535
- delete(keyCondition) {
3536
- const executor = async (params) => {
3078
+ if (!isForced) {
3079
+ let shouldUpdateIndex = false;
3080
+ try {
3081
+ const currentKey = indexDef.generateKey(currentData);
3082
+ const updatedKey = indexDef.generateKey(updatedItem);
3083
+ if (currentKey.pk !== updatedKey.pk || currentKey.sk !== updatedKey.sk) {
3084
+ shouldUpdateIndex = true;
3085
+ }
3086
+ } catch {
3087
+ shouldUpdateIndex = true;
3088
+ }
3089
+ if (!shouldUpdateIndex) {
3090
+ continue;
3091
+ }
3092
+ }
3093
+ let key;
3537
3094
  try {
3538
- const result = await this.dynamoClient.delete({
3539
- TableName: params.tableName,
3540
- Key: this.createKeyForPrimaryIndex(keyCondition),
3541
- ConditionExpression: params.conditionExpression,
3542
- ExpressionAttributeNames: params.expressionAttributeNames,
3543
- ExpressionAttributeValues: params.expressionAttributeValues,
3544
- ReturnValues: params.returnValues
3545
- });
3546
- return {
3547
- item: result.Attributes
3548
- };
3095
+ key = indexDef.generateKey(updatedItem);
3549
3096
  } catch (error) {
3550
- console.error("Error deleting item:", error);
3097
+ if (error instanceof Error) {
3098
+ throw new Error(`Missing attributes: ${error.message}`);
3099
+ }
3551
3100
  throw error;
3552
3101
  }
3553
- };
3554
- return new DeleteBuilder(executor, this.tableName, keyCondition);
3102
+ if (this.hasUndefinedValues(key)) {
3103
+ throw new Error(
3104
+ `Missing attributes: Cannot update entity: insufficient data to regenerate index "${indexName}". All attributes required by the index must be provided in the update operation, or the index must be marked as readOnly.`
3105
+ );
3106
+ }
3107
+ const gsiConfig = this.table.gsis[indexName];
3108
+ if (!gsiConfig) {
3109
+ throw new Error(`GSI configuration not found for index: ${indexName}`);
3110
+ }
3111
+ if (key.pk) {
3112
+ attributes[gsiConfig.partitionKey] = key.pk;
3113
+ }
3114
+ if (key.sk && gsiConfig.sortKey) {
3115
+ attributes[gsiConfig.sortKey] = key.sk;
3116
+ }
3117
+ }
3118
+ return attributes;
3555
3119
  }
3556
3120
  /**
3557
- * Updates an item in the table
3121
+ * Check if a key has undefined values
3558
3122
  *
3559
- * @param keyCondition The primary key of the item to update
3560
- * @returns An UpdateBuilder instance for chaining update operations and conditions
3123
+ * @param key - The index key to check
3124
+ * @returns True if the key contains undefined values, false otherwise
3561
3125
  */
3562
- update(keyCondition) {
3563
- const executor = async (params) => {
3564
- try {
3565
- const result = await this.dynamoClient.update({
3566
- TableName: params.tableName,
3567
- Key: this.createKeyForPrimaryIndex(keyCondition),
3568
- UpdateExpression: params.updateExpression,
3569
- ConditionExpression: params.conditionExpression,
3570
- ExpressionAttributeNames: params.expressionAttributeNames,
3571
- ExpressionAttributeValues: params.expressionAttributeValues,
3572
- ReturnValues: params.returnValues
3573
- });
3574
- return {
3575
- item: result.Attributes
3576
- };
3577
- } catch (error) {
3578
- console.error("Error updating item:", error);
3579
- throw error;
3580
- }
3581
- };
3582
- return new UpdateBuilder(executor, this.tableName, keyCondition);
3126
+ hasUndefinedValues(key) {
3127
+ return (key.pk?.includes("undefined") ?? false) || (key.sk?.includes("undefined") ?? false);
3583
3128
  }
3584
- /**
3585
- * Creates a transaction builder for performing multiple operations atomically
3586
- */
3587
- transactionBuilder() {
3588
- const executor = async (params) => {
3589
- await this.dynamoClient.transactWrite(params);
3590
- };
3591
- return new TransactionBuilder(executor, {
3592
- partitionKey: this.partitionKey,
3593
- sortKey: this.sortKey
3594
- });
3129
+ };
3130
+
3131
+ // src/entity/index-utils.ts
3132
+ function buildIndexes(dataForKeyGeneration, table, indexes, excludeReadOnly = false) {
3133
+ if (!indexes) {
3134
+ return {};
3595
3135
  }
3596
- /**
3597
- * Creates a batch builder for performing multiple operations efficiently with optional type inference
3598
- *
3599
- * @example Basic Usage
3600
- * ```typescript
3601
- * const batch = table.batchBuilder();
3602
- *
3603
- * // Add operations
3604
- * userRepo.create(newUser).withBatch(batch);
3605
- * orderRepo.get({ id: 'order-1' }).withBatch(batch);
3606
- *
3607
- * // Execute operations
3608
- * const result = await batch.execute();
3609
- * ```
3610
- *
3611
- * @example Typed Usage
3612
- * ```typescript
3613
- * // Define entity types for the batch
3614
- * const batch = table.batchBuilder<{
3615
- * User: UserEntity;
3616
- * Order: OrderEntity;
3617
- * Product: ProductEntity;
3618
- * }>();
3619
- *
3620
- * // Add operations with type information
3621
- * userRepo.create(newUser).withBatch(batch, 'User');
3622
- * orderRepo.get({ id: 'order-1' }).withBatch(batch, 'Order');
3623
- * productRepo.delete({ id: 'old-product' }).withBatch(batch, 'Product');
3624
- *
3625
- * // Execute and get typed results
3626
- * const result = await batch.execute();
3627
- * const users: UserEntity[] = result.reads.itemsByType.User;
3628
- * const orders: OrderEntity[] = result.reads.itemsByType.Order;
3629
- * ```
3630
- */
3631
- batchBuilder() {
3632
- const batchWriteExecutor = async (operations) => {
3633
- return this.batchWrite(operations);
3634
- };
3635
- const batchGetExecutor = async (keys) => {
3636
- return this.batchGet(keys);
3637
- };
3638
- return new BatchBuilder(batchWriteExecutor, batchGetExecutor, {
3639
- partitionKey: this.partitionKey,
3640
- sortKey: this.sortKey
3641
- });
3136
+ const indexBuilder = new IndexBuilder(table, indexes);
3137
+ return indexBuilder.buildForCreate(dataForKeyGeneration, { excludeReadOnly });
3138
+ }
3139
+ function buildIndexUpdates(currentData, updates, table, indexes, forceRebuildIndexes) {
3140
+ if (!indexes) {
3141
+ return {};
3642
3142
  }
3643
- /**
3644
- * Executes a transaction using a callback function
3645
- *
3646
- * @param callback A function that receives a transaction context and performs operations on it
3647
- * @param options Optional transaction options
3648
- * @returns A promise that resolves when the transaction is complete
3649
- */
3650
- async transaction(callback, options) {
3651
- const transactionExecutor = async (params) => {
3652
- await this.dynamoClient.transactWrite(params);
3143
+ const indexBuilder = new IndexBuilder(table, indexes);
3144
+ return indexBuilder.buildForUpdate(currentData, updates, { forceRebuildIndexes });
3145
+ }
3146
+
3147
+ // src/entity/entity.ts
3148
+ function defineEntity(config) {
3149
+ const entityTypeAttributeName = config.settings?.entityTypeAttributeName ?? "entityType";
3150
+ const buildIndexes2 = (dataForKeyGeneration, table, excludeReadOnly = false) => {
3151
+ return buildIndexes(dataForKeyGeneration, table, config.indexes, excludeReadOnly);
3152
+ };
3153
+ const wrapMethodWithPreparation = (originalMethod, prepareFn, context) => {
3154
+ const wrappedMethod = (...args) => {
3155
+ prepareFn();
3156
+ return originalMethod.call(context, ...args);
3653
3157
  };
3654
- const transaction = new TransactionBuilder(transactionExecutor, {
3655
- partitionKey: this.partitionKey,
3656
- sortKey: this.sortKey
3657
- });
3658
- if (options) {
3659
- transaction.withOptions(options);
3660
- }
3661
- const result = await callback(transaction);
3662
- await transaction.execute();
3663
- return result;
3664
- }
3665
- /**
3666
- * Creates a condition check operation for use in transactions
3667
- *
3668
- * This is useful for when you require a transaction to succeed only when a specific condition is met on a
3669
- * a record within the database that you are not directly updating.
3670
- *
3671
- * For example, you are updating a record and you want to ensure that another record exists and/or has a specific value before proceeding.
3672
- */
3673
- conditionCheck(keyCondition) {
3674
- return new ConditionCheckBuilder(this.tableName, keyCondition);
3675
- }
3676
- /**
3677
- * Performs a batch get operation to retrieve multiple items at once
3678
- *
3679
- * @param keys Array of primary keys to retrieve
3680
- * @returns A promise that resolves to the retrieved items
3681
- */
3682
- async batchGet(keys) {
3683
- const allItems = [];
3684
- const allUnprocessedKeys = [];
3685
- for (const chunk of chunkArray(keys, DDB_BATCH_GET_LIMIT)) {
3686
- const formattedKeys = chunk.map((key) => ({
3687
- [this.partitionKey]: key.pk,
3688
- ...this.sortKey ? { [this.sortKey]: key.sk } : {}
3689
- }));
3690
- const params = {
3691
- RequestItems: {
3692
- [this.tableName]: {
3693
- Keys: formattedKeys
3694
- }
3695
- }
3696
- };
3697
- try {
3698
- const result = await this.dynamoClient.batchGet(params);
3699
- if (result.Responses?.[this.tableName]) {
3700
- allItems.push(...result.Responses[this.tableName]);
3701
- }
3702
- const unprocessedKeysArray = result.UnprocessedKeys?.[this.tableName]?.Keys || [];
3703
- const unprocessedKeys = unprocessedKeysArray.map((key) => ({
3704
- pk: key[this.partitionKey],
3705
- sk: this.sortKey ? key[this.sortKey] : void 0
3706
- }));
3707
- if (unprocessedKeys.length > 0) {
3708
- allUnprocessedKeys.push(...unprocessedKeys);
3158
+ Object.setPrototypeOf(wrappedMethod, originalMethod);
3159
+ const propertyNames = Object.getOwnPropertyNames(originalMethod);
3160
+ for (let i = 0; i < propertyNames.length; i++) {
3161
+ const prop = propertyNames[i];
3162
+ if (prop !== "length" && prop !== "name" && prop !== "prototype") {
3163
+ const descriptor = Object.getOwnPropertyDescriptor(originalMethod, prop);
3164
+ if (descriptor && descriptor.writable !== false && !descriptor.get) {
3165
+ wrappedMethod[prop] = originalMethod[prop];
3709
3166
  }
3710
- } catch (error) {
3711
- console.error("Error in batch get operation:", error);
3712
- throw error;
3713
3167
  }
3714
3168
  }
3715
- return {
3716
- items: allItems,
3717
- unprocessedKeys: allUnprocessedKeys
3718
- };
3719
- }
3720
- /**
3721
- * Performs a batch write operation to put or delete multiple items at once
3722
- *
3723
- * @param operations Array of put or delete operations
3724
- * @returns A promise that resolves to any unprocessed operations
3725
- */
3726
- async batchWrite(operations) {
3727
- const allUnprocessedItems = [];
3728
- for (const chunk of chunkArray(operations, DDB_BATCH_WRITE_LIMIT)) {
3729
- const writeRequests = chunk.map((operation) => {
3730
- if (operation.type === "put") {
3731
- return {
3732
- PutRequest: {
3733
- Item: operation.item
3169
+ return wrappedMethod;
3170
+ };
3171
+ const generateTimestamps = (timestampsToGenerate, data) => {
3172
+ if (!config.settings?.timestamps) return {};
3173
+ const timestamps = {};
3174
+ const now = /* @__PURE__ */ new Date();
3175
+ const unixTime = Math.floor(Date.now() / 1e3);
3176
+ const { createdAt, updatedAt } = config.settings.timestamps;
3177
+ if (createdAt && timestampsToGenerate.includes("createdAt") && !data.createdAt) {
3178
+ const name = createdAt.attributeName ?? "createdAt";
3179
+ timestamps[name] = createdAt.format === "UNIX" ? unixTime : now.toISOString();
3180
+ }
3181
+ if (updatedAt && timestampsToGenerate.includes("updatedAt") && !data.updatedAt) {
3182
+ const name = updatedAt.attributeName ?? "updatedAt";
3183
+ timestamps[name] = updatedAt.format === "UNIX" ? unixTime : now.toISOString();
3184
+ }
3185
+ return timestamps;
3186
+ };
3187
+ return {
3188
+ name: config.name,
3189
+ createRepository: (table) => {
3190
+ const repository = {
3191
+ create: (data) => {
3192
+ const builder = table.create({});
3193
+ const prepareValidatedItemAsync = async () => {
3194
+ const validatedData = await config.schema["~standard"].validate(data);
3195
+ if ("issues" in validatedData && validatedData.issues) {
3196
+ throw new Error(`Validation failed: ${validatedData.issues.map((i) => i.message).join(", ")}`);
3734
3197
  }
3198
+ const dataForKeyGeneration = {
3199
+ ...validatedData.value,
3200
+ ...generateTimestamps(["createdAt", "updatedAt"], validatedData.value)
3201
+ };
3202
+ const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
3203
+ const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
3204
+ const validatedItem = {
3205
+ ...dataForKeyGeneration,
3206
+ [entityTypeAttributeName]: config.name,
3207
+ [table.partitionKey]: primaryKey.pk,
3208
+ ...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
3209
+ ...indexes
3210
+ };
3211
+ Object.assign(builder, { item: validatedItem });
3212
+ return validatedItem;
3735
3213
  };
3736
- }
3737
- return {
3738
- DeleteRequest: {
3739
- Key: this.createKeyForPrimaryIndex(operation.key)
3740
- }
3741
- };
3742
- });
3743
- const params = {
3744
- RequestItems: {
3745
- [this.tableName]: writeRequests
3746
- }
3747
- };
3748
- try {
3749
- const result = await this.dynamoClient.batchWrite(params);
3750
- const unprocessedRequestsArray = result.UnprocessedItems?.[this.tableName] || [];
3751
- if (unprocessedRequestsArray.length > 0) {
3752
- const unprocessedItems = unprocessedRequestsArray.map((request) => {
3753
- if (request?.PutRequest?.Item) {
3754
- return {
3755
- type: "put",
3756
- item: request.PutRequest.Item
3757
- };
3214
+ const prepareValidatedItemSync = () => {
3215
+ const validationResult = config.schema["~standard"].validate(data);
3216
+ if (validationResult instanceof Promise) {
3217
+ throw new Error(
3218
+ "Async validation is not supported in withBatch or withTransaction. The schema must support synchronous validation for compatibility."
3219
+ );
3758
3220
  }
3759
- if (request?.DeleteRequest?.Key) {
3760
- return {
3761
- type: "delete",
3762
- key: {
3763
- pk: request.DeleteRequest.Key[this.partitionKey],
3764
- sk: this.sortKey ? request.DeleteRequest.Key[this.sortKey] : void 0
3765
- }
3766
- };
3221
+ if ("issues" in validationResult && validationResult.issues) {
3222
+ throw new Error(`Validation failed: ${validationResult.issues.map((i) => i.message).join(", ")}`);
3767
3223
  }
3768
- throw new Error("Invalid unprocessed item format returned from DynamoDB");
3769
- });
3770
- allUnprocessedItems.push(...unprocessedItems);
3224
+ const dataForKeyGeneration = {
3225
+ ...validationResult.value,
3226
+ ...generateTimestamps(["createdAt", "updatedAt"], validationResult.value)
3227
+ };
3228
+ const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
3229
+ const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
3230
+ const validatedItem = {
3231
+ ...dataForKeyGeneration,
3232
+ [entityTypeAttributeName]: config.name,
3233
+ [table.partitionKey]: primaryKey.pk,
3234
+ ...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
3235
+ ...indexes
3236
+ };
3237
+ Object.assign(builder, { item: validatedItem });
3238
+ return validatedItem;
3239
+ };
3240
+ const originalExecute = builder.execute;
3241
+ builder.execute = async () => {
3242
+ await prepareValidatedItemAsync();
3243
+ return await originalExecute.call(builder);
3244
+ };
3245
+ const originalWithTransaction = builder.withTransaction;
3246
+ if (originalWithTransaction) {
3247
+ builder.withTransaction = wrapMethodWithPreparation(
3248
+ originalWithTransaction,
3249
+ prepareValidatedItemSync,
3250
+ builder
3251
+ );
3252
+ }
3253
+ const originalWithBatch = builder.withBatch;
3254
+ if (originalWithBatch) {
3255
+ builder.withBatch = wrapMethodWithPreparation(originalWithBatch, prepareValidatedItemSync, builder);
3256
+ }
3257
+ return createEntityAwarePutBuilder(builder, config.name);
3258
+ },
3259
+ upsert: (data) => {
3260
+ const builder = table.put({});
3261
+ const prepareValidatedItemAsync = async () => {
3262
+ const validatedData = await config.schema["~standard"].validate(data);
3263
+ if ("issues" in validatedData && validatedData.issues) {
3264
+ throw new Error(`Validation failed: ${validatedData.issues.map((i) => i.message).join(", ")}`);
3265
+ }
3266
+ const dataForKeyGeneration = {
3267
+ ...validatedData.value,
3268
+ ...generateTimestamps(["createdAt", "updatedAt"], validatedData.value)
3269
+ };
3270
+ const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
3271
+ const indexes = buildIndexes2(dataForKeyGeneration, table, false);
3272
+ const validatedItem = {
3273
+ [table.partitionKey]: primaryKey.pk,
3274
+ ...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
3275
+ ...dataForKeyGeneration,
3276
+ [entityTypeAttributeName]: config.name,
3277
+ ...indexes
3278
+ };
3279
+ Object.assign(builder, { item: validatedItem });
3280
+ return validatedItem;
3281
+ };
3282
+ const prepareValidatedItemSync = () => {
3283
+ const validationResult = config.schema["~standard"].validate(data);
3284
+ if (validationResult instanceof Promise) {
3285
+ throw new Error(
3286
+ "Async validation is not supported in withTransaction or withBatch. Use execute() instead."
3287
+ );
3288
+ }
3289
+ if ("issues" in validationResult && validationResult.issues) {
3290
+ throw new Error(`Validation failed: ${validationResult.issues.map((i) => i.message).join(", ")}`);
3291
+ }
3292
+ const dataForKeyGeneration = {
3293
+ ...validationResult.value,
3294
+ ...generateTimestamps(["createdAt", "updatedAt"], validationResult.value)
3295
+ };
3296
+ const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
3297
+ const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
3298
+ const validatedItem = {
3299
+ [table.partitionKey]: primaryKey.pk,
3300
+ ...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
3301
+ ...dataForKeyGeneration,
3302
+ [entityTypeAttributeName]: config.name,
3303
+ ...indexes
3304
+ };
3305
+ Object.assign(builder, { item: validatedItem });
3306
+ return validatedItem;
3307
+ };
3308
+ const originalExecute = builder.execute;
3309
+ builder.execute = async () => {
3310
+ await prepareValidatedItemAsync();
3311
+ const result = await originalExecute.call(builder);
3312
+ if (!result) {
3313
+ throw new Error("Failed to upsert item");
3314
+ }
3315
+ return result;
3316
+ };
3317
+ const originalWithTransaction = builder.withTransaction;
3318
+ if (originalWithTransaction) {
3319
+ builder.withTransaction = wrapMethodWithPreparation(
3320
+ originalWithTransaction,
3321
+ prepareValidatedItemSync,
3322
+ builder
3323
+ );
3324
+ }
3325
+ const originalWithBatch = builder.withBatch;
3326
+ if (originalWithBatch) {
3327
+ builder.withBatch = wrapMethodWithPreparation(originalWithBatch, prepareValidatedItemSync, builder);
3328
+ }
3329
+ return createEntityAwarePutBuilder(builder, config.name);
3330
+ },
3331
+ get: (key) => {
3332
+ return createEntityAwareGetBuilder(table.get(config.primaryKey.generateKey(key)), config.name);
3333
+ },
3334
+ update: (key, data) => {
3335
+ const primaryKeyObj = config.primaryKey.generateKey(key);
3336
+ const builder = table.update(primaryKeyObj);
3337
+ builder.condition(eq(entityTypeAttributeName, config.name));
3338
+ const entityAwareBuilder = createEntityAwareUpdateBuilder(builder, config.name);
3339
+ entityAwareBuilder.configureEntityLogic({
3340
+ data,
3341
+ key,
3342
+ table,
3343
+ indexes: config.indexes,
3344
+ generateTimestamps: () => generateTimestamps(["updatedAt"], data),
3345
+ buildIndexUpdates
3346
+ });
3347
+ return entityAwareBuilder;
3348
+ },
3349
+ delete: (key) => {
3350
+ const builder = table.delete(config.primaryKey.generateKey(key));
3351
+ builder.condition(eq(entityTypeAttributeName, config.name));
3352
+ return createEntityAwareDeleteBuilder(builder, config.name);
3353
+ },
3354
+ query: Object.entries(config.queries || {}).reduce((acc, [key, inputCallback]) => {
3355
+ acc[key] = (input) => {
3356
+ const queryEntity = {
3357
+ scan: repository.scan,
3358
+ get: (key2) => createEntityAwareGetBuilder(table.get(key2), config.name),
3359
+ query: (keyCondition) => {
3360
+ return table.query(keyCondition);
3361
+ }
3362
+ };
3363
+ const queryBuilderCallback = inputCallback(input);
3364
+ const builder = queryBuilderCallback(queryEntity);
3365
+ if (builder && typeof builder === "object" && "filter" in builder && typeof builder.filter === "function") {
3366
+ builder.filter(eq(entityTypeAttributeName, config.name));
3367
+ }
3368
+ if (builder && typeof builder === "object" && "execute" in builder) {
3369
+ const originalExecute = builder.execute;
3370
+ builder.execute = async () => {
3371
+ const queryFn = config.queries[key];
3372
+ if (queryFn && typeof queryFn === "function") {
3373
+ const schema = queryFn.schema;
3374
+ if (schema?.["~standard"]?.validate && typeof schema["~standard"].validate === "function") {
3375
+ const validationResult = schema["~standard"].validate(input);
3376
+ if ("issues" in validationResult && validationResult.issues) {
3377
+ throw new Error(
3378
+ `Validation failed: ${validationResult.issues.map((issue) => issue.message).join(", ")}`
3379
+ );
3380
+ }
3381
+ }
3382
+ }
3383
+ const result = await originalExecute.call(builder);
3384
+ if (!result) {
3385
+ throw new Error("Failed to execute query");
3386
+ }
3387
+ return result;
3388
+ };
3389
+ }
3390
+ return builder;
3391
+ };
3392
+ return acc;
3393
+ }, {}),
3394
+ scan: () => {
3395
+ const builder = table.scan();
3396
+ builder.filter(eq(entityTypeAttributeName, config.name));
3397
+ return builder;
3771
3398
  }
3772
- } catch (error) {
3773
- console.error("Error in batch write operation:", error);
3774
- throw error;
3399
+ };
3400
+ return repository;
3401
+ }
3402
+ };
3403
+ }
3404
+ function createQueries() {
3405
+ return {
3406
+ input: (schema) => ({
3407
+ query: (handler) => {
3408
+ const queryFn = (input) => (entity) => handler({ input, entity });
3409
+ queryFn.schema = schema;
3410
+ return queryFn;
3411
+ }
3412
+ })
3413
+ };
3414
+ }
3415
+ function createIndex() {
3416
+ return {
3417
+ input: (schema) => {
3418
+ const createIndexBuilder = (isReadOnly = false) => ({
3419
+ partitionKey: (pkFn) => ({
3420
+ sortKey: (skFn) => {
3421
+ const index = {
3422
+ name: "custom",
3423
+ partitionKey: "pk",
3424
+ sortKey: "sk",
3425
+ isReadOnly,
3426
+ generateKey: (item) => {
3427
+ const data = schema["~standard"].validate(item);
3428
+ if ("issues" in data && data.issues) {
3429
+ throw new Error(`Index validation failed: ${data.issues.map((i) => i.message).join(", ")}`);
3430
+ }
3431
+ const validData = "value" in data ? data.value : item;
3432
+ return { pk: pkFn(validData), sk: skFn(validData) };
3433
+ }
3434
+ };
3435
+ return Object.assign(index, {
3436
+ readOnly: (value = false) => ({
3437
+ ...index,
3438
+ isReadOnly: value
3439
+ })
3440
+ });
3441
+ },
3442
+ withoutSortKey: () => {
3443
+ const index = {
3444
+ name: "custom",
3445
+ partitionKey: "pk",
3446
+ isReadOnly,
3447
+ generateKey: (item) => {
3448
+ const data = schema["~standard"].validate(item);
3449
+ if ("issues" in data && data.issues) {
3450
+ throw new Error(`Index validation failed: ${data.issues.map((i) => i.message).join(", ")}`);
3451
+ }
3452
+ const validData = "value" in data ? data.value : item;
3453
+ return { pk: pkFn(validData) };
3454
+ }
3455
+ };
3456
+ return Object.assign(index, {
3457
+ readOnly: (value = true) => ({
3458
+ ...index,
3459
+ isReadOnly: value
3460
+ })
3461
+ });
3462
+ }
3463
+ }),
3464
+ readOnly: (value = true) => createIndexBuilder(value)
3465
+ });
3466
+ return createIndexBuilder(false);
3467
+ }
3468
+ };
3469
+ }
3470
+
3471
+ // src/builders/condition-check-builder.ts
3472
+ var ConditionCheckBuilder = class {
3473
+ key;
3474
+ tableName;
3475
+ conditionExpression;
3476
+ constructor(tableName, key) {
3477
+ this.tableName = tableName;
3478
+ this.key = key;
3479
+ }
3480
+ /**
3481
+ * Adds a condition that must be satisfied for the check to succeed.
3482
+ *
3483
+ * @example
3484
+ * ```typescript
3485
+ * // Check dinosaur health and behavior
3486
+ * builder.condition(op =>
3487
+ * op.and([
3488
+ * op.gt('stats.health', 50),
3489
+ * op.not(op.eq('status', 'SEDATED')),
3490
+ * op.lt('aggressionLevel', 8)
3491
+ * ])
3492
+ * );
3493
+ *
3494
+ * // Verify habitat conditions
3495
+ * builder.condition(op =>
3496
+ * op.and([
3497
+ * op.eq('powerStatus', 'ONLINE'),
3498
+ * op.between('temperature', 20, 30),
3499
+ * op.attributeExists('lastMaintenance')
3500
+ * ])
3501
+ * );
3502
+ *
3503
+ * // Check breeding conditions
3504
+ * builder.condition(op =>
3505
+ * op.and([
3506
+ * op.eq('species', 'VELOCIRAPTOR'),
3507
+ * op.gte('age', 3),
3508
+ * op.eq('geneticPurity', 100)
3509
+ * ])
3510
+ * );
3511
+ * ```
3512
+ *
3513
+ * @param condition - Either a Condition DynamoItem or a callback function that builds the condition
3514
+ * @returns The builder instance for method chaining
3515
+ */
3516
+ condition(condition) {
3517
+ if (typeof condition === "function") {
3518
+ const conditionOperator = {
3519
+ eq,
3520
+ ne,
3521
+ lt,
3522
+ lte,
3523
+ gt,
3524
+ gte,
3525
+ between,
3526
+ inArray,
3527
+ beginsWith,
3528
+ contains,
3529
+ attributeExists,
3530
+ attributeNotExists,
3531
+ and,
3532
+ or,
3533
+ not
3534
+ };
3535
+ this.conditionExpression = condition(conditionOperator);
3536
+ } else {
3537
+ this.conditionExpression = condition;
3538
+ }
3539
+ return this;
3540
+ }
3541
+ /**
3542
+ * Generates the DynamoDB command parameters for direct execution.
3543
+ * Use this method when you want to:
3544
+ * - Execute the condition check as a standalone operation
3545
+ * - Get the raw DynamoDB command for custom execution
3546
+ * - Inspect the generated command parameters
3547
+ *
3548
+ * @example
3549
+ * ```ts
3550
+ * const command = new ConditionCheckBuilder('myTable', { id: '123' })
3551
+ * .condition(op => op.attributeExists('status'))
3552
+ * .toDynamoCommand();
3553
+ * // Use command with DynamoDB client
3554
+ * ```
3555
+ *
3556
+ * @throws {Error} If no condition has been set
3557
+ * @returns The DynamoDB command parameters
3558
+ */
3559
+ toDynamoCommand() {
3560
+ if (!this.conditionExpression) {
3561
+ throw new Error("Condition is required for condition check operations");
3562
+ }
3563
+ const { expression, names, values } = prepareExpressionParams(this.conditionExpression);
3564
+ if (!expression) {
3565
+ throw new Error("Failed to generate condition expression");
3566
+ }
3567
+ return {
3568
+ tableName: this.tableName,
3569
+ key: this.key,
3570
+ conditionExpression: expression,
3571
+ expressionAttributeNames: names,
3572
+ expressionAttributeValues: values
3573
+ };
3574
+ }
3575
+ /**
3576
+ * Adds this condition check operation to a transaction.
3577
+ *
3578
+ * @example
3579
+ * ```ts
3580
+ * const transaction = new TransactionBuilder();
3581
+ * new ConditionCheckBuilder('habitats', { id: 'PADDOCK-B' })
3582
+ * .condition(op => op.and([
3583
+ * op.eq('securityStatus', 'ACTIVE'),
3584
+ * op.lt('currentOccupants', 3),
3585
+ * op.eq('habitatType', 'CARNIVORE')
3586
+ * ]))
3587
+ * .withTransaction(transaction);
3588
+ * // Add dinosaur transfer operations
3589
+ * ```
3590
+ *
3591
+ * @param transaction - The transaction builder to add this operation to
3592
+ * @throws {Error} If no condition has been set
3593
+ * @returns The builder instance for method chaining
3594
+ */
3595
+ withTransaction(transaction) {
3596
+ if (!this.conditionExpression) {
3597
+ throw new Error("Condition is required for condition check operations");
3598
+ }
3599
+ const command = this.toDynamoCommand();
3600
+ transaction.conditionCheckWithCommand(command);
3601
+ return this;
3602
+ }
3603
+ /**
3604
+ * Gets a human-readable representation of the condition check command
3605
+ * with all expression placeholders replaced by their actual values.
3606
+ *
3607
+ * @example
3608
+ * ```ts
3609
+ * const debugInfo = new ConditionCheckBuilder('dinosaurs', { id: 'TREX-001' })
3610
+ * .condition(op => op.and([
3611
+ * op.between('stats.health', 50, 100),
3612
+ * op.not(op.eq('status', 'SEDATED')),
3613
+ * op.attributeExists('lastFeedingTime')
3614
+ * op.eq('version', 1)
3615
+ * ]))
3616
+ * .debug();
3617
+ * console.log(debugInfo);
3618
+ * ```
3619
+ *
3620
+ * @returns A readable representation of the condition check command with resolved expressions
3621
+ */
3622
+ debug() {
3623
+ const command = this.toDynamoCommand();
3624
+ return debugCommand(command);
3625
+ }
3626
+ };
3627
+
3628
+ // src/builders/get-builder.ts
3629
+ var GetBuilder = class {
3630
+ /**
3631
+ * Creates a new GetBuilder instance.
3632
+ *
3633
+ * @param executor - Function that executes the get operation
3634
+ * @param key - Primary key of the item to retrieve
3635
+ * @param tableName - Name of the DynamoDB table
3636
+ */
3637
+ constructor(executor, key, tableName) {
3638
+ this.executor = executor;
3639
+ this.params = {
3640
+ tableName,
3641
+ key
3642
+ };
3643
+ }
3644
+ params;
3645
+ options = {};
3646
+ selectedFields = /* @__PURE__ */ new Set();
3647
+ /**
3648
+ * Specifies which attributes to return in the get results.
3649
+ *
3650
+ * @example
3651
+ * ```typescript
3652
+ * // Select single attribute
3653
+ * builder.select('species')
3654
+ *
3655
+ * // Select multiple attributes
3656
+ * builder.select(['id', 'species', 'diet'])
3657
+ *
3658
+ * // Chain multiple select calls
3659
+ * builder
3660
+ * .select('id')
3661
+ * .select(['species', 'diet'])
3662
+ * ```
3663
+ *
3664
+ * @param fields - A single field name or an array of field names to return
3665
+ * @returns The builder instance for method chaining
3666
+ */
3667
+ select(fields) {
3668
+ if (typeof fields === "string") {
3669
+ this.selectedFields.add(fields);
3670
+ } else if (Array.isArray(fields)) {
3671
+ for (const field of fields) {
3672
+ this.selectedFields.add(field);
3775
3673
  }
3776
3674
  }
3675
+ this.options.projection = Array.from(this.selectedFields);
3676
+ return this;
3677
+ }
3678
+ /**
3679
+ * Sets whether to use strongly consistent reads for the get operation.
3680
+ * Use this method when you need:
3681
+ * - The most up-to-date dinosaur data
3682
+ * - To ensure you're reading the latest dinosaur status
3683
+ * - Critical safety information about dangerous species
3684
+ *
3685
+ * Note: Consistent reads consume twice the throughput
3686
+ *
3687
+ * @example
3688
+ * ```typescript
3689
+ * // Get the latest T-Rex data
3690
+ * const result = await new GetBuilder(executor, { pk: 'dinosaur#123', sk: 'profile' })
3691
+ * .consistentRead()
3692
+ * .execute();
3693
+ * ```
3694
+ *
3695
+ * @param consistentRead - Whether to use consistent reads (defaults to true)
3696
+ * @returns The builder instance for method chaining
3697
+ */
3698
+ consistentRead(consistentRead = true) {
3699
+ this.params.consistentRead = consistentRead;
3700
+ return this;
3701
+ }
3702
+ /**
3703
+ * Adds this get operation to a batch with optional entity type information.
3704
+ *
3705
+ * @example Basic Usage
3706
+ * ```ts
3707
+ * const batch = table.batchBuilder();
3708
+ *
3709
+ * // Add multiple get operations to batch
3710
+ * dinosaurRepo.get({ id: 'dino-1' }).withBatch(batch);
3711
+ * dinosaurRepo.get({ id: 'dino-2' }).withBatch(batch);
3712
+ * dinosaurRepo.get({ id: 'dino-3' }).withBatch(batch);
3713
+ *
3714
+ * // Execute all gets efficiently
3715
+ * const results = await batch.execute();
3716
+ * ```
3717
+ *
3718
+ * @example Typed Usage
3719
+ * ```ts
3720
+ * const batch = table.batchBuilder<{
3721
+ * User: UserEntity;
3722
+ * Order: OrderEntity;
3723
+ * }>();
3724
+ *
3725
+ * // Add operations with type information
3726
+ * userRepo.get({ id: 'user-1' }).withBatch(batch, 'User');
3727
+ * orderRepo.get({ id: 'order-1' }).withBatch(batch, 'Order');
3728
+ *
3729
+ * // Execute and get typed results
3730
+ * const result = await batch.execute();
3731
+ * const users: UserEntity[] = result.reads.itemsByType.User;
3732
+ * const orders: OrderEntity[] = result.reads.itemsByType.Order;
3733
+ * ```
3734
+ *
3735
+ * @param batch - The batch builder to add this operation to
3736
+ * @param entityType - Optional entity type key for type tracking
3737
+ */
3738
+ withBatch(batch, entityType) {
3739
+ const command = this.toDynamoCommand();
3740
+ batch.getWithCommand(command, entityType);
3741
+ }
3742
+ /**
3743
+ * Converts the builder configuration to a DynamoDB command
3744
+ */
3745
+ toDynamoCommand() {
3746
+ const expressionParams = {
3747
+ expressionAttributeNames: {}};
3748
+ const projectionExpression = Array.from(this.selectedFields).map((p) => generateAttributeName(expressionParams, p)).join(", ");
3749
+ const { expressionAttributeNames } = expressionParams;
3777
3750
  return {
3778
- unprocessedItems: allUnprocessedItems
3751
+ ...this.params,
3752
+ projectionExpression: projectionExpression.length > 0 ? projectionExpression : void 0,
3753
+ expressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 ? expressionAttributeNames : void 0
3779
3754
  };
3780
3755
  }
3756
+ /**
3757
+ * Executes the get operation against DynamoDB.
3758
+ *
3759
+ * @example
3760
+ * ```typescript
3761
+ * try {
3762
+ * const result = await new GetBuilder(executor, { pk: 'dinosaur#123', sk: 'profile' })
3763
+ * .select(['species', 'name', 'diet'])
3764
+ * .consistentRead()
3765
+ * .execute();
3766
+ *
3767
+ * if (result.item) {
3768
+ * console.log('Dinosaur found:', result.item);
3769
+ * } else {
3770
+ * console.log('Dinosaur not found');
3771
+ * }
3772
+ * } catch (error) {
3773
+ * console.error('Error getting dinosaur:', error);
3774
+ * }
3775
+ * ```
3776
+ *
3777
+ * @returns A promise that resolves to an object containing:
3778
+ * - item: The retrieved dinosaur or undefined if not found
3779
+ */
3780
+ async execute() {
3781
+ const command = this.toDynamoCommand();
3782
+ return this.executor(command);
3783
+ }
3781
3784
  };
3782
3785
 
3783
- // src/builders/entity-aware-builders.ts
3784
- function createEntityAwareBuilder(builder, entityName) {
3785
- return new Proxy(builder, {
3786
- get(target, prop, receiver) {
3787
- if (prop === "entityName") {
3788
- return entityName;
3789
- }
3790
- if (prop === "withBatch" && typeof target[prop] === "function") {
3791
- return (batch, entityType) => {
3792
- const typeToUse = entityType ?? entityName;
3793
- const fn = target[prop];
3794
- return fn.call(target, batch, typeToUse);
3795
- };
3796
- }
3797
- return Reflect.get(target, prop, receiver);
3798
- }
3799
- });
3800
- }
3801
- function createEntityAwarePutBuilder(builder, entityName) {
3802
- return createEntityAwareBuilder(builder, entityName);
3803
- }
3804
- function createEntityAwareGetBuilder(builder, entityName) {
3805
- return createEntityAwareBuilder(builder, entityName);
3806
- }
3807
- function createEntityAwareDeleteBuilder(builder, entityName) {
3808
- return createEntityAwareBuilder(builder, entityName);
3809
- }
3810
- var EntityAwareUpdateBuilder = class {
3811
- forceRebuildIndexes = [];
3812
- entityName;
3813
- builder;
3814
- entityConfig;
3815
- updateDataApplied = false;
3816
- constructor(builder, entityName) {
3817
- this.builder = builder;
3818
- this.entityName = entityName;
3786
+ // src/builders/scan-builder.ts
3787
+ var ScanBuilder = class _ScanBuilder extends FilterBuilder {
3788
+ executor;
3789
+ constructor(executor) {
3790
+ super();
3791
+ this.executor = executor;
3819
3792
  }
3820
3793
  /**
3821
- * Configure entity-specific logic for automatic timestamp generation and index updates
3794
+ * Creates a deep clone of this ScanBuilder instance.
3795
+ *
3796
+ * @returns A new ScanBuilder instance with the same configuration
3822
3797
  */
3823
- configureEntityLogic(config) {
3824
- this.entityConfig = config;
3798
+ clone() {
3799
+ const clone = new _ScanBuilder(this.executor);
3800
+ clone.options = {
3801
+ ...this.options,
3802
+ filter: this.deepCloneFilter(this.options.filter)
3803
+ };
3804
+ clone.selectedFields = new Set(this.selectedFields);
3805
+ return clone;
3806
+ }
3807
+ deepCloneFilter(filter) {
3808
+ if (!filter) return filter;
3809
+ if (filter.type === "and" || filter.type === "or") {
3810
+ return {
3811
+ ...filter,
3812
+ conditions: filter.conditions?.map((condition) => this.deepCloneFilter(condition)).filter((c) => c !== void 0)
3813
+ };
3814
+ }
3815
+ return { ...filter };
3825
3816
  }
3826
3817
  /**
3827
- * Forces a rebuild of one or more readonly indexes during the update operation.
3818
+ * Executes the scan against DynamoDB and returns a generator that behaves like an array.
3828
3819
  *
3829
- * By default, readonly indexes are not updated during entity updates to prevent
3830
- * errors when required index attributes are missing. This method allows you to
3831
- * override that behavior and force specific indexes to be rebuilt.
3820
+ * The generator automatically handles pagination and provides array-like methods
3821
+ * for processing results efficiently without loading everything into memory at once.
3832
3822
  *
3833
3823
  * @example
3834
3824
  * ```typescript
3835
- * // Force rebuild a single readonly index
3836
- * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
3837
- * .forceIndexRebuild('gsi1')
3838
- * .execute();
3825
+ * try {
3826
+ * // Find all dinosaurs with high aggression levels with automatic pagination
3827
+ * const results = await new ScanBuilder(executor)
3828
+ * .filter(op =>
3829
+ * op.and([
3830
+ * op.eq('status', 'ACTIVE'),
3831
+ * op.gt('aggressionLevel', 7)
3832
+ * ])
3833
+ * )
3834
+ * .execute();
3839
3835
  *
3840
- * // Force rebuild multiple readonly indexes
3841
- * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
3842
- * .forceIndexRebuild(['gsi1', 'gsi2'])
3843
- * .execute();
3836
+ * // Use like an array with automatic pagination
3837
+ * for await (const dinosaur of results) {
3838
+ * console.log(`Processing dangerous dinosaur: ${dinosaur.name}`);
3839
+ * }
3844
3840
  *
3845
- * // Chain with other update operations
3846
- * const result = await repo.update({ id: 'TREX-001' }, { status: 'ACTIVE' })
3847
- * .set('lastUpdated', new Date().toISOString())
3848
- * .forceIndexRebuild('gsi1')
3849
- * .condition(op => op.eq('status', 'INACTIVE'))
3850
- * .execute();
3841
+ * // Or convert to array and use array methods
3842
+ * const allItems = await results.toArray();
3843
+ * const criticalThreats = allItems.filter(dino => dino.aggressionLevel > 9);
3844
+ * const totalCount = allItems.length;
3845
+ * } catch (error) {
3846
+ * console.error('Security scan failed:', error);
3847
+ * }
3851
3848
  * ```
3852
3849
  *
3853
- * @param indexes - A single index name or array of index names to force rebuild
3854
- * @returns The builder instance for method chaining
3850
+ * @returns A promise that resolves to a ResultGenerator that behaves like an array
3855
3851
  */
3856
- forceIndexRebuild(indexes) {
3857
- if (Array.isArray(indexes)) {
3858
- this.forceRebuildIndexes = [...this.forceRebuildIndexes, ...indexes];
3859
- } else {
3860
- this.forceRebuildIndexes.push(indexes);
3861
- }
3862
- return this;
3852
+ async execute() {
3853
+ const directExecutor = () => this.executor(this.options);
3854
+ return new ResultIterator(this, directExecutor);
3855
+ }
3856
+ };
3857
+
3858
+ // src/utils/chunk-array.ts
3859
+ function* chunkArray(array, size) {
3860
+ if (size <= 0) {
3861
+ throw new Error("Chunk size must be greater than 0");
3862
+ }
3863
+ for (let i = 0; i < array.length; i += size) {
3864
+ yield array.slice(i, i + size);
3863
3865
  }
3866
+ }
3867
+
3868
+ // src/table.ts
3869
+ var DDB_BATCH_WRITE_LIMIT = 25;
3870
+ var DDB_BATCH_GET_LIMIT = 100;
3871
+ var Table = class {
3872
+ dynamoClient;
3873
+ tableName;
3864
3874
  /**
3865
- * Gets the list of indexes that should be force rebuilt.
3866
- * This is used internally by entity update logic.
3867
- *
3868
- * @returns Array of index names to force rebuild
3875
+ * The column name of the partitionKey for the Table
3869
3876
  */
3870
- getForceRebuildIndexes() {
3871
- return [...this.forceRebuildIndexes];
3872
- }
3877
+ partitionKey;
3873
3878
  /**
3874
- * Apply entity-specific update data (timestamps and index updates)
3875
- * This is called automatically when needed
3879
+ * The column name of the sortKey for the Table
3876
3880
  */
3877
- applyEntityUpdates() {
3878
- if (!this.entityConfig || this.updateDataApplied) return;
3879
- const timestamps = this.entityConfig.generateTimestamps();
3880
- const updatedItem = { ...this.entityConfig.key, ...this.entityConfig.data, ...timestamps };
3881
- const indexUpdates = this.entityConfig.buildIndexUpdates(
3882
- this.entityConfig.key,
3883
- updatedItem,
3884
- this.entityConfig.table,
3885
- this.entityConfig.indexes,
3886
- this.forceRebuildIndexes
3887
- );
3888
- this.builder.set({ ...this.entityConfig.data, ...timestamps, ...indexUpdates });
3889
- this.updateDataApplied = true;
3881
+ sortKey;
3882
+ /**
3883
+ * The Global Secondary Indexes that are configured on this table
3884
+ */
3885
+ gsis;
3886
+ constructor(config) {
3887
+ this.dynamoClient = config.client;
3888
+ this.tableName = config.tableName;
3889
+ this.partitionKey = config.indexes.partitionKey;
3890
+ this.sortKey = config.indexes.sortKey;
3891
+ this.gsis = config.indexes.gsis || {};
3890
3892
  }
3891
- set(valuesOrPath, value) {
3892
- if (typeof valuesOrPath === "object") {
3893
- this.builder.set(valuesOrPath);
3894
- } else {
3895
- if (value === void 0) {
3896
- throw new Error("Value is required when setting a single path");
3893
+ createKeyForPrimaryIndex(keyCondition) {
3894
+ const primaryCondition = { [this.partitionKey]: keyCondition.pk };
3895
+ if (this.sortKey) {
3896
+ if (!keyCondition.sk) {
3897
+ throw new Error("Sort key has not been provided but the Table has a sort key");
3897
3898
  }
3898
- this.builder.set(valuesOrPath, value);
3899
+ primaryCondition[this.sortKey] = keyCondition.sk;
3899
3900
  }
3900
- return this;
3901
- }
3902
- remove(path) {
3903
- this.builder.remove(path);
3904
- return this;
3905
- }
3906
- add(path, value) {
3907
- this.builder.add(path, value);
3908
- return this;
3909
- }
3910
- deleteElementsFromSet(path, value) {
3911
- this.builder.deleteElementsFromSet(path, value);
3912
- return this;
3913
- }
3914
- condition(condition) {
3915
- this.builder.condition(condition);
3916
- return this;
3917
- }
3918
- returnValues(returnValues) {
3919
- this.builder.returnValues(returnValues);
3920
- return this;
3921
- }
3922
- toDynamoCommand() {
3923
- return this.builder.toDynamoCommand();
3924
- }
3925
- withTransaction(transaction) {
3926
- this.applyEntityUpdates();
3927
- this.builder.withTransaction(transaction);
3928
- }
3929
- debug() {
3930
- return this.builder.debug();
3931
- }
3932
- async execute() {
3933
- this.updateDataApplied = false;
3934
- this.applyEntityUpdates();
3935
- return this.builder.execute();
3901
+ return primaryCondition;
3936
3902
  }
3937
- };
3938
- function createEntityAwareUpdateBuilder(builder, entityName) {
3939
- return new EntityAwareUpdateBuilder(builder, entityName);
3940
- }
3941
-
3942
- // src/entity/ddb-indexing.ts
3943
- var IndexBuilder = class {
3944
3903
  /**
3945
- * Creates a new IndexBuilder instance
3904
+ * Creates a new item in the table, it will fail if the item already exists.
3946
3905
  *
3947
- * @param table - The DynamoDB table instance
3948
- * @param indexes - The index definitions
3906
+ * By default, this method returns the input values passed to the create operation
3907
+ * upon successful creation.
3908
+ *
3909
+ * You can customise the return behaviour by chaining the `.returnValues()` method:
3910
+ *
3911
+ * @param item The item to create
3912
+ * @returns A PutBuilder instance for chaining additional conditions and executing the create operation
3913
+ *
3914
+ * @example
3915
+ * ```ts
3916
+ * // Create with default behavior (returns input values)
3917
+ * const result = await table.create({
3918
+ * id: 'user-123',
3919
+ * name: 'John Doe',
3920
+ * email: 'john@example.com'
3921
+ * }).execute();
3922
+ * console.log(result); // Returns the input object
3923
+ *
3924
+ * // Create with no return value for better performance
3925
+ * await table.create(userData).returnValues('NONE').execute();
3926
+ *
3927
+ * // Create and get fresh data from dynamodb using a strongly consistent read
3928
+ * const freshData = await table.create(userData).returnValues('CONSISTENT').execute();
3929
+ *
3930
+ * // Create and get previous values (if the item was overwritten)
3931
+ * const oldData = await table.create(userData).returnValues('ALL_OLD').execute();
3932
+ * ```
3949
3933
  */
3950
- constructor(table, indexes = {}) {
3951
- this.table = table;
3952
- this.indexes = indexes;
3934
+ create(item) {
3935
+ return this.put(item).condition((op) => op.attributeNotExists(this.partitionKey)).returnValues("INPUT");
3936
+ }
3937
+ get(keyCondition) {
3938
+ const executor = async (params) => {
3939
+ try {
3940
+ const result = await this.dynamoClient.get({
3941
+ TableName: params.tableName,
3942
+ Key: this.createKeyForPrimaryIndex(keyCondition),
3943
+ ProjectionExpression: params.projectionExpression,
3944
+ ExpressionAttributeNames: params.expressionAttributeNames,
3945
+ ConsistentRead: params.consistentRead
3946
+ });
3947
+ return {
3948
+ item: result.Item ? result.Item : void 0
3949
+ };
3950
+ } catch (error) {
3951
+ console.error("Error getting item:", error);
3952
+ throw error;
3953
+ }
3954
+ };
3955
+ return new GetBuilder(executor, keyCondition, this.tableName);
3953
3956
  }
3954
3957
  /**
3955
- * Build index attributes for item creation
3958
+ * Updates an item in the table
3956
3959
  *
3957
- * @param item - The item to generate indexes for
3958
- * @param options - Options for building indexes
3959
- * @returns Record of GSI attribute names to their values
3960
+ * @param item The item to update
3961
+ * @returns A PutBuilder instance for chaining conditions and executing the put operation
3960
3962
  */
3961
- buildForCreate(item, options = {}) {
3962
- const attributes = {};
3963
- for (const [indexName, indexDef] of Object.entries(this.indexes)) {
3964
- if (options.excludeReadOnly && indexDef.isReadOnly) {
3965
- continue;
3966
- }
3967
- const key = indexDef.generateKey(item);
3968
- const gsiConfig = this.table.gsis[indexName];
3969
- if (!gsiConfig) {
3970
- throw new Error(`GSI configuration not found for index: ${indexName}`);
3971
- }
3972
- if (key.pk) {
3973
- attributes[gsiConfig.partitionKey] = key.pk;
3974
- }
3975
- if (key.sk && gsiConfig.sortKey) {
3976
- attributes[gsiConfig.sortKey] = key.sk;
3963
+ put(item) {
3964
+ const executor = async (params) => {
3965
+ try {
3966
+ const result = await this.dynamoClient.put({
3967
+ TableName: params.tableName,
3968
+ Item: params.item,
3969
+ ConditionExpression: params.conditionExpression,
3970
+ ExpressionAttributeNames: params.expressionAttributeNames,
3971
+ ExpressionAttributeValues: params.expressionAttributeValues,
3972
+ // CONSISTENT and INPUT are not valid ReturnValues for DDB, so we set NONE as we are not interested in its
3973
+ // response and will be handling these cases separately
3974
+ ReturnValues: params.returnValues === "CONSISTENT" || params.returnValues === "INPUT" ? "NONE" : params.returnValues
3975
+ });
3976
+ if (params.returnValues === "INPUT") {
3977
+ return params.item;
3978
+ }
3979
+ if (params.returnValues === "CONSISTENT") {
3980
+ const getResult = await this.dynamoClient.get({
3981
+ TableName: params.tableName,
3982
+ Key: this.createKeyForPrimaryIndex({
3983
+ pk: params.item[this.partitionKey],
3984
+ ...this.sortKey && { sk: params.item[this.sortKey] }
3985
+ }),
3986
+ ConsistentRead: true
3987
+ });
3988
+ return getResult.Item;
3989
+ }
3990
+ return result.Attributes;
3991
+ } catch (error) {
3992
+ console.error("Error creating item:", error);
3993
+ throw error;
3977
3994
  }
3978
- }
3979
- return attributes;
3995
+ };
3996
+ return new PutBuilder(executor, item, this.tableName);
3980
3997
  }
3981
3998
  /**
3982
- * Build index attributes for item updates
3983
- *
3984
- * @param currentData - The current data before update
3985
- * @param updates - The update data
3986
- * @param options - Options for building indexes
3987
- * @returns Record of GSI attribute names to their updated values
3999
+ * Creates a query builder for complex queries
4000
+ * If useIndex is called on the returned QueryBuilder, it will use the GSI configuration
3988
4001
  */
3989
- buildForUpdate(currentData, updates, options = {}) {
3990
- const attributes = {};
3991
- const updatedItem = { ...currentData, ...updates };
3992
- if (options.forceRebuildIndexes && options.forceRebuildIndexes.length > 0) {
3993
- const invalidIndexes = options.forceRebuildIndexes.filter((indexName) => !this.indexes[indexName]);
3994
- if (invalidIndexes.length > 0) {
3995
- throw new Error(
3996
- `Cannot force rebuild unknown indexes: ${invalidIndexes.join(", ")}. Available indexes: ${Object.keys(this.indexes).join(", ")}`
3997
- );
4002
+ query(keyCondition) {
4003
+ const pkAttributeName = this.partitionKey;
4004
+ const skAttributeName = this.sortKey;
4005
+ let keyConditionExpression = eq(pkAttributeName, keyCondition.pk);
4006
+ if (keyCondition.sk) {
4007
+ if (!skAttributeName) {
4008
+ throw new Error("Sort key is not defined for Index");
3998
4009
  }
4010
+ const keyConditionOperator = {
4011
+ eq: (value) => eq(skAttributeName, value),
4012
+ lt: (value) => lt(skAttributeName, value),
4013
+ lte: (value) => lte(skAttributeName, value),
4014
+ gt: (value) => gt(skAttributeName, value),
4015
+ gte: (value) => gte(skAttributeName, value),
4016
+ between: (lower, upper) => between(skAttributeName, lower, upper),
4017
+ beginsWith: (value) => beginsWith(skAttributeName, value),
4018
+ and: (...conditions) => and(...conditions)
4019
+ };
4020
+ const skCondition = keyCondition.sk(keyConditionOperator);
4021
+ keyConditionExpression = and(eq(pkAttributeName, keyCondition.pk), skCondition);
3999
4022
  }
4000
- for (const [indexName, indexDef] of Object.entries(this.indexes)) {
4001
- const isForced = options.forceRebuildIndexes?.includes(indexName);
4002
- if (indexDef.isReadOnly && !isForced) {
4003
- continue;
4004
- }
4005
- if (!isForced) {
4006
- let shouldUpdateIndex = false;
4007
- try {
4008
- const currentKey = indexDef.generateKey(currentData);
4009
- const updatedKey = indexDef.generateKey(updatedItem);
4010
- if (currentKey.pk !== updatedKey.pk || currentKey.sk !== updatedKey.sk) {
4011
- shouldUpdateIndex = true;
4023
+ const executor = async (originalKeyCondition, options) => {
4024
+ let finalKeyCondition = originalKeyCondition;
4025
+ if (options.indexName) {
4026
+ const gsiName = String(options.indexName);
4027
+ const gsi = this.gsis[gsiName];
4028
+ if (!gsi) {
4029
+ throw new Error(`GSI with name "${gsiName}" does not exist on table "${this.tableName}"`);
4030
+ }
4031
+ const gsiPkAttributeName = gsi.partitionKey;
4032
+ const gsiSkAttributeName = gsi.sortKey;
4033
+ let pkValue;
4034
+ let skValue;
4035
+ let extractedSkCondition;
4036
+ if (originalKeyCondition.type === "eq") {
4037
+ pkValue = originalKeyCondition.value;
4038
+ } else if (originalKeyCondition.type === "and" && originalKeyCondition.conditions) {
4039
+ const pkCondition = originalKeyCondition.conditions.find(
4040
+ (c) => c.type === "eq" && c.attr === pkAttributeName
4041
+ );
4042
+ if (pkCondition && pkCondition.type === "eq") {
4043
+ pkValue = pkCondition.value;
4044
+ }
4045
+ const skConditions = originalKeyCondition.conditions.filter((c) => c.attr === skAttributeName);
4046
+ if (skConditions.length > 0) {
4047
+ if (skConditions.length === 1) {
4048
+ extractedSkCondition = skConditions[0];
4049
+ if (extractedSkCondition && extractedSkCondition.type === "eq") {
4050
+ skValue = extractedSkCondition.value;
4051
+ }
4052
+ } else if (skConditions.length > 1) {
4053
+ extractedSkCondition = and(...skConditions);
4054
+ }
4012
4055
  }
4013
- } catch {
4014
- shouldUpdateIndex = true;
4015
4056
  }
4016
- if (!shouldUpdateIndex) {
4017
- continue;
4057
+ if (!pkValue) {
4058
+ throw new Error("Could not extract partition key value from key condition");
4059
+ }
4060
+ let gsiKeyCondition = eq(gsiPkAttributeName, pkValue);
4061
+ if (skValue && gsiSkAttributeName) {
4062
+ gsiKeyCondition = and(gsiKeyCondition, eq(gsiSkAttributeName, skValue));
4063
+ } else if (extractedSkCondition && gsiSkAttributeName) {
4064
+ if (extractedSkCondition.attr === skAttributeName) {
4065
+ const updatedSkCondition = {
4066
+ ...extractedSkCondition,
4067
+ attr: gsiSkAttributeName
4068
+ };
4069
+ gsiKeyCondition = and(gsiKeyCondition, updatedSkCondition);
4070
+ } else {
4071
+ gsiKeyCondition = and(gsiKeyCondition, extractedSkCondition);
4072
+ }
4018
4073
  }
4074
+ finalKeyCondition = gsiKeyCondition;
4019
4075
  }
4020
- let key;
4076
+ const expressionParams = {
4077
+ expressionAttributeNames: {},
4078
+ expressionAttributeValues: {},
4079
+ valueCounter: { count: 0 }
4080
+ };
4081
+ const keyConditionExpression2 = buildExpression(finalKeyCondition, expressionParams);
4082
+ let filterExpression;
4083
+ if (options.filter) {
4084
+ filterExpression = buildExpression(options.filter, expressionParams);
4085
+ }
4086
+ const projectionExpression = options.projection?.map((p) => generateAttributeName(expressionParams, p)).join(", ");
4087
+ const { expressionAttributeNames, expressionAttributeValues } = expressionParams;
4088
+ const { indexName, limit, consistentRead, scanIndexForward, lastEvaluatedKey } = options;
4089
+ const params = {
4090
+ TableName: this.tableName,
4091
+ KeyConditionExpression: keyConditionExpression2,
4092
+ FilterExpression: filterExpression,
4093
+ ExpressionAttributeNames: expressionAttributeNames,
4094
+ ExpressionAttributeValues: expressionAttributeValues,
4095
+ IndexName: indexName,
4096
+ Limit: limit,
4097
+ ConsistentRead: consistentRead,
4098
+ ScanIndexForward: scanIndexForward,
4099
+ ProjectionExpression: projectionExpression,
4100
+ ExclusiveStartKey: lastEvaluatedKey
4101
+ };
4021
4102
  try {
4022
- key = indexDef.generateKey(updatedItem);
4103
+ const result = await this.dynamoClient.query(params);
4104
+ return {
4105
+ items: result.Items,
4106
+ lastEvaluatedKey: result.LastEvaluatedKey
4107
+ };
4023
4108
  } catch (error) {
4024
- if (error instanceof Error) {
4025
- throw new Error(`Missing attributes: ${error.message}`);
4026
- }
4109
+ console.log(debugCommand(params));
4110
+ console.error("Error querying items:", error);
4027
4111
  throw error;
4028
4112
  }
4029
- if (this.hasUndefinedValues(key)) {
4030
- throw new Error(
4031
- `Missing attributes: Cannot update entity: insufficient data to regenerate index "${indexName}". All attributes required by the index must be provided in the update operation, or the index must be marked as readOnly.`
4032
- );
4113
+ };
4114
+ return new QueryBuilder(executor, keyConditionExpression);
4115
+ }
4116
+ /**
4117
+ * Creates a scan builder for scanning the entire table
4118
+ * Use this when you need to:
4119
+ * - Process all items in a table
4120
+ * - Apply filters to a large dataset
4121
+ * - Use a GSI for scanning
4122
+ *
4123
+ * @returns A ScanBuilder instance for chaining operations
4124
+ */
4125
+ scan() {
4126
+ const executor = async (options) => {
4127
+ const expressionParams = {
4128
+ expressionAttributeNames: {},
4129
+ expressionAttributeValues: {},
4130
+ valueCounter: { count: 0 }
4131
+ };
4132
+ let filterExpression;
4133
+ if (options.filter) {
4134
+ filterExpression = buildExpression(options.filter, expressionParams);
4033
4135
  }
4034
- const gsiConfig = this.table.gsis[indexName];
4035
- if (!gsiConfig) {
4036
- throw new Error(`GSI configuration not found for index: ${indexName}`);
4136
+ const projectionExpression = options.projection?.map((p) => generateAttributeName(expressionParams, p)).join(", ");
4137
+ const { expressionAttributeNames, expressionAttributeValues } = expressionParams;
4138
+ const { indexName, limit, consistentRead, lastEvaluatedKey } = options;
4139
+ const params = {
4140
+ TableName: this.tableName,
4141
+ FilterExpression: filterExpression,
4142
+ ExpressionAttributeNames: Object.keys(expressionAttributeNames).length > 0 ? expressionAttributeNames : void 0,
4143
+ ExpressionAttributeValues: Object.keys(expressionAttributeValues).length > 0 ? expressionAttributeValues : void 0,
4144
+ IndexName: indexName,
4145
+ Limit: limit,
4146
+ ConsistentRead: consistentRead,
4147
+ ProjectionExpression: projectionExpression,
4148
+ ExclusiveStartKey: lastEvaluatedKey
4149
+ };
4150
+ try {
4151
+ const result = await this.dynamoClient.scan(params);
4152
+ return {
4153
+ items: result.Items,
4154
+ lastEvaluatedKey: result.LastEvaluatedKey
4155
+ };
4156
+ } catch (error) {
4157
+ console.log(debugCommand(params));
4158
+ console.error("Error scanning items:", error);
4159
+ throw error;
4037
4160
  }
4038
- if (key.pk) {
4039
- attributes[gsiConfig.partitionKey] = key.pk;
4161
+ };
4162
+ return new ScanBuilder(executor);
4163
+ }
4164
+ delete(keyCondition) {
4165
+ const executor = async (params) => {
4166
+ try {
4167
+ const result = await this.dynamoClient.delete({
4168
+ TableName: params.tableName,
4169
+ Key: this.createKeyForPrimaryIndex(keyCondition),
4170
+ ConditionExpression: params.conditionExpression,
4171
+ ExpressionAttributeNames: params.expressionAttributeNames,
4172
+ ExpressionAttributeValues: params.expressionAttributeValues,
4173
+ ReturnValues: params.returnValues
4174
+ });
4175
+ return {
4176
+ item: result.Attributes
4177
+ };
4178
+ } catch (error) {
4179
+ console.error("Error deleting item:", error);
4180
+ throw error;
4040
4181
  }
4041
- if (key.sk && gsiConfig.sortKey) {
4042
- attributes[gsiConfig.sortKey] = key.sk;
4182
+ };
4183
+ return new DeleteBuilder(executor, this.tableName, keyCondition);
4184
+ }
4185
+ /**
4186
+ * Updates an item in the table
4187
+ *
4188
+ * @param keyCondition The primary key of the item to update
4189
+ * @returns An UpdateBuilder instance for chaining update operations and conditions
4190
+ */
4191
+ update(keyCondition) {
4192
+ const executor = async (params) => {
4193
+ try {
4194
+ const result = await this.dynamoClient.update({
4195
+ TableName: params.tableName,
4196
+ Key: this.createKeyForPrimaryIndex(keyCondition),
4197
+ UpdateExpression: params.updateExpression,
4198
+ ConditionExpression: params.conditionExpression,
4199
+ ExpressionAttributeNames: params.expressionAttributeNames,
4200
+ ExpressionAttributeValues: params.expressionAttributeValues,
4201
+ ReturnValues: params.returnValues
4202
+ });
4203
+ return {
4204
+ item: result.Attributes
4205
+ };
4206
+ } catch (error) {
4207
+ console.error("Error updating item:", error);
4208
+ throw error;
4043
4209
  }
4044
- }
4045
- return attributes;
4210
+ };
4211
+ return new UpdateBuilder(executor, this.tableName, keyCondition);
4212
+ }
4213
+ /**
4214
+ * Creates a transaction builder for performing multiple operations atomically
4215
+ */
4216
+ transactionBuilder() {
4217
+ const executor = async (params) => {
4218
+ await this.dynamoClient.transactWrite(params);
4219
+ };
4220
+ return new TransactionBuilder(executor, {
4221
+ partitionKey: this.partitionKey,
4222
+ sortKey: this.sortKey
4223
+ });
4046
4224
  }
4047
4225
  /**
4048
- * Check if a key has undefined values
4226
+ * Creates a batch builder for performing multiple operations efficiently with optional type inference
4049
4227
  *
4050
- * @param key - The index key to check
4051
- * @returns True if the key contains undefined values, false otherwise
4228
+ * @example Basic Usage
4229
+ * ```typescript
4230
+ * const batch = table.batchBuilder();
4231
+ *
4232
+ * // Add operations
4233
+ * userRepo.create(newUser).withBatch(batch);
4234
+ * orderRepo.get({ id: 'order-1' }).withBatch(batch);
4235
+ *
4236
+ * // Execute operations
4237
+ * const result = await batch.execute();
4238
+ * ```
4239
+ *
4240
+ * @example Typed Usage
4241
+ * ```typescript
4242
+ * // Define entity types for the batch
4243
+ * const batch = table.batchBuilder<{
4244
+ * User: UserEntity;
4245
+ * Order: OrderEntity;
4246
+ * Product: ProductEntity;
4247
+ * }>();
4248
+ *
4249
+ * // Add operations with type information
4250
+ * userRepo.create(newUser).withBatch(batch, 'User');
4251
+ * orderRepo.get({ id: 'order-1' }).withBatch(batch, 'Order');
4252
+ * productRepo.delete({ id: 'old-product' }).withBatch(batch, 'Product');
4253
+ *
4254
+ * // Execute and get typed results
4255
+ * const result = await batch.execute();
4256
+ * const users: UserEntity[] = result.reads.itemsByType.User;
4257
+ * const orders: OrderEntity[] = result.reads.itemsByType.Order;
4258
+ * ```
4052
4259
  */
4053
- hasUndefinedValues(key) {
4054
- return (key.pk?.includes("undefined") ?? false) || (key.sk?.includes("undefined") ?? false);
4055
- }
4056
- };
4057
-
4058
- // src/entity/index-utils.ts
4059
- function buildIndexes(dataForKeyGeneration, table, indexes, excludeReadOnly = false) {
4060
- if (!indexes) {
4061
- return {};
4062
- }
4063
- const indexBuilder = new IndexBuilder(table, indexes);
4064
- return indexBuilder.buildForCreate(dataForKeyGeneration, { excludeReadOnly });
4065
- }
4066
- function buildIndexUpdates(currentData, updates, table, indexes, forceRebuildIndexes) {
4067
- if (!indexes) {
4068
- return {};
4260
+ batchBuilder() {
4261
+ const batchWriteExecutor = async (operations) => {
4262
+ return this.batchWrite(operations);
4263
+ };
4264
+ const batchGetExecutor = async (keys) => {
4265
+ return this.batchGet(keys);
4266
+ };
4267
+ return new BatchBuilder(batchWriteExecutor, batchGetExecutor, {
4268
+ partitionKey: this.partitionKey,
4269
+ sortKey: this.sortKey
4270
+ });
4069
4271
  }
4070
- const indexBuilder = new IndexBuilder(table, indexes);
4071
- return indexBuilder.buildForUpdate(currentData, updates, { forceRebuildIndexes });
4072
- }
4073
-
4074
- // src/entity/entity.ts
4075
- function defineEntity(config) {
4076
- const entityTypeAttributeName = config.settings?.entityTypeAttributeName ?? "entityType";
4077
- const buildIndexes2 = (dataForKeyGeneration, table, excludeReadOnly = false) => {
4078
- return buildIndexes(dataForKeyGeneration, table, config.indexes, excludeReadOnly);
4079
- };
4080
- const wrapMethodWithPreparation = (originalMethod, prepareFn, context) => {
4081
- const wrappedMethod = (...args) => {
4082
- prepareFn();
4083
- return originalMethod.call(context, ...args);
4272
+ /**
4273
+ * Executes a transaction using a callback function
4274
+ *
4275
+ * @param callback A function that receives a transaction context and performs operations on it
4276
+ * @param options Optional transaction options
4277
+ * @returns A promise that resolves when the transaction is complete
4278
+ */
4279
+ async transaction(callback, options) {
4280
+ const transactionExecutor = async (params) => {
4281
+ await this.dynamoClient.transactWrite(params);
4084
4282
  };
4085
- Object.setPrototypeOf(wrappedMethod, originalMethod);
4086
- const propertyNames = Object.getOwnPropertyNames(originalMethod);
4087
- for (let i = 0; i < propertyNames.length; i++) {
4088
- const prop = propertyNames[i];
4089
- if (prop !== "length" && prop !== "name" && prop !== "prototype") {
4090
- const descriptor = Object.getOwnPropertyDescriptor(originalMethod, prop);
4091
- if (descriptor && descriptor.writable !== false && !descriptor.get) {
4092
- wrappedMethod[prop] = originalMethod[prop];
4093
- }
4094
- }
4095
- }
4096
- return wrappedMethod;
4097
- };
4098
- const generateTimestamps = (timestampsToGenerate, data) => {
4099
- if (!config.settings?.timestamps) return {};
4100
- const timestamps = {};
4101
- const now = /* @__PURE__ */ new Date();
4102
- const unixTime = Math.floor(Date.now() / 1e3);
4103
- const { createdAt, updatedAt } = config.settings.timestamps;
4104
- if (createdAt && timestampsToGenerate.includes("createdAt") && !data.createdAt) {
4105
- const name = createdAt.attributeName ?? "createdAt";
4106
- timestamps[name] = createdAt.format === "UNIX" ? unixTime : now.toISOString();
4107
- }
4108
- if (updatedAt && timestampsToGenerate.includes("updatedAt") && !data.updatedAt) {
4109
- const name = updatedAt.attributeName ?? "updatedAt";
4110
- timestamps[name] = updatedAt.format === "UNIX" ? unixTime : now.toISOString();
4283
+ const transaction = new TransactionBuilder(transactionExecutor, {
4284
+ partitionKey: this.partitionKey,
4285
+ sortKey: this.sortKey
4286
+ });
4287
+ if (options) {
4288
+ transaction.withOptions(options);
4111
4289
  }
4112
- return timestamps;
4113
- };
4114
- return {
4115
- name: config.name,
4116
- createRepository: (table) => {
4117
- const repository = {
4118
- create: (data) => {
4119
- const builder = table.create({});
4120
- const prepareValidatedItemAsync = async () => {
4121
- const validatedData = await config.schema["~standard"].validate(data);
4122
- if ("issues" in validatedData && validatedData.issues) {
4123
- throw new Error(`Validation failed: ${validatedData.issues.map((i) => i.message).join(", ")}`);
4124
- }
4125
- const dataForKeyGeneration = {
4126
- ...validatedData.value,
4127
- ...generateTimestamps(["createdAt", "updatedAt"], validatedData.value)
4128
- };
4129
- const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
4130
- const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
4131
- const validatedItem = {
4132
- ...dataForKeyGeneration,
4133
- [entityTypeAttributeName]: config.name,
4134
- [table.partitionKey]: primaryKey.pk,
4135
- ...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
4136
- ...indexes
4137
- };
4138
- Object.assign(builder, { item: validatedItem });
4139
- return validatedItem;
4140
- };
4141
- const prepareValidatedItemSync = () => {
4142
- const validationResult = config.schema["~standard"].validate(data);
4143
- if (validationResult instanceof Promise) {
4144
- throw new Error(
4145
- "Async validation is not supported in withBatch or withTransaction. The schema must support synchronous validation for compatibility."
4146
- );
4147
- }
4148
- if ("issues" in validationResult && validationResult.issues) {
4149
- throw new Error(`Validation failed: ${validationResult.issues.map((i) => i.message).join(", ")}`);
4150
- }
4151
- const dataForKeyGeneration = {
4152
- ...validationResult.value,
4153
- ...generateTimestamps(["createdAt", "updatedAt"], validationResult.value)
4154
- };
4155
- const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
4156
- const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
4157
- const validatedItem = {
4158
- ...dataForKeyGeneration,
4159
- [entityTypeAttributeName]: config.name,
4160
- [table.partitionKey]: primaryKey.pk,
4161
- ...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
4162
- ...indexes
4163
- };
4164
- Object.assign(builder, { item: validatedItem });
4165
- return validatedItem;
4166
- };
4167
- const originalExecute = builder.execute;
4168
- builder.execute = async () => {
4169
- await prepareValidatedItemAsync();
4170
- return await originalExecute.call(builder);
4171
- };
4172
- const originalWithTransaction = builder.withTransaction;
4173
- if (originalWithTransaction) {
4174
- builder.withTransaction = wrapMethodWithPreparation(
4175
- originalWithTransaction,
4176
- prepareValidatedItemSync,
4177
- builder
4178
- );
4179
- }
4180
- const originalWithBatch = builder.withBatch;
4181
- if (originalWithBatch) {
4182
- builder.withBatch = wrapMethodWithPreparation(originalWithBatch, prepareValidatedItemSync, builder);
4183
- }
4184
- return createEntityAwarePutBuilder(builder, config.name);
4185
- },
4186
- upsert: (data) => {
4187
- const builder = table.put({});
4188
- const prepareValidatedItemAsync = async () => {
4189
- const validatedData = await config.schema["~standard"].validate(data);
4190
- if ("issues" in validatedData && validatedData.issues) {
4191
- throw new Error(`Validation failed: ${validatedData.issues.map((i) => i.message).join(", ")}`);
4192
- }
4193
- const dataForKeyGeneration = {
4194
- ...validatedData.value,
4195
- ...generateTimestamps(["createdAt", "updatedAt"], validatedData.value)
4196
- };
4197
- const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
4198
- const indexes = buildIndexes2(dataForKeyGeneration, table, false);
4199
- const validatedItem = {
4200
- [table.partitionKey]: primaryKey.pk,
4201
- ...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
4202
- ...dataForKeyGeneration,
4203
- [entityTypeAttributeName]: config.name,
4204
- ...indexes
4205
- };
4206
- Object.assign(builder, { item: validatedItem });
4207
- return validatedItem;
4208
- };
4209
- const prepareValidatedItemSync = () => {
4210
- const validationResult = config.schema["~standard"].validate(data);
4211
- if (validationResult instanceof Promise) {
4212
- throw new Error(
4213
- "Async validation is not supported in withTransaction or withBatch. Use execute() instead."
4214
- );
4215
- }
4216
- if ("issues" in validationResult && validationResult.issues) {
4217
- throw new Error(`Validation failed: ${validationResult.issues.map((i) => i.message).join(", ")}`);
4218
- }
4219
- const dataForKeyGeneration = {
4220
- ...validationResult.value,
4221
- ...generateTimestamps(["createdAt", "updatedAt"], validationResult.value)
4222
- };
4223
- const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
4224
- const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
4225
- const validatedItem = {
4226
- [table.partitionKey]: primaryKey.pk,
4227
- ...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
4228
- ...dataForKeyGeneration,
4229
- [entityTypeAttributeName]: config.name,
4230
- ...indexes
4231
- };
4232
- Object.assign(builder, { item: validatedItem });
4233
- return validatedItem;
4234
- };
4235
- const originalExecute = builder.execute;
4236
- builder.execute = async () => {
4237
- await prepareValidatedItemAsync();
4238
- const result = await originalExecute.call(builder);
4239
- if (!result) {
4240
- throw new Error("Failed to upsert item");
4290
+ const result = await callback(transaction);
4291
+ await transaction.execute();
4292
+ return result;
4293
+ }
4294
+ /**
4295
+ * Creates a condition check operation for use in transactions
4296
+ *
4297
+ * This is useful for when you require a transaction to succeed only when a specific condition is met on a
4298
+ * a record within the database that you are not directly updating.
4299
+ *
4300
+ * For example, you are updating a record and you want to ensure that another record exists and/or has a specific value before proceeding.
4301
+ */
4302
+ conditionCheck(keyCondition) {
4303
+ return new ConditionCheckBuilder(this.tableName, keyCondition);
4304
+ }
4305
+ /**
4306
+ * Performs a batch get operation to retrieve multiple items at once
4307
+ *
4308
+ * @param keys Array of primary keys to retrieve
4309
+ * @returns A promise that resolves to the retrieved items
4310
+ */
4311
+ async batchGet(keys) {
4312
+ const allItems = [];
4313
+ const allUnprocessedKeys = [];
4314
+ for (const chunk of chunkArray(keys, DDB_BATCH_GET_LIMIT)) {
4315
+ const formattedKeys = chunk.map((key) => ({
4316
+ [this.partitionKey]: key.pk,
4317
+ ...this.sortKey ? { [this.sortKey]: key.sk } : {}
4318
+ }));
4319
+ const params = {
4320
+ RequestItems: {
4321
+ [this.tableName]: {
4322
+ Keys: formattedKeys
4323
+ }
4324
+ }
4325
+ };
4326
+ try {
4327
+ const result = await this.dynamoClient.batchGet(params);
4328
+ if (result.Responses?.[this.tableName]) {
4329
+ allItems.push(...result.Responses[this.tableName]);
4330
+ }
4331
+ const unprocessedKeysArray = result.UnprocessedKeys?.[this.tableName]?.Keys || [];
4332
+ const unprocessedKeys = unprocessedKeysArray.map((key) => ({
4333
+ pk: key[this.partitionKey],
4334
+ sk: this.sortKey ? key[this.sortKey] : void 0
4335
+ }));
4336
+ if (unprocessedKeys.length > 0) {
4337
+ allUnprocessedKeys.push(...unprocessedKeys);
4338
+ }
4339
+ } catch (error) {
4340
+ console.error("Error in batch get operation:", error);
4341
+ throw error;
4342
+ }
4343
+ }
4344
+ return {
4345
+ items: allItems,
4346
+ unprocessedKeys: allUnprocessedKeys
4347
+ };
4348
+ }
4349
+ /**
4350
+ * Performs a batch write operation to put or delete multiple items at once
4351
+ *
4352
+ * @param operations Array of put or delete operations
4353
+ * @returns A promise that resolves to any unprocessed operations
4354
+ */
4355
+ async batchWrite(operations) {
4356
+ const allUnprocessedItems = [];
4357
+ for (const chunk of chunkArray(operations, DDB_BATCH_WRITE_LIMIT)) {
4358
+ const writeRequests = chunk.map((operation) => {
4359
+ if (operation.type === "put") {
4360
+ return {
4361
+ PutRequest: {
4362
+ Item: operation.item
4241
4363
  }
4242
- return result;
4243
4364
  };
4244
- const originalWithTransaction = builder.withTransaction;
4245
- if (originalWithTransaction) {
4246
- builder.withTransaction = wrapMethodWithPreparation(
4247
- originalWithTransaction,
4248
- prepareValidatedItemSync,
4249
- builder
4250
- );
4251
- }
4252
- const originalWithBatch = builder.withBatch;
4253
- if (originalWithBatch) {
4254
- builder.withBatch = wrapMethodWithPreparation(originalWithBatch, prepareValidatedItemSync, builder);
4365
+ }
4366
+ return {
4367
+ DeleteRequest: {
4368
+ Key: this.createKeyForPrimaryIndex(operation.key)
4255
4369
  }
4256
- return createEntityAwarePutBuilder(builder, config.name);
4257
- },
4258
- get: (key) => {
4259
- return createEntityAwareGetBuilder(table.get(config.primaryKey.generateKey(key)), config.name);
4260
- },
4261
- update: (key, data) => {
4262
- const primaryKeyObj = config.primaryKey.generateKey(key);
4263
- const builder = table.update(primaryKeyObj);
4264
- builder.condition(eq(entityTypeAttributeName, config.name));
4265
- const entityAwareBuilder = createEntityAwareUpdateBuilder(builder, config.name);
4266
- entityAwareBuilder.configureEntityLogic({
4267
- data,
4268
- key,
4269
- table,
4270
- indexes: config.indexes,
4271
- generateTimestamps: () => generateTimestamps(["updatedAt"], data),
4272
- buildIndexUpdates
4273
- });
4274
- return entityAwareBuilder;
4275
- },
4276
- delete: (key) => {
4277
- const builder = table.delete(config.primaryKey.generateKey(key));
4278
- builder.condition(eq(entityTypeAttributeName, config.name));
4279
- return createEntityAwareDeleteBuilder(builder, config.name);
4280
- },
4281
- query: Object.entries(config.queries || {}).reduce((acc, [key, inputCallback]) => {
4282
- acc[key] = (input) => {
4283
- const queryEntity = {
4284
- scan: repository.scan,
4285
- get: (key2) => createEntityAwareGetBuilder(table.get(key2), config.name),
4286
- query: (keyCondition) => {
4287
- return table.query(keyCondition);
4288
- }
4289
- };
4290
- const queryBuilderCallback = inputCallback(input);
4291
- const builder = queryBuilderCallback(queryEntity);
4292
- if (builder && typeof builder === "object" && "filter" in builder && typeof builder.filter === "function") {
4293
- builder.filter(eq(entityTypeAttributeName, config.name));
4370
+ };
4371
+ });
4372
+ const params = {
4373
+ RequestItems: {
4374
+ [this.tableName]: writeRequests
4375
+ }
4376
+ };
4377
+ try {
4378
+ const result = await this.dynamoClient.batchWrite(params);
4379
+ const unprocessedRequestsArray = result.UnprocessedItems?.[this.tableName] || [];
4380
+ if (unprocessedRequestsArray.length > 0) {
4381
+ const unprocessedItems = unprocessedRequestsArray.map((request) => {
4382
+ if (request?.PutRequest?.Item) {
4383
+ return {
4384
+ type: "put",
4385
+ item: request.PutRequest.Item
4386
+ };
4294
4387
  }
4295
- if (builder && typeof builder === "object" && "execute" in builder) {
4296
- const originalExecute = builder.execute;
4297
- builder.execute = async () => {
4298
- const queryFn = config.queries[key];
4299
- if (queryFn && typeof queryFn === "function") {
4300
- const schema = queryFn.schema;
4301
- if (schema?.["~standard"]?.validate && typeof schema["~standard"].validate === "function") {
4302
- const validationResult = schema["~standard"].validate(input);
4303
- if ("issues" in validationResult && validationResult.issues) {
4304
- throw new Error(
4305
- `Validation failed: ${validationResult.issues.map((issue) => issue.message).join(", ")}`
4306
- );
4307
- }
4308
- }
4309
- }
4310
- const result = await originalExecute.call(builder);
4311
- if (!result) {
4312
- throw new Error("Failed to execute query");
4388
+ if (request?.DeleteRequest?.Key) {
4389
+ return {
4390
+ type: "delete",
4391
+ key: {
4392
+ pk: request.DeleteRequest.Key[this.partitionKey],
4393
+ sk: this.sortKey ? request.DeleteRequest.Key[this.sortKey] : void 0
4313
4394
  }
4314
- return result;
4315
4395
  };
4316
4396
  }
4317
- return builder;
4318
- };
4319
- return acc;
4320
- }, {}),
4321
- scan: () => {
4322
- const builder = table.scan();
4323
- builder.filter(eq(entityTypeAttributeName, config.name));
4324
- return builder;
4397
+ throw new Error("Invalid unprocessed item format returned from DynamoDB");
4398
+ });
4399
+ allUnprocessedItems.push(...unprocessedItems);
4325
4400
  }
4326
- };
4327
- return repository;
4328
- }
4329
- };
4330
- }
4331
- function createQueries() {
4332
- return {
4333
- input: (schema) => ({
4334
- query: (handler) => {
4335
- const queryFn = (input) => (entity) => handler({ input, entity });
4336
- queryFn.schema = schema;
4337
- return queryFn;
4401
+ } catch (error) {
4402
+ console.error("Error in batch write operation:", error);
4403
+ throw error;
4338
4404
  }
4339
- })
4340
- };
4341
- }
4342
- function createIndex() {
4343
- return {
4344
- input: (schema) => {
4345
- const createIndexBuilder = (isReadOnly = false) => ({
4346
- partitionKey: (pkFn) => ({
4347
- sortKey: (skFn) => {
4348
- const index = {
4349
- name: "custom",
4350
- partitionKey: "pk",
4351
- sortKey: "sk",
4352
- isReadOnly,
4353
- generateKey: (item) => {
4354
- const data = schema["~standard"].validate(item);
4355
- if ("issues" in data && data.issues) {
4356
- throw new Error(`Index validation failed: ${data.issues.map((i) => i.message).join(", ")}`);
4357
- }
4358
- const validData = "value" in data ? data.value : item;
4359
- return { pk: pkFn(validData), sk: skFn(validData) };
4360
- }
4361
- };
4362
- return Object.assign(index, {
4363
- readOnly: (value = false) => ({
4364
- ...index,
4365
- isReadOnly: value
4366
- })
4367
- });
4368
- },
4369
- withoutSortKey: () => {
4370
- const index = {
4371
- name: "custom",
4372
- partitionKey: "pk",
4373
- isReadOnly,
4374
- generateKey: (item) => {
4375
- const data = schema["~standard"].validate(item);
4376
- if ("issues" in data && data.issues) {
4377
- throw new Error(`Index validation failed: ${data.issues.map((i) => i.message).join(", ")}`);
4378
- }
4379
- const validData = "value" in data ? data.value : item;
4380
- return { pk: pkFn(validData) };
4381
- }
4382
- };
4383
- return Object.assign(index, {
4384
- readOnly: (value = true) => ({
4385
- ...index,
4386
- isReadOnly: value
4387
- })
4388
- });
4389
- }
4390
- }),
4391
- readOnly: (value = true) => createIndexBuilder(value)
4392
- });
4393
- return createIndexBuilder(false);
4394
4405
  }
4395
- };
4396
- }
4406
+ return {
4407
+ unprocessedItems: allUnprocessedItems
4408
+ };
4409
+ }
4410
+ };
4397
4411
 
4398
4412
  // src/utils/partition-key-template.ts
4399
4413
  function partitionKey(strings, ...keys) {