mega-framework 0.1.6 → 0.1.7

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 (233) hide show
  1. package/bin/mega-ws-hub.js +2 -2
  2. package/package.json +32 -8
  3. package/sample/crud/.env +1 -1
  4. package/sample/crud/.env.example +1 -1
  5. package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
  6. package/sample/crud/.mega/journal/snapshot.json +261 -0
  7. package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
  8. package/sample/crud/apps/main/controllers/web-controller.js +7 -5
  9. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
  10. package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
  11. package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
  12. package/sample/crud/apps/main/models/log-partition-model.js +105 -0
  13. package/sample/crud/apps/main/models/note-model.js +79 -0
  14. package/sample/crud/apps/main/models/user-level-model.js +24 -0
  15. package/sample/crud/apps/main/models/user-model.js +146 -0
  16. package/sample/crud/apps/main/models/user-type-model.js +21 -0
  17. package/sample/crud/apps/main/models/wallet-model.js +24 -0
  18. package/sample/crud/apps/main/routes/users.js +55 -10
  19. package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
  20. package/sample/crud/apps/main/services/auth-service.js +39 -24
  21. package/sample/crud/apps/main/services/log-partition-service.js +101 -0
  22. package/sample/crud/apps/main/services/note-service.js +6 -6
  23. package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
  24. package/sample/crud/apps/main/services/user-service.js +62 -21
  25. package/sample/crud/apps/main/views/auth/login.ejs +6 -6
  26. package/sample/crud/apps/main/views/auth/register.ejs +46 -5
  27. package/sample/crud/apps/main/views/users/edit.ejs +42 -5
  28. package/sample/crud/apps/main/views/users/list.ejs +6 -2
  29. package/sample/crud/apps/main/views/users/new.ejs +56 -4
  30. package/sample/crud/docs/log_partition_design.mm.md +23 -0
  31. package/sample/crud/mega.config.js +3 -2
  32. package/sample/crud/package.json +1 -1
  33. package/sample/crud/scripts/start-ws-hub.sh +2 -2
  34. package/sample/simple/package.json +2 -2
  35. package/src/adapters/adapter-manager.js +2 -1
  36. package/src/adapters/adapter-options.js +30 -0
  37. package/src/adapters/maria-adapter.js +26 -3
  38. package/src/adapters/mega-db-adapter.js +7 -1
  39. package/src/adapters/mongo-adapter.js +19 -1
  40. package/src/adapters/postgres-adapter.js +25 -2
  41. package/src/adapters/sqlite-adapter.js +20 -1
  42. package/src/cli/commands/new.js +13 -3
  43. package/src/cli/commands/scaffold.js +137 -33
  44. package/src/cli/generators/index.js +82 -2
  45. package/src/cli/index.js +353 -100
  46. package/src/core/ajv-mapper.js +27 -2
  47. package/src/core/boot.js +464 -245
  48. package/src/core/cluster-metrics.js +13 -4
  49. package/src/core/ctx-builder.js +6 -2
  50. package/src/core/envelope.js +112 -12
  51. package/src/core/hub-link.js +65 -4
  52. package/src/core/i18n.js +11 -1
  53. package/src/core/index.js +6 -2
  54. package/src/core/mega-app.js +201 -463
  55. package/src/core/mega-cluster.js +4 -1
  56. package/src/core/mega-server.js +40 -9
  57. package/src/core/migration/dialect-registry.js +107 -0
  58. package/src/core/migration/dialects/README.md +62 -0
  59. package/src/core/migration/dialects/maria.js +496 -0
  60. package/src/core/migration/dialects/mongo.js +824 -0
  61. package/src/core/migration/dialects/postgres.js +563 -0
  62. package/src/core/migration/dialects/sqlite.js +476 -0
  63. package/src/core/migration/differ.js +456 -0
  64. package/src/core/migration/generate.js +508 -0
  65. package/src/core/migration/journal.js +167 -0
  66. package/src/core/migration/model-scan.js +84 -0
  67. package/src/core/migration/mongo-migration-db.js +97 -0
  68. package/src/core/migration/schema-builder.js +400 -0
  69. package/src/core/migration/schema-validator.js +315 -0
  70. package/src/core/migration-lock.js +205 -0
  71. package/src/core/migration-runner.js +166 -38
  72. package/src/core/multipart.js +28 -5
  73. package/src/core/pipeline.js +129 -0
  74. package/src/core/router.js +70 -65
  75. package/src/core/security.js +67 -9
  76. package/src/core/workers-manager.js +12 -1
  77. package/src/core/ws-cluster.js +10 -3
  78. package/src/core/ws-message.js +48 -4
  79. package/src/core/ws-presence.js +624 -0
  80. package/src/core/ws-roster.js +4 -1
  81. package/src/core/ws-upgrade.js +118 -12
  82. package/src/index.js +1 -1
  83. package/src/lib/hub-protocol.js +29 -0
  84. package/src/lib/mega-health.js +25 -4
  85. package/src/lib/mega-job-queue.js +98 -21
  86. package/src/lib/mega-job.js +29 -0
  87. package/src/lib/mega-metrics.js +3 -12
  88. package/src/lib/mega-plugin.js +34 -3
  89. package/src/lib/mega-schedule.js +40 -22
  90. package/src/lib/mega-shutdown.js +114 -39
  91. package/src/lib/mega-tracing.js +66 -19
  92. package/src/lib/mega-worker.js +5 -1
  93. package/src/lib/otel-resource.js +36 -0
  94. package/src/{cli → lib}/ws-hub.js +51 -8
  95. package/src/models/crud-sql-builder.js +133 -0
  96. package/src/models/mega-model.js +82 -2
  97. package/src/models/model-crud.js +483 -0
  98. package/src/models/mongo-crud.js +285 -0
  99. package/templates/model/code-mongo.tpl +35 -0
  100. package/templates/model/code.tpl +15 -1
  101. package/templates/model/test-mongo.tpl +38 -0
  102. package/templates/model/test.tpl +4 -0
  103. package/types/adapters/adapter-manager.d.ts +95 -0
  104. package/types/adapters/adapter-options.d.ts +91 -0
  105. package/types/adapters/file-adapter.d.ts +94 -0
  106. package/types/adapters/file-session-adapter.d.ts +101 -0
  107. package/types/adapters/index.d.ts +20 -0
  108. package/types/adapters/maria-adapter.d.ts +115 -0
  109. package/types/adapters/mega-adapter.d.ts +215 -0
  110. package/types/adapters/mega-bus-adapter.d.ts +45 -0
  111. package/types/adapters/mega-cache-adapter.d.ts +47 -0
  112. package/types/adapters/mega-db-adapter.d.ts +47 -0
  113. package/types/adapters/mega-lock-adapter.d.ts +62 -0
  114. package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
  115. package/types/adapters/mega-session-adapter.d.ts +32 -0
  116. package/types/adapters/mongo-adapter.d.ts +139 -0
  117. package/types/adapters/nats-adapter.d.ts +108 -0
  118. package/types/adapters/postgres-adapter.d.ts +139 -0
  119. package/types/adapters/redis-adapter.d.ts +70 -0
  120. package/types/adapters/redis-session-adapter.d.ts +82 -0
  121. package/types/adapters/redlock-adapter.d.ts +149 -0
  122. package/types/adapters/registry.d.ts +46 -0
  123. package/types/adapters/sqlite-adapter.d.ts +106 -0
  124. package/types/auth/index.d.ts +24 -0
  125. package/types/cli/commands/console-cmd.d.ts +37 -0
  126. package/types/cli/commands/new.d.ts +16 -0
  127. package/types/cli/commands/routes.d.ts +36 -0
  128. package/types/cli/commands/scaffold.d.ts +78 -0
  129. package/types/cli/commands/test-cmd.d.ts +14 -0
  130. package/types/cli/generators/index.d.ts +112 -0
  131. package/types/cli/index.d.ts +249 -0
  132. package/types/cli/template-engine.d.ts +40 -0
  133. package/types/core/ajv-mapper.d.ts +27 -0
  134. package/types/core/boot.d.ts +233 -0
  135. package/types/core/cluster-metrics.d.ts +52 -0
  136. package/types/core/config-loader.d.ts +13 -0
  137. package/types/core/config-validator.d.ts +30 -0
  138. package/types/core/ctx-builder.d.ts +80 -0
  139. package/types/core/envelope.d.ts +79 -0
  140. package/types/core/error-mapper.d.ts +17 -0
  141. package/types/core/formbody.d.ts +41 -0
  142. package/types/core/hub-link.d.ts +264 -0
  143. package/types/core/i18n.d.ts +178 -0
  144. package/types/core/index.d.ts +28 -0
  145. package/types/core/mega-app.d.ts +529 -0
  146. package/types/core/mega-cluster.d.ts +104 -0
  147. package/types/core/mega-server.d.ts +91 -0
  148. package/types/core/mega-service.d.ts +31 -0
  149. package/types/core/migration/dialect-registry.d.ts +22 -0
  150. package/types/core/migration/dialects/maria.d.ts +99 -0
  151. package/types/core/migration/dialects/mongo.d.ts +89 -0
  152. package/types/core/migration/dialects/postgres.d.ts +117 -0
  153. package/types/core/migration/dialects/sqlite.d.ts +111 -0
  154. package/types/core/migration/differ.d.ts +47 -0
  155. package/types/core/migration/generate.d.ts +56 -0
  156. package/types/core/migration/journal.d.ts +52 -0
  157. package/types/core/migration/model-scan.d.ts +19 -0
  158. package/types/core/migration/mongo-migration-db.d.ts +7 -0
  159. package/types/core/migration/schema-builder.d.ts +197 -0
  160. package/types/core/migration/schema-validator.d.ts +20 -0
  161. package/types/core/migration-lock.d.ts +33 -0
  162. package/types/core/migration-runner.d.ts +101 -0
  163. package/types/core/multipart.d.ts +86 -0
  164. package/types/core/openapi.d.ts +62 -0
  165. package/types/core/pipeline.d.ts +92 -0
  166. package/types/core/router.d.ts +159 -0
  167. package/types/core/routes-loader.d.ts +21 -0
  168. package/types/core/scope-registry.d.ts +14 -0
  169. package/types/core/security.d.ts +77 -0
  170. package/types/core/services-loader.d.ts +27 -0
  171. package/types/core/session-cleanup-schedule.d.ts +19 -0
  172. package/types/core/session-store.d.ts +18 -0
  173. package/types/core/session.d.ts +77 -0
  174. package/types/core/static-assets.d.ts +73 -0
  175. package/types/core/template.d.ts +106 -0
  176. package/types/core/workers-manager.d.ts +79 -0
  177. package/types/core/ws-cluster.d.ts +208 -0
  178. package/types/core/ws-compression.d.ts +112 -0
  179. package/types/core/ws-controller.d.ts +65 -0
  180. package/types/core/ws-message.d.ts +106 -0
  181. package/types/core/ws-presence.d.ts +273 -0
  182. package/types/core/ws-roster.d.ts +96 -0
  183. package/types/core/ws-upgrade.d.ts +231 -0
  184. package/types/errors/config-error.d.ts +10 -0
  185. package/types/errors/http-errors.d.ts +120 -0
  186. package/types/errors/index.d.ts +3 -0
  187. package/types/errors/mega-error.d.ts +32 -0
  188. package/types/index.d.ts +39 -0
  189. package/types/lib/asp/config.d.ts +49 -0
  190. package/types/lib/asp/crypto.d.ts +43 -0
  191. package/types/lib/asp/errors.d.ts +30 -0
  192. package/types/lib/asp/nonce-cache.d.ts +52 -0
  193. package/types/lib/asp/plugin.d.ts +30 -0
  194. package/types/lib/asp/ws-terminator.d.ts +45 -0
  195. package/types/lib/env-mapper.d.ts +14 -0
  196. package/types/lib/hub-protocol.d.ts +106 -0
  197. package/types/lib/index.d.ts +22 -0
  198. package/types/lib/logger/telegram-core.d.ts +104 -0
  199. package/types/lib/logger/telegram-transport.d.ts +45 -0
  200. package/types/lib/mega-brute-force.d.ts +66 -0
  201. package/types/lib/mega-circuit-breaker.d.ts +241 -0
  202. package/types/lib/mega-cron.d.ts +66 -0
  203. package/types/lib/mega-hash.d.ts +32 -0
  204. package/types/lib/mega-health.d.ts +41 -0
  205. package/types/lib/mega-job-queue.d.ts +176 -0
  206. package/types/lib/mega-job-worker.d.ts +130 -0
  207. package/types/lib/mega-job.d.ts +138 -0
  208. package/types/lib/mega-logger.d.ts +45 -0
  209. package/types/lib/mega-metrics.d.ts +285 -0
  210. package/types/lib/mega-plugin.d.ts +245 -0
  211. package/types/lib/mega-retry.d.ts +85 -0
  212. package/types/lib/mega-schedule.d.ts +260 -0
  213. package/types/lib/mega-shutdown.d.ts +135 -0
  214. package/types/lib/mega-tracing.d.ts +224 -0
  215. package/types/lib/mega-worker.d.ts +127 -0
  216. package/types/lib/otel-resource.d.ts +16 -0
  217. package/types/lib/worker-runner/process-entry.d.ts +1 -0
  218. package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
  219. package/types/lib/worker-runner/thread-entry.d.ts +1 -0
  220. package/types/lib/ws-hub.d.ts +234 -0
  221. package/types/models/crud-sql-builder.d.ts +48 -0
  222. package/types/models/index.d.ts +1 -0
  223. package/types/models/mega-model.d.ts +138 -0
  224. package/types/models/model-crud.d.ts +82 -0
  225. package/types/models/mongo-crud.d.ts +59 -0
  226. package/types/test/index.d.ts +84 -0
  227. package/.env +0 -127
  228. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
  229. package/sample/crud/apps/main/models/note.js +0 -71
  230. package/sample/crud/apps/main/models/user.js +0 -86
  231. package/sample/crud/package-lock.json +0 -5665
  232. package/sample/crud/yarn.lock +0 -2142
  233. package/sample/simple/package-lock.json +0 -1851
@@ -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 로거.
@@ -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] - listen 기본값 (listen() 인자로 override 가능)
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 hostHeader = String(req.headers.host || '')
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}'. Known hosts: ${[...this._hostMap.keys()].join(', ')}`,
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 hostHeader = String(req.headers.host || '')
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 과 동일 패턴). MegaServer
116
- // 아직 pino 미통합이라 비치명적 소켓 에러는 console.warn 으로 명시 로그.
140
+ // exception → 프로세스 크래시 하는 것을 막는다 (H1 과 동일 패턴). 비치명적 소켓 에러는
141
+ // 주입 로거(warn)로, 미주입이면 console.warn 폴백으로 명시 로그.
117
142
  socket.on('error', (err) => {
118
- console.warn('[MegaServer] raw socket error on unmapped-host upgrade:', err?.message ?? err)
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 용).