@thinkdata/cli 0.1.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/bin/thinkdata.d.ts +3 -0
- package/dist/bin/thinkdata.d.ts.map +1 -0
- package/dist/bin/thinkdata.js +43 -0
- package/dist/bin/thinkdata.js.map +1 -0
- package/dist/commands/compare.d.ts +41 -0
- package/dist/commands/compare.d.ts.map +1 -0
- package/dist/commands/compare.js +264 -0
- package/dist/commands/compare.js.map +1 -0
- package/dist/commands/extract.d.ts +16 -0
- package/dist/commands/extract.d.ts.map +1 -0
- package/dist/commands/extract.js +255 -0
- package/dist/commands/extract.js.map +1 -0
- package/dist/converter/schemaToERD.d.ts +56 -0
- package/dist/converter/schemaToERD.d.ts.map +1 -0
- package/dist/converter/schemaToERD.js +80 -0
- package/dist/converter/schemaToERD.js.map +1 -0
- package/dist/extractors/index.d.ts +20 -0
- package/dist/extractors/index.d.ts.map +1 -0
- package/dist/extractors/index.js +81 -0
- package/dist/extractors/index.js.map +1 -0
- package/dist/extractors/mysql.d.ts +14 -0
- package/dist/extractors/mysql.d.ts.map +1 -0
- package/dist/extractors/mysql.js +198 -0
- package/dist/extractors/mysql.js.map +1 -0
- package/dist/extractors/postgres.d.ts +22 -0
- package/dist/extractors/postgres.d.ts.map +1 -0
- package/dist/extractors/postgres.js +259 -0
- package/dist/extractors/postgres.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thinkdata.d.ts","sourceRoot":"","sources":["../../src/bin/thinkdata.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @thinkdata/cli — ThinkERD 스키마 추출 CLI
|
|
4
|
+
*
|
|
5
|
+
* 사용법:
|
|
6
|
+
* npx @thinkdata/cli extract "postgresql://user@host/db"
|
|
7
|
+
* npx @thinkdata/cli extract --interactive
|
|
8
|
+
* npx @thinkdata/cli compare "postgresql://..." --token tk_...
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import { extractCommand } from '../commands/extract.js';
|
|
12
|
+
import { compareCommand } from '../commands/compare.js';
|
|
13
|
+
const program = new Command();
|
|
14
|
+
program
|
|
15
|
+
.name('thinkdata')
|
|
16
|
+
.description('ThinkERD CLI — Database schema extraction and comparison')
|
|
17
|
+
.version('0.1.0');
|
|
18
|
+
// ── extract 명령 ──
|
|
19
|
+
program
|
|
20
|
+
.command('extract [connection-string]')
|
|
21
|
+
.description('Extract schema from a database and optionally send to ThinkERD')
|
|
22
|
+
.option('-t, --type <dbms>', 'DBMS type (postgres, mysql)')
|
|
23
|
+
.option('-H, --host <host>', 'Database host')
|
|
24
|
+
.option('-p, --port <port>', 'Database port', parseInt)
|
|
25
|
+
.option('-d, --db <database>', 'Database name')
|
|
26
|
+
.option('-u, --user <user>', 'Database user')
|
|
27
|
+
.option('--password-env <envvar>', 'Environment variable containing the password')
|
|
28
|
+
.option('-s, --schema <schema>', 'Target schema (default: public)')
|
|
29
|
+
.option('--ssl', 'Enable SSL connection')
|
|
30
|
+
.option('--token <token>', 'ThinkERD transfer token')
|
|
31
|
+
.option('-o, --output <file>', 'Save to JSON file instead of sending')
|
|
32
|
+
.option('-i, --interactive', 'Interactive wizard mode')
|
|
33
|
+
.action(extractCommand);
|
|
34
|
+
// ── compare 명령 (스텁) ──
|
|
35
|
+
program
|
|
36
|
+
.command('compare [connection-string]')
|
|
37
|
+
.description('Compare database schema with ThinkERD model')
|
|
38
|
+
.option('--token <token>', 'ThinkERD transfer token')
|
|
39
|
+
.option('--project <id>', 'ThinkERD project ID')
|
|
40
|
+
.option('-f, --file <file>', 'Model JSON file to compare against')
|
|
41
|
+
.action(compareCommand);
|
|
42
|
+
program.parse();
|
|
43
|
+
//# sourceMappingURL=thinkdata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thinkdata.js","sourceRoot":"","sources":["../../src/bin/thinkdata.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACF,IAAI,CAAC,WAAW,CAAC;KACjB,WAAW,CAAC,0DAA0D,CAAC;KACvE,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB,mBAAmB;AACnB,OAAO;KACF,OAAO,CAAC,6BAA6B,CAAC;KACtC,WAAW,CAAC,gEAAgE,CAAC;KAC7E,MAAM,CAAC,mBAAmB,EAAE,6BAA6B,CAAC;KAC1D,MAAM,CAAC,mBAAmB,EAAE,eAAe,CAAC;KAC5C,MAAM,CAAC,mBAAmB,EAAE,eAAe,EAAE,QAAQ,CAAC;KACtD,MAAM,CAAC,qBAAqB,EAAE,eAAe,CAAC;KAC9C,MAAM,CAAC,mBAAmB,EAAE,eAAe,CAAC;KAC5C,MAAM,CAAC,yBAAyB,EAAE,8CAA8C,CAAC;KACjF,MAAM,CAAC,uBAAuB,EAAE,iCAAiC,CAAC;KAClE,MAAM,CAAC,OAAO,EAAE,uBAAuB,CAAC;KACxC,MAAM,CAAC,iBAAiB,EAAE,yBAAyB,CAAC;KACpD,MAAM,CAAC,qBAAqB,EAAE,sCAAsC,CAAC;KACrE,MAAM,CAAC,mBAAmB,EAAE,yBAAyB,CAAC;KACtD,MAAM,CAAC,cAAc,CAAC,CAAC;AAE5B,wBAAwB;AACxB,OAAO;KACF,OAAO,CAAC,6BAA6B,CAAC;KACtC,WAAW,CAAC,6CAA6C,CAAC;KAC1D,MAAM,CAAC,iBAAiB,EAAE,yBAAyB,CAAC;KACpD,MAAM,CAAC,gBAAgB,EAAE,qBAAqB,CAAC;KAC/C,MAAM,CAAC,mBAAmB,EAAE,oCAAoC,CAAC;KACjE,MAAM,CAAC,cAAc,CAAC,CAAC;AAE5B,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ExtractedSchema, ExtractedTable, ExtractedColumn } from '../types.js';
|
|
2
|
+
/** 비교 결과 */
|
|
3
|
+
export interface SchemaDiff {
|
|
4
|
+
/** DB에만 존재하는 테이블 (추가됨) */
|
|
5
|
+
added: ExtractedTable[];
|
|
6
|
+
/** 모델에만 존재하는 테이블 (누락됨) */
|
|
7
|
+
removed: string[];
|
|
8
|
+
/** 양쪽 모두 존재하지만 변경된 테이블 */
|
|
9
|
+
modified: TableDiff[];
|
|
10
|
+
/** FK 불일치 */
|
|
11
|
+
fkDiffs: FKDiff[];
|
|
12
|
+
}
|
|
13
|
+
export interface TableDiff {
|
|
14
|
+
tableName: string;
|
|
15
|
+
addedColumns: ExtractedColumn[];
|
|
16
|
+
removedColumns: string[];
|
|
17
|
+
modifiedColumns: ColumnDiff[];
|
|
18
|
+
}
|
|
19
|
+
export interface ColumnDiff {
|
|
20
|
+
columnName: string;
|
|
21
|
+
field: string;
|
|
22
|
+
dbValue: string;
|
|
23
|
+
modelValue: string;
|
|
24
|
+
}
|
|
25
|
+
export interface FKDiff {
|
|
26
|
+
name: string;
|
|
27
|
+
type: 'added' | 'removed' | 'modified';
|
|
28
|
+
details?: string;
|
|
29
|
+
}
|
|
30
|
+
interface CompareOptions {
|
|
31
|
+
token?: string;
|
|
32
|
+
file?: string;
|
|
33
|
+
project?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function compareCommand(connectionString: string | undefined, options: CompareOptions): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* 두 ExtractedSchema를 비교하여 차이점을 반환
|
|
38
|
+
*/
|
|
39
|
+
export declare function compareSchemas(db: ExtractedSchema, model: ExtractedSchema): SchemaDiff;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=compare.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.d.ts","sourceRoot":"","sources":["../../src/commands/compare.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,eAAe,EAAyC,MAAM,aAAa,CAAC;AAE3H,YAAY;AACZ,MAAM,WAAW,UAAU;IACvB,0BAA0B;IAC1B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0BAA0B;IAC1B,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,aAAa;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,eAAe,EAAE,UAAU,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,MAAM;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,UAAU,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,cAAc;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,cAAc,CAChC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,OAAO,EAAE,cAAc,GACxB,OAAO,CAAC,IAAI,CAAC,CA+Df;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,GAAG,UAAU,CAoCtF"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compare 명령 — DB 스키마와 ThinkERD 모델 비교
|
|
3
|
+
*
|
|
4
|
+
* 사용법:
|
|
5
|
+
* npx @thinkdata/cli compare "postgresql://..." --token tk_...
|
|
6
|
+
* npx @thinkdata/cli compare "postgresql://..." --file model.json
|
|
7
|
+
*/
|
|
8
|
+
import * as p from '@clack/prompts';
|
|
9
|
+
import pc from 'picocolors';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { createExtractor, parseConnectionString } from '../extractors/index.js';
|
|
12
|
+
export async function compareCommand(connectionString, options) {
|
|
13
|
+
console.log();
|
|
14
|
+
p.intro(pc.bgMagenta(pc.white(' ThinkERD Schema Compare v0.1.0 ')));
|
|
15
|
+
if (!connectionString) {
|
|
16
|
+
p.log.error('연결 문자열이 필요합니다.');
|
|
17
|
+
p.log.info('사용법: npx @thinkdata/cli compare "postgresql://user@host/db" --file model.json');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
// 1. DB 연결 → 현재 스키마 추출
|
|
22
|
+
const config = parseConnectionString(connectionString);
|
|
23
|
+
if (!config.password) {
|
|
24
|
+
const pw = await p.password({ message: '비밀번호' });
|
|
25
|
+
if (p.isCancel(pw)) {
|
|
26
|
+
p.cancel();
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
config.password = pw;
|
|
30
|
+
}
|
|
31
|
+
const extractor = createExtractor(config.type);
|
|
32
|
+
const s1 = p.spinner();
|
|
33
|
+
s1.start('DB 연결 및 스키마 추출 중...');
|
|
34
|
+
const dbSchema = await extractor.extract(config);
|
|
35
|
+
const dbTableCount = dbSchema.tables.length;
|
|
36
|
+
s1.stop(`DB 스키마 추출: ${pc.cyan(String(dbTableCount))}개 테이블`);
|
|
37
|
+
// 2. 모델 스키마 가져오기
|
|
38
|
+
let modelSchema;
|
|
39
|
+
if (options.file) {
|
|
40
|
+
const s2 = p.spinner();
|
|
41
|
+
s2.start('모델 파일 로딩 중...');
|
|
42
|
+
const raw = readFileSync(options.file, 'utf-8');
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
modelSchema = parsed.schema || parsed;
|
|
45
|
+
s2.stop(`모델 로드: ${pc.cyan(String(modelSchema.tables.length))}개 테이블`);
|
|
46
|
+
}
|
|
47
|
+
else if (options.token) {
|
|
48
|
+
p.log.warning('ThinkERD API에서 모델 가져오기는 아직 구현 중입니다.');
|
|
49
|
+
p.log.info('--file 옵션으로 JSON 모델 파일을 지정하세요.');
|
|
50
|
+
p.log.info('팁: npx @thinkdata/cli extract "..." --output model.json 으로 모델 파일을 생성할 수 있습니다.');
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
p.log.error('비교 대상이 필요합니다. --file <model.json> 또는 --token <tk_...>');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
// 비밀번호 즉시 폐기
|
|
58
|
+
config.password = '';
|
|
59
|
+
// 3. 비교 실행
|
|
60
|
+
const s3 = p.spinner();
|
|
61
|
+
s3.start('스키마 비교 중...');
|
|
62
|
+
const diff = compareSchemas(dbSchema, modelSchema);
|
|
63
|
+
s3.stop('비교 완료');
|
|
64
|
+
// 4. 결과 출력
|
|
65
|
+
printDiff(diff);
|
|
66
|
+
p.outro(pc.green('비교 완료!'));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 두 ExtractedSchema를 비교하여 차이점을 반환
|
|
75
|
+
*/
|
|
76
|
+
export function compareSchemas(db, model) {
|
|
77
|
+
const dbTableMap = new Map(db.tables.map(t => [t.name, t]));
|
|
78
|
+
const modelTableMap = new Map(model.tables.map(t => [t.name, t]));
|
|
79
|
+
// 추가된 테이블 (DB에만 존재)
|
|
80
|
+
const added = [];
|
|
81
|
+
for (const [name, table] of dbTableMap) {
|
|
82
|
+
if (!modelTableMap.has(name)) {
|
|
83
|
+
added.push(table);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// 누락된 테이블 (모델에만 존재)
|
|
87
|
+
const removed = [];
|
|
88
|
+
for (const name of modelTableMap.keys()) {
|
|
89
|
+
if (!dbTableMap.has(name)) {
|
|
90
|
+
removed.push(name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// 변경된 테이블
|
|
94
|
+
const modified = [];
|
|
95
|
+
for (const [name, dbTable] of dbTableMap) {
|
|
96
|
+
const modelTable = modelTableMap.get(name);
|
|
97
|
+
if (!modelTable)
|
|
98
|
+
continue;
|
|
99
|
+
const tableDiff = compareTable(dbTable, modelTable);
|
|
100
|
+
if (tableDiff) {
|
|
101
|
+
modified.push(tableDiff);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// FK 비교
|
|
105
|
+
const fkDiffs = compareForeignKeys(db.foreignKeys, model.foreignKeys);
|
|
106
|
+
return { added, removed, modified, fkDiffs };
|
|
107
|
+
}
|
|
108
|
+
function compareTable(db, model) {
|
|
109
|
+
const dbColMap = new Map(db.columns.map(c => [c.name, c]));
|
|
110
|
+
const modelColMap = new Map(model.columns.map(c => [c.name, c]));
|
|
111
|
+
const addedColumns = [];
|
|
112
|
+
const removedColumns = [];
|
|
113
|
+
const modifiedColumns = [];
|
|
114
|
+
// DB에만 있는 컬럼
|
|
115
|
+
for (const [name, col] of dbColMap) {
|
|
116
|
+
if (!modelColMap.has(name)) {
|
|
117
|
+
addedColumns.push(col);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// 모델에만 있는 컬럼
|
|
121
|
+
for (const name of modelColMap.keys()) {
|
|
122
|
+
if (!dbColMap.has(name)) {
|
|
123
|
+
removedColumns.push(name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 양쪽 모두 있는 컬럼 → 속성 비교
|
|
127
|
+
for (const [name, dbCol] of dbColMap) {
|
|
128
|
+
const modelCol = modelColMap.get(name);
|
|
129
|
+
if (!modelCol)
|
|
130
|
+
continue;
|
|
131
|
+
// 타입 비교
|
|
132
|
+
if (normalizeType(dbCol.type) !== normalizeType(modelCol.type)) {
|
|
133
|
+
modifiedColumns.push({
|
|
134
|
+
columnName: name,
|
|
135
|
+
field: 'type',
|
|
136
|
+
dbValue: dbCol.type,
|
|
137
|
+
modelValue: modelCol.type,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// nullable 비교
|
|
141
|
+
if (dbCol.nullable !== modelCol.nullable) {
|
|
142
|
+
modifiedColumns.push({
|
|
143
|
+
columnName: name,
|
|
144
|
+
field: 'nullable',
|
|
145
|
+
dbValue: String(dbCol.nullable),
|
|
146
|
+
modelValue: String(modelCol.nullable),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// default 비교
|
|
150
|
+
const dbDefault = dbCol.defaultValue || '';
|
|
151
|
+
const modelDefault = modelCol.defaultValue || '';
|
|
152
|
+
if (normalizeDefault(dbDefault) !== normalizeDefault(modelDefault)) {
|
|
153
|
+
modifiedColumns.push({
|
|
154
|
+
columnName: name,
|
|
155
|
+
field: 'default',
|
|
156
|
+
dbValue: dbDefault || '(none)',
|
|
157
|
+
modelValue: modelDefault || '(none)',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (addedColumns.length === 0 && removedColumns.length === 0 && modifiedColumns.length === 0) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
tableName: db.name,
|
|
166
|
+
addedColumns,
|
|
167
|
+
removedColumns,
|
|
168
|
+
modifiedColumns,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function compareForeignKeys(dbFKs, modelFKs) {
|
|
172
|
+
const dbMap = new Map(dbFKs.map(fk => [fk.name, fk]));
|
|
173
|
+
const modelMap = new Map(modelFKs.map(fk => [fk.name, fk]));
|
|
174
|
+
const diffs = [];
|
|
175
|
+
for (const [name, dbFK] of dbMap) {
|
|
176
|
+
if (!modelMap.has(name)) {
|
|
177
|
+
diffs.push({ name, type: 'added' });
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const modelFK = modelMap.get(name);
|
|
181
|
+
const changes = [];
|
|
182
|
+
if (dbFK.onDelete !== modelFK.onDelete) {
|
|
183
|
+
changes.push(`ON DELETE: ${modelFK.onDelete || 'NO ACTION'} → ${dbFK.onDelete || 'NO ACTION'}`);
|
|
184
|
+
}
|
|
185
|
+
if (dbFK.onUpdate !== modelFK.onUpdate) {
|
|
186
|
+
changes.push(`ON UPDATE: ${modelFK.onUpdate || 'NO ACTION'} → ${dbFK.onUpdate || 'NO ACTION'}`);
|
|
187
|
+
}
|
|
188
|
+
if (dbFK.sourceColumn !== modelFK.sourceColumn || dbFK.targetColumn !== modelFK.targetColumn) {
|
|
189
|
+
changes.push(`columns: ${modelFK.sourceColumn}→${modelFK.targetColumn} vs ${dbFK.sourceColumn}→${dbFK.targetColumn}`);
|
|
190
|
+
}
|
|
191
|
+
if (changes.length > 0) {
|
|
192
|
+
diffs.push({ name, type: 'modified', details: changes.join('; ') });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const name of modelMap.keys()) {
|
|
197
|
+
if (!dbMap.has(name)) {
|
|
198
|
+
diffs.push({ name, type: 'removed' });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return diffs;
|
|
202
|
+
}
|
|
203
|
+
// ─── 정규화 헬퍼 ─────────────────────────────────────────
|
|
204
|
+
function normalizeType(type) {
|
|
205
|
+
return type.toLowerCase().replace(/\s+/g, '');
|
|
206
|
+
}
|
|
207
|
+
function normalizeDefault(val) {
|
|
208
|
+
// PostgreSQL: nextval('...'), NULL 등 제거
|
|
209
|
+
return val.replace(/::[\w\s]+$/g, '').replace(/^'|'$/g, '').trim().toLowerCase();
|
|
210
|
+
}
|
|
211
|
+
// ─── 출력 ─────────────────────────────────────────
|
|
212
|
+
function printDiff(diff) {
|
|
213
|
+
const hasChanges = diff.added.length > 0 || diff.removed.length > 0 ||
|
|
214
|
+
diff.modified.length > 0 || diff.fkDiffs.length > 0;
|
|
215
|
+
if (!hasChanges) {
|
|
216
|
+
p.log.success(pc.green('✅ DB와 모델이 동일합니다. 차이점이 없습니다.'));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
console.log();
|
|
220
|
+
console.log(pc.bold('┌─────────────────────────────────────────┐'));
|
|
221
|
+
console.log(pc.bold('│ Schema Compare: DB vs ERD Model │'));
|
|
222
|
+
console.log(pc.bold('├─────────────────────────────────────────┤'));
|
|
223
|
+
if (diff.added.length > 0) {
|
|
224
|
+
for (const t of diff.added) {
|
|
225
|
+
console.log(pc.green(`│ 🟢 추가됨 (DB에만): ${t.name.padEnd(20)} (${t.columns.length} cols) │`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (diff.removed.length > 0) {
|
|
229
|
+
for (const name of diff.removed) {
|
|
230
|
+
console.log(pc.red(`│ 🔴 누락됨 (모델에만): ${name.padEnd(18)} │`));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (diff.modified.length > 0) {
|
|
234
|
+
for (const td of diff.modified) {
|
|
235
|
+
for (const cd of td.modifiedColumns) {
|
|
236
|
+
console.log(pc.yellow(`│ 🟠 변경됨: ${td.tableName}.${cd.columnName}`.padEnd(42) + '│'));
|
|
237
|
+
console.log(pc.yellow(`│ ${cd.field}: ${cd.modelValue} → ${cd.dbValue}`.padEnd(42) + '│'));
|
|
238
|
+
}
|
|
239
|
+
for (const col of td.addedColumns) {
|
|
240
|
+
console.log(pc.green(`│ 🟢 컬럼 추가: ${td.tableName}.${col.name}`.padEnd(42) + '│'));
|
|
241
|
+
}
|
|
242
|
+
for (const colName of td.removedColumns) {
|
|
243
|
+
console.log(pc.red(`│ 🔴 컬럼 누락: ${td.tableName}.${colName}`.padEnd(42) + '│'));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (diff.fkDiffs.length > 0) {
|
|
248
|
+
for (const fk of diff.fkDiffs) {
|
|
249
|
+
const icon = fk.type === 'added' ? '🟢' : fk.type === 'removed' ? '🔴' : '🟡';
|
|
250
|
+
console.log(pc.yellow(`│ ${icon} FK ${fk.type}: ${fk.name}`.padEnd(42) + '│'));
|
|
251
|
+
if (fk.details) {
|
|
252
|
+
console.log(pc.dim(`│ ${fk.details}`.padEnd(42) + '│'));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
console.log(pc.bold('└─────────────────────────────────────────┘'));
|
|
257
|
+
// 요약
|
|
258
|
+
const totalChanges = diff.added.length + diff.removed.length +
|
|
259
|
+
diff.modified.reduce((sum, m) => sum + m.addedColumns.length + m.removedColumns.length + m.modifiedColumns.length, 0) +
|
|
260
|
+
diff.fkDiffs.length;
|
|
261
|
+
console.log();
|
|
262
|
+
p.log.info(`총 ${pc.bold(String(totalChanges))}개 차이점 발견`);
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=compare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.js","sourceRoot":"","sources":["../../src/commands/compare.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface ExtractOptions {
|
|
2
|
+
type?: string;
|
|
3
|
+
host?: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
db?: string;
|
|
6
|
+
user?: string;
|
|
7
|
+
passwordEnv?: string;
|
|
8
|
+
schema?: string;
|
|
9
|
+
ssl?: boolean;
|
|
10
|
+
token?: string;
|
|
11
|
+
output?: string;
|
|
12
|
+
interactive?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function extractCommand(connectionString: string | undefined, options: ExtractOptions): Promise<void>;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=extract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../src/commands/extract.ts"],"names":[],"mappings":"AAeA,UAAU,cAAc;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,wBAAsB,cAAc,CAChC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,OAAO,EAAE,cAAc,GACxB,OAAO,CAAC,IAAI,CAAC,CAiIf"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract 명령 — 데이터베이스 스키마 추출
|
|
3
|
+
*
|
|
4
|
+
* 3가지 모드:
|
|
5
|
+
* 1. 연결 문자열: npx @thinkdata/cli extract "postgresql://..."
|
|
6
|
+
* 2. 개별 인수: npx @thinkdata/cli extract --type postgres --host ... --user ...
|
|
7
|
+
* 3. 인터랙티브: npx @thinkdata/cli extract --interactive
|
|
8
|
+
*/
|
|
9
|
+
import * as p from '@clack/prompts';
|
|
10
|
+
import pc from 'picocolors';
|
|
11
|
+
import { writeFileSync } from 'node:fs';
|
|
12
|
+
import { createExtractor, parseConnectionString } from '../extractors/index.js';
|
|
13
|
+
import { convertToERD } from '../converter/schemaToERD.js';
|
|
14
|
+
export async function extractCommand(connectionString, options) {
|
|
15
|
+
console.log();
|
|
16
|
+
p.intro(pc.bgCyan(pc.black(' ThinkERD Schema Extractor v0.1.0 ')));
|
|
17
|
+
let config;
|
|
18
|
+
try {
|
|
19
|
+
if (options.interactive || (!connectionString && !options.type)) {
|
|
20
|
+
// 인터랙티브 모드
|
|
21
|
+
config = await interactiveWizard();
|
|
22
|
+
}
|
|
23
|
+
else if (connectionString) {
|
|
24
|
+
// 연결 문자열 모드
|
|
25
|
+
config = parseConnectionString(connectionString);
|
|
26
|
+
// 비밀번호가 연결 문자열에 없으면 환경 변수 또는 프롬프트
|
|
27
|
+
if (!config.password) {
|
|
28
|
+
config.password = await resolvePassword(options.passwordEnv);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// 개별 인수 모드
|
|
33
|
+
if (!options.type || !options.host || !options.db || !options.user) {
|
|
34
|
+
p.log.error('연결 정보 부족. --interactive 를 사용하거나 필수 인수를 제공하세요.');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
config = {
|
|
38
|
+
type: options.type,
|
|
39
|
+
host: options.host,
|
|
40
|
+
port: options.port || 5432,
|
|
41
|
+
database: options.db,
|
|
42
|
+
user: options.user,
|
|
43
|
+
password: await resolvePassword(options.passwordEnv),
|
|
44
|
+
schema: options.schema,
|
|
45
|
+
ssl: options.ssl,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// 스키마 오버라이드
|
|
49
|
+
if (options.schema)
|
|
50
|
+
config.schema = options.schema;
|
|
51
|
+
if (options.ssl)
|
|
52
|
+
config.ssl = true;
|
|
53
|
+
// ── 1단계: 연결 테스트 ──
|
|
54
|
+
const extractor = createExtractor(config.type);
|
|
55
|
+
const s1 = p.spinner();
|
|
56
|
+
s1.start('연결 테스트 중...');
|
|
57
|
+
let version;
|
|
58
|
+
try {
|
|
59
|
+
version = await extractor.testConnection(config);
|
|
60
|
+
s1.stop(`연결 성공 — ${pc.green(config.type)} ${pc.dim(version)}`);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
s1.stop(pc.red('연결 실패'));
|
|
64
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
65
|
+
p.outro(pc.red('연결을 확인하고 다시 시도하세요.'));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
// ── 2단계: 스키마 추출 ──
|
|
69
|
+
const s2 = p.spinner();
|
|
70
|
+
s2.start('스키마 추출 중...');
|
|
71
|
+
const schema = await extractor.extract(config);
|
|
72
|
+
const totalCols = schema.tables.reduce((sum, t) => sum + t.columns.length, 0);
|
|
73
|
+
s2.stop(`추출 완료 — ${pc.cyan(`${schema.tables.length}`)}개 테이블, ${pc.cyan(`${totalCols}`)}개 컬럼, ${pc.cyan(`${schema.foreignKeys.length}`)}개 FK`);
|
|
74
|
+
// ── 3단계: ERD 변환 ──
|
|
75
|
+
const s3 = p.spinner();
|
|
76
|
+
s3.start('ERD 변환 중...');
|
|
77
|
+
const erd = convertToERD(schema);
|
|
78
|
+
s3.stop(`변환 완료 — ${pc.cyan(`${erd.summary.tableCount}`)}개 엔터티, ${pc.cyan(`${erd.summary.fkCount}`)}개 관계`);
|
|
79
|
+
// ── 4단계: 출력 ──
|
|
80
|
+
if (options.output) {
|
|
81
|
+
// JSON 파일 저장
|
|
82
|
+
const payload = { schema, erd };
|
|
83
|
+
writeFileSync(options.output, JSON.stringify(payload, null, 2), 'utf-8');
|
|
84
|
+
p.log.success(`파일 저장: ${pc.underline(options.output)}`);
|
|
85
|
+
}
|
|
86
|
+
else if (options.token) {
|
|
87
|
+
// ThinkERD API 전송
|
|
88
|
+
const s4 = p.spinner();
|
|
89
|
+
s4.start('ThinkERD로 전송 중...');
|
|
90
|
+
try {
|
|
91
|
+
await sendToThinkERD(options.token, schema, erd);
|
|
92
|
+
s4.stop(pc.green('전송 완료!'));
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
s4.stop(pc.red('전송 실패'));
|
|
96
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// 선택지 제공
|
|
101
|
+
const action = await p.select({
|
|
102
|
+
message: '결과를 어떻게 처리할까요?',
|
|
103
|
+
options: [
|
|
104
|
+
{ value: 'json', label: 'JSON 파일로 저장' },
|
|
105
|
+
{ value: 'stdout', label: '표준 출력 (stdout)' },
|
|
106
|
+
{ value: 'send', label: 'ThinkERD로 전송 (로그인 필요)' },
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
if (p.isCancel(action)) {
|
|
110
|
+
p.cancel('취소되었습니다.');
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
switch (action) {
|
|
114
|
+
case 'json': {
|
|
115
|
+
const filename = `schema-${config.database}-${new Date().toISOString().split('T')[0]}.json`;
|
|
116
|
+
const payload = { schema, erd };
|
|
117
|
+
writeFileSync(filename, JSON.stringify(payload, null, 2), 'utf-8');
|
|
118
|
+
p.log.success(`파일 저장: ${pc.underline(filename)}`);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case 'stdout':
|
|
122
|
+
console.log(JSON.stringify({ schema, erd }, null, 2));
|
|
123
|
+
break;
|
|
124
|
+
case 'send':
|
|
125
|
+
p.log.warning('ThinkERD 전송은 --token 인수가 필요합니다.');
|
|
126
|
+
p.log.info('ThinkERD 웹에서 "DB에서 가져오기" → 토큰을 받아 사용하세요.');
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// 비밀번호 메모리 삭제 (보안)
|
|
131
|
+
config.password = '';
|
|
132
|
+
p.outro(pc.green('완료!'));
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ─── 인터랙티브 위저드 ─────────────────────────────────────────
|
|
140
|
+
async function interactiveWizard() {
|
|
141
|
+
const dbms = await p.select({
|
|
142
|
+
message: 'DBMS를 선택하세요',
|
|
143
|
+
options: [
|
|
144
|
+
{ value: 'postgres', label: 'PostgreSQL' },
|
|
145
|
+
{ value: 'mysql', label: 'MySQL / MariaDB' },
|
|
146
|
+
{ value: 'oracle', label: 'Oracle (준비 중)', hint: 'coming soon' },
|
|
147
|
+
{ value: 'mssql', label: 'SQL Server (준비 중)', hint: 'coming soon' },
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
if (p.isCancel(dbms)) {
|
|
151
|
+
p.cancel();
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
if (dbms === 'oracle' || dbms === 'mssql') {
|
|
155
|
+
p.log.warning(`${dbms}은 아직 지원 준비 중입니다.`);
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
const inputMode = await p.select({
|
|
159
|
+
message: '연결 정보 입력 방식',
|
|
160
|
+
options: [
|
|
161
|
+
{ value: 'connstr', label: '연결 문자열 (Connection String)' },
|
|
162
|
+
{ value: 'fields', label: '개별 항목 입력' },
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
if (p.isCancel(inputMode)) {
|
|
166
|
+
p.cancel();
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
if (inputMode === 'connstr') {
|
|
170
|
+
const connStr = await p.text({
|
|
171
|
+
message: '연결 문자열을 입력하세요',
|
|
172
|
+
placeholder: `${dbms}://user@localhost:${dbms === 'mysql' ? '3306' : '5432'}/mydb`,
|
|
173
|
+
});
|
|
174
|
+
if (p.isCancel(connStr)) {
|
|
175
|
+
p.cancel();
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
const config = parseConnectionString(connStr);
|
|
179
|
+
if (!config.password) {
|
|
180
|
+
config.password = await promptPassword();
|
|
181
|
+
}
|
|
182
|
+
return config;
|
|
183
|
+
}
|
|
184
|
+
// 개별 항목 입력
|
|
185
|
+
const host = await p.text({ message: '호스트', placeholder: 'localhost', defaultValue: 'localhost' });
|
|
186
|
+
if (p.isCancel(host)) {
|
|
187
|
+
p.cancel();
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
const port = await p.text({
|
|
191
|
+
message: '포트',
|
|
192
|
+
placeholder: dbms === 'mysql' ? '3306' : '5432',
|
|
193
|
+
defaultValue: dbms === 'mysql' ? '3306' : '5432',
|
|
194
|
+
});
|
|
195
|
+
if (p.isCancel(port)) {
|
|
196
|
+
p.cancel();
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
const database = await p.text({ message: '데이터베이스 이름', placeholder: 'mydb' });
|
|
200
|
+
if (p.isCancel(database)) {
|
|
201
|
+
p.cancel();
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
const user = await p.text({ message: '사용자', placeholder: 'admin' });
|
|
205
|
+
if (p.isCancel(user)) {
|
|
206
|
+
p.cancel();
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
const password = await promptPassword();
|
|
210
|
+
return {
|
|
211
|
+
type: dbms,
|
|
212
|
+
host: host,
|
|
213
|
+
port: parseInt(port, 10),
|
|
214
|
+
database: database,
|
|
215
|
+
user: user,
|
|
216
|
+
password,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// ─── 헬퍼 ─────────────────────────────────────────
|
|
220
|
+
async function promptPassword() {
|
|
221
|
+
const pw = await p.password({ message: '비밀번호' });
|
|
222
|
+
if (p.isCancel(pw)) {
|
|
223
|
+
p.cancel();
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
return pw;
|
|
227
|
+
}
|
|
228
|
+
async function resolvePassword(envVar) {
|
|
229
|
+
if (envVar) {
|
|
230
|
+
const pw = process.env[envVar];
|
|
231
|
+
if (!pw) {
|
|
232
|
+
p.log.error(`환경 변수 ${envVar}가 설정되지 않았습니다.`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
return pw;
|
|
236
|
+
}
|
|
237
|
+
return promptPassword();
|
|
238
|
+
}
|
|
239
|
+
async function sendToThinkERD(token, schema, erd) {
|
|
240
|
+
const { request } = await import('undici');
|
|
241
|
+
const baseUrl = process.env.THINKERD_API_URL || 'https://api.thinkdata.dev';
|
|
242
|
+
const response = await request(`${baseUrl}/api/cli/import-schema`, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: {
|
|
245
|
+
'Content-Type': 'application/json',
|
|
246
|
+
'Authorization': `Bearer ${token}`,
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify({ schema, erd }),
|
|
249
|
+
});
|
|
250
|
+
if (response.statusCode !== 200 && response.statusCode !== 201) {
|
|
251
|
+
const body = await response.body.text();
|
|
252
|
+
throw new Error(`API 오류 (${response.statusCode}): ${body}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=extract.js.map
|