underpost 3.2.0 → 3.2.3

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/src/cli/db.js CHANGED
@@ -549,6 +549,7 @@ class UnderpostDB {
549
549
  * @param {boolean} [options.k3s=false] - k3s cluster flag.
550
550
  * @param {boolean} [options.kubeadm=false] - kubeadm cluster flag.
551
551
  * @param {boolean} [options.kind=false] - kind cluster flag.
552
+ * @param {boolean} [options.repoBackup=false] - Backs up repositories (git commit+push) inside deployment pods via kubectl exec.
552
553
  * @return {Promise<void>} Resolves when operation is complete.
553
554
  */
554
555
  async callback(
@@ -577,350 +578,375 @@ class UnderpostDB {
577
578
  k3s: false,
578
579
  kubeadm: false,
579
580
  kind: false,
581
+ repoBackup: false,
580
582
  },
581
583
  ) {
582
- loadCronDeployEnv();
583
- const newBackupTimestamp = new Date().getTime();
584
- const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
585
-
586
- if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
587
-
588
- // Handle clean-fs-collection operation
589
- if (options.cleanFsCollection || options.cleanFsDryRun) {
590
- logger.info('Starting File collection cleanup operation', { deployList });
591
- await Underpost.db.cleanFsCollection(deployList, {
592
- hosts: options.hosts,
593
- paths: options.paths,
594
- dryRun: options.cleanFsDryRun,
595
- });
596
- return;
597
- }
584
+ // Ensure engine-private is available (clone if inside a deployment
585
+ // container where globalSecretClean has already removed it).
586
+ const firstDeployId = deployList !== 'dd' ? deployList.split(',')[0].trim() : '';
587
+ try {
588
+ loadCronDeployEnv();
589
+ const newBackupTimestamp = new Date().getTime();
590
+ const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
591
+
592
+ if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
593
+
594
+ // Handle repository backup (git commit+push inside deployment pod)
595
+ if (options.repoBackup) {
596
+ const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
597
+ for (const _deployId of deployList.split(',')) {
598
+ const deployId = _deployId.trim();
599
+ if (!deployId) continue;
600
+ logger.info('Starting pod repository backup', { deployId, namespace });
601
+ Underpost.repo.backupPodRepositories({
602
+ deployId,
603
+ namespace,
604
+ env: options.dev ? 'development' : 'production',
605
+ });
606
+ }
607
+ return;
608
+ }
598
609
 
599
- logger.info('Starting database operation', {
600
- deployList,
601
- namespace,
602
- import: options.import,
603
- export: options.export,
604
- });
610
+ // Handle clean-fs-collection operation
611
+ if (options.cleanFsCollection || options.cleanFsDryRun) {
612
+ logger.info('Starting File collection cleanup operation', { deployList });
613
+ await Underpost.db.cleanFsCollection(deployList, {
614
+ hosts: options.hosts,
615
+ paths: options.paths,
616
+ dryRun: options.cleanFsDryRun,
617
+ });
618
+ return;
619
+ }
605
620
 
606
- if (options.primaryPodEnsure) {
607
- const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: options.primaryPodEnsure });
608
- if (!primaryPodName) {
609
- const baseCommand = options.dev ? 'node bin' : 'underpost';
610
- const baseClusterCommand = options.dev ? ' --dev' : '';
611
- let clusterFlag = options.k3s ? ' --k3s' : options.kubeadm ? ' --kubeadm' : '';
612
- shellExec(`${baseCommand} cluster${baseClusterCommand}${clusterFlag} --mongodb`);
621
+ logger.info('Starting database operation', {
622
+ deployList,
623
+ namespace,
624
+ import: options.import,
625
+ export: options.export,
626
+ });
627
+
628
+ if (options.primaryPodEnsure) {
629
+ const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: options.primaryPodEnsure });
630
+ if (!primaryPodName) {
631
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
632
+ const baseClusterCommand = options.dev ? ' --dev' : '';
633
+ let clusterFlag = options.k3s ? ' --k3s' : options.kubeadm ? ' --kubeadm' : '';
634
+ shellExec(`${baseCommand} cluster${baseClusterCommand}${clusterFlag} --mongodb`);
635
+ }
636
+ return;
613
637
  }
614
- return;
615
- }
616
638
 
617
- // Track processed repositories to avoid duplicate Git operations
618
- const processedRepos = new Set();
619
- // Track processed host+path combinations to avoid duplicates
620
- const processedHostPaths = new Set();
639
+ // Track processed repositories to avoid duplicate Git operations
640
+ const processedRepos = new Set();
641
+ // Track processed host+path combinations to avoid duplicates
642
+ const processedHostPaths = new Set();
621
643
 
622
- for (const _deployId of deployList.split(',')) {
623
- const deployId = _deployId.trim();
624
- if (!deployId) continue;
644
+ for (const _deployId of deployList.split(',')) {
645
+ const deployId = _deployId.trim();
646
+ if (!deployId) continue;
625
647
 
626
- logger.info('Processing deployment', { deployId });
648
+ logger.info('Processing deployment', { deployId });
627
649
 
628
- /** @type {Object.<string, Object.<string, DatabaseConfig>>} */
629
- const dbs = {};
630
- const repoName = `engine-${deployId.includes('dd-') ? deployId.split('dd-')[1] : deployId}-cron-backups`;
650
+ /** @type {Object.<string, Object.<string, DatabaseConfig>>} */
651
+ const dbs = {};
652
+ const repoName = `engine-${deployId.includes('dd-') ? deployId.split('dd-')[1] : deployId}-cron-backups`;
631
653
 
632
- // Load server configuration
633
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
634
- if (!fs.existsSync(confServerPath)) {
635
- logger.error('Configuration file not found', { path: confServerPath });
636
- continue;
637
- }
654
+ // Load server configuration
655
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
656
+ if (!fs.existsSync(confServerPath)) {
657
+ logger.error('Configuration file not found', { path: confServerPath });
658
+ continue;
659
+ }
638
660
 
639
- const confServer = loadConfServerJson(confServerPath, { resolve: true });
640
-
641
- // Build database configuration map
642
- for (const host of Object.keys(confServer)) {
643
- for (const path of Object.keys(confServer[host])) {
644
- const { db } = confServer[host][path];
645
- if (db) {
646
- const { provider, name, user, password } = db;
647
- if (!dbs[provider]) dbs[provider] = {};
648
-
649
- if (!(name in dbs[provider])) {
650
- dbs[provider][name] = {
651
- user,
652
- password,
653
- hostFolder: host + path.replaceAll('/', '-'),
654
- host,
655
- path,
656
- };
661
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
662
+
663
+ // Build database configuration map
664
+ for (const host of Object.keys(confServer)) {
665
+ for (const path of Object.keys(confServer[host])) {
666
+ const { db } = confServer[host][path];
667
+ if (db) {
668
+ const { provider, name, user, password } = db;
669
+ if (!dbs[provider]) dbs[provider] = {};
670
+
671
+ if (!(name in dbs[provider])) {
672
+ dbs[provider][name] = {
673
+ user,
674
+ password,
675
+ hostFolder: host + path.replaceAll('/', '-'),
676
+ host,
677
+ path,
678
+ };
679
+ }
657
680
  }
658
681
  }
659
682
  }
660
- }
661
683
 
662
- // Handle Git operations - execute only once per repository
663
- if (!processedRepos.has(repoName)) {
664
- logger.info('Processing Git operations for repository', { repoName, deployId });
665
- if (options.git === true) {
666
- Underpost.repo.manageBackupRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
667
- Underpost.repo.manageBackupRepo({ repoName, operation: 'pull' });
668
- }
669
-
670
- if (options.macroRollbackExport) {
671
- // Only clone if not already done by git option above
672
- if (options.git !== true) {
684
+ // Handle Git operations - execute only once per repository
685
+ if (!processedRepos.has(repoName)) {
686
+ logger.info('Processing Git operations for repository', { repoName, deployId });
687
+ if (options.git === true) {
673
688
  Underpost.repo.manageBackupRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
674
689
  Underpost.repo.manageBackupRepo({ repoName, operation: 'pull' });
675
690
  }
676
691
 
677
- const nCommits = parseInt(options.macroRollbackExport);
678
- const repoPath = `../${repoName}`;
679
- const username = process.env.GITHUB_USERNAME;
680
-
681
- if (fs.existsSync(repoPath) && username) {
682
- logger.info('Executing macro rollback export', { repoName, nCommits });
683
- shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
684
- shellExec(`cd ${repoPath} && git reset`);
685
- shellExec(`cd ${repoPath} && git checkout .`);
686
- shellExec(`cd ${repoPath} && git clean -f -d`);
687
- shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
688
- } else {
689
- if (!username) logger.error('GITHUB_USERNAME environment variable not set');
690
- logger.warn('Repository not found for macro rollback', { repoPath });
692
+ if (options.macroRollbackExport) {
693
+ // Only clone if not already done by git option above
694
+ if (options.git !== true) {
695
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
696
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'pull' });
697
+ }
698
+
699
+ const nCommits = parseInt(options.macroRollbackExport);
700
+ const repoPath = `../${repoName}`;
701
+ const username = process.env.GITHUB_USERNAME;
702
+
703
+ if (fs.existsSync(repoPath) && username) {
704
+ logger.info('Executing macro rollback export', { repoName, nCommits });
705
+ shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
706
+ shellExec(`cd ${repoPath} && git reset`);
707
+ shellExec(`cd ${repoPath} && git checkout .`);
708
+ shellExec(`cd ${repoPath} && git clean -f -d`);
709
+ shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
710
+ } else {
711
+ if (!username) logger.error('GITHUB_USERNAME environment variable not set');
712
+ logger.warn('Repository not found for macro rollback', { repoPath });
713
+ }
691
714
  }
692
- }
693
715
 
694
- processedRepos.add(repoName);
695
- logger.info('Repository marked as processed', { repoName });
696
- } else {
697
- logger.info('Skipping Git operations for already processed repository', { repoName, deployId });
698
- }
716
+ processedRepos.add(repoName);
717
+ logger.info('Repository marked as processed', { repoName });
718
+ } else {
719
+ logger.info('Skipping Git operations for already processed repository', { repoName, deployId });
720
+ }
699
721
 
700
- // Process each database provider
701
- for (const provider of Object.keys(dbs)) {
702
- for (const dbName of Object.keys(dbs[provider])) {
703
- const { hostFolder, user, password, host, path } = dbs[provider][dbName];
722
+ // Process each database provider
723
+ for (const provider of Object.keys(dbs)) {
724
+ for (const dbName of Object.keys(dbs[provider])) {
725
+ const { hostFolder, user, password, host, path } = dbs[provider][dbName];
704
726
 
705
- // Create unique identifier for host+path combination
706
- const hostPathKey = `${deployId}:${host}:${path}`;
727
+ // Create unique identifier for host+path combination
728
+ const hostPathKey = `${deployId}:${host}:${path}`;
707
729
 
708
- // Skip if this host+path combination was already processed
709
- if (processedHostPaths.has(hostPathKey)) {
710
- logger.info('Skipping already processed host/path', { dbName, host, path, deployId });
711
- continue;
712
- }
713
-
714
- // Filter by hosts and paths if specified
715
- if (
716
- (options.hosts &&
717
- !options.hosts
718
- .split(',')
719
- .map((h) => h.trim())
720
- .includes(host)) ||
721
- (options.paths &&
722
- !options.paths
723
- .split(',')
724
- .map((p) => p.trim())
725
- .includes(path))
726
- ) {
727
- logger.info('Skipping database due to host/path filter', { dbName, host, path });
728
- continue;
729
- }
730
-
731
- if (!hostFolder) {
732
- logger.warn('No hostFolder defined for database', { dbName, provider });
733
- continue;
734
- }
730
+ // Skip if this host+path combination was already processed
731
+ if (processedHostPaths.has(hostPathKey)) {
732
+ logger.info('Skipping already processed host/path', { dbName, host, path, deployId });
733
+ continue;
734
+ }
735
735
 
736
- logger.info('Processing database', { hostFolder, provider, dbName, deployId });
736
+ // Filter by hosts and paths if specified
737
+ if (
738
+ (options.hosts &&
739
+ !options.hosts
740
+ .split(',')
741
+ .map((h) => h.trim())
742
+ .includes(host)) ||
743
+ (options.paths &&
744
+ !options.paths
745
+ .split(',')
746
+ .map((p) => p.trim())
747
+ .includes(path))
748
+ ) {
749
+ logger.info('Skipping database due to host/path filter', { dbName, host, path });
750
+ continue;
751
+ }
737
752
 
738
- const latestBackupTimestamp = Underpost.db._getLatestBackupTimestamp(`../${repoName}/${hostFolder}`);
753
+ if (!hostFolder) {
754
+ logger.warn('No hostFolder defined for database', { dbName, provider });
755
+ continue;
756
+ }
739
757
 
740
- dbs[provider][dbName].currentBackupTimestamp = latestBackupTimestamp;
758
+ logger.info('Processing database', { hostFolder, provider, dbName, deployId });
741
759
 
742
- const currentTimestamp = latestBackupTimestamp || newBackupTimestamp;
743
- const sqlContainerPath = `/home/${dbName}.sql`;
744
- const fromPartsPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}-parths.json`;
745
- const toSqlPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}.sql`;
746
- const toNewSqlPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}.sql`;
747
- const toBsonPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}`;
748
- const toNewBsonPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}`;
760
+ const latestBackupTimestamp = Underpost.db._getLatestBackupTimestamp(`../${repoName}/${hostFolder}`);
749
761
 
750
- // Merge split SQL files if needed for import
751
- if (options.import === true && fs.existsSync(fromPartsPath) && !fs.existsSync(toSqlPath)) {
752
- const names = JSON.parse(fs.readFileSync(fromPartsPath, 'utf8')).map((_path) => {
753
- return `../${repoName}/${hostFolder}/${currentTimestamp}/${_path.split('/').pop()}`;
754
- });
755
- logger.info('Merging backup parts', { fromPartsPath, toSqlPath, parts: names.length });
756
- await mergeFile(names, toSqlPath);
757
- }
762
+ dbs[provider][dbName].currentBackupTimestamp = latestBackupTimestamp;
758
763
 
759
- // Get target pods based on provider and options
760
- let targetPods = [];
761
- const podCriteria = {
762
- podNames: options.podName,
763
- namespace,
764
- deployId: provider === 'mariadb' ? 'mariadb' : 'mongo',
765
- };
764
+ const currentTimestamp = latestBackupTimestamp || newBackupTimestamp;
765
+ const sqlContainerPath = `/home/${dbName}.sql`;
766
+ const fromPartsPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}-parths.json`;
767
+ const toSqlPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}.sql`;
768
+ const toNewSqlPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}.sql`;
769
+ const toBsonPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}`;
770
+ const toNewBsonPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}`;
766
771
 
767
- targetPods = Underpost.kubectl.getFilteredPods(podCriteria);
772
+ // Merge split SQL files if needed for import
773
+ if (options.import === true && fs.existsSync(fromPartsPath) && !fs.existsSync(toSqlPath)) {
774
+ const names = JSON.parse(fs.readFileSync(fromPartsPath, 'utf8')).map((_path) => {
775
+ return `../${repoName}/${hostFolder}/${currentTimestamp}/${_path.split('/').pop()}`;
776
+ });
777
+ logger.info('Merging backup parts', { fromPartsPath, toSqlPath, parts: names.length });
778
+ await mergeFile(names, toSqlPath);
779
+ }
768
780
 
769
- // Fallback to default if no custom pods specified
770
- if (targetPods.length === 0 && !options.podName) {
771
- const defaultPods = Underpost.kubectl.get(
772
- provider === 'mariadb' ? 'mariadb' : 'mongo',
773
- 'pods',
781
+ // Get target pods based on provider and options
782
+ let targetPods = [];
783
+ const podCriteria = {
784
+ podNames: options.podName,
774
785
  namespace,
775
- );
776
- console.log('defaultPods', defaultPods);
777
- targetPods = defaultPods;
778
- }
786
+ deployId: provider === 'mariadb' ? 'mariadb' : 'mongo',
787
+ };
779
788
 
780
- if (targetPods.length === 0) {
781
- logger.warn('No pods found matching criteria', { provider, criteria: podCriteria });
782
- continue;
783
- }
789
+ targetPods = Underpost.kubectl.getFilteredPods(podCriteria);
790
+
791
+ // Fallback to default if no custom pods specified
792
+ if (targetPods.length === 0 && !options.podName) {
793
+ const defaultPods = Underpost.kubectl.get(
794
+ provider === 'mariadb' ? 'mariadb' : 'mongo',
795
+ 'pods',
796
+ namespace,
797
+ );
798
+ console.log('defaultPods', defaultPods);
799
+ targetPods = defaultPods;
800
+ }
784
801
 
785
- // Handle primary pod detection for MongoDB
786
- let podsToProcess = [];
787
- if (provider === 'mongoose' && !options.allPods) {
788
- // For MongoDB, always use primary pod unless allPods is true
789
- if (!targetPods || targetPods.length === 0) {
790
- logger.warn('No MongoDB pods available to check for primary');
791
- podsToProcess = [];
792
- } else {
793
- const firstPod = targetPods[0].NAME;
794
- const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: firstPod });
795
-
796
- if (primaryPodName) {
797
- const primaryPod = targetPods.find((p) => p.NAME === primaryPodName);
798
- if (primaryPod) {
799
- podsToProcess = [primaryPod];
800
- logger.info('Using MongoDB primary pod', { primaryPod: primaryPodName });
802
+ if (targetPods.length === 0) {
803
+ logger.warn('No pods found matching criteria', { provider, criteria: podCriteria });
804
+ continue;
805
+ }
806
+
807
+ // Handle primary pod detection for MongoDB
808
+ let podsToProcess = [];
809
+ if (provider === 'mongoose' && !options.allPods) {
810
+ // For MongoDB, always use primary pod unless allPods is true
811
+ if (!targetPods || targetPods.length === 0) {
812
+ logger.warn('No MongoDB pods available to check for primary');
813
+ podsToProcess = [];
814
+ } else {
815
+ const firstPod = targetPods[0].NAME;
816
+ const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: firstPod });
817
+
818
+ if (primaryPodName) {
819
+ const primaryPod = targetPods.find((p) => p.NAME === primaryPodName);
820
+ if (primaryPod) {
821
+ podsToProcess = [primaryPod];
822
+ logger.info('Using MongoDB primary pod', { primaryPod: primaryPodName });
823
+ } else {
824
+ logger.warn('Primary pod not in filtered list, using first pod', { primaryPodName });
825
+ podsToProcess = [targetPods[0]];
826
+ }
801
827
  } else {
802
- logger.warn('Primary pod not in filtered list, using first pod', { primaryPodName });
828
+ logger.warn('Could not detect primary pod, using first pod');
803
829
  podsToProcess = [targetPods[0]];
804
830
  }
805
- } else {
806
- logger.warn('Could not detect primary pod, using first pod');
807
- podsToProcess = [targetPods[0]];
808
831
  }
832
+ } else {
833
+ // For MariaDB or when allPods is true, limit to first pod unless allPods is true
834
+ podsToProcess = options.allPods === true ? targetPods : [targetPods[0]];
809
835
  }
810
- } else {
811
- // For MariaDB or when allPods is true, limit to first pod unless allPods is true
812
- podsToProcess = options.allPods === true ? targetPods : [targetPods[0]];
813
- }
814
836
 
815
- logger.info(`Processing ${podsToProcess.length} pod(s) for ${provider}`, {
816
- dbName,
817
- pods: podsToProcess.map((p) => p.NAME),
818
- });
837
+ logger.info(`Processing ${podsToProcess.length} pod(s) for ${provider}`, {
838
+ dbName,
839
+ pods: podsToProcess.map((p) => p.NAME),
840
+ });
819
841
 
820
- // Process each pod
821
- for (const pod of podsToProcess) {
822
- logger.info('Processing pod', { podName: pod.NAME, node: pod.NODE, status: pod.STATUS });
823
-
824
- switch (provider) {
825
- case 'mariadb': {
826
- if (options.stats === true) {
827
- const stats = Underpost.db._getMariaDBStats({
828
- podName: pod.NAME,
829
- namespace,
830
- dbName,
831
- user,
832
- password,
833
- });
834
- if (stats) {
835
- Underpost.db._displayStats({ provider, dbName, stats });
842
+ // Process each pod
843
+ for (const pod of podsToProcess) {
844
+ logger.info('Processing pod', { podName: pod.NAME, node: pod.NODE, status: pod.STATUS });
845
+
846
+ switch (provider) {
847
+ case 'mariadb': {
848
+ if (options.stats === true) {
849
+ const stats = Underpost.db._getMariaDBStats({
850
+ podName: pod.NAME,
851
+ namespace,
852
+ dbName,
853
+ user,
854
+ password,
855
+ });
856
+ if (stats) {
857
+ Underpost.db._displayStats({ provider, dbName, stats });
858
+ }
836
859
  }
837
- }
838
860
 
839
- if (options.import === true) {
840
- Underpost.db._importMariaDB({
841
- pod,
842
- namespace,
843
- dbName,
844
- user,
845
- password,
846
- sqlPath: toSqlPath,
847
- });
848
- }
861
+ if (options.import === true) {
862
+ Underpost.db._importMariaDB({
863
+ pod,
864
+ namespace,
865
+ dbName,
866
+ user,
867
+ password,
868
+ sqlPath: toSqlPath,
869
+ });
870
+ }
849
871
 
850
- if (options.export === true) {
851
- const outputPath = options.outPath || toNewSqlPath;
852
- await Underpost.db._exportMariaDB({
853
- pod,
854
- namespace,
855
- dbName,
856
- user,
857
- password,
858
- outputPath,
859
- });
872
+ if (options.export === true) {
873
+ const outputPath = options.outPath || toNewSqlPath;
874
+ await Underpost.db._exportMariaDB({
875
+ pod,
876
+ namespace,
877
+ dbName,
878
+ user,
879
+ password,
880
+ outputPath,
881
+ });
882
+ }
883
+ break;
860
884
  }
861
- break;
862
- }
863
885
 
864
- case 'mongoose': {
865
- if (options.stats === true) {
866
- const stats = Underpost.db._getMongoStats({
867
- podName: pod.NAME,
868
- namespace,
869
- dbName,
870
- });
871
- if (stats) {
872
- Underpost.db._displayStats({ provider, dbName, stats });
886
+ case 'mongoose': {
887
+ if (options.stats === true) {
888
+ const stats = Underpost.db._getMongoStats({
889
+ podName: pod.NAME,
890
+ namespace,
891
+ dbName,
892
+ });
893
+ if (stats) {
894
+ Underpost.db._displayStats({ provider, dbName, stats });
895
+ }
873
896
  }
874
- }
875
897
 
876
- if (options.import === true) {
877
- const bsonPath = options.outPath || toBsonPath;
878
- Underpost.db._importMongoDB({
879
- pod,
880
- namespace,
881
- dbName,
882
- bsonPath,
883
- drop: options.drop,
884
- preserveUUID: options.preserveUUID,
885
- });
886
- }
898
+ if (options.import === true) {
899
+ const bsonPath = options.outPath || toBsonPath;
900
+ Underpost.db._importMongoDB({
901
+ pod,
902
+ namespace,
903
+ dbName,
904
+ bsonPath,
905
+ drop: options.drop,
906
+ preserveUUID: options.preserveUUID,
907
+ });
908
+ }
887
909
 
888
- if (options.export === true) {
889
- const outputPath = options.outPath || toNewBsonPath;
890
- Underpost.db._exportMongoDB({
891
- pod,
892
- namespace,
893
- dbName,
894
- outputPath,
895
- collections: options.collections,
896
- });
910
+ if (options.export === true) {
911
+ const outputPath = options.outPath || toNewBsonPath;
912
+ Underpost.db._exportMongoDB({
913
+ pod,
914
+ namespace,
915
+ dbName,
916
+ outputPath,
917
+ collections: options.collections,
918
+ });
919
+ }
920
+ break;
897
921
  }
898
- break;
899
- }
900
922
 
901
- default:
902
- logger.warn('Unsupported database provider', { provider });
903
- break;
923
+ default:
924
+ logger.warn('Unsupported database provider', { provider });
925
+ break;
926
+ }
904
927
  }
928
+
929
+ // Mark this host+path combination as processed
930
+ processedHostPaths.add(hostPathKey);
905
931
  }
932
+ }
906
933
 
907
- // Mark this host+path combination as processed
908
- processedHostPaths.add(hostPathKey);
934
+ // Commit and push to Git if enabled - execute only once per repository
935
+ if (options.export === true && options.git === true && !processedRepos.has(`${repoName}-committed`)) {
936
+ const commitMessage = `${new Date(newBackupTimestamp).toLocaleDateString()} ${new Date(
937
+ newBackupTimestamp,
938
+ ).toLocaleTimeString()}`;
939
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'commit', message: commitMessage });
940
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'push' });
941
+ processedRepos.add(`${repoName}-committed`);
909
942
  }
910
943
  }
911
944
 
912
- // Commit and push to Git if enabled - execute only once per repository
913
- if (options.export === true && options.git === true && !processedRepos.has(`${repoName}-committed`)) {
914
- const commitMessage = `${new Date(newBackupTimestamp).toLocaleDateString()} ${new Date(
915
- newBackupTimestamp,
916
- ).toLocaleTimeString()}`;
917
- Underpost.repo.manageBackupRepo({ repoName, operation: 'commit', message: commitMessage });
918
- Underpost.repo.manageBackupRepo({ repoName, operation: 'push' });
919
- processedRepos.add(`${repoName}-committed`);
920
- }
945
+ logger.info('Database operation completed successfully');
946
+ } catch (error) {
947
+ logger.error('Database operation failed', { error: error.message });
948
+ throw error;
921
949
  }
922
-
923
- logger.info('Database operation completed successfully');
924
950
  },
925
951
 
926
952
  /**
@@ -932,6 +958,8 @@ class UnderpostDB {
932
958
  * @param {string} [deployId=process.env.DEFAULT_DEPLOY_ID] - The deployment ID.
933
959
  * @param {string} [host=process.env.DEFAULT_DEPLOY_HOST] - The host identifier.
934
960
  * @param {string} [path=process.env.DEFAULT_DEPLOY_PATH] - The path identifier.
961
+ * @param {object} [options] - Options.
962
+ * @param {boolean} [options.dev=false] - Development mode flag.
935
963
  * @return {Promise<void>} Resolves when metadata creation is complete.
936
964
  * @throws {Error} If database configuration is invalid or connection fails.
937
965
  */
@@ -939,175 +967,181 @@ class UnderpostDB {
939
967
  deployId = process.env.DEFAULT_DEPLOY_ID,
940
968
  host = process.env.DEFAULT_DEPLOY_HOST,
941
969
  path = process.env.DEFAULT_DEPLOY_PATH,
970
+ options = { dev: false },
942
971
  ) {
943
- loadCronDeployEnv();
944
- deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
945
- host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
946
- path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
947
-
948
- logger.info('Creating cluster metadata', { deployId, host, path });
972
+ try {
973
+ loadCronDeployEnv();
974
+ deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
975
+ host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
976
+ path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
949
977
 
950
- const env = 'production';
951
- const deployListPath = './engine-private/deploy/dd.router';
978
+ logger.info('Creating cluster metadata', { deployId, host, path });
952
979
 
953
- if (!fs.existsSync(deployListPath)) {
954
- logger.error('Deploy router file not found', { path: deployListPath });
955
- throw new Error(`Deploy router file not found: ${deployListPath}`);
956
- }
980
+ const env = 'production';
981
+ const deployListPath = './engine-private/deploy/dd.router';
957
982
 
958
- const deployList = fs.readFileSync(deployListPath, 'utf8').split(',');
983
+ if (!fs.existsSync(deployListPath)) {
984
+ logger.error('Deploy router file not found', { path: deployListPath });
985
+ throw new Error(`Deploy router file not found: ${deployListPath}`);
986
+ }
959
987
 
960
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
961
- if (!fs.existsSync(confServerPath)) {
962
- logger.error('Server configuration not found', { path: confServerPath });
963
- throw new Error(`Server configuration not found: ${confServerPath}`);
964
- }
988
+ const deployList = fs.readFileSync(deployListPath, 'utf8').split(',');
965
989
 
966
- const { db } = loadConfServerJson(confServerPath, { resolve: true })[host][path];
990
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
991
+ if (!fs.existsSync(confServerPath)) {
992
+ logger.error('Server configuration not found', { path: confServerPath });
993
+ throw new Error(`Server configuration not found: ${confServerPath}`);
994
+ }
967
995
 
968
- const maxRetries = 5;
969
- const retryDelay = 3000;
970
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
971
- try {
972
- await DataBaseProvider.load({ apis: ['instance', 'cron'], host, path, db });
973
- break;
974
- } catch (err) {
975
- if (attempt === maxRetries) {
976
- logger.error('Failed to connect to database after retries', { attempts: maxRetries, error: err.message });
977
- throw err;
996
+ const { db } = loadConfServerJson(confServerPath, { resolve: true })[host][path];
997
+
998
+ const maxRetries = 5;
999
+ const retryDelay = 3000;
1000
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
1001
+ try {
1002
+ await DataBaseProvider.load({ apis: ['instance', 'cron'], host, path, db });
1003
+ break;
1004
+ } catch (err) {
1005
+ if (attempt === maxRetries) {
1006
+ logger.error('Failed to connect to database after retries', { attempts: maxRetries, error: err.message });
1007
+ throw err;
1008
+ }
1009
+ logger.warn('Database connection failed, retrying...', { attempt, maxRetries, error: err.message });
1010
+ await timer(retryDelay);
978
1011
  }
979
- logger.warn('Database connection failed, retrying...', { attempt, maxRetries, error: err.message });
980
- await timer(retryDelay);
981
1012
  }
982
- }
983
1013
 
984
- try {
985
- /** @type {import('../api/instance/instance.model.js').InstanceModel} */
986
- const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance;
1014
+ try {
1015
+ /** @type {import('../api/instance/instance.model.js').InstanceModel} */
1016
+ const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance;
987
1017
 
988
- await Instance.deleteMany();
989
- logger.info('Cleared existing instance metadata');
1018
+ await Instance.deleteMany();
1019
+ logger.info('Cleared existing instance metadata');
990
1020
 
991
- for (const _deployId of deployList) {
992
- const deployId = _deployId.trim();
993
- if (!deployId) continue;
1021
+ for (const _deployId of deployList) {
1022
+ const deployId = _deployId.trim();
1023
+ if (!deployId) continue;
994
1024
 
995
- logger.info('Processing deployment for metadata', { deployId });
1025
+ logger.info('Processing deployment for metadata', { deployId });
996
1026
 
997
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
998
- if (!fs.existsSync(confServerPath)) {
999
- logger.warn('Configuration not found for deployment', { deployId, path: confServerPath });
1000
- continue;
1001
- }
1027
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1028
+ if (!fs.existsSync(confServerPath)) {
1029
+ logger.warn('Configuration not found for deployment', { deployId, path: confServerPath });
1030
+ continue;
1031
+ }
1002
1032
 
1003
- const confServer = loadReplicas(deployId, loadConfServerJson(confServerPath, { resolve: true }));
1004
- const router = await Underpost.deploy.routerFactory(deployId, env);
1005
- const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
1033
+ const confServer = loadReplicas(deployId, loadConfServerJson(confServerPath, { resolve: true }));
1034
+ const router = await Underpost.deploy.routerFactory(deployId, env);
1035
+ const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
1006
1036
 
1007
- for (const host of Object.keys(confServer)) {
1008
- for (const { path, port } of pathPortAssignmentData[host]) {
1009
- if (!confServer[host][path]) continue;
1037
+ for (const host of Object.keys(confServer)) {
1038
+ for (const { path, port } of pathPortAssignmentData[host]) {
1039
+ if (!confServer[host][path]) continue;
1010
1040
 
1011
- const { client, runtime, apis, peer } = confServer[host][path];
1041
+ const { client, runtime, apis, peer } = confServer[host][path];
1012
1042
 
1013
- // Save main instance
1014
- {
1015
- const body = {
1016
- deployId,
1017
- host,
1018
- path,
1019
- port,
1020
- client,
1021
- runtime,
1022
- apis,
1023
- };
1043
+ // Save main instance
1044
+ {
1045
+ const body = {
1046
+ deployId,
1047
+ host,
1048
+ path,
1049
+ port,
1050
+ client,
1051
+ runtime,
1052
+ apis,
1053
+ };
1054
+
1055
+ logger.info('Saving instance metadata', body);
1056
+ await new Instance(body).save();
1057
+ }
1024
1058
 
1025
- logger.info('Saving instance metadata', body);
1026
- await new Instance(body).save();
1059
+ // Save peer instance if exists
1060
+ if (peer) {
1061
+ const body = {
1062
+ deployId,
1063
+ host,
1064
+ path: path === '/' ? '/peer' : `${path}/peer`,
1065
+ port: port + 1,
1066
+ runtime: 'nodejs',
1067
+ };
1068
+
1069
+ logger.info('Saving peer instance metadata', body);
1070
+ await new Instance(body).save();
1071
+ }
1027
1072
  }
1073
+ }
1028
1074
 
1029
- // Save peer instance if exists
1030
- if (peer) {
1075
+ // Process additional instances
1076
+ const confInstancesPath = `./engine-private/conf/${deployId}/conf.instances.json`;
1077
+ if (fs.existsSync(confInstancesPath)) {
1078
+ const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
1079
+ for (const instance of confInstances) {
1080
+ const { id, host, path, fromPort, metadata } = instance;
1081
+ const { runtime } = metadata;
1031
1082
  const body = {
1032
1083
  deployId,
1033
1084
  host,
1034
- path: path === '/' ? '/peer' : `${path}/peer`,
1035
- port: port + 1,
1036
- runtime: 'nodejs',
1085
+ path,
1086
+ port: fromPort,
1087
+ client: id,
1088
+ runtime,
1037
1089
  };
1038
-
1039
- logger.info('Saving peer instance metadata', body);
1090
+ logger.info('Saving additional instance metadata', body);
1040
1091
  await new Instance(body).save();
1041
1092
  }
1042
1093
  }
1043
1094
  }
1044
-
1045
- // Process additional instances
1046
- const confInstancesPath = `./engine-private/conf/${deployId}/conf.instances.json`;
1047
- if (fs.existsSync(confInstancesPath)) {
1048
- const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
1049
- for (const instance of confInstances) {
1050
- const { id, host, path, fromPort, metadata } = instance;
1051
- const { runtime } = metadata;
1052
- const body = {
1053
- deployId,
1054
- host,
1055
- path,
1056
- port: fromPort,
1057
- client: id,
1058
- runtime,
1059
- };
1060
- logger.info('Saving additional instance metadata', body);
1061
- await new Instance(body).save();
1062
- }
1063
- }
1095
+ } catch (error) {
1096
+ logger.error('Failed to create instance metadata', { error: error.message });
1097
+ throw error;
1064
1098
  }
1065
- } catch (error) {
1066
- logger.error('Failed to create instance metadata', { error: error.message });
1067
- throw error;
1068
- }
1069
1099
 
1070
- try {
1071
- const cronDeployPath = './engine-private/deploy/dd.cron';
1072
- if (!fs.existsSync(cronDeployPath)) {
1073
- logger.warn('Cron deploy file not found', { path: cronDeployPath });
1074
- return;
1075
- }
1100
+ try {
1101
+ const cronDeployPath = './engine-private/deploy/dd.cron';
1102
+ if (!fs.existsSync(cronDeployPath)) {
1103
+ logger.warn('Cron deploy file not found', { path: cronDeployPath });
1104
+ return;
1105
+ }
1076
1106
 
1077
- const cronDeployId = fs.readFileSync(cronDeployPath, 'utf8').trim();
1078
- const confCronPath = `./engine-private/conf/${cronDeployId}/conf.cron.json`;
1107
+ const cronDeployId = fs.readFileSync(cronDeployPath, 'utf8').trim();
1108
+ const confCronPath = `./engine-private/conf/${cronDeployId}/conf.cron.json`;
1079
1109
 
1080
- if (!fs.existsSync(confCronPath)) {
1081
- logger.warn('Cron configuration not found', { path: confCronPath });
1082
- return;
1083
- }
1110
+ if (!fs.existsSync(confCronPath)) {
1111
+ logger.warn('Cron configuration not found', { path: confCronPath });
1112
+ return;
1113
+ }
1084
1114
 
1085
- const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
1115
+ const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
1086
1116
 
1087
- await DataBaseProvider.load({ apis: ['cron'], host, path, db });
1117
+ await DataBaseProvider.load({ apis: ['cron'], host, path, db });
1088
1118
 
1089
- /** @type {import('../api/cron/cron.model.js').CronModel} */
1090
- const Cron = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Cron;
1119
+ /** @type {import('../api/cron/cron.model.js').CronModel} */
1120
+ const Cron = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Cron;
1091
1121
 
1092
- await Cron.deleteMany();
1093
- logger.info('Cleared existing cron metadata');
1122
+ await Cron.deleteMany();
1123
+ logger.info('Cleared existing cron metadata');
1094
1124
 
1095
- for (const jobId of Object.keys(confCron.jobs)) {
1096
- const body = {
1097
- jobId,
1098
- deployId: Underpost.cron.getRelatedDeployIdList(jobId),
1099
- expression: confCron.jobs[jobId].expression,
1100
- enabled: confCron.jobs[jobId].enabled,
1101
- };
1102
- logger.info('Saving cron metadata', body);
1103
- await new Cron(body).save();
1125
+ for (const jobId of Object.keys(confCron.jobs)) {
1126
+ const body = {
1127
+ jobId,
1128
+ deployId: Underpost.cron.getRelatedDeployIdList(jobId),
1129
+ expression: confCron.jobs[jobId].expression,
1130
+ enabled: confCron.jobs[jobId].enabled,
1131
+ };
1132
+ logger.info('Saving cron metadata', body);
1133
+ await new Cron(body).save();
1134
+ }
1135
+ } catch (error) {
1136
+ logger.error('Failed to create cron metadata', { error: error.message });
1104
1137
  }
1138
+
1139
+ await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
1140
+ logger.info('Cluster metadata creation completed');
1105
1141
  } catch (error) {
1106
- logger.error('Failed to create cron metadata', { error: error.message });
1142
+ logger.error('Cluster metadata creation failed', { error: error.message });
1143
+ throw error;
1107
1144
  }
1108
-
1109
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
1110
- logger.info('Cluster metadata creation completed');
1111
1145
  },
1112
1146
 
1113
1147
  /**
@@ -1121,6 +1155,7 @@ class UnderpostDB {
1121
1155
  * @param {string} [options.hosts=''] - Comma-separated list of hosts to filter.
1122
1156
  * @param {string} [options.paths=''] - Comma-separated list of paths to filter.
1123
1157
  * @param {boolean} [options.dryRun=false] - If true, only reports what would be deleted.
1158
+ * @param {boolean} [options.dev=false] - Development mode flag.
1124
1159
  * @return {Promise<void>} Resolves when clean operation is complete.
1125
1160
  */
1126
1161
  async cleanFsCollection(
@@ -1129,213 +1164,220 @@ class UnderpostDB {
1129
1164
  hosts: '',
1130
1165
  paths: '',
1131
1166
  dryRun: false,
1167
+ dev: false,
1132
1168
  },
1133
1169
  ) {
1134
- loadCronDeployEnv();
1135
- if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
1136
-
1137
- logger.info('Starting File collection cleanup', { deployList, options });
1170
+ const firstDeployId = deployList !== 'dd' ? deployList.split(',')[0].trim() : '';
1171
+ try {
1172
+ loadCronDeployEnv();
1173
+ if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
1138
1174
 
1139
- // Load file.ref.json to know which models reference File
1140
- const fileRefPath = './src/api/file/file.ref.json';
1141
- if (!fs.existsSync(fileRefPath)) {
1142
- logger.error('file.ref.json not found', { path: fileRefPath });
1143
- return;
1144
- }
1175
+ logger.info('Starting File collection cleanup', { deployList, options });
1145
1176
 
1146
- const fileRefData = JSON.parse(fs.readFileSync(fileRefPath, 'utf8'));
1147
- logger.info('Loaded file reference configuration', { apis: fileRefData.length });
1177
+ // Load file.ref.json to know which models reference File
1178
+ const fileRefPath = './src/api/file/file.ref.json';
1179
+ if (!fs.existsSync(fileRefPath)) {
1180
+ logger.error('file.ref.json not found', { path: fileRefPath });
1181
+ return;
1182
+ }
1148
1183
 
1149
- // Filter hosts and paths if specified
1150
- const filterHosts = options.hosts ? options.hosts.split(',').map((h) => h.trim()) : [];
1151
- const filterPaths = options.paths ? options.paths.split(',').map((p) => p.trim()) : [];
1184
+ const fileRefData = JSON.parse(fs.readFileSync(fileRefPath, 'utf8'));
1185
+ logger.info('Loaded file reference configuration', { apis: fileRefData.length });
1152
1186
 
1153
- // Track all connections to close them at the end
1154
- const connectionsToClose = [];
1187
+ // Filter hosts and paths if specified
1188
+ const filterHosts = options.hosts ? options.hosts.split(',').map((h) => h.trim()) : [];
1189
+ const filterPaths = options.paths ? options.paths.split(',').map((p) => p.trim()) : [];
1155
1190
 
1156
- for (const _deployId of deployList.split(',')) {
1157
- const deployId = _deployId.trim();
1158
- if (!deployId) continue;
1191
+ // Track all connections to close them at the end
1192
+ const connectionsToClose = [];
1159
1193
 
1160
- logger.info('Processing deployment for File cleanup', { deployId });
1194
+ for (const _deployId of deployList.split(',')) {
1195
+ const deployId = _deployId.trim();
1196
+ if (!deployId) continue;
1161
1197
 
1162
- // Load server configuration
1163
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1164
- if (!fs.existsSync(confServerPath)) {
1165
- logger.error('Configuration file not found', { path: confServerPath });
1166
- continue;
1167
- }
1198
+ logger.info('Processing deployment for File cleanup', { deployId });
1168
1199
 
1169
- const confServer = loadConfServerJson(confServerPath, { resolve: true });
1200
+ // Load server configuration
1201
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1202
+ if (!fs.existsSync(confServerPath)) {
1203
+ logger.error('Configuration file not found', { path: confServerPath });
1204
+ continue;
1205
+ }
1170
1206
 
1171
- // Process each host+path combination
1172
- for (const host of Object.keys(confServer)) {
1173
- if (filterHosts.length > 0 && !filterHosts.includes(host)) continue;
1207
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
1174
1208
 
1175
- for (const path of Object.keys(confServer[host])) {
1176
- if (filterPaths.length > 0 && !filterPaths.includes(path)) continue;
1209
+ // Process each host+path combination
1210
+ for (const host of Object.keys(confServer)) {
1211
+ if (filterHosts.length > 0 && !filterHosts.includes(host)) continue;
1177
1212
 
1178
- const { db, apis } = confServer[host][path];
1179
- if (!db || !apis) continue;
1213
+ for (const path of Object.keys(confServer[host])) {
1214
+ if (filterPaths.length > 0 && !filterPaths.includes(path)) continue;
1180
1215
 
1181
- // Check if 'file' api is in the apis list
1182
- if (!apis.includes('file')) {
1183
- logger.info('Skipping - no file api in configuration', { host, path });
1184
- continue;
1185
- }
1216
+ const { db, apis } = confServer[host][path];
1217
+ if (!db || !apis) continue;
1186
1218
 
1187
- // logger.info('Processing host+path with file api', { host, path, db: db.name });
1188
-
1189
- try {
1190
- // Connect to database with retry
1191
- let dbProvider;
1192
- for (let attempt = 1; attempt <= 3; attempt++) {
1193
- try {
1194
- dbProvider = await DataBaseProvider.load({ apis, host, path, db });
1195
- break;
1196
- } catch (err) {
1197
- if (attempt === 3) throw err;
1198
- logger.warn('Database connection failed, retrying...', { attempt, host, path, error: err.message });
1199
- await timer(3000);
1200
- }
1201
- }
1202
- if (!dbProvider || !dbProvider.models) {
1203
- logger.error('Failed to load database provider', { host, path });
1219
+ // Check if 'file' api is in the apis list
1220
+ if (!apis.includes('file')) {
1221
+ logger.info('Skipping - no file api in configuration', { host, path });
1204
1222
  continue;
1205
1223
  }
1206
1224
 
1207
- const { models } = dbProvider;
1225
+ // logger.info('Processing host+path with file api', { host, path, db: db.name });
1226
+
1227
+ try {
1228
+ // Connect to database with retry
1229
+ let dbProvider;
1230
+ for (let attempt = 1; attempt <= 3; attempt++) {
1231
+ try {
1232
+ dbProvider = await DataBaseProvider.load({ apis, host, path, db });
1233
+ break;
1234
+ } catch (err) {
1235
+ if (attempt === 3) throw err;
1236
+ logger.warn('Database connection failed, retrying...', { attempt, host, path, error: err.message });
1237
+ await timer(3000);
1238
+ }
1239
+ }
1240
+ if (!dbProvider || !dbProvider.models) {
1241
+ logger.error('Failed to load database provider', { host, path });
1242
+ continue;
1243
+ }
1208
1244
 
1209
- // Track this connection for cleanup
1210
- connectionsToClose.push({ host, path, dbProvider });
1245
+ const { models } = dbProvider;
1211
1246
 
1212
- // Check if File model exists
1213
- if (!models.File) {
1214
- logger.warn('File model not loaded', { host, path });
1215
- continue;
1216
- }
1247
+ // Track this connection for cleanup
1248
+ connectionsToClose.push({ host, path, dbProvider });
1217
1249
 
1218
- // Get all File documents
1219
- const allFiles = await models.File.find({}, '_id').lean();
1220
- logger.info('Found File documents', { count: allFiles.length, host, path });
1250
+ // Check if File model exists
1251
+ if (!models.File) {
1252
+ logger.warn('File model not loaded', { host, path });
1253
+ continue;
1254
+ }
1221
1255
 
1222
- if (allFiles.length === 0) continue;
1256
+ // Get all File documents
1257
+ const allFiles = await models.File.find({}, '_id').lean();
1258
+ logger.info('Found File documents', { count: allFiles.length, host, path });
1223
1259
 
1224
- // Track which File IDs are referenced
1225
- const referencedFileIds = new Set();
1260
+ if (allFiles.length === 0) continue;
1226
1261
 
1227
- // Check each API from file.ref.json
1228
- for (const refConfig of fileRefData) {
1229
- const { api, model: modelFields } = refConfig;
1262
+ // Track which File IDs are referenced
1263
+ const referencedFileIds = new Set();
1230
1264
 
1231
- // Check if this API is loaded in current context
1232
- const modelName = api
1233
- .split('-')
1234
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1235
- .join('');
1236
- const Model = models[modelName];
1265
+ // Check each API from file.ref.json
1266
+ for (const refConfig of fileRefData) {
1267
+ const { api, model: modelFields } = refConfig;
1237
1268
 
1238
- if (!Model) {
1239
- logger.debug('Model not loaded in current context', { api, modelName, host, path });
1240
- continue;
1241
- }
1269
+ // Check if this API is loaded in current context
1270
+ const modelName = api
1271
+ .split('-')
1272
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1273
+ .join('');
1274
+ const Model = models[modelName];
1275
+
1276
+ if (!Model) {
1277
+ logger.debug('Model not loaded in current context', { api, modelName, host, path });
1278
+ continue;
1279
+ }
1242
1280
 
1243
- logger.info('Checking references in model', { api, modelName });
1281
+ logger.info('Checking references in model', { api, modelName });
1244
1282
 
1245
- // Helper function to recursively check field references
1246
- const checkFieldReferences = async (fieldPath, fieldConfig) => {
1247
- for (const [fieldName, fieldValue] of Object.entries(fieldConfig)) {
1248
- const currentPath = fieldPath ? `${fieldPath}.${fieldName}` : fieldName;
1283
+ // Helper function to recursively check field references
1284
+ const checkFieldReferences = async (fieldPath, fieldConfig) => {
1285
+ for (const [fieldName, fieldValue] of Object.entries(fieldConfig)) {
1286
+ const currentPath = fieldPath ? `${fieldPath}.${fieldName}` : fieldName;
1249
1287
 
1250
- if (fieldValue === true) {
1251
- // This is a File reference field
1252
- const query = {};
1253
- query[currentPath] = { $exists: true, $ne: null };
1288
+ if (fieldValue === true) {
1289
+ // This is a File reference field
1290
+ const query = {};
1291
+ query[currentPath] = { $exists: true, $ne: null };
1254
1292
 
1255
- const docs = await Model.find(query, currentPath).lean();
1293
+ const docs = await Model.find(query, currentPath).lean();
1256
1294
 
1257
- for (const doc of docs) {
1258
- // Navigate to the nested field
1259
- const parts = currentPath.split('.');
1260
- let value = doc;
1261
- for (const part of parts) {
1262
- value = value?.[part];
1263
- }
1295
+ for (const doc of docs) {
1296
+ // Navigate to the nested field
1297
+ const parts = currentPath.split('.');
1298
+ let value = doc;
1299
+ for (const part of parts) {
1300
+ value = value?.[part];
1301
+ }
1264
1302
 
1265
- if (value) {
1266
- if (Array.isArray(value)) {
1267
- value.forEach((id) => id && referencedFileIds.add(id.toString()));
1268
- } else {
1269
- referencedFileIds.add(value.toString());
1303
+ if (value) {
1304
+ if (Array.isArray(value)) {
1305
+ value.forEach((id) => id && referencedFileIds.add(id.toString()));
1306
+ } else {
1307
+ referencedFileIds.add(value.toString());
1308
+ }
1270
1309
  }
1271
1310
  }
1272
- }
1273
1311
 
1274
- logger.info('Found references', {
1275
- model: modelName,
1276
- field: currentPath,
1277
- count: docs.length,
1278
- });
1279
- } else if (typeof fieldValue === 'object') {
1280
- // Nested object, recurse
1281
- await checkFieldReferences(currentPath, fieldValue);
1312
+ logger.info('Found references', {
1313
+ model: modelName,
1314
+ field: currentPath,
1315
+ count: docs.length,
1316
+ });
1317
+ } else if (typeof fieldValue === 'object') {
1318
+ // Nested object, recurse
1319
+ await checkFieldReferences(currentPath, fieldValue);
1320
+ }
1282
1321
  }
1283
- }
1284
- };
1322
+ };
1285
1323
 
1286
- await checkFieldReferences('', modelFields);
1287
- }
1288
-
1289
- logger.info('Total referenced File IDs', { count: referencedFileIds.size, host, path });
1324
+ await checkFieldReferences('', modelFields);
1325
+ }
1290
1326
 
1291
- // Find orphaned files
1292
- const orphanedFiles = allFiles.filter((file) => !referencedFileIds.has(file._id.toString()));
1327
+ logger.info('Total referenced File IDs', { count: referencedFileIds.size, host, path });
1293
1328
 
1294
- if (orphanedFiles.length === 0) {
1295
- logger.info('No orphaned files found', { host, path });
1296
- } else {
1297
- logger.info('Found orphaned files', { count: orphanedFiles.length, host, path });
1329
+ // Find orphaned files
1330
+ const orphanedFiles = allFiles.filter((file) => !referencedFileIds.has(file._id.toString()));
1298
1331
 
1299
- if (options.dryRun) {
1300
- logger.info('Dry run - would delete files', {
1301
- count: orphanedFiles.length,
1302
- ids: orphanedFiles.map((f) => f._id.toString()),
1303
- });
1332
+ if (orphanedFiles.length === 0) {
1333
+ logger.info('No orphaned files found', { host, path });
1304
1334
  } else {
1305
- const orphanedIds = orphanedFiles.map((f) => f._id);
1306
- const deleteResult = await models.File.deleteMany({ _id: { $in: orphanedIds } });
1307
- logger.info('Deleted orphaned files', {
1308
- deletedCount: deleteResult.deletedCount,
1309
- host,
1310
- path,
1311
- });
1335
+ logger.info('Found orphaned files', { count: orphanedFiles.length, host, path });
1336
+
1337
+ if (options.dryRun) {
1338
+ logger.info('Dry run - would delete files', {
1339
+ count: orphanedFiles.length,
1340
+ ids: orphanedFiles.map((f) => f._id.toString()),
1341
+ });
1342
+ } else {
1343
+ const orphanedIds = orphanedFiles.map((f) => f._id);
1344
+ const deleteResult = await models.File.deleteMany({ _id: { $in: orphanedIds } });
1345
+ logger.info('Deleted orphaned files', {
1346
+ deletedCount: deleteResult.deletedCount,
1347
+ host,
1348
+ path,
1349
+ });
1350
+ }
1312
1351
  }
1352
+ } catch (error) {
1353
+ logger.error('Error processing host+path', {
1354
+ host,
1355
+ path,
1356
+ error: error.message,
1357
+ });
1313
1358
  }
1314
- } catch (error) {
1315
- logger.error('Error processing host+path', {
1316
- host,
1317
- path,
1318
- error: error.message,
1319
- });
1320
1359
  }
1321
1360
  }
1322
1361
  }
1323
- }
1324
1362
 
1325
- // Close all connections
1326
- logger.info('Closing all database connections', { count: connectionsToClose.length });
1327
- for (const { host, path, dbProvider } of connectionsToClose) {
1328
- try {
1329
- if (dbProvider && dbProvider.close) {
1330
- await dbProvider.close();
1331
- logger.info('Connection closed', { host, path });
1363
+ // Close all connections
1364
+ logger.info('Closing all database connections', { count: connectionsToClose.length });
1365
+ for (const { host, path, dbProvider } of connectionsToClose) {
1366
+ try {
1367
+ if (dbProvider && dbProvider.close) {
1368
+ await dbProvider.close();
1369
+ logger.info('Connection closed', { host, path });
1370
+ }
1371
+ } catch (error) {
1372
+ logger.error('Error closing connection', { host, path, error: error.message });
1332
1373
  }
1333
- } catch (error) {
1334
- logger.error('Error closing connection', { host, path, error: error.message });
1335
1374
  }
1336
- }
1337
1375
 
1338
- logger.info('File collection cleanup completed');
1376
+ logger.info('File collection cleanup completed');
1377
+ } catch (error) {
1378
+ logger.error('File collection cleanup failed', { error: error.message });
1379
+ throw error;
1380
+ }
1339
1381
  },
1340
1382
 
1341
1383
  /**
@@ -1354,6 +1396,7 @@ class UnderpostDB {
1354
1396
  * @param {boolean} [options.export=false] - Export metadata to backup.
1355
1397
  * @param {boolean} [options.instances=false] - Process instances collection.
1356
1398
  * @param {boolean} [options.crons=false] - Process crons collection.
1399
+ * @param {boolean} [options.dev=false] - Development mode flag.
1357
1400
  * @return {Promise<void>} Resolves when backup operation is complete.
1358
1401
  */
1359
1402
  async clusterMetadataBackupCallback(
@@ -1367,70 +1410,76 @@ class UnderpostDB {
1367
1410
  export: false,
1368
1411
  instances: false,
1369
1412
  crons: false,
1413
+ dev: false,
1370
1414
  },
1371
1415
  ) {
1372
- loadCronDeployEnv();
1373
- deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
1374
- host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
1375
- path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
1376
-
1377
- logger.info('Starting cluster metadata backup operation', {
1378
- deployId,
1379
- host,
1380
- path,
1381
- options,
1382
- });
1383
-
1384
- if (options.generate === true) {
1385
- logger.info('Generating cluster metadata');
1386
- await Underpost.db.clusterMetadataFactory(deployId, host, path);
1387
- }
1416
+ try {
1417
+ loadCronDeployEnv();
1418
+ deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
1419
+ host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
1420
+ path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
1421
+
1422
+ logger.info('Starting cluster metadata backup operation', {
1423
+ deployId,
1424
+ host,
1425
+ path,
1426
+ options,
1427
+ });
1388
1428
 
1389
- if (options.instances === true) {
1390
- const outputPath = './engine-private/instances';
1391
- if (!fs.existsSync(outputPath)) {
1392
- fs.mkdirSync(outputPath, { recursive: true });
1429
+ if (options.generate === true) {
1430
+ logger.info('Generating cluster metadata');
1431
+ await Underpost.db.clusterMetadataFactory(deployId, host, path);
1393
1432
  }
1394
- const collection = 'instances';
1395
1433
 
1396
- if (options.export === true) {
1397
- logger.info('Exporting instances collection', { outputPath });
1398
- shellExec(
1399
- `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1400
- );
1401
- }
1434
+ if (options.instances === true) {
1435
+ const outputPath = './engine-private/instances';
1436
+ if (!fs.existsSync(outputPath)) {
1437
+ fs.mkdirSync(outputPath, { recursive: true });
1438
+ }
1439
+ const collection = 'instances';
1402
1440
 
1403
- if (options.import === true) {
1404
- logger.info('Importing instances collection', { outputPath });
1405
- shellExec(
1406
- `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1407
- );
1408
- }
1409
- }
1441
+ if (options.export === true) {
1442
+ logger.info('Exporting instances collection', { outputPath });
1443
+ shellExec(
1444
+ `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1445
+ );
1446
+ }
1410
1447
 
1411
- if (options.crons === true) {
1412
- const outputPath = './engine-private/crons';
1413
- if (!fs.existsSync(outputPath)) {
1414
- fs.mkdirSync(outputPath, { recursive: true });
1448
+ if (options.import === true) {
1449
+ logger.info('Importing instances collection', { outputPath });
1450
+ shellExec(
1451
+ `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1452
+ );
1453
+ }
1415
1454
  }
1416
- const collection = 'crons';
1417
1455
 
1418
- if (options.export === true) {
1419
- logger.info('Exporting crons collection', { outputPath });
1420
- shellExec(
1421
- `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1422
- );
1423
- }
1456
+ if (options.crons === true) {
1457
+ const outputPath = './engine-private/crons';
1458
+ if (!fs.existsSync(outputPath)) {
1459
+ fs.mkdirSync(outputPath, { recursive: true });
1460
+ }
1461
+ const collection = 'crons';
1462
+
1463
+ if (options.export === true) {
1464
+ logger.info('Exporting crons collection', { outputPath });
1465
+ shellExec(
1466
+ `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1467
+ );
1468
+ }
1424
1469
 
1425
- if (options.import === true) {
1426
- logger.info('Importing crons collection', { outputPath });
1427
- shellExec(
1428
- `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1429
- );
1470
+ if (options.import === true) {
1471
+ logger.info('Importing crons collection', { outputPath });
1472
+ shellExec(
1473
+ `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1474
+ );
1475
+ }
1430
1476
  }
1431
- }
1432
1477
 
1433
- logger.info('Cluster metadata backup operation completed');
1478
+ logger.info('Cluster metadata backup operation completed');
1479
+ } catch (error) {
1480
+ logger.error('Cluster metadata backup operation failed', { error: error.message });
1481
+ throw error;
1482
+ }
1434
1483
  },
1435
1484
  };
1436
1485
  }