@gaonjs/cli 0.47.0 → 0.55.0
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/dist/commands/check.d.ts +3 -1
- package/dist/commands/check.js +44 -2
- package/dist/commands/db.js +9 -0
- package/dist/commands/gen.d.ts +2 -0
- package/dist/commands/gen.js +3 -1
- package/dist/commands/new.js +13 -0
- package/dist/commands/test.js +16 -3
- package/dist/db/journal.d.ts +8 -4
- package/dist/db/journal.js +58 -14
- package/dist/db/migrate.d.ts +3 -1
- package/dist/db/migrate.js +14 -14
- package/dist/db/replay.js +3 -3
- package/dist/db/resolve.d.ts +11 -1
- package/dist/db/resolve.js +24 -2
- package/dist/db/status.js +17 -4
- package/dist/db.js +26 -5
- package/dist/dev.d.ts +6 -4
- package/dist/dev.js +9 -4
- package/dist/doctor/fixers/index.d.ts +1 -1
- package/dist/doctor/fixers/index.js +6 -1
- package/dist/doctor/locale-parity.js +4 -1
- package/dist/doctor/render-return.d.ts +11 -0
- package/dist/doctor/render-return.js +143 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor.d.ts +3 -2
- package/dist/doctor.js +16 -5
- package/dist/generate.d.ts +20 -1
- package/dist/generate.js +120 -21
- package/dist/hub.js +2 -0
- package/dist/i18n-config.d.ts +32 -0
- package/dist/i18n-config.js +170 -0
- package/dist/index.js +141 -29
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.js +13 -6
- package/dist/messages-gen.d.ts +1 -1
- package/dist/messages-gen.js +16 -5
- package/dist/templates/auth/Dashboard.vue.tpl +3 -2
- package/dist/templates/auth/Login.vue.tpl +3 -5
- package/dist/templates/auth/Signup.vue.tpl +3 -5
- package/dist/templates/auth/jwt.app.config.ts.tpl +18 -0
- package/dist/templates/auth/jwt.auth.wiring.ts.tpl +16 -0
- package/dist/templates/auth/jwt.routes.ts.tpl +7 -0
- package/dist/templates/auth/jwt.session.controller.ts.tpl +36 -0
- package/dist/templates/project/AGENTS.md.tpl +3 -2
- package/dist/templates/project/CLAUDE.md.tpl +1 -1
- package/dist/templates/project/Dockerfile.tpl +11 -1
- package/dist/templates/project/agents/async.md.tpl +70 -14
- package/dist/templates/project/agents/data.md.tpl +173 -52
- package/dist/templates/project/agents/frontend.md.tpl +19 -4
- package/dist/templates/project/agents/i18n.md.tpl +20 -2
- package/dist/templates/project/agents/mail.md.tpl +8 -1
- package/dist/templates/project/agents/realtime.md.tpl +25 -6
- package/dist/templates/project/agents/seal.md.tpl +8 -3
- package/dist/templates/project/agents/security.md.tpl +50 -19
- package/dist/templates/project/agents/storage.md.tpl +39 -6
- package/dist/templates/project/agents/web.md.tpl +54 -22
- package/dist/work.d.ts +3 -0
- package/dist/work.js +4 -0
- package/package.json +7 -7
package/dist/commands/check.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface CheckRegen {
|
|
|
40
40
|
readonly tables: boolean;
|
|
41
41
|
/** routes.d.ts 를 재생성한 앱 이름. */
|
|
42
42
|
readonly apps: readonly string[];
|
|
43
|
+
/** messages.d.ts(i18n 카탈로그 키 유니온)를 재생성했는가 — 결정 414(축 보고 누락 봉합). */
|
|
44
|
+
readonly messages?: boolean;
|
|
43
45
|
/** 실패 시 에러 + 수리 안내. done/skipped 는 undefined. */
|
|
44
46
|
readonly output?: string;
|
|
45
47
|
}
|
|
@@ -52,7 +54,7 @@ export interface CheckReport {
|
|
|
52
54
|
/**
|
|
53
55
|
* `gaon check` 진입점. 검사 전에 .gaon 을 재생성(규칙 3)한 뒤 각 단계를
|
|
54
56
|
* 순서대로 실행하고, 하나라도 실패하면 exit 1. --only 지정 시 그 단계만
|
|
55
|
-
* 실행. --
|
|
57
|
+
* 실행. doctor 는 기본 포함(결정 157) — --no-doctor 로만 뺀다.
|
|
56
58
|
*
|
|
57
59
|
* §9 실 인프라 · 목업 X — 실 spawn 으로 검증한다.
|
|
58
60
|
*/
|
package/dist/commands/check.js
CHANGED
|
@@ -32,6 +32,8 @@ import { generateTablesDts } from '@gaonjs/data';
|
|
|
32
32
|
import { generateRoutesDts } from '@gaonjs/web';
|
|
33
33
|
import { computeDoctorResult } from '../doctor.js';
|
|
34
34
|
import { regenerateGaonOnce, resolveDevLayout } from '../dev.js';
|
|
35
|
+
import { analyzeProjectI18n, resolveLocalesDir } from '../i18n-config.js';
|
|
36
|
+
import { loadLocales } from '@gaonjs/i18n';
|
|
35
37
|
import { registerTsResolve } from '../tsResolve.js';
|
|
36
38
|
import { listFrontendApps, verifyAppDist } from '../dev/build.js';
|
|
37
39
|
import { generateMessagesDts } from '../messages-gen.js';
|
|
@@ -239,9 +241,41 @@ async function runDoctorStep(cwd) {
|
|
|
239
241
|
* 실패를 내지 않게 한다. --only 여부와 무관하게 항상 먼저 돈다 —
|
|
240
242
|
* typecheck·vue-tsc·build 가 모두 .gaon 을 소비하기 때문.
|
|
241
243
|
*/
|
|
244
|
+
/**
|
|
245
|
+
* 결정 413: 결정 353(부팅 fail-loud)의 정적 미러 — i18n 을 켰는데 카탈로그가 비었거나
|
|
246
|
+
* fallbackLng/supportedLngs 가 지목한 파일이 없으면 부팅이 죽는다. 같은 사유·같은 수리
|
|
247
|
+
* 안내를 check 단계에서 먼저 낸다. 정적으로 못 읽은 키는 건너뛴다(결정 412 경고가 별도로 알림).
|
|
248
|
+
*/
|
|
249
|
+
function verifyI18nCatalogs(cwd) {
|
|
250
|
+
const cfg = analyzeProjectI18n(cwd);
|
|
251
|
+
if (!cfg.declared)
|
|
252
|
+
return undefined;
|
|
253
|
+
const dir = cfg.dir ?? 'locales';
|
|
254
|
+
const abs = resolveLocalesDir(cwd, cfg.dir);
|
|
255
|
+
const resources = existsSync(abs) ? loadLocales(abs) : {};
|
|
256
|
+
if (Object.keys(resources).length === 0) {
|
|
257
|
+
return (`i18n 이 설정됐지만 로케일 카탈로그가 비어 있습니다: ${abs}\n` +
|
|
258
|
+
` → ${dir}/${cfg.fallbackLng ?? '<fallbackLng>'}.json 을 만들거나(최소 fallbackLng 카탈로그),\n` +
|
|
259
|
+
` → gaon.config.ts 의 i18n.dir 이 실제 카탈로그 폴더를 가리키는지 확인하세요.\n` +
|
|
260
|
+
` (이 상태로는 gaon serve 가 부팅에서 실패합니다 — 결정 353)`);
|
|
261
|
+
}
|
|
262
|
+
const wanted = [...(cfg.fallbackLng ? [cfg.fallbackLng] : []), ...(cfg.supportedLngs ?? [])];
|
|
263
|
+
const missing = wanted.filter((lng, i, arr) => arr.indexOf(lng) === i && !resources[lng]);
|
|
264
|
+
if (missing.length > 0) {
|
|
265
|
+
return (`i18n 설정이 지목한 로케일의 카탈로그 파일이 없습니다: ${missing.join(', ')}\n` +
|
|
266
|
+
` → ${dir}/ 에 ${missing.map((l) => `${l}.json`).join(' · ')} 을 추가하거나,\n` +
|
|
267
|
+
` → gaon.config.ts 의 fallbackLng/supportedLngs 에서 해당 로케일을 제거하세요.\n` +
|
|
268
|
+
` (이 상태로는 gaon serve 가 부팅에서 실패합니다 — 결정 353)`);
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
242
272
|
async function regenerateGaon(cwd) {
|
|
243
273
|
const layout = resolveDevLayout(cwd);
|
|
244
274
|
if (!layout.schemaDir && layout.apps.length === 0 && !layout.localesDir) {
|
|
275
|
+
// 카탈로그가 통째로 없어도 config 가 i18n 을 켰으면 부팅이 죽는다 — 그 경우만 잡는다.
|
|
276
|
+
const problem = verifyI18nCatalogs(cwd);
|
|
277
|
+
if (problem)
|
|
278
|
+
return { status: 'failed', tables: false, apps: [], output: problem };
|
|
245
279
|
return { status: 'skipped', tables: false, apps: [] };
|
|
246
280
|
}
|
|
247
281
|
// 생성기는 사용자 스키마·컨트롤러 .ts 를 동적 import 한다. TS-for-ESM
|
|
@@ -255,7 +289,15 @@ async function regenerateGaon(cwd) {
|
|
|
255
289
|
regenerateMessages: generateMessagesDts,
|
|
256
290
|
regenerateEnv: generateEnvDts,
|
|
257
291
|
});
|
|
258
|
-
|
|
292
|
+
// 결정 413: 부팅(결정 353)이 throw 하는 조건을 **정적으로 미리** 본다 — check 는
|
|
293
|
+
// AI 자가검증의 정본 경로인데, 통과해 놓고 `gaon serve` 가 죽으면 자가검증이
|
|
294
|
+
// 부팅 가능성을 담보하지 못한다(i18n 한정 구멍). 재료(dir·fallbackLng·supportedLngs)는
|
|
295
|
+
// 이미 정적으로 있다.
|
|
296
|
+
const i18nProblem = verifyI18nCatalogs(cwd);
|
|
297
|
+
if (i18nProblem) {
|
|
298
|
+
return { status: 'failed', tables: result.tables, apps: result.apps, messages: result.messages, output: i18nProblem };
|
|
299
|
+
}
|
|
300
|
+
return { status: 'done', tables: result.tables, apps: result.apps, messages: result.messages };
|
|
259
301
|
}
|
|
260
302
|
catch (err) {
|
|
261
303
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -271,7 +313,7 @@ async function regenerateGaon(cwd) {
|
|
|
271
313
|
/**
|
|
272
314
|
* `gaon check` 진입점. 검사 전에 .gaon 을 재생성(규칙 3)한 뒤 각 단계를
|
|
273
315
|
* 순서대로 실행하고, 하나라도 실패하면 exit 1. --only 지정 시 그 단계만
|
|
274
|
-
* 실행. --
|
|
316
|
+
* 실행. doctor 는 기본 포함(결정 157) — --no-doctor 로만 뺀다.
|
|
275
317
|
*
|
|
276
318
|
* §9 실 인프라 · 목업 X — 실 spawn 으로 검증한다.
|
|
277
319
|
*/
|
package/dist/commands/db.js
CHANGED
|
@@ -79,6 +79,15 @@ export async function runDbCommand(subcommand, opts = {}) {
|
|
|
79
79
|
emit(r.text, r.json);
|
|
80
80
|
return r.exitCode;
|
|
81
81
|
}
|
|
82
|
+
// 결정 367: seed 는 커넥션별 반복이 아니라 **1회 실행**이다 — domain/seed.ts 는
|
|
83
|
+
// 프로젝트 단일 시드라 키당 재실행하면 중복 삽입이고, 패스마다 단일 커넥션만
|
|
84
|
+
// 등록해 타 커넥션 모델이 '미등록' throw 로 순회가 항상 exit 1 이었다.
|
|
85
|
+
// runDbSeedCommand 가 선언된 전 SQL 커넥션을 등록하고 한 번 실행한다.
|
|
86
|
+
if (subcommand === 'seed') {
|
|
87
|
+
const r = await runDbSeedCommand({ root: cwd, json, configPath });
|
|
88
|
+
emit(r.text, r.json);
|
|
89
|
+
return r.exitCode;
|
|
90
|
+
}
|
|
82
91
|
return runAcrossConnections(subcommand, keys, runOne, emit, json, mongoKeys);
|
|
83
92
|
}
|
|
84
93
|
// 단일 커넥션 경로(--db <키> · reset · env-only 폴백) — 종전 동작 그대로.
|
package/dist/commands/gen.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface GenResult {
|
|
|
13
13
|
readonly apps: readonly string[];
|
|
14
14
|
/** env.d.ts 를 재생성했는가(프론트 앱 존재 · 결정 198). */
|
|
15
15
|
readonly env?: boolean;
|
|
16
|
+
/** messages.d.ts 를 재생성했는가(locales/ 존재 · 결정 158). 결정 414: 재생성만 하고 보고에서 빠져 있었다. */
|
|
17
|
+
readonly messages?: boolean;
|
|
16
18
|
/** 실패 시 에러 + 수리 안내. */
|
|
17
19
|
readonly error?: string;
|
|
18
20
|
}
|
package/dist/commands/gen.js
CHANGED
|
@@ -48,7 +48,7 @@ export async function runGenCommand(opts = {}) {
|
|
|
48
48
|
let result;
|
|
49
49
|
try {
|
|
50
50
|
const r = await regenerateProjectGaon(cwd);
|
|
51
|
-
result = { ok: true, skipped: r.skipped, tables: r.tables, apps: r.apps, env: r.env };
|
|
51
|
+
result = { ok: true, skipped: r.skipped, tables: r.tables, apps: r.apps, env: r.env, messages: r.messages };
|
|
52
52
|
}
|
|
53
53
|
catch (err) {
|
|
54
54
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -77,6 +77,8 @@ export async function runGenCommand(opts = {}) {
|
|
|
77
77
|
for (const app of result.apps) {
|
|
78
78
|
parts.push(`apps/${app}/.gaon/routes.d.ts`, `apps/${app}/.gaon/routes.manifest.ts`);
|
|
79
79
|
}
|
|
80
|
+
if (result.messages)
|
|
81
|
+
parts.push('.gaon/messages.d.ts');
|
|
80
82
|
if (result.env)
|
|
81
83
|
parts.push('.gaon/env.d.ts');
|
|
82
84
|
process.stdout.write(` ✓ gaon gen — ${parts.join(', ')}\n`);
|
package/dist/commands/new.js
CHANGED
|
@@ -44,6 +44,19 @@ export function validateProjectName(name) {
|
|
|
44
44
|
` 대문자·언더스코어(_)는 인프라 기동·버킷 생성에서 실패합니다.\n` +
|
|
45
45
|
` → 예: gaon new my-app · gaon new demo`);
|
|
46
46
|
}
|
|
47
|
+
// 결정 409: 결정 315 가 남긴 같은 클래스의 잔여 두 갈래 — 버킷명은 **영숫자로 끝나야**
|
|
48
|
+
// 하고 **3~63자**여야 한다. 종전 정규식은 'my-app-'·'ab' 를 통과시켜 생성은 성공하고
|
|
49
|
+
// `gaon dev` 의 버킷 생성(mc mb)에서 뒤늦게 깨졌다.
|
|
50
|
+
if (name.endsWith('-')) {
|
|
51
|
+
throw new Error(`프로젝트 이름은 하이픈(-)으로 끝날 수 없습니다: ${name}\n` +
|
|
52
|
+
` → 스토리지 버킷명은 영문/숫자로 끝나야 해 'mc mb' 가 실패합니다.\n` +
|
|
53
|
+
` → 예: gaon new ${name.replace(/-+$/, '')}`);
|
|
54
|
+
}
|
|
55
|
+
if (name.length < 3 || name.length > 63) {
|
|
56
|
+
throw new Error(`프로젝트 이름은 3~63자여야 합니다(현재 ${name.length}자): ${name}\n` +
|
|
57
|
+
` → 이름이 스토리지 버킷명으로 그대로 쓰이고, 버킷명 길이 제한이 3~63자입니다.\n` +
|
|
58
|
+
` → 예: gaon new my-app`);
|
|
59
|
+
}
|
|
47
60
|
}
|
|
48
61
|
/**
|
|
49
62
|
* 파사드(gaonjs) 패키지의 실 버전을 찾는다 — package.json 을 위로 훑는다.
|
package/dist/commands/test.js
CHANGED
|
@@ -106,7 +106,11 @@ async function provisionTestDatabases(cwd, json, onOutput) {
|
|
|
106
106
|
connectionOverride: testCfg,
|
|
107
107
|
});
|
|
108
108
|
if (res.exitCode !== 0) {
|
|
109
|
-
|
|
109
|
+
// 결정 360: json 모드도 구조화 에러를 낸다 — 이전엔 이 분기만 0 바이트로
|
|
110
|
+
// exit 1 이라 자동화가 실패 원인을 전혀 못 받았다(catch 분기와 비대칭).
|
|
111
|
+
if (json)
|
|
112
|
+
writeOut(JSON.stringify({ ok: false, kind: 'provision', db: key, error: res.text }) + '\n');
|
|
113
|
+
else
|
|
110
114
|
writeErr(` ✗ 테스트 DB '${key}' 마이그레이션 실패\n${res.text}\n`);
|
|
111
115
|
return false;
|
|
112
116
|
}
|
|
@@ -211,18 +215,27 @@ export async function runTestCommand(args = [], opts = {}) {
|
|
|
211
215
|
else {
|
|
212
216
|
writeOut(` gaon test · ${cmd} ${spawnArgs.join(' ')} (scope=${scope})\n`);
|
|
213
217
|
}
|
|
218
|
+
// 결정 360: --json 은 stdout 이 순수 JSON 라인이어야 한다(결정 269 check 와 대칭).
|
|
219
|
+
// 이전엔 json 모드도 stdio:'inherit' 라 vitest 사람용 출력이 JSON 라인 사이에 섞여
|
|
220
|
+
// `gaon test --json | jq` 류 자동화가 깨졌다. 자식 출력은 {kind:'output'} 라인으로 감싼다.
|
|
221
|
+
const jsonPipe = json && !capture;
|
|
214
222
|
const exitCode = await new Promise((resolvePromise) => {
|
|
215
223
|
// capture(MCP) 모드는 자식 출력을 파이프로 모아 싱크로 흘린다(도구 응답에 담기).
|
|
216
|
-
// CLI 경로(미지정)는 vitest 컬러 출력·리포터를 그대로 보이게 stdio 를 상속한다.
|
|
224
|
+
// CLI 사람 경로(미지정)는 vitest 컬러 출력·리포터를 그대로 보이게 stdio 를 상속한다.
|
|
217
225
|
const child = spawn(cmd, spawnArgs, {
|
|
218
226
|
cwd,
|
|
219
227
|
env: testEnv,
|
|
220
|
-
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
228
|
+
stdio: capture || jsonPipe ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
221
229
|
});
|
|
222
230
|
if (capture) {
|
|
223
231
|
child.stdout?.on('data', (d) => opts.onOutput(d.toString('utf8')));
|
|
224
232
|
child.stderr?.on('data', (d) => opts.onOutput(d.toString('utf8')));
|
|
225
233
|
}
|
|
234
|
+
else if (jsonPipe) {
|
|
235
|
+
const emit = (stream) => (d) => writeOut(JSON.stringify({ kind: 'output', stream, chunk: d.toString('utf8') }) + '\n');
|
|
236
|
+
child.stdout?.on('data', emit('stdout'));
|
|
237
|
+
child.stderr?.on('data', emit('stderr'));
|
|
238
|
+
}
|
|
226
239
|
child.on('error', (err) => {
|
|
227
240
|
writeErr(` ✗ gaon test spawn 실패: ${String(err)}\n`);
|
|
228
241
|
resolvePromise(127);
|
package/dist/db/journal.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Kysely } from 'kysely';
|
|
2
|
+
import { type AdapterName } from '@gaonjs/data';
|
|
2
3
|
export type MigrationKind = 'file' | 'schema';
|
|
3
4
|
export interface JournalEntry {
|
|
4
5
|
readonly id: string;
|
|
@@ -17,13 +18,16 @@ export interface JournalEntry {
|
|
|
17
18
|
* pg 는 count 를 bigint(문자열)로, mysql 은 number 로 돌려주므로 `Number()` 로 통일한다
|
|
18
19
|
* (양 방언 공통 · 결정 268).
|
|
19
20
|
*/
|
|
20
|
-
export declare function journalExists(db: Kysely<any
|
|
21
|
+
export declare function journalExists(db: Kysely<any>, adapter?: AdapterName): Promise<boolean>;
|
|
21
22
|
/**
|
|
22
23
|
* _gaon_migrations 를 만든다(존재하면 no-op). id/kind/applied_at/db_key/
|
|
23
24
|
* statements/summary/down_sql — text·varchar·timestamp·integer 는 postgres·
|
|
24
25
|
* mysql 공통 지원 타입. kind 기본값 'schema' 는 구버전 스키마 호환용.
|
|
26
|
+
* 결정 380: mysql 의 down_sql 은 **mediumtext**(16MB) — TEXT(64KB)는 큰 배치의
|
|
27
|
+
* down SQL(JSON 배열)이 조용히 잘려 저장 파손 위험이었다. 기존(legacy) 테이블은
|
|
28
|
+
* data_type 확인 후 한 번만 MODIFY 로 승격한다(매 실행 재빌드 방지). pg text 는 무제한.
|
|
25
29
|
*/
|
|
26
|
-
export declare function ensureJournal(db: Kysely<any
|
|
30
|
+
export declare function ensureJournal(db: Kysely<any>, adapter?: AdapterName): Promise<void>;
|
|
27
31
|
export interface RecordArgs {
|
|
28
32
|
readonly id: string;
|
|
29
33
|
readonly kind: MigrationKind;
|
|
@@ -35,7 +39,7 @@ export interface RecordArgs {
|
|
|
35
39
|
/** 이력 한 행을 남긴다. 호출자가 트랜잭션(trx)을 넘기면 그 안에서 실행된다. */
|
|
36
40
|
export declare function recordEntry(db: Kysely<any>, e: RecordArgs): Promise<void>;
|
|
37
41
|
/** 가장 최근 적용된 이력(롤백 대상). 원장 미존재 시 undefined. */
|
|
38
|
-
export declare function latestEntry(db: Kysely<any
|
|
42
|
+
export declare function latestEntry(db: Kysely<any>, adapter?: AdapterName): Promise<JournalEntry | undefined>;
|
|
39
43
|
/** kind='file' 로 적용된 파일명 집합. 원장 미존재 시 빈 집합. */
|
|
40
|
-
export declare function appliedFileMigrations(db: Kysely<any
|
|
44
|
+
export declare function appliedFileMigrations(db: Kysely<any>, adapter?: AdapterName): Promise<Set<string>>;
|
|
41
45
|
export declare function deleteEntry(db: Kysely<any>, id: string): Promise<void>;
|
package/dist/db/journal.js
CHANGED
|
@@ -17,12 +17,23 @@ import { MIGRATIONS_TABLE } from '@gaonjs/data';
|
|
|
17
17
|
* pg 는 count 를 bigint(문자열)로, mysql 은 number 로 돌려주므로 `Number()` 로 통일한다
|
|
18
18
|
* (양 방언 공통 · 결정 268).
|
|
19
19
|
*/
|
|
20
|
-
export async function journalExists(db) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
export async function journalExists(db, adapter = 'postgres') {
|
|
21
|
+
// 결정 326: mysql/mariadb 의 information_schema.tables 는 **서버 전체(모든 database)** 를
|
|
22
|
+
// 보므로 같은 서버 다른 DB 의 _gaon_migrations 에 오탐했다 — 존재 오판 뒤
|
|
23
|
+
// appliedFileMigrations 가 없는 테이블을 select 해 migrate/status 가 죽는다(공유 dev 서버
|
|
24
|
+
// 현실 시나리오) → database() 로 현재 DB 에 한정한다. pg 는 카탈로그가 database 단위라
|
|
25
|
+
// 종전대로 둔다(스키마('public') 한정은 커스텀 search_path 프로젝트를 깨서 안 한다).
|
|
26
|
+
const rows = adapter === 'mysql'
|
|
27
|
+
? await sql `
|
|
28
|
+
select count(*) as n
|
|
29
|
+
from information_schema.tables
|
|
30
|
+
where table_name = ${MIGRATIONS_TABLE} and table_schema = database()
|
|
31
|
+
`.execute(db)
|
|
32
|
+
: await sql `
|
|
33
|
+
select count(*) as n
|
|
34
|
+
from information_schema.tables
|
|
35
|
+
where table_name = ${MIGRATIONS_TABLE}
|
|
36
|
+
`.execute(db);
|
|
26
37
|
const first = rows.rows[0];
|
|
27
38
|
return first ? Number(first.n) > 0 : false;
|
|
28
39
|
}
|
|
@@ -30,31 +41,64 @@ export async function journalExists(db) {
|
|
|
30
41
|
* _gaon_migrations 를 만든다(존재하면 no-op). id/kind/applied_at/db_key/
|
|
31
42
|
* statements/summary/down_sql — text·varchar·timestamp·integer 는 postgres·
|
|
32
43
|
* mysql 공통 지원 타입. kind 기본값 'schema' 는 구버전 스키마 호환용.
|
|
44
|
+
* 결정 380: mysql 의 down_sql 은 **mediumtext**(16MB) — TEXT(64KB)는 큰 배치의
|
|
45
|
+
* down SQL(JSON 배열)이 조용히 잘려 저장 파손 위험이었다. 기존(legacy) 테이블은
|
|
46
|
+
* data_type 확인 후 한 번만 MODIFY 로 승격한다(매 실행 재빌드 방지). pg text 는 무제한.
|
|
33
47
|
*/
|
|
34
|
-
export async function ensureJournal(db) {
|
|
48
|
+
export async function ensureJournal(db, adapter = 'postgres') {
|
|
49
|
+
const downType = adapter === 'mysql' ? 'mediumtext' : 'text';
|
|
35
50
|
await sql
|
|
36
51
|
.raw(`create table if not exists ${MIGRATIONS_TABLE} (` +
|
|
37
52
|
` id varchar(255) not null primary key,` +
|
|
38
53
|
` kind varchar(16) not null default 'schema',` +
|
|
39
|
-
` applied_at timestamp not null,` +
|
|
54
|
+
` applied_at timestamp(6) not null,` +
|
|
40
55
|
` db_key varchar(64) not null,` +
|
|
41
56
|
` statements integer not null,` +
|
|
42
57
|
` summary text not null,` +
|
|
43
|
-
` down_sql
|
|
58
|
+
` down_sql ${downType}` +
|
|
44
59
|
`)`)
|
|
45
60
|
.execute(db);
|
|
61
|
+
if (adapter === 'mysql') {
|
|
62
|
+
// 결정 380: legacy 테이블 승격 — down_sql text→mediumtext(64KB 절단 방지) ·
|
|
63
|
+
// applied_at 초 해상 timestamp→timestamp(6)(ms 단조 증가가 저장에서 뭉개지지 않게).
|
|
64
|
+
// 둘 다 상태 확인 후 한 번만 MODIFY(매 실행 테이블 재빌드 방지).
|
|
65
|
+
const cur = await sql `
|
|
66
|
+
select column_name as c, data_type as t, datetime_precision as p
|
|
67
|
+
from information_schema.columns
|
|
68
|
+
where table_schema = database() and table_name = ${MIGRATIONS_TABLE}
|
|
69
|
+
and column_name in ('down_sql', 'applied_at')
|
|
70
|
+
`.execute(db);
|
|
71
|
+
for (const row of cur.rows) {
|
|
72
|
+
if (row.c === 'down_sql' && row.t.toLowerCase() === 'text') {
|
|
73
|
+
await sql.raw(`alter table ${MIGRATIONS_TABLE} modify down_sql mediumtext`).execute(db);
|
|
74
|
+
}
|
|
75
|
+
if (row.c === 'applied_at' && Number(row.p ?? 0) === 0) {
|
|
76
|
+
await sql.raw(`alter table ${MIGRATIONS_TABLE} modify applied_at timestamp(6) not null`).execute(db);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 결정 380: 프로세스 내 단조 증가 applied_at — 같은 migrate 실행의 파일 리플레이·schema
|
|
82
|
+
// 배치가 같은 ms 에 기록되면 롤백 대상 선정(`order by applied_at desc, id desc`)이 서로
|
|
83
|
+
// 다른 id 체계(파일명 vs `<epochMs>-<n>`)의 **사전순**으로 갈려 최신이 아닌 항목을 되돌릴
|
|
84
|
+
// 수 있었다. 동률이면 1ms 씩 밀어 순서를 시각에 각인한다(동시 다중 migrate 는 원래 미지원).
|
|
85
|
+
let lastAppliedAtMs = 0;
|
|
86
|
+
function nextAppliedAt() {
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
lastAppliedAtMs = now > lastAppliedAtMs ? now : lastAppliedAtMs + 1;
|
|
89
|
+
return new Date(lastAppliedAtMs);
|
|
46
90
|
}
|
|
47
91
|
/** 이력 한 행을 남긴다. 호출자가 트랜잭션(trx)을 넘기면 그 안에서 실행된다. */
|
|
48
92
|
export async function recordEntry(db, e) {
|
|
49
93
|
await sql `
|
|
50
94
|
insert into ${sql.ref(MIGRATIONS_TABLE)}
|
|
51
95
|
(id, kind, applied_at, db_key, statements, summary, down_sql)
|
|
52
|
-
values (${e.id}, ${e.kind}, ${
|
|
96
|
+
values (${e.id}, ${e.kind}, ${nextAppliedAt()}, ${e.dbKey}, ${e.statements}, ${e.summary}, ${e.downSql})
|
|
53
97
|
`.execute(db);
|
|
54
98
|
}
|
|
55
99
|
/** 가장 최근 적용된 이력(롤백 대상). 원장 미존재 시 undefined. */
|
|
56
|
-
export async function latestEntry(db) {
|
|
57
|
-
if (!(await journalExists(db)))
|
|
100
|
+
export async function latestEntry(db, adapter = 'postgres') {
|
|
101
|
+
if (!(await journalExists(db, adapter)))
|
|
58
102
|
return undefined;
|
|
59
103
|
const rows = await sql `
|
|
60
104
|
select id, kind, db_key, statements, summary, down_sql, applied_at
|
|
@@ -65,8 +109,8 @@ export async function latestEntry(db) {
|
|
|
65
109
|
return rows.rows[0];
|
|
66
110
|
}
|
|
67
111
|
/** kind='file' 로 적용된 파일명 집합. 원장 미존재 시 빈 집합. */
|
|
68
|
-
export async function appliedFileMigrations(db) {
|
|
69
|
-
if (!(await journalExists(db)))
|
|
112
|
+
export async function appliedFileMigrations(db, adapter = 'postgres') {
|
|
113
|
+
if (!(await journalExists(db, adapter)))
|
|
70
114
|
return new Set();
|
|
71
115
|
const rows = await sql `
|
|
72
116
|
select id from ${sql.ref(MIGRATIONS_TABLE)} where kind = 'file'
|
package/dist/db/migrate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ConnectionConfig } from '@gaonjs/data';
|
|
1
|
+
import { type MigrationOp, type ConnectionConfig } from '@gaonjs/data';
|
|
2
2
|
import { listMigrationFiles } from './replay.js';
|
|
3
3
|
export interface DbMigrateOptions {
|
|
4
4
|
readonly cwd: string;
|
|
@@ -16,6 +16,8 @@ export interface DbMigrateResult {
|
|
|
16
16
|
readonly text: string;
|
|
17
17
|
readonly json: unknown;
|
|
18
18
|
}
|
|
19
|
+
/** drop 계열 부속 op 을 사람이 읽는 한 줄로 — 크게 알리는 note 용(status 도 재사용 · 결정 326). */
|
|
20
|
+
export declare function describeDeferred(op: MigrationOp): string;
|
|
19
21
|
/**
|
|
20
22
|
* `gaon db migrate` — 진짜 apply(합성형: replay → schema-diff). 실 DB 필요(§9).
|
|
21
23
|
*/
|
package/dist/db/migrate.js
CHANGED
|
@@ -12,13 +12,12 @@
|
|
|
12
12
|
//
|
|
13
13
|
// 트랜잭션: postgres 는 트랜잭셔널 DDL 이라 각 단계가 원자적. mysql/mariadb 는
|
|
14
14
|
// DDL 이 autocommit 이라 순차 실행하고 이력만 남긴다(§4.5 방언 차이).
|
|
15
|
-
import {
|
|
16
|
-
import { computeMigration, renderUp, renderDown, summarizeOp, DEFERRED_DROP_KINDS } from '@gaonjs/data';
|
|
15
|
+
import { applyStatements, computeMigration, renderUp, renderDown, summarizeOp, partitionDeferredOps } from '@gaonjs/data';
|
|
17
16
|
import { resolveDbTarget } from './resolve.js';
|
|
18
17
|
import { ensureJournal, journalExists, recordEntry } from './journal.js';
|
|
19
18
|
import { listMigrationFiles, replayPending, rollbackLast } from './replay.js';
|
|
20
|
-
/** drop 계열 부속 op 을 사람이 읽는 한 줄로 — 크게 알리는 note
|
|
21
|
-
function describeDeferred(op) {
|
|
19
|
+
/** drop 계열 부속 op 을 사람이 읽는 한 줄로 — 크게 알리는 note 용(status 도 재사용 · 결정 326). */
|
|
20
|
+
export function describeDeferred(op) {
|
|
22
21
|
switch (op.kind) {
|
|
23
22
|
case 'dropUnique':
|
|
24
23
|
return `unique(${op.name}: ${op.columns.join(', ')})`;
|
|
@@ -62,12 +61,15 @@ export async function runDbMigrate(opts) {
|
|
|
62
61
|
// 자동 적용하지 않는다** — replay 가 만든/외부 객체·수동 추가한 제약을 보호한다. 제거는
|
|
63
62
|
// 손작성 마이그로 하고, 'gaon db diff' 가 계속 보여준다(조용히 넘어가지 않는다). ADD·SET
|
|
64
63
|
// (unique·index·check·default 추가)은 자동 적용해 "조용한 no-op"(결정 220 배경)을 없앤다.
|
|
64
|
+
// 결정 366: 단 **재생성 쌍**(같은 이름 dropIndex→addIndex / dropCheck→addCheck — 인덱스
|
|
65
|
+
// method 변경·enum 값 변경)의 drop 반쪽은 defer 하지 않는다 — kind 만 보고 걸러내면
|
|
66
|
+
// add 반쪽이 기존 객체 위에 실행돼 "already exists" 로 migrate 가 매번 죽었다.
|
|
65
67
|
const full = await computeMigration(target.db, target.tables, target.dialect);
|
|
66
|
-
const
|
|
68
|
+
const { apply: schemaOps, deferred } = partitionDeferredOps(full.ops);
|
|
69
|
+
const droppedTables = deferred
|
|
67
70
|
.filter((o) => o.kind === 'dropTable')
|
|
68
71
|
.map((o) => o.name);
|
|
69
|
-
const deferredDrops =
|
|
70
|
-
const schemaOps = full.ops.filter((o) => !DEFERRED_DROP_KINDS.has(o.kind));
|
|
72
|
+
const deferredDrops = deferred.filter((o) => o.kind !== 'dropTable');
|
|
71
73
|
const plan = {
|
|
72
74
|
ops: schemaOps,
|
|
73
75
|
up: renderUp(schemaOps, target.dialect),
|
|
@@ -116,23 +118,21 @@ export async function runDbMigrate(opts) {
|
|
|
116
118
|
};
|
|
117
119
|
}
|
|
118
120
|
// baseline 감지 — journal 이 없는데 createTable 이 포함 → 안내(§7.5.3).
|
|
119
|
-
const hadJournal = await journalExists(target.db);
|
|
121
|
+
const hadJournal = await journalExists(target.db, target.adapter);
|
|
120
122
|
const hasCreate = plan.ops.some((o) => o.kind === 'createTable');
|
|
121
123
|
const baselineNote = !hadJournal && hasCreate
|
|
122
124
|
? ` ℹ 최초 마이그레이션(_gaon_migrations 미존재) — 이 배치가 baseline 이 됩니다.\n` +
|
|
123
125
|
` → 기존 DB 를 이어 쓰고 있었다면, 먼저 'gaon db diff' 로 계획을 확인하세요.`
|
|
124
126
|
: '';
|
|
125
127
|
// 3) schema-diff apply — up 을 순차 적용하고 이력 한 행(kind='schema')을 남긴다.
|
|
126
|
-
await ensureJournal(target.db);
|
|
128
|
+
await ensureJournal(target.db, target.adapter);
|
|
127
129
|
const id = batchId(plan.up.length);
|
|
128
130
|
const summary = JSON.stringify(ops);
|
|
129
131
|
const downSql = JSON.stringify(plan.down);
|
|
130
132
|
const applySchema = async (db) => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
await sql.raw(stmt).execute(db);
|
|
135
|
-
}
|
|
133
|
+
// 결정 374: raw 실행 대신 applyStatements 경유 — 비호환 타입 변경(F5 · 결정 275)의
|
|
134
|
+
// 수리 안내(enrichAlterError · USING 절 예시)가 정본 CLI 경로에서도 나온다.
|
|
135
|
+
await applyStatements(db, [...plan.up]);
|
|
136
136
|
await recordEntry(db, {
|
|
137
137
|
id,
|
|
138
138
|
kind: 'schema',
|
package/dist/db/replay.js
CHANGED
|
@@ -66,12 +66,12 @@ export async function replayPending(opts) {
|
|
|
66
66
|
const all = listMigrationFiles(opts.cwd);
|
|
67
67
|
if (all.length === 0)
|
|
68
68
|
return { applied: [], pending: [], all: [] };
|
|
69
|
-
const done = await appliedFileMigrations(opts.db);
|
|
69
|
+
const done = await appliedFileMigrations(opts.db, opts.adapter);
|
|
70
70
|
const pending = all.filter((f) => !done.has(f));
|
|
71
71
|
if (opts.dryRun || pending.length === 0) {
|
|
72
72
|
return { applied: [], pending, all };
|
|
73
73
|
}
|
|
74
|
-
await ensureJournal(opts.db);
|
|
74
|
+
await ensureJournal(opts.db, opts.adapter);
|
|
75
75
|
const applied = [];
|
|
76
76
|
for (const file of pending) {
|
|
77
77
|
const mig = await loadMigration(opts.cwd, file);
|
|
@@ -104,7 +104,7 @@ export async function replayPending(opts) {
|
|
|
104
104
|
* 되돌린 뒤 이력 행을 지운다. --dry-run 은 대상만 보고 실행하지 않는다.
|
|
105
105
|
*/
|
|
106
106
|
export async function rollbackLast(opts) {
|
|
107
|
-
const entry = await latestEntry(opts.db);
|
|
107
|
+
const entry = await latestEntry(opts.db, opts.adapter);
|
|
108
108
|
if (!entry)
|
|
109
109
|
return { rolledBack: null, kind: null };
|
|
110
110
|
if (opts.dryRun)
|
package/dist/db/resolve.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ConnectionConfig, type TableDef } from '@gaonjs/data';
|
|
1
|
+
import { type ConnectionConfig, type SqlConnectionConfig, type TableDef } from '@gaonjs/data';
|
|
2
2
|
import type { Kysely } from 'kysely';
|
|
3
3
|
import { type Dialect, type AdapterName } from '@gaonjs/data';
|
|
4
4
|
export interface ResolveDbOptions {
|
|
@@ -48,4 +48,14 @@ export interface ConnectionKeysByKind {
|
|
|
48
48
|
* config 선언이 없으면(env-only) SQL ['main'] 폴백 — listConnectionKeys 와 정합.
|
|
49
49
|
*/
|
|
50
50
|
export declare function listConnectionKeysByKind(cwd: string, configPath?: string): Promise<ConnectionKeysByKind>;
|
|
51
|
+
/**
|
|
52
|
+
* 결정 367: 선언된 **전 SQL 커넥션**의 설정 목록 — `gaon db seed` 가 시드 실행 전에
|
|
53
|
+
* 전부 등록하는 데 쓴다(시드는 커넥션을 몰라도 된다 · §10.1 — 모델이 자기 커넥션에
|
|
54
|
+
* 자동 바인딩). config 선언이 없으면(env-only) GAON_DATABASE_URL 기반 main 하나.
|
|
55
|
+
* 문서형(mongodb)은 제외(§7.4 — gaon db 는 SQL 축).
|
|
56
|
+
*/
|
|
57
|
+
export declare function listSqlConnectionConfigs(cwd: string, configPath?: string): Promise<Array<{
|
|
58
|
+
key: string;
|
|
59
|
+
cfg: SqlConnectionConfig;
|
|
60
|
+
}>>;
|
|
51
61
|
export declare function resolveDbTarget(opts: ResolveDbOptions): Promise<ResolvedDbTarget>;
|
package/dist/db/resolve.js
CHANGED
|
@@ -119,6 +119,23 @@ export async function listConnectionKeysByKind(cwd, configPath) {
|
|
|
119
119
|
}
|
|
120
120
|
return { sql, mongo };
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* 결정 367: 선언된 **전 SQL 커넥션**의 설정 목록 — `gaon db seed` 가 시드 실행 전에
|
|
124
|
+
* 전부 등록하는 데 쓴다(시드는 커넥션을 몰라도 된다 · §10.1 — 모델이 자기 커넥션에
|
|
125
|
+
* 자동 바인딩). config 선언이 없으면(env-only) GAON_DATABASE_URL 기반 main 하나.
|
|
126
|
+
* 문서형(mongodb)은 제외(§7.4 — gaon db 는 SQL 축).
|
|
127
|
+
*/
|
|
128
|
+
export async function listSqlConnectionConfigs(cwd, configPath) {
|
|
129
|
+
const { config } = await loadDbConfig(resolvePath(cwd), configPath);
|
|
130
|
+
const entries = Object.entries(config.db ?? {});
|
|
131
|
+
if (entries.length === 0) {
|
|
132
|
+
const url = env.optional('GAON_DATABASE_URL');
|
|
133
|
+
return url ? [{ key: 'main', cfg: dbConfigFromUrl(url) }] : [];
|
|
134
|
+
}
|
|
135
|
+
return entries
|
|
136
|
+
.filter(([, cfg]) => cfg?.adapter !== 'mongodb')
|
|
137
|
+
.map(([key, cfg]) => ({ key, cfg: cfg }));
|
|
138
|
+
}
|
|
122
139
|
export async function resolveDbTarget(opts) {
|
|
123
140
|
const cwd = resolvePath(opts.cwd);
|
|
124
141
|
// 사용자가 --config <path> 를 준 경우 그 경로에서 default export 를 로드한다.
|
|
@@ -153,9 +170,14 @@ export async function resolveDbTarget(opts) {
|
|
|
153
170
|
// 결정 279(§7.4): 문서형(mongodb) 커넥션은 마이그레이션이 없다 — SQL 전용 경로(diff/migrate/
|
|
154
171
|
// introspect)로 흘리지 않고 명확히 안내한다. 몽고 인덱스는 Mongoose 스키마 선언으로 관리한다.
|
|
155
172
|
if (connCfg.adapter === 'mongodb') {
|
|
173
|
+
// 결정 384: 안내문을 실동작(결정 333·365)으로 정정 — 이전 문구("autoIndex 기본값이
|
|
174
|
+
// 부팅 시 보장 · dev·prod 공통")는 운영 autoIndex=false 이후 거짓 안내였다.
|
|
156
175
|
throw new Error(`[gaon db] 커넥션 '${dbKey}' 은 문서형(mongodb)입니다 — 문서형은 마이그레이션이 없습니다(§7.4).\n` +
|
|
157
|
-
` → 몽고 인덱스는 Mongoose 스키마 선언(index:true / schema.index())으로 관리합니다
|
|
158
|
-
`
|
|
176
|
+
` → 몽고 인덱스는 Mongoose 스키마 선언(index:true / schema.index())으로 관리합니다 — dev·test 는 autoIndex 가\n` +
|
|
177
|
+
` 첫 사용 시 만들고, 운영(NODE_ENV=production)은 autoIndex 기본 false 라 배포 절차에서 syncMongoIndexes() 로\n` +
|
|
178
|
+
` 명시 동기화합니다(결정 333·365).\n` +
|
|
179
|
+
` → gaon db 서브커맨드(diff/migrate/status/seed/reset)는 SQL 커넥션(postgres·mysql) 전용입니다 —\n` +
|
|
180
|
+
` 키 생략 시 순회가 문서형을 자동으로 건너뜁니다(결정 292).`);
|
|
159
181
|
}
|
|
160
182
|
const db = createDb(connCfg);
|
|
161
183
|
registerConnection(dbKey, db, connCfg.adapter);
|
package/dist/db/status.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// db/migrations/*.ts 각각의 적용/미적용과, 스키마↔DB 사이에 아직 반영 안 된
|
|
4
4
|
// 변경(schema drift) 수를 함께 보여준다. 아무것도 실행하지 않는다.
|
|
5
|
-
import { computeMigration } from '@gaonjs/data';
|
|
5
|
+
import { computeMigration, summarizeOp, partitionDeferredOps } from '@gaonjs/data';
|
|
6
6
|
import { resolveDbTarget } from './resolve.js';
|
|
7
7
|
import { appliedFileMigrations } from './journal.js';
|
|
8
|
+
import { describeDeferred } from './migrate.js';
|
|
8
9
|
import { listMigrationFiles } from './replay.js';
|
|
9
10
|
export async function runDbStatus(opts) {
|
|
10
11
|
const target = await resolveDbTarget({
|
|
@@ -14,16 +15,23 @@ export async function runDbStatus(opts) {
|
|
|
14
15
|
});
|
|
15
16
|
try {
|
|
16
17
|
const files = listMigrationFiles(opts.cwd);
|
|
17
|
-
const done = await appliedFileMigrations(target.db);
|
|
18
|
+
const done = await appliedFileMigrations(target.db, target.adapter);
|
|
18
19
|
const migrations = files.map((f) => ({ file: `db/migrations/${f}`, applied: done.has(f) }));
|
|
19
20
|
const pendingFiles = migrations.filter((m) => !m.applied).length;
|
|
20
21
|
// migrate 는 스키마에 없는 테이블을 자동 DROP 하지 않으므로(결정 39 · §4.8)
|
|
21
22
|
// dropTable 은 drift 로 세지 않는다 — 별도로 참고 정보(extraTables)로만 보고.
|
|
22
23
|
const plan = await computeMigration(target.db, target.tables, target.dialect);
|
|
23
|
-
|
|
24
|
+
// 결정 326: deferred aux drop(제약·인덱스·기본값 제거)도 migrate 가 자동 적용하지
|
|
25
|
+
// 않는다(결정 220) — drift 로 세면 "'gaon db migrate' 로 반영하세요" 가 영원히 안
|
|
26
|
+
// 지워지는 거짓 안내가 된다. dropTable 과 같은 축의 참고 항목으로 분리한다.
|
|
27
|
+
// 결정 366: 재생성 쌍(인덱스 method·enum 값 변경)의 drop 반쪽은 migrate 가 자동
|
|
28
|
+
// 적용하므로 deferred 가 아니라 drift(schemaChanges)로 계상한다 — migrate 와 한 판정.
|
|
29
|
+
const { apply, deferred } = partitionDeferredOps(plan.ops);
|
|
30
|
+
const extraTables = deferred
|
|
24
31
|
.filter((o) => o.kind === 'dropTable')
|
|
25
32
|
.map((o) => o.name);
|
|
26
|
-
const
|
|
33
|
+
const deferredDrops = deferred.filter((o) => o.kind !== 'dropTable');
|
|
34
|
+
const schemaChanges = apply.length;
|
|
27
35
|
const lines = [];
|
|
28
36
|
if (migrations.length === 0) {
|
|
29
37
|
lines.push(` [${opts.dbKey}] 마이그레이션 파일 없음 (db/migrations/).`);
|
|
@@ -42,6 +50,10 @@ export async function runDbStatus(opts) {
|
|
|
42
50
|
if (extraTables.length > 0) {
|
|
43
51
|
lines.push(` 참고: 스키마에 없는 테이블 ${extraTables.length}개(자동 DROP 안 함): ${extraTables.join(', ')}`);
|
|
44
52
|
}
|
|
53
|
+
if (deferredDrops.length > 0) {
|
|
54
|
+
lines.push(` 참고: 스키마에서 사라진 제약·인덱스·기본값 ${deferredDrops.length}개(자동 제거 안 함 — ` +
|
|
55
|
+
`'gaon db diff' 의 down SQL 로 손작성 마이그): ${deferredDrops.map(describeDeferred).join(', ')}`);
|
|
56
|
+
}
|
|
45
57
|
return {
|
|
46
58
|
exitCode: 0,
|
|
47
59
|
text: lines.join('\n'),
|
|
@@ -52,6 +64,7 @@ export async function runDbStatus(opts) {
|
|
|
52
64
|
pendingFiles,
|
|
53
65
|
schemaChanges,
|
|
54
66
|
extraTables,
|
|
67
|
+
deferredDrops: deferredDrops.map(summarizeOp),
|
|
55
68
|
},
|
|
56
69
|
};
|
|
57
70
|
}
|
package/dist/db.js
CHANGED
|
@@ -11,7 +11,7 @@ import { join } from 'node:path';
|
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
import { createDb, registerConnection, destroyAllConnections, isSeedDef, } from '@gaonjs/data';
|
|
13
13
|
import { registerTsResolve } from './tsResolve.js';
|
|
14
|
-
import { resolveDbTarget } from './db/resolve.js';
|
|
14
|
+
import { resolveDbTarget, listSqlConnectionConfigs } from './db/resolve.js';
|
|
15
15
|
import { registerInProjectData, isSplitDataInstance } from './db/projectData.js';
|
|
16
16
|
/** DB URL 에서 어댑터를 추정한다(gaon work 와 동일 규칙). */
|
|
17
17
|
function dbConfigFromUrl(url) {
|
|
@@ -77,15 +77,36 @@ export async function runDbSeedCommand(opts = {}) {
|
|
|
77
77
|
throw new Error(`[gaon db seed] 커넥션 '${dbKey}' 에 url 이 없어 시드를 실행할 수 없습니다.\n` +
|
|
78
78
|
` → gaon.config.ts 의 db.${dbKey} 에 url 을 두거나 .env 의 GAON_DATABASE_URL 을 설정하세요.`);
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
// 결정 367: 시드는 커넥션을 몰라도 된다(§10.1 — 모델이 자기 커넥션에 자동 바인딩).
|
|
81
|
+
// dbKey 하나만 등록하면 ≥2 SQL 커넥션 프로젝트의 시드가 타 커넥션 모델의 '미등록'
|
|
82
|
+
// throw 로 항상 실패했다(그리고 그 에러가 결정 156 오진 안내로 이어졌다) — 선언된
|
|
83
|
+
// **전 SQL 커넥션**을 등록하고 시드는 1회만 실행한다. url 없는 보조 커넥션은 연결
|
|
84
|
+
// 불가라 건너뛴다(그 커넥션 모델을 시드가 쓰면 미등록 에러가 원인을 가리킨다).
|
|
85
|
+
const others = (await listSqlConnectionConfigs(root, opts.configPath)).filter((c) => c.key !== dbKey && ('url' in c.cfg ? c.cfg.url : true));
|
|
86
|
+
const registered = [dbKey];
|
|
87
|
+
const projHandles = [];
|
|
88
|
+
const primaryHandle = await registerInProjectData(root, dbKey, { adapter: target.adapter, url: target.url, poolMax: 1 });
|
|
89
|
+
if (primaryHandle)
|
|
90
|
+
projHandles.push(primaryHandle);
|
|
91
|
+
for (const { key, cfg } of others) {
|
|
92
|
+
registerConnection(key, createDb(cfg), cfg.adapter);
|
|
93
|
+
const h = await registerInProjectData(root, key, { ...cfg, poolMax: 1 });
|
|
94
|
+
if (h)
|
|
95
|
+
projHandles.push(h);
|
|
96
|
+
registered.push(key);
|
|
97
|
+
}
|
|
81
98
|
try {
|
|
82
99
|
const def = await loadSeed(root);
|
|
83
100
|
await runSeed(def, root);
|
|
84
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
exitCode: 0,
|
|
103
|
+
text: `${SEED_OK.text} (커넥션: ${registered.join(', ')})`,
|
|
104
|
+
json: { command: 'seed', ok: true, connections: registered },
|
|
105
|
+
};
|
|
85
106
|
}
|
|
86
107
|
finally {
|
|
87
|
-
|
|
88
|
-
await
|
|
108
|
+
for (const h of projHandles)
|
|
109
|
+
await h.close();
|
|
89
110
|
await target.close();
|
|
90
111
|
}
|
|
91
112
|
}
|