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.
- package/README.md +138 -0
- package/dist/s3db.cjs.js +2 -2
- package/dist/s3db.es.js +2 -2
- package/mcp/.env.example +117 -0
- package/mcp/{server.js → entrypoint.js} +1941 -683
- package/mcp/tools/bulk.js +112 -0
- package/mcp/tools/connection.js +228 -0
- package/mcp/tools/crud.js +579 -0
- package/mcp/tools/debugging.js +299 -0
- package/mcp/tools/export-import.js +281 -0
- package/mcp/tools/index.js +67 -0
- package/mcp/tools/partitions.js +223 -0
- package/mcp/tools/query.js +150 -0
- package/mcp/tools/resources.js +96 -0
- package/mcp/tools/stats.js +281 -0
- package/package.json +17 -7
- package/src/database.class.js +1 -1
|
@@ -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
|
-
|
|
511
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1241
|
+
async handleDbDisconnect(args) {
|
|
1242
|
+
if (!database || !database.isConnected()) {
|
|
1243
|
+
return { success: false, message: 'No database connection to disconnect' };
|
|
1244
|
+
}
|
|
517
1245
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
break;
|
|
1246
|
+
await database.disconnect();
|
|
1247
|
+
database = null;
|
|
521
1248
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1249
|
+
return {
|
|
1250
|
+
success: true,
|
|
1251
|
+
message: 'Disconnected from S3DB database'
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
525
1254
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1255
|
+
async handleDbStatus(args) {
|
|
1256
|
+
if (!database) {
|
|
1257
|
+
return {
|
|
1258
|
+
connected: false,
|
|
1259
|
+
message: 'No database instance created'
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
529
1262
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1273
|
+
async handleDbCreateResource(args) {
|
|
1274
|
+
this.ensureConnected();
|
|
1275
|
+
|
|
1276
|
+
const { name, attributes, behavior = 'user-managed', timestamps = false, partitions, paranoid = true } = args;
|
|
537
1277
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
1278
|
+
const resource = await database.createResource({
|
|
1279
|
+
name,
|
|
1280
|
+
attributes,
|
|
1281
|
+
behavior,
|
|
1282
|
+
timestamps,
|
|
1283
|
+
partitions,
|
|
1284
|
+
paranoid
|
|
1285
|
+
});
|
|
541
1286
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
599
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
1688
|
+
getResource(resourceName) {
|
|
1689
|
+
this.ensureConnected();
|
|
635
1690
|
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
672
|
-
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
712
|
-
|
|
1719
|
+
if (hasValues) {
|
|
1720
|
+
partitionInfo[partitionName] = partitionValues;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
713
1723
|
}
|
|
714
1724
|
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
725
|
-
|
|
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
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
830
|
-
this.ensureConnected();
|
|
831
|
-
|
|
832
|
-
const { name, attributes, behavior = 'user-managed', timestamps = false, partitions, paranoid = true } = args;
|
|
1803
|
+
// 🔍 DEBUGGING TOOLS HANDLERS
|
|
833
1804
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
-
|
|
1810
|
+
const inspection = {
|
|
844
1811
|
success: true,
|
|
845
1812
|
resource: {
|
|
846
1813
|
name: resource.name,
|
|
847
1814
|
behavior: resource.behavior,
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
timestamps: resource.config.timestamps
|
|
851
|
-
}
|
|
852
|
-
};
|
|
853
|
-
}
|
|
1815
|
+
version: resource.version,
|
|
1816
|
+
createdBy: resource.createdBy || 'user',
|
|
854
1817
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
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
|
|
1864
|
+
async handleDbGetMetadata(args) {
|
|
943
1865
|
this.ensureConnected();
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
|
1923
|
+
async handleDbHealthCheck(args) {
|
|
986
1924
|
this.ensureConnected();
|
|
987
|
-
const {
|
|
988
|
-
|
|
989
|
-
const
|
|
990
|
-
await resource.deleteMany(ids);
|
|
991
|
-
|
|
992
|
-
return {
|
|
1925
|
+
const { includeOrphanedPartitions = true } = args;
|
|
1926
|
+
|
|
1927
|
+
const health = {
|
|
993
1928
|
success: true,
|
|
994
|
-
|
|
995
|
-
|
|
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
|
|
1976
|
+
async handleResourceGetRaw(args) {
|
|
1000
1977
|
this.ensureConnected();
|
|
1001
|
-
const { resourceName, id
|
|
1002
|
-
|
|
1978
|
+
const { resourceName, id } = args;
|
|
1003
1979
|
const resource = this.getResource(resourceName);
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
2019
|
+
// 📊 QUERY & FILTERING TOOLS HANDLERS
|
|
2020
|
+
|
|
2021
|
+
async handleResourceQuery(args) {
|
|
1025
2022
|
this.ensureConnected();
|
|
1026
|
-
const { resourceName, limit = 100, offset = 0
|
|
1027
|
-
|
|
2023
|
+
const { resourceName, filters, limit = 100, offset = 0 } = args;
|
|
1028
2024
|
const resource = this.getResource(resourceName);
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
|
2050
|
+
async handleResourceSearch(args) {
|
|
1062
2051
|
this.ensureConnected();
|
|
1063
|
-
const { resourceName,
|
|
1064
|
-
|
|
2052
|
+
const { resourceName, searchText, fields, caseSensitive = false, limit = 100 } = args;
|
|
1065
2053
|
const resource = this.getResource(resourceName);
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
-
|
|
2099
|
+
// 🔧 PARTITION MANAGEMENT TOOLS HANDLERS
|
|
2100
|
+
|
|
2101
|
+
async handleResourceListPartitions(args) {
|
|
1081
2102
|
this.ensureConnected();
|
|
1082
|
-
const { resourceName
|
|
1083
|
-
|
|
2103
|
+
const { resourceName } = args;
|
|
1084
2104
|
const resource = this.getResource(resourceName);
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
2111
|
+
partitions: Object.keys(partitions),
|
|
2112
|
+
count: Object.keys(partitions).length,
|
|
2113
|
+
details: partitions
|
|
1107
2114
|
};
|
|
1108
2115
|
}
|
|
1109
2116
|
|
|
1110
|
-
async
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
|
2168
|
+
async handleDbFindOrphanedPartitions(args) {
|
|
1126
2169
|
this.ensureConnected();
|
|
1127
|
-
const { resourceName
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
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
|
|
2196
|
+
async handleDbRemoveOrphanedPartitions(args) {
|
|
1143
2197
|
this.ensureConnected();
|
|
1144
|
-
|
|
1145
|
-
const
|
|
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
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
2271
|
+
error: error.message,
|
|
2272
|
+
filters,
|
|
2273
|
+
updates
|
|
1233
2274
|
};
|
|
1234
2275
|
}
|
|
1235
2276
|
}
|
|
1236
2277
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
-
|
|
2301
|
+
// 💾 EXPORT/IMPORT TOOLS HANDLERS
|
|
2302
|
+
|
|
2303
|
+
async handleResourceExport(args) {
|
|
1245
2304
|
this.ensureConnected();
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
2395
|
+
try {
|
|
2396
|
+
const results = [];
|
|
2397
|
+
let processed = 0;
|
|
1262
2398
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const
|
|
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
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
}
|
|
2403
|
+
let batchResults;
|
|
2404
|
+
switch (mode) {
|
|
2405
|
+
case 'insert':
|
|
2406
|
+
batchResults = await resource.insertMany(batch);
|
|
2407
|
+
break;
|
|
1274
2408
|
|
|
1275
|
-
|
|
1276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
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
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
-
|
|
1323
|
-
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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
|
|