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