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.
- package/bin/mega-ws-hub.js +2 -2
- package/package.json +32 -8
- package/sample/crud/.env +156 -8
- package/sample/crud/.env.example +153 -28
- 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/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 +63 -3
- package/sample/crud/package.json +3 -3
- package/sample/crud/scripts/start-ws-hub.sh +2 -2
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +30 -0
- package/src/adapters/maria-adapter.js +26 -3
- package/src/adapters/mega-db-adapter.js +7 -1
- package/src/adapters/mongo-adapter.js +19 -1
- package/src/adapters/postgres-adapter.js +25 -2
- package/src/adapters/sqlite-adapter.js +20 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +137 -33
- package/src/cli/generators/index.js +82 -2
- package/src/cli/index.js +478 -104
- package/src/core/ajv-mapper.js +27 -2
- package/src/core/boot.js +485 -237
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/config-validator.js +25 -0
- package/src/core/ctx-builder.js +6 -2
- package/src/core/envelope.js +112 -12
- package/src/core/hub-link.js +65 -4
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +6 -2
- package/src/core/mega-app.js +223 -481
- package/src/core/mega-cluster.js +54 -13
- 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 +129 -0
- package/src/core/router.js +70 -65
- package/src/core/scope-registry.js +0 -1
- package/src/core/security.js +67 -9
- 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 +624 -0
- package/src/core/ws-roster.js +4 -1
- package/src/core/ws-upgrade.js +118 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-health.js +25 -4
- package/src/lib/mega-job-queue.js +98 -21
- package/src/lib/mega-job.js +29 -0
- package/src/lib/mega-logger.js +1 -1
- package/src/lib/mega-metrics.js +3 -12
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +162 -49
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +5 -1
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +51 -8
- 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/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 +91 -0
- package/types/adapters/file-adapter.d.ts +94 -0
- package/types/adapters/file-session-adapter.d.ts +101 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +115 -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 +47 -0
- package/types/adapters/mega-db-adapter.d.ts +47 -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 +139 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +139 -0
- package/types/adapters/redis-adapter.d.ts +70 -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 +106 -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 +112 -0
- package/types/cli/index.d.ts +249 -0
- package/types/cli/template-engine.d.ts +40 -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 +80 -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 +264 -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 +92 -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 +18 -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 +96 -0
- package/types/core/ws-upgrade.d.ts +231 -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 +241 -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 +41 -0
- package/types/lib/mega-job-queue.d.ts +176 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +138 -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 +127 -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 +234 -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
|
@@ -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
|
+
}
|