s3db.js 11.2.6 → 11.3.2

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.
@@ -498,862 +498,2120 @@ class S3dbMCPServer {
498
498
  },
499
499
  required: []
500
500
  }
501
+ },
502
+ // 🔍 DEBUGGING TOOLS
503
+ {
504
+ name: 'dbInspectResource',
505
+ description: 'Inspect detailed information about a resource including schema, partitions, behaviors, and configuration',
506
+ inputSchema: {
507
+ type: 'object',
508
+ properties: {
509
+ resourceName: {
510
+ type: 'string',
511
+ description: 'Name of the resource to inspect'
512
+ }
513
+ },
514
+ required: ['resourceName']
515
+ }
516
+ },
517
+ {
518
+ name: 'dbGetMetadata',
519
+ description: 'Get raw metadata.json from the S3 bucket for debugging',
520
+ inputSchema: {
521
+ type: 'object',
522
+ properties: {},
523
+ required: []
524
+ }
525
+ },
526
+ {
527
+ name: 'resourceValidate',
528
+ description: 'Validate data against resource schema without inserting',
529
+ inputSchema: {
530
+ type: 'object',
531
+ properties: {
532
+ resourceName: {
533
+ type: 'string',
534
+ description: 'Name of the resource'
535
+ },
536
+ data: {
537
+ type: 'object',
538
+ description: 'Data to validate'
539
+ }
540
+ },
541
+ required: ['resourceName', 'data']
542
+ }
543
+ },
544
+ {
545
+ name: 'dbHealthCheck',
546
+ description: 'Perform comprehensive health check on database including orphaned partitions detection',
547
+ inputSchema: {
548
+ type: 'object',
549
+ properties: {
550
+ includeOrphanedPartitions: {
551
+ type: 'boolean',
552
+ description: 'Include orphaned partitions check',
553
+ default: true
554
+ }
555
+ },
556
+ required: []
557
+ }
558
+ },
559
+ {
560
+ name: 'resourceGetRaw',
561
+ description: 'Get raw S3 object data (metadata + body) for debugging',
562
+ inputSchema: {
563
+ type: 'object',
564
+ properties: {
565
+ resourceName: {
566
+ type: 'string',
567
+ description: 'Name of the resource'
568
+ },
569
+ id: {
570
+ type: 'string',
571
+ description: 'Document ID'
572
+ }
573
+ },
574
+ required: ['resourceName', 'id']
575
+ }
576
+ },
577
+ // 📊 QUERY & FILTERING TOOLS
578
+ {
579
+ name: 'resourceQuery',
580
+ description: 'Query documents with complex filters and conditions',
581
+ inputSchema: {
582
+ type: 'object',
583
+ properties: {
584
+ resourceName: {
585
+ type: 'string',
586
+ description: 'Name of the resource'
587
+ },
588
+ filters: {
589
+ type: 'object',
590
+ description: 'Query filters (e.g., {status: "active", age: {$gt: 18}})'
591
+ },
592
+ limit: {
593
+ type: 'number',
594
+ description: 'Maximum number of results',
595
+ default: 100
596
+ },
597
+ offset: {
598
+ type: 'number',
599
+ description: 'Number of results to skip',
600
+ default: 0
601
+ }
602
+ },
603
+ required: ['resourceName', 'filters']
604
+ }
605
+ },
606
+ {
607
+ name: 'resourceSearch',
608
+ description: 'Search for documents by text in specific fields',
609
+ inputSchema: {
610
+ type: 'object',
611
+ properties: {
612
+ resourceName: {
613
+ type: 'string',
614
+ description: 'Name of the resource'
615
+ },
616
+ searchText: {
617
+ type: 'string',
618
+ description: 'Text to search for'
619
+ },
620
+ fields: {
621
+ type: 'array',
622
+ items: { type: 'string' },
623
+ description: 'Fields to search in (if not specified, searches all string fields)'
624
+ },
625
+ caseSensitive: {
626
+ type: 'boolean',
627
+ description: 'Case-sensitive search',
628
+ default: false
629
+ },
630
+ limit: {
631
+ type: 'number',
632
+ description: 'Maximum number of results',
633
+ default: 100
634
+ }
635
+ },
636
+ required: ['resourceName', 'searchText']
637
+ }
638
+ },
639
+ // 🔧 PARTITION MANAGEMENT TOOLS
640
+ {
641
+ name: 'resourceListPartitions',
642
+ description: 'List all partitions defined for a resource',
643
+ inputSchema: {
644
+ type: 'object',
645
+ properties: {
646
+ resourceName: {
647
+ type: 'string',
648
+ description: 'Name of the resource'
649
+ }
650
+ },
651
+ required: ['resourceName']
652
+ }
653
+ },
654
+ {
655
+ name: 'resourceListPartitionValues',
656
+ description: 'List unique values for a specific partition field',
657
+ inputSchema: {
658
+ type: 'object',
659
+ properties: {
660
+ resourceName: {
661
+ type: 'string',
662
+ description: 'Name of the resource'
663
+ },
664
+ partitionName: {
665
+ type: 'string',
666
+ description: 'Name of the partition'
667
+ },
668
+ limit: {
669
+ type: 'number',
670
+ description: 'Maximum number of values to return',
671
+ default: 1000
672
+ }
673
+ },
674
+ required: ['resourceName', 'partitionName']
675
+ }
676
+ },
677
+ {
678
+ name: 'dbFindOrphanedPartitions',
679
+ description: 'Find partitions that reference fields no longer in the schema',
680
+ inputSchema: {
681
+ type: 'object',
682
+ properties: {
683
+ resourceName: {
684
+ type: 'string',
685
+ description: 'Name of specific resource to check (optional - checks all if not provided)'
686
+ }
687
+ },
688
+ required: []
689
+ }
690
+ },
691
+ {
692
+ name: 'dbRemoveOrphanedPartitions',
693
+ description: 'Remove orphaned partitions from resource configuration',
694
+ inputSchema: {
695
+ type: 'object',
696
+ properties: {
697
+ resourceName: {
698
+ type: 'string',
699
+ description: 'Name of the resource'
700
+ },
701
+ dryRun: {
702
+ type: 'boolean',
703
+ description: 'Preview changes without applying them',
704
+ default: true
705
+ }
706
+ },
707
+ required: ['resourceName']
708
+ }
709
+ },
710
+ // 🚀 BULK OPERATIONS TOOLS
711
+ {
712
+ name: 'resourceUpdateMany',
713
+ description: 'Update multiple documents matching a query filter',
714
+ inputSchema: {
715
+ type: 'object',
716
+ properties: {
717
+ resourceName: {
718
+ type: 'string',
719
+ description: 'Name of the resource'
720
+ },
721
+ filters: {
722
+ type: 'object',
723
+ description: 'Query filters to select documents'
724
+ },
725
+ updates: {
726
+ type: 'object',
727
+ description: 'Updates to apply to matching documents'
728
+ },
729
+ limit: {
730
+ type: 'number',
731
+ description: 'Maximum number of documents to update',
732
+ default: 1000
733
+ }
734
+ },
735
+ required: ['resourceName', 'filters', 'updates']
736
+ }
737
+ },
738
+ {
739
+ name: 'resourceBulkUpsert',
740
+ description: 'Upsert multiple documents (insert if not exists, update if exists)',
741
+ inputSchema: {
742
+ type: 'object',
743
+ properties: {
744
+ resourceName: {
745
+ type: 'string',
746
+ description: 'Name of the resource'
747
+ },
748
+ data: {
749
+ type: 'array',
750
+ description: 'Array of documents to upsert (must include id field)'
751
+ }
752
+ },
753
+ required: ['resourceName', 'data']
754
+ }
755
+ },
756
+ // 💾 EXPORT/IMPORT TOOLS
757
+ {
758
+ name: 'resourceExport',
759
+ description: 'Export resource data to JSON, CSV, or NDJSON format',
760
+ inputSchema: {
761
+ type: 'object',
762
+ properties: {
763
+ resourceName: {
764
+ type: 'string',
765
+ description: 'Name of the resource'
766
+ },
767
+ format: {
768
+ type: 'string',
769
+ description: 'Export format',
770
+ enum: ['json', 'ndjson', 'csv'],
771
+ default: 'json'
772
+ },
773
+ filters: {
774
+ type: 'object',
775
+ description: 'Optional filters to export subset of data'
776
+ },
777
+ fields: {
778
+ type: 'array',
779
+ items: { type: 'string' },
780
+ description: 'Specific fields to export (exports all if not specified)'
781
+ },
782
+ limit: {
783
+ type: 'number',
784
+ description: 'Maximum number of records to export'
785
+ }
786
+ },
787
+ required: ['resourceName']
788
+ }
789
+ },
790
+ {
791
+ name: 'resourceImport',
792
+ description: 'Import data from JSON or NDJSON format into a resource',
793
+ inputSchema: {
794
+ type: 'object',
795
+ properties: {
796
+ resourceName: {
797
+ type: 'string',
798
+ description: 'Name of the resource'
799
+ },
800
+ data: {
801
+ type: 'array',
802
+ description: 'Array of documents to import'
803
+ },
804
+ mode: {
805
+ type: 'string',
806
+ description: 'Import mode',
807
+ enum: ['insert', 'upsert', 'replace'],
808
+ default: 'insert'
809
+ },
810
+ batchSize: {
811
+ type: 'number',
812
+ description: 'Batch size for bulk operations',
813
+ default: 100
814
+ }
815
+ },
816
+ required: ['resourceName', 'data']
817
+ }
818
+ },
819
+ {
820
+ name: 'dbBackupMetadata',
821
+ description: 'Create a backup of the metadata.json file',
822
+ inputSchema: {
823
+ type: 'object',
824
+ properties: {
825
+ timestamp: {
826
+ type: 'boolean',
827
+ description: 'Include timestamp in backup name',
828
+ default: true
829
+ }
830
+ },
831
+ required: []
832
+ }
833
+ },
834
+ // 📈 ENHANCED STATS TOOLS
835
+ {
836
+ name: 'resourceGetStats',
837
+ description: 'Get detailed statistics for a specific resource',
838
+ inputSchema: {
839
+ type: 'object',
840
+ properties: {
841
+ resourceName: {
842
+ type: 'string',
843
+ description: 'Name of the resource'
844
+ },
845
+ includePartitionStats: {
846
+ type: 'boolean',
847
+ description: 'Include partition statistics',
848
+ default: true
849
+ }
850
+ },
851
+ required: ['resourceName']
852
+ }
853
+ },
854
+ {
855
+ name: 'cacheGetStats',
856
+ description: 'Get detailed cache statistics including hit/miss ratios and memory usage',
857
+ inputSchema: {
858
+ type: 'object',
859
+ properties: {
860
+ resourceName: {
861
+ type: 'string',
862
+ description: 'Get stats for specific resource (optional - gets all if not provided)'
863
+ }
864
+ },
865
+ required: []
866
+ }
501
867
  }
502
868
  ]
503
869
  };
504
870
  });
505
871
 
506
- // Handle tool calls
507
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
508
- const { name, arguments: args } = request.params;
872
+ // Handle tool calls
873
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
874
+ const { name, arguments: args } = request.params;
875
+
876
+ try {
877
+ let result;
878
+
879
+ switch (name) {
880
+ case 'dbConnect':
881
+ result = await this.handleDbConnect(args);
882
+ break;
883
+
884
+ case 'dbDisconnect':
885
+ result = await this.handleDbDisconnect(args);
886
+ break;
887
+
888
+ case 'dbStatus':
889
+ result = await this.handleDbStatus(args);
890
+ break;
891
+
892
+ case 'dbCreateResource':
893
+ result = await this.handleDbCreateResource(args);
894
+ break;
895
+
896
+ case 'dbListResources':
897
+ result = await this.handleDbListResources(args);
898
+ break;
899
+
900
+ case 'resourceInsert':
901
+ result = await this.handleResourceInsert(args);
902
+ break;
903
+
904
+ case 'resourceInsertMany':
905
+ result = await this.handleResourceInsertMany(args);
906
+ break;
907
+
908
+ case 'resourceGet':
909
+ result = await this.handleResourceGet(args);
910
+ break;
911
+
912
+ case 'resourceGetMany':
913
+ result = await this.handleResourceGetMany(args);
914
+ break;
915
+
916
+ case 'resourceUpdate':
917
+ result = await this.handleResourceUpdate(args);
918
+ break;
919
+
920
+ case 'resourceUpsert':
921
+ result = await this.handleResourceUpsert(args);
922
+ break;
923
+
924
+ case 'resourceDelete':
925
+ result = await this.handleResourceDelete(args);
926
+ break;
927
+
928
+ case 'resourceDeleteMany':
929
+ result = await this.handleResourceDeleteMany(args);
930
+ break;
931
+
932
+ case 'resourceExists':
933
+ result = await this.handleResourceExists(args);
934
+ break;
935
+
936
+ case 'resourceList':
937
+ result = await this.handleResourceList(args);
938
+ break;
939
+
940
+ case 'resourceListIds':
941
+ result = await this.handleResourceListIds(args);
942
+ break;
943
+
944
+ case 'resourceCount':
945
+ result = await this.handleResourceCount(args);
946
+ break;
947
+
948
+ case 'resourceGetAll':
949
+ result = await this.handleResourceGetAll(args);
950
+ break;
951
+
952
+ case 'resourceDeleteAll':
953
+ result = await this.handleResourceDeleteAll(args);
954
+ break;
955
+
956
+ case 'dbGetStats':
957
+ result = await this.handleDbGetStats(args);
958
+ break;
959
+
960
+ case 'dbClearCache':
961
+ result = await this.handleDbClearCache(args);
962
+ break;
963
+
964
+ // Debugging tools
965
+ case 'dbInspectResource':
966
+ result = await this.handleDbInspectResource(args);
967
+ break;
968
+
969
+ case 'dbGetMetadata':
970
+ result = await this.handleDbGetMetadata(args);
971
+ break;
972
+
973
+ case 'resourceValidate':
974
+ result = await this.handleResourceValidate(args);
975
+ break;
976
+
977
+ case 'dbHealthCheck':
978
+ result = await this.handleDbHealthCheck(args);
979
+ break;
980
+
981
+ case 'resourceGetRaw':
982
+ result = await this.handleResourceGetRaw(args);
983
+ break;
984
+
985
+ // Query & filtering tools
986
+ case 'resourceQuery':
987
+ result = await this.handleResourceQuery(args);
988
+ break;
989
+
990
+ case 'resourceSearch':
991
+ result = await this.handleResourceSearch(args);
992
+ break;
993
+
994
+ // Partition management tools
995
+ case 'resourceListPartitions':
996
+ result = await this.handleResourceListPartitions(args);
997
+ break;
998
+
999
+ case 'resourceListPartitionValues':
1000
+ result = await this.handleResourceListPartitionValues(args);
1001
+ break;
1002
+
1003
+ case 'dbFindOrphanedPartitions':
1004
+ result = await this.handleDbFindOrphanedPartitions(args);
1005
+ break;
1006
+
1007
+ case 'dbRemoveOrphanedPartitions':
1008
+ result = await this.handleDbRemoveOrphanedPartitions(args);
1009
+ break;
1010
+
1011
+ // Bulk operations tools
1012
+ case 'resourceUpdateMany':
1013
+ result = await this.handleResourceUpdateMany(args);
1014
+ break;
1015
+
1016
+ case 'resourceBulkUpsert':
1017
+ result = await this.handleResourceBulkUpsert(args);
1018
+ break;
1019
+
1020
+ // Export/import tools
1021
+ case 'resourceExport':
1022
+ result = await this.handleResourceExport(args);
1023
+ break;
1024
+
1025
+ case 'resourceImport':
1026
+ result = await this.handleResourceImport(args);
1027
+ break;
1028
+
1029
+ case 'dbBackupMetadata':
1030
+ result = await this.handleDbBackupMetadata(args);
1031
+ break;
1032
+
1033
+ // Enhanced stats tools
1034
+ case 'resourceGetStats':
1035
+ result = await this.handleResourceGetStats(args);
1036
+ break;
1037
+
1038
+ case 'cacheGetStats':
1039
+ result = await this.handleCacheGetStats(args);
1040
+ break;
1041
+
1042
+ default:
1043
+ throw new Error(`Unknown tool: ${name}`);
1044
+ }
1045
+
1046
+ return {
1047
+ content: [
1048
+ {
1049
+ type: 'text',
1050
+ text: JSON.stringify(result, null, 2)
1051
+ }
1052
+ ]
1053
+ };
1054
+
1055
+ } catch (error) {
1056
+ return {
1057
+ content: [
1058
+ {
1059
+ type: 'text',
1060
+ text: JSON.stringify({
1061
+ error: error.message,
1062
+ type: error.constructor.name,
1063
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
1064
+ }, null, 2)
1065
+ }
1066
+ ],
1067
+ isError: true
1068
+ };
1069
+ }
1070
+ });
1071
+ }
1072
+
1073
+ setupTransport() {
1074
+ const transport = process.argv.includes('--transport=sse') || process.env.MCP_TRANSPORT === 'sse'
1075
+ ? new SSEServerTransport('/sse', process.env.MCP_SERVER_HOST || '0.0.0.0', parseInt(process.env.MCP_SERVER_PORT || '17500'))
1076
+ : new StdioServerTransport();
1077
+
1078
+ this.server.connect(transport);
1079
+
1080
+ // SSE specific setup
1081
+ if (transport instanceof SSEServerTransport) {
1082
+ const host = process.env.MCP_SERVER_HOST || '0.0.0.0';
1083
+ const port = process.env.MCP_SERVER_PORT || '17500';
1084
+
1085
+ console.log(`S3DB MCP Server running on http://${host}:${port}/sse`);
1086
+
1087
+ // Add health check endpoint for SSE transport
1088
+ this.setupHealthCheck(host, port);
1089
+ }
1090
+ }
1091
+
1092
+ setupHealthCheck(host, port) {
1093
+ import('http').then(({ createServer }) => {
1094
+ const healthServer = createServer((req, res) => {
1095
+ if (req.url === '/health') {
1096
+ const healthStatus = {
1097
+ status: 'healthy',
1098
+ timestamp: new Date().toISOString(),
1099
+ uptime: process.uptime(),
1100
+ version: SERVER_VERSION,
1101
+ database: {
1102
+ connected: database ? database.isConnected() : false,
1103
+ bucket: database?.bucket || null,
1104
+ keyPrefix: database?.keyPrefix || null,
1105
+ resourceCount: database ? Object.keys(database.resources || {}).length : 0
1106
+ },
1107
+ memory: process.memoryUsage(),
1108
+ environment: {
1109
+ nodeVersion: process.version,
1110
+ platform: process.platform,
1111
+ transport: 'sse'
1112
+ }
1113
+ };
1114
+
1115
+ res.writeHead(200, {
1116
+ 'Content-Type': 'application/json',
1117
+ 'Access-Control-Allow-Origin': '*',
1118
+ 'Access-Control-Allow-Methods': 'GET',
1119
+ 'Access-Control-Allow-Headers': 'Content-Type'
1120
+ });
1121
+ res.end(JSON.stringify(healthStatus, null, 2));
1122
+ } else {
1123
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
1124
+ res.end('Not Found');
1125
+ }
1126
+ });
1127
+
1128
+ // Listen on a different port for health checks to avoid conflicts
1129
+ const healthPort = parseInt(port) + 1;
1130
+ healthServer.listen(healthPort, host, () => {
1131
+ console.log(`Health check endpoint: http://${host}:${healthPort}/health`);
1132
+ });
1133
+ }).catch(err => {
1134
+ console.warn('Could not setup health check endpoint:', err.message);
1135
+ });
1136
+ }
1137
+
1138
+ // Database connection handlers
1139
+ async handleDbConnect(args) {
1140
+ const {
1141
+ connectionString,
1142
+ verbose = false,
1143
+ parallelism = 10,
1144
+ passphrase = 'secret',
1145
+ versioningEnabled = false,
1146
+ enableCache = true,
1147
+ enableCosts = true,
1148
+ cacheDriver = 'memory', // 'memory', 'filesystem', or 'custom'
1149
+ cacheMaxSize = 1000,
1150
+ cacheTtl = 300000, // 5 minutes
1151
+ cacheDirectory = './cache', // For filesystem cache
1152
+ cachePrefix = 'cache'
1153
+ } = args;
1154
+
1155
+ if (database && database.isConnected()) {
1156
+ return { success: false, message: 'Database is already connected' };
1157
+ }
1158
+
1159
+ // Setup plugins array
1160
+ const plugins = [];
1161
+
1162
+ // Always add CostsPlugin (unless explicitly disabled)
1163
+ const costsEnabled = enableCosts !== false && process.env.S3DB_COSTS_ENABLED !== 'false';
1164
+ if (costsEnabled) {
1165
+ plugins.push(CostsPlugin);
1166
+ }
1167
+
1168
+ // Add CachePlugin (enabled by default, configurable)
1169
+ const cacheEnabled = enableCache !== false && process.env.S3DB_CACHE_ENABLED !== 'false';
1170
+
1171
+ // Declare cache variables in outer scope to avoid reference errors
1172
+ let cacheMaxSizeEnv, cacheTtlEnv, cacheDriverEnv, cacheDirectoryEnv, cachePrefixEnv;
1173
+
1174
+ if (cacheEnabled) {
1175
+ cacheMaxSizeEnv = process.env.S3DB_CACHE_MAX_SIZE ? parseInt(process.env.S3DB_CACHE_MAX_SIZE) : cacheMaxSize;
1176
+ cacheTtlEnv = process.env.S3DB_CACHE_TTL ? parseInt(process.env.S3DB_CACHE_TTL) : cacheTtl;
1177
+ cacheDriverEnv = process.env.S3DB_CACHE_DRIVER || cacheDriver;
1178
+ cacheDirectoryEnv = process.env.S3DB_CACHE_DIRECTORY || cacheDirectory;
1179
+ cachePrefixEnv = process.env.S3DB_CACHE_PREFIX || cachePrefix;
1180
+
1181
+ let cacheConfig = {
1182
+ includePartitions: true
1183
+ };
1184
+
1185
+ if (cacheDriverEnv === 'filesystem') {
1186
+ // Filesystem cache configuration
1187
+ cacheConfig.driver = new FilesystemCache({
1188
+ directory: cacheDirectoryEnv,
1189
+ prefix: cachePrefixEnv,
1190
+ ttl: cacheTtlEnv,
1191
+ enableCompression: true,
1192
+ enableStats: verbose,
1193
+ enableCleanup: true,
1194
+ cleanupInterval: 300000, // 5 minutes
1195
+ createDirectory: true
1196
+ });
1197
+ } else {
1198
+ // Memory cache configuration (default)
1199
+ cacheConfig.driver = 'memory';
1200
+ cacheConfig.memoryOptions = {
1201
+ maxSize: cacheMaxSizeEnv,
1202
+ ttl: cacheTtlEnv,
1203
+ enableStats: verbose
1204
+ };
1205
+ }
1206
+
1207
+ plugins.push(new CachePlugin(cacheConfig));
1208
+ }
1209
+
1210
+ database = new S3db({
1211
+ connectionString,
1212
+ verbose,
1213
+ parallelism,
1214
+ passphrase,
1215
+ versioningEnabled,
1216
+ plugins
1217
+ });
1218
+
1219
+ await database.connect();
509
1220
 
510
- try {
511
- let result;
1221
+ return {
1222
+ success: true,
1223
+ message: 'Connected to S3DB database',
1224
+ status: {
1225
+ connected: database.isConnected(),
1226
+ bucket: database.bucket,
1227
+ keyPrefix: database.keyPrefix,
1228
+ version: database.s3dbVersion,
1229
+ plugins: {
1230
+ costs: costsEnabled,
1231
+ cache: cacheEnabled,
1232
+ cacheDriver: cacheEnabled ? cacheDriverEnv : null,
1233
+ cacheDirectory: cacheEnabled && cacheDriverEnv === 'filesystem' ? cacheDirectoryEnv : null,
1234
+ cacheMaxSize: cacheEnabled && cacheDriverEnv === 'memory' ? cacheMaxSizeEnv : null,
1235
+ cacheTtl: cacheEnabled ? cacheTtlEnv : null
1236
+ }
1237
+ }
1238
+ };
1239
+ }
512
1240
 
513
- switch (name) {
514
- case 'dbConnect':
515
- result = await this.handleDbConnect(args);
516
- break;
1241
+ async handleDbDisconnect(args) {
1242
+ if (!database || !database.isConnected()) {
1243
+ return { success: false, message: 'No database connection to disconnect' };
1244
+ }
517
1245
 
518
- case 'dbDisconnect':
519
- result = await this.handleDbDisconnect(args);
520
- break;
1246
+ await database.disconnect();
1247
+ database = null;
521
1248
 
522
- case 'dbStatus':
523
- result = await this.handleDbStatus(args);
524
- break;
1249
+ return {
1250
+ success: true,
1251
+ message: 'Disconnected from S3DB database'
1252
+ };
1253
+ }
525
1254
 
526
- case 'dbCreateResource':
527
- result = await this.handleDbCreateResource(args);
528
- break;
1255
+ async handleDbStatus(args) {
1256
+ if (!database) {
1257
+ return {
1258
+ connected: false,
1259
+ message: 'No database instance created'
1260
+ };
1261
+ }
529
1262
 
530
- case 'dbListResources':
531
- result = await this.handleDbListResources(args);
532
- break;
1263
+ return {
1264
+ connected: database.isConnected(),
1265
+ bucket: database.bucket,
1266
+ keyPrefix: database.keyPrefix,
1267
+ version: database.s3dbVersion,
1268
+ resourceCount: Object.keys(database.resources || {}).length,
1269
+ resources: Object.keys(database.resources || {})
1270
+ };
1271
+ }
533
1272
 
534
- case 'resourceInsert':
535
- result = await this.handleResourceInsert(args);
536
- break;
1273
+ async handleDbCreateResource(args) {
1274
+ this.ensureConnected();
1275
+
1276
+ const { name, attributes, behavior = 'user-managed', timestamps = false, partitions, paranoid = true } = args;
537
1277
 
538
- case 'resourceInsertMany':
539
- result = await this.handleResourceInsertMany(args);
540
- break;
1278
+ const resource = await database.createResource({
1279
+ name,
1280
+ attributes,
1281
+ behavior,
1282
+ timestamps,
1283
+ partitions,
1284
+ paranoid
1285
+ });
541
1286
 
542
- case 'resourceGet':
543
- result = await this.handleResourceGet(args);
544
- break;
1287
+ return {
1288
+ success: true,
1289
+ resource: {
1290
+ name: resource.name,
1291
+ behavior: resource.behavior,
1292
+ attributes: resource.attributes,
1293
+ partitions: resource.config.partitions,
1294
+ timestamps: resource.config.timestamps
1295
+ }
1296
+ };
1297
+ }
1298
+
1299
+ async handleDbListResources(args) {
1300
+ this.ensureConnected();
1301
+
1302
+ const resourceList = await database.listResources();
1303
+
1304
+ return {
1305
+ success: true,
1306
+ resources: resourceList,
1307
+ count: resourceList.length
1308
+ };
1309
+ }
1310
+
1311
+ // Resource operation handlers
1312
+ async handleResourceInsert(args) {
1313
+ this.ensureConnected();
1314
+ const { resourceName, data } = args;
1315
+
1316
+ const resource = this.getResource(resourceName);
1317
+ const result = await resource.insert(data);
1318
+
1319
+ // Extract partition information for cache invalidation
1320
+ const partitionInfo = this._extractPartitionInfo(resource, result);
1321
+
1322
+ // Generate cache invalidation patterns
1323
+ const cacheInvalidationPatterns = this._generateCacheInvalidationPatterns(resource, result, 'insert');
1324
+
1325
+ return {
1326
+ success: true,
1327
+ data: result,
1328
+ ...(partitionInfo && { partitionInfo }),
1329
+ cacheInvalidationPatterns
1330
+ };
1331
+ }
1332
+
1333
+ async handleResourceInsertMany(args) {
1334
+ this.ensureConnected();
1335
+ const { resourceName, data } = args;
1336
+
1337
+ const resource = this.getResource(resourceName);
1338
+ const result = await resource.insertMany(data);
1339
+
1340
+ return {
1341
+ success: true,
1342
+ data: result,
1343
+ count: result.length
1344
+ };
1345
+ }
1346
+
1347
+ async handleResourceGet(args) {
1348
+ this.ensureConnected();
1349
+ const { resourceName, id, partition, partitionValues } = args;
1350
+
1351
+ const resource = this.getResource(resourceName);
1352
+
1353
+ // Use partition information for optimized retrieval if provided
1354
+ let options = {};
1355
+ if (partition && partitionValues) {
1356
+ options.partition = partition;
1357
+ options.partitionValues = partitionValues;
1358
+ }
1359
+
1360
+ const result = await resource.get(id, options);
1361
+
1362
+ // Extract partition information from result
1363
+ const partitionInfo = this._extractPartitionInfo(resource, result);
1364
+
1365
+ return {
1366
+ success: true,
1367
+ data: result,
1368
+ ...(partitionInfo && { partitionInfo })
1369
+ };
1370
+ }
1371
+
1372
+ async handleResourceGetMany(args) {
1373
+ this.ensureConnected();
1374
+ const { resourceName, ids } = args;
1375
+
1376
+ const resource = this.getResource(resourceName);
1377
+ const result = await resource.getMany(ids);
1378
+
1379
+ return {
1380
+ success: true,
1381
+ data: result,
1382
+ count: result.length
1383
+ };
1384
+ }
545
1385
 
546
- case 'resourceGetMany':
547
- result = await this.handleResourceGetMany(args);
548
- break;
1386
+ async handleResourceUpdate(args) {
1387
+ this.ensureConnected();
1388
+ const { resourceName, id, data } = args;
1389
+
1390
+ const resource = this.getResource(resourceName);
1391
+ const result = await resource.update(id, data);
1392
+
1393
+ // Extract partition information for cache invalidation
1394
+ const partitionInfo = this._extractPartitionInfo(resource, result);
1395
+
1396
+ return {
1397
+ success: true,
1398
+ data: result,
1399
+ ...(partitionInfo && { partitionInfo })
1400
+ };
1401
+ }
549
1402
 
550
- case 'resourceUpdate':
551
- result = await this.handleResourceUpdate(args);
552
- break;
1403
+ async handleResourceUpsert(args) {
1404
+ this.ensureConnected();
1405
+ const { resourceName, data } = args;
1406
+
1407
+ const resource = this.getResource(resourceName);
1408
+ const result = await resource.upsert(data);
1409
+
1410
+ return {
1411
+ success: true,
1412
+ data: result
1413
+ };
1414
+ }
553
1415
 
554
- case 'resourceUpsert':
555
- result = await this.handleResourceUpsert(args);
556
- break;
1416
+ async handleResourceDelete(args) {
1417
+ this.ensureConnected();
1418
+ const { resourceName, id } = args;
1419
+
1420
+ const resource = this.getResource(resourceName);
1421
+ await resource.delete(id);
1422
+
1423
+ return {
1424
+ success: true,
1425
+ message: `Document ${id} deleted from ${resourceName}`
1426
+ };
1427
+ }
557
1428
 
558
- case 'resourceDelete':
559
- result = await this.handleResourceDelete(args);
560
- break;
1429
+ async handleResourceDeleteMany(args) {
1430
+ this.ensureConnected();
1431
+ const { resourceName, ids } = args;
1432
+
1433
+ const resource = this.getResource(resourceName);
1434
+ await resource.deleteMany(ids);
1435
+
1436
+ return {
1437
+ success: true,
1438
+ message: `${ids.length} documents deleted from ${resourceName}`,
1439
+ deletedIds: ids
1440
+ };
1441
+ }
561
1442
 
562
- case 'resourceDeleteMany':
563
- result = await this.handleResourceDeleteMany(args);
564
- break;
1443
+ async handleResourceExists(args) {
1444
+ this.ensureConnected();
1445
+ const { resourceName, id, partition, partitionValues } = args;
1446
+
1447
+ const resource = this.getResource(resourceName);
1448
+
1449
+ // Use partition information for optimized existence check if provided
1450
+ let options = {};
1451
+ if (partition && partitionValues) {
1452
+ options.partition = partition;
1453
+ options.partitionValues = partitionValues;
1454
+ }
1455
+
1456
+ const exists = await resource.exists(id, options);
1457
+
1458
+ return {
1459
+ success: true,
1460
+ exists,
1461
+ id,
1462
+ resource: resourceName,
1463
+ ...(partition && { partition }),
1464
+ ...(partitionValues && { partitionValues })
1465
+ };
1466
+ }
565
1467
 
566
- case 'resourceExists':
567
- result = await this.handleResourceExists(args);
568
- break;
1468
+ async handleResourceList(args) {
1469
+ this.ensureConnected();
1470
+ const { resourceName, limit = 100, offset = 0, partition, partitionValues } = args;
1471
+
1472
+ const resource = this.getResource(resourceName);
1473
+ const options = { limit, offset };
1474
+
1475
+ if (partition && partitionValues) {
1476
+ options.partition = partition;
1477
+ options.partitionValues = partitionValues;
1478
+ }
1479
+
1480
+ const result = await resource.list(options);
1481
+
1482
+ // Generate cache key hint for intelligent caching
1483
+ const cacheKeyHint = this._generateCacheKeyHint(resourceName, 'list', {
1484
+ limit,
1485
+ offset,
1486
+ partition,
1487
+ partitionValues
1488
+ });
1489
+
1490
+ return {
1491
+ success: true,
1492
+ data: result,
1493
+ count: result.length,
1494
+ pagination: {
1495
+ limit,
1496
+ offset,
1497
+ hasMore: result.length === limit
1498
+ },
1499
+ cacheKeyHint,
1500
+ ...(partition && { partition }),
1501
+ ...(partitionValues && { partitionValues })
1502
+ };
1503
+ }
569
1504
 
570
- case 'resourceList':
571
- result = await this.handleResourceList(args);
572
- break;
1505
+ async handleResourceListIds(args) {
1506
+ this.ensureConnected();
1507
+ const { resourceName, limit = 1000, offset = 0 } = args;
1508
+
1509
+ const resource = this.getResource(resourceName);
1510
+ const result = await resource.listIds({ limit, offset });
1511
+
1512
+ return {
1513
+ success: true,
1514
+ ids: result,
1515
+ count: result.length,
1516
+ pagination: {
1517
+ limit,
1518
+ offset,
1519
+ hasMore: result.length === limit
1520
+ }
1521
+ };
1522
+ }
573
1523
 
574
- case 'resourceListIds':
575
- result = await this.handleResourceListIds(args);
576
- break;
1524
+ async handleResourceCount(args) {
1525
+ this.ensureConnected();
1526
+ const { resourceName, partition, partitionValues } = args;
1527
+
1528
+ const resource = this.getResource(resourceName);
1529
+ const options = {};
1530
+
1531
+ if (partition && partitionValues) {
1532
+ options.partition = partition;
1533
+ options.partitionValues = partitionValues;
1534
+ }
1535
+
1536
+ const count = await resource.count(options);
1537
+
1538
+ // Generate cache key hint for intelligent caching
1539
+ const cacheKeyHint = this._generateCacheKeyHint(resourceName, 'count', {
1540
+ partition,
1541
+ partitionValues
1542
+ });
1543
+
1544
+ return {
1545
+ success: true,
1546
+ count,
1547
+ resource: resourceName,
1548
+ cacheKeyHint,
1549
+ ...(partition && { partition }),
1550
+ ...(partitionValues && { partitionValues })
1551
+ };
1552
+ }
577
1553
 
578
- case 'resourceCount':
579
- result = await this.handleResourceCount(args);
580
- break;
1554
+ async handleResourceGetAll(args) {
1555
+ this.ensureConnected();
1556
+ const { resourceName } = args;
1557
+
1558
+ const resource = this.getResource(resourceName);
1559
+ const result = await resource.getAll();
1560
+
1561
+ return {
1562
+ success: true,
1563
+ data: result,
1564
+ count: result.length,
1565
+ warning: result.length > 1000 ? 'Large dataset returned. Consider using resourceList with pagination.' : undefined
1566
+ };
1567
+ }
581
1568
 
582
- case 'resourceGetAll':
583
- result = await this.handleResourceGetAll(args);
584
- break;
1569
+ async handleResourceDeleteAll(args) {
1570
+ this.ensureConnected();
1571
+ const { resourceName, confirm } = args;
1572
+
1573
+ if (!confirm) {
1574
+ throw new Error('Confirmation required. Set confirm: true to proceed with deleting all data.');
1575
+ }
1576
+
1577
+ const resource = this.getResource(resourceName);
1578
+ await resource.deleteAll();
1579
+
1580
+ return {
1581
+ success: true,
1582
+ message: `All documents deleted from ${resourceName}`
1583
+ };
1584
+ }
585
1585
 
586
- case 'resourceDeleteAll':
587
- result = await this.handleResourceDeleteAll(args);
588
- break;
1586
+ async handleDbGetStats(args) {
1587
+ this.ensureConnected();
1588
+
1589
+ const stats = {
1590
+ database: {
1591
+ connected: database.isConnected(),
1592
+ bucket: database.bucket,
1593
+ keyPrefix: database.keyPrefix,
1594
+ version: database.s3dbVersion,
1595
+ resourceCount: Object.keys(database.resources || {}).length,
1596
+ resources: Object.keys(database.resources || {})
1597
+ },
1598
+ costs: null,
1599
+ cache: null
1600
+ };
589
1601
 
590
- case 'dbGetStats':
591
- result = await this.handleDbGetStats(args);
592
- break;
1602
+ // Get costs from client if available
1603
+ if (database.client && database.client.costs) {
1604
+ stats.costs = {
1605
+ total: database.client.costs.total,
1606
+ totalRequests: database.client.costs.requests.total,
1607
+ requestsByType: { ...database.client.costs.requests },
1608
+ eventsByType: { ...database.client.costs.events },
1609
+ estimatedCostUSD: database.client.costs.total
1610
+ };
1611
+ }
593
1612
 
594
- case 'dbClearCache':
595
- result = await this.handleDbClearCache(args);
596
- break;
1613
+ // Get cache stats from plugins if available
1614
+ try {
1615
+ const cachePlugin = database.pluginList?.find(p => p.constructor.name === 'CachePlugin');
1616
+ if (cachePlugin && cachePlugin.driver) {
1617
+ const cacheSize = await cachePlugin.driver.size();
1618
+ const cacheKeys = await cachePlugin.driver.keys();
1619
+
1620
+ stats.cache = {
1621
+ enabled: true,
1622
+ driver: cachePlugin.driver.constructor.name,
1623
+ size: cacheSize,
1624
+ maxSize: cachePlugin.driver.maxSize || 'unlimited',
1625
+ ttl: cachePlugin.driver.ttl || 'no expiration',
1626
+ keyCount: cacheKeys.length,
1627
+ sampleKeys: cacheKeys.slice(0, 5) // First 5 keys as sample
1628
+ };
1629
+ } else {
1630
+ stats.cache = { enabled: false };
1631
+ }
1632
+ } catch (error) {
1633
+ stats.cache = { enabled: false, error: error.message };
1634
+ }
597
1635
 
598
- default:
599
- throw new Error(`Unknown tool: ${name}`);
600
- }
1636
+ return {
1637
+ success: true,
1638
+ stats
1639
+ };
1640
+ }
601
1641
 
1642
+ async handleDbClearCache(args) {
1643
+ this.ensureConnected();
1644
+ const { resourceName } = args;
1645
+
1646
+ try {
1647
+ const cachePlugin = database.pluginList?.find(p => p.constructor.name === 'CachePlugin');
1648
+ if (!cachePlugin || !cachePlugin.driver) {
602
1649
  return {
603
- content: [
604
- {
605
- type: 'text',
606
- text: JSON.stringify(result, null, 2)
607
- }
608
- ]
1650
+ success: false,
1651
+ message: 'Cache is not enabled or available'
609
1652
  };
1653
+ }
610
1654
 
611
- } catch (error) {
1655
+ if (resourceName) {
1656
+ // Clear cache for specific resource
1657
+ const resource = this.getResource(resourceName);
1658
+ await cachePlugin.clearCacheForResource(resource);
1659
+
612
1660
  return {
613
- content: [
614
- {
615
- type: 'text',
616
- text: JSON.stringify({
617
- error: error.message,
618
- type: error.constructor.name,
619
- stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
620
- }, null, 2)
621
- }
622
- ],
623
- isError: true
1661
+ success: true,
1662
+ message: `Cache cleared for resource: ${resourceName}`
1663
+ };
1664
+ } else {
1665
+ // Clear all cache
1666
+ await cachePlugin.driver.clear();
1667
+
1668
+ return {
1669
+ success: true,
1670
+ message: 'All cache cleared'
624
1671
  };
625
1672
  }
626
- });
1673
+ } catch (error) {
1674
+ return {
1675
+ success: false,
1676
+ message: `Failed to clear cache: ${error.message}`
1677
+ };
1678
+ }
627
1679
  }
628
1680
 
629
- setupTransport() {
630
- const transport = process.argv.includes('--transport=sse') || process.env.MCP_TRANSPORT === 'sse'
631
- ? new SSEServerTransport('/sse', process.env.MCP_SERVER_HOST || '0.0.0.0', parseInt(process.env.MCP_SERVER_PORT || '17500'))
632
- : new StdioServerTransport();
1681
+ // Helper methods
1682
+ ensureConnected() {
1683
+ if (!database || !database.isConnected()) {
1684
+ throw new Error('Database not connected. Use dbConnect tool first.');
1685
+ }
1686
+ }
633
1687
 
634
- this.server.connect(transport);
1688
+ getResource(resourceName) {
1689
+ this.ensureConnected();
635
1690
 
636
- // SSE specific setup
637
- if (transport instanceof SSEServerTransport) {
638
- const host = process.env.MCP_SERVER_HOST || '0.0.0.0';
639
- const port = process.env.MCP_SERVER_PORT || '17500';
640
-
641
- console.log(`S3DB MCP Server running on http://${host}:${port}/sse`);
642
-
643
- // Add health check endpoint for SSE transport
644
- this.setupHealthCheck(host, port);
1691
+ if (!database.resources[resourceName]) {
1692
+ throw new Error(`Resource '${resourceName}' not found. Available resources: ${Object.keys(database.resources).join(', ')}`);
645
1693
  }
1694
+
1695
+ return database.resources[resourceName];
646
1696
  }
647
1697
 
648
- setupHealthCheck(host, port) {
649
- import('http').then(({ createServer }) => {
650
- const healthServer = createServer((req, res) => {
651
- if (req.url === '/health') {
652
- const healthStatus = {
653
- status: 'healthy',
654
- timestamp: new Date().toISOString(),
655
- uptime: process.uptime(),
656
- version: SERVER_VERSION,
657
- database: {
658
- connected: database ? database.isConnected() : false,
659
- bucket: database?.bucket || null,
660
- keyPrefix: database?.keyPrefix || null,
661
- resourceCount: database ? Object.keys(database.resources || {}).length : 0
662
- },
663
- memory: process.memoryUsage(),
664
- environment: {
665
- nodeVersion: process.version,
666
- platform: process.platform,
667
- transport: 'sse'
668
- }
669
- };
1698
+ // Helper method to extract partition information from data for cache optimization
1699
+ _extractPartitionInfo(resource, data) {
1700
+ if (!resource || !data || !resource.config?.partitions) {
1701
+ return null;
1702
+ }
670
1703
 
671
- res.writeHead(200, {
672
- 'Content-Type': 'application/json',
673
- 'Access-Control-Allow-Origin': '*',
674
- 'Access-Control-Allow-Methods': 'GET',
675
- 'Access-Control-Allow-Headers': 'Content-Type'
676
- });
677
- res.end(JSON.stringify(healthStatus, null, 2));
678
- } else {
679
- res.writeHead(404, { 'Content-Type': 'text/plain' });
680
- res.end('Not Found');
681
- }
682
- });
1704
+ const partitionInfo = {};
1705
+ const partitions = resource.config.partitions;
683
1706
 
684
- // Listen on a different port for health checks to avoid conflicts
685
- const healthPort = parseInt(port) + 1;
686
- healthServer.listen(healthPort, host, () => {
687
- console.log(`Health check endpoint: http://${host}:${healthPort}/health`);
688
- });
689
- }).catch(err => {
690
- console.warn('Could not setup health check endpoint:', err.message);
691
- });
692
- }
1707
+ for (const [partitionName, partitionConfig] of Object.entries(partitions)) {
1708
+ if (partitionConfig.fields) {
1709
+ const partitionValues = {};
1710
+ let hasValues = false;
693
1711
 
694
- // Database connection handlers
695
- async handleDbConnect(args) {
696
- const {
697
- connectionString,
698
- verbose = false,
699
- parallelism = 10,
700
- passphrase = 'secret',
701
- versioningEnabled = false,
702
- enableCache = true,
703
- enableCosts = true,
704
- cacheDriver = 'memory', // 'memory', 'filesystem', or 'custom'
705
- cacheMaxSize = 1000,
706
- cacheTtl = 300000, // 5 minutes
707
- cacheDirectory = './cache', // For filesystem cache
708
- cachePrefix = 'cache'
709
- } = args;
1712
+ for (const fieldName of Object.keys(partitionConfig.fields)) {
1713
+ if (data[fieldName] !== undefined && data[fieldName] !== null) {
1714
+ partitionValues[fieldName] = data[fieldName];
1715
+ hasValues = true;
1716
+ }
1717
+ }
710
1718
 
711
- if (database && database.isConnected()) {
712
- return { success: false, message: 'Database is already connected' };
1719
+ if (hasValues) {
1720
+ partitionInfo[partitionName] = partitionValues;
1721
+ }
1722
+ }
713
1723
  }
714
1724
 
715
- // Setup plugins array
716
- const plugins = [];
717
-
718
- // Always add CostsPlugin (unless explicitly disabled)
719
- const costsEnabled = enableCosts !== false && process.env.S3DB_COSTS_ENABLED !== 'false';
720
- if (costsEnabled) {
721
- plugins.push(CostsPlugin);
722
- }
1725
+ return Object.keys(partitionInfo).length > 0 ? partitionInfo : null;
1726
+ }
723
1727
 
724
- // Add CachePlugin (enabled by default, configurable)
725
- const cacheEnabled = enableCache !== false && process.env.S3DB_CACHE_ENABLED !== 'false';
726
-
727
- // Declare cache variables in outer scope to avoid reference errors
728
- let cacheMaxSizeEnv, cacheTtlEnv, cacheDriverEnv, cacheDirectoryEnv, cachePrefixEnv;
1728
+ // Helper method to generate intelligent cache keys including partition information
1729
+ _generateCacheKeyHint(resourceName, action, params = {}) {
1730
+ const keyParts = [`resource=${resourceName}`, `action=${action}`];
729
1731
 
730
- if (cacheEnabled) {
731
- cacheMaxSizeEnv = process.env.S3DB_CACHE_MAX_SIZE ? parseInt(process.env.S3DB_CACHE_MAX_SIZE) : cacheMaxSize;
732
- cacheTtlEnv = process.env.S3DB_CACHE_TTL ? parseInt(process.env.S3DB_CACHE_TTL) : cacheTtl;
733
- cacheDriverEnv = process.env.S3DB_CACHE_DRIVER || cacheDriver;
734
- cacheDirectoryEnv = process.env.S3DB_CACHE_DIRECTORY || cacheDirectory;
735
- cachePrefixEnv = process.env.S3DB_CACHE_PREFIX || cachePrefix;
736
-
737
- let cacheConfig = {
738
- includePartitions: true
739
- };
1732
+ // Add partition information if present
1733
+ if (params.partition && params.partitionValues) {
1734
+ keyParts.push(`partition=${params.partition}`);
740
1735
 
741
- if (cacheDriverEnv === 'filesystem') {
742
- // Filesystem cache configuration
743
- cacheConfig.driver = new FilesystemCache({
744
- directory: cacheDirectoryEnv,
745
- prefix: cachePrefixEnv,
746
- ttl: cacheTtlEnv,
747
- enableCompression: true,
748
- enableStats: verbose,
749
- enableCleanup: true,
750
- cleanupInterval: 300000, // 5 minutes
751
- createDirectory: true
752
- });
753
- } else {
754
- // Memory cache configuration (default)
755
- cacheConfig.driver = 'memory';
756
- cacheConfig.memoryOptions = {
757
- maxSize: cacheMaxSizeEnv,
758
- ttl: cacheTtlEnv,
759
- enableStats: verbose
760
- };
1736
+ // Sort partition values for consistent cache keys
1737
+ const sortedValues = Object.entries(params.partitionValues)
1738
+ .sort(([a], [b]) => a.localeCompare(b))
1739
+ .map(([key, value]) => `${key}=${value}`)
1740
+ .join('&');
1741
+
1742
+ if (sortedValues) {
1743
+ keyParts.push(`values=${sortedValues}`);
1744
+ }
1745
+ }
1746
+
1747
+ // Add other parameters (excluding partition info to avoid duplication)
1748
+ const otherParams = { ...params };
1749
+ delete otherParams.partition;
1750
+ delete otherParams.partitionValues;
1751
+
1752
+ if (Object.keys(otherParams).length > 0) {
1753
+ const sortedParams = Object.entries(otherParams)
1754
+ .sort(([a], [b]) => a.localeCompare(b))
1755
+ .map(([key, value]) => `${key}=${value}`)
1756
+ .join('&');
1757
+
1758
+ if (sortedParams) {
1759
+ keyParts.push(`params=${sortedParams}`);
761
1760
  }
762
-
763
- plugins.push(new CachePlugin(cacheConfig));
764
1761
  }
1762
+
1763
+ return keyParts.join('/') + '.json.gz';
1764
+ }
765
1765
 
766
- database = new S3db({
767
- connectionString,
768
- verbose,
769
- parallelism,
770
- passphrase,
771
- versioningEnabled,
772
- plugins
773
- });
1766
+ // Helper method to generate cache invalidation patterns based on data changes
1767
+ _generateCacheInvalidationPatterns(resource, data, action = 'write') {
1768
+ const patterns = [];
1769
+ const resourceName = resource.name;
774
1770
 
775
- await database.connect();
1771
+ // Always invalidate general resource cache
1772
+ patterns.push(`resource=${resourceName}/action=list`);
1773
+ patterns.push(`resource=${resourceName}/action=count`);
1774
+ patterns.push(`resource=${resourceName}/action=getAll`);
776
1775
 
777
- return {
778
- success: true,
779
- message: 'Connected to S3DB database',
780
- status: {
781
- connected: database.isConnected(),
782
- bucket: database.bucket,
783
- keyPrefix: database.keyPrefix,
784
- version: database.s3dbVersion,
785
- plugins: {
786
- costs: costsEnabled,
787
- cache: cacheEnabled,
788
- cacheDriver: cacheEnabled ? cacheDriverEnv : null,
789
- cacheDirectory: cacheEnabled && cacheDriverEnv === 'filesystem' ? cacheDirectoryEnv : null,
790
- cacheMaxSize: cacheEnabled && cacheDriverEnv === 'memory' ? cacheMaxSizeEnv : null,
791
- cacheTtl: cacheEnabled ? cacheTtlEnv : null
1776
+ // Extract partition info and invalidate partition-specific cache
1777
+ const partitionInfo = this._extractPartitionInfo(resource, data);
1778
+ if (partitionInfo) {
1779
+ for (const [partitionName, partitionValues] of Object.entries(partitionInfo)) {
1780
+ const sortedValues = Object.entries(partitionValues)
1781
+ .sort(([a], [b]) => a.localeCompare(b))
1782
+ .map(([key, value]) => `${key}=${value}`)
1783
+ .join('&');
1784
+
1785
+ if (sortedValues) {
1786
+ // Invalidate specific partition caches
1787
+ patterns.push(`resource=${resourceName}/action=list/partition=${partitionName}/values=${sortedValues}`);
1788
+ patterns.push(`resource=${resourceName}/action=count/partition=${partitionName}/values=${sortedValues}`);
1789
+ patterns.push(`resource=${resourceName}/action=listIds/partition=${partitionName}/values=${sortedValues}`);
792
1790
  }
793
1791
  }
794
- };
795
- }
796
-
797
- async handleDbDisconnect(args) {
798
- if (!database || !database.isConnected()) {
799
- return { success: false, message: 'No database connection to disconnect' };
800
1792
  }
801
1793
 
802
- await database.disconnect();
803
- database = null;
804
-
805
- return {
806
- success: true,
807
- message: 'Disconnected from S3DB database'
808
- };
809
- }
810
-
811
- async handleDbStatus(args) {
812
- if (!database) {
813
- return {
814
- connected: false,
815
- message: 'No database instance created'
816
- };
1794
+ // For specific document operations, invalidate document cache
1795
+ if (data.id) {
1796
+ patterns.push(`resource=${resourceName}/action=get/params=id=${data.id}`);
1797
+ patterns.push(`resource=${resourceName}/action=exists/params=id=${data.id}`);
817
1798
  }
818
1799
 
819
- return {
820
- connected: database.isConnected(),
821
- bucket: database.bucket,
822
- keyPrefix: database.keyPrefix,
823
- version: database.s3dbVersion,
824
- resourceCount: Object.keys(database.resources || {}).length,
825
- resources: Object.keys(database.resources || {})
826
- };
1800
+ return patterns;
827
1801
  }
828
1802
 
829
- async handleDbCreateResource(args) {
830
- this.ensureConnected();
831
-
832
- const { name, attributes, behavior = 'user-managed', timestamps = false, partitions, paranoid = true } = args;
1803
+ // 🔍 DEBUGGING TOOLS HANDLERS
833
1804
 
834
- const resource = await database.createResource({
835
- name,
836
- attributes,
837
- behavior,
838
- timestamps,
839
- partitions,
840
- paranoid
841
- });
1805
+ async handleDbInspectResource(args) {
1806
+ this.ensureConnected();
1807
+ const { resourceName } = args;
1808
+ const resource = this.getResource(resourceName);
842
1809
 
843
- return {
1810
+ const inspection = {
844
1811
  success: true,
845
1812
  resource: {
846
1813
  name: resource.name,
847
1814
  behavior: resource.behavior,
848
- attributes: resource.attributes,
849
- partitions: resource.config.partitions,
850
- timestamps: resource.config.timestamps
851
- }
852
- };
853
- }
1815
+ version: resource.version,
1816
+ createdBy: resource.createdBy || 'user',
854
1817
 
855
- async handleDbListResources(args) {
856
- this.ensureConnected();
857
-
858
- const resourceList = await database.listResources();
859
-
860
- return {
861
- success: true,
862
- resources: resourceList,
863
- count: resourceList.length
864
- };
865
- }
1818
+ schema: {
1819
+ attributes: resource.attributes,
1820
+ attributeCount: Object.keys(resource.attributes || {}).length,
1821
+ fieldTypes: {}
1822
+ },
866
1823
 
867
- // Resource operation handlers
868
- async handleResourceInsert(args) {
869
- this.ensureConnected();
870
- const { resourceName, data } = args;
871
-
872
- const resource = this.getResource(resourceName);
873
- const result = await resource.insert(data);
874
-
875
- // Extract partition information for cache invalidation
876
- const partitionInfo = this._extractPartitionInfo(resource, result);
877
-
878
- // Generate cache invalidation patterns
879
- const cacheInvalidationPatterns = this._generateCacheInvalidationPatterns(resource, result, 'insert');
880
-
881
- return {
882
- success: true,
883
- data: result,
884
- ...(partitionInfo && { partitionInfo }),
885
- cacheInvalidationPatterns
886
- };
887
- }
1824
+ partitions: resource.config.partitions ? {
1825
+ count: Object.keys(resource.config.partitions).length,
1826
+ definitions: resource.config.partitions,
1827
+ orphaned: resource.findOrphanedPartitions ? resource.findOrphanedPartitions() : null
1828
+ } : null,
1829
+
1830
+ configuration: {
1831
+ timestamps: resource.config.timestamps,
1832
+ paranoid: resource.config.paranoid,
1833
+ strictValidation: resource.strictValidation,
1834
+ asyncPartitions: resource.config.asyncPartitions,
1835
+ versioningEnabled: resource.config.versioningEnabled,
1836
+ autoDecrypt: resource.config.autoDecrypt
1837
+ },
888
1838
 
889
- async handleResourceInsertMany(args) {
890
- this.ensureConnected();
891
- const { resourceName, data } = args;
892
-
893
- const resource = this.getResource(resourceName);
894
- const result = await resource.insertMany(data);
895
-
896
- return {
897
- success: true,
898
- data: result,
899
- count: result.length
1839
+ hooks: resource.config.hooks ? {
1840
+ beforeInsert: resource.config.hooks.beforeInsert?.length || 0,
1841
+ afterInsert: resource.config.hooks.afterInsert?.length || 0,
1842
+ beforeUpdate: resource.config.hooks.beforeUpdate?.length || 0,
1843
+ afterUpdate: resource.config.hooks.afterUpdate?.length || 0,
1844
+ beforeDelete: resource.config.hooks.beforeDelete?.length || 0,
1845
+ afterDelete: resource.config.hooks.afterDelete?.length || 0
1846
+ } : null,
1847
+
1848
+ s3Paths: {
1849
+ metadataKey: `${database.keyPrefix}metadata.json`,
1850
+ resourcePrefix: `${database.keyPrefix}resource=${resourceName}/`
1851
+ }
1852
+ }
900
1853
  };
901
- }
902
1854
 
903
- async handleResourceGet(args) {
904
- this.ensureConnected();
905
- const { resourceName, id, partition, partitionValues } = args;
906
-
907
- const resource = this.getResource(resourceName);
908
-
909
- // Use partition information for optimized retrieval if provided
910
- let options = {};
911
- if (partition && partitionValues) {
912
- options.partition = partition;
913
- options.partitionValues = partitionValues;
1855
+ // Analyze field types
1856
+ for (const [fieldName, fieldDef] of Object.entries(resource.attributes || {})) {
1857
+ const typeStr = typeof fieldDef === 'string' ? fieldDef : fieldDef.type;
1858
+ inspection.resource.schema.fieldTypes[fieldName] = typeStr;
914
1859
  }
915
-
916
- const result = await resource.get(id, options);
917
-
918
- // Extract partition information from result
919
- const partitionInfo = this._extractPartitionInfo(resource, result);
920
-
921
- return {
922
- success: true,
923
- data: result,
924
- ...(partitionInfo && { partitionInfo })
925
- };
926
- }
927
1860
 
928
- async handleResourceGetMany(args) {
929
- this.ensureConnected();
930
- const { resourceName, ids } = args;
931
-
932
- const resource = this.getResource(resourceName);
933
- const result = await resource.getMany(ids);
934
-
935
- return {
936
- success: true,
937
- data: result,
938
- count: result.length
939
- };
1861
+ return inspection;
940
1862
  }
941
1863
 
942
- async handleResourceUpdate(args) {
1864
+ async handleDbGetMetadata(args) {
943
1865
  this.ensureConnected();
944
- const { resourceName, id, data } = args;
945
-
946
- const resource = this.getResource(resourceName);
947
- const result = await resource.update(id, data);
948
-
949
- // Extract partition information for cache invalidation
950
- const partitionInfo = this._extractPartitionInfo(resource, result);
951
-
952
- return {
953
- success: true,
954
- data: result,
955
- ...(partitionInfo && { partitionInfo })
956
- };
1866
+
1867
+ const metadataKey = `${database.keyPrefix}metadata.json`;
1868
+
1869
+ try {
1870
+ const response = await database.client.getObject({
1871
+ Bucket: database.bucket,
1872
+ Key: metadataKey
1873
+ });
1874
+
1875
+ const metadataContent = await response.Body.transformToString();
1876
+ const metadata = JSON.parse(metadataContent);
1877
+
1878
+ return {
1879
+ success: true,
1880
+ metadata,
1881
+ s3Info: {
1882
+ key: metadataKey,
1883
+ bucket: database.bucket,
1884
+ lastModified: response.LastModified,
1885
+ size: response.ContentLength,
1886
+ etag: response.ETag
1887
+ }
1888
+ };
1889
+ } catch (error) {
1890
+ return {
1891
+ success: false,
1892
+ error: error.message,
1893
+ key: metadataKey
1894
+ };
1895
+ }
957
1896
  }
958
1897
 
959
- async handleResourceUpsert(args) {
1898
+ async handleResourceValidate(args) {
960
1899
  this.ensureConnected();
961
1900
  const { resourceName, data } = args;
962
-
963
1901
  const resource = this.getResource(resourceName);
964
- const result = await resource.upsert(data);
965
-
966
- return {
967
- success: true,
968
- data: result
969
- };
970
- }
971
1902
 
972
- async handleResourceDelete(args) {
973
- this.ensureConnected();
974
- const { resourceName, id } = args;
975
-
976
- const resource = this.getResource(resourceName);
977
- await resource.delete(id);
978
-
979
- return {
980
- success: true,
981
- message: `Document ${id} deleted from ${resourceName}`
982
- };
1903
+ try {
1904
+ // Use the schema validator if available
1905
+ const validationResult = resource.schema.validate(data);
1906
+
1907
+ return {
1908
+ success: true,
1909
+ valid: validationResult === true,
1910
+ errors: validationResult === true ? [] : validationResult,
1911
+ data: data
1912
+ };
1913
+ } catch (error) {
1914
+ return {
1915
+ success: false,
1916
+ valid: false,
1917
+ error: error.message,
1918
+ data: data
1919
+ };
1920
+ }
983
1921
  }
984
1922
 
985
- async handleResourceDeleteMany(args) {
1923
+ async handleDbHealthCheck(args) {
986
1924
  this.ensureConnected();
987
- const { resourceName, ids } = args;
988
-
989
- const resource = this.getResource(resourceName);
990
- await resource.deleteMany(ids);
991
-
992
- return {
1925
+ const { includeOrphanedPartitions = true } = args;
1926
+
1927
+ const health = {
993
1928
  success: true,
994
- message: `${ids.length} documents deleted from ${resourceName}`,
995
- deletedIds: ids
1929
+ timestamp: new Date().toISOString(),
1930
+ database: {
1931
+ connected: database.isConnected(),
1932
+ bucket: database.bucket,
1933
+ keyPrefix: database.keyPrefix,
1934
+ version: database.s3dbVersion
1935
+ },
1936
+ resources: {
1937
+ total: Object.keys(database.resources || {}).length,
1938
+ list: Object.keys(database.resources || {}),
1939
+ details: {}
1940
+ },
1941
+ issues: []
996
1942
  };
1943
+
1944
+ // Check each resource
1945
+ for (const [name, resource] of Object.entries(database.resources || {})) {
1946
+ const resourceHealth = {
1947
+ name,
1948
+ behavior: resource.behavior,
1949
+ attributeCount: Object.keys(resource.attributes || {}).length,
1950
+ partitionCount: resource.config.partitions ? Object.keys(resource.config.partitions).length : 0
1951
+ };
1952
+
1953
+ // Check for orphaned partitions
1954
+ if (includeOrphanedPartitions && resource.findOrphanedPartitions) {
1955
+ const orphaned = resource.findOrphanedPartitions();
1956
+ if (Object.keys(orphaned).length > 0) {
1957
+ resourceHealth.orphanedPartitions = orphaned;
1958
+ health.issues.push({
1959
+ severity: 'warning',
1960
+ resource: name,
1961
+ type: 'orphaned_partitions',
1962
+ message: `Resource '${name}' has ${Object.keys(orphaned).length} orphaned partition(s)`,
1963
+ details: orphaned
1964
+ });
1965
+ }
1966
+ }
1967
+
1968
+ health.resources.details[name] = resourceHealth;
1969
+ }
1970
+
1971
+ health.healthy = health.issues.length === 0;
1972
+
1973
+ return health;
997
1974
  }
998
1975
 
999
- async handleResourceExists(args) {
1976
+ async handleResourceGetRaw(args) {
1000
1977
  this.ensureConnected();
1001
- const { resourceName, id, partition, partitionValues } = args;
1002
-
1978
+ const { resourceName, id } = args;
1003
1979
  const resource = this.getResource(resourceName);
1004
-
1005
- // Use partition information for optimized existence check if provided
1006
- let options = {};
1007
- if (partition && partitionValues) {
1008
- options.partition = partition;
1009
- options.partitionValues = partitionValues;
1980
+
1981
+ try {
1982
+ // Build S3 key
1983
+ const key = `${database.keyPrefix}resource=${resourceName}/id=${id}.json`;
1984
+
1985
+ const response = await database.client.getObject({
1986
+ Bucket: database.bucket,
1987
+ Key: key
1988
+ });
1989
+
1990
+ const body = await response.Body.transformToString();
1991
+ const bodyData = body ? JSON.parse(body) : null;
1992
+
1993
+ return {
1994
+ success: true,
1995
+ s3Object: {
1996
+ key,
1997
+ bucket: database.bucket,
1998
+ metadata: response.Metadata || {},
1999
+ contentLength: response.ContentLength,
2000
+ lastModified: response.LastModified,
2001
+ etag: response.ETag,
2002
+ contentType: response.ContentType
2003
+ },
2004
+ data: {
2005
+ metadata: response.Metadata,
2006
+ body: bodyData
2007
+ }
2008
+ };
2009
+ } catch (error) {
2010
+ return {
2011
+ success: false,
2012
+ error: error.message,
2013
+ id,
2014
+ resource: resourceName
2015
+ };
1010
2016
  }
1011
-
1012
- const exists = await resource.exists(id, options);
1013
-
1014
- return {
1015
- success: true,
1016
- exists,
1017
- id,
1018
- resource: resourceName,
1019
- ...(partition && { partition }),
1020
- ...(partitionValues && { partitionValues })
1021
- };
1022
2017
  }
1023
2018
 
1024
- async handleResourceList(args) {
2019
+ // 📊 QUERY & FILTERING TOOLS HANDLERS
2020
+
2021
+ async handleResourceQuery(args) {
1025
2022
  this.ensureConnected();
1026
- const { resourceName, limit = 100, offset = 0, partition, partitionValues } = args;
1027
-
2023
+ const { resourceName, filters, limit = 100, offset = 0 } = args;
1028
2024
  const resource = this.getResource(resourceName);
1029
- const options = { limit, offset };
1030
-
1031
- if (partition && partitionValues) {
1032
- options.partition = partition;
1033
- options.partitionValues = partitionValues;
2025
+
2026
+ try {
2027
+ // Use the query method from resource
2028
+ const results = await resource.query(filters, { limit, offset });
2029
+
2030
+ return {
2031
+ success: true,
2032
+ data: results,
2033
+ count: results.length,
2034
+ filters,
2035
+ pagination: {
2036
+ limit,
2037
+ offset,
2038
+ hasMore: results.length === limit
2039
+ }
2040
+ };
2041
+ } catch (error) {
2042
+ return {
2043
+ success: false,
2044
+ error: error.message,
2045
+ filters
2046
+ };
1034
2047
  }
1035
-
1036
- const result = await resource.list(options);
1037
-
1038
- // Generate cache key hint for intelligent caching
1039
- const cacheKeyHint = this._generateCacheKeyHint(resourceName, 'list', {
1040
- limit,
1041
- offset,
1042
- partition,
1043
- partitionValues
1044
- });
1045
-
1046
- return {
1047
- success: true,
1048
- data: result,
1049
- count: result.length,
1050
- pagination: {
1051
- limit,
1052
- offset,
1053
- hasMore: result.length === limit
1054
- },
1055
- cacheKeyHint,
1056
- ...(partition && { partition }),
1057
- ...(partitionValues && { partitionValues })
1058
- };
1059
2048
  }
1060
2049
 
1061
- async handleResourceListIds(args) {
2050
+ async handleResourceSearch(args) {
1062
2051
  this.ensureConnected();
1063
- const { resourceName, limit = 1000, offset = 0 } = args;
1064
-
2052
+ const { resourceName, searchText, fields, caseSensitive = false, limit = 100 } = args;
1065
2053
  const resource = this.getResource(resourceName);
1066
- const result = await resource.listIds({ limit, offset });
1067
-
1068
- return {
1069
- success: true,
1070
- ids: result,
1071
- count: result.length,
1072
- pagination: {
1073
- limit,
1074
- offset,
1075
- hasMore: result.length === limit
2054
+
2055
+ try {
2056
+ // Get all documents and filter in memory
2057
+ const allDocs = await resource.list({ limit: limit * 2 }); // Fetch more to ensure we have enough after filtering
2058
+
2059
+ const searchString = caseSensitive ? searchText : searchText.toLowerCase();
2060
+
2061
+ // Determine fields to search
2062
+ let searchFields = fields;
2063
+ if (!searchFields || searchFields.length === 0) {
2064
+ // Auto-detect string fields
2065
+ searchFields = Object.keys(resource.attributes || {}).filter(key => {
2066
+ const attr = resource.attributes[key];
2067
+ const type = typeof attr === 'string' ? attr.split('|')[0] : attr.type;
2068
+ return type === 'string';
2069
+ });
1076
2070
  }
1077
- };
2071
+
2072
+ // Filter documents
2073
+ const results = allDocs.filter(doc => {
2074
+ return searchFields.some(field => {
2075
+ const value = doc[field];
2076
+ if (!value) return false;
2077
+ const valueString = caseSensitive ? String(value) : String(value).toLowerCase();
2078
+ return valueString.includes(searchString);
2079
+ });
2080
+ }).slice(0, limit);
2081
+
2082
+ return {
2083
+ success: true,
2084
+ data: results,
2085
+ count: results.length,
2086
+ searchText,
2087
+ searchFields,
2088
+ caseSensitive
2089
+ };
2090
+ } catch (error) {
2091
+ return {
2092
+ success: false,
2093
+ error: error.message,
2094
+ searchText
2095
+ };
2096
+ }
1078
2097
  }
1079
2098
 
1080
- async handleResourceCount(args) {
2099
+ // 🔧 PARTITION MANAGEMENT TOOLS HANDLERS
2100
+
2101
+ async handleResourceListPartitions(args) {
1081
2102
  this.ensureConnected();
1082
- const { resourceName, partition, partitionValues } = args;
1083
-
2103
+ const { resourceName } = args;
1084
2104
  const resource = this.getResource(resourceName);
1085
- const options = {};
1086
-
1087
- if (partition && partitionValues) {
1088
- options.partition = partition;
1089
- options.partitionValues = partitionValues;
1090
- }
1091
-
1092
- const count = await resource.count(options);
1093
-
1094
- // Generate cache key hint for intelligent caching
1095
- const cacheKeyHint = this._generateCacheKeyHint(resourceName, 'count', {
1096
- partition,
1097
- partitionValues
1098
- });
1099
-
2105
+
2106
+ const partitions = resource.config.partitions || {};
2107
+
1100
2108
  return {
1101
2109
  success: true,
1102
- count,
1103
2110
  resource: resourceName,
1104
- cacheKeyHint,
1105
- ...(partition && { partition }),
1106
- ...(partitionValues && { partitionValues })
2111
+ partitions: Object.keys(partitions),
2112
+ count: Object.keys(partitions).length,
2113
+ details: partitions
1107
2114
  };
1108
2115
  }
1109
2116
 
1110
- async handleResourceGetAll(args) {
2117
+ async handleResourceListPartitionValues(args) {
1111
2118
  this.ensureConnected();
1112
- const { resourceName } = args;
1113
-
2119
+ const { resourceName, partitionName, limit = 1000 } = args;
1114
2120
  const resource = this.getResource(resourceName);
1115
- const result = await resource.getAll();
1116
-
1117
- return {
1118
- success: true,
1119
- data: result,
1120
- count: result.length,
1121
- warning: result.length > 1000 ? 'Large dataset returned. Consider using resourceList with pagination.' : undefined
1122
- };
2121
+
2122
+ if (!resource.config.partitions || !resource.config.partitions[partitionName]) {
2123
+ throw new Error(`Partition '${partitionName}' not found in resource '${resourceName}'`);
2124
+ }
2125
+
2126
+ try {
2127
+ // List all objects with this partition prefix
2128
+ const prefix = `${database.keyPrefix}resource=${resourceName}/partition=${partitionName}/`;
2129
+
2130
+ const response = await database.client.listObjectsV2({
2131
+ Bucket: database.bucket,
2132
+ Prefix: prefix,
2133
+ MaxKeys: limit
2134
+ });
2135
+
2136
+ // Extract unique partition values from keys
2137
+ const partitionValues = new Set();
2138
+
2139
+ for (const obj of response.Contents || []) {
2140
+ // Parse partition values from key
2141
+ const keyParts = obj.Key.split('/');
2142
+ const partitionPart = keyParts.find(part => part.startsWith('partition='));
2143
+ if (partitionPart) {
2144
+ const valuesPart = keyParts.slice(keyParts.indexOf(partitionPart) + 1).find(part => !part.startsWith('id='));
2145
+ if (valuesPart) {
2146
+ partitionValues.add(valuesPart);
2147
+ }
2148
+ }
2149
+ }
2150
+
2151
+ return {
2152
+ success: true,
2153
+ resource: resourceName,
2154
+ partition: partitionName,
2155
+ values: Array.from(partitionValues),
2156
+ count: partitionValues.size
2157
+ };
2158
+ } catch (error) {
2159
+ return {
2160
+ success: false,
2161
+ error: error.message,
2162
+ resource: resourceName,
2163
+ partition: partitionName
2164
+ };
2165
+ }
1123
2166
  }
1124
2167
 
1125
- async handleResourceDeleteAll(args) {
2168
+ async handleDbFindOrphanedPartitions(args) {
1126
2169
  this.ensureConnected();
1127
- const { resourceName, confirm } = args;
1128
-
1129
- if (!confirm) {
1130
- throw new Error('Confirmation required. Set confirm: true to proceed with deleting all data.');
2170
+ const { resourceName } = args;
2171
+
2172
+ const orphanedByResource = {};
2173
+ const resourcesToCheck = resourceName
2174
+ ? [resourceName]
2175
+ : Object.keys(database.resources || {});
2176
+
2177
+ for (const name of resourcesToCheck) {
2178
+ const resource = database.resources[name];
2179
+ if (resource && resource.findOrphanedPartitions) {
2180
+ const orphaned = resource.findOrphanedPartitions();
2181
+ if (Object.keys(orphaned).length > 0) {
2182
+ orphanedByResource[name] = orphaned;
2183
+ }
2184
+ }
1131
2185
  }
1132
-
1133
- const resource = this.getResource(resourceName);
1134
- await resource.deleteAll();
1135
-
2186
+
1136
2187
  return {
1137
2188
  success: true,
1138
- message: `All documents deleted from ${resourceName}`
2189
+ orphanedPartitions: orphanedByResource,
2190
+ affectedResources: Object.keys(orphanedByResource),
2191
+ count: Object.keys(orphanedByResource).length,
2192
+ hasIssues: Object.keys(orphanedByResource).length > 0
1139
2193
  };
1140
2194
  }
1141
2195
 
1142
- async handleDbGetStats(args) {
2196
+ async handleDbRemoveOrphanedPartitions(args) {
1143
2197
  this.ensureConnected();
1144
-
1145
- const stats = {
1146
- database: {
1147
- connected: database.isConnected(),
1148
- bucket: database.bucket,
1149
- keyPrefix: database.keyPrefix,
1150
- version: database.s3dbVersion,
1151
- resourceCount: Object.keys(database.resources || {}).length,
1152
- resources: Object.keys(database.resources || {})
1153
- },
1154
- costs: null,
1155
- cache: null
1156
- };
2198
+ const { resourceName, dryRun = true } = args;
2199
+ const resource = this.getResource(resourceName);
1157
2200
 
1158
- // Get costs from client if available
1159
- if (database.client && database.client.costs) {
1160
- stats.costs = {
1161
- total: database.client.costs.total,
1162
- totalRequests: database.client.costs.requests.total,
1163
- requestsByType: { ...database.client.costs.requests },
1164
- eventsByType: { ...database.client.costs.events },
1165
- estimatedCostUSD: database.client.costs.total
2201
+ if (!resource.removeOrphanedPartitions) {
2202
+ throw new Error(`Resource '${resourceName}' does not support removeOrphanedPartitions method`);
2203
+ }
2204
+
2205
+ // Find orphaned partitions first
2206
+ const orphaned = resource.findOrphanedPartitions();
2207
+
2208
+ if (Object.keys(orphaned).length === 0) {
2209
+ return {
2210
+ success: true,
2211
+ message: 'No orphaned partitions found',
2212
+ resource: resourceName,
2213
+ dryRun
1166
2214
  };
1167
2215
  }
1168
2216
 
1169
- // Get cache stats from plugins if available
1170
- try {
1171
- const cachePlugin = database.pluginList?.find(p => p.constructor.name === 'CachePlugin');
1172
- if (cachePlugin && cachePlugin.driver) {
1173
- const cacheSize = await cachePlugin.driver.size();
1174
- const cacheKeys = await cachePlugin.driver.keys();
1175
-
1176
- stats.cache = {
1177
- enabled: true,
1178
- driver: cachePlugin.driver.constructor.name,
1179
- size: cacheSize,
1180
- maxSize: cachePlugin.driver.maxSize || 'unlimited',
1181
- ttl: cachePlugin.driver.ttl || 'no expiration',
1182
- keyCount: cacheKeys.length,
1183
- sampleKeys: cacheKeys.slice(0, 5) // First 5 keys as sample
1184
- };
1185
- } else {
1186
- stats.cache = { enabled: false };
1187
- }
1188
- } catch (error) {
1189
- stats.cache = { enabled: false, error: error.message };
2217
+ if (dryRun) {
2218
+ return {
2219
+ success: true,
2220
+ message: 'Dry run - no changes made',
2221
+ resource: resourceName,
2222
+ orphanedPartitions: orphaned,
2223
+ wouldRemove: Object.keys(orphaned),
2224
+ dryRun: true
2225
+ };
1190
2226
  }
1191
2227
 
2228
+ // Actually remove
2229
+ const removed = resource.removeOrphanedPartitions();
2230
+
2231
+ // Save metadata
2232
+ await database.uploadMetadataFile();
2233
+
1192
2234
  return {
1193
2235
  success: true,
1194
- stats
2236
+ message: `Removed ${Object.keys(removed).length} orphaned partition(s)`,
2237
+ resource: resourceName,
2238
+ removedPartitions: removed,
2239
+ dryRun: false
1195
2240
  };
1196
2241
  }
1197
2242
 
1198
- async handleDbClearCache(args) {
2243
+ // 🚀 BULK OPERATIONS TOOLS HANDLERS
2244
+
2245
+ async handleResourceUpdateMany(args) {
1199
2246
  this.ensureConnected();
1200
- const { resourceName } = args;
1201
-
2247
+ const { resourceName, filters, updates, limit = 1000 } = args;
2248
+ const resource = this.getResource(resourceName);
2249
+
1202
2250
  try {
1203
- const cachePlugin = database.pluginList?.find(p => p.constructor.name === 'CachePlugin');
1204
- if (!cachePlugin || !cachePlugin.driver) {
1205
- return {
1206
- success: false,
1207
- message: 'Cache is not enabled or available'
1208
- };
1209
- }
2251
+ // Query documents matching filters
2252
+ const docs = await resource.query(filters, { limit });
1210
2253
 
1211
- if (resourceName) {
1212
- // Clear cache for specific resource
1213
- const resource = this.getResource(resourceName);
1214
- await cachePlugin.clearCacheForResource(resource);
1215
-
1216
- return {
1217
- success: true,
1218
- message: `Cache cleared for resource: ${resourceName}`
1219
- };
1220
- } else {
1221
- // Clear all cache
1222
- await cachePlugin.driver.clear();
1223
-
1224
- return {
1225
- success: true,
1226
- message: 'All cache cleared'
1227
- };
1228
- }
2254
+ // Update each document
2255
+ const updatePromises = docs.map(doc =>
2256
+ resource.update(doc.id, updates)
2257
+ );
2258
+
2259
+ const results = await Promise.all(updatePromises);
2260
+
2261
+ return {
2262
+ success: true,
2263
+ updatedCount: results.length,
2264
+ filters,
2265
+ updates,
2266
+ data: results
2267
+ };
1229
2268
  } catch (error) {
1230
2269
  return {
1231
2270
  success: false,
1232
- message: `Failed to clear cache: ${error.message}`
2271
+ error: error.message,
2272
+ filters,
2273
+ updates
1233
2274
  };
1234
2275
  }
1235
2276
  }
1236
2277
 
1237
- // Helper methods
1238
- ensureConnected() {
1239
- if (!database || !database.isConnected()) {
1240
- throw new Error('Database not connected. Use dbConnect tool first.');
2278
+ async handleResourceBulkUpsert(args) {
2279
+ this.ensureConnected();
2280
+ const { resourceName, data } = args;
2281
+ const resource = this.getResource(resourceName);
2282
+
2283
+ try {
2284
+ // Upsert each document
2285
+ const upsertPromises = data.map(doc => resource.upsert(doc));
2286
+ const results = await Promise.all(upsertPromises);
2287
+
2288
+ return {
2289
+ success: true,
2290
+ upsertedCount: results.length,
2291
+ data: results
2292
+ };
2293
+ } catch (error) {
2294
+ return {
2295
+ success: false,
2296
+ error: error.message
2297
+ };
1241
2298
  }
1242
2299
  }
1243
2300
 
1244
- getResource(resourceName) {
2301
+ // 💾 EXPORT/IMPORT TOOLS HANDLERS
2302
+
2303
+ async handleResourceExport(args) {
1245
2304
  this.ensureConnected();
1246
-
1247
- if (!database.resources[resourceName]) {
1248
- throw new Error(`Resource '${resourceName}' not found. Available resources: ${Object.keys(database.resources).join(', ')}`);
2305
+ const { resourceName, format = 'json', filters, fields, limit } = args;
2306
+ const resource = this.getResource(resourceName);
2307
+
2308
+ try {
2309
+ // Get data
2310
+ let data;
2311
+ if (filters) {
2312
+ data = await resource.query(filters, limit ? { limit } : {});
2313
+ } else if (limit) {
2314
+ data = await resource.list({ limit });
2315
+ } else {
2316
+ data = await resource.getAll();
2317
+ }
2318
+
2319
+ // Filter fields if specified
2320
+ if (fields && fields.length > 0) {
2321
+ data = data.map(doc => {
2322
+ const filtered = {};
2323
+ for (const field of fields) {
2324
+ if (doc[field] !== undefined) {
2325
+ filtered[field] = doc[field];
2326
+ }
2327
+ }
2328
+ return filtered;
2329
+ });
2330
+ }
2331
+
2332
+ let exportData;
2333
+ let contentType;
2334
+
2335
+ switch (format) {
2336
+ case 'json':
2337
+ exportData = JSON.stringify(data, null, 2);
2338
+ contentType = 'application/json';
2339
+ break;
2340
+
2341
+ case 'ndjson':
2342
+ exportData = data.map(doc => JSON.stringify(doc)).join('\n');
2343
+ contentType = 'application/x-ndjson';
2344
+ break;
2345
+
2346
+ case 'csv':
2347
+ // Simple CSV conversion
2348
+ if (data.length === 0) {
2349
+ exportData = '';
2350
+ } else {
2351
+ const headers = Object.keys(data[0]);
2352
+ const csvRows = [headers.join(',')];
2353
+ for (const doc of data) {
2354
+ const row = headers.map(h => {
2355
+ const val = doc[h];
2356
+ if (val === null || val === undefined) return '';
2357
+ if (typeof val === 'object') return JSON.stringify(val);
2358
+ return String(val).includes(',') ? `"${val}"` : val;
2359
+ });
2360
+ csvRows.push(row.join(','));
2361
+ }
2362
+ exportData = csvRows.join('\n');
2363
+ }
2364
+ contentType = 'text/csv';
2365
+ break;
2366
+
2367
+ default:
2368
+ throw new Error(`Unsupported format: ${format}`);
2369
+ }
2370
+
2371
+ return {
2372
+ success: true,
2373
+ resource: resourceName,
2374
+ format,
2375
+ recordCount: data.length,
2376
+ exportData,
2377
+ contentType,
2378
+ size: exportData.length
2379
+ };
2380
+ } catch (error) {
2381
+ return {
2382
+ success: false,
2383
+ error: error.message,
2384
+ resource: resourceName,
2385
+ format
2386
+ };
1249
2387
  }
1250
-
1251
- return database.resources[resourceName];
1252
2388
  }
1253
2389
 
1254
- // Helper method to extract partition information from data for cache optimization
1255
- _extractPartitionInfo(resource, data) {
1256
- if (!resource || !data || !resource.config?.partitions) {
1257
- return null;
1258
- }
2390
+ async handleResourceImport(args) {
2391
+ this.ensureConnected();
2392
+ const { resourceName, data, mode = 'insert', batchSize = 100 } = args;
2393
+ const resource = this.getResource(resourceName);
1259
2394
 
1260
- const partitionInfo = {};
1261
- const partitions = resource.config.partitions;
2395
+ try {
2396
+ const results = [];
2397
+ let processed = 0;
1262
2398
 
1263
- for (const [partitionName, partitionConfig] of Object.entries(partitions)) {
1264
- if (partitionConfig.fields) {
1265
- const partitionValues = {};
1266
- let hasValues = false;
2399
+ // Process in batches
2400
+ for (let i = 0; i < data.length; i += batchSize) {
2401
+ const batch = data.slice(i, i + batchSize);
1267
2402
 
1268
- for (const fieldName of Object.keys(partitionConfig.fields)) {
1269
- if (data[fieldName] !== undefined && data[fieldName] !== null) {
1270
- partitionValues[fieldName] = data[fieldName];
1271
- hasValues = true;
1272
- }
1273
- }
2403
+ let batchResults;
2404
+ switch (mode) {
2405
+ case 'insert':
2406
+ batchResults = await resource.insertMany(batch);
2407
+ break;
1274
2408
 
1275
- if (hasValues) {
1276
- partitionInfo[partitionName] = partitionValues;
2409
+ case 'upsert':
2410
+ batchResults = await Promise.all(batch.map(doc => resource.upsert(doc)));
2411
+ break;
2412
+
2413
+ case 'replace':
2414
+ // Delete all first if first batch
2415
+ if (i === 0) {
2416
+ await resource.deleteAll();
2417
+ }
2418
+ batchResults = await resource.insertMany(batch);
2419
+ break;
2420
+
2421
+ default:
2422
+ throw new Error(`Unsupported mode: ${mode}`);
1277
2423
  }
2424
+
2425
+ results.push(...batchResults);
2426
+ processed += batch.length;
1278
2427
  }
1279
- }
1280
2428
 
1281
- return Object.keys(partitionInfo).length > 0 ? partitionInfo : null;
2429
+ return {
2430
+ success: true,
2431
+ resource: resourceName,
2432
+ mode,
2433
+ importedCount: results.length,
2434
+ totalRecords: data.length,
2435
+ batchSize
2436
+ };
2437
+ } catch (error) {
2438
+ return {
2439
+ success: false,
2440
+ error: error.message,
2441
+ resource: resourceName,
2442
+ mode,
2443
+ processed
2444
+ };
2445
+ }
1282
2446
  }
1283
2447
 
1284
- // Helper method to generate intelligent cache keys including partition information
1285
- _generateCacheKeyHint(resourceName, action, params = {}) {
1286
- const keyParts = [`resource=${resourceName}`, `action=${action}`];
1287
-
1288
- // Add partition information if present
1289
- if (params.partition && params.partitionValues) {
1290
- keyParts.push(`partition=${params.partition}`);
1291
-
1292
- // Sort partition values for consistent cache keys
1293
- const sortedValues = Object.entries(params.partitionValues)
1294
- .sort(([a], [b]) => a.localeCompare(b))
1295
- .map(([key, value]) => `${key}=${value}`)
1296
- .join('&');
1297
-
1298
- if (sortedValues) {
1299
- keyParts.push(`values=${sortedValues}`);
1300
- }
2448
+ async handleDbBackupMetadata(args) {
2449
+ this.ensureConnected();
2450
+ const { timestamp = true } = args;
2451
+
2452
+ try {
2453
+ const metadataKey = `${database.keyPrefix}metadata.json`;
2454
+
2455
+ // Read current metadata
2456
+ const response = await database.client.getObject({
2457
+ Bucket: database.bucket,
2458
+ Key: metadataKey
2459
+ });
2460
+
2461
+ const metadataContent = await response.Body.transformToString();
2462
+
2463
+ // Create backup key
2464
+ const backupSuffix = timestamp ? `-backup-${Date.now()}` : '-backup';
2465
+ const backupKey = metadataKey.replace('.json', `${backupSuffix}.json`);
2466
+
2467
+ // Save backup
2468
+ await database.client.putObject({
2469
+ Bucket: database.bucket,
2470
+ Key: backupKey,
2471
+ Body: metadataContent,
2472
+ ContentType: 'application/json'
2473
+ });
2474
+
2475
+ return {
2476
+ success: true,
2477
+ message: 'Metadata backup created',
2478
+ backup: {
2479
+ key: backupKey,
2480
+ bucket: database.bucket,
2481
+ timestamp: new Date().toISOString(),
2482
+ size: metadataContent.length
2483
+ },
2484
+ original: {
2485
+ key: metadataKey
2486
+ }
2487
+ };
2488
+ } catch (error) {
2489
+ return {
2490
+ success: false,
2491
+ error: error.message
2492
+ };
1301
2493
  }
1302
-
1303
- // Add other parameters (excluding partition info to avoid duplication)
1304
- const otherParams = { ...params };
1305
- delete otherParams.partition;
1306
- delete otherParams.partitionValues;
1307
-
1308
- if (Object.keys(otherParams).length > 0) {
1309
- const sortedParams = Object.entries(otherParams)
1310
- .sort(([a], [b]) => a.localeCompare(b))
1311
- .map(([key, value]) => `${key}=${value}`)
1312
- .join('&');
1313
-
1314
- if (sortedParams) {
1315
- keyParts.push(`params=${sortedParams}`);
2494
+ }
2495
+
2496
+ // 📈 ENHANCED STATS TOOLS HANDLERS
2497
+
2498
+ async handleResourceGetStats(args) {
2499
+ this.ensureConnected();
2500
+ const { resourceName, includePartitionStats = true } = args;
2501
+ const resource = this.getResource(resourceName);
2502
+
2503
+ try {
2504
+ const stats = {
2505
+ success: true,
2506
+ resource: resourceName,
2507
+ totalDocuments: await resource.count(),
2508
+ schema: {
2509
+ attributeCount: Object.keys(resource.attributes || {}).length,
2510
+ attributes: Object.keys(resource.attributes || {})
2511
+ },
2512
+ configuration: {
2513
+ behavior: resource.behavior,
2514
+ timestamps: resource.config.timestamps,
2515
+ paranoid: resource.config.paranoid,
2516
+ asyncPartitions: resource.config.asyncPartitions
2517
+ }
2518
+ };
2519
+
2520
+ // Partition stats
2521
+ if (includePartitionStats && resource.config.partitions) {
2522
+ stats.partitions = {
2523
+ count: Object.keys(resource.config.partitions).length,
2524
+ details: {}
2525
+ };
2526
+
2527
+ for (const [partitionName, partitionConfig] of Object.entries(resource.config.partitions)) {
2528
+ try {
2529
+ const partitionCount = await resource.count({ partition: partitionName });
2530
+ stats.partitions.details[partitionName] = {
2531
+ fields: Object.keys(partitionConfig.fields || {}),
2532
+ documentCount: partitionCount
2533
+ };
2534
+ } catch (error) {
2535
+ stats.partitions.details[partitionName] = {
2536
+ fields: Object.keys(partitionConfig.fields || {}),
2537
+ error: error.message
2538
+ };
2539
+ }
2540
+ }
1316
2541
  }
2542
+
2543
+ return stats;
2544
+ } catch (error) {
2545
+ return {
2546
+ success: false,
2547
+ error: error.message,
2548
+ resource: resourceName
2549
+ };
1317
2550
  }
1318
-
1319
- return keyParts.join('/') + '.json.gz';
1320
2551
  }
1321
2552
 
1322
- // Helper method to generate cache invalidation patterns based on data changes
1323
- _generateCacheInvalidationPatterns(resource, data, action = 'write') {
1324
- const patterns = [];
1325
- const resourceName = resource.name;
1326
-
1327
- // Always invalidate general resource cache
1328
- patterns.push(`resource=${resourceName}/action=list`);
1329
- patterns.push(`resource=${resourceName}/action=count`);
1330
- patterns.push(`resource=${resourceName}/action=getAll`);
1331
-
1332
- // Extract partition info and invalidate partition-specific cache
1333
- const partitionInfo = this._extractPartitionInfo(resource, data);
1334
- if (partitionInfo) {
1335
- for (const [partitionName, partitionValues] of Object.entries(partitionInfo)) {
1336
- const sortedValues = Object.entries(partitionValues)
1337
- .sort(([a], [b]) => a.localeCompare(b))
1338
- .map(([key, value]) => `${key}=${value}`)
1339
- .join('&');
1340
-
1341
- if (sortedValues) {
1342
- // Invalidate specific partition caches
1343
- patterns.push(`resource=${resourceName}/action=list/partition=${partitionName}/values=${sortedValues}`);
1344
- patterns.push(`resource=${resourceName}/action=count/partition=${partitionName}/values=${sortedValues}`);
1345
- patterns.push(`resource=${resourceName}/action=listIds/partition=${partitionName}/values=${sortedValues}`);
2553
+ async handleCacheGetStats(args) {
2554
+ this.ensureConnected();
2555
+ const { resourceName } = args;
2556
+
2557
+ try {
2558
+ const cachePlugin = database.pluginList?.find(p => p.constructor.name === 'CachePlugin');
2559
+
2560
+ if (!cachePlugin || !cachePlugin.driver) {
2561
+ return {
2562
+ success: false,
2563
+ message: 'Cache is not enabled or available'
2564
+ };
2565
+ }
2566
+
2567
+ const allKeys = await cachePlugin.driver.keys();
2568
+ const cacheSize = await cachePlugin.driver.size();
2569
+
2570
+ const stats = {
2571
+ success: true,
2572
+ enabled: true,
2573
+ driver: cachePlugin.driver.constructor.name,
2574
+ totalKeys: allKeys.length,
2575
+ totalSize: cacheSize,
2576
+ config: {
2577
+ maxSize: cachePlugin.driver.maxSize || 'unlimited',
2578
+ ttl: cachePlugin.driver.ttl || 'no expiration'
2579
+ }
2580
+ };
2581
+
2582
+ // Resource-specific stats if requested
2583
+ if (resourceName) {
2584
+ const resourceKeys = allKeys.filter(key => key.includes(`resource=${resourceName}`));
2585
+ stats.resource = {
2586
+ name: resourceName,
2587
+ keys: resourceKeys.length,
2588
+ sampleKeys: resourceKeys.slice(0, 5)
2589
+ };
2590
+ } else {
2591
+ // Group by resource
2592
+ const byResource = {};
2593
+ for (const key of allKeys) {
2594
+ const match = key.match(/resource=([^/]+)/);
2595
+ if (match) {
2596
+ const res = match[1];
2597
+ byResource[res] = (byResource[res] || 0) + 1;
2598
+ }
1346
2599
  }
2600
+ stats.byResource = byResource;
1347
2601
  }
2602
+
2603
+ // Memory stats for memory cache
2604
+ if (cachePlugin.driver.constructor.name === 'MemoryCache' && cachePlugin.driver.getMemoryStats) {
2605
+ stats.memory = cachePlugin.driver.getMemoryStats();
2606
+ }
2607
+
2608
+ return stats;
2609
+ } catch (error) {
2610
+ return {
2611
+ success: false,
2612
+ error: error.message
2613
+ };
1348
2614
  }
1349
-
1350
- // For specific document operations, invalidate document cache
1351
- if (data.id) {
1352
- patterns.push(`resource=${resourceName}/action=get/params=id=${data.id}`);
1353
- patterns.push(`resource=${resourceName}/action=exists/params=id=${data.id}`);
1354
- }
1355
-
1356
- return patterns;
1357
2615
  }
1358
2616
  }
1359
2617