mega-framework 0.1.0

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 (322) hide show
  1. package/.env +127 -0
  2. package/.env.example +186 -0
  3. package/.prettierrc.json +8 -0
  4. package/CHANGELOG.md +259 -0
  5. package/LICENSE +21 -0
  6. package/README.md +153 -0
  7. package/bin/mega-ws-hub.js +15 -0
  8. package/bin/mega.js +38 -0
  9. package/docker-compose.yml +201 -0
  10. package/eslint.config.js +57 -0
  11. package/infra/otel-collector-config.yaml +43 -0
  12. package/jsconfig.json +18 -0
  13. package/package.json +121 -0
  14. package/sample/crud/.env +18 -0
  15. package/sample/crud/.env.example +50 -0
  16. package/sample/crud/README.md +85 -0
  17. package/sample/crud/apps/main/app.config.js +114 -0
  18. package/sample/crud/apps/main/channels/chat-bus.js +115 -0
  19. package/sample/crud/apps/main/channels/chat-channel.js +145 -0
  20. package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
  21. package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
  22. package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
  23. package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
  24. package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
  25. package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
  26. package/sample/crud/apps/main/controllers/note-controller.js +116 -0
  27. package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
  28. package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
  29. package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
  30. package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
  31. package/sample/crud/apps/main/controllers/user-controller.js +34 -0
  32. package/sample/crud/apps/main/controllers/web-controller.js +137 -0
  33. package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
  34. package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
  35. package/sample/crud/apps/main/jobs/email-job.js +72 -0
  36. package/sample/crud/apps/main/locales/client/en.json +3 -0
  37. package/sample/crud/apps/main/locales/client/ko.json +3 -0
  38. package/sample/crud/apps/main/locales/server/en.json +316 -0
  39. package/sample/crud/apps/main/locales/server/ko.json +316 -0
  40. package/sample/crud/apps/main/middleware/web-auth.js +40 -0
  41. package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
  42. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
  43. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
  44. package/sample/crud/apps/main/models/note.js +71 -0
  45. package/sample/crud/apps/main/models/user.js +86 -0
  46. package/sample/crud/apps/main/public/css/app.css +101 -0
  47. package/sample/crud/apps/main/public/css/guide.css +137 -0
  48. package/sample/crud/apps/main/public/js/app.js +54 -0
  49. package/sample/crud/apps/main/public/js/perf.js +129 -0
  50. package/sample/crud/apps/main/public/js/theme-init.js +12 -0
  51. package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
  52. package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
  53. package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
  54. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  55. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  56. package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
  57. package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
  58. package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
  59. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
  60. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
  61. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
  62. package/sample/crud/apps/main/routes/auth.js +15 -0
  63. package/sample/crud/apps/main/routes/cron.js +14 -0
  64. package/sample/crud/apps/main/routes/guide.js +25 -0
  65. package/sample/crud/apps/main/routes/jobs.js +14 -0
  66. package/sample/crud/apps/main/routes/logs.js +28 -0
  67. package/sample/crud/apps/main/routes/metrics.js +13 -0
  68. package/sample/crud/apps/main/routes/notes.js +19 -0
  69. package/sample/crud/apps/main/routes/perf.js +47 -0
  70. package/sample/crud/apps/main/routes/redis.js +14 -0
  71. package/sample/crud/apps/main/routes/tracing.js +14 -0
  72. package/sample/crud/apps/main/routes/upload.js +16 -0
  73. package/sample/crud/apps/main/routes/users.js +54 -0
  74. package/sample/crud/apps/main/routes/web.js +23 -0
  75. package/sample/crud/apps/main/routes/worker.js +15 -0
  76. package/sample/crud/apps/main/routes/ws.js +30 -0
  77. package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
  78. package/sample/crud/apps/main/services/auth-service.js +74 -0
  79. package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
  80. package/sample/crud/apps/main/services/guide-service.js +145 -0
  81. package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
  82. package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
  83. package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
  84. package/sample/crud/apps/main/services/note-service.js +75 -0
  85. package/sample/crud/apps/main/services/perf-service.js +302 -0
  86. package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
  87. package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
  88. package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
  89. package/sample/crud/apps/main/services/user-service.js +65 -0
  90. package/sample/crud/apps/main/views/auth/login.ejs +57 -0
  91. package/sample/crud/apps/main/views/auth/register.ejs +71 -0
  92. package/sample/crud/apps/main/views/cron/index.ejs +92 -0
  93. package/sample/crud/apps/main/views/guide/index.ejs +24 -0
  94. package/sample/crud/apps/main/views/guide/page.ejs +64 -0
  95. package/sample/crud/apps/main/views/home.ejs +82 -0
  96. package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
  97. package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
  98. package/sample/crud/apps/main/views/logs/index.ejs +80 -0
  99. package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
  100. package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
  101. package/sample/crud/apps/main/views/notes/list.ejs +74 -0
  102. package/sample/crud/apps/main/views/notes/new.ejs +45 -0
  103. package/sample/crud/apps/main/views/perf/index.ejs +90 -0
  104. package/sample/crud/apps/main/views/redis/index.ejs +65 -0
  105. package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
  106. package/sample/crud/apps/main/views/upload/index.ejs +79 -0
  107. package/sample/crud/apps/main/views/users/edit.ejs +48 -0
  108. package/sample/crud/apps/main/views/users/list.ejs +81 -0
  109. package/sample/crud/apps/main/views/users/new.ejs +48 -0
  110. package/sample/crud/apps/main/views/worker/index.ejs +70 -0
  111. package/sample/crud/apps/main/views/ws/index.ejs +62 -0
  112. package/sample/crud/apps/main/workers/hash-worker.js +17 -0
  113. package/sample/crud/apps/main/workers/hash.task.js +22 -0
  114. package/sample/crud/ecosystem.config.cjs +9 -0
  115. package/sample/crud/mega.config.js +105 -0
  116. package/sample/crud/package-lock.json +5665 -0
  117. package/sample/crud/package.json +28 -0
  118. package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
  119. package/sample/crud/test/apps/main/auth-service.test.js +93 -0
  120. package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
  121. package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
  122. package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
  123. package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
  124. package/sample/crud/test/apps/main/email-job.test.js +76 -0
  125. package/sample/crud/test/apps/main/guide-service.test.js +68 -0
  126. package/sample/crud/test/apps/main/hash-task.test.js +30 -0
  127. package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
  128. package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
  129. package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
  130. package/sample/crud/test/apps/main/note-service.test.js +68 -0
  131. package/sample/crud/test/apps/main/perf-service.test.js +121 -0
  132. package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
  133. package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
  134. package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
  135. package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
  136. package/sample/crud/test/apps/main/user-service.test.js +65 -0
  137. package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
  138. package/sample/crud/vitest.config.js +8 -0
  139. package/sample/crud/yarn.lock +2142 -0
  140. package/sample/simple/.env.example +15 -0
  141. package/sample/simple/README.md +52 -0
  142. package/sample/simple/apps/main/app.config.js +35 -0
  143. package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
  144. package/sample/simple/apps/main/locales/client/en.json +3 -0
  145. package/sample/simple/apps/main/locales/client/ko.json +3 -0
  146. package/sample/simple/apps/main/locales/server/en.json +23 -0
  147. package/sample/simple/apps/main/locales/server/ko.json +23 -0
  148. package/sample/simple/apps/main/public/css/app.css +101 -0
  149. package/sample/simple/apps/main/public/hello.txt +1 -0
  150. package/sample/simple/apps/main/public/js/app.js +54 -0
  151. package/sample/simple/apps/main/public/js/theme-init.js +12 -0
  152. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  153. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  154. package/sample/simple/apps/main/routes/index.js +9 -0
  155. package/sample/simple/apps/main/routes/pages.js +12 -0
  156. package/sample/simple/apps/main/views/index.ejs +56 -0
  157. package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
  158. package/sample/simple/ecosystem.config.cjs +10 -0
  159. package/sample/simple/mega.config.js +27 -0
  160. package/sample/simple/package-lock.json +1851 -0
  161. package/sample/simple/package.json +25 -0
  162. package/sample/simple/test/apps/main/index.test.js +13 -0
  163. package/sample/simple/vitest.config.js +8 -0
  164. package/src/adapters/adapter-manager.js +305 -0
  165. package/src/adapters/adapter-options.js +208 -0
  166. package/src/adapters/file-adapter.js +350 -0
  167. package/src/adapters/file-session-adapter.js +363 -0
  168. package/src/adapters/index.js +38 -0
  169. package/src/adapters/maria-adapter.js +425 -0
  170. package/src/adapters/mega-adapter.js +511 -0
  171. package/src/adapters/mega-bus-adapter.js +81 -0
  172. package/src/adapters/mega-cache-adapter.js +94 -0
  173. package/src/adapters/mega-db-adapter.js +72 -0
  174. package/src/adapters/mega-lock-adapter.js +118 -0
  175. package/src/adapters/mega-log-sink-adapter.js +46 -0
  176. package/src/adapters/mega-session-adapter.js +72 -0
  177. package/src/adapters/mongo-adapter.js +396 -0
  178. package/src/adapters/nats-adapter.js +370 -0
  179. package/src/adapters/postgres-adapter.js +341 -0
  180. package/src/adapters/redis-adapter.js +331 -0
  181. package/src/adapters/redis-session-adapter.js +261 -0
  182. package/src/adapters/redlock-adapter.js +385 -0
  183. package/src/adapters/registry.js +157 -0
  184. package/src/adapters/sqlite-adapter.js +309 -0
  185. package/src/auth/index.js +103 -0
  186. package/src/cli/commands/console-cmd.js +56 -0
  187. package/src/cli/commands/new.js +101 -0
  188. package/src/cli/commands/routes.js +107 -0
  189. package/src/cli/commands/scaffold.js +120 -0
  190. package/src/cli/commands/test-cmd.js +45 -0
  191. package/src/cli/generators/index.js +368 -0
  192. package/src/cli/index.js +472 -0
  193. package/src/cli/template-engine.js +72 -0
  194. package/src/cli/ws-hub.js +582 -0
  195. package/src/core/ajv-mapper.js +80 -0
  196. package/src/core/boot.js +323 -0
  197. package/src/core/cluster-metrics.js +278 -0
  198. package/src/core/config-loader.js +115 -0
  199. package/src/core/config-validator.js +322 -0
  200. package/src/core/ctx-builder.js +253 -0
  201. package/src/core/envelope.js +88 -0
  202. package/src/core/error-mapper.js +116 -0
  203. package/src/core/formbody.js +69 -0
  204. package/src/core/hub-link.js +552 -0
  205. package/src/core/i18n.js +525 -0
  206. package/src/core/index.js +63 -0
  207. package/src/core/mega-app.js +1138 -0
  208. package/src/core/mega-cluster.js +232 -0
  209. package/src/core/mega-server.js +176 -0
  210. package/src/core/mega-service.js +41 -0
  211. package/src/core/migration-runner.js +196 -0
  212. package/src/core/multipart.js +282 -0
  213. package/src/core/openapi.js +114 -0
  214. package/src/core/router.js +388 -0
  215. package/src/core/routes-loader.js +57 -0
  216. package/src/core/scope-registry.js +53 -0
  217. package/src/core/security.js +275 -0
  218. package/src/core/services-loader.js +98 -0
  219. package/src/core/session-cleanup-schedule.js +57 -0
  220. package/src/core/session-store.js +55 -0
  221. package/src/core/session.js +414 -0
  222. package/src/core/static-assets.js +126 -0
  223. package/src/core/template.js +294 -0
  224. package/src/core/workers-manager.js +193 -0
  225. package/src/core/ws-compression.js +112 -0
  226. package/src/core/ws-controller.js +109 -0
  227. package/src/core/ws-message.js +176 -0
  228. package/src/core/ws-upgrade.js +445 -0
  229. package/src/errors/config-error.js +16 -0
  230. package/src/errors/http-errors.js +130 -0
  231. package/src/errors/index.js +19 -0
  232. package/src/errors/mega-error.js +34 -0
  233. package/src/eslint-plugin/index.js +15 -0
  234. package/src/eslint-plugin/no-direct-model-import.js +113 -0
  235. package/src/index.js +131 -0
  236. package/src/lib/asp/config.js +83 -0
  237. package/src/lib/asp/crypto.js +145 -0
  238. package/src/lib/asp/errors.js +49 -0
  239. package/src/lib/asp/nonce-cache.js +94 -0
  240. package/src/lib/asp/plugin.js +263 -0
  241. package/src/lib/asp/ws-terminator.js +101 -0
  242. package/src/lib/env-mapper.js +222 -0
  243. package/src/lib/hub-protocol.js +322 -0
  244. package/src/lib/index.js +42 -0
  245. package/src/lib/logger/telegram-core.js +150 -0
  246. package/src/lib/logger/telegram-transport.js +126 -0
  247. package/src/lib/mega-brute-force.js +225 -0
  248. package/src/lib/mega-circuit-breaker.js +412 -0
  249. package/src/lib/mega-cron.js +169 -0
  250. package/src/lib/mega-hash.js +179 -0
  251. package/src/lib/mega-health.js +91 -0
  252. package/src/lib/mega-job-queue.js +600 -0
  253. package/src/lib/mega-job-worker.js +295 -0
  254. package/src/lib/mega-job.js +140 -0
  255. package/src/lib/mega-logger.js +128 -0
  256. package/src/lib/mega-metrics.js +661 -0
  257. package/src/lib/mega-plugin.js +650 -0
  258. package/src/lib/mega-retry.js +95 -0
  259. package/src/lib/mega-schedule.js +507 -0
  260. package/src/lib/mega-shutdown.js +176 -0
  261. package/src/lib/mega-tracing.js +715 -0
  262. package/src/lib/mega-worker.js +653 -0
  263. package/src/lib/worker-runner/process-entry.js +30 -0
  264. package/src/lib/worker-runner/task-dispatch.js +72 -0
  265. package/src/lib/worker-runner/thread-entry.js +26 -0
  266. package/src/models/index.js +7 -0
  267. package/src/models/mega-model.js +151 -0
  268. package/src/test/index.js +288 -0
  269. package/templates/adapter/code.tpl +40 -0
  270. package/templates/adapter/test.tpl +13 -0
  271. package/templates/app/app.config.tpl +10 -0
  272. package/templates/app/route.tpl +10 -0
  273. package/templates/app/test.tpl +13 -0
  274. package/templates/channel/code.tpl +38 -0
  275. package/templates/channel/test.tpl +19 -0
  276. package/templates/controller/code.tpl +16 -0
  277. package/templates/controller/route.tpl +9 -0
  278. package/templates/controller/test.tpl +14 -0
  279. package/templates/job/code.tpl +23 -0
  280. package/templates/job/test.tpl +17 -0
  281. package/templates/locale/code.tpl +3 -0
  282. package/templates/locale/test.tpl +13 -0
  283. package/templates/middleware/code.tpl +13 -0
  284. package/templates/middleware/test.tpl +11 -0
  285. package/templates/migration/code.tpl +20 -0
  286. package/templates/migration/test.tpl +14 -0
  287. package/templates/model/code.tpl +21 -0
  288. package/templates/model/test.tpl +29 -0
  289. package/templates/project/app.config.tpl +8 -0
  290. package/templates/project/app.config.views.tpl +37 -0
  291. package/templates/project/ecosystem.config.tpl +10 -0
  292. package/templates/project/env.tpl +12 -0
  293. package/templates/project/gitignore.tpl +8 -0
  294. package/templates/project/locales/client/en.json.tpl +3 -0
  295. package/templates/project/locales/client/ko.json.tpl +3 -0
  296. package/templates/project/locales/server/en.json.tpl +17 -0
  297. package/templates/project/locales/server/ko.json.tpl +17 -0
  298. package/templates/project/mega.config.tpl +11 -0
  299. package/templates/project/package.tpl +25 -0
  300. package/templates/project/public/css/app.css +101 -0
  301. package/templates/project/public/js/app.js +54 -0
  302. package/templates/project/public/js/theme-init.js +12 -0
  303. package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  304. package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
  305. package/templates/project/readme.tpl +48 -0
  306. package/templates/project/route.test.tpl +13 -0
  307. package/templates/project/route.test.views.tpl +15 -0
  308. package/templates/project/route.tpl +10 -0
  309. package/templates/project/route.views.tpl +10 -0
  310. package/templates/project/views/index.ejs.tpl +58 -0
  311. package/templates/project/views/layout.ejs.tpl +73 -0
  312. package/templates/project/vitest.config.tpl +8 -0
  313. package/templates/route/code.tpl +11 -0
  314. package/templates/route/test.tpl +26 -0
  315. package/templates/schedule/code.tpl +19 -0
  316. package/templates/schedule/test.tpl +17 -0
  317. package/templates/service/code.tpl +18 -0
  318. package/templates/service/test.tpl +17 -0
  319. package/templates/worker/code.tpl +14 -0
  320. package/templates/worker/task.tpl +13 -0
  321. package/templates/worker/test.tpl +18 -0
  322. package/vitest.config.js +33 -0
@@ -0,0 +1,396 @@
1
+ // @ts-check
2
+ /**
3
+ * MegaMongoAdapter — MongoDB 어댑터 (`mongodb` 공식 driver 래퍼, ADR-108).
4
+ *
5
+ * **첫 Document DB 어댑터**. SQL 어댑터(Sqlite/Postgres/Maria)와 API 패턴과
6
+ * 트랜잭션 모델이 다르다 — `db.collection(...)` 도큐먼트 API + `session.withTransaction`.
7
+ *
8
+ * # 표준 표면 (MegaDbAdapter 상속)
9
+ * - `_connect()` — `new MongoClient(uri, opts)` + `client.connect()` + `db.command({ ping })` 검증
10
+ * (driver 는 connect 시점 lazy import — SQL 어댑터 패턴 정합, ADR-105/106/107).
11
+ * - `_disconnect()`— `client.close()`.
12
+ * - `_native()` — `Db` 인스턴스(raw handle, ADR-009). `MegaModel.db` 가 노출 →
13
+ * 사용자는 `this.db.collection(table)` 로 도큐먼트 API 접근.
14
+ * - `healthCheck()`— `db.command({ ping: 1 })` 실행으로 실제 응답성 확인.
15
+ * - `getStats()` — 베이스 stats + mongo 특화(driver/dbName + CMAP 이벤트 기반 풀 카운터).
16
+ * - `withTransaction(fn)` — 명시적 트랜잭션 경계 (ADR-010). 아래 "트랜잭션 패턴" 참조.
17
+ *
18
+ * # 트랜잭션 패턴 — `session.withTransaction` 위임 (driver 가 commit/retry 관리)
19
+ * `client.startSession()` 으로 세션 1개를 잡아 `session.withTransaction(() => fn(db, session))`
20
+ * 로 위임한다. driver v6 의 `withTransaction` 은 **콜백 반환값을 그대로 반환**하고(공식 문서),
21
+ * 콜백 성공 시 commit / throw 시 abort 를 driver 가 처리한다. 또한 `TransientTransactionError`/
22
+ * `UnknownTransactionCommitResult` 발생 시 **exponential backoff 자동 retry** 도 driver 위임이다
23
+ * (우리가 따로 retry 루프를 만들지 않는다). `session.endSession()` 은 성공/실패 무관 `finally` 에서
24
+ * 반드시 호출해 세션 leak 을 막는다.
25
+ *
26
+ * `fn` 은 `(db, session)` 두 인자를 받는다 — 사용자는 도큐먼트 연산에 `{ session }` 을 넘겨야
27
+ * 해당 연산이 트랜잭션에 묶인다. 예:
28
+ * ```js
29
+ * await User.withTransaction(async (db, session) => {
30
+ * await db.collection('users').insertOne({ name: 'kim' }, { session })
31
+ * await db.collection('logs').insertOne({ event: 'signup' }, { session })
32
+ * })
33
+ * ```
34
+ *
35
+ * # nested 트랜잭션 정책 — 재진입 거부 (Sqlite 와 동일, Postgres SAVEPOINT 와 분기, ADR-108)
36
+ * 진행 중 트랜잭션 위에서 `withTransaction` 을 다시 부르면 즉시
37
+ * `adapter.nested_transaction_unsupported` 를 throw 한다(Sqlite 어댑터와 **동일 에러 코드**).
38
+ * 진행 중 여부는 **`AsyncLocalStorage`** 로 추적한다(Postgres 패턴 정합) — `withTransaction` 진입
39
+ * 시 store 가 있으면 nested 로 판정. MongoDB 는 단일 세션 내 트랜잭션 nesting 을 지원하지 않으며
40
+ * (한 세션에 트랜잭션은 하나), 매 nested 마다 새 세션을 따로 잡으면 별개 트랜잭션이 되어 부분
41
+ * 롤백 의미가 깨지므로(바깥 abort 가 안쪽에 전파 안 됨) **명시적 거부**가 silent 혼선보다 안전.
42
+ * SAVEPOINT 동등물이 도큐먼트 모델에 없어 Postgres 의 nested 와 분기한다.
43
+ *
44
+ * ⚠️ ALS 격리는 **서로 다른 top-level 호출 사이**의 동시성에만 race-free 를 보장한다(Postgres와
45
+ * 동일 경계). 단일 트랜잭션 내부에서 sibling 을 `Promise.all` 로 병렬 실행하는 것은
46
+ * nested 거부 대상이며 애초에 지원하지 않는다 — nested 는 항상 순차여야 하고 본 어댑터는 거부한다.
47
+ *
48
+ * # replica set 요구사항 (중요)
49
+ * MongoDB 트랜잭션은 **replica set(또는 mongos)** 에서만 동작한다 — standalone 은 미지원
50
+ * (server 에러: "Transaction numbers are only allowed on a replica set member or mongos").
51
+ * `withTransaction` 자체 코드는 standalone 에서도 nested 거부까지는 동작하나, 실제 commit 은
52
+ * replica set 이 필요하다. docker-compose 의 `mongo:7` 은 standalone 이므로 트랜잭션 통합
53
+ * 테스트는 replica set Mongo(예: URL 에 `?replicaSet=rs0`)를 `MEGA_MONGO_URL` 로 주입할 때만
54
+ * 실행되고, 그 외엔 자동 skip 된다(ADR-108).
55
+ *
56
+ * # 설정 (services.databases.<key>) — 통합 옵션 구조 (ADR-109)
57
+ * ```js
58
+ * services: {
59
+ * databases: {
60
+ * primary: {
61
+ * driver: 'mongodb',
62
+ * // (a) 연결 — url 또는 discrete (host/port/user/password)
63
+ * url: 'mongodb://user:pw@host:27017/mega_test?authSource=admin', // 또는 ↓
64
+ * host: 'localhost', port: 27017, user: 'mega', password: '...',
65
+ * // dbName — 선택할 DB. url 의 path 에 있으면 추출, 명시 지정이 우선. (url 과 충돌 X)
66
+ * dbName: 'mega_test',
67
+ * // (b) pool — 공통 풀 인터페이스
68
+ * pool: { min: 0, max: 10, idleTimeoutMs: 60000, acquireTimeoutMs: 10000 },
69
+ * // (c) options — MongoClient passthrough
70
+ * options: { authSource: 'admin', replicaSet: 'rs0', tls: true, readPreference: 'primary',
71
+ * serverSelectionTimeoutMS: 5000, connectTimeoutMS: 10000 },
72
+ * },
73
+ * },
74
+ * }
75
+ * ```
76
+ * `url`(또는 deprecated 별칭 `connectionString`) **XOR** discrete(host/port/user/password). `dbName`
77
+ * 은 connection 과 **별개 축**(client.db 선택)이라 url 과 충돌하지 않으며, url path 에 db 가 있으면
78
+ * 거기서 추출(명시 `dbName` 우선). 연결·dbName 둘 다 없으면 `adapter.connection_required`/
79
+ * `adapter.dbname_required`. pool 매핑: min→minPoolSize, max→maxPoolSize, idleTimeoutMs→maxIdleTimeMS,
80
+ * acquireTimeoutMs→waitQueueTimeoutMS. `maxLifetimeMs` 는 mongo 미지원(throw). 비밀번호/url 은
81
+ * healthCheck/getStats/에러 details 에 절대 노출하지 않는다(정합).
82
+ *
83
+ * @module adapters/mongo-adapter
84
+ */
85
+ import { AsyncLocalStorage } from 'node:async_hooks'
86
+ import { MegaInternalError, MegaValidationError } from '../errors/http-errors.js'
87
+ import { MegaDbAdapter } from './mega-db-adapter.js'
88
+ import { resolveConnection, normalizePool, assertPlainObject, MONGO_POOL_SPEC } from './adapter-options.js'
89
+ import * as Registry from './registry.js'
90
+
91
+ /**
92
+ * @typedef {object} MongoConfig
93
+ * @property {string} [driver] - 'mongodb' (매니저가 사용 — 어댑터는 무시).
94
+ * @property {string} [url] - `mongodb://...` 연결 문자열 (discrete 와 배타).
95
+ * @property {string} [connectionString] - `url` 의 deprecated 별칭 (하위 호환).
96
+ * @property {string} [host] @property {number} [port] @property {string} [user] @property {string} [password]
97
+ * @property {string} [dbName] - 선택할 DB (url path 에서 추출 가능, 명시 우선).
98
+ * @property {{ min?: number, max?: number, idleTimeoutMs?: number, acquireTimeoutMs?: number }} [pool] - 공통 풀 인터페이스.
99
+ * @property {Record<string, any>} [options] - MongoClient passthrough (authSource, replicaSet, tls, readPreference, serverSelectionTimeoutMS, …).
100
+ */
101
+
102
+ /**
103
+ * url path 에서 dbName 추출 (명시 dbName 없을 때 폴백). multi-host/SRV 등 WHATWG URL 파싱 실패는
104
+ * undefined 폴백 → 상위에서 dbName 명시 요구로 처리.
105
+ * @param {string} url @returns {string | undefined}
106
+ */
107
+ function extractMongoDbName(url) {
108
+ try {
109
+ const db = new URL(url).pathname.replace(/^\//, '')
110
+ return db.length > 0 ? db : undefined
111
+ } catch {
112
+ // multi-host(`h1,h2`)/SRV URL 은 표준 URL 파서가 거부할 수 있음 — 추출 실패 시 dbName 명시 요구로 폴백.
113
+ return undefined
114
+ }
115
+ }
116
+
117
+ /**
118
+ * discrete 연결필드(host/port/user/password)로 mongodb:// URI 구성. authSource 등은 options 로.
119
+ * @param {{ host?: string, port?: number, user?: string, password?: string }} conn @returns {string}
120
+ */
121
+ function buildMongoUri({ host, port, user, password }) {
122
+ const auth = user !== undefined ? `${encodeURIComponent(user)}${password !== undefined ? ':' + encodeURIComponent(password) : ''}@` : ''
123
+ const hostPart = host ?? 'localhost'
124
+ const portPart = port !== undefined ? `:${port}` : ''
125
+ return `mongodb://${auth}${hostPart}${portPart}`
126
+ }
127
+
128
+ /**
129
+ * @typedef {object} MongoTxContext - AsyncLocalStorage 에 담기는 트랜잭션 컨텍스트.
130
+ * @property {import('mongodb').ClientSession} session - 진행 중 트랜잭션을 소유한 세션.
131
+ */
132
+
133
+ /**
134
+ * CMAP(Connection Monitoring and Pooling) 이벤트 누적 카운터. mongodb driver 는 풀 통계를
135
+ * 직접 노출하는 공개 API 가 없어(`topology` 는 비공개 내부), 표준 CMAP 이벤트(기본 발생)를
136
+ * 구독해 누적한다. open=created-closed, inUse=checkedOut-checkedIn 으로 파생.
137
+ * @typedef {object} PoolCounters
138
+ * @property {number} created @property {number} closed
139
+ * @property {number} checkedOut @property {number} checkedIn
140
+ */
141
+
142
+ export class MegaMongoAdapter extends MegaDbAdapter {
143
+ /** @type {import('mongodb').MongoClient | null} 연결된 MongoClient (connect 후에만). */
144
+ #client = null
145
+ /** @type {import('mongodb').Db | null} 선택된 Db 인스턴스 (connect 후에만). */
146
+ #db = null
147
+ /** @type {string} 연결 문자열 (시크릿 포함 — 외부 노출 금지). */
148
+ #connectionString
149
+ /** @type {string} 데이터베이스 이름. */
150
+ #dbName
151
+ /** @type {import('mongodb').MongoClientOptions} _connect 에서 `new MongoClient()` 에 넘길 옵션. */
152
+ #clientOptions
153
+ /**
154
+ * 진행 중 트랜잭션 추적기 — nested 호출을 감지해 거부한다(인스턴스 카운터 X).
155
+ * 동시 top-level 트랜잭션은 각자 `run` 으로 격리된 store 를 가진다(async race-free, Postgres 정합).
156
+ * @type {AsyncLocalStorage<MongoTxContext>}
157
+ */
158
+ #txContext = new AsyncLocalStorage()
159
+ /** @type {PoolCounters} CMAP 이벤트 누적 풀 카운터. */
160
+ #pool = { created: 0, closed: 0, checkedOut: 0, checkedIn: 0 }
161
+ /**
162
+ * 등록한 CMAP 리스너 — disconnect 시 정확히 제거하기 위해 보관(누수·재연결 시 중복 방지).
163
+ * @type {Array<[string, (...args: any[]) => void]>}
164
+ */
165
+ #poolListeners = []
166
+
167
+ /**
168
+ * @param {MongoConfig} [config] - services.databases.<key> 설정.
169
+ * @throws {MegaValidationError} `adapter.connection_required` - url·discrete 둘 다 없음.
170
+ * @throws {MegaValidationError} `adapter.connection_conflict` - url + discrete 동시 지정.
171
+ * @throws {MegaValidationError} `adapter.dbname_required` - dbName 누락 + url path 에도 없음.
172
+ * @throws {MegaValidationError} `adapter.invalid_option` - 옵션 타입/범위 오류.
173
+ */
174
+ constructor(config = /** @type {any} */ ({})) {
175
+ super(config)
176
+
177
+ // 연결 모드(url XOR discrete) 결정. dbName 은 connection 과 별개 축이라 url 과 충돌하지 않음(ADR-109).
178
+ const conn = resolveConnection(config, { driver: 'mongodb', dbConflictsWithUrl: false })
179
+ const uri = conn.url !== undefined ? conn.url : buildMongoUri(conn)
180
+
181
+ // dbName: 명시 우선, 없으면 url path 에서 추출. 둘 다 없으면 fail-fast(client.db(dbName) 필수).
182
+ let dbName = config.dbName
183
+ if ((dbName === undefined || dbName === null) && conn.url !== undefined) {
184
+ dbName = extractMongoDbName(conn.url)
185
+ }
186
+ if (typeof dbName !== 'string' || dbName.length === 0) {
187
+ throw new MegaValidationError(
188
+ 'adapter.dbname_required',
189
+ 'mongodb: "dbName" is required (the database to select via client.db(dbName)). Provide it explicitly or include a path in the url (mongodb://host/<dbName>).',
190
+ { details: { driver: 'mongodb', dbName: dbName ?? null } },
191
+ )
192
+ }
193
+ this.#connectionString = uri
194
+ this.#dbName = dbName
195
+
196
+ // 공통 풀 인터페이스 → mongo 풀 키 매핑 + options passthrough.
197
+ const poolOpts = normalizePool(config.pool, MONGO_POOL_SPEC, 'mongodb')
198
+ assertPlainObject('options', config.options, { driver: 'mongodb' })
199
+ /** @type {import('mongodb').MongoClientOptions} */
200
+ const clientOptions = {}
201
+ if (config.options !== undefined) Object.assign(clientOptions, config.options)
202
+ Object.assign(clientOptions, poolOpts) // pool(정규화) 나중 — 겹치면 명시 pool 인터페이스가 이긴다.
203
+ this.#clientOptions = clientOptions
204
+ }
205
+
206
+ /**
207
+ * `MongoClient` 생성 + connect + `ping` 으로 실제 연결 검증.
208
+ * driver 는 **connect 시점에 lazy import** — 모듈 import(배럴 경유 자기등록)만으로는 mongodb 를
209
+ * 로드하지 않아, 본 어댑터를 안 쓰는 환경이 mongodb 설치를 강제받지 않는다(SQL 어댑터 정합).
210
+ *
211
+ * @protected
212
+ * @returns {Promise<void>}
213
+ */
214
+ async _connect() {
215
+ const { MongoClient } = await import('mongodb')
216
+ const client = new MongoClient(this.#connectionString, this.#clientOptions)
217
+ // CMAP 풀 이벤트 구독은 connect 전에 — 초기 연결 생성/체크아웃 이벤트도 빠짐없이 카운트.
218
+ this.#subscribePoolEvents(client)
219
+ try {
220
+ await client.connect()
221
+ const db = client.db(this.#dbName)
222
+ // connect 직후 실제 ping 으로 연결·인증 검증 — 잘못된 자격증명/호스트를 부팅 시 잡는다(fail-fast).
223
+ await db.command({ ping: 1 })
224
+ this.#client = client
225
+ this.#db = db
226
+ } catch (err) {
227
+ // 검증 실패 시 leak 방지를 위해 client 정리 후 원본 에러 전파.
228
+ this.#unsubscribePoolEvents(client)
229
+ try {
230
+ await client.close()
231
+ } catch (closeErr) {
232
+ // 검증 실패 후 정리(close) 실패는 비치명적 — 원본 연결 에러가 진짜 원인.
233
+ console.warn('[MegaMongoAdapter] client.close() failed after connect validation error (original error wins):', closeErr)
234
+ }
235
+ throw err
236
+ }
237
+ }
238
+
239
+ /**
240
+ * MongoClient 종료(모든 연결 정리). 베이스 상태 머신이 connected 상태에서만 호출 보장.
241
+ * @protected
242
+ * @returns {Promise<void>}
243
+ */
244
+ async _disconnect() {
245
+ if (this.#client !== null) {
246
+ const client = this.#client
247
+ this.#client = null
248
+ this.#db = null
249
+ this.#unsubscribePoolEvents(client)
250
+ await client.close()
251
+ }
252
+ }
253
+
254
+ /**
255
+ * raw Db handle (ADR-009). `connect()` 후에만 베이스 `native` getter 가 호출한다.
256
+ * @protected
257
+ * @returns {import('mongodb').Db}
258
+ */
259
+ _native() {
260
+ if (this.#db === null) {
261
+ // 베이스 native getter 가 state 검증을 먼저 하므로 정상 경로에선 도달 안 함 — 방어.
262
+ return this._notImplemented('native')
263
+ }
264
+ return this.#db
265
+ }
266
+
267
+ /**
268
+ * 헬스 체크 — 실제 `ping` 으로 응답성 확인 (베이스 디폴트는 상태만 반영).
269
+ * 실패는 throw 없이 `ok:false` + 사유(베이스 계약). 비밀번호/connectionString 은 노출하지 않는다.
270
+ *
271
+ * @returns {Promise<{ ok: boolean, driver: 'mongodb', state: string, dbName?: string, error?: string }>}
272
+ */
273
+ async healthCheck() {
274
+ if (this.state !== 'connected' || this.#db === null) {
275
+ return { ok: false, driver: 'mongodb', state: this.state }
276
+ }
277
+ try {
278
+ const res = await this.#db.command({ ping: 1 })
279
+ // mongo ping 응답은 `{ ok: 1 }`.
280
+ const ok = res?.ok === 1
281
+ return { ok, driver: 'mongodb', state: this.state, dbName: this.#dbName }
282
+ } catch (err) {
283
+ return {
284
+ ok: false,
285
+ driver: 'mongodb',
286
+ state: this.state,
287
+ dbName: this.#dbName,
288
+ error: err instanceof Error ? err.message : String(err),
289
+ }
290
+ }
291
+ }
292
+
293
+ /**
294
+ * 누적 통계 + mongo 특화(driver/dbName + CMAP 풀 카운터). 연결 전이면 카운터는 0.
295
+ * @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, dbName: string, pool: { created: number, closed: number, checkedOut: number, checkedIn: number, open: number, inUse: number } }}
296
+ */
297
+ getStats() {
298
+ const { created, closed, checkedOut, checkedIn } = this.#pool
299
+ return {
300
+ ...super.getStats(),
301
+ driver: 'mongodb',
302
+ dbName: this.#dbName,
303
+ pool: {
304
+ created,
305
+ closed,
306
+ checkedOut,
307
+ checkedIn,
308
+ // 파생값 — open=살아있는 연결 수, inUse=현재 체크아웃된(작업 중) 연결 수.
309
+ open: created - closed,
310
+ inUse: checkedOut - checkedIn,
311
+ },
312
+ }
313
+ }
314
+
315
+ /**
316
+ * 명시적 트랜잭션 경계 (ADR-010, ADR-108). `session.withTransaction` 에 위임한다 —
317
+ * driver 가 commit/abort + transient 에러 자동 retry 를 관리(모듈 docstring 참조).
318
+ *
319
+ * `fn` 은 `(db, session)` 을 받는다. 성공 시 driver 가 commit 후 `fn` 반환값을 그대로 돌려주고,
320
+ * throw 시 abort 후 원본 에러를 re-throw 한다. nested 호출은 ALS 로 감지해 거부(Sqlite 와 동일).
321
+ * `session.endSession()` 은 `finally` 에서 반드시 호출(leak 방지).
322
+ *
323
+ * hook(`onCallStart/onCallEnd`) + 상태 검증 + stats 누적은 `_instrument` 가 처리한다(ADR-077).
324
+ *
325
+ * @template T
326
+ * @param {(db: import('mongodb').Db, session: import('mongodb').ClientSession) => Promise<T> | T} fn
327
+ * @returns {Promise<T>}
328
+ * @throws {MegaInternalError} `adapter.nested_transaction_unsupported` - 이미 트랜잭션 진행 중.
329
+ */
330
+ async withTransaction(fn) {
331
+ return this._instrument('withTransaction', { table: undefined }, async () => {
332
+ // nested 거부 — 진행 중 트랜잭션이 있으면 즉시 throw (Sqlite 와 동일 정책·에러 코드, ADR-108).
333
+ // 검사를 driver 호출 전에 두어, standalone(트랜잭션 미지원)에서도 nested 거부는 동작한다.
334
+ if (this.#txContext.getStore() !== undefined) {
335
+ throw new MegaInternalError(
336
+ 'adapter.nested_transaction_unsupported',
337
+ `${this.constructor.name}.withTransaction() cannot nest — a transaction is already in progress on this session (ADR-108). MongoDB does not support nested transactions within one session.`,
338
+ { details: { adapter: this.constructor.name, dbName: this.#dbName } },
339
+ )
340
+ }
341
+ const client = /** @type {import('mongodb').MongoClient} */ (this.#client)
342
+ const db = /** @type {import('mongodb').Db} */ (this.#db)
343
+ const session = client.startSession()
344
+ try {
345
+ // ALS 로 세션을 격리 — 내부에서 withTransaction 재호출 시 store 가 보여 nested 거부됨.
346
+ // session.withTransaction(v6)은 콜백 반환값을 그대로 반환한다(공식 문서).
347
+ return await this.#txContext.run({ session }, () => session.withTransaction(async () => fn(db, session)))
348
+ } finally {
349
+ // 성공/실패 무관 — 세션 반드시 정리(leak 방지). endSession 실패는 원본 결과/에러를
350
+ // 가리지 않도록 격리 후 경고만.
351
+ try {
352
+ await session.endSession()
353
+ } catch (endErr) {
354
+ console.warn('[MegaMongoAdapter] session.endSession() failed (original result/error wins):', endErr)
355
+ }
356
+ }
357
+ })
358
+ }
359
+
360
+ /**
361
+ * CMAP 풀 이벤트(기본 발생)를 구독해 풀 카운터를 누적한다. mongodb driver 가 풀 통계를 직접
362
+ * 노출하지 않으므로 표준 이벤트로 대체(getStats 의 pool 필드). 리스너는 disconnect 시 제거한다.
363
+ *
364
+ * @param {import('mongodb').MongoClient} client
365
+ * @returns {void}
366
+ */
367
+ #subscribePoolEvents(client) {
368
+ /** @type {Array<[string, () => void]>} */
369
+ const listeners = [
370
+ ['connectionCreated', () => (this.#pool.created += 1)],
371
+ ['connectionClosed', () => (this.#pool.closed += 1)],
372
+ ['connectionCheckedOut', () => (this.#pool.checkedOut += 1)],
373
+ ['connectionCheckedIn', () => (this.#pool.checkedIn += 1)],
374
+ ]
375
+ for (const [event, handler] of listeners) {
376
+ client.on(event, handler)
377
+ this.#poolListeners.push([event, handler])
378
+ }
379
+ }
380
+
381
+ /**
382
+ * 구독했던 CMAP 리스너를 정확히 제거(누수·재연결 중복 방지).
383
+ * @param {import('mongodb').MongoClient} client
384
+ * @returns {void}
385
+ */
386
+ #unsubscribePoolEvents(client) {
387
+ for (const [event, handler] of this.#poolListeners) {
388
+ client.off(event, handler)
389
+ }
390
+ this.#poolListeners = []
391
+ }
392
+ }
393
+
394
+ // 빌트인 driver 자기등록 (ADR-044) — 배럴(`adapters/index.js`)이 본 모듈을 import 하면 트리거된다.
395
+ // mongodb 는 _connect() 의 lazy import 까지 로드되지 않으므로 등록은 안전(미사용 환경 비강제).
396
+ Registry.register('mongodb', MegaMongoAdapter)