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.
- package/bin/mega-ws-hub.js +2 -2
- package/package.json +32 -8
- package/sample/crud/.env +1 -1
- package/sample/crud/.env.example +1 -1
- package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
- package/sample/crud/.mega/journal/snapshot.json +261 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
- package/sample/crud/apps/main/controllers/web-controller.js +7 -5
- package/sample/crud/apps/main/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 +3 -2
- package/sample/crud/package.json +1 -1
- 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 +353 -100
- package/src/core/ajv-mapper.js +27 -2
- package/src/core/boot.js +464 -245
- package/src/core/cluster-metrics.js +13 -4
- 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 +201 -463
- package/src/core/mega-cluster.js +4 -1
- package/src/core/mega-server.js +40 -9
- package/src/core/migration/dialect-registry.js +107 -0
- package/src/core/migration/dialects/README.md +62 -0
- package/src/core/migration/dialects/maria.js +496 -0
- package/src/core/migration/dialects/mongo.js +824 -0
- package/src/core/migration/dialects/postgres.js +563 -0
- package/src/core/migration/dialects/sqlite.js +476 -0
- package/src/core/migration/differ.js +456 -0
- package/src/core/migration/generate.js +508 -0
- package/src/core/migration/journal.js +167 -0
- package/src/core/migration/model-scan.js +84 -0
- package/src/core/migration/mongo-migration-db.js +97 -0
- package/src/core/migration/schema-builder.js +400 -0
- package/src/core/migration/schema-validator.js +315 -0
- package/src/core/migration-lock.js +205 -0
- package/src/core/migration-runner.js +166 -38
- package/src/core/multipart.js +28 -5
- package/src/core/pipeline.js +129 -0
- package/src/core/router.js +70 -65
- 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-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 +114 -39
- 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,824 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* mongodb dialect (ADR-209) — 빌더 record 를 SQL 이 아니라 **mongo command JS 문**으로 렌더한다
|
|
4
|
+
* (`usesSqlDdl: false` — 생성 파일의 `db` 는 mongodb `Db` 인스턴스).
|
|
5
|
+
*
|
|
6
|
+
* SQL dialect 와의 핵심 차이:
|
|
7
|
+
* - **스키마 = collection validator(`$jsonSchema`)** — 컬럼 단위 ALTER 가 없고 validator 를
|
|
8
|
+
* 통째로 교체한다(`collMod`). 그래서 컬럼 수준 변경은 differ 가 `rebuildTable` op 1개로
|
|
9
|
+
* 수렴하고(`dependsOnRebuild` + `rebuildTriggerKinds` 확장 — 테이블 재생성이 아니라 validator
|
|
10
|
+
* 교체로 렌더), 인덱스/UNIQUE 변경만 개별 op 로 남는다.
|
|
11
|
+
* - **validator 는 기존 도큐먼트를 변환하지 않는다** — 엄격해진 스키마는 이후 insert/update
|
|
12
|
+
* 시점에 거부될 뿐이다. backfill(`updateMany`)은 자동 생성 범위 밖(사용자 raw 편집 — 생성
|
|
13
|
+
* 파일에 경고 주석 동반).
|
|
14
|
+
* - **rename 은 `$rename` updateMany 동반** — validator 의 속성명만 바꾸면 기존 도큐먼트는 옛
|
|
15
|
+
* 필드명으로 남아 strict 검증과 어긋난다. rename 은 사용자가 명시 확정한 의도이므로 데이터
|
|
16
|
+
* 이동까지 생성한다(SQL dialect 의 RENAME COLUMN 데이터 보존 의미 정합). 순서는 **collMod
|
|
17
|
+
* 먼저, $rename 다음** — strict 검증은 update 결과를 "현재" validator 로 평가하므로 교체 전
|
|
18
|
+
* $rename 은 옛 스키마의 additionalProperties: false 에 걸린다(실측).
|
|
19
|
+
* - **idempotent 렌더(ADR-210)** — no-tx 다단의 부분 실패 후 **같은 파일 재실행이 1차 복구
|
|
20
|
+
* 수단**(ADR-208 M-1 원칙)이다: dropIndex/dropCollection 은 not-found(27/26) 무해 통과,
|
|
21
|
+
* createCollection 은 exists(48) 시 validator collMod 수렴, createIndex 는 동일 스펙 재실행이
|
|
22
|
+
* 서버 자연 멱등(스펙 충돌 85/86 은 진짜 결함이라 그대로 실패), collMod/$rename 은 자연 멱등.
|
|
23
|
+
* - **트랜잭션 없음** — `collMod`/`create`/`drop`/인덱스 DDL 은 multi-document 트랜잭션 안에서
|
|
24
|
+
* 실행할 수 없고(공식 문서 — 트랜잭션은 CRUD 중심, 4.4+ 의 create 예외도 제한적), standalone
|
|
25
|
+
* 은 트랜잭션 자체가 없다. 생성 파일은 항상 `export const transaction = false`.
|
|
26
|
+
* - **명시 거부(escape hatch 안내)**: `.references()`(FK 개념 없음), `.check()`(cross-field 는
|
|
27
|
+
* `$jsonSchema` 범위 밖 — raw 에서 `$expr` validator), `.default()`(`$jsonSchema` 는 default
|
|
28
|
+
* 미지원 — 앱 레벨), serial/bigSerial(`_id: ObjectId` 사용), `_id` 외 `.primary()`/복합 PK,
|
|
29
|
+
* SQL `where` 인덱스(JSON partialFilterExpression 문자열만 허용), 표현식 인덱스.
|
|
30
|
+
*
|
|
31
|
+
* bsonType 매핑 한계(정직 명시): smallInteger→int(구분 없음), time→string(native 없음),
|
|
32
|
+
* uuid→string(`crypto.randomUUID()` 정합 — binData(subtype 4) UUID 는 raw 마이그레이션),
|
|
33
|
+
* timestamp/timestamptz/date→date(시간대 개념 없음 — UTC Date 단일), json/jsonb→object.
|
|
34
|
+
*
|
|
35
|
+
* @module core/migration/dialects/mongo
|
|
36
|
+
*/
|
|
37
|
+
import { MegaConfigError } from '../../../errors/config-error.js'
|
|
38
|
+
|
|
39
|
+
// ── 능력/한계 속성 (dialects/README.md contract) ────────────────────────────
|
|
40
|
+
/** mongo namespace 한도(db 포함 120byte)에서 db 명을 제외한 보수적 collection/필드 한도. */
|
|
41
|
+
export const identifierMaxBytes = 64
|
|
42
|
+
/** mongo 4.2+ 인덱스 빌드는 항상 online(hybrid build) — --concurrent 는 의미 변화 없이 허용. */
|
|
43
|
+
export const supportsConcurrentIndex = true
|
|
44
|
+
/** enum 은 `$jsonSchema` 의 enum 키워드 — CHECK 도 native enum 타입도 아니다. */
|
|
45
|
+
export const enumStrategy = 'jsonschema-enum'
|
|
46
|
+
/** renameCollection 은 트랜잭션 안에서 불가(공식 문서). */
|
|
47
|
+
export const supportsRenameInTx = false
|
|
48
|
+
/** validator 교체(collMod)는 즉시 적용 — 단 본 dialect 는 트랜잭션 자체를 쓰지 않는다. */
|
|
49
|
+
export const canDropColumnInTx = false
|
|
50
|
+
/** 컬럼 수준 변경을 rebuildTable(=validator 교체) 1 op 로 수렴한다. */
|
|
51
|
+
export const dependsOnRebuild = true
|
|
52
|
+
/** constraint 개념이 없다. */
|
|
53
|
+
export const supportsRenameConstraint = false
|
|
54
|
+
/** FK 자체가 없다 — differ 의 FK 분리/동기화 경로 전부 비활성. */
|
|
55
|
+
export const supportsAlterAddFk = false
|
|
56
|
+
/** FK 가 없으니 해당 없음. */
|
|
57
|
+
export const requiresDropFkBeforeDropColumn = false
|
|
58
|
+
/** SQL 이 아니라 mongo command JS 문을 렌더한다 — generate 가 파일 템플릿을 분기한다. */
|
|
59
|
+
export const usesSqlDdl = false
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* differ 의 수렴 트리거 확장 — mongo 는 validator 가 통짜라 **모든 컬럼 수준 변경**이 수렴 대상이다
|
|
63
|
+
* (sqlite 기본 집합 + addColumn/renameColumn/setComment). 인덱스/UNIQUE 변경은 collMod 와 무관한
|
|
64
|
+
* 개별 op 로 남는다.
|
|
65
|
+
*/
|
|
66
|
+
export const rebuildTriggerKinds = new Set([
|
|
67
|
+
'alterType', 'setNotNull', 'dropNotNull', 'setDefault', 'dropDefault',
|
|
68
|
+
'addCheck', 'dropCheck', 'addFk', 'dropFk', 'renameFk', 'addPk', 'dropPk', 'changePk',
|
|
69
|
+
'dropColumn', 'addColumn', 'renameColumn', 'setComment',
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* collection/필드 이름 검증 — mongo 는 SQL 인용이 없어서 quoteIdent 는 검증 깔때기 + 원형 반환.
|
|
74
|
+
* `$` 시작(연산자 충돌), `.`(경로 구분자), `system.` prefix(예약), 빈 문자열, NUL 을 거부한다.
|
|
75
|
+
* @param {string} name @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function quoteIdent(name) {
|
|
78
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
79
|
+
throw new MegaConfigError('migration.schema_invalid', `mongo dialect: 빈 식별자는 사용할 수 없습니다.`, { details: { identifier: name } })
|
|
80
|
+
}
|
|
81
|
+
if (Buffer.byteLength(name, 'utf8') > identifierMaxBytes) {
|
|
82
|
+
throw new MegaConfigError(
|
|
83
|
+
'migration.identifier_too_long',
|
|
84
|
+
`식별자 '${name}' 가 mongo 권장 한도(${identifierMaxBytes}byte)를 초과합니다 — 이름을 줄이세요.`,
|
|
85
|
+
{ details: { identifier: name, bytes: Buffer.byteLength(name, 'utf8'), max: identifierMaxBytes } },
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
if (name.startsWith('$') || name.includes('.') || name.includes('\0') || name.startsWith('system.')) {
|
|
89
|
+
throw new MegaConfigError(
|
|
90
|
+
'migration.schema_invalid',
|
|
91
|
+
`mongo dialect: 식별자 '${name}' 는 사용할 수 없습니다 — '$' 시작·'.' 포함·NUL·'system.' prefix 는 mongo 네임스페이스 규칙 위반입니다.`,
|
|
92
|
+
{ details: { identifier: name } },
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return name
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** JS 코드에 박는 값 literal — JSON 직렬화(따옴표·escape 안전). @param {unknown} value @returns {string} */
|
|
99
|
+
export function quoteLiteral(value) {
|
|
100
|
+
return JSON.stringify(value)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* contract 형식 충족용 — mongo 에는 CHECK 가 없다. enum 은 `$jsonSchema` 의 enum 키워드로
|
|
105
|
+
* 렌더되므로 이 함수가 정상 흐름에서 호출되면 통합 결함이다(silent 우회 금지).
|
|
106
|
+
* @param {string} col @param {string[]} values @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
export function enumCheckExpr(col, values) {
|
|
109
|
+
throw new MegaConfigError(
|
|
110
|
+
'migration.dialect_feature_unsupported',
|
|
111
|
+
`mongo dialect: CHECK 식이 없습니다 — enum 은 $jsonSchema 의 enum 으로 렌더됩니다 (col '${col}').`,
|
|
112
|
+
{ details: { col, values } },
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── 명명 표준 (SQL dialect 와 동일 규칙 — diff 식별자 안정성) ────────────────
|
|
117
|
+
/** @param {string} table @param {string[]} cols @param {boolean} [unique] @returns {string} */
|
|
118
|
+
export function indexName(table, cols, unique = false) {
|
|
119
|
+
// dot-path(중첩 필드) 인덱스는 이름 합성 시 '.' 를 '_' 로 — 인덱스 이름의 네임스페이스 혼동 방지.
|
|
120
|
+
return `${unique ? 'uniq' : 'idx'}_${table}_${cols.map((c) => parseIndexColumn(c).field.replaceAll('.', '_')).join('_')}`
|
|
121
|
+
}
|
|
122
|
+
/** @param {string} table @param {Record<string, any>} ix @returns {string} */
|
|
123
|
+
export function resolveIndexName(table, ix) {
|
|
124
|
+
if (ix.name !== undefined) return ix.name
|
|
125
|
+
if (ix.expression !== undefined) {
|
|
126
|
+
throw new MegaConfigError(
|
|
127
|
+
'migration.dialect_feature_unsupported',
|
|
128
|
+
`mongo dialect: 표현식 인덱스를 지원하지 않습니다 (table '${table}') — wildcard/계산 필드 인덱스는 raw 마이그레이션(createIndex)으로 작성하세요.`,
|
|
129
|
+
{ details: { table, expression: ix.expression } },
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
return indexName(table, ix.columns, ix.unique === true)
|
|
133
|
+
}
|
|
134
|
+
/** @param {string} table @param {string} col @returns {string} */
|
|
135
|
+
export function uniqueName(table, col) {
|
|
136
|
+
return `uniq_${table}_${col}`
|
|
137
|
+
}
|
|
138
|
+
/** @param {string} table @param {string} col @param {string} refTable @returns {string} */
|
|
139
|
+
export function fkName(table, col, refTable) {
|
|
140
|
+
return `fk_${table}_${col}_${refTable}`
|
|
141
|
+
}
|
|
142
|
+
/** @param {string} table @param {string} col @returns {string} */
|
|
143
|
+
export function checkName(table, col) {
|
|
144
|
+
return `chk_${table}_${col}`
|
|
145
|
+
}
|
|
146
|
+
/** mongo PK 는 _id 단일 — diff 정규화용 합성 명칭. @param {string} table @returns {string} */
|
|
147
|
+
export function pkName(table) {
|
|
148
|
+
return `pk_${table}`
|
|
149
|
+
}
|
|
150
|
+
/** @param {string} table @returns {string} */
|
|
151
|
+
export function inlinePkName(table) {
|
|
152
|
+
return `pk_${table}`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── $jsonSchema 합성 ─────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* mongo 에서 표현 불가한 def 속성의 명시 거부 — silent 누락으로 "선언했는데 반영 안 됨" 을 막는다.
|
|
159
|
+
* @param {string} table @param {string} col @param {Record<string, any>} def
|
|
160
|
+
*/
|
|
161
|
+
function assertSupportedDef(table, col, def) {
|
|
162
|
+
const where = { table, column: col }
|
|
163
|
+
if (def.type === 'serial' || def.type === 'bigSerial') {
|
|
164
|
+
throw new MegaConfigError(
|
|
165
|
+
'migration.dialect_feature_unsupported',
|
|
166
|
+
`mongo dialect: table '${table}' 컬럼 '${col}' — mongo 는 SERIAL(자동증가) 대신 ObjectId 를 사용합니다. ` +
|
|
167
|
+
`'_id: t.objectId().primary()'(생략 가능 — _id 는 자동) 로 선언하거나 필드를 제거하세요.`,
|
|
168
|
+
{ details: { ...where, type: def.type } },
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
if (def.references !== undefined) {
|
|
172
|
+
throw new MegaConfigError(
|
|
173
|
+
'migration.fk_not_supported',
|
|
174
|
+
`mongo dialect: table '${table}' 컬럼 '${col}' 의 .references() — mongo 에는 FK/참조 무결성이 없습니다. ` +
|
|
175
|
+
'참조 검증이 필요하면 앱 레벨(서비스 코드)에서 처리하고 schema 에서는 제거하세요.',
|
|
176
|
+
{ details: where },
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
if (def.check !== undefined) {
|
|
180
|
+
throw new MegaConfigError(
|
|
181
|
+
'migration.dialect_feature_unsupported',
|
|
182
|
+
`mongo dialect: table '${table}' 컬럼 '${col}' 의 .check() — cross-field/임의 식 검증은 $jsonSchema 범위 밖입니다. ` +
|
|
183
|
+
`생성 파일을 raw 편집해 collMod validator 에 $expr 를 추가하세요(예: { $and: [{ $jsonSchema: {…} }, { $expr: {…} }] }).`,
|
|
184
|
+
{ details: { ...where, expr: def.check.expr } },
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
if (def.default !== undefined) {
|
|
188
|
+
throw new MegaConfigError(
|
|
189
|
+
'migration.dialect_feature_unsupported',
|
|
190
|
+
`mongo dialect: table '${table}' 컬럼 '${col}' 의 .default() — $jsonSchema 는 default 를 지원하지 않습니다(검증 전용). ` +
|
|
191
|
+
'기본값은 도큐먼트 생성 시 앱 레벨에서 채우세요.',
|
|
192
|
+
{ details: { ...where, default: def.default } },
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
if (def.notNull === true && def.nullable === true) {
|
|
196
|
+
throw new MegaConfigError(
|
|
197
|
+
'migration.schema_invalid',
|
|
198
|
+
`mongo dialect: table '${table}' 컬럼 '${col}' — .notNull() 과 .nullable() 은 동시에 쓸 수 없습니다(모순).`,
|
|
199
|
+
{ details: where },
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
if (def.primary === true && col !== '_id') {
|
|
203
|
+
throw new MegaConfigError(
|
|
204
|
+
'migration.dialect_feature_unsupported',
|
|
205
|
+
`mongo dialect: table '${table}' 컬럼 '${col}' 의 .primary() — mongo PK 는 '_id' 뿐입니다. ` +
|
|
206
|
+
`'_id: t.objectId().primary()' 로 선언하거나(생략 시 자동 ObjectId), 고유성이 목적이면 .unique() 를 사용하세요.`,
|
|
207
|
+
{ details: where },
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 컬럼 def → `$jsonSchema` 속성 스키마. 중첩(object shape / array items)은 재귀.
|
|
214
|
+
* @param {string} table @param {string} col @param {Record<string, any>} def
|
|
215
|
+
* @returns {Record<string, any>}
|
|
216
|
+
*/
|
|
217
|
+
function propertySchema(table, col, def) {
|
|
218
|
+
assertSupportedDef(table, col, def)
|
|
219
|
+
/** @type {Record<string, any>} */
|
|
220
|
+
const s = {}
|
|
221
|
+
switch (def.type) {
|
|
222
|
+
case 'objectId':
|
|
223
|
+
s.bsonType = 'objectId'
|
|
224
|
+
break
|
|
225
|
+
case 'integer':
|
|
226
|
+
case 'smallInteger': // mongo 는 int32 단일 — small 구분 없음(모듈 docstring 한계 명시)
|
|
227
|
+
s.bsonType = 'int'
|
|
228
|
+
break
|
|
229
|
+
case 'bigInteger':
|
|
230
|
+
s.bsonType = 'long'
|
|
231
|
+
break
|
|
232
|
+
case 'real':
|
|
233
|
+
case 'doublePrecision':
|
|
234
|
+
s.bsonType = 'double'
|
|
235
|
+
break
|
|
236
|
+
case 'decimal':
|
|
237
|
+
s.bsonType = 'decimal' // Decimal128 — precision/scale 은 $jsonSchema 로 표현 불가(검증 밖)
|
|
238
|
+
break
|
|
239
|
+
case 'varchar':
|
|
240
|
+
s.bsonType = 'string'
|
|
241
|
+
s.maxLength = def.length
|
|
242
|
+
break
|
|
243
|
+
case 'char':
|
|
244
|
+
s.bsonType = 'string'
|
|
245
|
+
s.minLength = def.length
|
|
246
|
+
s.maxLength = def.length
|
|
247
|
+
break
|
|
248
|
+
case 'text':
|
|
249
|
+
s.bsonType = 'string'
|
|
250
|
+
break
|
|
251
|
+
case 'boolean':
|
|
252
|
+
s.bsonType = 'bool'
|
|
253
|
+
break
|
|
254
|
+
case 'timestamp':
|
|
255
|
+
case 'timestamptz':
|
|
256
|
+
case 'date':
|
|
257
|
+
s.bsonType = 'date'
|
|
258
|
+
break
|
|
259
|
+
case 'time':
|
|
260
|
+
s.bsonType = 'string' // mongo 에 time-of-day native 없음 — ISO 'HH:mm:ss' 문자열 규약
|
|
261
|
+
break
|
|
262
|
+
case 'uuid':
|
|
263
|
+
s.bsonType = 'string' // crypto.randomUUID() 문자열 정합 — binData(subtype 4)는 raw 마이그레이션
|
|
264
|
+
break
|
|
265
|
+
case 'json':
|
|
266
|
+
case 'jsonb':
|
|
267
|
+
s.bsonType = 'object'
|
|
268
|
+
break
|
|
269
|
+
case 'bytea':
|
|
270
|
+
s.bsonType = 'binData'
|
|
271
|
+
break
|
|
272
|
+
case 'enum':
|
|
273
|
+
s.enum = [...def.values]
|
|
274
|
+
break
|
|
275
|
+
case 'object': {
|
|
276
|
+
s.bsonType = 'object'
|
|
277
|
+
if (def.shape !== undefined) {
|
|
278
|
+
const nested = nestedObjectSchema(table, col, def.shape)
|
|
279
|
+
if (Object.keys(nested.properties).length > 0) s.properties = nested.properties
|
|
280
|
+
if (nested.required.length > 0) s.required = nested.required
|
|
281
|
+
s.additionalProperties = false
|
|
282
|
+
}
|
|
283
|
+
break
|
|
284
|
+
}
|
|
285
|
+
case 'array': {
|
|
286
|
+
s.bsonType = 'array'
|
|
287
|
+
if (def.items !== undefined) s.items = propertySchema(table, `${col}[]`, def.items)
|
|
288
|
+
if (def.uniqueItems === true) s.uniqueItems = true
|
|
289
|
+
break
|
|
290
|
+
}
|
|
291
|
+
default:
|
|
292
|
+
throw new MegaConfigError('migration.schema_invalid', `mongo dialect: 알 수 없는 타입 '${def.type}'.`, { details: { table, column: col, type: def.type } })
|
|
293
|
+
}
|
|
294
|
+
if (typeof def.comment === 'string') s.description = def.comment
|
|
295
|
+
// .nullable() — SQL 의 "NULL 저장 가능" 의미를 mongo 로 번역: 필드 생략뿐 아니라 **명시 null** 도
|
|
296
|
+
// 허용한다(bsonType 유니온 / enum 에 null 추가). 미선언 시 mongo 는 필드 생략만 허용(실측 121).
|
|
297
|
+
if (def.nullable === true) {
|
|
298
|
+
if (s.bsonType !== undefined) s.bsonType = [s.bsonType, 'null']
|
|
299
|
+
if (s.enum !== undefined) s.enum = [...s.enum, null]
|
|
300
|
+
}
|
|
301
|
+
return s
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 중첩 object shape → { properties, required }. 중첩 단계에서는 notNull → required 만 제약으로
|
|
306
|
+
* 인정한다(unique/primary/references/check 는 최상위 전용 — assertSupportedDef 가 거부).
|
|
307
|
+
* @param {string} table @param {string} parentCol @param {Record<string, any>} shape
|
|
308
|
+
* @returns {{ properties: Record<string, any>, required: string[] }}
|
|
309
|
+
*/
|
|
310
|
+
function nestedObjectSchema(table, parentCol, shape) {
|
|
311
|
+
/** @type {Record<string, any>} */
|
|
312
|
+
const properties = {}
|
|
313
|
+
/** @type {string[]} */
|
|
314
|
+
const required = []
|
|
315
|
+
for (const [key, def] of Object.entries(shape)) {
|
|
316
|
+
quoteIdent(key)
|
|
317
|
+
properties[key] = propertySchema(table, `${parentCol}.${key}`, def)
|
|
318
|
+
if (def.notNull === true) required.push(key)
|
|
319
|
+
}
|
|
320
|
+
return { properties, required }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* record → collection `$jsonSchema`. `additionalProperties: false`(strict) 라 `_id` 를 항상
|
|
325
|
+
* properties 에 포함한다(_id 는 모든 도큐먼트에 존재 — 누락 시 insert 전부 거부되는 함정).
|
|
326
|
+
* @param {Record<string, any>} record
|
|
327
|
+
* @returns {Record<string, any>}
|
|
328
|
+
*/
|
|
329
|
+
export function jsonSchemaOf(record) {
|
|
330
|
+
const table = record.table
|
|
331
|
+
if (record.primaryKey !== undefined) {
|
|
332
|
+
throw new MegaConfigError(
|
|
333
|
+
'migration.dialect_feature_unsupported',
|
|
334
|
+
`mongo dialect: table '${table}' 의 t.primary([...]) — mongo 는 복합 PK 를 지원하지 않습니다(_id 단일). ` +
|
|
335
|
+
'복합 고유성이 목적이면 t.index([...], { unique: true }) 를 사용하세요.',
|
|
336
|
+
{ details: { table, primaryKey: record.primaryKey } },
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
/** @type {Record<string, any>} */
|
|
340
|
+
const properties = {}
|
|
341
|
+
/** @type {string[]} */
|
|
342
|
+
const required = []
|
|
343
|
+
if (record.columns._id === undefined) properties._id = { bsonType: 'objectId' }
|
|
344
|
+
for (const [col, def] of Object.entries(record.columns)) {
|
|
345
|
+
quoteIdent(col)
|
|
346
|
+
properties[col] = propertySchema(table, col, /** @type {any} */ (def))
|
|
347
|
+
if (/** @type {any} */ (def).notNull === true) required.push(col)
|
|
348
|
+
}
|
|
349
|
+
/** @type {Record<string, any>} */
|
|
350
|
+
const schema = { bsonType: 'object' }
|
|
351
|
+
if (required.length > 0) schema.required = required
|
|
352
|
+
schema.properties = properties
|
|
353
|
+
schema.additionalProperties = false
|
|
354
|
+
return schema
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** record 의 validation 옵션(모델 static validation) 정규화 — 미지정 시 strict/error. */
|
|
358
|
+
const VALIDATION_LEVELS = ['strict', 'moderate']
|
|
359
|
+
const VALIDATION_ACTIONS = ['error', 'warn']
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @param {Record<string, any>} record
|
|
363
|
+
* @returns {{ validationLevel: string, validationAction: string }}
|
|
364
|
+
*/
|
|
365
|
+
function validationOptionsOf(record) {
|
|
366
|
+
const v = record.validation ?? {}
|
|
367
|
+
const level = v.level ?? 'strict'
|
|
368
|
+
const action = v.action ?? 'error'
|
|
369
|
+
if (!VALIDATION_LEVELS.includes(level) || !VALIDATION_ACTIONS.includes(action)) {
|
|
370
|
+
throw new MegaConfigError(
|
|
371
|
+
'migration.schema_invalid',
|
|
372
|
+
`mongo dialect: table '${record.table}' 의 static validation — level 은 [${VALIDATION_LEVELS.join('|')}], action 은 [${VALIDATION_ACTIONS.join('|')}] 만 허용됩니다.`,
|
|
373
|
+
{ details: { table: record.table, validation: v } },
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
return { validationLevel: level, validationAction: action }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── JS 문 렌더 ───────────────────────────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 값 → JS 코드 객체 literal (2-space, 키 순서 = 삽입 순서 보존). JSON.stringify 기반이라
|
|
383
|
+
* 결정적 출력 — 생성 파일 diff 노이즈 없음.
|
|
384
|
+
* @param {unknown} obj @param {string} indent - 후속 행 들여쓰기.
|
|
385
|
+
* @returns {string}
|
|
386
|
+
*/
|
|
387
|
+
function js(obj, indent) {
|
|
388
|
+
return JSON.stringify(obj, null, 2).split('\n').join(`\n${indent}`)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** @param {string} table @returns {string} `db.collection('t')` 접근 코드. */
|
|
392
|
+
function coll(table) {
|
|
393
|
+
return `db.collection(${quoteLiteral(quoteIdent(table))})`
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 인덱스 컬럼 항목 파싱 — `'field'`(asc) | `'field:desc'`. 필드 존재 검증은 schema-validator 가
|
|
398
|
+
* suffix 제거 후 수행한다.
|
|
399
|
+
* @param {string} entry @returns {{ field: string, dir: 1 | -1 }}
|
|
400
|
+
*/
|
|
401
|
+
export function parseIndexColumn(entry) {
|
|
402
|
+
if (entry.endsWith(':desc')) return { field: entry.slice(0, -5), dir: -1 }
|
|
403
|
+
if (entry.endsWith(':asc')) return { field: entry.slice(0, -4), dir: 1 }
|
|
404
|
+
return { field: entry, dir: 1 }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** mongo 인덱스 타입으로 허용하는 using 값 (그 외 SQL 의 USING 은 미지원 거부). */
|
|
408
|
+
const INDEX_USING = ['text', '2dsphere', 'hashed']
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* partialFilterExpression 이 허용하는 연산자(공식 문서의 닫힌 목록 — MongoDB 6.0+ 에서 $in/$or 포함).
|
|
412
|
+
* 그 외($ne/$nin/$not/$regex/$expr …)는 서버가 createIndex 시점에 거부한다 — generate 가 미리 막지
|
|
413
|
+
* 않으면 직전 dropIndex 가 이미 실행돼 기존 인덱스 유실 + 부분 적용으로 증폭된다(audit M-3 실측).
|
|
414
|
+
*/
|
|
415
|
+
const PARTIAL_FILTER_OPS = new Set(['$eq', '$gt', '$gte', '$lt', '$lte', '$in', '$exists', '$type', '$and', '$or'])
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* partialFilterExpression 재귀 검증 — 화이트리스트 밖 연산자·$exists:false 를 generate 시점에 거부.
|
|
419
|
+
* @param {string} table @param {unknown} expr @param {string} [path]
|
|
420
|
+
*/
|
|
421
|
+
function assertPartialFilter(table, expr, path = 'partialFilterExpression') {
|
|
422
|
+
if (expr === null || typeof expr !== 'object' || Array.isArray(expr)) {
|
|
423
|
+
throw new MegaConfigError(
|
|
424
|
+
'migration.dialect_feature_unsupported',
|
|
425
|
+
`mongo dialect: 부분 인덱스 식 '${path}' 는 객체여야 합니다 (table '${table}').`,
|
|
426
|
+
{ details: { table, path } },
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
for (const [key, value] of Object.entries(expr)) {
|
|
430
|
+
if (key.startsWith('$')) {
|
|
431
|
+
if (!PARTIAL_FILTER_OPS.has(key)) {
|
|
432
|
+
throw new MegaConfigError(
|
|
433
|
+
'migration.dialect_feature_unsupported',
|
|
434
|
+
`mongo dialect: partialFilterExpression 의 '${key}' 는 mongo 가 부분 인덱스에서 지원하지 않습니다 — ` +
|
|
435
|
+
`허용: [${[...PARTIAL_FILTER_OPS].join(', ')}] (공식 문서의 닫힌 목록, table '${table}', 위치: ${path}). ` +
|
|
436
|
+
'다른 조건이 필요하면 raw 마이그레이션 대신 조건을 지원 연산자로 재표현하세요(예: $ne → $in 의 여집합 불가 — 설계 재고).',
|
|
437
|
+
{ details: { table, operator: key, path } },
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
if (key === '$and' || key === '$or') {
|
|
441
|
+
if (!Array.isArray(value)) {
|
|
442
|
+
throw new MegaConfigError('migration.dialect_feature_unsupported', `mongo dialect: '${key}' 는 배열이어야 합니다 (table '${table}', 위치: ${path}).`, { details: { table, path } })
|
|
443
|
+
}
|
|
444
|
+
for (let i = 0; i < value.length; i++) assertPartialFilter(table, value[i], `${path}.${key}[${i}]`)
|
|
445
|
+
} else if (key === '$exists' && value !== true) {
|
|
446
|
+
throw new MegaConfigError(
|
|
447
|
+
'migration.dialect_feature_unsupported',
|
|
448
|
+
`mongo dialect: partialFilterExpression 의 $exists 는 true 만 지원됩니다(공식 문서) (table '${table}', 위치: ${path}).`,
|
|
449
|
+
{ details: { table, path } },
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
continue
|
|
453
|
+
}
|
|
454
|
+
// 필드 조건 — 값이 객체면 연산자 객체로 보고 재귀, 스칼라/배열이면 equality(허용).
|
|
455
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
456
|
+
assertPartialFilter(table, value, `${path}.${key}`)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* 인덱스 생성 문 — createIndex(spec, options). `where` 는 mongo 에선 **JSON 문자열**
|
|
463
|
+
* (partialFilterExpression)로 해석한다 — SQL 식은 번역 불가라 명시 거부.
|
|
464
|
+
* @param {string} table @param {Record<string, any>} ix @returns {string}
|
|
465
|
+
*/
|
|
466
|
+
function indexCreateStatement(table, ix) {
|
|
467
|
+
const name = resolveIndexName(table, ix)
|
|
468
|
+
quoteIdent(name)
|
|
469
|
+
if (ix.using !== undefined && !INDEX_USING.includes(ix.using)) {
|
|
470
|
+
throw new MegaConfigError(
|
|
471
|
+
'migration.dialect_feature_unsupported',
|
|
472
|
+
`mongo dialect: 인덱스 using '${ix.using}' 미지원 — 허용: [${INDEX_USING.join(', ')}] (table '${table}').`,
|
|
473
|
+
{ details: { table, using: ix.using } },
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
/** @type {Record<string, any>} */
|
|
477
|
+
const spec = {}
|
|
478
|
+
for (const entry of ix.columns) {
|
|
479
|
+
const { field, dir } = parseIndexColumn(entry)
|
|
480
|
+
// dot-path(중첩 필드 인덱스 — mongo 핵심 패턴) 허용: 세그먼트별로 이름 규칙 검증.
|
|
481
|
+
for (const seg of field.split('.')) quoteIdent(seg)
|
|
482
|
+
spec[field] = ix.using !== undefined ? ix.using : dir
|
|
483
|
+
}
|
|
484
|
+
/** @type {Record<string, any>} */
|
|
485
|
+
const options = { name }
|
|
486
|
+
if (ix.unique === true) options.unique = true
|
|
487
|
+
if (ix.where !== undefined) {
|
|
488
|
+
let parsed
|
|
489
|
+
try {
|
|
490
|
+
parsed = JSON.parse(ix.where)
|
|
491
|
+
} catch (err) {
|
|
492
|
+
throw new MegaConfigError(
|
|
493
|
+
'migration.dialect_feature_unsupported',
|
|
494
|
+
`mongo dialect: 부분 인덱스의 where 는 JSON(partialFilterExpression) 문자열이어야 합니다 — SQL 식은 번역되지 않습니다. ` +
|
|
495
|
+
`예: t.index(['qty'], { where: '{"qty": {"$gt": 0}}' }) (table '${table}', where: ${JSON.stringify(ix.where)}).`,
|
|
496
|
+
{ cause: err, details: { table, where: ix.where } },
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
assertPartialFilter(table, parsed)
|
|
500
|
+
options.partialFilterExpression = parsed
|
|
501
|
+
}
|
|
502
|
+
return `await ${coll(table)}.createIndex(${js(spec, '')}, ${js(options, '')})`
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* 인덱스 삭제 문 — idempotent: 부분 실패 후 재실행이 1차 복구 수단(ADR-208 M-1 원칙)이라
|
|
507
|
+
* 이미 없는 인덱스(IndexNotFound, code 27)는 무해 통과시킨다. 그 외 에러는 전파.
|
|
508
|
+
* @param {string} table @param {string} name @returns {string}
|
|
509
|
+
*/
|
|
510
|
+
function dropIndexStatement(table, name) {
|
|
511
|
+
return (
|
|
512
|
+
`await ${coll(table)}.dropIndex(${quoteLiteral(name)}).catch((err) => {
|
|
513
|
+
` +
|
|
514
|
+
` // 재실행 복구: 이미 없는 인덱스는 무해(IndexNotFound) — 그 외 에러는 전파.
|
|
515
|
+
` +
|
|
516
|
+
` if (err.codeName !== 'IndexNotFound' && err.code !== 27) throw err
|
|
517
|
+
` +
|
|
518
|
+
`})`
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** 인덱스 삭제 문. @param {string} table @param {Record<string, any>} ix @returns {string} */
|
|
523
|
+
function indexDropStatement(table, ix) {
|
|
524
|
+
return dropIndexStatement(table, resolveIndexName(table, ix))
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** 컬렉션 삭제 문 — idempotent(NamespaceNotFound, code 26 무해 통과). @param {string} table @returns {string} */
|
|
528
|
+
function dropCollectionStatement(table) {
|
|
529
|
+
return (
|
|
530
|
+
`await db.dropCollection(${quoteLiteral(quoteIdent(table))}).catch((err) => {
|
|
531
|
+
` +
|
|
532
|
+
` // 재실행 복구: 이미 없는 컬렉션은 무해(NamespaceNotFound) — 그 외 에러는 전파.
|
|
533
|
+
` +
|
|
534
|
+
` if (err.codeName !== 'NamespaceNotFound' && err.code !== 26) throw err
|
|
535
|
+
` +
|
|
536
|
+
`})`
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** 컬럼 .unique() → UNIQUE 인덱스 생성 문 목록(sqlite 패턴 — 후행 토글이 validator 와 무관). */
|
|
541
|
+
/** @param {string} table @param {string} col @param {Record<string, any>} def @returns {string[]} */
|
|
542
|
+
function uniqueIndexStatements(table, col, def) {
|
|
543
|
+
if (def.unique === undefined) return []
|
|
544
|
+
const name = def.unique === true ? uniqueName(table, col) : def.unique.name
|
|
545
|
+
return [`await ${coll(table)}.createIndex(${js({ [col]: 1 }, '')}, ${js({ name, unique: true }, '')})`]
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* createCollection 문(+UNIQUE 인덱스) — validator/validationLevel/validationAction 동반.
|
|
550
|
+
* @param {Record<string, any>} record @returns {string[]}
|
|
551
|
+
*/
|
|
552
|
+
function createCollectionStatements(record) {
|
|
553
|
+
const table = quoteIdent(record.table)
|
|
554
|
+
const schema = jsonSchemaOf(record)
|
|
555
|
+
const { validationLevel, validationAction } = validationOptionsOf(record)
|
|
556
|
+
/** @type {string[]} */
|
|
557
|
+
const out = []
|
|
558
|
+
// strict + required 신설 컬렉션은 안전(빈 컬렉션) — 주석 불필요. 기존 도큐먼트 경고는 collMod 측.
|
|
559
|
+
// idempotent: 부분 실패 후 재실행에서 이미 존재(NamespaceExists, code 48)하면 validator 를
|
|
560
|
+
// 목표 상태로 collMod 동기화한다(수렴 — 외부 선생성 컬렉션도 같은 경로로 합류).
|
|
561
|
+
out.push(
|
|
562
|
+
`{\n` +
|
|
563
|
+
` const validator = { $jsonSchema: ${js(schema, ' ')} }\n` +
|
|
564
|
+
` await db.createCollection(${quoteLiteral(table)}, { validator, validationLevel: ${quoteLiteral(validationLevel)}, validationAction: ${quoteLiteral(validationAction)} }).catch(async (err) => {\n` +
|
|
565
|
+
` // 재실행 복구: 이미 존재(NamespaceExists)하면 validator 만 목표 상태로 동기화 — 그 외 에러는 전파.\n` +
|
|
566
|
+
` if (err.codeName !== 'NamespaceExists' && err.code !== 48) throw err\n` +
|
|
567
|
+
` await db.command({ collMod: ${quoteLiteral(table)}, validator, validationLevel: ${quoteLiteral(validationLevel)}, validationAction: ${quoteLiteral(validationAction)} })\n` +
|
|
568
|
+
` })\n` +
|
|
569
|
+
`}`,
|
|
570
|
+
)
|
|
571
|
+
for (const [col, def] of Object.entries(record.columns)) out.push(...uniqueIndexStatements(table, col, /** @type {any} */ (def)))
|
|
572
|
+
return out
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* collMod 가 "엄격해지는" 변경의 경고 주석 산출 — mongo collMod 는 기존 위배 행이 있어도 항상
|
|
577
|
+
* 성공하고 도큐먼트 단위로 **늦게**(update 시점 121) 터진다. SQL 은 적용 시점에 실패해 즉시
|
|
578
|
+
* 드러나므로(maria 1265 실측 — ADR-206 M-3 축소 경고와 동급 보호), mongo 는 경고 필요성이 더
|
|
579
|
+
* 높다(audit M-1). up/down 모두 같은 산출기를 쓴다(방향 대칭).
|
|
580
|
+
*
|
|
581
|
+
* @param {Record<string, any>} record - 교체 목표 record.
|
|
582
|
+
* @param {Record<string, any>} prevRecord - 직전 record.
|
|
583
|
+
* @param {{ renamedInto?: Set<string>, renamedAway?: Set<string> }} [renameSets]
|
|
584
|
+
* - renamedInto: record 쪽에서 rename 으로 생긴 필드($rename 이 데이터를 옮김 — backfill 불요).
|
|
585
|
+
* - renamedAway: prevRecord 쪽에서 rename 으로 떠나는 필드(잔존 아님 — $unset 불요).
|
|
586
|
+
* @returns {string[]} 경고 주석 행 목록(없으면 빈 배열).
|
|
587
|
+
*/
|
|
588
|
+
function collModWarnings(record, prevRecord, { renamedInto = new Set(), renamedAway = new Set() } = {}) {
|
|
589
|
+
const table = record.table
|
|
590
|
+
/** @type {string[]} */
|
|
591
|
+
const warnings = []
|
|
592
|
+
|
|
593
|
+
// ① required 신설 — 해당 필드 없는 기존 도큐먼트의 update 가 121.
|
|
594
|
+
const prevRequired = new Set(Object.entries(prevRecord.columns).filter(([, d]) => /** @type {any} */ (d).notNull === true).map(([c]) => c))
|
|
595
|
+
const newRequired = Object.entries(record.columns)
|
|
596
|
+
.filter(([c, d]) => /** @type {any} */ (d).notNull === true && !prevRequired.has(c) && !renamedInto.has(c))
|
|
597
|
+
.map(([c]) => c)
|
|
598
|
+
if (newRequired.length > 0) {
|
|
599
|
+
warnings.push(
|
|
600
|
+
`// 경고: [${newRequired.join(', ')}] 가 새로 required 입니다 — 해당 필드가 없는 기존 도큐먼트는 이후`,
|
|
601
|
+
`// update/replace 가 검증에 걸립니다. 필요 시 본 문장 앞에 backfill 을 직접 추가하세요.`,
|
|
602
|
+
`// 예: await ${coll(table)}.updateMany({ ${newRequired[0]}: { $exists: false } }, { $set: { ${newRequired[0]}: /* 기본값 */ } })`,
|
|
603
|
+
)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ② 필드 제거 — mongo 의 "제거" 는 validator 변경일 뿐 데이터가 지워지지 않고,
|
|
607
|
+
// additionalProperties: false 라 잔존 필드 보유 도큐먼트의 모든 update 가 121 이 된다.
|
|
608
|
+
const removed = Object.keys(prevRecord.columns).filter((c) => record.columns[c] === undefined && !renamedAway.has(c))
|
|
609
|
+
if (removed.length > 0) {
|
|
610
|
+
warnings.push(
|
|
611
|
+
`// 경고: [${removed.join(', ')}] 필드가 스키마에서 제거됩니다 — 데이터는 지워지지 않으며, 잔존 필드를`,
|
|
612
|
+
`// 가진 도큐먼트는 이후 모든 update 가 검증(additionalProperties)에 걸립니다. 본 문장 뒤에 정리를 권장합니다.`,
|
|
613
|
+
`// 예: await ${coll(table)}.updateMany({ ${removed[0]}: { $exists: true } }, { $unset: { ${removed[0]}: '' } }, { bypassDocumentValidation: true })`,
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ③ 같은 이름 필드의 축소/타입 변경 — 기존 위배 행은 collMod 시점엔 조용히 통과하고 update 에서 121.
|
|
618
|
+
for (const [c, d] of Object.entries(record.columns)) {
|
|
619
|
+
const pd = /** @type {any} */ (prevRecord.columns[c])
|
|
620
|
+
if (pd === undefined) continue
|
|
621
|
+
const cd = /** @type {any} */ (d)
|
|
622
|
+
if (pd.type === 'enum' && cd.type === 'enum') {
|
|
623
|
+
const removedValues = (pd.values ?? []).filter((/** @type {string} */ v) => !(cd.values ?? []).includes(v))
|
|
624
|
+
if (removedValues.length > 0) {
|
|
625
|
+
warnings.push(
|
|
626
|
+
`// 경고: '${c}' 의 enum 값 [${removedValues.join(', ')}] 이 제거됩니다 — 그 값을 가진 기존 도큐먼트는`,
|
|
627
|
+
`// 이후 update 가 검증에 걸립니다. 적용 전 backfill(값 치환)을 권장합니다.`,
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
} else if ((pd.type === 'varchar' || pd.type === 'char') && (cd.type === 'varchar' || cd.type === 'char') && typeof pd.length === 'number' && typeof cd.length === 'number' && cd.length < pd.length) {
|
|
631
|
+
warnings.push(
|
|
632
|
+
`// 경고: '${c}' 의 최대 길이가 ${pd.length} → ${cd.length} 로 축소됩니다 — 초과 길이 기존 도큐먼트는 이후 update 가 검증에 걸립니다.`,
|
|
633
|
+
)
|
|
634
|
+
} else if (pd.type !== cd.type) {
|
|
635
|
+
warnings.push(
|
|
636
|
+
`// 경고: '${c}' 타입이 ${pd.type} → ${cd.type} 로 바뀝니다 — 기존 도큐먼트는 변환되지 않으며, 옛 타입 잔존 행은 이후 update 가 검증에 걸립니다.`,
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return warnings
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* collMod(validator 교체) 문 — 엄격해지는 변경 4종(required 신설·필드 제거·enum/길이 축소·타입
|
|
645
|
+
* 변경)의 경고 주석 동반. collMod 자체는 같은 값 재설정에 에러가 없어 자연 멱등이다.
|
|
646
|
+
* @param {Record<string, any>} record - 교체 목표 record.
|
|
647
|
+
* @param {Record<string, any> | null} prevRecord - 직전 record (경고 산출용 — null 이면 경고 생략).
|
|
648
|
+
* @param {{ renamedInto?: Set<string>, renamedAway?: Set<string> }} [renameSets]
|
|
649
|
+
* @returns {string}
|
|
650
|
+
*/
|
|
651
|
+
function collModStatement(record, prevRecord, renameSets = {}) {
|
|
652
|
+
const schema = jsonSchemaOf(record)
|
|
653
|
+
const { validationLevel, validationAction } = validationOptionsOf(record)
|
|
654
|
+
const warnings = prevRecord === null ? [] : collModWarnings(record, prevRecord, renameSets)
|
|
655
|
+
const stmt =
|
|
656
|
+
`await db.command({\n` +
|
|
657
|
+
` collMod: ${quoteLiteral(quoteIdent(record.table))},\n` +
|
|
658
|
+
` validator: { $jsonSchema: ${js(schema, ' ')} },\n` +
|
|
659
|
+
` validationLevel: ${quoteLiteral(validationLevel)},\n` +
|
|
660
|
+
` validationAction: ${quoteLiteral(validationAction)},\n` +
|
|
661
|
+
`})`
|
|
662
|
+
return warnings.length > 0 ? `${warnings.join('\n')}\n${stmt}` : stmt
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* rebuildTable(=validator 교체) 렌더 — collMod + (differ 수렴이 삼킨) 인덱스/UNIQUE 변경 재계산 +
|
|
667
|
+
* rename 의 `$rename` 데이터 이동. **컬렉션 재생성이 아니다** — 도큐먼트는 그대로 둔다.
|
|
668
|
+
* @param {Record<string, any>} from @param {Record<string, any>} to
|
|
669
|
+
* @param {Record<string, string>} renames - from→to 필드 매핑(사용자 확정).
|
|
670
|
+
* @returns {{ up: string[], down: string[] }}
|
|
671
|
+
*/
|
|
672
|
+
function renderValidatorRebuild(from, to, renames) {
|
|
673
|
+
const table = to.table
|
|
674
|
+
/** @type {string[]} */
|
|
675
|
+
const up = []
|
|
676
|
+
/** @type {string[]} */
|
|
677
|
+
const down = []
|
|
678
|
+
|
|
679
|
+
// 1) validator 교체를 **먼저** — strict 검증은 update 의 "결과 도큐먼트" 를 새 validator 로
|
|
680
|
+
// 평가하므로, $rename 을 교체 전에 돌리면 옛 validator(additionalProperties: false)가 새
|
|
681
|
+
// 필드명을 거부해 적용이 실패한다(실측 — Document failed validation). 교체 후 $rename 의
|
|
682
|
+
// 결과는 새 스키마에 부합해 통과한다. down 도 동일 원리(from 으로 교체 후 역 $rename).
|
|
683
|
+
// 경고는 up/down 모두 산출(방향 대칭 — audit M-1). rename 으로 생기는/떠나는 필드는
|
|
684
|
+
// $rename 이 데이터를 옮기므로 backfill/$unset 경고에서 제외.
|
|
685
|
+
const oldNames = new Set(Object.keys(renames))
|
|
686
|
+
const newNames = new Set(Object.values(renames))
|
|
687
|
+
up.push(collModStatement(to, from, { renamedInto: newNames, renamedAway: oldNames }))
|
|
688
|
+
down.push(collModStatement(from, to, { renamedInto: oldNames, renamedAway: newNames }))
|
|
689
|
+
|
|
690
|
+
// 2) rename — 데이터 이동(updateMany $rename). down 은 역방향. bypassDocumentValidation:
|
|
691
|
+
// 검증은 update "결과 도큐먼트 전체" 를 평가하므로, rename 과 무관한 필드의 기존 드리프트
|
|
692
|
+
// (예: 직전 타입 변경의 미변환 데이터)가 있으면 $rename 자체가 거부된다(실측 — down 경로).
|
|
693
|
+
// SQL 의 RENAME COLUMN 도 행 데이터를 검증하지 않는다 — 동일 의미로 우회한다(이동만 수행).
|
|
694
|
+
for (const [oldName, newName] of Object.entries(renames)) {
|
|
695
|
+
quoteIdent(oldName)
|
|
696
|
+
quoteIdent(newName)
|
|
697
|
+
up.push(`await ${coll(table)}.updateMany({}, { $rename: { ${quoteLiteral(oldName)}: ${quoteLiteral(newName)} } }, { bypassDocumentValidation: true })`)
|
|
698
|
+
down.push(`await ${coll(table)}.updateMany({}, { $rename: { ${quoteLiteral(newName)}: ${quoteLiteral(oldName)} } }, { bypassDocumentValidation: true })`)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// 3) 수렴이 삼킨 인덱스/UNIQUE 변경 재계산 — 이름 기준 식별(differ 의 인덱스 diff 와 동일 규칙).
|
|
702
|
+
/** @type {Map<string, { create: string, drop: string, def: any }>} */
|
|
703
|
+
const fromIx = new Map()
|
|
704
|
+
/** @type {Map<string, { create: string, drop: string, def: any }>} */
|
|
705
|
+
const toIx = new Map()
|
|
706
|
+
const collect = (/** @type {Record<string, any>} */ record, /** @type {Map<string, any>} */ map) => {
|
|
707
|
+
for (const ix of record.indexes ?? []) {
|
|
708
|
+
map.set(resolveIndexName(record.table, ix), { create: indexCreateStatement(record.table, ix), drop: indexDropStatement(record.table, ix), def: ix })
|
|
709
|
+
}
|
|
710
|
+
for (const [col, def] of Object.entries(record.columns)) {
|
|
711
|
+
const d = /** @type {any} */ (def)
|
|
712
|
+
if (d.unique === undefined) continue
|
|
713
|
+
const name = d.unique === true ? uniqueName(record.table, col) : d.unique.name
|
|
714
|
+
map.set(name, {
|
|
715
|
+
create: uniqueIndexStatements(record.table, col, d)[0],
|
|
716
|
+
drop: dropIndexStatement(record.table, name),
|
|
717
|
+
def: { unique: true, columns: [col] },
|
|
718
|
+
})
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
collect(from, fromIx)
|
|
722
|
+
collect(to, toIx)
|
|
723
|
+
for (const [name, f] of fromIx) {
|
|
724
|
+
const t = toIx.get(name)
|
|
725
|
+
if (t === undefined || !isEqualJson(f.def, t.def)) {
|
|
726
|
+
up.push(f.drop)
|
|
727
|
+
down.push(f.create)
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
for (const [name, t] of toIx) {
|
|
731
|
+
const f = fromIx.get(name)
|
|
732
|
+
if (f === undefined || !isEqualJson(f.def, t.def)) {
|
|
733
|
+
up.push(t.create)
|
|
734
|
+
down.push(t.drop)
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// down 은 이미 의미 순서(validator 원복 → 역 $rename → 인덱스 역계산) — 추가 역순 불요.
|
|
739
|
+
return { up, down }
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** @param {unknown} a @param {unknown} b @returns {boolean} */
|
|
743
|
+
function isEqualJson(a, b) {
|
|
744
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* 변경 연산(op) 1개 → `{ up: string[], down: string[] }` (mongo command JS 문).
|
|
749
|
+
* 컬럼 수준 op 는 differ 가 rebuildTable 로 수렴하므로 도달하지 않는다(도달 시 명시 에러).
|
|
750
|
+
* @param {Record<string, any>} op
|
|
751
|
+
* @returns {{ up: string[], down: string[] }}
|
|
752
|
+
*/
|
|
753
|
+
export function renderOp(op) {
|
|
754
|
+
const table = op.table
|
|
755
|
+
switch (op.kind) {
|
|
756
|
+
case 'createTable':
|
|
757
|
+
return {
|
|
758
|
+
up: createCollectionStatements(op.record),
|
|
759
|
+
down: [`// 경고: 컬렉션과 모든 도큐먼트가 영구 삭제됩니다.\n${dropCollectionStatement(table)}`],
|
|
760
|
+
}
|
|
761
|
+
case 'dropTable': {
|
|
762
|
+
// 테이블(모델) 제거 또는 collection 이름 변경의 drop 측 — mongo 의 이름 변경은
|
|
763
|
+
// db.renameCollection (트랜잭션 불가)이 있으나 differ 는 모델 단위 drop+create 로 본다
|
|
764
|
+
// (SQL dialect 와 동일 — 데이터 이전은 자동화 범위 밖). 이름만 바꾸는 의도라면 이 파일을
|
|
765
|
+
// raw 편집해 `await db.renameCollection('old', 'new')` 1문으로 대체하세요.
|
|
766
|
+
const down = createCollectionStatements(op.record)
|
|
767
|
+
for (const ix of op.record.indexes ?? []) down.push(indexCreateStatement(table, ix))
|
|
768
|
+
return {
|
|
769
|
+
up: [
|
|
770
|
+
`// 경고: 컬렉션과 모든 도큐먼트가 영구 삭제됩니다. 이름 변경 의도라면 본 문장을\n` +
|
|
771
|
+
`// await db.renameCollection(${quoteLiteral(table)}, '<새이름>') 으로 직접 교체하세요(도큐먼트 보존).\n` +
|
|
772
|
+
dropCollectionStatement(table),
|
|
773
|
+
],
|
|
774
|
+
down,
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
case 'rebuildTable':
|
|
778
|
+
return renderValidatorRebuild(op.from, op.to, op.renames ?? {})
|
|
779
|
+
case 'addIndex': {
|
|
780
|
+
const stmt = indexCreateStatement(table, op.index)
|
|
781
|
+
// mongo 4.2+ 인덱스 빌드는 항상 online(hybrid) — --concurrent 는 동작 차이가 없다(명시 주석).
|
|
782
|
+
return {
|
|
783
|
+
up: [op.concurrent === true ? `// mongo 4.2+ 인덱스 빌드는 항상 online — --concurrent 는 표기일 뿐 동작 차이 없음.\n${stmt}` : stmt],
|
|
784
|
+
down: [indexDropStatement(table, op.index)],
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
case 'dropIndex':
|
|
788
|
+
return { up: [indexDropStatement(table, op.index)], down: [indexCreateStatement(table, op.index)] }
|
|
789
|
+
case 'addUnique':
|
|
790
|
+
return {
|
|
791
|
+
up: [`await ${coll(table)}.createIndex(${js({ [op.column]: 1 }, '')}, ${js({ name: op.name, unique: true }, '')})`],
|
|
792
|
+
down: [dropIndexStatement(table, op.name)],
|
|
793
|
+
}
|
|
794
|
+
case 'dropUnique':
|
|
795
|
+
return {
|
|
796
|
+
up: [dropIndexStatement(table, op.name)],
|
|
797
|
+
down: [`await ${coll(table)}.createIndex(${js({ [op.column]: 1 }, '')}, ${js({ name: op.name, unique: true }, '')})`],
|
|
798
|
+
}
|
|
799
|
+
default:
|
|
800
|
+
throw new MegaConfigError(
|
|
801
|
+
'migration.schema_invalid',
|
|
802
|
+
`mongo dialect: 알 수 없는 변경 연산 '${op.kind}' — 컬럼 수준 변경은 rebuildTriggerKinds 수렴으로 도달하지 않아야 합니다(통합 결함).`,
|
|
803
|
+
{ details: { kind: op.kind } },
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* ops 배열 → { up, down } (down 은 역순 — 적용의 정확한 역).
|
|
810
|
+
* @param {Array<Record<string, any>>} ops
|
|
811
|
+
* @returns {{ up: string[], down: string[] }}
|
|
812
|
+
*/
|
|
813
|
+
export function renderOps(ops) {
|
|
814
|
+
/** @type {string[]} */
|
|
815
|
+
const up = []
|
|
816
|
+
/** @type {string[][]} */
|
|
817
|
+
const downs = []
|
|
818
|
+
for (const op of ops) {
|
|
819
|
+
const r = renderOp(op)
|
|
820
|
+
up.push(...r.up)
|
|
821
|
+
downs.push(r.down)
|
|
822
|
+
}
|
|
823
|
+
return { up, down: downs.reverse().flat() }
|
|
824
|
+
}
|