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