cyberia 3.1.3 → 3.2.9

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.
Files changed (377) hide show
  1. package/.env.example +0 -2
  2. package/.github/workflows/engine-cyberia.cd.yml +10 -8
  3. package/.github/workflows/engine-cyberia.ci.yml +12 -29
  4. package/.github/workflows/ghpkg.ci.yml +4 -4
  5. package/.github/workflows/npmpkg.ci.yml +28 -11
  6. package/.github/workflows/publish.ci.yml +21 -2
  7. package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -5
  8. package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
  9. package/.github/workflows/release.cd.yml +14 -10
  10. package/CHANGELOG.md +783 -1
  11. package/CLI-HELP.md +95 -18
  12. package/Dockerfile +0 -2
  13. package/README.md +290 -220
  14. package/bin/build.js +24 -7
  15. package/bin/cyberia.js +2838 -252
  16. package/bin/deploy.js +747 -125
  17. package/bin/file.js +9 -0
  18. package/bin/index.js +2838 -252
  19. package/bin/vs.js +1 -1
  20. package/conf.js +99 -65
  21. package/deployment.yaml +18 -164
  22. package/hardhat/hardhat.config.js +13 -13
  23. package/hardhat/ignition/modules/ObjectLayerToken.js +1 -1
  24. package/hardhat/package-lock.json +2559 -5864
  25. package/hardhat/package.json +14 -23
  26. package/hardhat/scripts/deployObjectLayerToken.js +1 -1
  27. package/hardhat/test/ObjectLayerToken.js +4 -2
  28. package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
  29. package/hardhat/types/ethers-contracts/common.ts +92 -0
  30. package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
  31. package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
  32. package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
  33. package/hardhat/types/ethers-contracts/index.ts +6 -0
  34. package/jsconfig.json +1 -1
  35. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +6 -5
  36. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +6 -5
  37. package/manifests/deployment/dd-cyberia-development/deployment.yaml +18 -164
  38. package/manifests/deployment/dd-cyberia-development/proxy.yaml +7 -79
  39. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  40. package/manifests/deployment/dd-test-development/deployment.yaml +112 -28
  41. package/manifests/deployment/dd-test-development/proxy.yaml +46 -1
  42. package/manifests/deployment/playwright/deployment.yaml +1 -1
  43. package/nodemon.json +1 -1
  44. package/package.json +39 -24
  45. package/proxy.yaml +7 -79
  46. package/scripts/k3s-node-setup.sh +2 -2
  47. package/scripts/nat-iptables.sh +103 -18
  48. package/scripts/rhel-grpc-setup.sh +56 -0
  49. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +58 -14
  50. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +23 -14
  51. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  52. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +148 -20
  53. package/src/api/core/core.controller.js +10 -10
  54. package/src/api/core/core.service.js +10 -10
  55. package/src/api/crypto/crypto.controller.js +8 -8
  56. package/src/api/crypto/crypto.service.js +8 -8
  57. package/src/api/cyberia-action/cyberia-action.controller.js +74 -0
  58. package/src/api/cyberia-action/cyberia-action.model.js +87 -0
  59. package/src/api/cyberia-action/cyberia-action.router.js +27 -0
  60. package/src/api/cyberia-action/cyberia-action.service.js +42 -0
  61. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +93 -0
  62. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
  63. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +29 -0
  64. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +51 -0
  65. package/src/api/cyberia-entity/cyberia-entity.controller.js +74 -0
  66. package/src/api/cyberia-entity/cyberia-entity.model.js +24 -0
  67. package/src/api/cyberia-entity/cyberia-entity.router.js +27 -0
  68. package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
  69. package/src/api/cyberia-instance/cyberia-fallback-world.js +178 -0
  70. package/src/api/cyberia-instance/cyberia-instance.controller.js +92 -0
  71. package/src/api/cyberia-instance/cyberia-instance.model.js +87 -0
  72. package/src/api/cyberia-instance/cyberia-instance.router.js +63 -0
  73. package/src/api/cyberia-instance/cyberia-instance.service.js +156 -0
  74. package/src/api/cyberia-instance/cyberia-portal-connector.js +260 -0
  75. package/src/api/cyberia-instance/cyberia-world-generator.js +505 -0
  76. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +74 -0
  77. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +574 -0
  78. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +231 -0
  79. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +27 -0
  80. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +46 -0
  81. package/src/api/cyberia-map/cyberia-map.controller.js +79 -0
  82. package/src/api/cyberia-map/cyberia-map.model.js +30 -0
  83. package/src/api/cyberia-map/cyberia-map.router.js +40 -0
  84. package/src/api/cyberia-map/cyberia-map.service.js +74 -0
  85. package/src/api/cyberia-quest/cyberia-quest.controller.js +74 -0
  86. package/src/api/cyberia-quest/cyberia-quest.model.js +67 -0
  87. package/src/api/cyberia-quest/cyberia-quest.router.js +27 -0
  88. package/src/api/cyberia-quest/cyberia-quest.service.js +42 -0
  89. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +74 -0
  90. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +49 -0
  91. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +27 -0
  92. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +42 -0
  93. package/src/api/default/default.controller.js +10 -10
  94. package/src/api/default/default.service.js +10 -10
  95. package/src/api/document/document.controller.js +12 -12
  96. package/src/api/document/document.model.js +10 -16
  97. package/src/api/file/file.controller.js +8 -8
  98. package/src/api/file/file.model.js +10 -10
  99. package/src/api/file/file.ref.json +18 -0
  100. package/src/api/file/file.service.js +36 -36
  101. package/src/api/instance/instance.controller.js +10 -10
  102. package/src/api/instance/instance.model.js +4 -10
  103. package/src/api/instance/instance.service.js +10 -10
  104. package/src/api/ipfs/ipfs.controller.js +15 -36
  105. package/src/api/ipfs/ipfs.model.js +47 -47
  106. package/src/api/ipfs/ipfs.router.js +8 -13
  107. package/src/api/ipfs/ipfs.service.js +67 -129
  108. package/src/api/object-layer/object-layer.controller.js +12 -12
  109. package/src/api/object-layer/object-layer.model.js +4 -17
  110. package/src/api/object-layer/object-layer.router.js +30 -0
  111. package/src/api/object-layer/object-layer.service.js +126 -43
  112. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +10 -10
  113. package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +6 -16
  114. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +18 -14
  115. package/src/api/test/test.controller.js +8 -8
  116. package/src/api/test/test.service.js +8 -8
  117. package/src/api/user/guest.service.js +99 -0
  118. package/src/api/user/user.controller.js +6 -6
  119. package/src/api/user/user.model.js +8 -13
  120. package/src/api/user/user.service.js +11 -27
  121. package/src/cli/cluster.js +68 -21
  122. package/src/cli/db.js +753 -825
  123. package/src/cli/deploy.js +215 -125
  124. package/src/cli/env.js +29 -0
  125. package/src/cli/fs.js +82 -8
  126. package/src/cli/image.js +43 -1
  127. package/src/cli/index.js +74 -3
  128. package/src/cli/kubectl.js +211 -0
  129. package/src/cli/release.js +340 -0
  130. package/src/cli/repository.js +475 -74
  131. package/src/cli/run.js +582 -43
  132. package/src/cli/secrets.js +73 -0
  133. package/src/cli/ssh.js +1 -1
  134. package/src/cli/static.js +43 -115
  135. package/src/cli/test.js +3 -3
  136. package/src/client/Cryptokoyn.index.js +18 -22
  137. package/src/client/CyberiaPortal.index.js +19 -24
  138. package/src/client/Default.index.js +21 -34
  139. package/src/client/Itemledger.index.js +20 -27
  140. package/src/client/Underpost.index.js +19 -24
  141. package/src/client/components/core/404.js +4 -4
  142. package/src/client/components/core/500.js +4 -4
  143. package/src/client/components/core/Account.js +73 -60
  144. package/src/client/components/core/AgGrid.js +23 -33
  145. package/src/client/components/core/Alert.js +12 -13
  146. package/src/client/components/core/AppStore.js +69 -0
  147. package/src/client/components/core/Auth.js +35 -37
  148. package/src/client/components/core/Badge.js +7 -13
  149. package/src/client/components/core/BtnIcon.js +15 -17
  150. package/src/client/components/core/CalendarCore.js +43 -64
  151. package/src/client/components/core/Chat.js +13 -15
  152. package/src/client/components/core/ClientEvents.js +87 -0
  153. package/src/client/components/core/ColorPaletteElement.js +309 -0
  154. package/src/client/components/core/Content.js +17 -14
  155. package/src/client/components/core/Css.js +15 -71
  156. package/src/client/components/core/CssCore.js +12 -16
  157. package/src/client/components/core/D3Chart.js +4 -4
  158. package/src/client/components/core/Docs.js +64 -91
  159. package/src/client/components/core/DropDown.js +194 -96
  160. package/src/client/components/core/EventBus.js +92 -0
  161. package/src/client/components/core/EventsUI.js +14 -17
  162. package/src/client/components/core/FileExplorer.js +96 -228
  163. package/src/client/components/core/FullScreen.js +47 -75
  164. package/src/client/components/core/Input.js +24 -69
  165. package/src/client/components/core/Keyboard.js +26 -19
  166. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  167. package/src/client/components/core/LoadingAnimation.js +25 -31
  168. package/src/client/components/core/LogIn.js +43 -43
  169. package/src/client/components/core/LogOut.js +25 -16
  170. package/src/client/components/core/Modal.js +462 -179
  171. package/src/client/components/core/NotificationManager.js +14 -18
  172. package/src/client/components/core/Panel.js +54 -51
  173. package/src/client/components/core/PanelForm.js +44 -144
  174. package/src/client/components/core/Polyhedron.js +110 -214
  175. package/src/client/components/core/PublicProfile.js +39 -32
  176. package/src/client/components/core/Recover.js +48 -44
  177. package/src/client/components/core/Responsive.js +88 -32
  178. package/src/client/components/core/RichText.js +9 -18
  179. package/src/client/components/core/Router.js +24 -3
  180. package/src/client/components/core/SearchBox.js +37 -37
  181. package/src/client/components/core/SignUp.js +39 -30
  182. package/src/client/components/core/SocketIo.js +112 -30
  183. package/src/client/components/core/SocketIoHandler.js +75 -0
  184. package/src/client/components/core/Stream.js +143 -95
  185. package/src/client/components/core/ToggleSwitch.js +8 -20
  186. package/src/client/components/core/ToolTip.js +5 -17
  187. package/src/client/components/core/Translate.js +56 -59
  188. package/src/client/components/core/Validator.js +26 -16
  189. package/src/client/components/core/Wallet.js +15 -26
  190. package/src/client/components/core/Webhook.js +40 -7
  191. package/src/client/components/core/Worker.js +163 -27
  192. package/src/client/components/core/windowGetDimensions.js +7 -7
  193. package/src/client/components/cryptokoyn/{MenuCryptokoyn.js → AppShellCryptokoyn.js} +59 -59
  194. package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
  195. package/src/client/components/cryptokoyn/CssCryptokoyn.js +15 -15
  196. package/src/client/components/cryptokoyn/LogInCryptokoyn.js +9 -7
  197. package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +8 -6
  198. package/src/client/components/cryptokoyn/RouterCryptokoyn.js +37 -0
  199. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +4 -4
  200. package/src/client/components/cryptokoyn/SignUpCryptokoyn.js +6 -4
  201. package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +3 -51
  202. package/src/client/components/cyberia/InstanceEngineCyberia.js +781 -0
  203. package/src/client/components/cyberia/MapEngineCyberia.js +1836 -2
  204. package/src/client/components/cyberia/ObjectLayerEngine.js +19 -0
  205. package/src/client/components/cyberia/ObjectLayerEngineModal.js +1220 -99
  206. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +252 -316
  207. package/src/client/components/cyberia-portal/{MenuCyberiaPortal.js → AppShellCyberiaPortal.js} +136 -103
  208. package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
  209. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +462 -32
  210. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +15 -15
  211. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +9 -7
  212. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +8 -6
  213. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +4 -4
  214. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +60 -0
  215. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +4 -4
  216. package/src/client/components/cyberia-portal/SignUpCyberiaPortal.js +6 -4
  217. package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +3 -49
  218. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -4
  219. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +91 -91
  220. package/src/client/components/default/AppStoreDefault.js +5 -0
  221. package/src/client/components/default/CssDefault.js +12 -12
  222. package/src/client/components/default/LogInDefault.js +9 -7
  223. package/src/client/components/default/LogOutDefault.js +8 -6
  224. package/src/client/components/default/RouterDefault.js +47 -0
  225. package/src/client/components/default/SettingsDefault.js +4 -4
  226. package/src/client/components/default/SignUpDefault.js +6 -4
  227. package/src/client/components/default/SocketIoDefault.js +3 -51
  228. package/src/client/components/default/TranslateDefault.js +3 -3
  229. package/src/client/components/itemledger/{MenuItemledger.js → AppShellItemledger.js} +59 -59
  230. package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
  231. package/src/client/components/itemledger/CssItemledger.js +15 -15
  232. package/src/client/components/itemledger/LogInItemledger.js +9 -7
  233. package/src/client/components/itemledger/LogOutItemledger.js +8 -6
  234. package/src/client/components/itemledger/RouterItemledger.js +38 -0
  235. package/src/client/components/itemledger/SettingsItemledger.js +4 -4
  236. package/src/client/components/itemledger/SignUpItemledger.js +6 -4
  237. package/src/client/components/itemledger/SocketIoItemledger.js +3 -51
  238. package/src/client/components/itemledger/TranslateItemledger.js +3 -3
  239. package/src/client/components/underpost/{MenuUnderpost.js → AppShellUnderpost.js} +92 -92
  240. package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
  241. package/src/client/components/underpost/CssUnderpost.js +14 -14
  242. package/src/client/components/underpost/CyberpunkBloggerUnderpost.js +4 -4
  243. package/src/client/components/underpost/DocumentSearchProvider.js +1 -1
  244. package/src/client/components/underpost/LabGalleryUnderpost.js +12 -15
  245. package/src/client/components/underpost/LogInUnderpost.js +9 -7
  246. package/src/client/components/underpost/LogOutUnderpost.js +8 -6
  247. package/src/client/components/underpost/RouterUnderpost.js +45 -0
  248. package/src/client/components/underpost/SettingsUnderpost.js +4 -4
  249. package/src/client/components/underpost/SignUpUnderpost.js +6 -4
  250. package/src/client/components/underpost/SocketIoUnderpost.js +3 -51
  251. package/src/client/components/underpost/TranslateUnderpost.js +4 -4
  252. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +235 -0
  253. package/src/client/public/cyberia-docs/ARCHITECTURE.md +443 -0
  254. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +417 -0
  255. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +313 -0
  256. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +260 -0
  257. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +241 -0
  258. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +300 -0
  259. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +279 -0
  260. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +206 -0
  261. package/src/client/public/cyberia-docs/ROADMAP.md +240 -0
  262. package/src/client/public/cyberia-docs/WHITE-PAPER.md +732 -0
  263. package/src/client/services/atlas-sprite-sheet/atlas-sprite-sheet.service.js +14 -20
  264. package/src/client/services/core/core.service.js +35 -55
  265. package/src/client/services/crypto/crypto.service.js +8 -13
  266. package/src/client/services/cyberia-action/cyberia-action.service.js +99 -0
  267. package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +99 -0
  268. package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
  269. package/src/client/services/cyberia-entity/cyberia-entity.service.js +99 -0
  270. package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
  271. package/src/client/services/cyberia-instance/cyberia-instance.service.js +116 -0
  272. package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +99 -0
  273. package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
  274. package/src/client/services/cyberia-map/cyberia-map.service.js +120 -0
  275. package/src/client/services/cyberia-quest/cyberia-quest.service.js +99 -0
  276. package/src/client/services/cyberia-quest-progress/cyberia-quest-progress.service.js +99 -0
  277. package/src/client/services/default/default.management.js +159 -267
  278. package/src/client/services/default/default.service.js +10 -16
  279. package/src/client/services/document/document.service.js +14 -19
  280. package/src/client/services/file/file.service.js +8 -13
  281. package/src/client/services/instance/instance.management.js +6 -6
  282. package/src/client/services/instance/instance.service.js +10 -15
  283. package/src/client/services/ipfs/ipfs.service.js +14 -40
  284. package/src/client/services/object-layer/object-layer.management.js +14 -14
  285. package/src/client/services/object-layer/object-layer.service.js +39 -24
  286. package/src/client/services/object-layer-render-frames/object-layer-render-frames.service.js +10 -16
  287. package/src/client/services/test/test.service.js +8 -13
  288. package/src/client/services/user/guest.service.js +86 -0
  289. package/src/client/services/user/user.management.js +6 -6
  290. package/src/client/services/user/user.service.js +14 -20
  291. package/src/client/ssr/body/404.js +3 -3
  292. package/src/client/ssr/body/500.js +3 -3
  293. package/src/client/ssr/body/CacheControl.js +5 -2
  294. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  295. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +13 -6
  296. package/src/client/ssr/head/PwaItemledger.js +197 -60
  297. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  298. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  299. package/src/client/ssr/offline/Maintenance.js +12 -11
  300. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  301. package/src/client/ssr/pages/CyberiaServerMetrics.js +1 -1
  302. package/src/client/ssr/pages/Test.js +2 -2
  303. package/src/client/sw/core.sw.js +212 -0
  304. package/src/grpc/cyberia/grpc-server.js +642 -0
  305. package/src/index.js +24 -1
  306. package/src/runtime/cyberia-client/Dockerfile +80 -0
  307. package/src/runtime/cyberia-server/Dockerfile +37 -0
  308. package/src/runtime/express/Dockerfile +5 -1
  309. package/src/runtime/express/Express.js +18 -1
  310. package/src/runtime/lampp/Dockerfile +17 -5
  311. package/src/runtime/lampp/Lampp.js +27 -4
  312. package/src/runtime/wp/Dockerfile +62 -0
  313. package/src/runtime/wp/Wp.js +639 -0
  314. package/src/server/atlas-sprite-sheet-generator.js +4 -2
  315. package/src/server/auth.js +24 -1
  316. package/src/server/backup.js +37 -9
  317. package/src/server/client-build-docs.js +52 -46
  318. package/src/server/client-build.js +356 -82
  319. package/src/server/client-formatted.js +140 -57
  320. package/src/server/conf.js +29 -13
  321. package/src/server/cron.js +25 -23
  322. package/src/server/data-query.js +32 -20
  323. package/src/server/dns.js +24 -1
  324. package/src/server/ipfs-client.js +253 -89
  325. package/src/server/object-layer.js +150 -114
  326. package/src/server/peer.js +8 -0
  327. package/src/server/process.js +13 -27
  328. package/src/server/runtime.js +25 -1
  329. package/src/server/semantic-layer-generator-floor.js +319 -0
  330. package/src/server/semantic-layer-generator-resource.js +259 -0
  331. package/src/server/semantic-layer-generator-skin.js +1164 -0
  332. package/src/server/semantic-layer-generator.js +211 -542
  333. package/src/server/shape-generator.js +108 -0
  334. package/src/server/start.js +19 -5
  335. package/src/server/valkey.js +141 -235
  336. package/src/ws/IoInterface.js +1 -10
  337. package/src/ws/IoServer.js +14 -33
  338. package/src/ws/core/channels/core.ws.chat.js +65 -20
  339. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  340. package/src/ws/core/channels/core.ws.stream.js +90 -31
  341. package/src/ws/core/core.ws.connection.js +12 -33
  342. package/src/ws/core/core.ws.emit.js +10 -26
  343. package/src/ws/core/core.ws.server.js +25 -58
  344. package/src/ws/default/channels/default.ws.main.js +53 -12
  345. package/src/ws/default/default.ws.connection.js +26 -13
  346. package/src/ws/default/default.ws.server.js +30 -12
  347. package/tsconfig.docs.json +15 -0
  348. package/typedoc.dd-cyberia.json +29 -0
  349. package/typedoc.json +29 -0
  350. package/WHITE-PAPER.md +0 -1540
  351. package/hardhat/README.md +0 -531
  352. package/hardhat/WHITE-PAPER.md +0 -1540
  353. package/jsdoc.dd-cyberia.json +0 -59
  354. package/jsdoc.json +0 -59
  355. package/src/api/object-layer/README.md +0 -347
  356. package/src/client/components/core/ColorPalette.js +0 -5267
  357. package/src/client/components/core/JoyStick.js +0 -80
  358. package/src/client/components/cryptokoyn/CommonCryptokoyn.js +0 -29
  359. package/src/client/components/cryptokoyn/ElementsCryptokoyn.js +0 -38
  360. package/src/client/components/cryptokoyn/RoutesCryptokoyn.js +0 -39
  361. package/src/client/components/cyberia-portal/ElementsCyberiaPortal.js +0 -38
  362. package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +0 -58
  363. package/src/client/components/cyberia-portal/ServerCyberiaPortal.js +0 -136
  364. package/src/client/components/default/ElementsDefault.js +0 -38
  365. package/src/client/components/default/RoutesDefault.js +0 -49
  366. package/src/client/components/itemledger/CommonItemledger.js +0 -29
  367. package/src/client/components/itemledger/ElementsItemledger.js +0 -38
  368. package/src/client/components/itemledger/RoutesItemledger.js +0 -40
  369. package/src/client/components/underpost/CommonUnderpost.js +0 -29
  370. package/src/client/components/underpost/ElementsUnderpost.js +0 -38
  371. package/src/client/components/underpost/RoutesUnderpost.js +0 -47
  372. package/src/client/sw/default.sw.js +0 -127
  373. package/src/client/sw/template.sw.js +0 -84
  374. package/src/ws/core/management/core.ws.chat.js +0 -8
  375. package/src/ws/core/management/core.ws.mailer.js +0 -16
  376. package/src/ws/core/management/core.ws.stream.js +0 -8
  377. package/src/ws/default/management/default.ws.main.js +0 -8
package/src/cli/db.js CHANGED
@@ -13,23 +13,9 @@ import fs from 'fs-extra';
13
13
  import { DataBaseProvider } from '../db/DataBaseProvider.js';
14
14
  import { loadReplicas, pathPortAssignmentFactory, loadCronDeployEnv } from '../server/conf.js';
15
15
  import Underpost from '../index.js';
16
+ import { timer } from '../client/components/core/CommonJs.js';
16
17
  const logger = loggerFactory(import.meta);
17
18
 
18
- /**
19
- * Redacts credentials from shell command strings before logging.
20
- * Masks passwords in `-p<password>`, `--password=<password>`, and `-P <password>` patterns.
21
- * @param {string} cmd - The raw command string.
22
- * @memberof UnderpostDB
23
- * @returns {string} The command with credentials replaced by `***`.
24
- */
25
- const sanitizeCommand = (cmd) => {
26
- if (typeof cmd !== 'string') return cmd;
27
- return cmd
28
- .replace(/-p['"]?[^\s'"]+/g, '-p***')
29
- .replace(/--password=['"]?[^\s'"]+/g, '--password=***')
30
- .replace(/-P\s+['"]?[^\s'"]+/g, '-P ***');
31
- };
32
-
33
19
  /**
34
20
  * Constants for database operations
35
21
  * @constant {number} MAX_BACKUP_RETENTION - Maximum number of backups to retain
@@ -98,132 +84,6 @@ class UnderpostDB {
98
84
  * @memberof UnderpostDB
99
85
  */
100
86
  static API = {
101
- /**
102
- * Helper: Gets filtered pods based on criteria.
103
- * @method _getFilteredPods
104
- * @memberof UnderpostDB
105
- * @param {Object} criteria - Filter criteria.
106
- * @param {string} [criteria.podNames] - Comma-separated pod name patterns.
107
- * @param {string} [criteria.namespace='default'] - Kubernetes namespace.
108
- * @param {string} [criteria.deployId] - Deployment ID pattern.
109
- * @return {Array<PodInfo>} Filtered pod list.
110
- */
111
- _getFilteredPods(criteria = {}) {
112
- const { podNames, namespace = 'default', deployId } = criteria;
113
-
114
- try {
115
- // Get all pods using Underpost.deploy.get
116
- let pods = Underpost.deploy.get(deployId || '', 'pods', namespace);
117
-
118
- // Filter by pod names if specified
119
- if (podNames) {
120
- const patterns = podNames.split(',').map((p) => p.trim());
121
- pods = pods.filter((pod) => {
122
- return patterns.some((pattern) => {
123
- // Support wildcards
124
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
125
- return regex.test(pod.NAME);
126
- });
127
- });
128
- }
129
-
130
- logger.info(`Found ${pods.length} pod(s) matching criteria`, { criteria, podNames: pods.map((p) => p.NAME) });
131
- return pods;
132
- } catch (error) {
133
- logger.error('Error filtering pods', { error: error.message, criteria });
134
- return [];
135
- }
136
- },
137
-
138
- /**
139
- * Helper: Executes kubectl command with error handling.
140
- * @method _executeKubectl
141
- * @memberof UnderpostDB
142
- * @param {string} command - kubectl command to execute.
143
- * @param {Object} [options={}] - Execution options.
144
- * @param {string} [options.context=''] - Command context for logging.
145
- * @return {string|null} Command output or null on error.
146
- */
147
- _executeKubectl(command, options = {}) {
148
- const { context = '' } = options;
149
-
150
- try {
151
- logger.info(`Executing kubectl command`, { command: sanitizeCommand(command), context });
152
- return shellExec(command, { stdout: true, disableLog: true });
153
- } catch (error) {
154
- logger.error(`kubectl command failed`, { command: sanitizeCommand(command), error: error.message, context });
155
- throw error;
156
- }
157
- },
158
-
159
- /**
160
- * Helper: Copies file to pod.
161
- * @method _copyToPod
162
- * @memberof UnderpostDB
163
- * @param {Object} params - Copy parameters.
164
- * @param {string} params.sourcePath - Source file path.
165
- * @param {string} params.podName - Target pod name.
166
- * @param {string} params.namespace - Pod namespace.
167
- * @param {string} params.destPath - Destination path in pod.
168
- * @return {boolean} Success status.
169
- */
170
- _copyToPod({ sourcePath, podName, namespace, destPath }) {
171
- try {
172
- const command = `sudo kubectl cp ${sourcePath} ${namespace}/${podName}:${destPath}`;
173
- Underpost.db._executeKubectl(command, { context: `copy to pod ${podName}` });
174
- return true;
175
- } catch (error) {
176
- logger.error('Failed to copy file to pod', { sourcePath, podName, destPath, error: error.message });
177
- return false;
178
- }
179
- },
180
-
181
- /**
182
- * Helper: Copies file from pod.
183
- * @method _copyFromPod
184
- * @memberof UnderpostDB
185
- * @param {Object} params - Copy parameters.
186
- * @param {string} params.podName - Source pod name.
187
- * @param {string} params.namespace - Pod namespace.
188
- * @param {string} params.sourcePath - Source path in pod.
189
- * @param {string} params.destPath - Destination file path.
190
- * @return {boolean} Success status.
191
- */
192
- _copyFromPod({ podName, namespace, sourcePath, destPath }) {
193
- try {
194
- const command = `sudo kubectl cp ${namespace}/${podName}:${sourcePath} ${destPath}`;
195
- Underpost.db._executeKubectl(command, { context: `copy from pod ${podName}` });
196
- return true;
197
- } catch (error) {
198
- logger.error('Failed to copy file from pod', { podName, sourcePath, destPath, error: error.message });
199
- return false;
200
- }
201
- },
202
-
203
- /**
204
- * Helper: Executes command in pod.
205
- * @method _execInPod
206
- * @memberof UnderpostDB
207
- * @param {Object} params - Execution parameters.
208
- * @param {string} params.podName - Pod name.
209
- * @param {string} params.namespace - Pod namespace.
210
- * @param {string} params.command - Command to execute.
211
- * @return {string|null} Command output or null.
212
- */
213
- _execInPod({ podName, namespace, command }) {
214
- try {
215
- const kubectlCmd = `sudo kubectl exec -n ${namespace} -i ${podName} -- sh -c "${command}"`;
216
- return Underpost.db._executeKubectl(kubectlCmd, { context: `exec in pod ${podName}` });
217
- } catch (error) {
218
- logger.error('Failed to execute command in pod', {
219
- podName,
220
- command: sanitizeCommand(command),
221
- error: error.message,
222
- });
223
- throw error;
224
- }
225
- },
226
-
227
87
  /**
228
88
  * Helper: Resolves the latest backup timestamp from an existing backup directory.
229
89
  * Scans the directory for numeric (epoch) sub-folders and returns the most recent one.
@@ -239,76 +99,6 @@ class UnderpostDB {
239
99
  return entries.sort((a, b) => parseInt(b) - parseInt(a))[0];
240
100
  },
241
101
 
242
- /**
243
- * Helper: Manages Git repository for backups.
244
- * @method _manageGitRepo
245
- * @memberof UnderpostDB
246
- * @param {Object} params - Git parameters.
247
- * @param {string} params.repoName - Repository name.
248
- * @param {string} params.operation - Operation (clone, pull, commit, push).
249
- * @param {string} [params.message=''] - Commit message.
250
- * @param {boolean} [params.forceClone=false] - Force remove and re-clone repository.
251
- * @return {boolean} Success status.
252
- */
253
- _manageGitRepo({ repoName, operation, message = '', forceClone = false }) {
254
- try {
255
- const username = process.env.GITHUB_USERNAME;
256
- if (!username) {
257
- logger.error('GITHUB_USERNAME environment variable not set');
258
- return false;
259
- }
260
-
261
- const repoPath = `../${repoName}`;
262
-
263
- switch (operation) {
264
- case 'clone':
265
- if (forceClone && fs.existsSync(repoPath)) {
266
- logger.info(`Force clone enabled, removing existing repository: ${repoName}`);
267
- fs.removeSync(repoPath);
268
- }
269
- if (!fs.existsSync(repoPath)) {
270
- shellExec(`cd .. && underpost clone ${username}/${repoName}`);
271
- logger.info(`Cloned repository: ${repoName}`);
272
- }
273
- break;
274
-
275
- case 'pull':
276
- if (fs.existsSync(repoPath)) {
277
- shellExec(`cd ${repoPath} && git checkout . && git clean -f -d`);
278
- shellExec(`cd ${repoPath} && underpost pull . ${username}/${repoName}`, {
279
- silent: true,
280
- });
281
- logger.info(`Pulled repository: ${repoName}`);
282
- }
283
- break;
284
-
285
- case 'commit':
286
- if (fs.existsSync(repoPath)) {
287
- shellExec(`cd ${repoPath} && git add .`);
288
- shellExec(`underpost cmt ${repoPath} backup '' '${message}'`);
289
- logger.info(`Committed to repository: ${repoName}`, { message });
290
- }
291
- break;
292
-
293
- case 'push':
294
- if (fs.existsSync(repoPath)) {
295
- shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName}`, { silent: true });
296
- logger.info(`Pushed repository: ${repoName}`);
297
- }
298
- break;
299
-
300
- default:
301
- logger.warn(`Unknown git operation: ${operation}`);
302
- return false;
303
- }
304
-
305
- return true;
306
- } catch (error) {
307
- logger.error(`Git operation failed`, { repoName, operation, error: error.message });
308
- return false;
309
- }
310
- },
311
-
312
102
  /**
313
103
  * Helper: Performs MariaDB import operation.
314
104
  * @method _importMariaDB
@@ -329,8 +119,20 @@ class UnderpostDB {
329
119
 
330
120
  logger.info('Importing MariaDB database', { podName, dbName });
331
121
 
122
+ // Always ensure the database exists first — required for WP even when no backup is available
123
+ Underpost.kubectl.run(
124
+ `kubectl exec -n ${namespace} -i ${podName} -- mariadb -p${password} -e 'CREATE DATABASE IF NOT EXISTS ${dbName};'`,
125
+ { context: `create database ${dbName}` },
126
+ );
127
+
128
+ // If no SQL file is available, the empty database is enough — return early
129
+ if (!sqlPath || !fs.existsSync(sqlPath)) {
130
+ logger.warn('No SQL backup file found — empty database ensured', { podName, dbName, sqlPath });
131
+ return true;
132
+ }
133
+
332
134
  // Remove existing SQL file in container
333
- Underpost.db._execInPod({
135
+ Underpost.kubectl.exec({
334
136
  podName,
335
137
  namespace,
336
138
  command: `rm -rf ${containerSqlPath}`,
@@ -338,7 +140,7 @@ class UnderpostDB {
338
140
 
339
141
  // Copy SQL file to pod
340
142
  if (
341
- !Underpost.db._copyToPod({
143
+ !Underpost.kubectl.cpTo({
342
144
  sourcePath: sqlPath,
343
145
  podName,
344
146
  namespace,
@@ -348,15 +150,9 @@ class UnderpostDB {
348
150
  return false;
349
151
  }
350
152
 
351
- // Create database if it doesn't exist
352
- Underpost.db._executeKubectl(
353
- `kubectl exec -n ${namespace} -i ${podName} -- mariadb -p${password} -e 'CREATE DATABASE IF NOT EXISTS ${dbName};'`,
354
- { context: `create database ${dbName}` },
355
- );
356
-
357
153
  // Import SQL file
358
154
  const importCmd = `mariadb -u ${user} -p${password} ${dbName} < ${containerSqlPath}`;
359
- Underpost.db._execInPod({ podName, namespace, command: importCmd });
155
+ Underpost.kubectl.exec({ podName, namespace, command: importCmd });
360
156
 
361
157
  logger.info('Successfully imported MariaDB database', { podName, dbName });
362
158
  return true;
@@ -387,7 +183,7 @@ class UnderpostDB {
387
183
  logger.info('Exporting MariaDB database', { podName, dbName });
388
184
 
389
185
  // Remove existing SQL file in container
390
- Underpost.db._execInPod({
186
+ Underpost.kubectl.exec({
391
187
  podName,
392
188
  namespace,
393
189
  command: `rm -rf ${containerSqlPath}`,
@@ -395,11 +191,11 @@ class UnderpostDB {
395
191
 
396
192
  // Dump database
397
193
  const dumpCmd = `mariadb-dump --user=${user} --password=${password} --lock-tables ${dbName} > ${containerSqlPath}`;
398
- Underpost.db._execInPod({ podName, namespace, command: dumpCmd });
194
+ Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
399
195
 
400
196
  // Copy SQL file from pod
401
197
  if (
402
- !Underpost.db._copyFromPod({
198
+ !Underpost.kubectl.cpFrom({
403
199
  podName,
404
200
  namespace,
405
201
  sourcePath: containerSqlPath,
@@ -442,8 +238,18 @@ class UnderpostDB {
442
238
 
443
239
  logger.info('Importing MongoDB database', { podName, dbName });
444
240
 
241
+ // If no BSON directory is available, skip — MongoDB creates the DB on first write
242
+ if (!bsonPath || !fs.existsSync(bsonPath)) {
243
+ logger.warn('No BSON backup directory found — database will be created on first write', {
244
+ podName,
245
+ dbName,
246
+ bsonPath,
247
+ });
248
+ return true;
249
+ }
250
+
445
251
  // Remove existing BSON directory in container
446
- Underpost.db._execInPod({
252
+ Underpost.kubectl.exec({
447
253
  podName,
448
254
  namespace,
449
255
  command: `rm -rf ${containerBsonPath}`,
@@ -451,7 +257,7 @@ class UnderpostDB {
451
257
 
452
258
  // Copy BSON directory to pod
453
259
  if (
454
- !Underpost.db._copyToPod({
260
+ !Underpost.kubectl.cpTo({
455
261
  sourcePath: bsonPath,
456
262
  podName,
457
263
  namespace,
@@ -465,7 +271,7 @@ class UnderpostDB {
465
271
  const restoreCmd = `mongorestore -d ${dbName} ${containerBsonPath}${drop ? ' --drop' : ''}${
466
272
  preserveUUID ? ' --preserveUUID' : ''
467
273
  }`;
468
- Underpost.db._execInPod({ podName, namespace, command: restoreCmd });
274
+ Underpost.kubectl.exec({ podName, namespace, command: restoreCmd });
469
275
 
470
276
  logger.info('Successfully imported MongoDB database', { podName, dbName });
471
277
  return true;
@@ -495,7 +301,7 @@ class UnderpostDB {
495
301
  logger.info('Exporting MongoDB database', { podName, dbName, collections });
496
302
 
497
303
  // Remove existing BSON directory in container
498
- Underpost.db._execInPod({
304
+ Underpost.kubectl.exec({
499
305
  podName,
500
306
  namespace,
501
307
  command: `rm -rf ${containerBsonPath}`,
@@ -506,16 +312,16 @@ class UnderpostDB {
506
312
  const collectionList = collections.split(',').map((c) => c.trim());
507
313
  for (const collection of collectionList) {
508
314
  const dumpCmd = `mongodump -d ${dbName} --collection ${collection} -o /`;
509
- Underpost.db._execInPod({ podName, namespace, command: dumpCmd });
315
+ Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
510
316
  }
511
317
  } else {
512
318
  const dumpCmd = `mongodump -d ${dbName} -o /`;
513
- Underpost.db._execInPod({ podName, namespace, command: dumpCmd });
319
+ Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
514
320
  }
515
321
 
516
322
  // Copy BSON directory from pod
517
323
  if (
518
- !Underpost.db._copyFromPod({
324
+ !Underpost.kubectl.cpFrom({
519
325
  podName,
520
326
  namespace,
521
327
  sourcePath: containerBsonPath,
@@ -743,6 +549,7 @@ class UnderpostDB {
743
549
  * @param {boolean} [options.k3s=false] - k3s cluster flag.
744
550
  * @param {boolean} [options.kubeadm=false] - kubeadm cluster flag.
745
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.
746
553
  * @return {Promise<void>} Resolves when operation is complete.
747
554
  */
748
555
  async callback(
@@ -771,346 +578,420 @@ class UnderpostDB {
771
578
  k3s: false,
772
579
  kubeadm: false,
773
580
  kind: false,
581
+ repoBackup: false,
774
582
  },
775
583
  ) {
776
- loadCronDeployEnv();
777
- const newBackupTimestamp = new Date().getTime();
778
- const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
779
-
780
- if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
781
-
782
- // Handle clean-fs-collection operation
783
- if (options.cleanFsCollection || options.cleanFsDryRun) {
784
- logger.info('Starting File collection cleanup operation', { deployList });
785
- await Underpost.db.cleanFsCollection(deployList, {
786
- hosts: options.hosts,
787
- paths: options.paths,
788
- dryRun: options.cleanFsDryRun,
789
- });
790
- return;
791
- }
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
+ }
792
609
 
793
- logger.info('Starting database operation', {
794
- deployList,
795
- namespace,
796
- import: options.import,
797
- export: options.export,
798
- });
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
+ }
620
+
621
+ logger.info('Starting database operation', {
622
+ deployList,
623
+ namespace,
624
+ import: options.import,
625
+ export: options.export,
626
+ });
799
627
 
800
- if (options.primaryPodEnsure) {
801
- const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: options.primaryPodEnsure });
802
- if (!primaryPodName) {
803
- const baseCommand = options.dev ? 'node bin' : 'underpost';
804
- const baseClusterCommand = options.dev ? ' --dev' : '';
805
- let clusterFlag = options.k3s ? ' --k3s' : options.kubeadm ? ' --kubeadm' : '';
806
- shellExec(`${baseCommand} cluster${baseClusterCommand}${clusterFlag} --mongodb`);
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;
807
637
  }
808
- return;
809
- }
810
638
 
811
- // Track processed repositories to avoid duplicate Git operations
812
- const processedRepos = new Set();
813
- // Track processed host+path combinations to avoid duplicates
814
- 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();
815
643
 
816
- for (const _deployId of deployList.split(',')) {
817
- const deployId = _deployId.trim();
818
- if (!deployId) continue;
644
+ for (const _deployId of deployList.split(',')) {
645
+ const deployId = _deployId.trim();
646
+ if (!deployId) continue;
819
647
 
820
- logger.info('Processing deployment', { deployId });
648
+ logger.info('Processing deployment', { deployId });
821
649
 
822
- /** @type {Object.<string, Object.<string, DatabaseConfig>>} */
823
- const dbs = {};
824
- 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`;
825
653
 
826
- // Load server configuration
827
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
828
- if (!fs.existsSync(confServerPath)) {
829
- logger.error('Configuration file not found', { path: confServerPath });
830
- continue;
831
- }
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
+ }
832
660
 
833
- const confServer = loadConfServerJson(confServerPath, { resolve: true });
834
-
835
- // Build database configuration map
836
- for (const host of Object.keys(confServer)) {
837
- for (const path of Object.keys(confServer[host])) {
838
- const { db } = confServer[host][path];
839
- if (db) {
840
- const { provider, name, user, password } = db;
841
- if (!dbs[provider]) dbs[provider] = {};
842
-
843
- if (!(name in dbs[provider])) {
844
- dbs[provider][name] = {
845
- user,
846
- password,
847
- hostFolder: host + path.replaceAll('/', '-'),
848
- host,
849
- path,
850
- };
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
+ }
851
680
  }
852
681
  }
853
682
  }
854
- }
855
-
856
- // Handle Git operations - execute only once per repository
857
- if (!processedRepos.has(repoName)) {
858
- logger.info('Processing Git operations for repository', { repoName, deployId });
859
- if (options.git === true) {
860
- Underpost.db._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
861
- Underpost.db._manageGitRepo({ repoName, operation: 'pull' });
862
- }
863
683
 
864
- if (options.macroRollbackExport) {
865
- // Only clone if not already done by git option above
866
- if (options.git !== true) {
867
- Underpost.db._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
868
- Underpost.db._manageGitRepo({ repoName, operation: 'pull' });
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) {
688
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
689
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'pull' });
869
690
  }
870
691
 
871
- const nCommits = parseInt(options.macroRollbackExport);
872
- const repoPath = `../${repoName}`;
873
- const username = process.env.GITHUB_USERNAME;
874
-
875
- if (fs.existsSync(repoPath) && username) {
876
- logger.info('Executing macro rollback export', { repoName, nCommits });
877
- shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
878
- shellExec(`cd ${repoPath} && git reset`);
879
- shellExec(`cd ${repoPath} && git checkout .`);
880
- shellExec(`cd ${repoPath} && git clean -f -d`);
881
- shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
882
- } else {
883
- if (!username) logger.error('GITHUB_USERNAME environment variable not set');
884
- logger.warn('Repository not found for macro rollback', { repoPath });
885
- }
886
- }
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
+ }
887
698
 
888
- processedRepos.add(repoName);
889
- logger.info('Repository marked as processed', { repoName });
890
- } else {
891
- logger.info('Skipping Git operations for already processed repository', { repoName, deployId });
892
- }
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
+ }
714
+ }
893
715
 
894
- // Process each database provider
895
- for (const provider of Object.keys(dbs)) {
896
- for (const dbName of Object.keys(dbs[provider])) {
897
- const { hostFolder, user, password, host, path } = dbs[provider][dbName];
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
+ }
898
721
 
899
- // Create unique identifier for host+path combination
900
- const hostPathKey = `${deployId}:${host}:${path}`;
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];
901
726
 
902
- // Skip if this host+path combination was already processed
903
- if (processedHostPaths.has(hostPathKey)) {
904
- logger.info('Skipping already processed host/path', { dbName, host, path, deployId });
905
- continue;
906
- }
727
+ // Create unique identifier for host+path combination
728
+ const hostPathKey = `${deployId}:${host}:${path}`;
907
729
 
908
- // Filter by hosts and paths if specified
909
- if (
910
- (options.hosts &&
911
- !options.hosts
912
- .split(',')
913
- .map((h) => h.trim())
914
- .includes(host)) ||
915
- (options.paths &&
916
- !options.paths
917
- .split(',')
918
- .map((p) => p.trim())
919
- .includes(path))
920
- ) {
921
- logger.info('Skipping database due to host/path filter', { dbName, host, path });
922
- continue;
923
- }
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
+ }
924
735
 
925
- if (!hostFolder) {
926
- logger.warn('No hostFolder defined for database', { dbName, provider });
927
- continue;
928
- }
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
+ }
929
752
 
930
- logger.info('Processing database', { hostFolder, provider, dbName, deployId });
753
+ if (!hostFolder) {
754
+ logger.warn('No hostFolder defined for database', { dbName, provider });
755
+ continue;
756
+ }
931
757
 
932
- const latestBackupTimestamp = Underpost.db._getLatestBackupTimestamp(`../${repoName}/${hostFolder}`);
758
+ logger.info('Processing database', { hostFolder, provider, dbName, deployId });
933
759
 
934
- dbs[provider][dbName].currentBackupTimestamp = latestBackupTimestamp;
760
+ const latestBackupTimestamp = Underpost.db._getLatestBackupTimestamp(`../${repoName}/${hostFolder}`);
935
761
 
936
- const currentTimestamp = latestBackupTimestamp || newBackupTimestamp;
937
- const sqlContainerPath = `/home/${dbName}.sql`;
938
- const fromPartsPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}-parths.json`;
939
- const toSqlPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}.sql`;
940
- const toNewSqlPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}.sql`;
941
- const toBsonPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}`;
942
- const toNewBsonPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}`;
762
+ dbs[provider][dbName].currentBackupTimestamp = latestBackupTimestamp;
943
763
 
944
- // Merge split SQL files if needed for import
945
- if (options.import === true && fs.existsSync(fromPartsPath) && !fs.existsSync(toSqlPath)) {
946
- const names = JSON.parse(fs.readFileSync(fromPartsPath, 'utf8')).map((_path) => {
947
- return `../${repoName}/${hostFolder}/${currentTimestamp}/${_path.split('/').pop()}`;
948
- });
949
- logger.info('Merging backup parts', { fromPartsPath, toSqlPath, parts: names.length });
950
- await mergeFile(names, toSqlPath);
951
- }
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}`;
952
771
 
953
- // Get target pods based on provider and options
954
- let targetPods = [];
955
- const podCriteria = {
956
- podNames: options.podName,
957
- namespace,
958
- deployId: provider === 'mariadb' ? 'mariadb' : 'mongo',
959
- };
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
+ }
960
780
 
961
- targetPods = Underpost.db._getFilteredPods(podCriteria);
781
+ // Get target pods based on provider and options
782
+ let targetPods = [];
783
+ const podCriteria = {
784
+ podNames: options.podName,
785
+ namespace,
786
+ deployId: provider === 'mariadb' ? 'mariadb' : 'mongo',
787
+ };
962
788
 
963
- // Fallback to default if no custom pods specified
964
- if (targetPods.length === 0 && !options.podName) {
965
- const defaultPods = Underpost.deploy.get(provider === 'mariadb' ? 'mariadb' : 'mongo', 'pods', namespace);
966
- console.log('defaultPods', defaultPods);
967
- targetPods = defaultPods;
968
- }
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
+ }
969
801
 
970
- if (targetPods.length === 0) {
971
- logger.warn('No pods found matching criteria', { provider, criteria: podCriteria });
972
- continue;
973
- }
802
+ if (targetPods.length === 0) {
803
+ logger.warn('No pods found matching criteria', { provider, criteria: podCriteria });
804
+ continue;
805
+ }
974
806
 
975
- // Handle primary pod detection for MongoDB
976
- let podsToProcess = [];
977
- if (provider === 'mongoose' && !options.allPods) {
978
- // For MongoDB, always use primary pod unless allPods is true
979
- if (!targetPods || targetPods.length === 0) {
980
- logger.warn('No MongoDB pods available to check for primary');
981
- podsToProcess = [];
982
- } else {
983
- const firstPod = targetPods[0].NAME;
984
- const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: firstPod });
985
-
986
- if (primaryPodName) {
987
- const primaryPod = targetPods.find((p) => p.NAME === primaryPodName);
988
- if (primaryPod) {
989
- podsToProcess = [primaryPod];
990
- logger.info('Using MongoDB primary pod', { primaryPod: primaryPodName });
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
+ }
991
827
  } else {
992
- logger.warn('Primary pod not in filtered list, using first pod', { primaryPodName });
828
+ logger.warn('Could not detect primary pod, using first pod');
993
829
  podsToProcess = [targetPods[0]];
994
830
  }
995
- } else {
996
- logger.warn('Could not detect primary pod, using first pod');
997
- podsToProcess = [targetPods[0]];
998
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]];
999
835
  }
1000
- } else {
1001
- // For MariaDB or when allPods is true, limit to first pod unless allPods is true
1002
- podsToProcess = options.allPods === true ? targetPods : [targetPods[0]];
1003
- }
1004
836
 
1005
- logger.info(`Processing ${podsToProcess.length} pod(s) for ${provider}`, {
1006
- dbName,
1007
- pods: podsToProcess.map((p) => p.NAME),
1008
- });
837
+ logger.info(`Processing ${podsToProcess.length} pod(s) for ${provider}`, {
838
+ dbName,
839
+ pods: podsToProcess.map((p) => p.NAME),
840
+ });
1009
841
 
1010
- // Process each pod
1011
- for (const pod of podsToProcess) {
1012
- logger.info('Processing pod', { podName: pod.NAME, node: pod.NODE, status: pod.STATUS });
1013
-
1014
- switch (provider) {
1015
- case 'mariadb': {
1016
- if (options.stats === true) {
1017
- const stats = Underpost.db._getMariaDBStats({
1018
- podName: pod.NAME,
1019
- namespace,
1020
- dbName,
1021
- user,
1022
- password,
1023
- });
1024
- if (stats) {
1025
- Underpost.db._displayStats({ provider, dbName, stats });
842
+ let exportSucceeded = false;
843
+
844
+ // Process each pod
845
+ for (const pod of podsToProcess) {
846
+ logger.info('Processing pod', { podName: pod.NAME, node: pod.NODE, status: pod.STATUS });
847
+
848
+ switch (provider) {
849
+ case 'mariadb': {
850
+ if (options.stats === true) {
851
+ const stats = Underpost.db._getMariaDBStats({
852
+ podName: pod.NAME,
853
+ namespace,
854
+ dbName,
855
+ user,
856
+ password,
857
+ });
858
+ if (stats) {
859
+ Underpost.db._displayStats({ provider, dbName, stats });
860
+ }
1026
861
  }
1027
- }
1028
862
 
1029
- if (options.import === true) {
1030
- Underpost.db._importMariaDB({
1031
- pod,
1032
- namespace,
1033
- dbName,
1034
- user,
1035
- password,
1036
- sqlPath: toSqlPath,
1037
- });
1038
- }
863
+ if (options.import === true) {
864
+ Underpost.db._importMariaDB({
865
+ pod,
866
+ namespace,
867
+ dbName,
868
+ user,
869
+ password,
870
+ sqlPath: toSqlPath,
871
+ });
872
+ }
1039
873
 
1040
- if (options.export === true) {
1041
- const outputPath = options.outPath || toNewSqlPath;
1042
- await Underpost.db._exportMariaDB({
1043
- pod,
1044
- namespace,
1045
- dbName,
1046
- user,
1047
- password,
1048
- outputPath,
1049
- });
874
+ if (options.export === true) {
875
+ const outputPath = options.outPath || toNewSqlPath;
876
+ const success = await Underpost.db._exportMariaDB({
877
+ pod,
878
+ namespace,
879
+ dbName,
880
+ user,
881
+ password,
882
+ outputPath,
883
+ });
884
+ exportSucceeded = exportSucceeded || success;
885
+ }
886
+ break;
1050
887
  }
1051
- break;
1052
- }
1053
888
 
1054
- case 'mongoose': {
1055
- if (options.stats === true) {
1056
- const stats = Underpost.db._getMongoStats({
1057
- podName: pod.NAME,
1058
- namespace,
1059
- dbName,
1060
- });
1061
- if (stats) {
1062
- Underpost.db._displayStats({ provider, dbName, stats });
889
+ case 'mongoose': {
890
+ if (options.stats === true) {
891
+ const stats = Underpost.db._getMongoStats({
892
+ podName: pod.NAME,
893
+ namespace,
894
+ dbName,
895
+ });
896
+ if (stats) {
897
+ Underpost.db._displayStats({ provider, dbName, stats });
898
+ }
1063
899
  }
1064
- }
1065
900
 
1066
- if (options.import === true) {
1067
- const bsonPath = options.outPath || toBsonPath;
1068
- Underpost.db._importMongoDB({
1069
- pod,
1070
- namespace,
1071
- dbName,
1072
- bsonPath,
1073
- drop: options.drop,
1074
- preserveUUID: options.preserveUUID,
1075
- });
1076
- }
901
+ if (options.import === true) {
902
+ const bsonPath = options.outPath || toBsonPath;
903
+ Underpost.db._importMongoDB({
904
+ pod,
905
+ namespace,
906
+ dbName,
907
+ bsonPath,
908
+ drop: options.drop,
909
+ preserveUUID: options.preserveUUID,
910
+ });
911
+ }
1077
912
 
1078
- if (options.export === true) {
1079
- const outputPath = options.outPath || toNewBsonPath;
1080
- Underpost.db._exportMongoDB({
1081
- pod,
1082
- namespace,
1083
- dbName,
1084
- outputPath,
1085
- collections: options.collections,
1086
- });
913
+ if (options.export === true) {
914
+ const outputPath = options.outPath || toNewBsonPath;
915
+ const success = Underpost.db._exportMongoDB({
916
+ pod,
917
+ namespace,
918
+ dbName,
919
+ outputPath,
920
+ collections: options.collections,
921
+ });
922
+ exportSucceeded = exportSucceeded || success;
923
+ }
924
+ break;
1087
925
  }
1088
- break;
926
+
927
+ default:
928
+ logger.warn('Unsupported database provider', { provider });
929
+ break;
1089
930
  }
931
+ }
1090
932
 
1091
- default:
1092
- logger.warn('Unsupported database provider', { provider });
1093
- break;
933
+ if (options.export === true && exportSucceeded === true) {
934
+ Underpost.db._enforceBackupRetention(`../${repoName}/${hostFolder}`);
1094
935
  }
936
+
937
+ // Mark this host+path combination as processed
938
+ processedHostPaths.add(hostPathKey);
1095
939
  }
940
+ }
1096
941
 
1097
- // Mark this host+path combination as processed
1098
- processedHostPaths.add(hostPathKey);
942
+ // Commit and push to Git if enabled - execute only once per repository
943
+ if (options.export === true && options.git === true && !processedRepos.has(`${repoName}-committed`)) {
944
+ const commitMessage = `${new Date(newBackupTimestamp).toLocaleDateString()} ${new Date(
945
+ newBackupTimestamp,
946
+ ).toLocaleTimeString()}`;
947
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'commit', message: commitMessage });
948
+ Underpost.repo.manageBackupRepo({ repoName, operation: 'push' });
949
+ processedRepos.add(`${repoName}-committed`);
1099
950
  }
1100
951
  }
1101
952
 
1102
- // Commit and push to Git if enabled - execute only once per repository
1103
- if (options.export === true && options.git === true && !processedRepos.has(`${repoName}-committed`)) {
1104
- const commitMessage = `${new Date(newBackupTimestamp).toLocaleDateString()} ${new Date(
1105
- newBackupTimestamp,
1106
- ).toLocaleTimeString()}`;
1107
- Underpost.db._manageGitRepo({ repoName, operation: 'commit', message: commitMessage });
1108
- Underpost.db._manageGitRepo({ repoName, operation: 'push' });
1109
- processedRepos.add(`${repoName}-committed`);
1110
- }
953
+ logger.info('Database operation completed successfully');
954
+ } catch (error) {
955
+ logger.error('Database operation failed', { error: error.message });
956
+ throw error;
1111
957
  }
958
+ },
959
+ /**
960
+ * Helper: Removes old timestamp backup folders and keeps only the newest ones.
961
+ * @method _enforceBackupRetention
962
+ * @memberof UnderpostDB
963
+ * @param {string} backupDir - Path to host-folder backup directory.
964
+ * @param {number} [maxRetention=MAX_BACKUP_RETENTION] - Maximum folders to keep.
965
+ * @return {number} Number of removed backup folders.
966
+ */
967
+ _enforceBackupRetention(backupDir, maxRetention = MAX_BACKUP_RETENTION) {
968
+ try {
969
+ if (!fs.existsSync(backupDir)) return 0;
970
+
971
+ const timestamps = fs
972
+ .readdirSync(backupDir)
973
+ .filter((entry) => /^\d+$/.test(entry))
974
+ .sort((a, b) => parseInt(b, 10) - parseInt(a, 10));
975
+
976
+ if (timestamps.length <= maxRetention) return 0;
1112
977
 
1113
- logger.info('Database operation completed successfully');
978
+ const staleTimestamps = timestamps.slice(maxRetention);
979
+ staleTimestamps.forEach((timestamp) => {
980
+ fs.removeSync(`${backupDir}/${timestamp}`);
981
+ });
982
+
983
+ logger.info('Pruned old backup timestamp folders', {
984
+ backupDir,
985
+ kept: maxRetention,
986
+ removed: staleTimestamps.length,
987
+ removedTimestamps: staleTimestamps,
988
+ });
989
+
990
+ return staleTimestamps.length;
991
+ } catch (error) {
992
+ logger.error('Failed to enforce backup retention', { backupDir, maxRetention, error: error.message });
993
+ return 0;
994
+ }
1114
995
  },
1115
996
 
1116
997
  /**
@@ -1122,6 +1003,8 @@ class UnderpostDB {
1122
1003
  * @param {string} [deployId=process.env.DEFAULT_DEPLOY_ID] - The deployment ID.
1123
1004
  * @param {string} [host=process.env.DEFAULT_DEPLOY_HOST] - The host identifier.
1124
1005
  * @param {string} [path=process.env.DEFAULT_DEPLOY_PATH] - The path identifier.
1006
+ * @param {object} [options] - Options.
1007
+ * @param {boolean} [options.dev=false] - Development mode flag.
1125
1008
  * @return {Promise<void>} Resolves when metadata creation is complete.
1126
1009
  * @throws {Error} If database configuration is invalid or connection fails.
1127
1010
  */
@@ -1129,161 +1012,181 @@ class UnderpostDB {
1129
1012
  deployId = process.env.DEFAULT_DEPLOY_ID,
1130
1013
  host = process.env.DEFAULT_DEPLOY_HOST,
1131
1014
  path = process.env.DEFAULT_DEPLOY_PATH,
1015
+ options = { dev: false },
1132
1016
  ) {
1133
- loadCronDeployEnv();
1134
- deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
1135
- host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
1136
- path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
1017
+ try {
1018
+ loadCronDeployEnv();
1019
+ deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
1020
+ host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
1021
+ path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
1137
1022
 
1138
- logger.info('Creating cluster metadata', { deployId, host, path });
1023
+ logger.info('Creating cluster metadata', { deployId, host, path });
1139
1024
 
1140
- const env = 'production';
1141
- const deployListPath = './engine-private/deploy/dd.router';
1025
+ const env = 'production';
1026
+ const deployListPath = './engine-private/deploy/dd.router';
1142
1027
 
1143
- if (!fs.existsSync(deployListPath)) {
1144
- logger.error('Deploy router file not found', { path: deployListPath });
1145
- throw new Error(`Deploy router file not found: ${deployListPath}`);
1146
- }
1028
+ if (!fs.existsSync(deployListPath)) {
1029
+ logger.error('Deploy router file not found', { path: deployListPath });
1030
+ throw new Error(`Deploy router file not found: ${deployListPath}`);
1031
+ }
1147
1032
 
1148
- const deployList = fs.readFileSync(deployListPath, 'utf8').split(',');
1033
+ const deployList = fs.readFileSync(deployListPath, 'utf8').split(',');
1149
1034
 
1150
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1151
- if (!fs.existsSync(confServerPath)) {
1152
- logger.error('Server configuration not found', { path: confServerPath });
1153
- throw new Error(`Server configuration not found: ${confServerPath}`);
1154
- }
1035
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1036
+ if (!fs.existsSync(confServerPath)) {
1037
+ logger.error('Server configuration not found', { path: confServerPath });
1038
+ throw new Error(`Server configuration not found: ${confServerPath}`);
1039
+ }
1155
1040
 
1156
- const { db } = loadConfServerJson(confServerPath, { resolve: true })[host][path];
1041
+ const { db } = loadConfServerJson(confServerPath, { resolve: true })[host][path];
1157
1042
 
1158
- try {
1159
- await DataBaseProvider.load({ apis: ['instance', 'cron'], host, path, db });
1043
+ const maxRetries = 5;
1044
+ const retryDelay = 3000;
1045
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
1046
+ try {
1047
+ await DataBaseProvider.load({ apis: ['instance', 'cron'], host, path, db });
1048
+ break;
1049
+ } catch (err) {
1050
+ if (attempt === maxRetries) {
1051
+ logger.error('Failed to connect to database after retries', { attempts: maxRetries, error: err.message });
1052
+ throw err;
1053
+ }
1054
+ logger.warn('Database connection failed, retrying...', { attempt, maxRetries, error: err.message });
1055
+ await timer(retryDelay);
1056
+ }
1057
+ }
1160
1058
 
1161
- /** @type {import('../api/instance/instance.model.js').InstanceModel} */
1162
- const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance;
1059
+ try {
1060
+ /** @type {import('../api/instance/instance.model.js').InstanceModel} */
1061
+ const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance;
1163
1062
 
1164
- await Instance.deleteMany();
1165
- logger.info('Cleared existing instance metadata');
1063
+ await Instance.deleteMany();
1064
+ logger.info('Cleared existing instance metadata');
1166
1065
 
1167
- for (const _deployId of deployList) {
1168
- const deployId = _deployId.trim();
1169
- if (!deployId) continue;
1066
+ for (const _deployId of deployList) {
1067
+ const deployId = _deployId.trim();
1068
+ if (!deployId) continue;
1170
1069
 
1171
- logger.info('Processing deployment for metadata', { deployId });
1070
+ logger.info('Processing deployment for metadata', { deployId });
1172
1071
 
1173
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1174
- if (!fs.existsSync(confServerPath)) {
1175
- logger.warn('Configuration not found for deployment', { deployId, path: confServerPath });
1176
- continue;
1177
- }
1072
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1073
+ if (!fs.existsSync(confServerPath)) {
1074
+ logger.warn('Configuration not found for deployment', { deployId, path: confServerPath });
1075
+ continue;
1076
+ }
1178
1077
 
1179
- const confServer = loadReplicas(deployId, loadConfServerJson(confServerPath, { resolve: true }));
1180
- const router = await Underpost.deploy.routerFactory(deployId, env);
1181
- const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
1078
+ const confServer = loadReplicas(deployId, loadConfServerJson(confServerPath, { resolve: true }));
1079
+ const router = await Underpost.deploy.routerFactory(deployId, env);
1080
+ const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
1182
1081
 
1183
- for (const host of Object.keys(confServer)) {
1184
- for (const { path, port } of pathPortAssignmentData[host]) {
1185
- if (!confServer[host][path]) continue;
1082
+ for (const host of Object.keys(confServer)) {
1083
+ for (const { path, port } of pathPortAssignmentData[host]) {
1084
+ if (!confServer[host][path]) continue;
1186
1085
 
1187
- const { client, runtime, apis, peer } = confServer[host][path];
1086
+ const { client, runtime, apis, peer } = confServer[host][path];
1188
1087
 
1189
- // Save main instance
1190
- {
1191
- const body = {
1192
- deployId,
1193
- host,
1194
- path,
1195
- port,
1196
- client,
1197
- runtime,
1198
- apis,
1199
- };
1088
+ // Save main instance
1089
+ {
1090
+ const body = {
1091
+ deployId,
1092
+ host,
1093
+ path,
1094
+ port,
1095
+ client,
1096
+ runtime,
1097
+ apis,
1098
+ };
1099
+
1100
+ logger.info('Saving instance metadata', body);
1101
+ await new Instance(body).save();
1102
+ }
1200
1103
 
1201
- logger.info('Saving instance metadata', body);
1202
- await new Instance(body).save();
1104
+ // Save peer instance if exists
1105
+ if (peer) {
1106
+ const body = {
1107
+ deployId,
1108
+ host,
1109
+ path: path === '/' ? '/peer' : `${path}/peer`,
1110
+ port: port + 1,
1111
+ runtime: 'nodejs',
1112
+ };
1113
+
1114
+ logger.info('Saving peer instance metadata', body);
1115
+ await new Instance(body).save();
1116
+ }
1203
1117
  }
1118
+ }
1204
1119
 
1205
- // Save peer instance if exists
1206
- if (peer) {
1120
+ // Process additional instances
1121
+ const confInstancesPath = `./engine-private/conf/${deployId}/conf.instances.json`;
1122
+ if (fs.existsSync(confInstancesPath)) {
1123
+ const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
1124
+ for (const instance of confInstances) {
1125
+ const { id, host, path, fromPort, metadata } = instance;
1126
+ const { runtime } = metadata;
1207
1127
  const body = {
1208
1128
  deployId,
1209
1129
  host,
1210
- path: path === '/' ? '/peer' : `${path}/peer`,
1211
- port: port + 1,
1212
- runtime: 'nodejs',
1130
+ path,
1131
+ port: fromPort,
1132
+ client: id,
1133
+ runtime,
1213
1134
  };
1214
-
1215
- logger.info('Saving peer instance metadata', body);
1135
+ logger.info('Saving additional instance metadata', body);
1216
1136
  await new Instance(body).save();
1217
1137
  }
1218
1138
  }
1219
1139
  }
1220
-
1221
- // Process additional instances
1222
- const confInstancesPath = `./engine-private/conf/${deployId}/conf.instances.json`;
1223
- if (fs.existsSync(confInstancesPath)) {
1224
- const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
1225
- for (const instance of confInstances) {
1226
- const { id, host, path, fromPort, metadata } = instance;
1227
- const { runtime } = metadata;
1228
- const body = {
1229
- deployId,
1230
- host,
1231
- path,
1232
- port: fromPort,
1233
- client: id,
1234
- runtime,
1235
- };
1236
- logger.info('Saving additional instance metadata', body);
1237
- await new Instance(body).save();
1238
- }
1239
- }
1140
+ } catch (error) {
1141
+ logger.error('Failed to create instance metadata', { error: error.message });
1142
+ throw error;
1240
1143
  }
1241
- } catch (error) {
1242
- logger.error('Failed to create instance metadata', { error: error.message });
1243
- throw error;
1244
- }
1245
1144
 
1246
- try {
1247
- const cronDeployPath = './engine-private/deploy/dd.cron';
1248
- if (!fs.existsSync(cronDeployPath)) {
1249
- logger.warn('Cron deploy file not found', { path: cronDeployPath });
1250
- return;
1251
- }
1145
+ try {
1146
+ const cronDeployPath = './engine-private/deploy/dd.cron';
1147
+ if (!fs.existsSync(cronDeployPath)) {
1148
+ logger.warn('Cron deploy file not found', { path: cronDeployPath });
1149
+ return;
1150
+ }
1252
1151
 
1253
- const cronDeployId = fs.readFileSync(cronDeployPath, 'utf8').trim();
1254
- const confCronPath = `./engine-private/conf/${cronDeployId}/conf.cron.json`;
1152
+ const cronDeployId = fs.readFileSync(cronDeployPath, 'utf8').trim();
1153
+ const confCronPath = `./engine-private/conf/${cronDeployId}/conf.cron.json`;
1255
1154
 
1256
- if (!fs.existsSync(confCronPath)) {
1257
- logger.warn('Cron configuration not found', { path: confCronPath });
1258
- return;
1259
- }
1155
+ if (!fs.existsSync(confCronPath)) {
1156
+ logger.warn('Cron configuration not found', { path: confCronPath });
1157
+ return;
1158
+ }
1260
1159
 
1261
- const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
1160
+ const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
1262
1161
 
1263
- await DataBaseProvider.load({ apis: ['cron'], host, path, db });
1162
+ await DataBaseProvider.load({ apis: ['cron'], host, path, db });
1264
1163
 
1265
- /** @type {import('../api/cron/cron.model.js').CronModel} */
1266
- const Cron = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Cron;
1164
+ /** @type {import('../api/cron/cron.model.js').CronModel} */
1165
+ const Cron = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Cron;
1267
1166
 
1268
- await Cron.deleteMany();
1269
- logger.info('Cleared existing cron metadata');
1167
+ await Cron.deleteMany();
1168
+ logger.info('Cleared existing cron metadata');
1270
1169
 
1271
- for (const jobId of Object.keys(confCron.jobs)) {
1272
- const body = {
1273
- jobId,
1274
- deployId: Underpost.cron.getRelatedDeployIdList(jobId),
1275
- expression: confCron.jobs[jobId].expression,
1276
- enabled: confCron.jobs[jobId].enabled,
1277
- };
1278
- logger.info('Saving cron metadata', body);
1279
- await new Cron(body).save();
1170
+ for (const jobId of Object.keys(confCron.jobs)) {
1171
+ const body = {
1172
+ jobId,
1173
+ deployId: Underpost.cron.getRelatedDeployIdList(jobId),
1174
+ expression: confCron.jobs[jobId].expression,
1175
+ enabled: confCron.jobs[jobId].enabled,
1176
+ };
1177
+ logger.info('Saving cron metadata', body);
1178
+ await new Cron(body).save();
1179
+ }
1180
+ } catch (error) {
1181
+ logger.error('Failed to create cron metadata', { error: error.message });
1280
1182
  }
1183
+
1184
+ await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
1185
+ logger.info('Cluster metadata creation completed');
1281
1186
  } catch (error) {
1282
- logger.error('Failed to create cron metadata', { error: error.message });
1187
+ logger.error('Cluster metadata creation failed', { error: error.message });
1188
+ throw error;
1283
1189
  }
1284
-
1285
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
1286
- logger.info('Cluster metadata creation completed');
1287
1190
  },
1288
1191
 
1289
1192
  /**
@@ -1297,6 +1200,7 @@ class UnderpostDB {
1297
1200
  * @param {string} [options.hosts=''] - Comma-separated list of hosts to filter.
1298
1201
  * @param {string} [options.paths=''] - Comma-separated list of paths to filter.
1299
1202
  * @param {boolean} [options.dryRun=false] - If true, only reports what would be deleted.
1203
+ * @param {boolean} [options.dev=false] - Development mode flag.
1300
1204
  * @return {Promise<void>} Resolves when clean operation is complete.
1301
1205
  */
1302
1206
  async cleanFsCollection(
@@ -1305,203 +1209,220 @@ class UnderpostDB {
1305
1209
  hosts: '',
1306
1210
  paths: '',
1307
1211
  dryRun: false,
1212
+ dev: false,
1308
1213
  },
1309
1214
  ) {
1310
- loadCronDeployEnv();
1311
- if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
1312
-
1313
- logger.info('Starting File collection cleanup', { deployList, options });
1314
-
1315
- // Load file.ref.json to know which models reference File
1316
- const fileRefPath = './src/api/file/file.ref.json';
1317
- if (!fs.existsSync(fileRefPath)) {
1318
- logger.error('file.ref.json not found', { path: fileRefPath });
1319
- return;
1320
- }
1215
+ const firstDeployId = deployList !== 'dd' ? deployList.split(',')[0].trim() : '';
1216
+ try {
1217
+ loadCronDeployEnv();
1218
+ if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
1321
1219
 
1322
- const fileRefData = JSON.parse(fs.readFileSync(fileRefPath, 'utf8'));
1323
- logger.info('Loaded file reference configuration', { apis: fileRefData.length });
1220
+ logger.info('Starting File collection cleanup', { deployList, options });
1324
1221
 
1325
- // Filter hosts and paths if specified
1326
- const filterHosts = options.hosts ? options.hosts.split(',').map((h) => h.trim()) : [];
1327
- const filterPaths = options.paths ? options.paths.split(',').map((p) => p.trim()) : [];
1222
+ // Load file.ref.json to know which models reference File
1223
+ const fileRefPath = './src/api/file/file.ref.json';
1224
+ if (!fs.existsSync(fileRefPath)) {
1225
+ logger.error('file.ref.json not found', { path: fileRefPath });
1226
+ return;
1227
+ }
1328
1228
 
1329
- // Track all connections to close them at the end
1330
- const connectionsToClose = [];
1229
+ const fileRefData = JSON.parse(fs.readFileSync(fileRefPath, 'utf8'));
1230
+ logger.info('Loaded file reference configuration', { apis: fileRefData.length });
1331
1231
 
1332
- for (const _deployId of deployList.split(',')) {
1333
- const deployId = _deployId.trim();
1334
- if (!deployId) continue;
1232
+ // Filter hosts and paths if specified
1233
+ const filterHosts = options.hosts ? options.hosts.split(',').map((h) => h.trim()) : [];
1234
+ const filterPaths = options.paths ? options.paths.split(',').map((p) => p.trim()) : [];
1335
1235
 
1336
- logger.info('Processing deployment for File cleanup', { deployId });
1236
+ // Track all connections to close them at the end
1237
+ const connectionsToClose = [];
1337
1238
 
1338
- // Load server configuration
1339
- const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1340
- if (!fs.existsSync(confServerPath)) {
1341
- logger.error('Configuration file not found', { path: confServerPath });
1342
- continue;
1343
- }
1239
+ for (const _deployId of deployList.split(',')) {
1240
+ const deployId = _deployId.trim();
1241
+ if (!deployId) continue;
1344
1242
 
1345
- const confServer = loadConfServerJson(confServerPath, { resolve: true });
1243
+ logger.info('Processing deployment for File cleanup', { deployId });
1346
1244
 
1347
- // Process each host+path combination
1348
- for (const host of Object.keys(confServer)) {
1349
- if (filterHosts.length > 0 && !filterHosts.includes(host)) continue;
1245
+ // Load server configuration
1246
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
1247
+ if (!fs.existsSync(confServerPath)) {
1248
+ logger.error('Configuration file not found', { path: confServerPath });
1249
+ continue;
1250
+ }
1350
1251
 
1351
- for (const path of Object.keys(confServer[host])) {
1352
- if (filterPaths.length > 0 && !filterPaths.includes(path)) continue;
1252
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
1353
1253
 
1354
- const { db, apis } = confServer[host][path];
1355
- if (!db || !apis) continue;
1254
+ // Process each host+path combination
1255
+ for (const host of Object.keys(confServer)) {
1256
+ if (filterHosts.length > 0 && !filterHosts.includes(host)) continue;
1356
1257
 
1357
- // Check if 'file' api is in the apis list
1358
- if (!apis.includes('file')) {
1359
- logger.info('Skipping - no file api in configuration', { host, path });
1360
- continue;
1361
- }
1258
+ for (const path of Object.keys(confServer[host])) {
1259
+ if (filterPaths.length > 0 && !filterPaths.includes(path)) continue;
1362
1260
 
1363
- // logger.info('Processing host+path with file api', { host, path, db: db.name });
1261
+ const { db, apis } = confServer[host][path];
1262
+ if (!db || !apis) continue;
1364
1263
 
1365
- try {
1366
- // Connect to database
1367
- const dbProvider = await DataBaseProvider.load({ apis, host, path, db });
1368
- if (!dbProvider || !dbProvider.models) {
1369
- logger.error('Failed to load database provider', { host, path });
1264
+ // Check if 'file' api is in the apis list
1265
+ if (!apis.includes('file')) {
1266
+ logger.info('Skipping - no file api in configuration', { host, path });
1370
1267
  continue;
1371
1268
  }
1372
1269
 
1373
- const { models } = dbProvider;
1270
+ // logger.info('Processing host+path with file api', { host, path, db: db.name });
1271
+
1272
+ try {
1273
+ // Connect to database with retry
1274
+ let dbProvider;
1275
+ for (let attempt = 1; attempt <= 3; attempt++) {
1276
+ try {
1277
+ dbProvider = await DataBaseProvider.load({ apis, host, path, db });
1278
+ break;
1279
+ } catch (err) {
1280
+ if (attempt === 3) throw err;
1281
+ logger.warn('Database connection failed, retrying...', { attempt, host, path, error: err.message });
1282
+ await timer(3000);
1283
+ }
1284
+ }
1285
+ if (!dbProvider || !dbProvider.models) {
1286
+ logger.error('Failed to load database provider', { host, path });
1287
+ continue;
1288
+ }
1374
1289
 
1375
- // Track this connection for cleanup
1376
- connectionsToClose.push({ host, path, dbProvider });
1290
+ const { models } = dbProvider;
1377
1291
 
1378
- // Check if File model exists
1379
- if (!models.File) {
1380
- logger.warn('File model not loaded', { host, path });
1381
- continue;
1382
- }
1292
+ // Track this connection for cleanup
1293
+ connectionsToClose.push({ host, path, dbProvider });
1383
1294
 
1384
- // Get all File documents
1385
- const allFiles = await models.File.find({}, '_id').lean();
1386
- logger.info('Found File documents', { count: allFiles.length, host, path });
1295
+ // Check if File model exists
1296
+ if (!models.File) {
1297
+ logger.warn('File model not loaded', { host, path });
1298
+ continue;
1299
+ }
1387
1300
 
1388
- if (allFiles.length === 0) continue;
1301
+ // Get all File documents
1302
+ const allFiles = await models.File.find({}, '_id').lean();
1303
+ logger.info('Found File documents', { count: allFiles.length, host, path });
1389
1304
 
1390
- // Track which File IDs are referenced
1391
- const referencedFileIds = new Set();
1305
+ if (allFiles.length === 0) continue;
1392
1306
 
1393
- // Check each API from file.ref.json
1394
- for (const refConfig of fileRefData) {
1395
- const { api, model: modelFields } = refConfig;
1307
+ // Track which File IDs are referenced
1308
+ const referencedFileIds = new Set();
1396
1309
 
1397
- // Check if this API is loaded in current context
1398
- const modelName = api
1399
- .split('-')
1400
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1401
- .join('');
1402
- const Model = models[modelName];
1310
+ // Check each API from file.ref.json
1311
+ for (const refConfig of fileRefData) {
1312
+ const { api, model: modelFields } = refConfig;
1403
1313
 
1404
- if (!Model) {
1405
- logger.debug('Model not loaded in current context', { api, modelName, host, path });
1406
- continue;
1407
- }
1314
+ // Check if this API is loaded in current context
1315
+ const modelName = api
1316
+ .split('-')
1317
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1318
+ .join('');
1319
+ const Model = models[modelName];
1408
1320
 
1409
- logger.info('Checking references in model', { api, modelName });
1321
+ if (!Model) {
1322
+ logger.debug('Model not loaded in current context', { api, modelName, host, path });
1323
+ continue;
1324
+ }
1410
1325
 
1411
- // Helper function to recursively check field references
1412
- const checkFieldReferences = async (fieldPath, fieldConfig) => {
1413
- for (const [fieldName, fieldValue] of Object.entries(fieldConfig)) {
1414
- const currentPath = fieldPath ? `${fieldPath}.${fieldName}` : fieldName;
1326
+ logger.info('Checking references in model', { api, modelName });
1415
1327
 
1416
- if (fieldValue === true) {
1417
- // This is a File reference field
1418
- const query = {};
1419
- query[currentPath] = { $exists: true, $ne: null };
1328
+ // Helper function to recursively check field references
1329
+ const checkFieldReferences = async (fieldPath, fieldConfig) => {
1330
+ for (const [fieldName, fieldValue] of Object.entries(fieldConfig)) {
1331
+ const currentPath = fieldPath ? `${fieldPath}.${fieldName}` : fieldName;
1420
1332
 
1421
- const docs = await Model.find(query, currentPath).lean();
1333
+ if (fieldValue === true) {
1334
+ // This is a File reference field
1335
+ const query = {};
1336
+ query[currentPath] = { $exists: true, $ne: null };
1422
1337
 
1423
- for (const doc of docs) {
1424
- // Navigate to the nested field
1425
- const parts = currentPath.split('.');
1426
- let value = doc;
1427
- for (const part of parts) {
1428
- value = value?.[part];
1429
- }
1338
+ const docs = await Model.find(query, currentPath).lean();
1339
+
1340
+ for (const doc of docs) {
1341
+ // Navigate to the nested field
1342
+ const parts = currentPath.split('.');
1343
+ let value = doc;
1344
+ for (const part of parts) {
1345
+ value = value?.[part];
1346
+ }
1430
1347
 
1431
- if (value) {
1432
- if (Array.isArray(value)) {
1433
- value.forEach((id) => id && referencedFileIds.add(id.toString()));
1434
- } else {
1435
- referencedFileIds.add(value.toString());
1348
+ if (value) {
1349
+ if (Array.isArray(value)) {
1350
+ value.forEach((id) => id && referencedFileIds.add(id.toString()));
1351
+ } else {
1352
+ referencedFileIds.add(value.toString());
1353
+ }
1436
1354
  }
1437
1355
  }
1438
- }
1439
1356
 
1440
- logger.info('Found references', {
1441
- model: modelName,
1442
- field: currentPath,
1443
- count: docs.length,
1444
- });
1445
- } else if (typeof fieldValue === 'object') {
1446
- // Nested object, recurse
1447
- await checkFieldReferences(currentPath, fieldValue);
1357
+ logger.info('Found references', {
1358
+ model: modelName,
1359
+ field: currentPath,
1360
+ count: docs.length,
1361
+ });
1362
+ } else if (typeof fieldValue === 'object') {
1363
+ // Nested object, recurse
1364
+ await checkFieldReferences(currentPath, fieldValue);
1365
+ }
1448
1366
  }
1449
- }
1450
- };
1367
+ };
1451
1368
 
1452
- await checkFieldReferences('', modelFields);
1453
- }
1454
-
1455
- logger.info('Total referenced File IDs', { count: referencedFileIds.size, host, path });
1369
+ await checkFieldReferences('', modelFields);
1370
+ }
1456
1371
 
1457
- // Find orphaned files
1458
- const orphanedFiles = allFiles.filter((file) => !referencedFileIds.has(file._id.toString()));
1372
+ logger.info('Total referenced File IDs', { count: referencedFileIds.size, host, path });
1459
1373
 
1460
- if (orphanedFiles.length === 0) {
1461
- logger.info('No orphaned files found', { host, path });
1462
- } else {
1463
- logger.info('Found orphaned files', { count: orphanedFiles.length, host, path });
1374
+ // Find orphaned files
1375
+ const orphanedFiles = allFiles.filter((file) => !referencedFileIds.has(file._id.toString()));
1464
1376
 
1465
- if (options.dryRun) {
1466
- logger.info('Dry run - would delete files', {
1467
- count: orphanedFiles.length,
1468
- ids: orphanedFiles.map((f) => f._id.toString()),
1469
- });
1377
+ if (orphanedFiles.length === 0) {
1378
+ logger.info('No orphaned files found', { host, path });
1470
1379
  } else {
1471
- const orphanedIds = orphanedFiles.map((f) => f._id);
1472
- const deleteResult = await models.File.deleteMany({ _id: { $in: orphanedIds } });
1473
- logger.info('Deleted orphaned files', {
1474
- deletedCount: deleteResult.deletedCount,
1475
- host,
1476
- path,
1477
- });
1380
+ logger.info('Found orphaned files', { count: orphanedFiles.length, host, path });
1381
+
1382
+ if (options.dryRun) {
1383
+ logger.info('Dry run - would delete files', {
1384
+ count: orphanedFiles.length,
1385
+ ids: orphanedFiles.map((f) => f._id.toString()),
1386
+ });
1387
+ } else {
1388
+ const orphanedIds = orphanedFiles.map((f) => f._id);
1389
+ const deleteResult = await models.File.deleteMany({ _id: { $in: orphanedIds } });
1390
+ logger.info('Deleted orphaned files', {
1391
+ deletedCount: deleteResult.deletedCount,
1392
+ host,
1393
+ path,
1394
+ });
1395
+ }
1478
1396
  }
1397
+ } catch (error) {
1398
+ logger.error('Error processing host+path', {
1399
+ host,
1400
+ path,
1401
+ error: error.message,
1402
+ });
1479
1403
  }
1480
- } catch (error) {
1481
- logger.error('Error processing host+path', {
1482
- host,
1483
- path,
1484
- error: error.message,
1485
- });
1486
1404
  }
1487
1405
  }
1488
1406
  }
1489
- }
1490
1407
 
1491
- // Close all connections
1492
- logger.info('Closing all database connections', { count: connectionsToClose.length });
1493
- for (const { host, path, dbProvider } of connectionsToClose) {
1494
- try {
1495
- if (dbProvider && dbProvider.close) {
1496
- await dbProvider.close();
1497
- logger.info('Connection closed', { host, path });
1408
+ // Close all connections
1409
+ logger.info('Closing all database connections', { count: connectionsToClose.length });
1410
+ for (const { host, path, dbProvider } of connectionsToClose) {
1411
+ try {
1412
+ if (dbProvider && dbProvider.close) {
1413
+ await dbProvider.close();
1414
+ logger.info('Connection closed', { host, path });
1415
+ }
1416
+ } catch (error) {
1417
+ logger.error('Error closing connection', { host, path, error: error.message });
1498
1418
  }
1499
- } catch (error) {
1500
- logger.error('Error closing connection', { host, path, error: error.message });
1501
1419
  }
1502
- }
1503
1420
 
1504
- logger.info('File collection cleanup completed');
1421
+ logger.info('File collection cleanup completed');
1422
+ } catch (error) {
1423
+ logger.error('File collection cleanup failed', { error: error.message });
1424
+ throw error;
1425
+ }
1505
1426
  },
1506
1427
 
1507
1428
  /**
@@ -1520,6 +1441,7 @@ class UnderpostDB {
1520
1441
  * @param {boolean} [options.export=false] - Export metadata to backup.
1521
1442
  * @param {boolean} [options.instances=false] - Process instances collection.
1522
1443
  * @param {boolean} [options.crons=false] - Process crons collection.
1444
+ * @param {boolean} [options.dev=false] - Development mode flag.
1523
1445
  * @return {Promise<void>} Resolves when backup operation is complete.
1524
1446
  */
1525
1447
  async clusterMetadataBackupCallback(
@@ -1533,70 +1455,76 @@ class UnderpostDB {
1533
1455
  export: false,
1534
1456
  instances: false,
1535
1457
  crons: false,
1458
+ dev: false,
1536
1459
  },
1537
1460
  ) {
1538
- loadCronDeployEnv();
1539
- deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
1540
- host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
1541
- path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
1542
-
1543
- logger.info('Starting cluster metadata backup operation', {
1544
- deployId,
1545
- host,
1546
- path,
1547
- options,
1548
- });
1549
-
1550
- if (options.generate === true) {
1551
- logger.info('Generating cluster metadata');
1552
- await Underpost.db.clusterMetadataFactory(deployId, host, path);
1553
- }
1461
+ try {
1462
+ loadCronDeployEnv();
1463
+ deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
1464
+ host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
1465
+ path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
1466
+
1467
+ logger.info('Starting cluster metadata backup operation', {
1468
+ deployId,
1469
+ host,
1470
+ path,
1471
+ options,
1472
+ });
1554
1473
 
1555
- if (options.instances === true) {
1556
- const outputPath = './engine-private/instances';
1557
- if (!fs.existsSync(outputPath)) {
1558
- fs.mkdirSync(outputPath, { recursive: true });
1474
+ if (options.generate === true) {
1475
+ logger.info('Generating cluster metadata');
1476
+ await Underpost.db.clusterMetadataFactory(deployId, host, path);
1559
1477
  }
1560
- const collection = 'instances';
1561
1478
 
1562
- if (options.export === true) {
1563
- logger.info('Exporting instances collection', { outputPath });
1564
- shellExec(
1565
- `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1566
- );
1567
- }
1479
+ if (options.instances === true) {
1480
+ const outputPath = './engine-private/instances';
1481
+ if (!fs.existsSync(outputPath)) {
1482
+ fs.mkdirSync(outputPath, { recursive: true });
1483
+ }
1484
+ const collection = 'instances';
1568
1485
 
1569
- if (options.import === true) {
1570
- logger.info('Importing instances collection', { outputPath });
1571
- shellExec(
1572
- `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1573
- );
1574
- }
1575
- }
1486
+ if (options.export === true) {
1487
+ logger.info('Exporting instances collection', { outputPath });
1488
+ shellExec(
1489
+ `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1490
+ );
1491
+ }
1576
1492
 
1577
- if (options.crons === true) {
1578
- const outputPath = './engine-private/crons';
1579
- if (!fs.existsSync(outputPath)) {
1580
- fs.mkdirSync(outputPath, { recursive: true });
1493
+ if (options.import === true) {
1494
+ logger.info('Importing instances collection', { outputPath });
1495
+ shellExec(
1496
+ `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1497
+ );
1498
+ }
1581
1499
  }
1582
- const collection = 'crons';
1583
1500
 
1584
- if (options.export === true) {
1585
- logger.info('Exporting crons collection', { outputPath });
1586
- shellExec(
1587
- `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1588
- );
1589
- }
1501
+ if (options.crons === true) {
1502
+ const outputPath = './engine-private/crons';
1503
+ if (!fs.existsSync(outputPath)) {
1504
+ fs.mkdirSync(outputPath, { recursive: true });
1505
+ }
1506
+ const collection = 'crons';
1507
+
1508
+ if (options.export === true) {
1509
+ logger.info('Exporting crons collection', { outputPath });
1510
+ shellExec(
1511
+ `node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1512
+ );
1513
+ }
1590
1514
 
1591
- if (options.import === true) {
1592
- logger.info('Importing crons collection', { outputPath });
1593
- shellExec(
1594
- `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1595
- );
1515
+ if (options.import === true) {
1516
+ logger.info('Importing crons collection', { outputPath });
1517
+ shellExec(
1518
+ `node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
1519
+ );
1520
+ }
1596
1521
  }
1597
- }
1598
1522
 
1599
- logger.info('Cluster metadata backup operation completed');
1523
+ logger.info('Cluster metadata backup operation completed');
1524
+ } catch (error) {
1525
+ logger.error('Cluster metadata backup operation failed', { error: error.message });
1526
+ throw error;
1527
+ }
1600
1528
  },
1601
1529
  };
1602
1530
  }