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.
- package/.env +127 -0
- package/.env.example +186 -0
- package/.prettierrc.json +8 -0
- package/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/mega-ws-hub.js +15 -0
- package/bin/mega.js +38 -0
- package/docker-compose.yml +201 -0
- package/eslint.config.js +57 -0
- package/infra/otel-collector-config.yaml +43 -0
- package/jsconfig.json +18 -0
- package/package.json +121 -0
- package/sample/crud/.env +18 -0
- package/sample/crud/.env.example +50 -0
- package/sample/crud/README.md +85 -0
- package/sample/crud/apps/main/app.config.js +114 -0
- package/sample/crud/apps/main/channels/chat-bus.js +115 -0
- package/sample/crud/apps/main/channels/chat-channel.js +145 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
- package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
- package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
- package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
- package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
- package/sample/crud/apps/main/controllers/note-controller.js +116 -0
- package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
- package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
- package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
- package/sample/crud/apps/main/controllers/user-controller.js +34 -0
- package/sample/crud/apps/main/controllers/web-controller.js +137 -0
- package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
- package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
- package/sample/crud/apps/main/jobs/email-job.js +72 -0
- package/sample/crud/apps/main/locales/client/en.json +3 -0
- package/sample/crud/apps/main/locales/client/ko.json +3 -0
- package/sample/crud/apps/main/locales/server/en.json +316 -0
- package/sample/crud/apps/main/locales/server/ko.json +316 -0
- package/sample/crud/apps/main/middleware/web-auth.js +40 -0
- package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
- package/sample/crud/apps/main/models/note.js +71 -0
- package/sample/crud/apps/main/models/user.js +86 -0
- package/sample/crud/apps/main/public/css/app.css +101 -0
- package/sample/crud/apps/main/public/css/guide.css +137 -0
- package/sample/crud/apps/main/public/js/app.js +54 -0
- package/sample/crud/apps/main/public/js/perf.js +129 -0
- package/sample/crud/apps/main/public/js/theme-init.js +12 -0
- package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
- package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
- package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
- package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
- package/sample/crud/apps/main/routes/auth.js +15 -0
- package/sample/crud/apps/main/routes/cron.js +14 -0
- package/sample/crud/apps/main/routes/guide.js +25 -0
- package/sample/crud/apps/main/routes/jobs.js +14 -0
- package/sample/crud/apps/main/routes/logs.js +28 -0
- package/sample/crud/apps/main/routes/metrics.js +13 -0
- package/sample/crud/apps/main/routes/notes.js +19 -0
- package/sample/crud/apps/main/routes/perf.js +47 -0
- package/sample/crud/apps/main/routes/redis.js +14 -0
- package/sample/crud/apps/main/routes/tracing.js +14 -0
- package/sample/crud/apps/main/routes/upload.js +16 -0
- package/sample/crud/apps/main/routes/users.js +54 -0
- package/sample/crud/apps/main/routes/web.js +23 -0
- package/sample/crud/apps/main/routes/worker.js +15 -0
- package/sample/crud/apps/main/routes/ws.js +30 -0
- package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
- package/sample/crud/apps/main/services/auth-service.js +74 -0
- package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
- package/sample/crud/apps/main/services/guide-service.js +145 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
- package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
- package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
- package/sample/crud/apps/main/services/note-service.js +75 -0
- package/sample/crud/apps/main/services/perf-service.js +302 -0
- package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
- package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
- package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
- package/sample/crud/apps/main/services/user-service.js +65 -0
- package/sample/crud/apps/main/views/auth/login.ejs +57 -0
- package/sample/crud/apps/main/views/auth/register.ejs +71 -0
- package/sample/crud/apps/main/views/cron/index.ejs +92 -0
- package/sample/crud/apps/main/views/guide/index.ejs +24 -0
- package/sample/crud/apps/main/views/guide/page.ejs +64 -0
- package/sample/crud/apps/main/views/home.ejs +82 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
- package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
- package/sample/crud/apps/main/views/logs/index.ejs +80 -0
- package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
- package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
- package/sample/crud/apps/main/views/notes/list.ejs +74 -0
- package/sample/crud/apps/main/views/notes/new.ejs +45 -0
- package/sample/crud/apps/main/views/perf/index.ejs +90 -0
- package/sample/crud/apps/main/views/redis/index.ejs +65 -0
- package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
- package/sample/crud/apps/main/views/upload/index.ejs +79 -0
- package/sample/crud/apps/main/views/users/edit.ejs +48 -0
- package/sample/crud/apps/main/views/users/list.ejs +81 -0
- package/sample/crud/apps/main/views/users/new.ejs +48 -0
- package/sample/crud/apps/main/views/worker/index.ejs +70 -0
- package/sample/crud/apps/main/views/ws/index.ejs +62 -0
- package/sample/crud/apps/main/workers/hash-worker.js +17 -0
- package/sample/crud/apps/main/workers/hash.task.js +22 -0
- package/sample/crud/ecosystem.config.cjs +9 -0
- package/sample/crud/mega.config.js +105 -0
- package/sample/crud/package-lock.json +5665 -0
- package/sample/crud/package.json +28 -0
- package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
- package/sample/crud/test/apps/main/auth-service.test.js +93 -0
- package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
- package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
- package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
- package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
- package/sample/crud/test/apps/main/email-job.test.js +76 -0
- package/sample/crud/test/apps/main/guide-service.test.js +68 -0
- package/sample/crud/test/apps/main/hash-task.test.js +30 -0
- package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
- package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
- package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/note-service.test.js +68 -0
- package/sample/crud/test/apps/main/perf-service.test.js +121 -0
- package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
- package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
- package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
- package/sample/crud/test/apps/main/user-service.test.js +65 -0
- package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
- package/sample/crud/vitest.config.js +8 -0
- package/sample/crud/yarn.lock +2142 -0
- package/sample/simple/.env.example +15 -0
- package/sample/simple/README.md +52 -0
- package/sample/simple/apps/main/app.config.js +35 -0
- package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
- package/sample/simple/apps/main/locales/client/en.json +3 -0
- package/sample/simple/apps/main/locales/client/ko.json +3 -0
- package/sample/simple/apps/main/locales/server/en.json +23 -0
- package/sample/simple/apps/main/locales/server/ko.json +23 -0
- package/sample/simple/apps/main/public/css/app.css +101 -0
- package/sample/simple/apps/main/public/hello.txt +1 -0
- package/sample/simple/apps/main/public/js/app.js +54 -0
- package/sample/simple/apps/main/public/js/theme-init.js +12 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/simple/apps/main/routes/index.js +9 -0
- package/sample/simple/apps/main/routes/pages.js +12 -0
- package/sample/simple/apps/main/views/index.ejs +56 -0
- package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
- package/sample/simple/ecosystem.config.cjs +10 -0
- package/sample/simple/mega.config.js +27 -0
- package/sample/simple/package-lock.json +1851 -0
- package/sample/simple/package.json +25 -0
- package/sample/simple/test/apps/main/index.test.js +13 -0
- package/sample/simple/vitest.config.js +8 -0
- package/src/adapters/adapter-manager.js +305 -0
- package/src/adapters/adapter-options.js +208 -0
- package/src/adapters/file-adapter.js +350 -0
- package/src/adapters/file-session-adapter.js +363 -0
- package/src/adapters/index.js +38 -0
- package/src/adapters/maria-adapter.js +425 -0
- package/src/adapters/mega-adapter.js +511 -0
- package/src/adapters/mega-bus-adapter.js +81 -0
- package/src/adapters/mega-cache-adapter.js +94 -0
- package/src/adapters/mega-db-adapter.js +72 -0
- package/src/adapters/mega-lock-adapter.js +118 -0
- package/src/adapters/mega-log-sink-adapter.js +46 -0
- package/src/adapters/mega-session-adapter.js +72 -0
- package/src/adapters/mongo-adapter.js +396 -0
- package/src/adapters/nats-adapter.js +370 -0
- package/src/adapters/postgres-adapter.js +341 -0
- package/src/adapters/redis-adapter.js +331 -0
- package/src/adapters/redis-session-adapter.js +261 -0
- package/src/adapters/redlock-adapter.js +385 -0
- package/src/adapters/registry.js +157 -0
- package/src/adapters/sqlite-adapter.js +309 -0
- package/src/auth/index.js +103 -0
- package/src/cli/commands/console-cmd.js +56 -0
- package/src/cli/commands/new.js +101 -0
- package/src/cli/commands/routes.js +107 -0
- package/src/cli/commands/scaffold.js +120 -0
- package/src/cli/commands/test-cmd.js +45 -0
- package/src/cli/generators/index.js +368 -0
- package/src/cli/index.js +472 -0
- package/src/cli/template-engine.js +72 -0
- package/src/cli/ws-hub.js +582 -0
- package/src/core/ajv-mapper.js +80 -0
- package/src/core/boot.js +323 -0
- package/src/core/cluster-metrics.js +278 -0
- package/src/core/config-loader.js +115 -0
- package/src/core/config-validator.js +322 -0
- package/src/core/ctx-builder.js +253 -0
- package/src/core/envelope.js +88 -0
- package/src/core/error-mapper.js +116 -0
- package/src/core/formbody.js +69 -0
- package/src/core/hub-link.js +552 -0
- package/src/core/i18n.js +525 -0
- package/src/core/index.js +63 -0
- package/src/core/mega-app.js +1138 -0
- package/src/core/mega-cluster.js +232 -0
- package/src/core/mega-server.js +176 -0
- package/src/core/mega-service.js +41 -0
- package/src/core/migration-runner.js +196 -0
- package/src/core/multipart.js +282 -0
- package/src/core/openapi.js +114 -0
- package/src/core/router.js +388 -0
- package/src/core/routes-loader.js +57 -0
- package/src/core/scope-registry.js +53 -0
- package/src/core/security.js +275 -0
- package/src/core/services-loader.js +98 -0
- package/src/core/session-cleanup-schedule.js +57 -0
- package/src/core/session-store.js +55 -0
- package/src/core/session.js +414 -0
- package/src/core/static-assets.js +126 -0
- package/src/core/template.js +294 -0
- package/src/core/workers-manager.js +193 -0
- package/src/core/ws-compression.js +112 -0
- package/src/core/ws-controller.js +109 -0
- package/src/core/ws-message.js +176 -0
- package/src/core/ws-upgrade.js +445 -0
- package/src/errors/config-error.js +16 -0
- package/src/errors/http-errors.js +130 -0
- package/src/errors/index.js +19 -0
- package/src/errors/mega-error.js +34 -0
- package/src/eslint-plugin/index.js +15 -0
- package/src/eslint-plugin/no-direct-model-import.js +113 -0
- package/src/index.js +131 -0
- package/src/lib/asp/config.js +83 -0
- package/src/lib/asp/crypto.js +145 -0
- package/src/lib/asp/errors.js +49 -0
- package/src/lib/asp/nonce-cache.js +94 -0
- package/src/lib/asp/plugin.js +263 -0
- package/src/lib/asp/ws-terminator.js +101 -0
- package/src/lib/env-mapper.js +222 -0
- package/src/lib/hub-protocol.js +322 -0
- package/src/lib/index.js +42 -0
- package/src/lib/logger/telegram-core.js +150 -0
- package/src/lib/logger/telegram-transport.js +126 -0
- package/src/lib/mega-brute-force.js +225 -0
- package/src/lib/mega-circuit-breaker.js +412 -0
- package/src/lib/mega-cron.js +169 -0
- package/src/lib/mega-hash.js +179 -0
- package/src/lib/mega-health.js +91 -0
- package/src/lib/mega-job-queue.js +600 -0
- package/src/lib/mega-job-worker.js +295 -0
- package/src/lib/mega-job.js +140 -0
- package/src/lib/mega-logger.js +128 -0
- package/src/lib/mega-metrics.js +661 -0
- package/src/lib/mega-plugin.js +650 -0
- package/src/lib/mega-retry.js +95 -0
- package/src/lib/mega-schedule.js +507 -0
- package/src/lib/mega-shutdown.js +176 -0
- package/src/lib/mega-tracing.js +715 -0
- package/src/lib/mega-worker.js +653 -0
- package/src/lib/worker-runner/process-entry.js +30 -0
- package/src/lib/worker-runner/task-dispatch.js +72 -0
- package/src/lib/worker-runner/thread-entry.js +26 -0
- package/src/models/index.js +7 -0
- package/src/models/mega-model.js +151 -0
- package/src/test/index.js +288 -0
- package/templates/adapter/code.tpl +40 -0
- package/templates/adapter/test.tpl +13 -0
- package/templates/app/app.config.tpl +10 -0
- package/templates/app/route.tpl +10 -0
- package/templates/app/test.tpl +13 -0
- package/templates/channel/code.tpl +38 -0
- package/templates/channel/test.tpl +19 -0
- package/templates/controller/code.tpl +16 -0
- package/templates/controller/route.tpl +9 -0
- package/templates/controller/test.tpl +14 -0
- package/templates/job/code.tpl +23 -0
- package/templates/job/test.tpl +17 -0
- package/templates/locale/code.tpl +3 -0
- package/templates/locale/test.tpl +13 -0
- package/templates/middleware/code.tpl +13 -0
- package/templates/middleware/test.tpl +11 -0
- package/templates/migration/code.tpl +20 -0
- package/templates/migration/test.tpl +14 -0
- package/templates/model/code.tpl +21 -0
- package/templates/model/test.tpl +29 -0
- package/templates/project/app.config.tpl +8 -0
- package/templates/project/app.config.views.tpl +37 -0
- package/templates/project/ecosystem.config.tpl +10 -0
- package/templates/project/env.tpl +12 -0
- package/templates/project/gitignore.tpl +8 -0
- package/templates/project/locales/client/en.json.tpl +3 -0
- package/templates/project/locales/client/ko.json.tpl +3 -0
- package/templates/project/locales/server/en.json.tpl +17 -0
- package/templates/project/locales/server/ko.json.tpl +17 -0
- package/templates/project/mega.config.tpl +11 -0
- package/templates/project/package.tpl +25 -0
- package/templates/project/public/css/app.css +101 -0
- package/templates/project/public/js/app.js +54 -0
- package/templates/project/public/js/theme-init.js +12 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/templates/project/readme.tpl +48 -0
- package/templates/project/route.test.tpl +13 -0
- package/templates/project/route.test.views.tpl +15 -0
- package/templates/project/route.tpl +10 -0
- package/templates/project/route.views.tpl +10 -0
- package/templates/project/views/index.ejs.tpl +58 -0
- package/templates/project/views/layout.ejs.tpl +73 -0
- package/templates/project/vitest.config.tpl +8 -0
- package/templates/route/code.tpl +11 -0
- package/templates/route/test.tpl +26 -0
- package/templates/schedule/code.tpl +19 -0
- package/templates/schedule/test.tpl +17 -0
- package/templates/service/code.tpl +18 -0
- package/templates/service/test.tpl +17 -0
- package/templates/worker/code.tpl +14 -0
- package/templates/worker/task.tpl +13 -0
- package/templates/worker/test.tpl +18 -0
- package/vitest.config.js +33 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* MegaMariaAdapter — MariaDB 어댑터 (`mariadb` = node-mariadb 래퍼, ADR-107).
|
|
4
|
+
*
|
|
5
|
+
* Postgres(ADR-106)에 이은 두 번째 SQL 어댑터. 풀 + manual 트랜잭션 + SAVEPOINT nested
|
|
6
|
+
* 구조를 Postgres 와 대칭으로 맞추되, mariadb 드라이버의 표면(트랜잭션 메서드·풀 통계·연결 옵션)에
|
|
7
|
+
* 정합한다.
|
|
8
|
+
*
|
|
9
|
+
* # 표준 표면 (MegaDbAdapter 상속)
|
|
10
|
+
* - `_connect()` — `createPool(cfg)` (driver 는 connect 시점 lazy import) + `SELECT 1` 검증.
|
|
11
|
+
* - `_disconnect()`— `pool.end()` (모든 연결 종료).
|
|
12
|
+
* - `_native()` — `mariadb` Pool 인스턴스(raw handle, ADR-009). `MegaModel.db` 가 노출.
|
|
13
|
+
* - `healthCheck()`— `pool.query('SELECT 1')` 실행으로 실제 응답성 확인.
|
|
14
|
+
* - `getStats()` — 베이스 stats + 풀 통계(total/idle/active/queue).
|
|
15
|
+
* - `withTransaction(fn)` — 명시적 트랜잭션 경계 (ADR-010). 아래 "트랜잭션 패턴" 참조.
|
|
16
|
+
*
|
|
17
|
+
* # 트랜잭션 패턴 — 풀 연결 1개 + `beginTransaction/commit/rollback`
|
|
18
|
+
* `pool.getConnection()` 으로 연결 1개를 잡아 `conn.beginTransaction() → fn(conn) → conn.commit()`
|
|
19
|
+
* (성공) / `conn.rollback()`(실패) 한다. `fn` 에는 **트랜잭션 컨텍스트 연결**(같은 connection)을
|
|
20
|
+
* 넘긴다. `conn.release()`(async)는 finally 에서 반드시 호출해 풀로 반납한다. rollback 자체 실패는
|
|
21
|
+
* 원본 에러를 가리지 않도록 격리 후 원본 re-throw.
|
|
22
|
+
*
|
|
23
|
+
* # nested 트랜잭션 정책 — SAVEPOINT (Sqlite 와 분기, ADR-107)
|
|
24
|
+
* MariaDB 도 SAVEPOINT 를 지원하므로 nested 를 **거부하지 않고**(Sqlite, ADR-105 와 분기) 같은
|
|
25
|
+
* 연결에 `SAVEPOINT sp_<depth>` 를 건다 — 성공 시 `RELEASE SAVEPOINT`, 실패 시 `ROLLBACK TO
|
|
26
|
+
* SAVEPOINT` 후 re-throw. 컨텍스트 추적은 Postgres 어댑터와 **동일하게 `AsyncLocalStorage`** 로
|
|
27
|
+
* 하며, store 의 `depth` 가 SAVEPOINT 이름을 nesting 별로 유일하게 만든다(동시 top-level
|
|
28
|
+
* 트랜잭션은 각자 독립 store → async race-free). 별도 풀 연결을 잡지 않아 `connectionLimit:1`
|
|
29
|
+
* 에서도 nested 가 deadlock 되지 않는다.
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ **"race-free" 의 경계**: race-free 는 **서로 다른 top-level 호출 사이**에만
|
|
32
|
+
* 해당한다. **단일 트랜잭션 내부**에서 sibling nested 를 `Promise.all` 등으로 **병렬** 실행하는 것은
|
|
33
|
+
* **미지원** — 같은 연결(단일 connection) 위 SAVEPOINT 이름이 `depth` 로만 결정되므로 두 sibling 이
|
|
34
|
+
* 동시에 `depth+1` 을 보면 둘 다 `sp_1` 을 만들어 충돌하고, mariadb 단일 connection 도 쿼리를
|
|
35
|
+
* 직렬화하므로 병렬 이득이 없다. nested 는 **순차 호출**(await 로 한 번에 하나)할 것.
|
|
36
|
+
*
|
|
37
|
+
* # 설정 (services.databases.<key>) — 통합 옵션 구조 (ADR-109)
|
|
38
|
+
* ```js
|
|
39
|
+
* services: {
|
|
40
|
+
* databases: {
|
|
41
|
+
* primary: {
|
|
42
|
+
* driver: 'mariadb',
|
|
43
|
+
* // (a) 연결 — url 또는 discrete (상호 배타)
|
|
44
|
+
* url: 'mariadb://user:pw@host:3306/db', // 또는 ↓
|
|
45
|
+
* host: 'localhost', port: 3306, user: 'mega', password: '...', database: 'mega_test',
|
|
46
|
+
* // (b) pool — 공통 풀 인터페이스
|
|
47
|
+
* pool: { min: 0, max: 10, idleTimeoutMs: 1800000, acquireTimeoutMs: 10000 },
|
|
48
|
+
* // (c) options — mariadb 특화 passthrough + bigIntStrategy
|
|
49
|
+
* options: { bigIntStrategy: 'number', charset: 'utf8mb4', multipleStatements: false },
|
|
50
|
+
* },
|
|
51
|
+
* },
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
* `url`(또는 deprecated 별칭 `connectionString`) **XOR** discrete 연결필드. 둘 다 없으면
|
|
55
|
+
* `adapter.connection_required`, 동시 지정은 `adapter.connection_conflict`. pool/options 는 어느
|
|
56
|
+
* 모드와도 조합 가능. pool 매핑: min→minimumIdle, max→connectionLimit, idleTimeoutMs→idleTimeout
|
|
57
|
+
* (**초**, ms÷1000), acquireTimeoutMs→acquireTimeout. `maxLifetimeMs` 는 mariadb 미지원(throw).
|
|
58
|
+
*
|
|
59
|
+
* **url + pool/options 조합**: mariadb `createPool` 은 문자열(URI) XOR 객체만 받으므로, url 을 받으면
|
|
60
|
+
* `new URL` 로 discrete 연결필드(host/port/user/password/database)로 **파싱해 항상 객체**로 구성한다.
|
|
61
|
+
* 따라서 url 의 **query string 파라미터는 미지원** — 있으면 `adapter.invalid_option` throw(옵션은
|
|
62
|
+
* `options` 로 넘길 것). bigIntStrategy 디폴트('number')를 적용하려면 객체 구성이 필수이기도 하다.
|
|
63
|
+
*
|
|
64
|
+
* **bigIntStrategy** (`options.bigIntStrategy`, 디폴트 `'number'`): MariaDB BIGINT 를 JS 로 받는 전략.
|
|
65
|
+
* - `'number'`(디폴트) → `bigIntAsNumber:true`. JS Number 편의성. ⚠️ **2^53(9,007,199,254,740,992)
|
|
66
|
+
* 초과 정수는 정밀도 손실** (IEEE-754 double 한계). 큰 ID/카운터는 'bigint'/'string' 권장.
|
|
67
|
+
* - `'bigint'` → `bigIntAsNumber:false`. 드라이버가 BigInt 반환(정밀도 무손실, 산술 시 BigInt 취급 필요).
|
|
68
|
+
* - `'string'` → `supportBigNumbers:true, bigNumberStrings:true, insertIdAsNumber:false`. 문자열 반환
|
|
69
|
+
* (무손실, 직렬화 친화). mariadb@3 에서 `bigNumberStrings` 단독은 BigInt 라 `supportBigNumbers` 동반 필수(실 서버 검증, ADR-109).
|
|
70
|
+
*
|
|
71
|
+
* 비밀번호/url 은 healthCheck/getStats/에러 details 에 노출하지 않는다(정합).
|
|
72
|
+
*
|
|
73
|
+
* @module adapters/maria-adapter
|
|
74
|
+
*/
|
|
75
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
76
|
+
import { MegaValidationError } from '../errors/http-errors.js'
|
|
77
|
+
import { MegaDbAdapter } from './mega-db-adapter.js'
|
|
78
|
+
import { resolveConnection, normalizePool, assertPlainObject, MARIA_POOL_SPEC } from './adapter-options.js'
|
|
79
|
+
import * as Registry from './registry.js'
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {object} MariaConfig
|
|
83
|
+
* @property {string} [driver] - 'mariadb' (매니저가 사용 — 어댑터는 무시).
|
|
84
|
+
* @property {string} [url] - `mariadb://user:pw@host:port/db` (discrete 필드와 배타, query string 미지원).
|
|
85
|
+
* @property {string} [connectionString] - `url` 의 deprecated 별칭 (하위 호환).
|
|
86
|
+
* @property {string} [host] @property {number} [port] @property {string} [user]
|
|
87
|
+
* @property {string} [password] @property {string} [database]
|
|
88
|
+
* @property {{ min?: number, max?: number, idleTimeoutMs?: number, acquireTimeoutMs?: number }} [pool] - 공통 풀 인터페이스.
|
|
89
|
+
* @property {Record<string, any> & { bigIntStrategy?: 'number' | 'bigint' | 'string' }} [options] - mariadb passthrough + bigIntStrategy.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @typedef {object} MariaTxContext - AsyncLocalStorage 에 담기는 트랜잭션 컨텍스트.
|
|
94
|
+
* @property {import('mariadb').PoolConnection} conn - 이 트랜잭션을 소유한 풀 연결.
|
|
95
|
+
* @property {number} depth - nesting 깊이 (top-level=0, 첫 nested=1, ...).
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* bigIntStrategy → mariadb 드라이버 플래그 (ADR-109, 디폴트 'number'=JS Number 편의성).
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ `'string'` 은 `supportBigNumbers:true` 가 **반드시 함께** 있어야 BIGINT 가 문자열로 온다 —
|
|
102
|
+
* mariadb@3 에서 `bigNumberStrings:true` 단독은 BigInt 를 반환한다(실 서버로 검증, ADR-109).
|
|
103
|
+
*/
|
|
104
|
+
const BIGINT_STRATEGIES = /** @type {const} */ ({
|
|
105
|
+
number: { bigIntAsNumber: true },
|
|
106
|
+
bigint: { bigIntAsNumber: false },
|
|
107
|
+
string: { supportBigNumbers: true, bigNumberStrings: true, insertIdAsNumber: false },
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* mariadb URL 을 discrete 연결필드로 파싱 (createPool 객체 모드용). query string 은 미지원(throw).
|
|
112
|
+
* @param {string} url @returns {Record<string, any>}
|
|
113
|
+
* @throws {MegaValidationError} `adapter.invalid_option` - 잘못된 URL 또는 query string 존재.
|
|
114
|
+
*/
|
|
115
|
+
function parseMariaUrl(url) {
|
|
116
|
+
let u
|
|
117
|
+
try {
|
|
118
|
+
u = new URL(url)
|
|
119
|
+
} catch {
|
|
120
|
+
// url 값은 비밀번호를 포함할 수 있어 메시지·details 어디에도 노출하지 않는다(L-1, ADR-106/107 정합).
|
|
121
|
+
// 디버깅엔 타입만으로 충분(잘못된 타입/형식 식별).
|
|
122
|
+
throw new MegaValidationError('adapter.invalid_option', 'mariadb "url" is not a valid URL', {
|
|
123
|
+
details: { driver: 'mariadb', option: 'url', type: typeof url },
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
if ([...u.searchParams.keys()].length > 0) {
|
|
127
|
+
// createPool 객체 모드로 변환하므로 URI query 파라미터를 옮길 표준 경로가 없다 — 명시 거부.
|
|
128
|
+
throw new MegaValidationError(
|
|
129
|
+
'adapter.invalid_option',
|
|
130
|
+
'mariadb: URL query parameters are not supported with the standardized signature; pass them via the "options" field instead.',
|
|
131
|
+
{ details: { driver: 'mariadb', option: 'url', params: [...u.searchParams.keys()] } },
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
/** @type {Record<string, any>} */
|
|
135
|
+
const out = {}
|
|
136
|
+
if (u.hostname) out.host = u.hostname
|
|
137
|
+
if (u.port) out.port = Number(u.port)
|
|
138
|
+
if (u.username) out.user = decodeURIComponent(u.username)
|
|
139
|
+
if (u.password) out.password = decodeURIComponent(u.password)
|
|
140
|
+
const db = u.pathname.replace(/^\//, '')
|
|
141
|
+
if (db) out.database = db
|
|
142
|
+
return out
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* options 에서 bigIntStrategy 를 뽑아 드라이버 플래그로 변환하고 나머지 passthrough 와 분리한다.
|
|
147
|
+
* 디폴트 'number'. env snake 별칭(`big_int_strategy`)도 인식(env-mapper OPTIONS_ 는 snake_case).
|
|
148
|
+
* @param {Record<string, any> | undefined} options
|
|
149
|
+
* @returns {{ flags: Record<string, boolean>, rest: Record<string, any> }}
|
|
150
|
+
* @throws {MegaValidationError} `adapter.invalid_option` - 알 수 없는 전략.
|
|
151
|
+
*/
|
|
152
|
+
function resolveBigIntStrategy(options) {
|
|
153
|
+
const rest = { ...(options ?? {}) }
|
|
154
|
+
const strategy = rest.bigIntStrategy ?? rest.big_int_strategy ?? 'number'
|
|
155
|
+
delete rest.bigIntStrategy
|
|
156
|
+
delete rest.big_int_strategy
|
|
157
|
+
if (typeof strategy !== 'string' || !Object.prototype.hasOwnProperty.call(BIGINT_STRATEGIES, strategy)) {
|
|
158
|
+
throw new MegaValidationError(
|
|
159
|
+
'adapter.invalid_option',
|
|
160
|
+
`mariadb: options.bigIntStrategy must be one of 'number' | 'bigint' | 'string'. Got ${JSON.stringify(strategy)}.`,
|
|
161
|
+
{ details: { driver: 'mariadb', option: 'options.bigIntStrategy', value: strategy } },
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
return { flags: { ...BIGINT_STRATEGIES[/** @type {keyof typeof BIGINT_STRATEGIES} */ (strategy)] }, rest }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class MegaMariaAdapter extends MegaDbAdapter {
|
|
168
|
+
/** @type {import('mariadb').Pool | null} 연결된 Pool 인스턴스 (connect 후에만). */
|
|
169
|
+
#pool = null
|
|
170
|
+
/** @type {Record<string, unknown>} _connect 의 `createPool()` 인자 (url 도 discrete 로 파싱해 항상 객체, ADR-109). */
|
|
171
|
+
#poolArg
|
|
172
|
+
/**
|
|
173
|
+
* 진행 중 트랜잭션 컨텍스트 추적기 (Postgres 어댑터와 동일 전략). nested 가 같은 연결을 재사용한다.
|
|
174
|
+
* @type {AsyncLocalStorage<MariaTxContext>}
|
|
175
|
+
*/
|
|
176
|
+
#txContext = new AsyncLocalStorage()
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {MariaConfig} [config] - services.databases.<key> 설정.
|
|
180
|
+
* @throws {MegaValidationError} `adapter.connection_required` - url·discrete 둘 다 없음.
|
|
181
|
+
* @throws {MegaValidationError} `adapter.connection_conflict` - url + discrete 동시 지정.
|
|
182
|
+
* @throws {MegaValidationError} `adapter.invalid_option` - 옵션 타입/범위 오류(bigIntStrategy 포함).
|
|
183
|
+
*/
|
|
184
|
+
constructor(config = /** @type {any} */ ({})) {
|
|
185
|
+
super(config)
|
|
186
|
+
|
|
187
|
+
// 연결 모드(url XOR discrete) 결정 + 충돌·필수 검증 (ADR-109 공용 헬퍼).
|
|
188
|
+
const conn = resolveConnection(config, { driver: 'mariadb', dbKey: 'database', dbConflictsWithUrl: true })
|
|
189
|
+
const poolOpts = normalizePool(config.pool, MARIA_POOL_SPEC, 'mariadb')
|
|
190
|
+
assertPlainObject('options', config.options, { driver: 'mariadb' })
|
|
191
|
+
|
|
192
|
+
// mariadb createPool 은 string XOR 객체 — url+pool/options 조합 + bigIntStrategy 디폴트 적용을 위해
|
|
193
|
+
// 항상 객체로 구성한다(url 은 discrete 로 파싱).
|
|
194
|
+
/** @type {Record<string, any>} */
|
|
195
|
+
const poolConfig = {}
|
|
196
|
+
if (conn.url !== undefined) Object.assign(poolConfig, parseMariaUrl(conn.url))
|
|
197
|
+
else {
|
|
198
|
+
if (conn.host !== undefined) poolConfig.host = conn.host
|
|
199
|
+
if (conn.port !== undefined) poolConfig.port = conn.port
|
|
200
|
+
if (conn.user !== undefined) poolConfig.user = conn.user
|
|
201
|
+
if (conn.password !== undefined) poolConfig.password = conn.password
|
|
202
|
+
if (conn.database !== undefined) poolConfig.database = conn.database
|
|
203
|
+
}
|
|
204
|
+
// bigIntStrategy 분리 → 드라이버 플래그 + 나머지 options passthrough.
|
|
205
|
+
const { flags, rest } = resolveBigIntStrategy(config.options)
|
|
206
|
+
Object.assign(poolConfig, rest, flags)
|
|
207
|
+
// pool(정규화) 나중 — 겹치면 명시 pool 인터페이스가 이긴다.
|
|
208
|
+
Object.assign(poolConfig, poolOpts)
|
|
209
|
+
this.#poolArg = poolConfig
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* `mariadb` Pool 생성 + `SELECT 1` 로 실제 연결 검증.
|
|
214
|
+
* driver 는 **connect 시점에 lazy import** — Sqlite/Postgres 패턴 정합(미사용 환경 비강제).
|
|
215
|
+
*
|
|
216
|
+
* @protected
|
|
217
|
+
* @returns {Promise<void>}
|
|
218
|
+
*/
|
|
219
|
+
async _connect() {
|
|
220
|
+
const mariadb = await import('mariadb')
|
|
221
|
+
// mariadb 는 named export(`createPool`) — default 는 비어 있을 수 있어 방어적으로 둘 다 본다.
|
|
222
|
+
const createPool = mariadb.createPool ?? /** @type {any} */ (mariadb).default?.createPool
|
|
223
|
+
const pool = createPool(/** @type {any} */ (this.#poolArg))
|
|
224
|
+
// connect 직후 실제 연결 1개를 잡아 SELECT 1 — 잘못된 자격증명/호스트를 부팅 시점에 잡는다(fail-fast).
|
|
225
|
+
try {
|
|
226
|
+
const conn = await pool.getConnection()
|
|
227
|
+
try {
|
|
228
|
+
await conn.query('SELECT 1')
|
|
229
|
+
} finally {
|
|
230
|
+
await conn.release()
|
|
231
|
+
}
|
|
232
|
+
} catch (err) {
|
|
233
|
+
try {
|
|
234
|
+
await pool.end()
|
|
235
|
+
} catch (endErr) {
|
|
236
|
+
// 검증 실패 후 정리(pool.end) 실패는 비치명적 — 원본 연결 에러가 진짜 원인.
|
|
237
|
+
console.warn('[MegaMariaAdapter] pool.end() failed after connect validation error (original error wins):', endErr)
|
|
238
|
+
}
|
|
239
|
+
throw err
|
|
240
|
+
}
|
|
241
|
+
this.#pool = pool
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Pool 종료(모든 연결 종료). 베이스 상태 머신이 connected 상태에서만 호출 보장.
|
|
246
|
+
* @protected
|
|
247
|
+
* @returns {Promise<void>}
|
|
248
|
+
*/
|
|
249
|
+
async _disconnect() {
|
|
250
|
+
if (this.#pool !== null) {
|
|
251
|
+
const pool = this.#pool
|
|
252
|
+
this.#pool = null
|
|
253
|
+
await pool.end()
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* raw Pool handle (ADR-009). `connect()` 후에만 베이스 `native` getter 가 호출한다.
|
|
259
|
+
* @protected
|
|
260
|
+
* @returns {import('mariadb').Pool}
|
|
261
|
+
*/
|
|
262
|
+
_native() {
|
|
263
|
+
if (this.#pool === null) {
|
|
264
|
+
// 베이스 native getter 가 state 검증을 먼저 하므로 정상 경로에선 도달 안 함 — 방어.
|
|
265
|
+
return this._notImplemented('native')
|
|
266
|
+
}
|
|
267
|
+
return this.#pool
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 헬스 체크 — 실제 `SELECT 1` 으로 응답성 확인. 실패는 throw 없이 `ok:false` + 사유(베이스 계약).
|
|
272
|
+
* 비밀번호/connectionString 은 노출하지 않는다.
|
|
273
|
+
*
|
|
274
|
+
* @returns {Promise<{ ok: boolean, driver: 'mariadb', state: string, error?: string }>}
|
|
275
|
+
*/
|
|
276
|
+
async healthCheck() {
|
|
277
|
+
if (this.state !== 'connected' || this.#pool === null) {
|
|
278
|
+
return { ok: false, driver: 'mariadb', state: this.state }
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const rows = await this.#pool.query('SELECT 1 AS ok')
|
|
282
|
+
// mariadb SELECT 1 은 BIGINT 라 드라이버 설정에 따라 BigInt 일 수 있어 Number 로 정규화.
|
|
283
|
+
const ok = Number(rows?.[0]?.ok) === 1
|
|
284
|
+
return { ok, driver: 'mariadb', state: this.state }
|
|
285
|
+
} catch (err) {
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
driver: 'mariadb',
|
|
289
|
+
state: this.state,
|
|
290
|
+
error: err instanceof Error ? err.message : String(err),
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 누적 통계 + 풀 통계(total/idle/active/queue). 연결 전이면 풀 통계는 0.
|
|
297
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, pool: { total: number, idle: number, active: number, queue: number } }}
|
|
298
|
+
*/
|
|
299
|
+
getStats() {
|
|
300
|
+
const pool = this.#pool
|
|
301
|
+
return {
|
|
302
|
+
...super.getStats(),
|
|
303
|
+
driver: 'mariadb',
|
|
304
|
+
pool: {
|
|
305
|
+
total: pool?.totalConnections() ?? 0,
|
|
306
|
+
idle: pool?.idleConnections() ?? 0,
|
|
307
|
+
active: pool?.activeConnections() ?? 0,
|
|
308
|
+
queue: pool?.taskQueueSize() ?? 0,
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 명시적 트랜잭션 경계 (ADR-010, ADR-107). top-level 은 풀 연결 1개 위
|
|
315
|
+
* `beginTransaction/commit/rollback`, nested 는 같은 연결 위 `SAVEPOINT`(모듈 docstring 참조).
|
|
316
|
+
*
|
|
317
|
+
* `fn` 은 **트랜잭션 컨텍스트 연결**(mariadb PoolConnection)을 인자로 받는다. 성공 시 commit(또는
|
|
318
|
+
* RELEASE SAVEPOINT) 후 반환값을 그대로 돌려주고, throw 시 rollback(또는 ROLLBACK TO SAVEPOINT)
|
|
319
|
+
* 후 원본 에러 re-throw.
|
|
320
|
+
*
|
|
321
|
+
* hook(`onCallStart/onCallEnd`) + 상태 검증 + stats 누적은 `_instrument` 가 처리한다(ADR-077).
|
|
322
|
+
*
|
|
323
|
+
* @template T
|
|
324
|
+
* @param {(conn: import('mariadb').PoolConnection) => Promise<T> | T} fn
|
|
325
|
+
* @returns {Promise<T>}
|
|
326
|
+
*/
|
|
327
|
+
async withTransaction(fn) {
|
|
328
|
+
return this._instrument('withTransaction', { table: undefined }, async () => {
|
|
329
|
+
const existing = this.#txContext.getStore()
|
|
330
|
+
// nested — 진행 중 트랜잭션이 있으면 같은 연결에 SAVEPOINT 를 건다(Sqlite 와 분기).
|
|
331
|
+
if (existing !== undefined) {
|
|
332
|
+
return this.#runSavepoint(existing, fn)
|
|
333
|
+
}
|
|
334
|
+
// top-level — 풀에서 연결 1개 획득 후 beginTransaction/commit/rollback.
|
|
335
|
+
const pool = /** @type {import('mariadb').Pool} */ (this.#pool)
|
|
336
|
+
const conn = await pool.getConnection()
|
|
337
|
+
try {
|
|
338
|
+
await conn.beginTransaction()
|
|
339
|
+
let result
|
|
340
|
+
try {
|
|
341
|
+
result = await this.#txContext.run({ conn, depth: 0 }, () => fn(conn))
|
|
342
|
+
} catch (err) {
|
|
343
|
+
await this.#rollback(conn)
|
|
344
|
+
throw err
|
|
345
|
+
}
|
|
346
|
+
await conn.commit()
|
|
347
|
+
return result
|
|
348
|
+
} finally {
|
|
349
|
+
// 성공/실패 무관 — 반드시 풀로 반납(누수 방지). mariadb release 는 async.
|
|
350
|
+
await conn.release()
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 계측된 SQL 쿼리 (ADR-138). 진행 중 트랜잭션이 있으면 그 컨텍스트의 연결을, 없으면 풀을 통해
|
|
357
|
+
* 실행한다 — 트랜잭션 안에서 호출해도 같은 connection 을 써 격리가 유지된다. `_instrument('query', …)`
|
|
358
|
+
* 가 자동 span(`mariadb.query`, `db.system.name`/`db.query.text`/`mega.rows_affected`)·stats·상태 검증을
|
|
359
|
+
* 처리한다. 결과는 mariadb native 를 그대로 돌려준다 — SELECT 는 row 배열, DML 은 `{ affectedRows, … }`(ADR-009).
|
|
360
|
+
*
|
|
361
|
+
* @param {string} sql - 파라미터화된 SQL(placeholder `?` 보존).
|
|
362
|
+
* @param {any[]} [params] - placeholder 바인딩 값.
|
|
363
|
+
* @returns {Promise<any>}
|
|
364
|
+
*/
|
|
365
|
+
async query(sql, params) {
|
|
366
|
+
return this._instrument(
|
|
367
|
+
'query',
|
|
368
|
+
this._queryStartAttrs(sql),
|
|
369
|
+
async () => {
|
|
370
|
+
const ctx = this.#txContext.getStore()
|
|
371
|
+
const runner = ctx !== undefined ? ctx.conn : /** @type {import('mariadb').Pool} */ (this.#pool)
|
|
372
|
+
return runner.query(sql, params)
|
|
373
|
+
},
|
|
374
|
+
// SELECT 는 row 배열 길이, DML 은 affectedRows. BigInt 설정 대비 Number 강제.
|
|
375
|
+
(res) => ({ 'mega.rows_affected': Array.isArray(res) ? res.length : Number(res?.affectedRows ?? 0) }),
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* nested 트랜잭션 — 진행 중 컨텍스트의 연결에 SAVEPOINT 를 걸어 부분 롤백을 지원한다.
|
|
381
|
+
* `depth` 는 store 에서 단조 증가하므로 SAVEPOINT 이름이 nesting 별로 유일하고, 정수라 SQL
|
|
382
|
+
* 인젝션 위험이 없다.
|
|
383
|
+
*
|
|
384
|
+
* @template T
|
|
385
|
+
* @param {MariaTxContext} ctx - 진행 중 트랜잭션 컨텍스트(부모).
|
|
386
|
+
* @param {(conn: import('mariadb').PoolConnection) => Promise<T> | T} fn
|
|
387
|
+
* @returns {Promise<T>}
|
|
388
|
+
*/
|
|
389
|
+
async #runSavepoint(ctx, fn) {
|
|
390
|
+
const depth = ctx.depth + 1
|
|
391
|
+
const name = `sp_${depth}`
|
|
392
|
+
const conn = ctx.conn
|
|
393
|
+
await conn.query(`SAVEPOINT ${name}`)
|
|
394
|
+
let result
|
|
395
|
+
try {
|
|
396
|
+
result = await this.#txContext.run({ conn, depth }, () => fn(conn))
|
|
397
|
+
} catch (err) {
|
|
398
|
+
// 부분 롤백 — SAVEPOINT 이후 변경만 되돌리고 바깥 트랜잭션은 유지. ROLLBACK TO 실패는 격리.
|
|
399
|
+
try {
|
|
400
|
+
await conn.query(`ROLLBACK TO SAVEPOINT ${name}`)
|
|
401
|
+
} catch (spErr) {
|
|
402
|
+
console.warn(`[MegaMariaAdapter] ROLLBACK TO SAVEPOINT ${name} failed (original error wins):`, spErr)
|
|
403
|
+
}
|
|
404
|
+
throw err
|
|
405
|
+
}
|
|
406
|
+
await conn.query(`RELEASE SAVEPOINT ${name}`)
|
|
407
|
+
return result
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* top-level 트랜잭션 rollback — 실패해도 원본 에러를 가리지 않도록 격리 후 무시.
|
|
412
|
+
* @param {import('mariadb').PoolConnection} conn @returns {Promise<void>}
|
|
413
|
+
*/
|
|
414
|
+
async #rollback(conn) {
|
|
415
|
+
try {
|
|
416
|
+
await conn.rollback()
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.warn('[MegaMariaAdapter] rollback failed (original error wins):', err)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 빌트인 driver 자기등록 (ADR-044) — 배럴(`adapters/index.js`)이 본 모듈을 import 하면 트리거된다.
|
|
424
|
+
// mariadb 는 _connect() 의 lazy import 까지 로드되지 않으므로 등록은 안전(미사용 환경 비강제).
|
|
425
|
+
Registry.register('mariadb', MegaMariaAdapter)
|