@gaonjs/cli 0.52.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 +2 -0
- package/dist/commands/check.js +43 -1
- 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/db/journal.d.ts +4 -1
- package/dist/db/journal.js +37 -4
- package/dist/db/migrate.js +11 -11
- package/dist/db/replay.js +1 -1
- package/dist/db/resolve.d.ts +11 -1
- package/dist/db/resolve.js +24 -2
- package/dist/db/status.js +9 -6
- package/dist/db.js +26 -5
- package/dist/dev.js +2 -2
- package/dist/doctor/locale-parity.js +2 -2
- package/dist/i18n-config.d.ts +20 -0
- package/dist/i18n-config.js +87 -12
- package/dist/index.js +105 -22
- package/dist/mcp/tools.js +4 -0
- package/dist/messages-gen.js +11 -3
- package/dist/templates/project/Dockerfile.tpl +11 -1
- package/dist/templates/project/agents/async.md.tpl +36 -7
- package/dist/templates/project/agents/data.md.tpl +82 -25
- package/dist/templates/project/agents/frontend.md.tpl +5 -1
- package/dist/templates/project/agents/i18n.md.tpl +15 -0
- package/dist/templates/project/agents/mail.md.tpl +6 -0
- package/dist/templates/project/agents/realtime.md.tpl +10 -1
- package/dist/templates/project/agents/seal.md.tpl +3 -1
- package/dist/templates/project/agents/security.md.tpl +16 -3
- package/dist/templates/project/agents/storage.md.tpl +36 -4
- package/dist/templates/project/agents/web.md.tpl +14 -0
- 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
|
}
|
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);
|
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/db/journal.d.ts
CHANGED
|
@@ -23,8 +23,11 @@ export declare function journalExists(db: Kysely<any>, adapter?: AdapterName): P
|
|
|
23
23
|
* _gaon_migrations 를 만든다(존재하면 no-op). id/kind/applied_at/db_key/
|
|
24
24
|
* statements/summary/down_sql — text·varchar·timestamp·integer 는 postgres·
|
|
25
25
|
* mysql 공통 지원 타입. kind 기본값 'schema' 는 구버전 스키마 호환용.
|
|
26
|
+
* 결정 380: mysql 의 down_sql 은 **mediumtext**(16MB) — TEXT(64KB)는 큰 배치의
|
|
27
|
+
* down SQL(JSON 배열)이 조용히 잘려 저장 파손 위험이었다. 기존(legacy) 테이블은
|
|
28
|
+
* data_type 확인 후 한 번만 MODIFY 로 승격한다(매 실행 재빌드 방지). pg text 는 무제한.
|
|
26
29
|
*/
|
|
27
|
-
export declare function ensureJournal(db: Kysely<any
|
|
30
|
+
export declare function ensureJournal(db: Kysely<any>, adapter?: AdapterName): Promise<void>;
|
|
28
31
|
export interface RecordArgs {
|
|
29
32
|
readonly id: string;
|
|
30
33
|
readonly kind: MigrationKind;
|
package/dist/db/journal.js
CHANGED
|
@@ -41,26 +41,59 @@ export async function journalExists(db, adapter = 'postgres') {
|
|
|
41
41
|
* _gaon_migrations 를 만든다(존재하면 no-op). id/kind/applied_at/db_key/
|
|
42
42
|
* statements/summary/down_sql — text·varchar·timestamp·integer 는 postgres·
|
|
43
43
|
* mysql 공통 지원 타입. kind 기본값 'schema' 는 구버전 스키마 호환용.
|
|
44
|
+
* 결정 380: mysql 의 down_sql 은 **mediumtext**(16MB) — TEXT(64KB)는 큰 배치의
|
|
45
|
+
* down SQL(JSON 배열)이 조용히 잘려 저장 파손 위험이었다. 기존(legacy) 테이블은
|
|
46
|
+
* data_type 확인 후 한 번만 MODIFY 로 승격한다(매 실행 재빌드 방지). pg text 는 무제한.
|
|
44
47
|
*/
|
|
45
|
-
export async function ensureJournal(db) {
|
|
48
|
+
export async function ensureJournal(db, adapter = 'postgres') {
|
|
49
|
+
const downType = adapter === 'mysql' ? 'mediumtext' : 'text';
|
|
46
50
|
await sql
|
|
47
51
|
.raw(`create table if not exists ${MIGRATIONS_TABLE} (` +
|
|
48
52
|
` id varchar(255) not null primary key,` +
|
|
49
53
|
` kind varchar(16) not null default 'schema',` +
|
|
50
|
-
` applied_at timestamp not null,` +
|
|
54
|
+
` applied_at timestamp(6) not null,` +
|
|
51
55
|
` db_key varchar(64) not null,` +
|
|
52
56
|
` statements integer not null,` +
|
|
53
57
|
` summary text not null,` +
|
|
54
|
-
` down_sql
|
|
58
|
+
` down_sql ${downType}` +
|
|
55
59
|
`)`)
|
|
56
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);
|
|
57
90
|
}
|
|
58
91
|
/** 이력 한 행을 남긴다. 호출자가 트랜잭션(trx)을 넘기면 그 안에서 실행된다. */
|
|
59
92
|
export async function recordEntry(db, e) {
|
|
60
93
|
await sql `
|
|
61
94
|
insert into ${sql.ref(MIGRATIONS_TABLE)}
|
|
62
95
|
(id, kind, applied_at, db_key, statements, summary, down_sql)
|
|
63
|
-
values (${e.id}, ${e.kind}, ${
|
|
96
|
+
values (${e.id}, ${e.kind}, ${nextAppliedAt()}, ${e.dbKey}, ${e.statements}, ${e.summary}, ${e.downSql})
|
|
64
97
|
`.execute(db);
|
|
65
98
|
}
|
|
66
99
|
/** 가장 최근 적용된 이력(롤백 대상). 원장 미존재 시 undefined. */
|
package/dist/db/migrate.js
CHANGED
|
@@ -12,8 +12,7 @@
|
|
|
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';
|
|
@@ -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),
|
|
@@ -123,16 +125,14 @@ export async function runDbMigrate(opts) {
|
|
|
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
|
@@ -71,7 +71,7 @@ export async function replayPending(opts) {
|
|
|
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);
|
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,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// db/migrations/*.ts 각각의 적용/미적용과, 스키마↔DB 사이에 아직 반영 안 된
|
|
4
4
|
// 변경(schema drift) 수를 함께 보여준다. 아무것도 실행하지 않는다.
|
|
5
|
-
import { computeMigration, summarizeOp,
|
|
5
|
+
import { computeMigration, summarizeOp, partitionDeferredOps } from '@gaonjs/data';
|
|
6
6
|
import { resolveDbTarget } from './resolve.js';
|
|
7
7
|
import { appliedFileMigrations } from './journal.js';
|
|
8
8
|
import { describeDeferred } from './migrate.js';
|
|
@@ -21,14 +21,17 @@ export async function runDbStatus(opts) {
|
|
|
21
21
|
// migrate 는 스키마에 없는 테이블을 자동 DROP 하지 않으므로(결정 39 · §4.8)
|
|
22
22
|
// dropTable 은 drift 로 세지 않는다 — 별도로 참고 정보(extraTables)로만 보고.
|
|
23
23
|
const plan = await computeMigration(target.db, target.tables, target.dialect);
|
|
24
|
-
const extraTables = plan.ops
|
|
25
|
-
.filter((o) => o.kind === 'dropTable')
|
|
26
|
-
.map((o) => o.name);
|
|
27
24
|
// 결정 326: deferred aux drop(제약·인덱스·기본값 제거)도 migrate 가 자동 적용하지
|
|
28
25
|
// 않는다(결정 220) — drift 로 세면 "'gaon db migrate' 로 반영하세요" 가 영원히 안
|
|
29
26
|
// 지워지는 거짓 안내가 된다. dropTable 과 같은 축의 참고 항목으로 분리한다.
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
// 결정 366: 재생성 쌍(인덱스 method·enum 값 변경)의 drop 반쪽은 migrate 가 자동
|
|
28
|
+
// 적용하므로 deferred 가 아니라 drift(schemaChanges)로 계상한다 — migrate 와 한 판정.
|
|
29
|
+
const { apply, deferred } = partitionDeferredOps(plan.ops);
|
|
30
|
+
const extraTables = deferred
|
|
31
|
+
.filter((o) => o.kind === 'dropTable')
|
|
32
|
+
.map((o) => o.name);
|
|
33
|
+
const deferredDrops = deferred.filter((o) => o.kind !== 'dropTable');
|
|
34
|
+
const schemaChanges = apply.length;
|
|
32
35
|
const lines = [];
|
|
33
36
|
if (migrations.length === 0) {
|
|
34
37
|
lines.push(` [${opts.dbKey}] 마이그레이션 파일 없음 (db/migrations/).`);
|
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
|
}
|
package/dist/dev.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { readdirSync, existsSync } from 'node:fs';
|
|
15
15
|
import { join, resolve } from 'node:path';
|
|
16
|
-
import { analyzeProjectI18n } from './i18n-config.js';
|
|
16
|
+
import { analyzeProjectI18n, resolveLocalesDir } from './i18n-config.js';
|
|
17
17
|
/** locales/*.json 변경(메시지 카탈로그) — messages.d.ts 재생성 트리거. */
|
|
18
18
|
const isMessagesChange = (f) => !f.includes('.gaon') && f.endsWith('.json');
|
|
19
19
|
const isSchemaChange = (f) => f.endsWith('.ts') && !f.endsWith('.d.ts') && !f.endsWith('.test.ts') && !f.includes('.gaon');
|
|
@@ -148,7 +148,7 @@ export function resolveDevLayout(cwd) {
|
|
|
148
148
|
// 결정 352: 카탈로그 위치·기준 로케일은 gaon.config.ts 의 i18n 블록을 따른다
|
|
149
149
|
// (정적 분석 · 하드코딩 'locales' 는 dir 커스텀 프로젝트에서 타입 축을 무소음으로 껐다).
|
|
150
150
|
const i18nCfg = analyzeProjectI18n(root);
|
|
151
|
-
const localesDirPath =
|
|
151
|
+
const localesDirPath = resolveLocalesDir(root, i18nCfg.dir);
|
|
152
152
|
return {
|
|
153
153
|
schemaDir: existsSync(schemaDirPath) ? schemaDirPath : undefined,
|
|
154
154
|
tablesOut: join(root, '.gaon', 'tables.d.ts'),
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
12
|
import { join, relative } from 'node:path';
|
|
13
13
|
import { loadLocales, flattenKeys } from '@gaonjs/i18n';
|
|
14
|
-
import { analyzeProjectI18n } from '../i18n-config.js';
|
|
14
|
+
import { analyzeProjectI18n, resolveLocalesDir } from '../i18n-config.js';
|
|
15
15
|
// 경고 한 줄이 폭주하지 않게 나열 상한 — 넘으면 "…외 N개" 로 접는다(detail 에는 전량).
|
|
16
16
|
const MAX_KEYS_SHOWN = 20;
|
|
17
17
|
// i18next 복수형 접미사(결정 181 · generator.ts 와 동일 규약). 로케일마다 필요한 접미사만
|
|
@@ -29,7 +29,7 @@ function pluralBase(key) {
|
|
|
29
29
|
export async function checkLocaleParity(cwd) {
|
|
30
30
|
// 결정 352: 카탈로그 위치는 config i18n.dir 을 따른다(정적 분석 · 하드코딩이던
|
|
31
31
|
// 'locales' 는 dir 커스텀 프로젝트에서 이 검사를 무소음으로 껐다).
|
|
32
|
-
const localesDir =
|
|
32
|
+
const localesDir = resolveLocalesDir(cwd, analyzeProjectI18n(cwd).dir);
|
|
33
33
|
if (!existsSync(localesDir))
|
|
34
34
|
return { rule: 'locale-parity', issues: [] };
|
|
35
35
|
const resources = loadLocales(localesDir);
|
package/dist/i18n-config.d.ts
CHANGED
|
@@ -5,8 +5,28 @@ export interface ConfigI18nAnalysis {
|
|
|
5
5
|
readonly dir?: string;
|
|
6
6
|
/** 정적 리터럴로 읽힌 fallbackLng. 못 읽으면 undefined. */
|
|
7
7
|
readonly fallbackLng?: string;
|
|
8
|
+
/** 정적 리터럴 배열로 읽힌 supportedLngs(결정 413 · check 미러용). 못 읽으면 undefined. */
|
|
9
|
+
readonly supportedLngs?: readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* 결정 412: **선언은 있는데 정적으로 못 읽은** 키들(예 `dir: LOCALES_DIR`).
|
|
12
|
+
* 이 경우 런타임은 실값을, 타입 축·doctor 는 폴백('locales'·정렬 첫 로케일)을 써서
|
|
13
|
+
* 두 축이 갈라진다 — 호출자가 경고를 낼 수 있게 표면화한다(무신호 금지).
|
|
14
|
+
*/
|
|
15
|
+
readonly unresolved: readonly ('dir' | 'fallbackLng')[];
|
|
8
16
|
}
|
|
9
17
|
/** gaon.config.ts 소스에서 i18n 블록의 dir·fallbackLng 리터럴을 뽑는다. */
|
|
10
18
|
export declare function analyzeConfigI18n(source: string): ConfigI18nAnalysis;
|
|
11
19
|
/** cwd 의 gaon.config.ts 를 읽어 i18n 블록을 분석한다. 파일이 없으면 미선언. */
|
|
12
20
|
export declare function analyzeProjectI18n(cwd: string): ConfigI18nAnalysis;
|
|
21
|
+
/**
|
|
22
|
+
* 결정 412: 카탈로그 디렉터리의 절대 경로. `dir` 이 절대 경로면 그대로 쓴다 —
|
|
23
|
+
* wire(런타임)는 이미 그렇게 해석하는데 CLI 축만 무조건 join 해서
|
|
24
|
+
* `join('/proj','/var/locales')` = `/proj/var/locales` 로 조용히 빗나갔다.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveLocalesDir(cwd: string, dir: string | undefined): string;
|
|
27
|
+
/**
|
|
28
|
+
* 결정 412: 정적으로 못 읽은 i18n 키를 한 줄 경고로 알린다(무신호 금지). 런타임은
|
|
29
|
+
* 실값을, 타입 축·doctor 는 폴백을 쓰므로 messages.d.ts 가 엉뚱한 카탈로그를 보거나
|
|
30
|
+
* 아예 안 생길 수 있다 — 어느 형태로 바꾸면 되는지까지 쓴다(§7.5.3).
|
|
31
|
+
*/
|
|
32
|
+
export declare function warnUnresolvedI18n(analysis: ConfigI18nAnalysis, write: (s: string) => void): void;
|