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.
- package/dist/{batch-builder-BPoHyN_Q.d.cts → batch-builder-BiQDIZ7p.d.cts} +1 -1
- package/dist/{batch-builder-Cdo49C2r.d.ts → batch-builder-CNsLS6sR.d.ts} +1 -1
- package/dist/builders/condition-check-builder.cjs.map +1 -1
- package/dist/builders/condition-check-builder.d.cts +3 -3
- package/dist/builders/condition-check-builder.d.ts +3 -3
- package/dist/builders/condition-check-builder.js.map +1 -1
- package/dist/builders/delete-builder.cjs.map +1 -1
- package/dist/builders/delete-builder.d.cts +4 -4
- package/dist/builders/delete-builder.d.ts +4 -4
- package/dist/builders/delete-builder.js.map +1 -1
- package/dist/builders/put-builder.cjs.map +1 -1
- package/dist/builders/put-builder.d.cts +4 -4
- package/dist/builders/put-builder.d.ts +4 -4
- package/dist/builders/put-builder.js.map +1 -1
- package/dist/builders/query-builder.cjs +19 -5
- package/dist/builders/query-builder.cjs.map +1 -1
- package/dist/builders/query-builder.d.cts +3 -3
- package/dist/builders/query-builder.d.ts +3 -3
- package/dist/builders/query-builder.js +19 -5
- package/dist/builders/query-builder.js.map +1 -1
- package/dist/builders/transaction-builder.cjs.map +1 -1
- package/dist/builders/transaction-builder.d.cts +2 -2
- package/dist/builders/transaction-builder.d.ts +2 -2
- package/dist/builders/transaction-builder.js.map +1 -1
- package/dist/builders/update-builder.cjs.map +1 -1
- package/dist/builders/update-builder.d.cts +3 -3
- package/dist/builders/update-builder.d.ts +3 -3
- package/dist/builders/update-builder.js.map +1 -1
- package/dist/{conditions-CC3NDfUU.d.cts → conditions-CcZL0sR2.d.cts} +1 -1
- package/dist/{conditions-DD0bvyHm.d.ts → conditions-D_w7vVYG.d.ts} +1 -1
- package/dist/conditions.d.cts +1 -1
- package/dist/conditions.d.ts +1 -1
- package/dist/entity.cjs.map +1 -1
- package/dist/entity.d.cts +10 -10
- package/dist/entity.d.ts +10 -10
- package/dist/entity.js.map +1 -1
- package/dist/index.cjs +1489 -1475
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +1489 -1475
- package/dist/index.js.map +1 -1
- package/dist/{query-builder-DoZzZz_c.d.cts → query-builder-D3URwK9k.d.cts} +2 -2
- package/dist/{query-builder-CUWdavZw.d.ts → query-builder-cfEkU0_w.d.ts} +2 -2
- package/dist/{table-f-3wsT7K.d.cts → table-ClST8nkR.d.cts} +3 -3
- package/dist/{table-CZBMkW2Z.d.ts → table-vE3cGoDy.d.ts} +3 -3
- package/dist/table.cjs +19 -5
- package/dist/table.cjs.map +1 -1
- package/dist/table.d.cts +4 -4
- package/dist/table.d.ts +4 -4
- package/dist/table.js +19 -5
- package/dist/table.js.map +1 -1
- 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/
|
|
917
|
-
var
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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.
|
|
928
|
-
|
|
929
|
-
|
|
767
|
+
this.item = item;
|
|
768
|
+
this.tableName = tableName;
|
|
769
|
+
this.options = {
|
|
770
|
+
returnValues: "NONE"
|
|
930
771
|
};
|
|
931
772
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
*
|
|
782
|
+
* Adds a condition that must be satisfied for the put operation to succeed.
|
|
937
783
|
*
|
|
938
784
|
* @example
|
|
939
|
-
* ```
|
|
940
|
-
* //
|
|
941
|
-
* builder.
|
|
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
|
-
* //
|
|
947
|
-
* builder
|
|
948
|
-
* .
|
|
949
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
* //
|
|
978
|
-
*
|
|
979
|
-
* .
|
|
980
|
-
*
|
|
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
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
-
|
|
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/
|
|
3232
|
-
function
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
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
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
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
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
3285
|
-
*
|
|
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
|
-
* ```
|
|
3289
|
-
* //
|
|
3290
|
-
* const result = await
|
|
3291
|
-
*
|
|
3292
|
-
*
|
|
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
|
-
* //
|
|
3301
|
-
* const
|
|
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
|
-
* //
|
|
3304
|
-
* const
|
|
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
|
-
|
|
3308
|
-
|
|
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
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
3020
|
+
* Creates a new IndexBuilder instance
|
|
3332
3021
|
*
|
|
3333
|
-
* @param
|
|
3334
|
-
* @
|
|
3022
|
+
* @param table - The DynamoDB table instance
|
|
3023
|
+
* @param indexes - The index definitions
|
|
3335
3024
|
*/
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
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
|
-
*
|
|
3373
|
-
*
|
|
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
|
-
|
|
3376
|
-
const
|
|
3377
|
-
const
|
|
3378
|
-
|
|
3379
|
-
|
|
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
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
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
|
-
|
|
3450
|
-
|
|
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
|
-
|
|
3460
|
-
|
|
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
|
|
3053
|
+
}
|
|
3054
|
+
return attributes;
|
|
3488
3055
|
}
|
|
3489
3056
|
/**
|
|
3490
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
3499
|
-
const
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
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
|
-
|
|
3510
|
-
|
|
3511
|
-
const
|
|
3512
|
-
|
|
3513
|
-
|
|
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
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3099
|
+
if (error instanceof Error) {
|
|
3100
|
+
throw new Error(`Missing attributes: ${error.message}`);
|
|
3101
|
+
}
|
|
3553
3102
|
throw error;
|
|
3554
3103
|
}
|
|
3555
|
-
|
|
3556
|
-
|
|
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
|
-
*
|
|
3123
|
+
* Check if a key has undefined values
|
|
3560
3124
|
*
|
|
3561
|
-
* @param
|
|
3562
|
-
* @returns
|
|
3125
|
+
* @param key - The index key to check
|
|
3126
|
+
* @returns True if the key contains undefined values, false otherwise
|
|
3563
3127
|
*/
|
|
3564
|
-
|
|
3565
|
-
|
|
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
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
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
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
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
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
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
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
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
|
-
|
|
3719
|
-
|
|
3720
|
-
};
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
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
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
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 (
|
|
3762
|
-
|
|
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
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
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
|
-
}
|
|
3775
|
-
|
|
3776
|
-
|
|
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
|
-
|
|
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/
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
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
|
-
*
|
|
3796
|
+
* Creates a deep clone of this ScanBuilder instance.
|
|
3797
|
+
*
|
|
3798
|
+
* @returns A new ScanBuilder instance with the same configuration
|
|
3824
3799
|
*/
|
|
3825
|
-
|
|
3826
|
-
|
|
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
|
-
*
|
|
3820
|
+
* Executes the scan against DynamoDB and returns a generator that behaves like an array.
|
|
3830
3821
|
*
|
|
3831
|
-
*
|
|
3832
|
-
*
|
|
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
|
-
*
|
|
3838
|
-
*
|
|
3839
|
-
*
|
|
3840
|
-
*
|
|
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
|
-
*
|
|
3843
|
-
*
|
|
3844
|
-
*
|
|
3845
|
-
*
|
|
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
|
-
*
|
|
3848
|
-
*
|
|
3849
|
-
* .
|
|
3850
|
-
* .
|
|
3851
|
-
*
|
|
3852
|
-
* .
|
|
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
|
-
* @
|
|
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
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
3873
|
-
return [...this.forceRebuildIndexes];
|
|
3874
|
-
}
|
|
3879
|
+
partitionKey;
|
|
3875
3880
|
/**
|
|
3876
|
-
*
|
|
3877
|
-
* This is called automatically when needed
|
|
3881
|
+
* The column name of the sortKey for the Table
|
|
3878
3882
|
*/
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
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
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
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.
|
|
3901
|
+
primaryCondition[this.sortKey] = keyCondition.sk;
|
|
3901
3902
|
}
|
|
3902
|
-
return
|
|
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
|
|
3906
|
+
* Creates a new item in the table, it will fail if the item already exists.
|
|
3948
3907
|
*
|
|
3949
|
-
*
|
|
3950
|
-
*
|
|
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
|
-
|
|
3953
|
-
this.
|
|
3954
|
-
|
|
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
|
-
*
|
|
3960
|
+
* Updates an item in the table
|
|
3958
3961
|
*
|
|
3959
|
-
* @param item
|
|
3960
|
-
* @
|
|
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
|
-
|
|
3964
|
-
const
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
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
|
|
3997
|
+
};
|
|
3998
|
+
return new PutBuilder(executor, item, this.tableName);
|
|
3982
3999
|
}
|
|
3983
4000
|
/**
|
|
3984
|
-
*
|
|
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
|
-
|
|
3992
|
-
const
|
|
3993
|
-
const
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
if (
|
|
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
|
-
|
|
4003
|
-
|
|
4004
|
-
if (
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
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 (!
|
|
4019
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4105
|
+
const result = await this.dynamoClient.query(params);
|
|
4106
|
+
return {
|
|
4107
|
+
items: result.Items,
|
|
4108
|
+
lastEvaluatedKey: result.LastEvaluatedKey
|
|
4109
|
+
};
|
|
4025
4110
|
} catch (error) {
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
}
|
|
4111
|
+
console.log(debugCommand(params));
|
|
4112
|
+
console.error("Error querying items:", error);
|
|
4029
4113
|
throw error;
|
|
4030
4114
|
}
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
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
|
|
4037
|
-
|
|
4038
|
-
|
|
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
|
-
|
|
4041
|
-
|
|
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
|
-
|
|
4044
|
-
|
|
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
|
|
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
|
-
*
|
|
4228
|
+
* Creates a batch builder for performing multiple operations efficiently with optional type inference
|
|
4051
4229
|
*
|
|
4052
|
-
* @
|
|
4053
|
-
*
|
|
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
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
};
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
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
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
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
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
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
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
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
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
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
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
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 (
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
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
|
-
|
|
4320
|
-
};
|
|
4321
|
-
|
|
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
|
-
|
|
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) {
|