sonamu 0.0.1

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.
Files changed (60) hide show
  1. package/.pnp.cjs +15552 -0
  2. package/.pnp.loader.mjs +285 -0
  3. package/.vscode/extensions.json +6 -0
  4. package/.vscode/settings.json +9 -0
  5. package/.yarnrc.yml +5 -0
  6. package/dist/bin/cli.d.ts +2 -0
  7. package/dist/bin/cli.d.ts.map +1 -0
  8. package/dist/bin/cli.js +123 -0
  9. package/dist/bin/cli.js.map +1 -0
  10. package/dist/index.js +34 -0
  11. package/package.json +60 -0
  12. package/src/api/caster.ts +72 -0
  13. package/src/api/code-converters.ts +552 -0
  14. package/src/api/context.ts +20 -0
  15. package/src/api/decorators.ts +63 -0
  16. package/src/api/index.ts +5 -0
  17. package/src/api/init.ts +128 -0
  18. package/src/bin/cli.ts +115 -0
  19. package/src/database/base-model.ts +287 -0
  20. package/src/database/db.ts +95 -0
  21. package/src/database/knex-plugins/knex-on-duplicate-update.ts +41 -0
  22. package/src/database/upsert-builder.ts +231 -0
  23. package/src/exceptions/error-handler.ts +29 -0
  24. package/src/exceptions/so-exceptions.ts +91 -0
  25. package/src/index.ts +17 -0
  26. package/src/shared/web.shared.ts.txt +119 -0
  27. package/src/smd/migrator.ts +1462 -0
  28. package/src/smd/smd-manager.ts +141 -0
  29. package/src/smd/smd-utils.ts +266 -0
  30. package/src/smd/smd.ts +533 -0
  31. package/src/syncer/index.ts +1 -0
  32. package/src/syncer/syncer.ts +1283 -0
  33. package/src/templates/base-template.ts +19 -0
  34. package/src/templates/generated.template.ts +247 -0
  35. package/src/templates/generated_http.template.ts +114 -0
  36. package/src/templates/index.ts +1 -0
  37. package/src/templates/init_enums.template.ts +71 -0
  38. package/src/templates/init_generated.template.ts +44 -0
  39. package/src/templates/init_types.template.ts +38 -0
  40. package/src/templates/model.template.ts +168 -0
  41. package/src/templates/model_test.template.ts +39 -0
  42. package/src/templates/service.template.ts +263 -0
  43. package/src/templates/smd.template.ts +49 -0
  44. package/src/templates/view_enums_buttonset.template.ts +34 -0
  45. package/src/templates/view_enums_dropdown.template.ts +67 -0
  46. package/src/templates/view_enums_select.template.ts +60 -0
  47. package/src/templates/view_form.template.ts +397 -0
  48. package/src/templates/view_id_all_select.template.ts +34 -0
  49. package/src/templates/view_id_async_select.template.ts +113 -0
  50. package/src/templates/view_list.template.ts +652 -0
  51. package/src/templates/view_list_columns.template.ts +59 -0
  52. package/src/templates/view_search_input.template.ts +67 -0
  53. package/src/testing/fixture-manager.ts +271 -0
  54. package/src/types/types.ts +668 -0
  55. package/src/typings/knex.d.ts +24 -0
  56. package/src/utils/controller.ts +21 -0
  57. package/src/utils/lodash-able.ts +11 -0
  58. package/src/utils/model.ts +33 -0
  59. package/src/utils/utils.ts +28 -0
  60. package/tsconfig.json +47 -0
@@ -0,0 +1,1462 @@
1
+ import _, {
2
+ difference,
3
+ differenceBy,
4
+ differenceWith,
5
+ groupBy,
6
+ intersectionBy,
7
+ pick,
8
+ sortBy,
9
+ uniq,
10
+ uniqBy,
11
+ } from "lodash";
12
+ import knex, { Knex } from "knex";
13
+ import prettier from "prettier";
14
+ import chalk from "chalk";
15
+ import { DateTime } from "luxon";
16
+ import {
17
+ existsSync,
18
+ mkdirSync,
19
+ readdirSync,
20
+ unlinkSync,
21
+ writeFileSync,
22
+ } from "fs";
23
+ import equal from "fast-deep-equal";
24
+ import { capitalize, pluralize, singularize, underscore } from "inflection";
25
+ import prompts from "prompts";
26
+ import { execSync } from "child_process";
27
+ import path from "path";
28
+
29
+ import {
30
+ GenMigrationCode,
31
+ isBelongsToOneRelationProp,
32
+ isHasManyRelationProp,
33
+ isManyToManyRelationProp,
34
+ isOneToOneRelationProp,
35
+ isRelationProp,
36
+ isVirtualProp,
37
+ isStringProp,
38
+ KnexColumnType,
39
+ MigrationColumn,
40
+ MigrationForeign,
41
+ MigrationIndex,
42
+ MigrationJoinTable,
43
+ MigrationSet,
44
+ MigrationSetAndJoinTable,
45
+ isTextProp,
46
+ isEnumProp,
47
+ isIntegerProp,
48
+ } from "../types/types";
49
+ import { propIf } from "../utils/lodash-able";
50
+ import { SMDManager } from "./smd-manager";
51
+ import { SMD } from "./smd";
52
+ import { SonamuDBConfig } from "../database/db";
53
+
54
+ type MigratorMode = "dev" | "deploy";
55
+ export type MigratorOptions = {
56
+ appRootPath: string;
57
+ knexfile: SonamuDBConfig;
58
+ readonly mode: MigratorMode;
59
+ };
60
+ export class Migrator {
61
+ appRootPath: string;
62
+ knexfile: SonamuDBConfig;
63
+ readonly mode: MigratorMode;
64
+
65
+ targets: {
66
+ compare?: Knex;
67
+ pending: Knex;
68
+ shadow: Knex;
69
+ apply: Knex[];
70
+ };
71
+
72
+ constructor(options: MigratorOptions) {
73
+ this.appRootPath = options.appRootPath;
74
+ this.knexfile = options.knexfile;
75
+ this.mode = options.mode;
76
+
77
+ if (this.mode === "dev") {
78
+ const devDB = knex(this.knexfile.development_master);
79
+ const testDB = knex(this.knexfile.test);
80
+ const fixtureLocalDB = knex(this.knexfile.fixture_local);
81
+
82
+ const applyDBs = [devDB, testDB, fixtureLocalDB];
83
+ if (
84
+ (this.knexfile.fixture_local.connection as Knex.MySql2ConnectionConfig)
85
+ .host !==
86
+ (this.knexfile.fixture_remote.connection as Knex.MySql2ConnectionConfig)
87
+ .host
88
+ ) {
89
+ const fixtureRemoteDB = knex(this.knexfile.fixture_remote);
90
+ applyDBs.push(fixtureRemoteDB);
91
+ }
92
+
93
+ this.targets = {
94
+ compare: devDB,
95
+ pending: devDB,
96
+ shadow: testDB,
97
+ apply: applyDBs,
98
+ };
99
+ } else if (this.mode === "deploy") {
100
+ const productionDB = knex(this.knexfile.production_master);
101
+ const testDB = knex(this.knexfile.test);
102
+
103
+ this.targets = {
104
+ pending: productionDB,
105
+ shadow: testDB,
106
+ apply: [productionDB],
107
+ };
108
+ } else {
109
+ throw new Error(`잘못된 모드 ${this.mode} 입력`);
110
+ }
111
+ }
112
+
113
+ async clearPendingList(): Promise<void> {
114
+ const [, pendingList] = (await this.targets.pending.migrate.list()) as [
115
+ unknown,
116
+ {
117
+ file: string;
118
+ directory: string;
119
+ }[]
120
+ ];
121
+ const migrationsDir = `${this.appRootPath}/api/src/migrations`;
122
+ const delList = pendingList.map((df) => {
123
+ return path.join(migrationsDir, df.file).replace(".js", ".ts");
124
+ });
125
+ for (let p of delList) {
126
+ if (existsSync(p)) {
127
+ unlinkSync(p);
128
+ }
129
+ }
130
+ await this.cleanUpDist(true);
131
+ }
132
+
133
+ async run(): Promise<void> {
134
+ // pending 마이그레이션 확인
135
+ const [, pendingList] = await this.targets.pending.migrate.list();
136
+ if (pendingList.length > 0) {
137
+ console.log(
138
+ chalk.red("pending 된 마이그레이션이 존재합니다."),
139
+ pendingList.map((pending: any) => pending.file)
140
+ );
141
+
142
+ // pending이 있는 경우 Shadow DB 테스트 진행 여부 컨펌
143
+ const answer = await prompts({
144
+ type: "confirm",
145
+ name: "value",
146
+ message: "Shadow DB 테스트를 진행하시겠습니까?",
147
+ initial: true,
148
+ });
149
+ if (answer.value === false) {
150
+ return;
151
+ }
152
+
153
+ console.time(chalk.blue("Migrator - runShadowTest"));
154
+ const result = await this.runShadowTest();
155
+ console.timeEnd(chalk.blue("Migrator - runShadowTest"));
156
+ if (result === true) {
157
+ await Promise.all(
158
+ this.targets.apply.map(async (applyDb) => {
159
+ const label = chalk.green(
160
+ `APPLIED ${
161
+ applyDb.client.connectionSettings.host
162
+ } ${applyDb.client.database()}`
163
+ );
164
+ console.time(label);
165
+ const [,] = await applyDb.migrate.latest();
166
+ console.timeEnd(label);
167
+ })
168
+ );
169
+ }
170
+ return;
171
+ }
172
+
173
+ // MD-DB간 비교하여 코드 생성 리턴
174
+ const codes = await this.compareMigrations();
175
+ if (codes.length === 0) {
176
+ console.log(chalk.green("\n현재 모두 싱크된 상태입니다."));
177
+ return;
178
+ }
179
+
180
+ // 현재 생성된 코드 표기
181
+ console.table(codes, ["type", "title"]);
182
+
183
+ /* 디버깅용 코드
184
+ console.log(codes[0].formatted);
185
+ process.exit();
186
+ */
187
+
188
+ // 실제 파일 생성 프롬프트
189
+ const answer = await prompts({
190
+ type: "confirm",
191
+ name: "value",
192
+ message: "마이그레이션 코드를 생성하시겠습니까?",
193
+ initial: false,
194
+ });
195
+ if (answer.value === false) {
196
+ return;
197
+ }
198
+
199
+ // 실제 코드 생성
200
+ const migrationsDir = `${this.appRootPath}/api/src/migrations`;
201
+ codes
202
+ .filter((code) => code.formatted)
203
+ .map((code, index) => {
204
+ const dateTag = DateTime.local()
205
+ .plus({ seconds: index })
206
+ .toFormat("yyyyMMddHHmmss");
207
+ const filePath = `${migrationsDir}/${dateTag}_${code.title}.ts`;
208
+ writeFileSync(filePath, code.formatted!);
209
+ console.log(chalk.green(`MIGRTAION CRETATED ${filePath}`));
210
+ });
211
+ }
212
+
213
+ async rollback() {
214
+ console.time(chalk.red("rollback:"));
215
+ const rollbackAllResult = await Promise.all(
216
+ this.targets.apply.map(async (db) => {
217
+ await db.migrate.forceFreeMigrationsLock();
218
+ return db.migrate.rollback(undefined, false);
219
+ })
220
+ );
221
+ console.dir({ rollbackAllResult }, { depth: null });
222
+ console.timeEnd(chalk.red("rollback:"));
223
+ }
224
+
225
+ async cleanUpDist(force: boolean = false): Promise<void> {
226
+ const files = (["src", "dist"] as const).reduce(
227
+ (r, which) => {
228
+ const migrationPath = path.join(
229
+ this.appRootPath,
230
+ "api",
231
+ which,
232
+ "migrations"
233
+ );
234
+ if (existsSync(migrationPath) === false) {
235
+ mkdirSync(migrationPath, {
236
+ recursive: true,
237
+ });
238
+ }
239
+ const files = readdirSync(migrationPath).filter(
240
+ (filename) => filename.startsWith(".") === false
241
+ );
242
+ r[which] = files;
243
+ return r;
244
+ },
245
+ {
246
+ src: [] as string[],
247
+ dist: [] as string[],
248
+ }
249
+ );
250
+
251
+ const diffOnSrc = differenceBy(
252
+ files.src,
253
+ files.dist,
254
+ (filename) => filename.split(".")[0]
255
+ );
256
+ if (diffOnSrc.length > 0) {
257
+ throw new Error(
258
+ "컴파일 되지 않은 파일이 있습니다.\n" + diffOnSrc.join("\n")
259
+ );
260
+ }
261
+
262
+ const diffOnDist = differenceBy(
263
+ files.dist,
264
+ files.src,
265
+ (filename) => filename.split(".")[0]
266
+ );
267
+ if (diffOnDist.length > 0) {
268
+ console.log(chalk.red("원본 ts파일을 찾을 수 없는 js파일이 있습니다."));
269
+ console.log(diffOnDist);
270
+
271
+ if (!force) {
272
+ const answer = await prompts({
273
+ type: "confirm",
274
+ name: "value",
275
+ message: "삭제를 진행하시겠습니까?",
276
+ initial: true,
277
+ });
278
+ if (answer.value === false) {
279
+ return;
280
+ }
281
+ }
282
+
283
+ const filesToRm = diffOnDist.map((filename) => {
284
+ return path.join(
285
+ this.appRootPath,
286
+ "api",
287
+ "dist",
288
+ "migrations",
289
+ filename
290
+ );
291
+ });
292
+ filesToRm.map((filePath) => {
293
+ unlinkSync(filePath);
294
+ });
295
+ console.log(chalk.green(`${filesToRm.length}건 삭제되었습니다!`));
296
+ }
297
+ }
298
+
299
+ async runShadowTest(): Promise<boolean> {
300
+ // ShadowDB 생성 후 테스트 진행
301
+ const tdb = knex(this.knexfile.test);
302
+ const tdbConn = this.knexfile.test.connection as Knex.ConnectionConfig;
303
+ const shadowDatabase = tdbConn.database + "__migration_shadow";
304
+ const tmpSqlPath = `/tmp/${shadowDatabase}.sql`;
305
+
306
+ // 테스트DB 덤프 후 Database명 치환
307
+ console.log(
308
+ chalk.magenta(`${tdbConn.database}의 데이터 ${tmpSqlPath}로 덤프`)
309
+ );
310
+ execSync(
311
+ `mysqldump -h${tdbConn.host} -u${tdbConn.user} -p'${tdbConn.password}' ${tdbConn.database} --single-transaction --no-create-db --triggers > ${tmpSqlPath};`
312
+ );
313
+ execSync(
314
+ `sed -i'' -e 's/\`${tdbConn.database}\`/\`${shadowDatabase}\`/g' ${tmpSqlPath};`
315
+ );
316
+
317
+ // 기존 ShadowDB 리셋
318
+ console.log(chalk.magenta(`${shadowDatabase} 리셋`));
319
+ await tdb.raw(`DROP DATABASE IF EXISTS ${shadowDatabase};`);
320
+ await tdb.raw(`CREATE DATABASE ${shadowDatabase};`);
321
+
322
+ // ShadowDB 테이블 + 데이터 생성
323
+ console.log(chalk.magenta(`${shadowDatabase} 데이터베이스 생성`));
324
+ execSync(
325
+ `mysql -h${tdbConn.host} -u${tdbConn.user} -p'${tdbConn.password}' ${shadowDatabase} < ${tmpSqlPath};`
326
+ );
327
+
328
+ // tdb 연결 종료
329
+ await tdb.destroy();
330
+
331
+ // shadow db 테스트 진행
332
+ const sdb = knex({
333
+ ...this.knexfile.test,
334
+ connection: {
335
+ ...tdbConn,
336
+ database: shadowDatabase,
337
+ },
338
+ });
339
+
340
+ try {
341
+ const [batchNo, log] = await sdb.migrate.latest();
342
+ console.log(chalk.green("Shadow DB 테스트에 성공했습니다!"), {
343
+ batchNo,
344
+ log,
345
+ });
346
+
347
+ // 생성한 Shadow DB 삭제
348
+ console.log(chalk.magenta(`${shadowDatabase} 삭제`));
349
+ await sdb.raw(`DROP DATABASE IF EXISTS ${shadowDatabase};`);
350
+
351
+ return true;
352
+ } catch (e) {
353
+ console.error(chalk.red("Shadow DB 테스트 진행 중 에러"), e);
354
+ return false;
355
+ } finally {
356
+ await sdb.destroy();
357
+ }
358
+ }
359
+
360
+ async resetAll() {
361
+ const answer = await prompts({
362
+ type: "confirm",
363
+ name: "value",
364
+ message: "모든 DB를 롤백하고 전체 마이그레이션 파일을 삭제하시겠습니까?",
365
+ initial: false,
366
+ });
367
+ if (answer.value === false) {
368
+ return;
369
+ }
370
+
371
+ console.time(chalk.red("rollback-all:"));
372
+ const rollbackAllResult = await Promise.all(
373
+ this.targets.apply.map(async (db) => {
374
+ await db.migrate.forceFreeMigrationsLock();
375
+ return db.migrate.rollback(undefined, true);
376
+ })
377
+ );
378
+ console.log({ rollbackAllResult });
379
+ console.timeEnd(chalk.red("rollback-all:"));
380
+
381
+ const migrationsDir = `${this.appRootPath}/api/src/migrations`;
382
+ console.time(chalk.red("delete migration files"));
383
+ execSync(`rm -f ${migrationsDir}/*`);
384
+ execSync(`rm -f ${migrationsDir.replace("/src/", "/dist/")}/*`);
385
+ console.timeEnd(chalk.red("delete migration files"));
386
+ }
387
+
388
+ async compareMigrations(): Promise<GenMigrationCode[]> {
389
+ // MD 순회하여 싱크
390
+ const smdIds = SMDManager.getAllIds();
391
+
392
+ // 조인테이블 포함하여 MD에서 MigrationSet 추출
393
+ const smdSetsWithJoinTable = smdIds
394
+ .filter((smdId) => {
395
+ const smd = SMDManager.get(smdId);
396
+ return smd.props.length > 0;
397
+ })
398
+ .map((smdId) => {
399
+ const smd = SMDManager.get(smdId);
400
+ return this.getMigrationSetFromMD(smd);
401
+ });
402
+
403
+ // 조인테이블만 추출
404
+ const joinTables = uniqBy(
405
+ smdSetsWithJoinTable.map((smdSet) => smdSet.joinTables).flat(),
406
+ (joinTable) => {
407
+ return joinTable.table;
408
+ }
409
+ );
410
+
411
+ // 조인테이블 포함하여 MigrationSet 배열
412
+ const smdSets: MigrationSet[] = [...smdSetsWithJoinTable, ...joinTables];
413
+
414
+ let codes: GenMigrationCode[] = (
415
+ await Promise.all(
416
+ smdSets.map(async (smdSet) => {
417
+ const dbSet = await this.getMigrationSetFromDB(smdSet.table);
418
+ if (dbSet === null) {
419
+ // 기존 테이블 없음, 새로 테이블 생성
420
+ return [
421
+ this.generateCreateCode_ColumnAndIndexes(
422
+ smdSet.table,
423
+ smdSet.columns,
424
+ smdSet.indexes
425
+ ),
426
+ ...this.generateCreateCode_Foreign(smdSet.table, smdSet.foreigns),
427
+ ];
428
+ }
429
+
430
+ // 기존 테이블 존재하는 케이스
431
+ const alterCodes: (GenMigrationCode | GenMigrationCode[] | null)[] = (
432
+ ["columnsAndIndexes", "foreigns"] as const
433
+ ).map((key) => {
434
+ // 배열 원소의 순서가 달라서 불일치가 발생하는걸 방지하기 위해 각 항목별로 정렬 처리 후 비교
435
+ if (key === "columnsAndIndexes") {
436
+ const smdColumns = sortBy(
437
+ smdSet.columns,
438
+ (a: any) => (a as MigrationColumn).name
439
+ );
440
+ const dbColumns = sortBy(
441
+ dbSet.columns,
442
+ (a: any) => (a as MigrationColumn).name
443
+ );
444
+
445
+ /* 디버깅용 코드, 특정 컬럼에서 불일치 발생할 때 확인
446
+ const smdCreatedAt = smdSet.columns.find(
447
+ (col) => col.name === "created_at"
448
+ );
449
+ const dbCreatedAt = dbSet.columns.find(
450
+ (col) => col.name === "created_at"
451
+ );
452
+ console.debug({ smdCreatedAt, dbCreatedAt });
453
+ */
454
+
455
+ const smdIndexes = sortBy(smdSet.indexes, (a: any) =>
456
+ (a as MigrationIndex).columns.join("-")
457
+ );
458
+ const dbIndexes = sortBy(dbSet.indexes, (a: any) =>
459
+ (a as MigrationIndex).columns.join("-")
460
+ );
461
+
462
+ const isEqualColumns = equal(smdColumns, dbColumns);
463
+ const isEqualIndexes = equal(smdIndexes, dbIndexes);
464
+ if (isEqualColumns && isEqualIndexes) {
465
+ return null;
466
+ } else {
467
+ // this.showMigrationSet("MD", smdSet);
468
+ // this.showMigrationSet("DB", dbSet);
469
+ return this.generateAlterCode_ColumnAndIndexes(
470
+ smdSet.table,
471
+ smdColumns,
472
+ smdIndexes,
473
+ dbColumns,
474
+ dbIndexes
475
+ );
476
+ }
477
+ } else {
478
+ const smdForeigns = sortBy(smdSet.foreigns, (a: any) =>
479
+ (a as MigrationForeign).columns.join("-")
480
+ );
481
+ const dbForeigns = sortBy(dbSet.foreigns, (a: any) =>
482
+ (a as MigrationForeign).columns.join("-")
483
+ );
484
+
485
+ if (equal(smdForeigns, dbForeigns) === false) {
486
+ // TODO FK alter
487
+ console.log(chalk.red(`FK 다름! ${smdSet.table}`));
488
+ // console.dir({ smdForeigns, dbForeigns }, { depth: null });
489
+ }
490
+ }
491
+ return null;
492
+ });
493
+ if (alterCodes.every((alterCode) => alterCode === null)) {
494
+ return null;
495
+ } else {
496
+ return alterCodes.filter((alterCode) => alterCode !== null).flat();
497
+ }
498
+ })
499
+ )
500
+ )
501
+ .flat()
502
+ .filter((code) => code !== null) as GenMigrationCode[];
503
+
504
+ /*
505
+ normal 타입이 앞으로, foreign 이 뒤로
506
+ */
507
+ codes.sort((codeA, codeB) => {
508
+ if (codeA.type === "foreign" && codeB.type == "normal") {
509
+ return 1;
510
+ } else if (codeA.type === "normal" && codeB.type === "foreign") {
511
+ return -1;
512
+ } else {
513
+ return 0;
514
+ }
515
+ });
516
+
517
+ return codes;
518
+ }
519
+
520
+ /*
521
+ 기존 테이블 정보 읽어서 MigrationSet 형식으로 리턴
522
+ */
523
+ async getMigrationSetFromDB(table: string): Promise<MigrationSet | null> {
524
+ let dbColumns: any[], dbIndexes: any[], dbForeigns: any[];
525
+ try {
526
+ [dbColumns, dbIndexes, dbForeigns] = await this.readTable(table);
527
+ } catch (e) {
528
+ return null;
529
+ }
530
+
531
+ const columns: MigrationColumn[] = dbColumns.map((dbColumn) => {
532
+ const dbColType = this.resolveDBColType(dbColumn.Type, dbColumn.Field);
533
+ return {
534
+ name: dbColumn.Field,
535
+ nullable: dbColumn.Null !== "NO",
536
+ ...dbColType,
537
+ ...propIf(dbColumn.Default !== null, {
538
+ defaultTo:
539
+ dbColType.type === "float"
540
+ ? parseFloat(dbColumn.Default).toString()
541
+ : dbColumn.Default,
542
+ }),
543
+ };
544
+ });
545
+
546
+ const dbIndexesGroup = groupBy(
547
+ dbIndexes.filter(
548
+ (dbIndex) =>
549
+ dbIndex.Key_name !== "PRIMARY" &&
550
+ !dbForeigns.find(
551
+ (dbForeign) => dbForeign.keyName === dbIndex.Key_name
552
+ )
553
+ ),
554
+ (dbIndex) => dbIndex.Key_name
555
+ );
556
+
557
+ // indexes 처리
558
+ const indexes: MigrationIndex[] = Object.keys(dbIndexesGroup).map(
559
+ (keyName) => {
560
+ const currentIndexes = dbIndexesGroup[keyName];
561
+ return {
562
+ type: currentIndexes[0].Non_unique === 1 ? "index" : "unique",
563
+ columns: currentIndexes.map(
564
+ (currentIndex) => currentIndex.Column_name
565
+ ),
566
+ ...propIf(currentIndexes.length > 1, {
567
+ name: keyName,
568
+ }),
569
+ };
570
+ }
571
+ );
572
+ // console.log(table);
573
+ // console.table(dbIndexes);
574
+ // console.table(dbForeigns);
575
+
576
+ // foreigns 처리
577
+ const foreigns: MigrationForeign[] = dbForeigns.map((dbForeign) => {
578
+ return {
579
+ columns: [dbForeign.from],
580
+ to: `${dbForeign.referencesTable}.${dbForeign.referencesField}`,
581
+ onUpdate: dbForeign.onUpdate,
582
+ onDelete: dbForeign.onDelete,
583
+ };
584
+ });
585
+
586
+ return {
587
+ table,
588
+ columns,
589
+ indexes,
590
+ foreigns,
591
+ };
592
+ }
593
+
594
+ resolveDBColType(
595
+ colType: string,
596
+ colField: string
597
+ ): Pick<MigrationColumn, "type" | "unsigned" | "length"> {
598
+ let [rawType, unsigned] = colType.split(" ");
599
+ const matched = rawType.match(/\(([0-9]+)\)/);
600
+ let length;
601
+ if (matched !== null && matched[1]) {
602
+ rawType = rawType.replace(/\(([0-9]+)\)/, "");
603
+ length = parseInt(matched[1]);
604
+ }
605
+
606
+ if (rawType === "char" && colField === "uuid") {
607
+ return {
608
+ type: "uuid",
609
+ ...propIf(length !== undefined, {}),
610
+ };
611
+ }
612
+
613
+ switch (rawType) {
614
+ case "int":
615
+ return {
616
+ type: "integer",
617
+ unsigned: unsigned === "unsigned",
618
+ };
619
+ case "float(8,2)":
620
+ return {
621
+ type: "float",
622
+ ...propIf(unsigned === "unsigned", {
623
+ unsigned: true,
624
+ }),
625
+ };
626
+ case "decimal(8,2)":
627
+ return {
628
+ type: "decimal",
629
+ ...propIf(unsigned === "unsigned", {
630
+ unsigned: true,
631
+ }),
632
+ };
633
+ case "varchar":
634
+ // case "char":
635
+ return {
636
+ type: "string",
637
+ ...propIf(length !== undefined, {
638
+ length,
639
+ }),
640
+ };
641
+ case "text":
642
+ case "mediumtext":
643
+ case "longtext":
644
+ case "timestamp":
645
+ case "json":
646
+ case "date":
647
+ case "time":
648
+ return {
649
+ type: rawType,
650
+ };
651
+ case "datetime":
652
+ return {
653
+ type: "dateTime",
654
+ };
655
+ case "tinyint":
656
+ return {
657
+ type: "boolean",
658
+ };
659
+ default:
660
+ throw new Error(`resolve 불가능한 DB컬럼 타입 ${colType} ${rawType}`);
661
+ }
662
+ }
663
+
664
+ /*
665
+ 기존 테이블 읽어서 cols, indexes 반환
666
+ */
667
+ async readTable(tableName: string): Promise<[any, any, any]> {
668
+ // 테이블 정보
669
+ try {
670
+ const [cols] = await this.targets.compare!.raw(
671
+ `SHOW FIELDS FROM ${tableName}`
672
+ );
673
+ const [indexes] = await this.targets.compare!.raw(
674
+ `SHOW INDEX FROM ${tableName}`
675
+ );
676
+ const [[row]] = await this.targets.compare!.raw(
677
+ `SHOW CREATE TABLE ${tableName}`
678
+ );
679
+ const ddl = row["Create Table"];
680
+ const matched = ddl.match(/CONSTRAINT .+/g);
681
+ const foreignKeys = (matched ?? []).map((line: string) => {
682
+ const matched = line.match(
683
+ /CONSTRAINT `(.+)` FOREIGN KEY \(`(.+)`\) REFERENCES `(.+)` \(`(.+)`\) ON DELETE ([A-Z ]+) ON UPDATE ([A-Z ]+)/
684
+ );
685
+ if (!matched) {
686
+ throw new Error(`인식할 수 없는 FOREIGN KEY CONSTRAINT ${line}`);
687
+ }
688
+ const [
689
+ ,
690
+ keyName,
691
+ from,
692
+ referencesTable,
693
+ referencesField,
694
+ onDelete,
695
+ onUpdate,
696
+ ] = matched;
697
+ return {
698
+ keyName,
699
+ from,
700
+ referencesTable,
701
+ referencesField,
702
+ onDelete,
703
+ onUpdate,
704
+ };
705
+ });
706
+ return [cols, indexes, foreignKeys];
707
+ } catch (e) {
708
+ throw e;
709
+ }
710
+ }
711
+
712
+ /*
713
+ MD 내용 읽어서 MigrationSetAndJoinTable 추출
714
+ */
715
+ getMigrationSetFromMD(smd: SMD): MigrationSetAndJoinTable {
716
+ const migrationSet: MigrationSetAndJoinTable = smd.props.reduce(
717
+ (r, prop) => {
718
+ // virtual 필드 제외
719
+ if (isVirtualProp(prop)) {
720
+ return r;
721
+ }
722
+ // HasMany 케이스는 아무 처리도 하지 않음
723
+ if (isHasManyRelationProp(prop)) {
724
+ return r;
725
+ }
726
+
727
+ // 일반 컬럼
728
+ if (!isRelationProp(prop)) {
729
+ // type resolve
730
+ let type: KnexColumnType;
731
+ if (isTextProp(prop)) {
732
+ type = prop.textType;
733
+ } else if (isEnumProp(prop)) {
734
+ type = "string";
735
+ } else {
736
+ type = prop.type as KnexColumnType;
737
+ }
738
+
739
+ const column = {
740
+ name: prop.name,
741
+ type,
742
+ ...(isIntegerProp(prop)
743
+ ? { unsigned: prop.unsigned === true }
744
+ : {}),
745
+ ...(isStringProp(prop) || isEnumProp(prop)
746
+ ? { length: prop.length }
747
+ : {}),
748
+ nullable: prop.nullable === true,
749
+ // DB에선 무조건 string으로 리턴되므로 반드시 string 처리
750
+ ...propIf(prop.dbDefault !== undefined, {
751
+ defaultTo: "" + prop.dbDefault,
752
+ }),
753
+ };
754
+
755
+ r.columns.push(column);
756
+ }
757
+
758
+ // 일반 컬럼 + ToOne 케이스 컬럼
759
+ if (
760
+ !isRelationProp(prop) ||
761
+ isBelongsToOneRelationProp(prop) ||
762
+ (isOneToOneRelationProp(prop) && prop.hasJoinColumn)
763
+ ) {
764
+ const propName = !isRelationProp(prop)
765
+ ? prop.name
766
+ : `${prop.name}_id`;
767
+
768
+ // index 처리
769
+ if (prop.index !== undefined) {
770
+ if (prop.index !== true) {
771
+ prop.index.map((indexName) => {
772
+ const namedOne = r.indexes.find(
773
+ (_index) => _index.name === indexName
774
+ );
775
+ if (namedOne) {
776
+ namedOne.columns.push(propName);
777
+ } else {
778
+ r.indexes.push({
779
+ type: "index",
780
+ columns: [propName],
781
+ name: indexName,
782
+ });
783
+ }
784
+ });
785
+ } else {
786
+ r.indexes.push({
787
+ type: "index",
788
+ columns: [propName],
789
+ });
790
+ }
791
+ }
792
+ // unique 처리
793
+ if (prop.unique !== undefined) {
794
+ if (prop.unique !== true) {
795
+ prop.unique.map((indexName) => {
796
+ const namedOne = r.indexes.find(
797
+ (_index) => _index.name === indexName
798
+ );
799
+ if (namedOne) {
800
+ namedOne.columns.push(propName);
801
+ } else {
802
+ r.indexes.push({
803
+ type: "unique",
804
+ columns: [propName],
805
+ name: indexName,
806
+ });
807
+ }
808
+ });
809
+ } else {
810
+ r.indexes.push({
811
+ type: "unique",
812
+ columns: [propName],
813
+ });
814
+ }
815
+ }
816
+ }
817
+
818
+ if (isManyToManyRelationProp(prop)) {
819
+ // ManyToMany 케이스
820
+ const relMd = SMDManager.get(prop.with);
821
+ const [table1, table2] = prop.joinTable.split("__");
822
+ const join = {
823
+ from: `${smd.table}.id`,
824
+ through: {
825
+ from: `${prop.joinTable}.${singularize(table1)}_id`,
826
+ to: `${prop.joinTable}.${singularize(table2)}_id`,
827
+ onUpdate: prop.onUpdate,
828
+ onDelete: prop.onDelete,
829
+ },
830
+ to: `${relMd.table}.id`,
831
+ };
832
+ const through = join.through;
833
+ const fields = [through.from, through.to];
834
+ r.joinTables.push({
835
+ table: through.from.split(".")[0],
836
+ indexes: [
837
+ {
838
+ type: "unique",
839
+ columns: ["uuid"],
840
+ },
841
+ ],
842
+ columns: [
843
+ {
844
+ name: "id",
845
+ type: "integer",
846
+ nullable: false,
847
+ unsigned: true,
848
+ },
849
+ ...fields.map((field) => {
850
+ return {
851
+ name: field.split(".")[1],
852
+ type: "integer",
853
+ nullable: false,
854
+ unsigned: true,
855
+ } as MigrationColumn;
856
+ }),
857
+ {
858
+ name: "uuid",
859
+ nullable: true,
860
+ type: "uuid",
861
+ },
862
+ ],
863
+ foreigns: fields.map((field) => {
864
+ return {
865
+ columns: [field.split(".")[1]],
866
+ to: through.to.includes(field) ? join.to : join.from,
867
+ onUpdate: through.onUpdate,
868
+ onDelete: through.onDelete,
869
+ };
870
+ }),
871
+ });
872
+ return r;
873
+ } else if (
874
+ isBelongsToOneRelationProp(prop) ||
875
+ (isOneToOneRelationProp(prop) && prop.hasJoinColumn)
876
+ ) {
877
+ // -OneRelation 케이스
878
+ const idColumnName = prop.name + "_id";
879
+ r.columns.push({
880
+ name: idColumnName,
881
+ type: "integer",
882
+ unsigned: true,
883
+ nullable: prop.nullable ?? false,
884
+ });
885
+ r.foreigns.push({
886
+ columns: [idColumnName],
887
+ to: `${underscore(pluralize(prop.with)).toLowerCase()}.id`,
888
+ onUpdate: prop.onUpdate,
889
+ onDelete: prop.onDelete,
890
+ });
891
+ }
892
+
893
+ return r;
894
+ },
895
+ {
896
+ table: smd.table,
897
+ columns: [] as MigrationColumn[],
898
+ indexes: [] as MigrationIndex[],
899
+ foreigns: [] as MigrationForeign[],
900
+ joinTables: [] as MigrationJoinTable[],
901
+ }
902
+ );
903
+
904
+ // uuid
905
+ migrationSet.columns = migrationSet.columns.concat({
906
+ name: "uuid",
907
+ nullable: true,
908
+ type: "uuid",
909
+ } as MigrationColumn);
910
+ migrationSet.indexes = migrationSet.indexes.concat({
911
+ type: "unique",
912
+ columns: ["uuid"],
913
+ } as MigrationIndex);
914
+
915
+ return migrationSet;
916
+ }
917
+
918
+ /*
919
+ MigrationColumn[] 읽어서 컬럼 정의하는 구문 생성
920
+ */
921
+ genColumnDefinitions(columns: MigrationColumn[]): string[] {
922
+ return columns.map((column) => {
923
+ const chains: string[] = [];
924
+ if (column.name === "id") {
925
+ return `table.increments().primary();`;
926
+ }
927
+
928
+ // type, length
929
+ let columnType = column.type;
930
+ let extraType: string | undefined;
931
+ if (columnType.includes("text") && columnType !== "text") {
932
+ extraType = columnType;
933
+ columnType = "text";
934
+ }
935
+ chains.push(
936
+ `${column.type}('${column.name}'${
937
+ column.length ? `, ${column.length}` : ""
938
+ }${extraType ? `, '${extraType}'` : ""})`
939
+ );
940
+ if (column.unsigned) {
941
+ chains.push("unsigned()");
942
+ }
943
+
944
+ // nullable
945
+ chains.push(column.nullable ? "nullable()" : "notNullable()");
946
+
947
+ // defaultTo
948
+ if (column.defaultTo !== undefined) {
949
+ chains.push(`defaultTo(knex.raw('${column.defaultTo}'))`);
950
+ }
951
+
952
+ return `table.${chains.join(".")};`;
953
+ });
954
+ }
955
+
956
+ /*
957
+ MigrationIndex[] 읽어서 인덱스/유니크 정의하는 구문 생성
958
+ */
959
+ genIndexDefinitions(indexes: MigrationIndex[]): string[] {
960
+ if (indexes.length === 0) {
961
+ return [];
962
+ }
963
+ const lines = uniq(
964
+ indexes.reduce((r, index) => {
965
+ if (index.name === undefined) {
966
+ r.push(`table.${index.type}(['${index.columns[0]}'])`);
967
+ } else {
968
+ r.push(
969
+ `table.${index.type}([${index.columns
970
+ .map((col) => `'${col}'`)
971
+ .join(",")}], '${index.name}')`
972
+ );
973
+ }
974
+ return r;
975
+ }, [] as string[])
976
+ );
977
+ return lines;
978
+ }
979
+
980
+ /*
981
+ MigrationForeign[] 읽어서 외부키 constraint 정의하는 구문 생성
982
+ */
983
+ genForeignDefinitions(
984
+ table: string,
985
+ foreigns: MigrationForeign[]
986
+ ): { up: string[]; down: string[] } {
987
+ return foreigns.reduce(
988
+ (r, foreign) => {
989
+ const columnsStringQuote = foreign.columns
990
+ .map((col) => `'${col.replace(`${table}.`, "")}'`)
991
+ .join(",");
992
+ r.up.push(
993
+ `table.foreign('${foreign.columns.join(",")}')
994
+ .references('${foreign.to}')
995
+ .onUpdate('${foreign.onUpdate}')
996
+ .onDelete('${foreign.onDelete}')`
997
+ );
998
+ r.down.push(`table.dropForeign([${columnsStringQuote}])`);
999
+ return r;
1000
+ },
1001
+ {
1002
+ up: [] as string[],
1003
+ down: [] as string[],
1004
+ }
1005
+ );
1006
+ }
1007
+
1008
+ /*
1009
+ 테이블 생성하는 케이스 - 컬럼/인덱스 생성
1010
+ */
1011
+ generateCreateCode_ColumnAndIndexes(
1012
+ table: string,
1013
+ columns: MigrationColumn[],
1014
+ indexes: MigrationIndex[]
1015
+ ): GenMigrationCode {
1016
+ // 컬럼, 인덱스 처리
1017
+ const lines: string[] = [
1018
+ 'import { Knex } from "knex";',
1019
+ "",
1020
+ "export async function up(knex: Knex): Promise<void> {",
1021
+ `return knex.schema.createTable("${table}", (table) => {`,
1022
+ "// columns",
1023
+ ...this.genColumnDefinitions(columns),
1024
+ "",
1025
+ "// indexes",
1026
+ ...this.genIndexDefinitions(indexes),
1027
+ "});",
1028
+ "}",
1029
+ "",
1030
+ "export async function down(knex: Knex): Promise<void> {",
1031
+ ` return knex.schema.dropTable("${table}");`,
1032
+ "}",
1033
+ ];
1034
+ return {
1035
+ table,
1036
+ type: "normal",
1037
+ title: `create__${table}`,
1038
+ formatted: prettier.format(lines.join("\n"), {
1039
+ parser: "typescript",
1040
+ }),
1041
+ };
1042
+ }
1043
+ /*
1044
+ 테이블 생성하는 케이스 - FK 생성
1045
+ */
1046
+ generateCreateCode_Foreign(
1047
+ table: string,
1048
+ foreigns: MigrationForeign[]
1049
+ ): GenMigrationCode[] {
1050
+ if (foreigns.length === 0) {
1051
+ return [];
1052
+ }
1053
+
1054
+ const { up, down } = this.genForeignDefinitions(table, foreigns);
1055
+ if (up.length === 0 && down.length === 0) {
1056
+ console.log("fk 가 뭔가 다릅니다");
1057
+ return [];
1058
+ }
1059
+
1060
+ const lines: string[] = [
1061
+ 'import { Knex } from "knex";',
1062
+ "",
1063
+ "export async function up(knex: Knex): Promise<void> {",
1064
+ `return knex.schema.alterTable("${table}", (table) => {`,
1065
+ "// create fk",
1066
+ ...up,
1067
+ "});",
1068
+ "}",
1069
+ "",
1070
+ "export async function down(knex: Knex): Promise<void> {",
1071
+ `return knex.schema.alterTable("${table}", (table) => {`,
1072
+ "// drop fk",
1073
+ ...down,
1074
+ "});",
1075
+ "}",
1076
+ ];
1077
+
1078
+ const foreignKeysString = foreigns
1079
+ .map((foreign) => foreign.columns.join("_"))
1080
+ .join("_");
1081
+ return [
1082
+ {
1083
+ table,
1084
+ type: "foreign",
1085
+ title: `foreign__${table}__${foreignKeysString}`,
1086
+ formatted: prettier.format(lines.join("\n"), {
1087
+ parser: "typescript",
1088
+ }),
1089
+ },
1090
+ ];
1091
+ }
1092
+
1093
+ /*
1094
+ 마이그레이션 컬럼 배열 비교용 코드
1095
+ */
1096
+ showMigrationSet(which: string, migrationSet: MigrationSet): void {
1097
+ const { columns, indexes, foreigns } = migrationSet;
1098
+ const styledChalk =
1099
+ which === "MD" ? chalk.bgGreen.black : chalk.bgBlue.black;
1100
+ console.log(
1101
+ styledChalk(
1102
+ `${which} ${migrationSet.table} Columns\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t`
1103
+ )
1104
+ );
1105
+ console.table(
1106
+ columns.map((column) => {
1107
+ return {
1108
+ ...pick(column, [
1109
+ "name",
1110
+ "type",
1111
+ "nullable",
1112
+ "unsigned",
1113
+ "length",
1114
+ "defaultTo",
1115
+ ]),
1116
+ };
1117
+ }),
1118
+ ["name", "type", "nullable", "unsigned", "length", "defaultTo"]
1119
+ );
1120
+
1121
+ if (indexes.length > 0) {
1122
+ console.log(
1123
+ styledChalk(
1124
+ `${which} ${migrationSet.table} Indexes\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t`
1125
+ )
1126
+ );
1127
+ console.table(
1128
+ indexes.map((index) => {
1129
+ return {
1130
+ ...pick(index, ["type", "columns", "name"]),
1131
+ };
1132
+ })
1133
+ );
1134
+ }
1135
+
1136
+ if (foreigns.length > 0) {
1137
+ console.log(
1138
+ chalk.bgMagenta.black(
1139
+ `${which} ${migrationSet.table} Foreigns\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t`
1140
+ )
1141
+ );
1142
+ console.table(
1143
+ foreigns.map((foreign) => {
1144
+ return {
1145
+ ...pick(foreign, ["columns", "to", "onUpdate", "onDelete"]),
1146
+ };
1147
+ })
1148
+ );
1149
+ }
1150
+ }
1151
+
1152
+ generateAlterCode_ColumnAndIndexes(
1153
+ table: string,
1154
+ smdColumns: MigrationColumn[],
1155
+ smdIndexes: MigrationIndex[],
1156
+ dbColumns: MigrationColumn[],
1157
+ dbIndexes: MigrationIndex[]
1158
+ ): GenMigrationCode[] {
1159
+ // console.log(chalk.cyan("MigrationColumns from DB"));
1160
+ // showMigrationSet(dbColumns);
1161
+ // console.log(chalk.cyan("MigrationColumns from MD"));
1162
+ // showMigrationSet(smdColumns);
1163
+
1164
+ /*
1165
+ 세부 비교 후 다른점 찾아서 코드 생성
1166
+
1167
+ 1. 컬럼갯수 다름: MD에 있으나, DB에 없다면 추가
1168
+ 2. 컬럼갯수 다름: MD에 없으나, DB에 있다면 삭제
1169
+ 3. 그외 컬럼(컬럼 갯수가 동일하거나, 다른 경우 동일한 컬럼끼리) => alter
1170
+ 4. 다른거 다 동일하고 index만 변경되는 경우
1171
+
1172
+ ** 컬럼명을 변경하는 경우는 따로 핸들링하지 않음
1173
+ => drop/add 형태의 마이그레이션 코드가 생성되는데, 수동으로 rename 코드로 수정하여 처리
1174
+ */
1175
+
1176
+ // 각 컬럼 이름 기준으로 add, drop, alter 여부 확인
1177
+ const alterColumnsTo = this.getAlterColumnsTo(smdColumns, dbColumns);
1178
+
1179
+ // 추출된 컬럼들을 기준으로 각각 라인 생성
1180
+ const alterColumnLinesTo = this.getAlterColumnLinesTo(
1181
+ alterColumnsTo,
1182
+ smdColumns
1183
+ );
1184
+
1185
+ // 인덱스의 add, drop 여부 확인
1186
+ const alterIndexesTo = this.getAlterIndexesTo(smdIndexes, dbIndexes);
1187
+
1188
+ // 추출된 인덱스들을 기준으로 각각 라인 생성
1189
+ const alterIndexLinesTo = this.getAlterIndexLinesTo(
1190
+ alterIndexesTo,
1191
+ alterColumnsTo
1192
+ );
1193
+
1194
+ const lines: string[] = [
1195
+ 'import { Knex } from "knex";',
1196
+ "",
1197
+ "export async function up(knex: Knex): Promise<void> {",
1198
+ `return knex.schema.alterTable("${table}", (table) => {`,
1199
+ ...(alterColumnsTo.add.length > 0 ? alterColumnLinesTo.add.up : []),
1200
+ ...(alterColumnsTo.drop.length > 0 ? alterColumnLinesTo.drop.up : []),
1201
+ ...(alterColumnsTo.alter.length > 0 ? alterColumnLinesTo.alter.up : []),
1202
+ ...(alterIndexesTo.add.length > 0 ? alterIndexLinesTo.add.up : []),
1203
+ ...(alterIndexesTo.drop.length > 0 ? alterIndexLinesTo.drop.up : []),
1204
+ "})",
1205
+ "}",
1206
+ "",
1207
+ "export async function down(knex: Knex): Promise<void> {",
1208
+ `return knex.schema.alterTable("${table}", (table) => {`,
1209
+ ...(alterColumnsTo.add.length > 0 ? alterColumnLinesTo.add.down : []),
1210
+ ...(alterColumnsTo.drop.length > 0 ? alterColumnLinesTo.drop.down : []),
1211
+ ...(alterColumnsTo.alter.length > 0 ? alterColumnLinesTo.alter.down : []),
1212
+ ...(alterIndexLinesTo.add.down.length > 1
1213
+ ? alterIndexLinesTo.add.down
1214
+ : []),
1215
+ ...(alterIndexLinesTo.drop.down.length > 1
1216
+ ? alterIndexLinesTo.drop.down
1217
+ : []),
1218
+ "})",
1219
+ "}",
1220
+ ];
1221
+
1222
+ const formatted = prettier.format(lines.join("\n"), {
1223
+ parser: "typescript",
1224
+ });
1225
+
1226
+ const title = [
1227
+ "alter",
1228
+ table,
1229
+ ...(["add", "drop", "alter"] as const)
1230
+ .map((action) => {
1231
+ const len = alterColumnsTo[action].length;
1232
+ if (len > 0) {
1233
+ return action + len;
1234
+ }
1235
+ return null;
1236
+ })
1237
+ .filter((part) => part !== null),
1238
+ ].join("_");
1239
+
1240
+ return [
1241
+ {
1242
+ table,
1243
+ title,
1244
+ formatted,
1245
+ type: "normal",
1246
+ },
1247
+ ];
1248
+ }
1249
+
1250
+ getAlterColumnsTo(
1251
+ smdColumns: MigrationColumn[],
1252
+ dbColumns: MigrationColumn[]
1253
+ ) {
1254
+ const columnsTo = {
1255
+ add: [] as MigrationColumn[],
1256
+ drop: [] as MigrationColumn[],
1257
+ alter: [] as MigrationColumn[],
1258
+ };
1259
+
1260
+ // 컬럼명 기준 비교
1261
+ const extraColumns = {
1262
+ db: differenceBy(dbColumns, smdColumns, (col) => col.name),
1263
+ smd: differenceBy(smdColumns, dbColumns, (col) => col.name),
1264
+ };
1265
+ if (extraColumns.smd.length > 0) {
1266
+ columnsTo.add = columnsTo.add.concat(extraColumns.smd);
1267
+ }
1268
+ if (extraColumns.db.length > 0) {
1269
+ columnsTo.drop = columnsTo.drop.concat(extraColumns.db);
1270
+ }
1271
+
1272
+ // 동일 컬럼명의 세부 필드 비교
1273
+ const sameDbColumns = intersectionBy(
1274
+ dbColumns,
1275
+ smdColumns,
1276
+ (col) => col.name
1277
+ );
1278
+ const sameMdColumns = intersectionBy(
1279
+ smdColumns,
1280
+ dbColumns,
1281
+ (col) => col.name
1282
+ );
1283
+ columnsTo.alter = differenceWith(sameDbColumns, sameMdColumns, (a, b) =>
1284
+ equal(a, b)
1285
+ );
1286
+
1287
+ return columnsTo;
1288
+ }
1289
+
1290
+ getAlterColumnLinesTo(
1291
+ columnsTo: ReturnType<Migrator["getAlterColumnsTo"]>,
1292
+ smdColumns: MigrationColumn[]
1293
+ ) {
1294
+ let linesTo = {
1295
+ add: {
1296
+ up: [] as string[],
1297
+ down: [] as string[],
1298
+ },
1299
+ drop: {
1300
+ up: [] as string[],
1301
+ down: [] as string[],
1302
+ },
1303
+ alter: {
1304
+ up: [] as string[],
1305
+ down: [] as string[],
1306
+ },
1307
+ };
1308
+
1309
+ linesTo.add = {
1310
+ up: ["// add", ...this.genColumnDefinitions(columnsTo.add)],
1311
+ down: [
1312
+ "// rollback - add",
1313
+ `table.dropColumns(${columnsTo.add
1314
+ .map((col) => `'${col.name}'`)
1315
+ .join(", ")})`,
1316
+ ],
1317
+ };
1318
+ linesTo.drop = {
1319
+ up: [
1320
+ "// drop",
1321
+ `table.dropColumns(${columnsTo.drop
1322
+ .map((col) => `'${col.name}'`)
1323
+ .join(", ")})`,
1324
+ ],
1325
+ down: [
1326
+ "// rollback - drop",
1327
+ ...this.genColumnDefinitions(columnsTo.drop),
1328
+ ],
1329
+ };
1330
+ linesTo.alter = columnsTo.alter.reduce(
1331
+ (r, dbColumn) => {
1332
+ const smdColumn = smdColumns.find((col) => col.name == dbColumn.name);
1333
+ if (smdColumn === undefined) {
1334
+ return r;
1335
+ }
1336
+
1337
+ // 컬럼 변경사항
1338
+ const columnDiffUp = difference(
1339
+ this.genColumnDefinitions([smdColumn]),
1340
+ this.genColumnDefinitions([dbColumn])
1341
+ );
1342
+ const columnDiffDown = difference(
1343
+ this.genColumnDefinitions([dbColumn]),
1344
+ this.genColumnDefinitions([smdColumn])
1345
+ );
1346
+ if (columnDiffUp.length > 0) {
1347
+ r.up = [
1348
+ ...r.up,
1349
+ "// alter column",
1350
+ ...columnDiffUp.map((l) => l.replace(";", "") + ".alter();"),
1351
+ ];
1352
+ r.down = [
1353
+ ...r.down,
1354
+ "// rollback - alter column",
1355
+ ...columnDiffDown.map((l) => l.replace(";", "") + ".alter();"),
1356
+ ];
1357
+ }
1358
+
1359
+ return r;
1360
+ },
1361
+ {
1362
+ up: [] as string[],
1363
+ down: [] as string[],
1364
+ }
1365
+ );
1366
+
1367
+ return linesTo;
1368
+ }
1369
+
1370
+ getAlterIndexesTo(smdIndexes: MigrationIndex[], dbIndexes: MigrationIndex[]) {
1371
+ // 인덱스 비교
1372
+ let indexesTo = {
1373
+ add: [] as MigrationIndex[],
1374
+ drop: [] as MigrationIndex[],
1375
+ };
1376
+ const extraIndexes = {
1377
+ db: differenceBy(dbIndexes, smdIndexes, (col) =>
1378
+ [col.type, col.name ?? col.columns.join("-")].join("//")
1379
+ ),
1380
+ smd: differenceBy(smdIndexes, dbIndexes, (col) =>
1381
+ [col.type, col.name ?? col.columns.join("-")].join("//")
1382
+ ),
1383
+ };
1384
+ if (extraIndexes.smd.length > 0) {
1385
+ indexesTo.add = indexesTo.add.concat(extraIndexes.smd);
1386
+ }
1387
+ if (extraIndexes.db.length > 0) {
1388
+ indexesTo.drop = indexesTo.drop.concat(extraIndexes.db);
1389
+ }
1390
+
1391
+ return indexesTo;
1392
+ }
1393
+
1394
+ getAlterIndexLinesTo(
1395
+ indexesTo: ReturnType<Migrator["getAlterIndexesTo"]>,
1396
+ columnsTo: ReturnType<Migrator["getAlterColumnsTo"]>
1397
+ ) {
1398
+ let linesTo = {
1399
+ add: {
1400
+ up: [] as string[],
1401
+ down: [] as string[],
1402
+ },
1403
+ drop: {
1404
+ up: [] as string[],
1405
+ down: [] as string[],
1406
+ },
1407
+ };
1408
+
1409
+ // 인덱스가 추가되는 경우, 컬럼과 같이 추가된 케이스에는 drop에서 제외해야함!
1410
+ linesTo.add = {
1411
+ up: ["// add indexes", ...this.genIndexDefinitions(indexesTo.add)],
1412
+ down: [
1413
+ "// rollback - add indexes",
1414
+ ...indexesTo.add
1415
+ .filter(
1416
+ (index) =>
1417
+ index.columns.every((colName) =>
1418
+ columnsTo.add.map((col) => col.name).includes(colName)
1419
+ ) === false
1420
+ )
1421
+ .map(
1422
+ (index) =>
1423
+ `table.drop${capitalize(index.type)}([${index.columns
1424
+ .map((columnName) => `'${columnName}'`)
1425
+ .join(",")}])`
1426
+ ),
1427
+ ],
1428
+ };
1429
+ // 인덱스가 삭제되는 경우, 컬럼과 같이 삭제된 케이스에는 drop에서 제외해야함!
1430
+ linesTo.drop = {
1431
+ up: [
1432
+ ...indexesTo.drop
1433
+ .filter(
1434
+ (index) =>
1435
+ index.columns.every((colName) =>
1436
+ columnsTo.drop.map((col) => col.name).includes(colName)
1437
+ ) === false
1438
+ )
1439
+ .map(
1440
+ (index) =>
1441
+ `table.drop${capitalize(index.type)}([${index.columns
1442
+ .map((columnName) => `'${columnName}'`)
1443
+ .join(",")}])`
1444
+ ),
1445
+ ],
1446
+ down: [
1447
+ "// rollback - drop indexes",
1448
+ ...this.genIndexDefinitions(indexesTo.drop),
1449
+ ],
1450
+ };
1451
+
1452
+ return linesTo;
1453
+ }
1454
+
1455
+ async destroy(): Promise<void> {
1456
+ await Promise.all(
1457
+ this.targets.apply.map((db) => {
1458
+ return db.destroy();
1459
+ })
1460
+ );
1461
+ }
1462
+ }