mega-framework 0.1.5 → 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 (236) hide show
  1. package/bin/mega-ws-hub.js +2 -2
  2. package/package.json +32 -8
  3. package/sample/crud/.env +156 -8
  4. package/sample/crud/.env.example +153 -28
  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 +63 -3
  32. package/sample/crud/package.json +3 -3
  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 +478 -104
  46. package/src/core/ajv-mapper.js +27 -2
  47. package/src/core/boot.js +485 -237
  48. package/src/core/cluster-metrics.js +13 -4
  49. package/src/core/config-validator.js +25 -0
  50. package/src/core/ctx-builder.js +6 -2
  51. package/src/core/envelope.js +112 -12
  52. package/src/core/hub-link.js +65 -4
  53. package/src/core/i18n.js +11 -1
  54. package/src/core/index.js +6 -2
  55. package/src/core/mega-app.js +223 -481
  56. package/src/core/mega-cluster.js +54 -13
  57. package/src/core/mega-server.js +40 -9
  58. package/src/core/migration/dialect-registry.js +107 -0
  59. package/src/core/migration/dialects/README.md +62 -0
  60. package/src/core/migration/dialects/maria.js +496 -0
  61. package/src/core/migration/dialects/mongo.js +824 -0
  62. package/src/core/migration/dialects/postgres.js +563 -0
  63. package/src/core/migration/dialects/sqlite.js +476 -0
  64. package/src/core/migration/differ.js +456 -0
  65. package/src/core/migration/generate.js +508 -0
  66. package/src/core/migration/journal.js +167 -0
  67. package/src/core/migration/model-scan.js +84 -0
  68. package/src/core/migration/mongo-migration-db.js +97 -0
  69. package/src/core/migration/schema-builder.js +400 -0
  70. package/src/core/migration/schema-validator.js +315 -0
  71. package/src/core/migration-lock.js +205 -0
  72. package/src/core/migration-runner.js +166 -38
  73. package/src/core/multipart.js +28 -5
  74. package/src/core/pipeline.js +129 -0
  75. package/src/core/router.js +70 -65
  76. package/src/core/scope-registry.js +0 -1
  77. package/src/core/security.js +67 -9
  78. package/src/core/workers-manager.js +12 -1
  79. package/src/core/ws-cluster.js +10 -3
  80. package/src/core/ws-message.js +48 -4
  81. package/src/core/ws-presence.js +624 -0
  82. package/src/core/ws-roster.js +4 -1
  83. package/src/core/ws-upgrade.js +118 -12
  84. package/src/index.js +1 -1
  85. package/src/lib/hub-protocol.js +29 -0
  86. package/src/lib/mega-health.js +25 -4
  87. package/src/lib/mega-job-queue.js +98 -21
  88. package/src/lib/mega-job.js +29 -0
  89. package/src/lib/mega-logger.js +1 -1
  90. package/src/lib/mega-metrics.js +3 -12
  91. package/src/lib/mega-plugin.js +34 -3
  92. package/src/lib/mega-schedule.js +40 -22
  93. package/src/lib/mega-shutdown.js +162 -49
  94. package/src/lib/mega-tracing.js +66 -19
  95. package/src/lib/mega-worker.js +5 -1
  96. package/src/lib/otel-resource.js +36 -0
  97. package/src/{cli → lib}/ws-hub.js +51 -8
  98. package/src/models/crud-sql-builder.js +133 -0
  99. package/src/models/mega-model.js +82 -2
  100. package/src/models/model-crud.js +483 -0
  101. package/src/models/mongo-crud.js +285 -0
  102. package/templates/model/code-mongo.tpl +35 -0
  103. package/templates/model/code.tpl +15 -1
  104. package/templates/model/test-mongo.tpl +38 -0
  105. package/templates/model/test.tpl +4 -0
  106. package/types/adapters/adapter-manager.d.ts +95 -0
  107. package/types/adapters/adapter-options.d.ts +91 -0
  108. package/types/adapters/file-adapter.d.ts +94 -0
  109. package/types/adapters/file-session-adapter.d.ts +101 -0
  110. package/types/adapters/index.d.ts +20 -0
  111. package/types/adapters/maria-adapter.d.ts +115 -0
  112. package/types/adapters/mega-adapter.d.ts +215 -0
  113. package/types/adapters/mega-bus-adapter.d.ts +45 -0
  114. package/types/adapters/mega-cache-adapter.d.ts +47 -0
  115. package/types/adapters/mega-db-adapter.d.ts +47 -0
  116. package/types/adapters/mega-lock-adapter.d.ts +62 -0
  117. package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
  118. package/types/adapters/mega-session-adapter.d.ts +32 -0
  119. package/types/adapters/mongo-adapter.d.ts +139 -0
  120. package/types/adapters/nats-adapter.d.ts +108 -0
  121. package/types/adapters/postgres-adapter.d.ts +139 -0
  122. package/types/adapters/redis-adapter.d.ts +70 -0
  123. package/types/adapters/redis-session-adapter.d.ts +82 -0
  124. package/types/adapters/redlock-adapter.d.ts +149 -0
  125. package/types/adapters/registry.d.ts +46 -0
  126. package/types/adapters/sqlite-adapter.d.ts +106 -0
  127. package/types/auth/index.d.ts +24 -0
  128. package/types/cli/commands/console-cmd.d.ts +37 -0
  129. package/types/cli/commands/new.d.ts +16 -0
  130. package/types/cli/commands/routes.d.ts +36 -0
  131. package/types/cli/commands/scaffold.d.ts +78 -0
  132. package/types/cli/commands/test-cmd.d.ts +14 -0
  133. package/types/cli/generators/index.d.ts +112 -0
  134. package/types/cli/index.d.ts +249 -0
  135. package/types/cli/template-engine.d.ts +40 -0
  136. package/types/core/ajv-mapper.d.ts +27 -0
  137. package/types/core/boot.d.ts +233 -0
  138. package/types/core/cluster-metrics.d.ts +52 -0
  139. package/types/core/config-loader.d.ts +13 -0
  140. package/types/core/config-validator.d.ts +30 -0
  141. package/types/core/ctx-builder.d.ts +80 -0
  142. package/types/core/envelope.d.ts +79 -0
  143. package/types/core/error-mapper.d.ts +17 -0
  144. package/types/core/formbody.d.ts +41 -0
  145. package/types/core/hub-link.d.ts +264 -0
  146. package/types/core/i18n.d.ts +178 -0
  147. package/types/core/index.d.ts +28 -0
  148. package/types/core/mega-app.d.ts +529 -0
  149. package/types/core/mega-cluster.d.ts +104 -0
  150. package/types/core/mega-server.d.ts +91 -0
  151. package/types/core/mega-service.d.ts +31 -0
  152. package/types/core/migration/dialect-registry.d.ts +22 -0
  153. package/types/core/migration/dialects/maria.d.ts +99 -0
  154. package/types/core/migration/dialects/mongo.d.ts +89 -0
  155. package/types/core/migration/dialects/postgres.d.ts +117 -0
  156. package/types/core/migration/dialects/sqlite.d.ts +111 -0
  157. package/types/core/migration/differ.d.ts +47 -0
  158. package/types/core/migration/generate.d.ts +56 -0
  159. package/types/core/migration/journal.d.ts +52 -0
  160. package/types/core/migration/model-scan.d.ts +19 -0
  161. package/types/core/migration/mongo-migration-db.d.ts +7 -0
  162. package/types/core/migration/schema-builder.d.ts +197 -0
  163. package/types/core/migration/schema-validator.d.ts +20 -0
  164. package/types/core/migration-lock.d.ts +33 -0
  165. package/types/core/migration-runner.d.ts +101 -0
  166. package/types/core/multipart.d.ts +86 -0
  167. package/types/core/openapi.d.ts +62 -0
  168. package/types/core/pipeline.d.ts +92 -0
  169. package/types/core/router.d.ts +159 -0
  170. package/types/core/routes-loader.d.ts +21 -0
  171. package/types/core/scope-registry.d.ts +14 -0
  172. package/types/core/security.d.ts +77 -0
  173. package/types/core/services-loader.d.ts +27 -0
  174. package/types/core/session-cleanup-schedule.d.ts +19 -0
  175. package/types/core/session-store.d.ts +18 -0
  176. package/types/core/session.d.ts +77 -0
  177. package/types/core/static-assets.d.ts +73 -0
  178. package/types/core/template.d.ts +106 -0
  179. package/types/core/workers-manager.d.ts +79 -0
  180. package/types/core/ws-cluster.d.ts +208 -0
  181. package/types/core/ws-compression.d.ts +112 -0
  182. package/types/core/ws-controller.d.ts +65 -0
  183. package/types/core/ws-message.d.ts +106 -0
  184. package/types/core/ws-presence.d.ts +273 -0
  185. package/types/core/ws-roster.d.ts +96 -0
  186. package/types/core/ws-upgrade.d.ts +231 -0
  187. package/types/errors/config-error.d.ts +10 -0
  188. package/types/errors/http-errors.d.ts +120 -0
  189. package/types/errors/index.d.ts +3 -0
  190. package/types/errors/mega-error.d.ts +32 -0
  191. package/types/index.d.ts +39 -0
  192. package/types/lib/asp/config.d.ts +49 -0
  193. package/types/lib/asp/crypto.d.ts +43 -0
  194. package/types/lib/asp/errors.d.ts +30 -0
  195. package/types/lib/asp/nonce-cache.d.ts +52 -0
  196. package/types/lib/asp/plugin.d.ts +30 -0
  197. package/types/lib/asp/ws-terminator.d.ts +45 -0
  198. package/types/lib/env-mapper.d.ts +14 -0
  199. package/types/lib/hub-protocol.d.ts +106 -0
  200. package/types/lib/index.d.ts +22 -0
  201. package/types/lib/logger/telegram-core.d.ts +104 -0
  202. package/types/lib/logger/telegram-transport.d.ts +45 -0
  203. package/types/lib/mega-brute-force.d.ts +66 -0
  204. package/types/lib/mega-circuit-breaker.d.ts +241 -0
  205. package/types/lib/mega-cron.d.ts +66 -0
  206. package/types/lib/mega-hash.d.ts +32 -0
  207. package/types/lib/mega-health.d.ts +41 -0
  208. package/types/lib/mega-job-queue.d.ts +176 -0
  209. package/types/lib/mega-job-worker.d.ts +130 -0
  210. package/types/lib/mega-job.d.ts +138 -0
  211. package/types/lib/mega-logger.d.ts +45 -0
  212. package/types/lib/mega-metrics.d.ts +285 -0
  213. package/types/lib/mega-plugin.d.ts +245 -0
  214. package/types/lib/mega-retry.d.ts +85 -0
  215. package/types/lib/mega-schedule.d.ts +260 -0
  216. package/types/lib/mega-shutdown.d.ts +135 -0
  217. package/types/lib/mega-tracing.d.ts +224 -0
  218. package/types/lib/mega-worker.d.ts +127 -0
  219. package/types/lib/otel-resource.d.ts +16 -0
  220. package/types/lib/worker-runner/process-entry.d.ts +1 -0
  221. package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
  222. package/types/lib/worker-runner/thread-entry.d.ts +1 -0
  223. package/types/lib/ws-hub.d.ts +234 -0
  224. package/types/models/crud-sql-builder.d.ts +48 -0
  225. package/types/models/index.d.ts +1 -0
  226. package/types/models/mega-model.d.ts +138 -0
  227. package/types/models/model-crud.d.ts +82 -0
  228. package/types/models/mongo-crud.d.ts +59 -0
  229. package/types/test/index.d.ts +84 -0
  230. package/.env +0 -127
  231. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
  232. package/sample/crud/apps/main/models/note.js +0 -71
  233. package/sample/crud/apps/main/models/user.js +0 -86
  234. package/sample/crud/package-lock.json +0 -5665
  235. package/sample/crud/yarn.lock +0 -2142
  236. package/sample/simple/package-lock.json +0 -1851
@@ -0,0 +1,84 @@
1
+ // @ts-check
2
+ /**
3
+ * 스키마 모델 file-scan (ADR-204) — `apps/<app>/models/**` 를 재귀 스캔해 `static schema` 를
4
+ * 선언한 모델을 수집한다(별도 register 단계 없음 — services-loader/ADR-148 의 자동 스캔 패턴 정합).
5
+ *
6
+ * 식별은 **duck-typing**: export 값이 `schema` 함수 + `table`/`adapter` 문자열을 가지면 모델로
7
+ * 본다(클래스 identity 비의존 — 프레임워크 패키지 사본이 달라도 동작). `static schema` 가 없는
8
+ * 모델(레거시 raw SQL 운용)은 **정상적으로 건너뛴다** — 자동 생성은 옵트인.
9
+ *
10
+ * 제외 규칙: `_` 로 시작하는 파일/디렉토리, `*.test.js`, `static skip = true` 옵트아웃.
11
+ *
12
+ * @module core/migration/model-scan
13
+ */
14
+ import { existsSync, readdirSync, statSync } from 'node:fs'
15
+ import { join, resolve as pathResolve } from 'node:path'
16
+ import { pathToFileURL } from 'node:url'
17
+ import { MegaConfigError } from '../../errors/config-error.js'
18
+
19
+ /**
20
+ * @typedef {{ name: string, table: string, adapter: string, app: string, file: string, Model: any }} ScannedModel
21
+ */
22
+
23
+ /**
24
+ * models 디렉토리 재귀 수집 — `_` prefix·테스트 파일 제외, 결정적 순서(이름순).
25
+ * @param {string} dir @returns {string[]} 절대 경로 목록.
26
+ */
27
+ function collectModelFiles(dir) {
28
+ /** @type {string[]} */
29
+ const out = []
30
+ const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
31
+ for (const e of entries) {
32
+ if (e.name.startsWith('_')) continue
33
+ const abs = join(dir, e.name)
34
+ if (e.isDirectory()) {
35
+ out.push(...collectModelFiles(abs))
36
+ continue
37
+ }
38
+ if (!e.name.endsWith('.js') || e.name.endsWith('.test.js')) continue
39
+ out.push(pathResolve(abs))
40
+ }
41
+ return out
42
+ }
43
+
44
+ /**
45
+ * 모든 앱의 models 폴더를 스캔해 schema 모델을 수집한다.
46
+ *
47
+ * @param {{ projectRoot: string, appNames: string[] }} opts
48
+ * @returns {Promise<ScannedModel[]>}
49
+ * @throws {MegaConfigError} `migration.model_load_failed` - 모델 파일 import 실패(문법 오류 등).
50
+ */
51
+ export async function scanSchemaModels({ projectRoot, appNames }) {
52
+ /** @type {ScannedModel[]} */
53
+ const out = []
54
+ for (const app of appNames) {
55
+ const dir = join(projectRoot, 'apps', app, 'models')
56
+ if (!existsSync(dir)) continue // 모델 폴더 부재는 정상(마이그레이션 폴더와 동일 규칙)
57
+ for (const file of collectModelFiles(dir)) {
58
+ let mod
59
+ try {
60
+ // mtime 쿼리로 ESM 모듈 캐시를 우회 — 같은 프로세스에서 모델 수정 후 재스캔(연속 generate·
61
+ // 테스트) 시 옛 모듈이 잡히면 diff 가 변경을 못 본다.
62
+ mod = await import(`${pathToFileURL(file).href}?mtime=${statSync(file).mtimeMs}`)
63
+ } catch (err) {
64
+ // ESM dynamic import 의 SyntaxError stack 은 V8 내부 프레임뿐(위치 정보 없음 — 실측)이라
65
+ // stack 병기는 노이즈만 더한다. 파일 경로 + 원문 메시지를 정확히 — 위치는 사용자가
66
+ // `node <file>` 직접 실행으로 확인 가능(cause 에 원본 보존).
67
+ throw new MegaConfigError(
68
+ 'migration.model_load_failed',
69
+ `모델 파일 로드 실패: ${file} — ${/** @type {any} */ (err).message}`,
70
+ { cause: err, details: { file, app } },
71
+ )
72
+ }
73
+ for (const exported of Object.values(mod)) {
74
+ const Model = /** @type {any} */ (exported)
75
+ if (typeof Model !== 'function') continue
76
+ if (typeof Model.schema !== 'function') continue // schema 미선언(레거시 raw SQL 모델) — 옵트인 제외
77
+ if (Model.skip === true) continue // 명시 옵트아웃
78
+ if (typeof Model.table !== 'string' || typeof Model.adapter !== 'string') continue
79
+ out.push({ name: Model.name, table: Model.table, adapter: Model.adapter, app, file, Model })
80
+ }
81
+ }
82
+ }
83
+ return out
84
+ }
@@ -0,0 +1,97 @@
1
+ // @ts-check
2
+ /**
3
+ * mongo 마이그레이션 db 셈(shim) (ADR-209) — 러너({@link module:core/migration-runner})는 이력
4
+ * 부기(`mega_migrations`)를 **framework-controlled SQL 상수**로 수행한다(ADR-149 — name 은
5
+ * 파일명 규약 검증, applied_at 은 ISO, checksum 은 sha-256 hex 라 인라인 안전). mongo 는 SQL 이
6
+ * 없으므로, native `Db` 를 감싼 Proxy 가 그 닫힌 SQL 집합만 collection 연산으로 번역한다 —
7
+ * 러너는 한 줄도 바뀌지 않는다(SQL dialect 경로 영향 0).
8
+ *
9
+ * 마이그레이션 파일(`up(db)/down(db)`)이 받는 `db` 도 이 Proxy 다 — `query` 외 모든 속성은
10
+ * native `Db` 로 위임되므로 생성 파일의 `db.createCollection(...)`/`db.collection(...)`/
11
+ * `db.command(...)` 가 그대로 동작한다. 임의 SQL(`db.query('SELECT …')`)은 명시 거부 —
12
+ * mongo 마이그레이션 본문은 도큐먼트 API 를 쓴다.
13
+ *
14
+ * 이력 도큐먼트: `{ _id: <name>, name, applied_at, checksum }` — `_id` 가 곧 PK 라 동시 적용
15
+ * 경합은 중복 키(E11000)로 차단되고, 러너의 실패 후 이력 재확인(ADR-208 M-2)이 패자를 skip 으로
16
+ * 흡수한다(SQL 경로와 동일 거동).
17
+ *
18
+ * @module core/migration/mongo-migration-db
19
+ */
20
+ import { MegaConfigError } from '../../errors/config-error.js'
21
+ import { MIGRATIONS_TABLE } from '../migration-runner.js'
22
+
23
+ /** 러너 부기 SQL → 번역기. 패턴은 migration-runner.js 의 상수 문장과 1:1 (회귀 테스트로 고정). */
24
+ const SQL_HANDLERS = [
25
+ {
26
+ // ensureTable — 이력 컬렉션은 첫 insert 시 자동 생성되므로 no-op (name=_id 가 PK).
27
+ re: new RegExp(`^CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} `),
28
+ run: async () => ({ rows: /** @type {any[]} */ ([]), rowCount: 0 }),
29
+ },
30
+ {
31
+ // checksum 컬럼 존재 검사 — 도큐먼트 모델엔 컬럼 개념이 없어 항상 "존재"(보강 ALTER 불요).
32
+ re: new RegExp(`^SELECT checksum FROM ${MIGRATIONS_TABLE} LIMIT 1$`),
33
+ run: async () => ({ rows: /** @type {any[]} */ ([]), rowCount: 0 }),
34
+ },
35
+ {
36
+ re: new RegExp(`^SELECT name, checksum FROM ${MIGRATIONS_TABLE}$`),
37
+ /** @param {import('mongodb').Db} db */
38
+ run: async (db) => {
39
+ const rows = await db.collection(MIGRATIONS_TABLE).find({}, { projection: { _id: 0, name: 1, checksum: 1 } }).toArray()
40
+ return { rows, rowCount: rows.length }
41
+ },
42
+ },
43
+ {
44
+ re: new RegExp(`^SELECT name FROM ${MIGRATIONS_TABLE} WHERE name = '([^']+)'$`),
45
+ /** @param {import('mongodb').Db} db @param {RegExpMatchArray} m */
46
+ run: async (db, m) => {
47
+ const doc = await db.collection(MIGRATIONS_TABLE).findOne({ name: m[1] }, { projection: { _id: 0, name: 1 } })
48
+ return { rows: doc === null ? [] : [doc], rowCount: doc === null ? 0 : 1 }
49
+ },
50
+ },
51
+ {
52
+ re: new RegExp(`^INSERT INTO ${MIGRATIONS_TABLE} \\(name, applied_at, checksum\\) VALUES \\('([^']+)', '([^']+)', '([^']+)'\\)$`),
53
+ /** @param {import('mongodb').Db} db @param {RegExpMatchArray} m */
54
+ run: async (db, m) => {
55
+ await db.collection(MIGRATIONS_TABLE).insertOne({ _id: /** @type {any} */ (m[1]), name: m[1], applied_at: m[2], checksum: m[3] })
56
+ return { rowCount: 1 }
57
+ },
58
+ },
59
+ {
60
+ re: new RegExp(`^DELETE FROM ${MIGRATIONS_TABLE} WHERE name = '([^']+)'$`),
61
+ /** @param {import('mongodb').Db} db @param {RegExpMatchArray} m */
62
+ run: async (db, m) => {
63
+ const r = await db.collection(MIGRATIONS_TABLE).deleteOne({ name: m[1] })
64
+ return { rowCount: r.deletedCount }
65
+ },
66
+ },
67
+ ]
68
+
69
+ /**
70
+ * native `Db` → 러너/마이그레이션 파일 공용 db 셈.
71
+ *
72
+ * @param {import('mongodb').Db} nativeDb - 연결된 mongodb `Db` (adapter.native).
73
+ * @returns {any} Proxy — `query(sql)` 은 부기 SQL 번역, 그 외는 native `Db` 위임.
74
+ */
75
+ export function createMongoMigrationDb(nativeDb) {
76
+ /** @param {string} sql @returns {Promise<any>} */
77
+ const query = async (sql) => {
78
+ for (const h of SQL_HANDLERS) {
79
+ const m = sql.match(h.re)
80
+ if (m !== null) return h.run(nativeDb, m)
81
+ }
82
+ throw new MegaConfigError(
83
+ 'migration.mongo_sql_unsupported',
84
+ `mongo 마이그레이션에서 SQL 을 실행할 수 없습니다 — 본문은 도큐먼트 API(db.collection(...)/db.command(...))를 사용하세요. ` +
85
+ `(받은 SQL 머리: ${sql.slice(0, 80)})`,
86
+ { details: { sql: sql.slice(0, 200) } },
87
+ )
88
+ }
89
+ return new Proxy(nativeDb, {
90
+ get(target, prop) {
91
+ if (prop === 'query') return query
92
+ const value = Reflect.get(target, prop, target) // receiver=proxy 면 Db 내부 private 접근이 깨질 수 있다
93
+ // Db 의 메서드는 내부 this 바인딩이 필요하다(collection/command/createCollection …).
94
+ return typeof value === 'function' ? value.bind(target) : value
95
+ },
96
+ })
97
+ }
@@ -0,0 +1,400 @@
1
+ // @ts-check
2
+ /**
3
+ * 스키마 빌더 (ADR-204) — 모델의 `static schema = (t) => ({ ... })` 선언을 **직렬화 가능한 record**
4
+ * 로 변환한다. record 가 journal snapshot(`.mega/journal/`)·diff·SQL 렌더의 단일 정본이다.
5
+ *
6
+ * 빌더는 마이그레이션 **생성 전용**이다 — 런타임 쿼리·CRUD 헬퍼는 만들지 않는다(ADR-009 ORM 부재
7
+ * 결정 준수). 모델은 여전히 native handle(`this.db`)·계측 쿼리(`this.query`)로 도메인 메서드를
8
+ * 직접 작성한다.
9
+ *
10
+ * # 사용 예
11
+ * ```js
12
+ * export class User extends MegaModel {
13
+ * static adapter = 'primary'
14
+ * static table = 'users'
15
+ * static schema = (t) => ({
16
+ * id: t.serial().primary(),
17
+ * email: t.varchar(200).notNull().unique(),
18
+ * role: t.enum(['admin', 'user']).default('user'),
19
+ * orgId: t.integer().references('Organization', 'id', { onDelete: 'cascade' }),
20
+ * createdAt: t.timestamptz().defaultNow(),
21
+ * })
22
+ * static indexes = (t) => [t.index(['role', 'createdAt'])]
23
+ * }
24
+ * ```
25
+ *
26
+ * # record 구조 (journal 직렬화 정본)
27
+ * `{ table, adapter, columns: { <name>: ColumnDef }, primaryKey?: string[], indexes: IndexDef[], validation? }`
28
+ * - ColumnDef: `{ type, length?, precision?, scale?, values?, primary?, notNull?, unique?,
29
+ * default?, check?, references?, comment? }` — 설정된 필드만 존재(미설정 키 부재 = 결정적 직렬화).
30
+ * - references: `{ model, column, table?, onDelete?, onUpdate?, name? }` — `table` 은 스냅샷 구성
31
+ * 시점에 모델 스캔 결과로 해석돼 채워진다(스냅샷 자기완결 — 과거 스냅샷 diff 에 모델 재스캔 불요).
32
+ *
33
+ * @module core/migration/schema-builder
34
+ */
35
+ import { MegaConfigError } from '../../errors/config-error.js'
36
+
37
+ /** FK onDelete/onUpdate 허용 동작 (PostgreSQL 참조 동작 5종). */
38
+ export const FK_ACTIONS = ['cascade', 'set null', 'set default', 'restrict', 'no action']
39
+
40
+ /**
41
+ * 컬럼 정의 체인 빌더 — 타입 메서드(`t.<type>()`)가 만들고, 제약 메서드를 체인한다.
42
+ * 최종 직렬화는 {@link ColumnBuilder#build} 가 수행한다(플레인 객체 반환).
43
+ */
44
+ export class ColumnBuilder {
45
+ /** @type {Record<string, any>} 누적 컬럼 정의. */
46
+ #def
47
+
48
+ /** @param {Record<string, any>} def - 타입 메서드가 만든 초기 정의(`{ type, ... }`). */
49
+ constructor(def) {
50
+ this.#def = def
51
+ }
52
+
53
+ /** PRIMARY KEY 지정(단일 컬럼). 복합 PK 는 `t.primary([cols])` 사용. @returns {this} */
54
+ primary() {
55
+ this.#def.primary = true
56
+ return this
57
+ }
58
+
59
+ /** NOT NULL 지정. @returns {this} */
60
+ notNull() {
61
+ this.#def.notNull = true
62
+ return this
63
+ }
64
+
65
+ /**
66
+ * 명시 null 허용(ADR-210) — SQL 에선 nullable 이 기본이라 no-op(선언적 표시)이고, mongo 에선
67
+ * `bsonType: ['<type>', 'null']` 유니온으로 렌더된다(미선언 시 mongo 는 필드 **생략만** 허용 —
68
+ * 명시 null 은 121 거부, 실측). `.notNull()` 과 동시 사용 불가.
69
+ * @returns {this}
70
+ */
71
+ nullable() {
72
+ this.#def.nullable = true
73
+ return this
74
+ }
75
+
76
+ /**
77
+ * UNIQUE 제약. 이름 미지정 시 dialect 명명 표준(`uniq_<table>_<col>`)을 따른다.
78
+ * @param {{ name?: string }} [opts]
79
+ * @returns {this}
80
+ */
81
+ unique(opts) {
82
+ this.#def.unique = opts?.name !== undefined ? { name: opts.name } : true
83
+ return this
84
+ }
85
+
86
+ /**
87
+ * DEFAULT 값. literal(string/number/boolean/null) 또는 `{ raw: 'expr' }`(raw SQL 식).
88
+ * @param {string | number | boolean | null | { raw: string }} value
89
+ * @returns {this}
90
+ */
91
+ default(value) {
92
+ this.#def.default = value
93
+ return this
94
+ }
95
+
96
+ /** `DEFAULT CURRENT_TIMESTAMP` 단축. @returns {this} */
97
+ defaultNow() {
98
+ this.#def.default = { raw: 'CURRENT_TIMESTAMP' }
99
+ return this
100
+ }
101
+
102
+ /**
103
+ * CHECK 제약. 이름 미지정 시 `chk_<table>_<col>`.
104
+ * @param {string} expr - CHECK 식(raw SQL).
105
+ * @param {{ name?: string }} [opts]
106
+ * @returns {this}
107
+ */
108
+ check(expr, opts) {
109
+ this.#def.check = opts?.name !== undefined ? { expr, name: opts.name } : { expr }
110
+ return this
111
+ }
112
+
113
+ /**
114
+ * FK 참조. 대상은 **모델 이름**(file-scan 결과의 클래스 이름)과 그 컬럼 — 실제 테이블명은
115
+ * 스냅샷 구성 시 해석된다.
116
+ * @param {string} modelName - 대상 모델 이름(예: 'Organization').
117
+ * @param {string} columnName - 대상 컬럼 이름(예: 'id').
118
+ * @param {{ onDelete?: string, onUpdate?: string, name?: string }} [opts]
119
+ * @returns {this}
120
+ */
121
+ references(modelName, columnName, opts = {}) {
122
+ /** @type {Record<string, any>} */
123
+ const ref = { model: modelName, column: columnName }
124
+ if (opts.onDelete !== undefined) ref.onDelete = opts.onDelete
125
+ if (opts.onUpdate !== undefined) ref.onUpdate = opts.onUpdate
126
+ if (opts.name !== undefined) ref.name = opts.name
127
+ this.#def.references = ref
128
+ return this
129
+ }
130
+
131
+ /** 컬럼 코멘트(`COMMENT ON COLUMN`). @param {string} text @returns {this} */
132
+ comment(text) {
133
+ this.#def.comment = text
134
+ return this
135
+ }
136
+
137
+ /** 직렬화 — 누적 정의의 플레인 사본 반환. @returns {Record<string, any>} */
138
+ build() {
139
+ return { ...this.#def }
140
+ }
141
+ }
142
+
143
+ /**
144
+ * 테이블 스키마 빌더 — `schema(t)`/`indexes(t)` 콜백이 받는 `t`.
145
+ * 타입 메서드는 {@link ColumnBuilder} 를, `index()` 는 IndexDef 를 반환한다.
146
+ * `primary([cols])` 는 복합 PK 를 빌더 내부 상태에 기록한다.
147
+ */
148
+ export class SchemaBuilder {
149
+ /** @type {string[] | undefined} 복합 PRIMARY KEY 컬럼 목록 (`t.primary([...])`). */
150
+ compositePrimaryKey = undefined
151
+
152
+ /** @param {Record<string, any>} def @returns {ColumnBuilder} */
153
+ #col(def) {
154
+ return new ColumnBuilder(def)
155
+ }
156
+
157
+ // ── 정수 ──────────────────────────────────────────────────────────────────
158
+ /** 자동증가 INT (postgres SERIAL). @returns {ColumnBuilder} */
159
+ serial() {
160
+ return this.#col({ type: 'serial' })
161
+ }
162
+ /** 자동증가 BIGINT (postgres BIGSERIAL). @returns {ColumnBuilder} */
163
+ bigSerial() {
164
+ return this.#col({ type: 'bigSerial' })
165
+ }
166
+ /** @returns {ColumnBuilder} */
167
+ integer() {
168
+ return this.#col({ type: 'integer' })
169
+ }
170
+ /** @returns {ColumnBuilder} */
171
+ bigInteger() {
172
+ return this.#col({ type: 'bigInteger' })
173
+ }
174
+ /** @returns {ColumnBuilder} */
175
+ smallInteger() {
176
+ return this.#col({ type: 'smallInteger' })
177
+ }
178
+
179
+ // ── 부동소수/정밀수 ──────────────────────────────────────────────────────
180
+ /** @returns {ColumnBuilder} */
181
+ real() {
182
+ return this.#col({ type: 'real' })
183
+ }
184
+ /** @returns {ColumnBuilder} */
185
+ doublePrecision() {
186
+ return this.#col({ type: 'doublePrecision' })
187
+ }
188
+ /**
189
+ * 고정 정밀 십진수 (postgres NUMERIC).
190
+ * @param {number} precision @param {number} scale @returns {ColumnBuilder}
191
+ */
192
+ decimal(precision, scale) {
193
+ return this.#col({ type: 'decimal', precision, scale })
194
+ }
195
+
196
+ // ── 문자열 ────────────────────────────────────────────────────────────────
197
+ /** @param {number} maxLength @returns {ColumnBuilder} */
198
+ varchar(maxLength) {
199
+ return this.#col({ type: 'varchar', length: maxLength })
200
+ }
201
+ /** @returns {ColumnBuilder} */
202
+ text() {
203
+ return this.#col({ type: 'text' })
204
+ }
205
+ /** @param {number} length @returns {ColumnBuilder} */
206
+ char(length) {
207
+ return this.#col({ type: 'char', length })
208
+ }
209
+
210
+ // ── 논리/시간/기타 ────────────────────────────────────────────────────────
211
+ /** @returns {ColumnBuilder} */
212
+ boolean() {
213
+ return this.#col({ type: 'boolean' })
214
+ }
215
+ /** @returns {ColumnBuilder} */
216
+ timestamp() {
217
+ return this.#col({ type: 'timestamp' })
218
+ }
219
+ /** @returns {ColumnBuilder} */
220
+ timestamptz() {
221
+ return this.#col({ type: 'timestamptz' })
222
+ }
223
+ /** @returns {ColumnBuilder} */
224
+ date() {
225
+ return this.#col({ type: 'date' })
226
+ }
227
+ /** @returns {ColumnBuilder} */
228
+ time() {
229
+ return this.#col({ type: 'time' })
230
+ }
231
+ /** @returns {ColumnBuilder} */
232
+ uuid() {
233
+ return this.#col({ type: 'uuid' })
234
+ }
235
+ /** @returns {ColumnBuilder} */
236
+ json() {
237
+ return this.#col({ type: 'json' })
238
+ }
239
+ /** @returns {ColumnBuilder} */
240
+ jsonb() {
241
+ return this.#col({ type: 'jsonb' })
242
+ }
243
+ /** @returns {ColumnBuilder} */
244
+ bytea() {
245
+ return this.#col({ type: 'bytea' })
246
+ }
247
+ /**
248
+ * MongoDB ObjectId (mongo 전용 — SQL dialect 는 렌더 시점에 거부). `_id` 는 생략해도 자동이며,
249
+ * 명시 선언은 `_id: t.objectId().primary()` 형태만 허용된다(ADR-209).
250
+ * @returns {ColumnBuilder}
251
+ */
252
+ objectId() {
253
+ return this.#col({ type: 'objectId' })
254
+ }
255
+
256
+ /**
257
+ * 중첩 도큐먼트(embedded object — mongo 전용). shape 미지정 시 자유형 object.
258
+ * 중첩 필드의 제약은 타입·길이·enum·notNull(→required)만 — unique/primary/references/check 는
259
+ * 최상위 전용이다(mongo dialect 가 렌더 시점에 거부).
260
+ * @param {Record<string, ColumnBuilder>} [shape] - { 필드명: t.<type>()… }.
261
+ * @returns {ColumnBuilder}
262
+ * @throws {MegaConfigError} shape 값이 빌더가 아닐 때.
263
+ */
264
+ object(shape) {
265
+ if (shape === undefined) return this.#col({ type: 'object' })
266
+ /** @type {Record<string, any>} */
267
+ const built = {}
268
+ for (const [key, builder] of Object.entries(shape)) {
269
+ if (!(builder instanceof ColumnBuilder)) {
270
+ throw new MegaConfigError('migration.schema_invalid', `t.object: 필드 '${key}' 가 빌더(t.<type>()…)가 아닙니다.`, { details: { field: key } })
271
+ }
272
+ built[key] = builder.build()
273
+ }
274
+ return this.#col({ type: 'object', shape: built })
275
+ }
276
+
277
+ /**
278
+ * 배열(mongo 전용). items 미지정 시 자유형 배열.
279
+ * @param {ColumnBuilder} [items] - 요소 타입(t.<type>()…).
280
+ * @param {{ uniqueItems?: boolean }} [opts]
281
+ * @returns {ColumnBuilder}
282
+ * @throws {MegaConfigError} items 가 빌더가 아닐 때.
283
+ */
284
+ array(items, opts = {}) {
285
+ /** @type {Record<string, any>} */
286
+ const def = { type: 'array' }
287
+ if (items !== undefined) {
288
+ if (!(items instanceof ColumnBuilder)) {
289
+ throw new MegaConfigError('migration.schema_invalid', `t.array: items 는 빌더(t.<type>()…)여야 합니다.`, { details: {} })
290
+ }
291
+ def.items = items.build()
292
+ }
293
+ if (opts.uniqueItems === true) def.uniqueItems = true
294
+ return this.#col(def)
295
+ }
296
+
297
+ /**
298
+ * 열거형 — postgres 렌더는 `TEXT` + `CHECK (col IN (...))` (native enum 타입 미사용 —
299
+ * 값 추가/삭제가 제약 교체로 단순해짐).
300
+ * @param {string[]} values @param {{ name?: string }} [opts] - name: CHECK 제약 이름 override.
301
+ * @returns {ColumnBuilder}
302
+ */
303
+ enum(values, opts) {
304
+ /** @type {Record<string, any>} */
305
+ const def = { type: 'enum', values: Array.isArray(values) ? [...values] : values }
306
+ if (opts?.name !== undefined) def.enumName = opts.name
307
+ return this.#col(def)
308
+ }
309
+
310
+ /**
311
+ * 복합 PRIMARY KEY 선언 — schema 콜백 안에서 `t.primary(['a','b'])` 로 호출(반환값 없음,
312
+ * 컬럼 맵에는 넣지 않는다). 단일 PK 는 컬럼 체인 `.primary()` 사용.
313
+ * @param {string[]} columns
314
+ * @returns {void}
315
+ */
316
+ primary(columns) {
317
+ this.compositePrimaryKey = Array.isArray(columns) ? [...columns] : columns
318
+ }
319
+
320
+ /**
321
+ * 인덱스 정의 — `static indexes = (t) => [t.index(...)]` 에서 사용.
322
+ * @param {string | string[] | { expression: string }} columnsOrExpr - 컬럼(들) 또는 표현식 인덱스.
323
+ * @param {{ name?: string, unique?: boolean, where?: string, using?: string }} [opts]
324
+ * @returns {Record<string, any>} IndexDef.
325
+ */
326
+ index(columnsOrExpr, opts = {}) {
327
+ /** @type {Record<string, any>} */
328
+ const def = {}
329
+ if (typeof columnsOrExpr === 'object' && !Array.isArray(columnsOrExpr) && columnsOrExpr !== null) {
330
+ def.expression = columnsOrExpr.expression
331
+ } else {
332
+ def.columns = Array.isArray(columnsOrExpr) ? [...columnsOrExpr] : [columnsOrExpr]
333
+ }
334
+ if (opts.name !== undefined) def.name = opts.name
335
+ if (opts.unique === true) def.unique = true
336
+ if (opts.where !== undefined) def.where = opts.where
337
+ if (opts.using !== undefined) def.using = opts.using
338
+ return def
339
+ }
340
+ }
341
+
342
+ /**
343
+ * 모델 클래스(또는 동형 객체)의 `static schema`/`static indexes` 를 실행해 record 를 만든다.
344
+ * 모델 식별은 duck-typing — `schema` 함수 + `table`/`adapter` 문자열(클래스 identity 비의존,
345
+ * 패키지 사본이 달라도 동작). 검증은 {@link module:core/migration/schema-validator} 가 별도 수행.
346
+ *
347
+ * @param {{ name?: string, table?: string, adapter?: string, schema?: Function, indexes?: Function }} Model
348
+ * @returns {{ table: string, adapter: string, columns: Record<string, any>, primaryKey?: string[], indexes: any[], validation?: any }}
349
+ * @throws {MegaConfigError} `migration.schema_invalid` - table/adapter 누락, schema 반환 형태 오류.
350
+ */
351
+ export function buildModelRecord(Model) {
352
+ const modelName = Model?.name || '(anonymous)'
353
+ if (typeof Model?.table !== 'string' || Model.table.length === 0) {
354
+ throw new MegaConfigError('migration.schema_invalid', `model '${modelName}': static table 이 비어 있습니다 — schema 모델은 table 필수.`, {
355
+ details: { model: modelName },
356
+ })
357
+ }
358
+ if (typeof Model?.adapter !== 'string' || Model.adapter.length === 0) {
359
+ throw new MegaConfigError('migration.schema_invalid', `model '${modelName}': static adapter 가 비어 있습니다 — schema 모델은 adapter 필수.`, {
360
+ details: { model: modelName },
361
+ })
362
+ }
363
+ const t = new SchemaBuilder()
364
+ const cols = /** @type {Function} */ (Model.schema)(t)
365
+ if (cols === null || typeof cols !== 'object' || Array.isArray(cols)) {
366
+ throw new MegaConfigError(
367
+ 'migration.schema_invalid',
368
+ `model '${modelName}': static schema 는 { 컬럼명: t.<type>()… } 플레인 객체를 반환해야 합니다.`,
369
+ { details: { model: modelName, returned: Array.isArray(cols) ? 'array' : typeof cols } },
370
+ )
371
+ }
372
+ /** @type {Record<string, any>} */
373
+ const columns = {}
374
+ for (const [name, builder] of Object.entries(cols)) {
375
+ if (!(builder instanceof ColumnBuilder)) {
376
+ throw new MegaConfigError(
377
+ 'migration.schema_invalid',
378
+ `model '${modelName}': 컬럼 '${name}' 이 빌더(t.<type>()…)가 아닙니다 — schema 콜백의 t 메서드로 정의하세요.`,
379
+ { details: { model: modelName, column: name } },
380
+ )
381
+ }
382
+ columns[name] = builder.build()
383
+ }
384
+ /** @type {{ table: string, adapter: string, columns: Record<string, any>, primaryKey?: string[], indexes: any[], validation?: any }} */
385
+ const record = { table: Model.table, adapter: Model.adapter, columns, indexes: [] }
386
+ if (t.compositePrimaryKey !== undefined) record.primaryKey = t.compositePrimaryKey
387
+ // mongo 전용 — collection validator 옵션(static validation = { level, action }). 값 검증은
388
+ // mongo dialect 렌더가 수행하고, SQL dialect 는 record 의 이 필드를 읽지 않는다(ADR-209).
389
+ if (/** @type {any} */ (Model).validation !== undefined) record.validation = /** @type {any} */ (Model).validation
390
+ if (typeof Model.indexes === 'function') {
391
+ const defs = Model.indexes(t)
392
+ if (!Array.isArray(defs)) {
393
+ throw new MegaConfigError('migration.schema_invalid', `model '${modelName}': static indexes 는 [t.index(...)] 배열을 반환해야 합니다.`, {
394
+ details: { model: modelName, returned: typeof defs },
395
+ })
396
+ }
397
+ record.indexes = defs
398
+ }
399
+ return record
400
+ }