@gaonjs/cli 0.2.0 → 0.4.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/__fixtures__/db-minimal/domain/schema/widgets.d.ts +12 -0
- package/dist/__fixtures__/db-minimal/domain/schema/widgets.js +7 -0
- package/dist/__fixtures__/db-minimal/gaon.config.d.ts +2 -0
- package/dist/__fixtures__/db-minimal/gaon.config.js +11 -0
- package/dist/commands/db.d.ts +20 -0
- package/dist/commands/db.js +74 -0
- package/dist/commands/dev.d.ts +68 -0
- package/dist/commands/dev.js +287 -0
- package/dist/commands/g.d.ts +26 -0
- package/dist/commands/g.js +124 -0
- package/dist/db/diff.d.ts +17 -0
- package/dist/db/diff.js +57 -0
- package/dist/db/index.d.ts +4 -0
- package/dist/db/index.js +8 -0
- package/dist/db/migrate.d.ts +16 -0
- package/dist/db/migrate.js +173 -0
- package/dist/db/reset.d.ts +18 -0
- package/dist/db/reset.js +150 -0
- package/dist/db/resolve.d.ts +32 -0
- package/dist/db/resolve.js +130 -0
- package/dist/dev/console.d.ts +39 -0
- package/dist/dev/console.js +100 -0
- package/dist/dev/docker.d.ts +52 -0
- package/dist/dev/docker.js +163 -0
- package/dist/dev/index.d.ts +14 -0
- package/dist/dev/index.js +10 -0
- package/dist/dev/tsc.d.ts +41 -0
- package/dist/dev/tsc.js +127 -0
- package/dist/dev/watcher.d.ts +50 -0
- package/dist/dev/watcher.js +95 -0
- package/dist/dev.d.ts +1 -16
- package/dist/dev.js +10 -66
- package/dist/doctor/connections.d.ts +14 -0
- package/dist/doctor/connections.js +168 -0
- package/dist/doctor/dependency-direction.d.ts +11 -0
- package/dist/doctor/dependency-direction.js +186 -0
- package/dist/doctor/migration-diff.d.ts +11 -0
- package/dist/doctor/migration-diff.js +109 -0
- package/dist/doctor/n-plus-one.d.ts +5 -0
- package/dist/doctor/n-plus-one.js +242 -0
- package/dist/doctor/reporter.d.ts +5 -0
- package/dist/doctor/reporter.js +41 -0
- package/dist/doctor/response-mixing.d.ts +12 -0
- package/dist/doctor/response-mixing.js +158 -0
- package/dist/doctor/setup.d.ts +26 -0
- package/dist/doctor/setup.js +52 -0
- package/dist/doctor/types.d.ts +37 -0
- package/dist/doctor/types.js +34 -0
- package/dist/doctor.d.ts +40 -23
- package/dist/doctor.js +131 -202
- package/dist/index.d.ts +14 -2
- package/dist/index.js +171 -28
- package/dist/scaffold/controller.d.ts +12 -0
- package/dist/scaffold/controller.js +50 -0
- package/dist/scaffold/index.d.ts +20 -0
- package/dist/scaffold/index.js +41 -0
- package/dist/scaffold/inflect.d.ts +19 -0
- package/dist/scaffold/inflect.js +50 -0
- package/dist/scaffold/job.d.ts +3 -0
- package/dist/scaffold/job.js +46 -0
- package/dist/scaffold/model.d.ts +8 -0
- package/dist/scaffold/model.js +66 -0
- package/dist/scaffold/page.d.ts +7 -0
- package/dist/scaffold/page.js +46 -0
- package/dist/serve.d.ts +18 -0
- package/dist/serve.js +79 -0
- package/dist/templates/auth/auth.wiring.ts.tpl +1 -1
- package/dist/templates/auth/session.controller.ts.tpl +1 -1
- package/package.json +4 -3
package/dist/db/diff.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @gaonjs/cli · `gaon db diff` — 스키마(desired) ↔ DB(current) 차이 계산 (M9-D)
|
|
2
|
+
//
|
|
3
|
+
// 순수 diff — 아무것도 실행하지 않는다(SQL 만 출력). computeMigration 이
|
|
4
|
+
// 방언별 up/down 을 이미 만든다(@gaonjs/data). 이 함수는 배선·표현 담당.
|
|
5
|
+
import { computeMigration } from '@gaonjs/data';
|
|
6
|
+
import { resolveDbTarget } from './resolve.js';
|
|
7
|
+
function opSummary(op) {
|
|
8
|
+
switch (op.kind) {
|
|
9
|
+
case 'createTable':
|
|
10
|
+
return { kind: op.kind, table: op.table.name };
|
|
11
|
+
case 'dropTable':
|
|
12
|
+
return { kind: op.kind, table: op.name };
|
|
13
|
+
case 'addColumn':
|
|
14
|
+
case 'dropColumn':
|
|
15
|
+
case 'alterColumn':
|
|
16
|
+
return { kind: op.kind, table: op.table, column: op.column };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `gaon db diff` — 커넥션 대비 스키마 diff 를 계산해 텍스트·JSON 을 낳는다.
|
|
21
|
+
* 아무것도 적용하지 않는다. exitCode 는 항상 0(변경 없음도 성공 · CI 에서
|
|
22
|
+
* 변경 존재 여부만 보고 싶으면 --json 의 changes 를 읽어라).
|
|
23
|
+
*/
|
|
24
|
+
export async function runDbDiff(opts) {
|
|
25
|
+
const target = await resolveDbTarget({
|
|
26
|
+
cwd: opts.cwd,
|
|
27
|
+
dbKey: opts.dbKey,
|
|
28
|
+
configPath: opts.configPath,
|
|
29
|
+
});
|
|
30
|
+
try {
|
|
31
|
+
const plan = await computeMigration(target.db, target.tables, target.dialect);
|
|
32
|
+
const ops = plan.ops.map(opSummary);
|
|
33
|
+
const json = {
|
|
34
|
+
command: 'diff',
|
|
35
|
+
db: opts.dbKey,
|
|
36
|
+
changes: ops.length,
|
|
37
|
+
ops,
|
|
38
|
+
up: plan.up,
|
|
39
|
+
down: plan.down,
|
|
40
|
+
};
|
|
41
|
+
const text = ops.length === 0
|
|
42
|
+
? ` [${opts.dbKey}] 변경 없음 — 스키마와 DB 가 일치합니다.`
|
|
43
|
+
: ` [${opts.dbKey}] ${ops.length}개 변경\n` +
|
|
44
|
+
` -- up --\n${indent(plan.up.join('\n'))}\n` +
|
|
45
|
+
` -- down --\n${indent(plan.down.join('\n'))}`;
|
|
46
|
+
return { exitCode: 0, text, json };
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await target.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function indent(text) {
|
|
53
|
+
return text
|
|
54
|
+
.split('\n')
|
|
55
|
+
.map((l) => (l ? ` ${l}` : l))
|
|
56
|
+
.join('\n');
|
|
57
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { runDbDiff, type DbDiffOptions, type DbDiffResult, } from './diff.js';
|
|
2
|
+
export { runDbMigrate, type DbMigrateOptions, type DbMigrateResult, } from './migrate.js';
|
|
3
|
+
export { runDbReset, type DbResetOptions, type DbResetResult, } from './reset.js';
|
|
4
|
+
export { resolveDbTarget, type ResolveDbOptions, type ResolvedDbTarget, } from './resolve.js';
|
package/dist/db/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// @gaonjs/cli · gaon db 서브커맨드 공용 모듈 (M9-D)
|
|
2
|
+
//
|
|
3
|
+
// dispatcher 는 commands/db.ts (runDbCommand) 가 담당하고, 여기서는 개별
|
|
4
|
+
// 서브커맨드 로직·타입만 노출한다. seed 는 기존 db.ts (M8) 를 그대로 위임.
|
|
5
|
+
export { runDbDiff, } from './diff.js';
|
|
6
|
+
export { runDbMigrate, } from './migrate.js';
|
|
7
|
+
export { runDbReset, } from './reset.js';
|
|
8
|
+
export { resolveDbTarget, } from './resolve.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface DbMigrateOptions {
|
|
2
|
+
readonly cwd: string;
|
|
3
|
+
readonly dbKey: string;
|
|
4
|
+
readonly json: boolean;
|
|
5
|
+
readonly dryRun: boolean;
|
|
6
|
+
readonly configPath?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DbMigrateResult {
|
|
9
|
+
readonly exitCode: number;
|
|
10
|
+
readonly text: string;
|
|
11
|
+
readonly json: unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `gaon db migrate` — 진짜 apply. 실 DB 필요(§9).
|
|
15
|
+
*/
|
|
16
|
+
export declare function runDbMigrate(opts: DbMigrateOptions): Promise<DbMigrateResult>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// @gaonjs/cli · `gaon db migrate` — 스키마 diff 적용 + 이력 기록 (M9-D)
|
|
2
|
+
//
|
|
3
|
+
// 흐름:
|
|
4
|
+
// 1) resolveDbTarget → computeMigration 로 up/down·ops 산출.
|
|
5
|
+
// 2) --dry-run 이면 SQL 만 출력하고 종료(실행 X).
|
|
6
|
+
// 3) 트랜잭션 안에서 up 을 순차 실행하고, 완료 후 _gaon_migrations 에
|
|
7
|
+
// 배치 한 행을 남긴다(감사 로그 — 스냅샷 기반 diff 는 재실행 시
|
|
8
|
+
// idempotent 지만, 이력은 언제 무엇이 적용됐는지를 남긴다).
|
|
9
|
+
// 4) journal 미존재 상태에서 createTable 이 포함되면 §7.5.3 방식 안내를
|
|
10
|
+
// 함께 낸다(baseline · 사용자가 기존 DB 를 갖고 있다면 diff 를 먼저
|
|
11
|
+
// 확인하도록).
|
|
12
|
+
//
|
|
13
|
+
// 트랜잭션: postgres 는 트랜잭셔널 DDL 이라 안전, mysql/mariadb 는 DDL 이
|
|
14
|
+
// autocommit 이라 실패 시 부분 적용이 남을 수 있다 — mysql 에서는 이력 행
|
|
15
|
+
// 삽입만 트랜잭션에 감싼다(에러 안내로 사용자에게 알린다).
|
|
16
|
+
import { sql } from 'kysely';
|
|
17
|
+
import { computeMigration, MIGRATIONS_TABLE } from '@gaonjs/data';
|
|
18
|
+
import { resolveDbTarget } from './resolve.js';
|
|
19
|
+
function opSummary(op) {
|
|
20
|
+
switch (op.kind) {
|
|
21
|
+
case 'createTable':
|
|
22
|
+
return { kind: op.kind, table: op.table.name };
|
|
23
|
+
case 'dropTable':
|
|
24
|
+
return { kind: op.kind, table: op.name };
|
|
25
|
+
case 'addColumn':
|
|
26
|
+
case 'dropColumn':
|
|
27
|
+
case 'alterColumn':
|
|
28
|
+
return { kind: op.kind, table: op.table, column: op.column };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** _gaon_migrations 존재 여부. information_schema 조회로 방언 무관하게 검사. */
|
|
32
|
+
async function journalExists(db) {
|
|
33
|
+
const rows = await sql `
|
|
34
|
+
select count(*)::text as n
|
|
35
|
+
from information_schema.tables
|
|
36
|
+
where table_name = ${MIGRATIONS_TABLE}
|
|
37
|
+
`.execute(db);
|
|
38
|
+
const first = rows.rows[0];
|
|
39
|
+
if (!first)
|
|
40
|
+
return false;
|
|
41
|
+
return Number(first.n) > 0;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* _gaon_migrations 를 만든다(존재하면 no-op). 컬럼:
|
|
45
|
+
* id 문자열 PK — 배치 식별자 (`<epochMs>-<적용문수>`).
|
|
46
|
+
* applied_at 타임스탬프 — 서버 기준 시각.
|
|
47
|
+
* db_key 커넥션 키 (§4.5).
|
|
48
|
+
* statements 이 배치의 SQL 문 수.
|
|
49
|
+
* summary JSON 문자열(ops 요약).
|
|
50
|
+
* text/varchar/timestamp/integer 는 postgres·mysql 공통 지원 타입.
|
|
51
|
+
*/
|
|
52
|
+
async function ensureJournal(db) {
|
|
53
|
+
// postgres·mysql 공통 CREATE TABLE IF NOT EXISTS.
|
|
54
|
+
// 기본값 표현은 각 방언이 자동으로 처리(now()/CURRENT_TIMESTAMP 모두 표준).
|
|
55
|
+
await sql.raw(`create table if not exists ${MIGRATIONS_TABLE} (` +
|
|
56
|
+
` id varchar(64) not null primary key,` +
|
|
57
|
+
` applied_at timestamp not null,` +
|
|
58
|
+
` db_key varchar(64) not null,` +
|
|
59
|
+
` statements integer not null,` +
|
|
60
|
+
` summary text not null` +
|
|
61
|
+
`)`).execute(db);
|
|
62
|
+
}
|
|
63
|
+
/** 배치 식별자 — 초 단위 epoch + 적용 문수. 사람도 읽고 정렬도 된다. */
|
|
64
|
+
function batchId(count) {
|
|
65
|
+
return `${Date.now()}-${count}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `gaon db migrate` — 진짜 apply. 실 DB 필요(§9).
|
|
69
|
+
*/
|
|
70
|
+
export async function runDbMigrate(opts) {
|
|
71
|
+
const target = await resolveDbTarget({
|
|
72
|
+
cwd: opts.cwd,
|
|
73
|
+
dbKey: opts.dbKey,
|
|
74
|
+
configPath: opts.configPath,
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
const plan = await computeMigration(target.db, target.tables, target.dialect);
|
|
78
|
+
const ops = plan.ops.map(opSummary);
|
|
79
|
+
// 아무것도 적용할 게 없다 — 성공으로 취급(스냅샷 diff 관례).
|
|
80
|
+
if (plan.up.length === 0) {
|
|
81
|
+
return {
|
|
82
|
+
exitCode: 0,
|
|
83
|
+
text: ` [${opts.dbKey}] 적용할 마이그레이션이 없습니다 — 스키마와 DB 가 일치합니다.`,
|
|
84
|
+
json: {
|
|
85
|
+
command: 'migrate',
|
|
86
|
+
db: opts.dbKey,
|
|
87
|
+
applied: 0,
|
|
88
|
+
ops: [],
|
|
89
|
+
dryRun: opts.dryRun,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// --dry-run: SQL 만 출력. 이력 테이블도 만들지 않는다.
|
|
94
|
+
if (opts.dryRun) {
|
|
95
|
+
return {
|
|
96
|
+
exitCode: 0,
|
|
97
|
+
text: ` [${opts.dbKey}] --dry-run · 적용하지 않고 SQL 만 출력합니다 (${plan.up.length}문)\n` +
|
|
98
|
+
plan.up.map((s) => ` ${s}`).join('\n'),
|
|
99
|
+
json: {
|
|
100
|
+
command: 'migrate',
|
|
101
|
+
db: opts.dbKey,
|
|
102
|
+
applied: 0,
|
|
103
|
+
ops,
|
|
104
|
+
up: plan.up,
|
|
105
|
+
dryRun: true,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// baseline 감지 — journal 이 없는데 createTable 이 포함 → 안내(§7.5.3).
|
|
110
|
+
// 에러가 아니라 정보성 노트로 처리(실행은 진행). 사용자가 기존 DB 를 갖고
|
|
111
|
+
// 있다면 diff 로 먼저 계획을 봐야 한다.
|
|
112
|
+
const hadJournal = await journalExists(target.db);
|
|
113
|
+
const hasCreate = plan.ops.some((o) => o.kind === 'createTable');
|
|
114
|
+
const baselineNote = !hadJournal && hasCreate
|
|
115
|
+
? ` ℹ 최초 마이그레이션(_gaon_migrations 미존재) — 이 배치가 baseline 이 됩니다.\n` +
|
|
116
|
+
` → 기존 DB 를 이어 쓰고 있었다면, 먼저 'gaon db diff' 로 계획을 확인하세요.\n` +
|
|
117
|
+
` → 이미 존재하는 테이블은 스키마와 일치해야 합니다(불일치 시 alterColumn 이 함께 실행됩니다).`
|
|
118
|
+
: '';
|
|
119
|
+
// apply — up 을 순차 적용하고 이력 한 행을 남긴다. postgres 는 트랜잭셔널 DDL.
|
|
120
|
+
// ensureJournal 은 트랜잭션 밖에서(mysql 호환) 만들고, insert 만 트랜잭션 안.
|
|
121
|
+
await ensureJournal(target.db);
|
|
122
|
+
const id = batchId(plan.up.length);
|
|
123
|
+
const summary = JSON.stringify(ops);
|
|
124
|
+
if (target.adapter === 'postgres') {
|
|
125
|
+
// postgres: DDL + insert 를 한 트랜잭션에 넣는다(원자적).
|
|
126
|
+
await target.db.transaction().execute(async (trx) => {
|
|
127
|
+
for (const stmt of plan.up) {
|
|
128
|
+
if (stmt.trim().startsWith('--'))
|
|
129
|
+
continue;
|
|
130
|
+
await sql.raw(stmt).execute(trx);
|
|
131
|
+
}
|
|
132
|
+
await sql `
|
|
133
|
+
insert into ${sql.ref(MIGRATIONS_TABLE)}
|
|
134
|
+
(id, applied_at, db_key, statements, summary)
|
|
135
|
+
values (${id}, ${new Date()}, ${opts.dbKey}, ${plan.up.length}, ${summary})
|
|
136
|
+
`.execute(trx);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// mysql/mariadb: DDL 은 autocommit 이라 트랜잭션에 감싸도 롤백되지 않는다.
|
|
141
|
+
// 정직하게 순차 실행하고, 이력만 별도로 남긴다(§4.5 방언 차이).
|
|
142
|
+
for (const stmt of plan.up) {
|
|
143
|
+
if (stmt.trim().startsWith('--'))
|
|
144
|
+
continue;
|
|
145
|
+
await sql.raw(stmt).execute(target.db);
|
|
146
|
+
}
|
|
147
|
+
await sql `
|
|
148
|
+
insert into ${sql.ref(MIGRATIONS_TABLE)}
|
|
149
|
+
(id, applied_at, db_key, statements, summary)
|
|
150
|
+
values (${id}, ${new Date()}, ${opts.dbKey}, ${plan.up.length}, ${summary})
|
|
151
|
+
`.execute(target.db);
|
|
152
|
+
}
|
|
153
|
+
const text = (baselineNote ? baselineNote + '\n' : '') +
|
|
154
|
+
` [${opts.dbKey}] ${plan.up.length}개 문 적용 완료 (batch ${id}).`;
|
|
155
|
+
return {
|
|
156
|
+
exitCode: 0,
|
|
157
|
+
text,
|
|
158
|
+
json: {
|
|
159
|
+
command: 'migrate',
|
|
160
|
+
db: opts.dbKey,
|
|
161
|
+
applied: plan.up.length,
|
|
162
|
+
ops,
|
|
163
|
+
up: plan.up,
|
|
164
|
+
batchId: id,
|
|
165
|
+
baseline: !hadJournal && hasCreate,
|
|
166
|
+
dryRun: false,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
await target.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface DbResetOptions {
|
|
2
|
+
readonly cwd: string;
|
|
3
|
+
readonly dbKey: string;
|
|
4
|
+
readonly json: boolean;
|
|
5
|
+
readonly yes: boolean;
|
|
6
|
+
readonly dryRun: boolean;
|
|
7
|
+
readonly configPath?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DbResetResult {
|
|
10
|
+
readonly exitCode: number;
|
|
11
|
+
readonly text: string;
|
|
12
|
+
readonly json: unknown;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* `gaon db reset` — 위험 명령. NODE_ENV=production 은 무조건 거부.
|
|
16
|
+
* --yes 로 실 파괴 확증. --dry-run 은 무엇을 지울지·만들지만 출력.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runDbReset(opts: DbResetOptions): Promise<DbResetResult>;
|
package/dist/db/reset.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// @gaonjs/cli · `gaon db reset` — 스키마 초기화 후 재마이그+시드 (M9-D)
|
|
2
|
+
//
|
|
3
|
+
// 위험 명령이라 fail-closed 로 잠근다(CLAUDE.md §8 보안 기본 켬):
|
|
4
|
+
// 1) NODE_ENV=production 이면 무조건 거부(옵션 무관).
|
|
5
|
+
// 2) --yes 없이는 실행하지 않는다(사용자 확증).
|
|
6
|
+
// 두 게이트 모두 통과 시 introspect → DROP ALL → 재마이그 → seed 순.
|
|
7
|
+
//
|
|
8
|
+
// seed 는 M8 존재 로직(runDbSeedCommand)을 그대로 위임한다 — domain/seed.ts
|
|
9
|
+
// 가 없으면 건너뛴다(파일 없음은 정상 케이스).
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { sql } from 'kysely';
|
|
13
|
+
import { snapshotFromDb, MIGRATIONS_TABLE } from '@gaonjs/data';
|
|
14
|
+
import { resolveDbTarget } from './resolve.js';
|
|
15
|
+
import { runDbMigrate } from './migrate.js';
|
|
16
|
+
import { runDbSeedCommand } from '../db.js';
|
|
17
|
+
/**
|
|
18
|
+
* `gaon db reset` — 위험 명령. NODE_ENV=production 은 무조건 거부.
|
|
19
|
+
* --yes 로 실 파괴 확증. --dry-run 은 무엇을 지울지·만들지만 출력.
|
|
20
|
+
*/
|
|
21
|
+
export async function runDbReset(opts) {
|
|
22
|
+
// 1) production fail-closed — 옵션·플래그 무관하게 거부.
|
|
23
|
+
if (process.env.NODE_ENV === 'production') {
|
|
24
|
+
const msg = ` ✗ [gaon db reset] NODE_ENV=production 에서 실행할 수 없습니다.\n` +
|
|
25
|
+
` → 실 운영 데이터 파괴 위험 · 개발/테스트 환경에서만 사용하세요.\n` +
|
|
26
|
+
` → 정말 필요하면 별도 절차(백업 + 수동 SQL)로 진행하고, 이 명령은\n` +
|
|
27
|
+
` NODE_ENV 를 비운(또는 development/test) 셸에서만 씁니다.`;
|
|
28
|
+
return {
|
|
29
|
+
exitCode: 1,
|
|
30
|
+
text: msg,
|
|
31
|
+
json: {
|
|
32
|
+
command: 'reset',
|
|
33
|
+
db: opts.dbKey,
|
|
34
|
+
ok: false,
|
|
35
|
+
reason: 'production-fail-closed',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// 2) --yes 게이트 — 사용자 확증. 프롬프트는 CLI 진입점에서 다루고,
|
|
40
|
+
// 이 함수는 순수 로직으로만 두어 --json 자동화와 정합.
|
|
41
|
+
if (!opts.yes) {
|
|
42
|
+
const msg = ` ✗ [gaon db reset] --yes 없이 실행할 수 없습니다 (실 데이터 파괴 확증 필요).\n` +
|
|
43
|
+
` → 확인 후 다음처럼 실행하세요:\n` +
|
|
44
|
+
` gaon db reset --yes --db ${opts.dbKey}\n` +
|
|
45
|
+
` → 무엇을 지울지·만들지만 보려면: gaon db reset --dry-run --db ${opts.dbKey}`;
|
|
46
|
+
// --dry-run 만 있고 --yes 없는 경우, dry-run 은 진행한다(파괴하지 않음).
|
|
47
|
+
if (!opts.dryRun) {
|
|
48
|
+
return {
|
|
49
|
+
exitCode: 1,
|
|
50
|
+
text: msg,
|
|
51
|
+
json: {
|
|
52
|
+
command: 'reset',
|
|
53
|
+
db: opts.dbKey,
|
|
54
|
+
ok: false,
|
|
55
|
+
reason: 'yes-required',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const target = await resolveDbTarget({
|
|
61
|
+
cwd: opts.cwd,
|
|
62
|
+
dbKey: opts.dbKey,
|
|
63
|
+
configPath: opts.configPath,
|
|
64
|
+
});
|
|
65
|
+
// seed 는 자체적으로 env GAON_DATABASE_URL 을 읽는다(M8 관례). config 에서
|
|
66
|
+
// 얻은 URL 이 있으면 그대로 넘겨 env 미설정 프로젝트도 reset → seed 가 돈다.
|
|
67
|
+
const resolvedUrl = target.url;
|
|
68
|
+
const droppedTables = [];
|
|
69
|
+
let dryRunResult;
|
|
70
|
+
try {
|
|
71
|
+
// 현재 DB 스냅샷 — 지울 테이블 목록. dialect.introspect 는 이미
|
|
72
|
+
// _gaon_migrations 를 제외하지만, reset 은 이력도 지워야 하므로 별도 처리.
|
|
73
|
+
const snapshot = await snapshotFromDb(target.db, target.dialect);
|
|
74
|
+
const userTables = Object.keys(snapshot).sort();
|
|
75
|
+
// dry-run: 무엇을 지울지·만들지를 계산해 출력. 실제 파괴는 안 함.
|
|
76
|
+
if (opts.dryRun) {
|
|
77
|
+
const dropSqls = userTables.map((t) => target.dialect.dropTable(t));
|
|
78
|
+
dryRunResult = {
|
|
79
|
+
exitCode: 0,
|
|
80
|
+
text: ` [${opts.dbKey}] --dry-run · 리셋 후 상태를 미리 봅니다.\n` +
|
|
81
|
+
` -- drop (${userTables.length}) --\n` +
|
|
82
|
+
dropSqls.map((s) => ` ${s}`).join('\n') +
|
|
83
|
+
(userTables.length ? '\n' : '') +
|
|
84
|
+
` drop table if exists ${MIGRATIONS_TABLE}\n` +
|
|
85
|
+
` → 이후 'gaon db migrate' 로 스키마 ${target.tables.length}개 테이블이 재생성됩니다.`,
|
|
86
|
+
json: {
|
|
87
|
+
command: 'reset',
|
|
88
|
+
db: opts.dbKey,
|
|
89
|
+
dryRun: true,
|
|
90
|
+
wouldDrop: [...userTables, MIGRATIONS_TABLE],
|
|
91
|
+
wouldMigrateTables: target.tables.map((t) => t.name),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// DROP ALL — 이력 테이블도 함께. 순서: 외래키 참조 대상이 나중에 지워지도록
|
|
97
|
+
// dialect.dropTable 이 CASCADE 를 붙인다(postgres). mysql 은 SET FOREIGN_KEY_CHECKS
|
|
98
|
+
// 우회가 필요하지만, 스키마 대상 테이블만 다루므로 대체로 안전.
|
|
99
|
+
for (const t of userTables) {
|
|
100
|
+
await sql.raw(target.dialect.dropTable(t)).execute(target.db);
|
|
101
|
+
droppedTables.push(t);
|
|
102
|
+
}
|
|
103
|
+
// 이력 테이블 삭제 — 존재하지 않아도 에러 없게.
|
|
104
|
+
await sql.raw(`drop table if exists ${MIGRATIONS_TABLE}`).execute(target.db);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
// dryRun·성공·실패 모든 경로에서 커넥션 정리.
|
|
109
|
+
await target.close();
|
|
110
|
+
}
|
|
111
|
+
if (dryRunResult)
|
|
112
|
+
return dryRunResult;
|
|
113
|
+
// 재마이그 — runDbMigrate 로 위임(이력 기록도 그대로 남는다).
|
|
114
|
+
const migrated = await runDbMigrate({
|
|
115
|
+
cwd: opts.cwd,
|
|
116
|
+
dbKey: opts.dbKey,
|
|
117
|
+
json: false, // 내부 호출 · 결과는 우리가 합쳐서 낸다
|
|
118
|
+
dryRun: false,
|
|
119
|
+
configPath: opts.configPath,
|
|
120
|
+
});
|
|
121
|
+
// seed — domain/seed.ts 가 있으면 실행. 없으면 조용히 건너뛴다.
|
|
122
|
+
const seedPath = join(opts.cwd, 'domain', 'seed.ts');
|
|
123
|
+
let seeded = false;
|
|
124
|
+
let seedText = '';
|
|
125
|
+
if (existsSync(seedPath)) {
|
|
126
|
+
const seedRes = await runDbSeedCommand({
|
|
127
|
+
root: opts.cwd,
|
|
128
|
+
json: false,
|
|
129
|
+
databaseUrl: resolvedUrl,
|
|
130
|
+
});
|
|
131
|
+
seeded = seedRes.exitCode === 0;
|
|
132
|
+
seedText = seedRes.text;
|
|
133
|
+
}
|
|
134
|
+
const migrateJson = migrated.json;
|
|
135
|
+
const text = ` [${opts.dbKey}] reset · ${droppedTables.length}개 테이블 삭제 + _gaon_migrations 초기화 → ` +
|
|
136
|
+
`${migrateJson.applied ?? 0}개 문 재적용` +
|
|
137
|
+
(seeded ? `\n${seedText}` : seedText ? `\n${seedText}` : '');
|
|
138
|
+
return {
|
|
139
|
+
exitCode: 0,
|
|
140
|
+
text,
|
|
141
|
+
json: {
|
|
142
|
+
command: 'reset',
|
|
143
|
+
db: opts.dbKey,
|
|
144
|
+
ok: true,
|
|
145
|
+
dropped: droppedTables,
|
|
146
|
+
applied: migrateJson.applied ?? 0,
|
|
147
|
+
seeded,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type TableDef } from '@gaonjs/data';
|
|
2
|
+
import type { Kysely } from 'kysely';
|
|
3
|
+
import { type Dialect, type AdapterName } from '@gaonjs/data';
|
|
4
|
+
export interface ResolveDbOptions {
|
|
5
|
+
readonly cwd: string;
|
|
6
|
+
readonly dbKey: string;
|
|
7
|
+
/** --config <path> 로 사용자가 지정한 config 경로. 없으면 cwd 관례. */
|
|
8
|
+
readonly configPath?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ResolvedDbTarget {
|
|
11
|
+
readonly dbKey: string;
|
|
12
|
+
readonly adapter: AdapterName;
|
|
13
|
+
readonly db: Kysely<any>;
|
|
14
|
+
readonly dialect: Dialect;
|
|
15
|
+
/** 이 커넥션에 바인딩된 스키마 테이블 정의 목록. */
|
|
16
|
+
readonly tables: TableDef[];
|
|
17
|
+
/** 실제 로드된 config 경로 (사람 출력용). 없으면 undefined. */
|
|
18
|
+
readonly configPath?: string;
|
|
19
|
+
/** 커넥션 URL — 다른 서브커맨드(seed)에 URL 전달용. url 없는 config 는 undefined. */
|
|
20
|
+
readonly url?: string;
|
|
21
|
+
/** 커넥션 정리 — CLI 종료 전 항상 호출. */
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* gaon.config.ts 를 로드해 target db 커넥션을 등록하고 스키마 테이블을 모아 온다.
|
|
26
|
+
*
|
|
27
|
+
* 커넥션 결정 순서:
|
|
28
|
+
* 1) config.db[dbKey] 가 있으면 그 어댑터·URL 사용
|
|
29
|
+
* 2) dbKey='main' 이고 GAON_DATABASE_URL 이 있으면 그 URL 로 main 을 세움
|
|
30
|
+
* 3) 그 외 → 수리 안내 에러
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveDbTarget(opts: ResolveDbOptions): Promise<ResolvedDbTarget>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// @gaonjs/cli · gaon db 공통 커넥션·스키마 해석 (M9-D)
|
|
2
|
+
//
|
|
3
|
+
// gaon.config.ts → db.<키> 로 커넥션을 조립하고, domain/schema/*.ts 를
|
|
4
|
+
// 스캔해 이 커넥션의 TableDef[] 를 돌려준다. diff/migrate/reset 이 이 값을
|
|
5
|
+
// 공유한다.
|
|
6
|
+
//
|
|
7
|
+
// config 우선순위:
|
|
8
|
+
// 1) opts.config (사용자 명시 --config <path>)
|
|
9
|
+
// 2) cwd/gaon.config.ts (findConfigPath 관례)
|
|
10
|
+
// config 도 GAON_DATABASE_URL 도 없으면 수리 안내 에러(§7.5.3).
|
|
11
|
+
//
|
|
12
|
+
// domain/schema/ 가 없으면 tables=[] (diff 는 dropTable 만 있는 상태).
|
|
13
|
+
// 스키마는 { db: '<키>' } 로 커넥션 바인딩된 것만 이 대상에 포함한다(§4.5).
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { join, resolve as resolvePath } from 'node:path';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
import { env, EnvError } from '@gaonjs/core';
|
|
18
|
+
import { findConfigPath, loadGaonConfig } from '@gaonjs/config';
|
|
19
|
+
import { createDb, registerConnection, destroyAllConnections, scanSchemaDir, } from '@gaonjs/data';
|
|
20
|
+
import { dialectFor } from '@gaonjs/data';
|
|
21
|
+
import { registerTsResolve } from '../tsResolve.js';
|
|
22
|
+
/** URL 에서 어댑터를 추정한다(seed·work 와 같은 관례). */
|
|
23
|
+
function dbConfigFromUrl(url) {
|
|
24
|
+
if (url.startsWith('mysql://') || url.startsWith('mariadb://')) {
|
|
25
|
+
return { adapter: 'mysql', url, poolMax: 4 };
|
|
26
|
+
}
|
|
27
|
+
return { adapter: 'postgres', url, poolMax: 4 };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* TableDef 정의를 스캔한다. 스키마 디렉터리가 없으면 [] 를 돌려준다.
|
|
31
|
+
* 스캔 결과는 커넥션 키로 걸러 diff·migrate 대상만 남긴다(§4.5).
|
|
32
|
+
*/
|
|
33
|
+
async function scanTables(cwd, dbKey) {
|
|
34
|
+
const schemaDir = join(cwd, 'domain', 'schema');
|
|
35
|
+
if (!existsSync(schemaDir))
|
|
36
|
+
return [];
|
|
37
|
+
// outDir 은 이 함수에서 실제로 파일을 쓰지 않으므로 관례 경로만 지정한다
|
|
38
|
+
// (scanSchemaDir 은 import 지정자 계산에만 사용). collectTables 는
|
|
39
|
+
// DiscoveredTable(이름·경로) 만 돌려주므로, 실제 TableDef 값은 모듈 네임스페이스에서
|
|
40
|
+
// 다시 꺼내야 한다 — 커넥션 키(§4.5)로 필터링해 이 db 것만 남긴다.
|
|
41
|
+
const outDir = join(cwd, '.gaon');
|
|
42
|
+
const modules = await scanSchemaDir(schemaDir, outDir);
|
|
43
|
+
const tables = [];
|
|
44
|
+
for (const mod of modules) {
|
|
45
|
+
for (const [, value] of Object.entries(mod.ns)) {
|
|
46
|
+
if (isTableDef(value) && value.db === dbKey)
|
|
47
|
+
tables.push(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return tables.sort((a, b) => a.name.localeCompare(b.name));
|
|
51
|
+
}
|
|
52
|
+
function isTableDef(v) {
|
|
53
|
+
return (typeof v === 'object' &&
|
|
54
|
+
v !== null &&
|
|
55
|
+
'name' in v &&
|
|
56
|
+
'defs' in v &&
|
|
57
|
+
'db' in v &&
|
|
58
|
+
typeof v.name === 'string');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* gaon.config.ts 를 로드해 target db 커넥션을 등록하고 스키마 테이블을 모아 온다.
|
|
62
|
+
*
|
|
63
|
+
* 커넥션 결정 순서:
|
|
64
|
+
* 1) config.db[dbKey] 가 있으면 그 어댑터·URL 사용
|
|
65
|
+
* 2) dbKey='main' 이고 GAON_DATABASE_URL 이 있으면 그 URL 로 main 을 세움
|
|
66
|
+
* 3) 그 외 → 수리 안내 에러
|
|
67
|
+
*/
|
|
68
|
+
export async function resolveDbTarget(opts) {
|
|
69
|
+
const cwd = resolvePath(opts.cwd);
|
|
70
|
+
registerTsResolve();
|
|
71
|
+
// 사용자가 --config <path> 를 준 경우 그 경로에서 default export 를 로드한다.
|
|
72
|
+
// 없으면 cwd 관례.
|
|
73
|
+
let configPath;
|
|
74
|
+
let config;
|
|
75
|
+
if (opts.configPath) {
|
|
76
|
+
configPath = resolvePath(opts.configPath);
|
|
77
|
+
if (!existsSync(configPath)) {
|
|
78
|
+
throw new Error(`[gaon db] --config 로 지정한 파일이 없습니다: ${configPath}\n` +
|
|
79
|
+
`→ 경로를 확인하거나 --config 없이 실행해 cwd/gaon.config.ts 를 쓰세요.`);
|
|
80
|
+
}
|
|
81
|
+
const mod = (await import(pathToFileURL(configPath).href));
|
|
82
|
+
if (!mod.default || typeof mod.default !== 'object') {
|
|
83
|
+
throw new Error(`[gaon db] --config 파일의 default export 가 객체가 아닙니다: ${configPath}\n` +
|
|
84
|
+
`→ export default defineConfig({ db: { main: { adapter: 'postgres', url: '...' } } }) 형태로 두세요.`);
|
|
85
|
+
}
|
|
86
|
+
config = mod.default;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
configPath = findConfigPath(cwd);
|
|
90
|
+
config = await loadGaonConfig(cwd);
|
|
91
|
+
}
|
|
92
|
+
const dbKey = opts.dbKey;
|
|
93
|
+
const cfg = config.db?.[dbKey];
|
|
94
|
+
let connCfg;
|
|
95
|
+
if (cfg) {
|
|
96
|
+
connCfg = cfg;
|
|
97
|
+
}
|
|
98
|
+
else if (dbKey === 'main') {
|
|
99
|
+
// 폴백: GAON_DATABASE_URL (seed 와 같은 관례).
|
|
100
|
+
const url = env.optional('GAON_DATABASE_URL');
|
|
101
|
+
if (!url) {
|
|
102
|
+
throw new EnvError(`[gaon db] main 커넥션을 결정할 수 없습니다.\n` +
|
|
103
|
+
` → gaon.config.ts 에 db.main 을 선언하거나, .env 에 다음 줄을 추가하세요:\n` +
|
|
104
|
+
` GAON_DATABASE_URL=postgres://user:pass@localhost:5432/mydb`);
|
|
105
|
+
}
|
|
106
|
+
connCfg = dbConfigFromUrl(url);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const known = Object.keys(config.db ?? {}).sort();
|
|
110
|
+
throw new Error(`[gaon db] 커넥션 '${dbKey}' 이(가) gaon.config.ts 에 없습니다.\n` +
|
|
111
|
+
` 등록된 키: ${known.length ? known.join(', ') : '(없음)'}\n` +
|
|
112
|
+
` → gaon.config.ts 의 db.${dbKey} 를 선언하고 다시 실행하세요.`);
|
|
113
|
+
}
|
|
114
|
+
const db = createDb(connCfg);
|
|
115
|
+
registerConnection(dbKey, db, connCfg.adapter);
|
|
116
|
+
const dialect = dialectFor(connCfg.adapter);
|
|
117
|
+
const tables = await scanTables(cwd, dbKey);
|
|
118
|
+
return {
|
|
119
|
+
dbKey,
|
|
120
|
+
adapter: connCfg.adapter,
|
|
121
|
+
db,
|
|
122
|
+
dialect,
|
|
123
|
+
tables,
|
|
124
|
+
configPath,
|
|
125
|
+
url: 'url' in connCfg ? connCfg.url : undefined,
|
|
126
|
+
async close() {
|
|
127
|
+
await destroyAllConnections();
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · dev/console — `gaon dev` 통합 콘솔 (M9-C)
|
|
3
|
+
*
|
|
4
|
+
* `gaon dev` 는 여러 자식(Docker · serve · tsc · vue-tsc · 파일 워처)의
|
|
5
|
+
* 출력을 한 콘솔로 합친다. 어느 소스에서 왔는지 즉시 알아보게 [tag] 를
|
|
6
|
+
* 접두어로 붙이고, 소스별 색으로 시선을 잡는다.
|
|
7
|
+
*
|
|
8
|
+
* 두 가지 모드:
|
|
9
|
+
* - human: ` [serve] ▶ 리슨 중 — ...` (색상 · 사람 눈)
|
|
10
|
+
* - json : `{"source":"serve","level":"info","msg":"..."}` (자동화 파싱)
|
|
11
|
+
*
|
|
12
|
+
* ANSI 는 TTY 일 때만 켠다(파이프 · 리다이렉트에는 순수 텍스트). 무의존
|
|
13
|
+
* 정책상 chalk 는 쓰지 않는다 — 필요한 코드만 직접 쓴다.
|
|
14
|
+
*/
|
|
15
|
+
/** 콘솔이 구분하는 로그 소스. */
|
|
16
|
+
export type DevSource = 'dev' | 'docker' | 'serve' | 'watcher' | 'tsc' | 'vue-tsc';
|
|
17
|
+
/** 로그 레벨 — human 은 색상 강조, json 은 필드로 실린다. */
|
|
18
|
+
export type DevLevel = 'info' | 'warn' | 'error';
|
|
19
|
+
export interface DevConsoleOptions {
|
|
20
|
+
readonly json?: boolean;
|
|
21
|
+
/** 색상 강제 on/off. 생략 시 stdout.isTTY 자동 감지. */
|
|
22
|
+
readonly color?: boolean;
|
|
23
|
+
/** 타임스탬프 표시(사람 모드). 생략 시 false. */
|
|
24
|
+
readonly timestamp?: boolean;
|
|
25
|
+
/** 출력 스트림(테스트 주입). 기본 process.stdout. */
|
|
26
|
+
readonly stdout?: NodeJS.WritableStream;
|
|
27
|
+
/** 에러 출력 스트림(테스트 주입). 기본 process.stderr. */
|
|
28
|
+
readonly stderr?: NodeJS.WritableStream;
|
|
29
|
+
}
|
|
30
|
+
export interface DevConsole {
|
|
31
|
+
log(source: DevSource, line: string, level?: DevLevel): void;
|
|
32
|
+
/** 여러 줄(개행 포함) 청크를 줄 단위로 나눠 로그. 자식 stdout 파이프용. */
|
|
33
|
+
pipe(source: DevSource, chunk: string | Buffer, level?: DevLevel): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 통합 콘솔을 만든다. json 모드면 소스별 JSON 라인, 그 외엔 색상 태그
|
|
37
|
+
* + 사람 텍스트로 출력한다. 에러 레벨은 stderr, 그 외는 stdout.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createDevConsole(opts?: DevConsoleOptions): DevConsole;
|