mega-framework 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/bin/mega-ws-hub.js +2 -2
- package/package.json +33 -9
- package/sample/crud/.env +10 -1
- package/sample/crud/.env.example +10 -1
- package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
- package/sample/crud/.mega/journal/snapshot.json +261 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
- package/sample/crud/apps/main/controllers/web-controller.js +7 -5
- package/sample/crud/apps/main/locales/server/en.json +12 -1
- package/sample/crud/apps/main/locales/server/ko.json +12 -1
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
- package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
- package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
- package/sample/crud/apps/main/models/log-partition-model.js +105 -0
- package/sample/crud/apps/main/models/note-model.js +79 -0
- package/sample/crud/apps/main/models/user-level-model.js +24 -0
- package/sample/crud/apps/main/models/user-model.js +146 -0
- package/sample/crud/apps/main/models/user-type-model.js +21 -0
- package/sample/crud/apps/main/models/wallet-model.js +24 -0
- package/sample/crud/apps/main/routes/users.js +55 -10
- package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
- package/sample/crud/apps/main/services/auth-service.js +39 -24
- package/sample/crud/apps/main/services/log-partition-service.js +101 -0
- package/sample/crud/apps/main/services/note-service.js +6 -6
- package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
- package/sample/crud/apps/main/services/user-service.js +62 -21
- package/sample/crud/apps/main/views/auth/login.ejs +6 -6
- package/sample/crud/apps/main/views/auth/register.ejs +46 -5
- package/sample/crud/apps/main/views/users/edit.ejs +42 -5
- package/sample/crud/apps/main/views/users/list.ejs +6 -2
- package/sample/crud/apps/main/views/users/new.ejs +56 -4
- package/sample/crud/docs/log_partition_design.mm.md +23 -0
- package/sample/crud/mega.config.js +10 -2
- package/sample/crud/package.json +3 -3
- package/sample/crud/scripts/start-ws-hub.sh +20 -6
- package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +44 -3
- package/src/adapters/file-adapter.js +9 -5
- package/src/adapters/file-session-adapter.js +4 -3
- package/src/adapters/maria-adapter.js +33 -7
- package/src/adapters/mega-cache-adapter.js +83 -6
- package/src/adapters/mega-db-adapter.js +10 -1
- package/src/adapters/mongo-adapter.js +40 -8
- package/src/adapters/postgres-adapter.js +33 -6
- package/src/adapters/redis-adapter.js +7 -3
- package/src/adapters/sqlite-adapter.js +26 -3
- package/src/cli/commands/console-cmd.js +3 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +173 -33
- package/src/cli/generators/index.js +140 -3
- package/src/cli/index.js +437 -155
- package/src/cli/watch.js +188 -0
- package/src/core/ajv-mapper.js +30 -3
- package/src/core/boot.js +464 -245
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/ctx-builder.js +65 -3
- package/src/core/envelope.js +119 -12
- package/src/core/hub-link.js +89 -18
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +7 -3
- package/src/core/mega-app.js +253 -505
- package/src/core/mega-cluster.js +4 -1
- package/src/core/mega-server.js +40 -9
- package/src/core/migration/dialect-registry.js +107 -0
- package/src/core/migration/dialects/README.md +62 -0
- package/src/core/migration/dialects/maria.js +496 -0
- package/src/core/migration/dialects/mongo.js +824 -0
- package/src/core/migration/dialects/postgres.js +563 -0
- package/src/core/migration/dialects/sqlite.js +476 -0
- package/src/core/migration/differ.js +456 -0
- package/src/core/migration/generate.js +508 -0
- package/src/core/migration/journal.js +167 -0
- package/src/core/migration/model-scan.js +84 -0
- package/src/core/migration/mongo-migration-db.js +97 -0
- package/src/core/migration/schema-builder.js +400 -0
- package/src/core/migration/schema-validator.js +315 -0
- package/src/core/migration-lock.js +205 -0
- package/src/core/migration-runner.js +166 -38
- package/src/core/multipart.js +28 -5
- package/src/core/pipeline.js +131 -0
- package/src/core/router.js +70 -65
- package/src/core/scope-registry.js +1 -0
- package/src/core/security.js +70 -12
- package/src/core/session-store.js +14 -1
- package/src/core/workers-manager.js +12 -1
- package/src/core/ws-cluster.js +10 -3
- package/src/core/ws-message.js +48 -4
- package/src/core/ws-presence.js +636 -0
- package/src/core/ws-roster.js +50 -8
- package/src/core/ws-upgrade.js +223 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-circuit-breaker.js +5 -3
- package/src/lib/mega-health.js +35 -4
- package/src/lib/mega-job-queue.js +151 -34
- package/src/lib/mega-job.js +37 -1
- package/src/lib/mega-metrics.js +31 -13
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +114 -39
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +33 -6
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +139 -15
- package/src/models/crud-sql-builder.js +133 -0
- package/src/models/mega-model.js +82 -2
- package/src/models/model-crud.js +483 -0
- package/src/models/mongo-crud.js +285 -0
- package/templates/adr/code.tpl +23 -0
- package/templates/model/code-mongo.tpl +35 -0
- package/templates/model/code.tpl +15 -1
- package/templates/model/test-mongo.tpl +38 -0
- package/templates/model/test.tpl +4 -0
- package/types/adapters/adapter-manager.d.ts +95 -0
- package/types/adapters/adapter-options.d.ts +93 -0
- package/types/adapters/file-adapter.d.ts +105 -0
- package/types/adapters/file-session-adapter.d.ts +103 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +117 -0
- package/types/adapters/mega-adapter.d.ts +215 -0
- package/types/adapters/mega-bus-adapter.d.ts +45 -0
- package/types/adapters/mega-cache-adapter.d.ts +73 -0
- package/types/adapters/mega-db-adapter.d.ts +50 -0
- package/types/adapters/mega-lock-adapter.d.ts +62 -0
- package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
- package/types/adapters/mega-session-adapter.d.ts +32 -0
- package/types/adapters/mongo-adapter.d.ts +150 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +141 -0
- package/types/adapters/redis-adapter.d.ts +78 -0
- package/types/adapters/redis-session-adapter.d.ts +82 -0
- package/types/adapters/redlock-adapter.d.ts +149 -0
- package/types/adapters/registry.d.ts +46 -0
- package/types/adapters/sqlite-adapter.d.ts +112 -0
- package/types/auth/index.d.ts +24 -0
- package/types/cli/commands/console-cmd.d.ts +37 -0
- package/types/cli/commands/new.d.ts +16 -0
- package/types/cli/commands/routes.d.ts +36 -0
- package/types/cli/commands/scaffold.d.ts +78 -0
- package/types/cli/commands/test-cmd.d.ts +14 -0
- package/types/cli/generators/index.d.ts +122 -0
- package/types/cli/index.d.ts +234 -0
- package/types/cli/template-engine.d.ts +40 -0
- package/types/cli/watch.d.ts +59 -0
- package/types/core/ajv-mapper.d.ts +27 -0
- package/types/core/boot.d.ts +233 -0
- package/types/core/cluster-metrics.d.ts +52 -0
- package/types/core/config-loader.d.ts +13 -0
- package/types/core/config-validator.d.ts +30 -0
- package/types/core/ctx-builder.d.ts +103 -0
- package/types/core/envelope.d.ts +79 -0
- package/types/core/error-mapper.d.ts +17 -0
- package/types/core/formbody.d.ts +41 -0
- package/types/core/hub-link.d.ts +266 -0
- package/types/core/i18n.d.ts +178 -0
- package/types/core/index.d.ts +28 -0
- package/types/core/mega-app.d.ts +529 -0
- package/types/core/mega-cluster.d.ts +104 -0
- package/types/core/mega-server.d.ts +91 -0
- package/types/core/mega-service.d.ts +31 -0
- package/types/core/migration/dialect-registry.d.ts +22 -0
- package/types/core/migration/dialects/maria.d.ts +99 -0
- package/types/core/migration/dialects/mongo.d.ts +89 -0
- package/types/core/migration/dialects/postgres.d.ts +117 -0
- package/types/core/migration/dialects/sqlite.d.ts +111 -0
- package/types/core/migration/differ.d.ts +47 -0
- package/types/core/migration/generate.d.ts +56 -0
- package/types/core/migration/journal.d.ts +52 -0
- package/types/core/migration/model-scan.d.ts +19 -0
- package/types/core/migration/mongo-migration-db.d.ts +7 -0
- package/types/core/migration/schema-builder.d.ts +197 -0
- package/types/core/migration/schema-validator.d.ts +20 -0
- package/types/core/migration-lock.d.ts +33 -0
- package/types/core/migration-runner.d.ts +101 -0
- package/types/core/multipart.d.ts +86 -0
- package/types/core/openapi.d.ts +62 -0
- package/types/core/pipeline.d.ts +93 -0
- package/types/core/router.d.ts +159 -0
- package/types/core/routes-loader.d.ts +21 -0
- package/types/core/scope-registry.d.ts +14 -0
- package/types/core/security.d.ts +77 -0
- package/types/core/services-loader.d.ts +27 -0
- package/types/core/session-cleanup-schedule.d.ts +19 -0
- package/types/core/session-store.d.ts +25 -0
- package/types/core/session.d.ts +77 -0
- package/types/core/static-assets.d.ts +73 -0
- package/types/core/template.d.ts +106 -0
- package/types/core/workers-manager.d.ts +79 -0
- package/types/core/ws-cluster.d.ts +208 -0
- package/types/core/ws-compression.d.ts +112 -0
- package/types/core/ws-controller.d.ts +65 -0
- package/types/core/ws-message.d.ts +106 -0
- package/types/core/ws-presence.d.ts +273 -0
- package/types/core/ws-roster.d.ts +108 -0
- package/types/core/ws-upgrade.d.ts +260 -0
- package/types/errors/config-error.d.ts +10 -0
- package/types/errors/http-errors.d.ts +120 -0
- package/types/errors/index.d.ts +3 -0
- package/types/errors/mega-error.d.ts +32 -0
- package/types/index.d.ts +39 -0
- package/types/lib/asp/config.d.ts +49 -0
- package/types/lib/asp/crypto.d.ts +43 -0
- package/types/lib/asp/errors.d.ts +30 -0
- package/types/lib/asp/nonce-cache.d.ts +52 -0
- package/types/lib/asp/plugin.d.ts +30 -0
- package/types/lib/asp/ws-terminator.d.ts +45 -0
- package/types/lib/env-mapper.d.ts +14 -0
- package/types/lib/hub-protocol.d.ts +106 -0
- package/types/lib/index.d.ts +22 -0
- package/types/lib/logger/telegram-core.d.ts +104 -0
- package/types/lib/logger/telegram-transport.d.ts +45 -0
- package/types/lib/mega-brute-force.d.ts +66 -0
- package/types/lib/mega-circuit-breaker.d.ts +243 -0
- package/types/lib/mega-cron.d.ts +66 -0
- package/types/lib/mega-hash.d.ts +32 -0
- package/types/lib/mega-health.d.ts +48 -0
- package/types/lib/mega-job-queue.d.ts +188 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +145 -0
- package/types/lib/mega-logger.d.ts +45 -0
- package/types/lib/mega-metrics.d.ts +285 -0
- package/types/lib/mega-plugin.d.ts +245 -0
- package/types/lib/mega-retry.d.ts +85 -0
- package/types/lib/mega-schedule.d.ts +260 -0
- package/types/lib/mega-shutdown.d.ts +135 -0
- package/types/lib/mega-tracing.d.ts +224 -0
- package/types/lib/mega-worker.d.ts +129 -0
- package/types/lib/otel-resource.d.ts +16 -0
- package/types/lib/worker-runner/process-entry.d.ts +1 -0
- package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
- package/types/lib/worker-runner/thread-entry.d.ts +1 -0
- package/types/lib/ws-hub.d.ts +259 -0
- package/types/models/crud-sql-builder.d.ts +48 -0
- package/types/models/index.d.ts +1 -0
- package/types/models/mega-model.d.ts +138 -0
- package/types/models/model-crud.d.ts +82 -0
- package/types/models/mongo-crud.d.ts +59 -0
- package/types/test/index.d.ts +84 -0
- package/.env +0 -127
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
- package/sample/crud/apps/main/models/note.js +0 -71
- package/sample/crud/apps/main/models/user.js +0 -86
- package/sample/crud/package-lock.json +0 -5665
- package/sample/crud/yarn.lock +0 -2142
- package/sample/simple/package-lock.json +0 -1851
package/src/core/mega-cluster.js
CHANGED
|
@@ -32,7 +32,10 @@ export class MegaCluster {
|
|
|
32
32
|
* @param {Object} [opts]
|
|
33
33
|
* @param {number | 'max'} [opts.instances] - 워커 수. 'max' 면 CPU 코어 수.
|
|
34
34
|
* @param {boolean} [opts.respawn=true] - 워커 crash 시 자동 재시작
|
|
35
|
-
* @param {number} [opts.gracePeriodMs=30000] - SIGTERM 후 강제 kill 까지
|
|
35
|
+
* @param {number} [opts.gracePeriodMs=30000] - SIGTERM 후 강제 kill 까지 대기. ⚠️ 워커가
|
|
36
|
+
* `MegaShutdown.setupSignals()` 를 쓰면 워커 종료 예산 상한은 hardKill(기본 60s)이다 — 마스터 grace 가
|
|
37
|
+
* 그보다 작으면 워커 drain 도중 SIGKILL 되는 예산 역전이 생긴다. `mega start` CLI 는
|
|
38
|
+
* `DEFAULT_HARD_KILL_MS + 5s` 로 위계화해 전달한다(직접 생성 시에도 동일 권장).
|
|
36
39
|
* @param {import('node:cluster').Cluster} [opts._cluster] - 테스트용 cluster 주입(기본 node:cluster). per ADR-165
|
|
37
40
|
* @param {NodeJS.Process} [opts._proc] - 테스트용 process 주입(기본 전역 process). per ADR-165
|
|
38
41
|
* @param {{ info?: Function, warn?: Function, error?: Function } | null} [opts.logger] - 조율 로그용 pino 로거.
|
package/src/core/mega-server.js
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
import { createServer as createHttpServer } from 'node:http'
|
|
3
3
|
import { MegaConfigError } from '../errors/config-error.js'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Host 헤더에서 hostname 만 추출한다 (소문자 정규화). IPv6 리터럴(`[::1]:3000`)은 hostname 자체가
|
|
7
|
+
* 콜론을 포함하므로 단순 `split(':')` 으로는 `'['` 가 나와 vhost 매칭이 전부 깨진다 — 대괄호 형식이면
|
|
8
|
+
* 괄호 안(`::1`)을, 아니면 첫 콜론 앞(포트 제거)을 취한다.
|
|
9
|
+
* @param {unknown} hostHeader - `req.headers.host` 원본.
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
export function hostnameOf(hostHeader) {
|
|
13
|
+
const h = String(hostHeader || '')
|
|
14
|
+
if (h.startsWith('[')) {
|
|
15
|
+
const end = h.indexOf(']')
|
|
16
|
+
return (end === -1 ? h.slice(1) : h.slice(1, end)).toLowerCase()
|
|
17
|
+
}
|
|
18
|
+
return h.split(':')[0].toLowerCase()
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
/**
|
|
6
22
|
* MegaServer — 여러 MegaApp 을 하나의 HTTP 서버에서 호스트네임 기반으로 라우팅.
|
|
7
23
|
*
|
|
@@ -17,7 +33,9 @@ import { MegaConfigError } from '../errors/config-error.js'
|
|
|
17
33
|
*/
|
|
18
34
|
export class MegaServer {
|
|
19
35
|
/**
|
|
20
|
-
* @param {{ port?: number, host?: string }} [opts] -
|
|
36
|
+
* @param {{ port?: number, host?: string, logger?: { debug?: Function, warn?: Function } }} [opts] -
|
|
37
|
+
* listen 기본값 (listen() 인자로 override 가능). `logger` 는 pino 호환 로거(선택) — 미매핑 host
|
|
38
|
+
* 404·upgrade 소켓 에러 같은 비치명 이벤트를 구조적으로 남긴다(미주입 시 console 폴백).
|
|
21
39
|
*/
|
|
22
40
|
constructor(opts = {}) {
|
|
23
41
|
/** @type {Map<string, import('./mega-app.js').MegaApp>} */
|
|
@@ -26,6 +44,8 @@ export class MegaServer {
|
|
|
26
44
|
this._apps = []
|
|
27
45
|
/** @type {import('node:http').Server | null} */
|
|
28
46
|
this._httpServer = null
|
|
47
|
+
/** @type {{ debug?: Function, warn?: Function } | null} */
|
|
48
|
+
this._log = opts.logger ?? null
|
|
29
49
|
this._opts = opts
|
|
30
50
|
}
|
|
31
51
|
|
|
@@ -73,6 +93,10 @@ export class MegaServer {
|
|
|
73
93
|
if (this._apps.length === 0) {
|
|
74
94
|
throw new Error('MegaServer.listen: no MegaApps mounted')
|
|
75
95
|
}
|
|
96
|
+
// 재호출 가드 — 호출마다 새 http.Server 를 만들면 기존 서버(리스너·소켓)가 참조만 잃고 누수된다.
|
|
97
|
+
if (this._httpServer) {
|
|
98
|
+
throw new Error('MegaServer.listen: already listening — call close() first.')
|
|
99
|
+
}
|
|
76
100
|
|
|
77
101
|
// 모든 Fastify 인스턴스 ready (라우트 등록 완료 보장)
|
|
78
102
|
for (const app of this._apps) {
|
|
@@ -80,10 +104,12 @@ export class MegaServer {
|
|
|
80
104
|
}
|
|
81
105
|
|
|
82
106
|
this._httpServer = createHttpServer((req, res) => {
|
|
83
|
-
const
|
|
84
|
-
const hostname = hostHeader.split(':')[0].toLowerCase()
|
|
107
|
+
const hostname = hostnameOf(req.headers.host)
|
|
85
108
|
const app = this._hostMap.get(hostname)
|
|
86
109
|
if (!app) {
|
|
110
|
+
// 등록 host 목록은 응답에 싣지 않는다 — 임의 클라이언트에게 내부 도메인 구성이 노출된다.
|
|
111
|
+
// 운영자 디버그용 목록은 debug 로그로만 남긴다.
|
|
112
|
+
this._log?.debug?.({ hostname, knownHosts: [...this._hostMap.keys()] }, 'host.not_mounted (404)')
|
|
87
113
|
res.statusCode = 404
|
|
88
114
|
res.setHeader('content-type', 'application/json')
|
|
89
115
|
res.end(
|
|
@@ -91,7 +117,7 @@ export class MegaServer {
|
|
|
91
117
|
ok: false,
|
|
92
118
|
error: {
|
|
93
119
|
code: 'host.not_mounted',
|
|
94
|
-
message: `No app mounted for host '${hostname}'
|
|
120
|
+
message: `No app mounted for host '${hostname}'.`,
|
|
95
121
|
},
|
|
96
122
|
}),
|
|
97
123
|
)
|
|
@@ -106,16 +132,16 @@ export class MegaServer {
|
|
|
106
132
|
// WS HTTP Upgrade 핸들오프 — 호스트 분기 후 해당 MegaApp 에 위임.
|
|
107
133
|
// Fastify 는 (websocket 플러그인 없으면) 'upgrade' 를 듣지 않으므로 여기서 직접 배선한다.
|
|
108
134
|
this._httpServer.on('upgrade', (req, socket, head) => {
|
|
109
|
-
const
|
|
110
|
-
const hostname = hostHeader.split(':')[0].toLowerCase()
|
|
135
|
+
const hostname = hostnameOf(req.headers.host)
|
|
111
136
|
const app = this._hostMap.get(hostname)
|
|
112
137
|
if (!app) {
|
|
113
138
|
// 매핑 안 된 host 의 upgrade 는 거부 (소켓 파괴). HTTP 404 와 동일 정책.
|
|
114
139
|
// L4: destroy 전 error 가드. listener 없는 raw 소켓이 ECONNRESET 등으로 uncaught
|
|
115
|
-
// exception → 프로세스 크래시 하는 것을 막는다 (H1 과 동일 패턴).
|
|
116
|
-
//
|
|
140
|
+
// exception → 프로세스 크래시 하는 것을 막는다 (H1 과 동일 패턴). 비치명적 소켓 에러는
|
|
141
|
+
// 주입 로거(warn)로, 미주입이면 console.warn 폴백으로 명시 로그.
|
|
117
142
|
socket.on('error', (err) => {
|
|
118
|
-
|
|
143
|
+
if (this._log?.warn) this._log.warn({ err, hostname }, 'raw socket error on unmapped-host upgrade')
|
|
144
|
+
else console.warn('[MegaServer] raw socket error on unmapped-host upgrade:', err?.message ?? err)
|
|
119
145
|
})
|
|
120
146
|
socket.destroy()
|
|
121
147
|
return
|
|
@@ -134,6 +160,9 @@ export class MegaServer {
|
|
|
134
160
|
// 다른 쪽 리스너를 제거해 누수를 막는다.
|
|
135
161
|
const onError = (/** @type {Error} */ err) => {
|
|
136
162
|
httpServer.removeListener('listening', onListening)
|
|
163
|
+
// listen 실패(EADDRINUSE 등)한 서버는 참조를 비운다 — 남기면 재호출 가드가 "이미 listening"
|
|
164
|
+
// 으로 오인해 같은 인스턴스의 재시도(포트 바꿔 listen)가 막힌다.
|
|
165
|
+
this._httpServer = null
|
|
137
166
|
reject(err)
|
|
138
167
|
}
|
|
139
168
|
const onListening = () => {
|
|
@@ -167,6 +196,8 @@ export class MegaServer {
|
|
|
167
196
|
this._httpServer.close((err) => (err ? reject(err) : resolve(undefined)))
|
|
168
197
|
})
|
|
169
198
|
}
|
|
199
|
+
// 참조 해제 — listen() 재호출 가드가 "이미 listening" 과 "close 후 재기동" 을 구분할 수 있게 한다.
|
|
200
|
+
this._httpServer = null
|
|
170
201
|
}
|
|
171
202
|
|
|
172
203
|
/** 등록된 호스트 목록 (디버그·테스트용). */
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Dialect 레지스트리 (ADR-204/205) — adapter driver 이름 → SQL 렌더 dialect 매핑 + **contract 검증**.
|
|
4
|
+
*
|
|
5
|
+
* dialect 는 `dialects/README.md` 의 표준 인터페이스(함수 12 + 속성 7)를 전부 구현해야 한다 —
|
|
6
|
+
* differ/generate 는 이 표면만 호출하므로, M-6(maria/sqlite) 확장은 dialect 모듈 추가만으로 동작한다.
|
|
7
|
+
* 미구현 멤버는 등록 조회 시점에 fail-fast(`migration.dialect_contract`) — 런타임 한가운데서
|
|
8
|
+
* undefined 호출로 죽는 것을 차단.
|
|
9
|
+
*
|
|
10
|
+
* 미지원 driver 는 **명시 fail-fast** —
|
|
11
|
+
* 조용히 건너뛰면 사용자가 "스키마를 선언했는데 마이그레이션이 안 나온다" 를 모른다(P7).
|
|
12
|
+
*
|
|
13
|
+
* @module core/migration/dialect-registry
|
|
14
|
+
*/
|
|
15
|
+
import { MegaConfigError } from '../../errors/config-error.js'
|
|
16
|
+
import * as postgres from './dialects/postgres.js'
|
|
17
|
+
import * as maria from './dialects/maria.js'
|
|
18
|
+
import * as sqlite from './dialects/sqlite.js'
|
|
19
|
+
import * as mongo from './dialects/mongo.js'
|
|
20
|
+
|
|
21
|
+
/** 표준 인터페이스 — 함수 멤버 (dialects/README.md 정본). */
|
|
22
|
+
const CONTRACT_FNS = [
|
|
23
|
+
'renderOps', 'renderOp',
|
|
24
|
+
'quoteIdent', 'quoteLiteral', 'enumCheckExpr',
|
|
25
|
+
'indexName', 'resolveIndexName', 'uniqueName', 'fkName', 'checkName', 'pkName', 'inlinePkName',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/** 표준 인터페이스 — 능력/한계 속성 멤버 (boolean | number | string). */
|
|
29
|
+
const CONTRACT_PROPS = [
|
|
30
|
+
'identifierMaxBytes',
|
|
31
|
+
'supportsConcurrentIndex',
|
|
32
|
+
'enumStrategy',
|
|
33
|
+
'supportsRenameInTx',
|
|
34
|
+
'canDropColumnInTx',
|
|
35
|
+
'dependsOnRebuild',
|
|
36
|
+
'supportsRenameConstraint',
|
|
37
|
+
'supportsAlterAddFk',
|
|
38
|
+
'requiresDropFkBeforeDropColumn',
|
|
39
|
+
'usesSqlDdl',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
/** @type {Record<string, Record<string, any>>} driver → dialect. */
|
|
43
|
+
const DIALECTS = {
|
|
44
|
+
postgres,
|
|
45
|
+
mariadb: maria,
|
|
46
|
+
sqlite,
|
|
47
|
+
mongodb: mongo,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 자동 생성을 지원하는 driver 목록. */
|
|
51
|
+
export const SUPPORTED_DRIVERS = Object.keys(DIALECTS)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* dialect 의 contract 충족 검증 — 미구현 멤버 목록을 모아 한 번에 보고.
|
|
55
|
+
* @param {string} driver @param {Record<string, any>} dialect
|
|
56
|
+
* @throws {MegaConfigError} `migration.dialect_contract`
|
|
57
|
+
*/
|
|
58
|
+
export function assertDialectContract(driver, dialect) {
|
|
59
|
+
const missing = [
|
|
60
|
+
...CONTRACT_FNS.filter((f) => typeof dialect[f] !== 'function').map((f) => `${f}()`),
|
|
61
|
+
...CONTRACT_PROPS.filter((p) => dialect[p] === undefined),
|
|
62
|
+
]
|
|
63
|
+
if (missing.length > 0) {
|
|
64
|
+
throw new MegaConfigError(
|
|
65
|
+
'migration.dialect_contract',
|
|
66
|
+
`dialect '${driver}' 가 표준 인터페이스를 충족하지 않습니다 — 누락: [${missing.join(', ')}] (dialects/README.md 참조).`,
|
|
67
|
+
{ details: { driver, missing } },
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* driver 의 dialect 조회(+contract 검증).
|
|
74
|
+
* @param {string} driver - services.databases.<key>.driver 값.
|
|
75
|
+
* @returns {Record<string, any>}
|
|
76
|
+
* @throws {MegaConfigError} `migration.dialect_unsupported` | `migration.dialect_contract`
|
|
77
|
+
*/
|
|
78
|
+
export function getDialect(driver) {
|
|
79
|
+
const dialect = DIALECTS[driver]
|
|
80
|
+
if (dialect === undefined) {
|
|
81
|
+
throw new MegaConfigError(
|
|
82
|
+
'migration.dialect_unsupported',
|
|
83
|
+
`driver '${driver}' 는 마이그레이션 자동 생성을 아직 지원하지 않습니다 — 지원: [${SUPPORTED_DRIVERS.join(', ')}]. ` +
|
|
84
|
+
'해당 adapter 의 변경은 raw SQL 마이그레이션(mega generate migration)으로 작성하세요.',
|
|
85
|
+
{ details: { driver, supported: SUPPORTED_DRIVERS } },
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
assertDialectContract(driver, dialect)
|
|
89
|
+
return dialect
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** DML facet(ADR-212) — CRUD(model-crud)가 쓰는 dialect 보조 멤버. DDL 계약과 별개. */
|
|
93
|
+
const DML_CONTRACT_FNS = ['placeholder', 'parseReadResult', 'parseWriteResult', 'upsertClause']
|
|
94
|
+
const DML_CONTRACT_PROPS = ['paramStyle', 'supportsReturning', 'supportsBulkReturning']
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* dialect 가 DML facet 을 충족하는지 — **누락 멤버 배열**(빈 배열=충족)을 반환한다(throw 아님 — 호출부
|
|
98
|
+
* model-crud 가 `model.*` 에러로 표면화). mongo 등 DML facet 미구현 dialect 는 비어있지 않은 배열을 돌려준다.
|
|
99
|
+
* @param {Record<string, any>} dialect
|
|
100
|
+
* @returns {string[]}
|
|
101
|
+
*/
|
|
102
|
+
export function dmlFacetMissing(dialect) {
|
|
103
|
+
return [
|
|
104
|
+
...DML_CONTRACT_FNS.filter((f) => typeof dialect?.[f] !== 'function').map((f) => `${f}()`),
|
|
105
|
+
...DML_CONTRACT_PROPS.filter((p) => dialect?.[p] === undefined),
|
|
106
|
+
]
|
|
107
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Migration Dialect Contract (ADR-205)
|
|
2
|
+
|
|
3
|
+
`mega migrate:generate` 의 diff 엔진(`differ.js`)과 호스트(`generate.js`)는 **이 인터페이스만**
|
|
4
|
+
호출한다. 새 dialect(maria/sqlite — M-6, mongodb — M-5)는 본 contract 를 구현한 모듈을
|
|
5
|
+
`dialect-registry.js` 의 `DIALECTS` 에 추가하는 것만으로 동작해야 한다. 누락 멤버는 `getDialect()` 가
|
|
6
|
+
`migration.dialect_contract` 로 fail-fast 한다. mongodb 는 SQL 이 아니라 **mongo command JS 문**을
|
|
7
|
+
렌더한다(`usesSqlDdl: false` — generate 가 파일 템플릿을 분기, ADR-209).
|
|
8
|
+
|
|
9
|
+
## 함수 (12)
|
|
10
|
+
|
|
11
|
+
| 멤버 | 의미 | postgres 구현 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `renderOps(ops)` | op 목록 → `{ up: string[], down: string[] }` (down 은 역순) | `dialects/postgres.js` |
|
|
14
|
+
| `renderOp(op)` | op 1개 → up/down 쌍. op 종류는 differ 가 정본(19종 + renameFk) | 〃 |
|
|
15
|
+
| `quoteIdent(name)` | 식별자 인용 깔때기 — **`identifierMaxBytes` 초과 fail-fast**(`migration.identifier_too_long`) 포함 | `"x"` (필요 시만) |
|
|
16
|
+
| `quoteLiteral(value)` | 값 literal 인용(injection 방지 깔때기). NaN/Infinity 거부 | `'..'` escape |
|
|
17
|
+
| `enumCheckExpr(col, values)` | enum 의 CHECK 식 — **유일한 enum 렌더 지점**(differ 는 values 만 전달) | `col IN ('a','b')` |
|
|
18
|
+
| `indexName(table, cols, unique?)` | 인덱스 명명 표준 | `idx_`/`uniq_` |
|
|
19
|
+
| `resolveIndexName(table, ix)` | IndexDef 의 유효 이름(명시 name 우선, 표현식은 name 필수) | 〃 |
|
|
20
|
+
| `uniqueName(table, col)` | UNIQUE 제약 명명 | `uniq_<t>_<c>` |
|
|
21
|
+
| `fkName(table, col, refTable)` | FK 제약 명명 | `fk_<t>_<c>_<r>` |
|
|
22
|
+
| `checkName(table, col)` | CHECK 제약 명명 | `chk_<t>_<c>` |
|
|
23
|
+
| `pkName(table)` | 명시(복합·후행) PK 명명 | `pk_<t>` |
|
|
24
|
+
| `inlinePkName(table)` | CREATE TABLE 인라인 단일 PK 의 **서버 자동 명명** | `<t>_pkey` |
|
|
25
|
+
|
|
26
|
+
명명 함수들은 diff 의 식별자이기도 하다 — 이름이 흔들리면 변경이 drop+add 로 오판되므로
|
|
27
|
+
**dialect 간에도 같은 표준**(`idx_/uniq_/fk_/chk_/pk_`)을 권장한다. 서버 자동 명명만
|
|
28
|
+
dialect 별로 다르다(`inlinePkName`).
|
|
29
|
+
|
|
30
|
+
## 능력/한계 속성 (10)
|
|
31
|
+
|
|
32
|
+
| 멤버 | 타입 | postgres | maria | sqlite | mongodb |
|
|
33
|
+
|---|---|---|---|---|---|
|
|
34
|
+
| `identifierMaxBytes` | number | 63 (byte) | 64 (**자** 단위 — utf8 멀티바이트도 64자, ADR-208 L-3) | `Infinity` | 64 (namespace 한도의 보수적 적용) |
|
|
35
|
+
| `supportsConcurrentIndex` | boolean | true (`CONCURRENTLY`, no-tx 필요) | true (`ALGORITHM=INPLACE, LOCK=NONE`) | false | true (4.2+ 항상 online — 표기일 뿐) |
|
|
36
|
+
| `enumStrategy` | string | `'check'` | `'enum-type'` (값 변경 = MODIFY COLUMN) | `'check'` | `'jsonschema-enum'` ($jsonSchema enum 키워드) |
|
|
37
|
+
| `supportsRenameInTx` | boolean | true | false (DDL 암묵 commit) | true (3.25+) | false (renameCollection 은 tx 불가) |
|
|
38
|
+
| `canDropColumnInTx` | boolean | true | false (DDL 암묵 commit) | true — 단 본 dialect 는 rebuild 사용 | false (DDL 자체가 tx 밖) |
|
|
39
|
+
| `dependsOnRebuild` | boolean | false | false | **true** — 트리거 op 는 rebuildTable 로 수렴 | **true** — validator 통짜 교체(collMod) 수렴 |
|
|
40
|
+
| `supportsRenameConstraint` | boolean | true (9.2+) | **false** (11.8 실측 — 구문 에러) → DROP+ADD | false (FK 인라인 — 동기화 불필요) | false (constraint 개념 없음) |
|
|
41
|
+
| `supportsAlterAddFk` | boolean | true | true (`ADD/DROP CONSTRAINT` 실측) | **false** — FK 는 CREATE TABLE 인라인, 변경은 rebuild | false (FK 없음 — `.references()` 는 명시 거부) |
|
|
42
|
+
| `requiresDropFkBeforeDropColumn` | boolean | false (DROP COLUMN 이 FK 동반 제거) | **true** — InnoDB 가 FK 인덱스 겸용 컬럼의 단독 DROP 을 거부(1553), differ 가 dropFk 선행 산출(ADR-208 H-1) | false (rebuild 경로) | false |
|
|
43
|
+
| `usesSqlDdl` | boolean | true | true | true | **false** — mongo command JS 문 렌더(파일의 `db` = mongodb `Db`, ADR-209) |
|
|
44
|
+
|
|
45
|
+
선택 멤버: `rebuildTriggerKinds`(Set) — `dependsOnRebuild` dialect 가 수렴 트리거 종류를 확장한다
|
|
46
|
+
(mongodb: addColumn/renameColumn/setComment 포함 — validator 가 통짜라 모든 컬럼 수준 변경이 수렴.
|
|
47
|
+
미지정 시 differ 의 sqlite 기본 집합).
|
|
48
|
+
|
|
49
|
+
## 구현 규칙
|
|
50
|
+
|
|
51
|
+
- **식별자/값은 반드시 `quoteIdent`/`quoteLiteral` 를 거친다** — differ 등 상위 계층은 SQL 조각을
|
|
52
|
+
직접 합성하지 않는다(H-1 재발 방지).
|
|
53
|
+
- 파괴적 변경(DROP TABLE/COLUMN)·위험 캐스트는 SQL 안에 `-- 경고`/`-- TODO` 주석을 동반한다
|
|
54
|
+
(silent 손실 금지 — 사용자가 적용 전 검토).
|
|
55
|
+
- `supportsRenameConstraint === false` 인 dialect 의 FK 이름 동기화는 differ 가 `dropFk + addFk` 로 산출한다.
|
|
56
|
+
- `dependsOnRebuild === true` 면 differ 가 ALTER 로 표현 못하는 테이블 변경 전체를 `rebuildTable`
|
|
57
|
+
op(`{ from, to, renames }`) 1개로 수렴한다 — sqlite 는 12-step 재생성을, mongodb 는 validator
|
|
58
|
+
교체(collMod 우선 → `$rename` — strict 검증이 update 결과를 현재 validator 로 평가하므로 순서가
|
|
59
|
+
정본, ADR-209)를 렌더한다. generate 는 rebuildTable 포함 파일(sqlite)·mongodb 파일 전체에
|
|
60
|
+
`export const transaction = false` 를 설정한다.
|
|
61
|
+
- op 의 형태(필드)는 `differ.js` 의 산출이 정본 — dialect 는 렌더만 한다. setNotNull/dropNotNull/
|
|
62
|
+
setDefault/dropDefault/setComment 는 대상 `def`(+`prevDef`)를 동반한다(maria MODIFY COLUMN 용).
|