sonamu 0.3.1 → 0.4.2

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 (83) hide show
  1. package/.pnp.cjs +11 -0
  2. package/dist/base-model-BzMJ2E_I.d.mts +43 -0
  3. package/dist/base-model-CWRKUX49.d.ts +43 -0
  4. package/dist/bin/cli.js +118 -89
  5. package/dist/bin/cli.js.map +1 -1
  6. package/dist/bin/cli.mjs +74 -45
  7. package/dist/bin/cli.mjs.map +1 -1
  8. package/dist/chunk-6HSW7OS3.js +1567 -0
  9. package/dist/chunk-6HSW7OS3.js.map +1 -0
  10. package/dist/chunk-FLPD24HS.mjs +231 -0
  11. package/dist/chunk-FLPD24HS.mjs.map +1 -0
  12. package/dist/{chunk-MPXE4IHO.mjs → chunk-PP2PSSAG.mjs} +5284 -5617
  13. package/dist/chunk-PP2PSSAG.mjs.map +1 -0
  14. package/dist/chunk-QK5XXJUX.mjs +280 -0
  15. package/dist/chunk-QK5XXJUX.mjs.map +1 -0
  16. package/dist/chunk-S6FYTR3V.mjs +1567 -0
  17. package/dist/chunk-S6FYTR3V.mjs.map +1 -0
  18. package/dist/chunk-U636LQJJ.js +231 -0
  19. package/dist/chunk-U636LQJJ.js.map +1 -0
  20. package/dist/chunk-W7KDVJLQ.js +280 -0
  21. package/dist/chunk-W7KDVJLQ.js.map +1 -0
  22. package/dist/{chunk-YXILRRDT.js → chunk-XT6LHCX5.js} +5252 -5585
  23. package/dist/chunk-XT6LHCX5.js.map +1 -0
  24. package/dist/database/drivers/knex/base-model.d.mts +16 -0
  25. package/dist/database/drivers/knex/base-model.d.ts +16 -0
  26. package/dist/database/drivers/knex/base-model.js +55 -0
  27. package/dist/database/drivers/knex/base-model.js.map +1 -0
  28. package/dist/database/drivers/knex/base-model.mjs +56 -0
  29. package/dist/database/drivers/knex/base-model.mjs.map +1 -0
  30. package/dist/database/drivers/kysely/base-model.d.mts +22 -0
  31. package/dist/database/drivers/kysely/base-model.d.ts +22 -0
  32. package/dist/database/drivers/kysely/base-model.js +64 -0
  33. package/dist/database/drivers/kysely/base-model.js.map +1 -0
  34. package/dist/database/drivers/kysely/base-model.mjs +65 -0
  35. package/dist/database/drivers/kysely/base-model.mjs.map +1 -0
  36. package/dist/index.d.mts +222 -928
  37. package/dist/index.d.ts +222 -928
  38. package/dist/index.js +13 -26
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +18 -31
  41. package/dist/index.mjs.map +1 -1
  42. package/dist/model-CAH_4oQh.d.mts +1042 -0
  43. package/dist/model-CAH_4oQh.d.ts +1042 -0
  44. package/import-to-require.js +27 -0
  45. package/package.json +24 -3
  46. package/src/api/caster.ts +6 -0
  47. package/src/api/code-converters.ts +3 -1
  48. package/src/api/sonamu.ts +41 -22
  49. package/src/bin/cli.ts +79 -46
  50. package/src/database/_batch_update.ts +16 -11
  51. package/src/database/base-model.abstract.ts +97 -0
  52. package/src/database/base-model.ts +214 -280
  53. package/src/database/code-generator.ts +72 -0
  54. package/src/database/db.abstract.ts +75 -0
  55. package/src/database/db.ts +21 -82
  56. package/src/database/drivers/knex/base-model.ts +55 -0
  57. package/src/database/drivers/knex/client.ts +209 -0
  58. package/src/database/drivers/knex/db.ts +227 -0
  59. package/src/database/drivers/knex/generator.ts +659 -0
  60. package/src/database/drivers/kysely/base-model.ts +89 -0
  61. package/src/database/drivers/kysely/client.ts +309 -0
  62. package/src/database/drivers/kysely/db.ts +238 -0
  63. package/src/database/drivers/kysely/generator.ts +714 -0
  64. package/src/database/types.ts +117 -0
  65. package/src/database/upsert-builder.ts +31 -18
  66. package/src/entity/entity-utils.ts +1 -1
  67. package/src/entity/migrator.ts +148 -711
  68. package/src/index.ts +1 -1
  69. package/src/syncer/syncer.ts +69 -27
  70. package/src/templates/generated_http.template.ts +14 -0
  71. package/src/templates/kysely_types.template.ts +205 -0
  72. package/src/templates/model.template.ts +2 -139
  73. package/src/templates/service.template.ts +3 -1
  74. package/src/testing/_relation-graph.ts +111 -0
  75. package/src/testing/fixture-manager.ts +216 -332
  76. package/src/types/types.ts +56 -6
  77. package/src/utils/utils.ts +56 -4
  78. package/src/utils/zod-error.ts +189 -0
  79. package/tsconfig.json +2 -2
  80. package/tsup.config.js +11 -10
  81. package/dist/chunk-MPXE4IHO.mjs.map +0 -1
  82. package/dist/chunk-YXILRRDT.js.map +0 -1
  83. /package/src/database/{knex-plugins → drivers/knex/plugins}/knex-on-duplicate-update.ts +0 -0
@@ -0,0 +1,714 @@
1
+ import _ from "lodash";
2
+ import prettier from "prettier";
3
+ import equal from "fast-deep-equal";
4
+ import {
5
+ GenMigrationCode,
6
+ MigrationColumn,
7
+ MigrationForeign,
8
+ MigrationIndex,
9
+ } from "../../../types/types";
10
+ import { CodeGenerator } from "../../code-generator";
11
+ import { EntityManager } from "../../../entity/entity-manager";
12
+
13
+ export class KyselyGenerator extends CodeGenerator {
14
+ async generateCreateCode_ColumnAndIndexes(
15
+ table: string,
16
+ columns: MigrationColumn[],
17
+ indexes: MigrationIndex[]
18
+ ): Promise<GenMigrationCode> {
19
+ // 컬럼, 인덱스 처리
20
+ const lines: string[] = [
21
+ "// @ts-ignore",
22
+ 'import { Kysely, sql } from "kysely";',
23
+ 'import { Database } from "sonamu";',
24
+ "",
25
+ "export async function up(db: Kysely<Database>): Promise<void> {",
26
+ `await db.schema.createTable("${table}")`,
27
+ ...this.genColumnDefinitions(columns),
28
+ ".execute();",
29
+ "",
30
+ "// indexes",
31
+ ...this.genIndexDefinitions(table, indexes),
32
+ "}",
33
+ "",
34
+ "export async function down(db: Kysely<Database>): Promise<void> {",
35
+ ` await db.schema.dropTable("${table}").execute();`,
36
+ "}",
37
+ ];
38
+
39
+ return {
40
+ table,
41
+ type: "normal",
42
+ title: `create__${table}`,
43
+ formatted: await prettier.format(lines.join("\n"), {
44
+ parser: "typescript",
45
+ }),
46
+ };
47
+ }
48
+
49
+ /*
50
+ 테이블 생성하는 케이스 - FK 생성
51
+ */
52
+ async generateCreateCode_Foreign(
53
+ table: string,
54
+ foreigns: MigrationForeign[]
55
+ ): Promise<GenMigrationCode[]> {
56
+ if (foreigns.length === 0) {
57
+ return [];
58
+ }
59
+
60
+ const { up, down } = this.genForeignDefinitions(table, foreigns);
61
+ if (up.length === 0 && down.length === 0) {
62
+ console.log("fk 가 뭔가 다릅니다");
63
+ return [];
64
+ }
65
+
66
+ const lines: string[] = [
67
+ "// @ts-ignore",
68
+ 'import { Kysely, sql } from "kysely";',
69
+ 'import { Database } from "sonamu";',
70
+ "",
71
+ "export async function up(db: Kysely<Database>): Promise<void> {",
72
+ ...up,
73
+ "}",
74
+ "",
75
+ "export async function down(db: Kysely<Database>): Promise<void> {",
76
+ ...down,
77
+ "}",
78
+ ];
79
+
80
+ const foreignKeysString = foreigns
81
+ .map((foreign) => foreign.columns.join("_"))
82
+ .join("_");
83
+ return [
84
+ {
85
+ table,
86
+ type: "foreign",
87
+ title: `foreign__${table}__${foreignKeysString}`,
88
+ formatted: await prettier.format(lines.join("\n"), {
89
+ parser: "typescript",
90
+ }),
91
+ },
92
+ ];
93
+ }
94
+
95
+ async generateAlterCode_ColumnAndIndexes(
96
+ table: string,
97
+ entityColumns: MigrationColumn[],
98
+ entityIndexes: MigrationIndex[],
99
+ dbColumns: MigrationColumn[],
100
+ dbIndexes: MigrationIndex[]
101
+ ): Promise<GenMigrationCode[]> {
102
+ /*
103
+ 세부 비교 후 다른점 찾아서 코드 생성
104
+
105
+ 1. 컬럼갯수 다름: MD에 있으나, DB에 없다면 추가
106
+ 2. 컬럼갯수 다름: MD에 없으나, DB에 있다면 삭제
107
+ 3. 그외 컬럼(컬럼 갯수가 동일하거나, 다른 경우 동일한 컬럼끼리) => alter
108
+ 4. 다른거 다 동일하고 index만 변경되는 경우
109
+
110
+ ** 컬럼명을 변경하는 경우는 따로 핸들링하지 않음
111
+ => drop/add 형태의 마이그레이션 코드가 생성되는데, 수동으로 rename 코드로 수정하여 처리
112
+ */
113
+
114
+ // 각 컬럼 이름 기준으로 add, drop, alter 여부 확인
115
+ const alterColumnsTo = this.getAlterColumnsTo(entityColumns, dbColumns);
116
+
117
+ // 추출된 컬럼들을 기준으로 각각 라인 생성
118
+ const alterColumnLinesTo = this.getAlterColumnLinesTo(
119
+ alterColumnsTo,
120
+ entityColumns
121
+ );
122
+
123
+ // 인덱스의 add, drop 여부 확인
124
+ const alterIndexesTo = this.getAlterIndexesTo(entityIndexes, dbIndexes);
125
+
126
+ // 추출된 인덱스들을 기준으로 각각 라인 생성
127
+ const alterIndexLinesTo = this.getAlterIndexLinesTo(
128
+ table,
129
+ alterIndexesTo,
130
+ alterColumnsTo
131
+ );
132
+
133
+ const alterColumnsToExist =
134
+ _.sumBy(Object.values(alterColumnsTo), (v) => v.length) > 0;
135
+ const lines: string[] = [
136
+ "// @ts-ignore",
137
+ 'import { Kysely, sql } from "kysely";',
138
+ 'import { Database } from "sonamu";',
139
+ "",
140
+ "export async function up(db: Kysely<Database>): Promise<void> {",
141
+ ...(alterColumnsToExist
142
+ ? [
143
+ `await db.schema.alterTable("${table}")`,
144
+ ...(alterColumnsTo.add.length > 0 ? alterColumnLinesTo.add.up : []),
145
+ ...(alterColumnsTo.drop.length > 0
146
+ ? alterColumnLinesTo.drop.up
147
+ : []),
148
+ ...(alterColumnsTo.alter.length > 0
149
+ ? alterColumnLinesTo.alter.up
150
+ : []),
151
+ ".execute();",
152
+ ]
153
+ : []),
154
+ ...(alterIndexesTo.add.length > 0 ? alterIndexLinesTo.add.up : []),
155
+ ...(alterIndexesTo.drop.length > 0 ? alterIndexLinesTo.drop.up : []),
156
+ "}",
157
+ "",
158
+ "export async function down(db: Kysely<Database>): Promise<void> {",
159
+ ...(alterColumnsToExist
160
+ ? [
161
+ `await db.schema.alterTable("${table}")`,
162
+ ...(alterColumnsTo.add.length > 0
163
+ ? alterColumnLinesTo.add.down
164
+ : []),
165
+ ...(alterColumnsTo.drop.length > 0
166
+ ? alterColumnLinesTo.drop.down
167
+ : []),
168
+ ...(alterColumnsTo.alter.length > 0
169
+ ? alterColumnLinesTo.alter.down
170
+ : []),
171
+ ".execute();",
172
+ ]
173
+ : []),
174
+ ...(alterIndexLinesTo.add.down.length > 0
175
+ ? alterIndexLinesTo.add.down
176
+ : []),
177
+ ...(alterIndexLinesTo.drop.down.length > 0
178
+ ? alterIndexLinesTo.drop.down
179
+ : []),
180
+ "}",
181
+ ];
182
+
183
+ const formatted = await prettier.format(lines.join("\n"), {
184
+ parser: "typescript",
185
+ });
186
+
187
+ const title = [
188
+ "alter",
189
+ table,
190
+ ...(["add", "drop", "alter"] as const)
191
+ .map((action) => {
192
+ const len = alterColumnsTo[action].length;
193
+ if (len > 0) {
194
+ return action + len;
195
+ }
196
+ return null;
197
+ })
198
+ .filter((part) => part !== null),
199
+ ].join("_");
200
+
201
+ return [
202
+ {
203
+ table,
204
+ title,
205
+ formatted,
206
+ type: "normal",
207
+ },
208
+ ];
209
+ }
210
+
211
+ async generateAlterCode_Foreigns(
212
+ table: string,
213
+ entityForeigns: MigrationForeign[],
214
+ dbForeigns: MigrationForeign[]
215
+ ): Promise<GenMigrationCode[]> {
216
+ const getKey = (mf: MigrationForeign): string => {
217
+ return [mf.columns.join("-"), mf.to].join("///");
218
+ };
219
+ const fkTo = entityForeigns.reduce(
220
+ (result, entityF) => {
221
+ const matchingDbF = dbForeigns.find(
222
+ (dbF) => getKey(entityF) === getKey(dbF)
223
+ );
224
+ if (!matchingDbF) {
225
+ result.add.push(entityF);
226
+ return result;
227
+ }
228
+
229
+ if (equal(entityF, matchingDbF) === false) {
230
+ result.alterSrc.push(matchingDbF);
231
+ result.alterDst.push(entityF);
232
+ return result;
233
+ }
234
+ return result;
235
+ },
236
+ {
237
+ add: [] as MigrationForeign[],
238
+ alterSrc: [] as MigrationForeign[],
239
+ alterDst: [] as MigrationForeign[],
240
+ }
241
+ );
242
+
243
+ const linesTo = {
244
+ add: this.genForeignDefinitions(table, fkTo.add),
245
+ alterSrc: this.genForeignDefinitions(table, fkTo.alterSrc),
246
+ alterDst: this.genForeignDefinitions(table, fkTo.alterDst),
247
+ };
248
+
249
+ const lines: string[] = [
250
+ "// @ts-ignore",
251
+ 'import { Kysely, sql } from "kysely";',
252
+ 'import { Database } from "sonamu";',
253
+ "",
254
+ "export async function up(db: Kysely<Database>): Promise<void> {",
255
+ ...linesTo.add.up,
256
+ ...linesTo.alterSrc.down,
257
+ ...linesTo.alterDst.up,
258
+ "}",
259
+ "",
260
+ "export async function down(db: Kysely<Database>): Promise<void> {",
261
+ ...linesTo.add.down,
262
+ ...linesTo.alterDst.down,
263
+ ...linesTo.alterSrc.up,
264
+ "}",
265
+ ];
266
+
267
+ const formatted = await prettier.format(lines.join("\n"), {
268
+ parser: "typescript",
269
+ });
270
+
271
+ const title = [
272
+ "alter",
273
+ table,
274
+ "foreigns",
275
+ // TODO 바뀌는 부분
276
+ ].join("_");
277
+
278
+ return [
279
+ {
280
+ table,
281
+ title,
282
+ formatted,
283
+ type: "normal",
284
+ },
285
+ ];
286
+ }
287
+
288
+ generateModelTemplate(
289
+ entityId: string,
290
+ def: { orderBy: string; search: string }
291
+ ) {
292
+ const names = EntityManager.getNamesFromId(entityId);
293
+ const entity = EntityManager.get(entityId);
294
+
295
+ return `
296
+ import { ListResult, asArray, NotFoundException, BadRequestException, api } from 'sonamu';
297
+ import { BaseModelClass } from 'sonamu/kysely';
298
+ import {
299
+ ${entityId}SubsetKey,
300
+ ${entityId}SubsetMapping,
301
+ } from "../sonamu.generated";
302
+ import {
303
+ ${names.camel}SubsetQueries,
304
+ } from "../sonamu.generated.sso";
305
+ import { ${entityId}ListParams, ${entityId}SaveParams } from "./${names.fs}.types";
306
+
307
+ /*
308
+ ${entityId} Model
309
+ */
310
+ class ${entityId}ModelClass extends BaseModelClass {
311
+ modelName = "${entityId}";
312
+
313
+ @api({ httpMethod: "GET", clients: ["axios", "swr"], resourceName: "${entityId}" })
314
+ async findById<T extends ${entityId}SubsetKey>(
315
+ subset: T,
316
+ id: number
317
+ ): Promise<${entityId}SubsetMapping[T]> {
318
+ const { rows } = await this.findMany(subset, {
319
+ id,
320
+ num: 1,
321
+ page: 1,
322
+ });
323
+ if (rows.length == 0) {
324
+ throw new NotFoundException(\`존재하지 않는 ${names.capital} ID \${id}\`);
325
+ }
326
+
327
+ return rows[0];
328
+ }
329
+
330
+ async findOne<T extends ${entityId}SubsetKey>(
331
+ subset: T,
332
+ listParams: ${entityId}ListParams
333
+ ): Promise<${entityId}SubsetMapping[T] | null> {
334
+ const { rows } = await this.findMany(subset, {
335
+ ...listParams,
336
+ num: 1,
337
+ page: 1,
338
+ });
339
+
340
+ return rows[0] ?? null;
341
+ }
342
+
343
+ @api({ httpMethod: "GET", clients: ["axios", "swr"], resourceName: "${names.capitalPlural}" })
344
+ async findMany<T extends ${entityId}SubsetKey>(
345
+ subset: T,
346
+ params: ${entityId}ListParams = {}
347
+ ): Promise<ListResult<${entityId}SubsetMapping[T]>> {
348
+ // params with defaults
349
+ params = {
350
+ num: 24,
351
+ page: 1,
352
+ search: "${def.search}",
353
+ orderBy: "${def.orderBy}",
354
+ ...params,
355
+ };
356
+
357
+ // build queries
358
+ let { rows, total } = await this.runSubsetQuery({
359
+ subset,
360
+ params,
361
+ subsetQuery: ${names.camel}SubsetQueries[subset],
362
+ build: ({ qb }) => {
363
+ // id
364
+ if (params.id) {
365
+ qb = qb.where("${entity.table}.id", "in", asArray(params.id));
366
+ }
367
+
368
+ // search-keyword
369
+ if (params.search && params.keyword && params.keyword.length > 0) {
370
+ if (params.search === "id") {
371
+ qb = qb.where("${entity.table}.id", "=", Number(params.keyword));
372
+ }
373
+ // } else if (params.search === "field") {
374
+ // qb = qb.where("${entity.table}.field", "like", \`%\${params.keyword}%\`);
375
+ // }
376
+ else {
377
+ throw new BadRequestException(
378
+ \`구현되지 않은 검색 필드 \${params.search}\`
379
+ );
380
+ }
381
+ }
382
+
383
+ // orderBy
384
+ if (params.orderBy) {
385
+ // default orderBy
386
+ const [orderByField, orderByDirec] = this.parseOrderBy(
387
+ params.orderBy
388
+ );
389
+ qb = qb.orderBy(orderByField, orderByDirec);
390
+ }
391
+
392
+ return qb;
393
+ },
394
+ debug: false,
395
+ });
396
+
397
+ return {
398
+ rows,
399
+ total,
400
+ };
401
+ }
402
+
403
+ @api({ httpMethod: "POST" })
404
+ async save(
405
+ spa: ${entityId}SaveParams[]
406
+ ): Promise<number[]> {
407
+ const wdb = this.getDB("w");
408
+ const ub = this.getUpsertBuilder();
409
+
410
+ // register
411
+ spa.map((sp) => {
412
+ ub.register("${entity.table}", sp);
413
+ });
414
+
415
+ // transaction
416
+ return wdb.transaction().execute(async (trx) => {
417
+ const ids = await ub.upsert(trx, "${entity.table}");
418
+
419
+ return ids;
420
+ });
421
+ }
422
+
423
+ @api({ httpMethod: "POST", guards: [ "admin" ] })
424
+ async del(ids: number[]): Promise<number> {
425
+ const wdb = this.getDB("w");
426
+
427
+ // transaction
428
+ await wdb.transaction().execute(async (trx) => {
429
+ return trx.deleteFrom("${entity.table}").where("${entity.table}.id", "in", ids).execute();
430
+ });
431
+
432
+ return ids.length;
433
+ }
434
+ }
435
+
436
+ export const ${entityId}Model = new ${entityId}ModelClass();
437
+ `.trim();
438
+ }
439
+
440
+ /*
441
+ MigrationColumn[] 읽어서 컬럼 정의하는 구문 생성
442
+ */
443
+ private genColumnDefinitions(columns: MigrationColumn[]): string[] {
444
+ return columns.map((column) => {
445
+ let str = "";
446
+ const chains: string[] = [];
447
+ if (column.name === "id") {
448
+ return `.addColumn("id", "integer", (col) => col.unsigned().autoIncrement().primaryKey())`;
449
+ }
450
+
451
+ if (column.type === "decimal") {
452
+ str = `.addColumn("${column.name}", "${column.type}(${column.precision}, ${column.scale})"`;
453
+ } else if (column.type.includes("text")) {
454
+ str = `.addColumn("${column.name}", sql\`${column.type.toUpperCase()}\``;
455
+ } else {
456
+ let columnType: string = column.type;
457
+ // FIXME: add double
458
+ if (columnType === "float") {
459
+ columnType = "float4";
460
+ } else if (columnType === "uuid") {
461
+ columnType = "char(36)";
462
+ } else if (columnType === "string") {
463
+ columnType = "varchar";
464
+ }
465
+ str = `.addColumn("${column.name}", "${columnType}${column.length ? `(${column.length})` : ""}"`;
466
+ }
467
+ if (column.unsigned) {
468
+ chains.push("unsigned()");
469
+ }
470
+
471
+ if (!column.nullable) {
472
+ chains.push("notNull()");
473
+ }
474
+
475
+ if (column.defaultTo !== undefined) {
476
+ if (
477
+ typeof column.defaultTo === "string" &&
478
+ column.defaultTo.startsWith(`"`)
479
+ ) {
480
+ chains.push(`defaultTo(${column.defaultTo})`);
481
+ } else {
482
+ chains.push(`defaultTo(sql\`${column.defaultTo}\`)`);
483
+ }
484
+ }
485
+ if (column.type === "uuid") {
486
+ chains.push("defaultTo(sql`UUID()`)");
487
+ }
488
+
489
+ return (
490
+ (chains.length > 0 ? `${str}, (col) => col.${chains.join(".")}` : str) +
491
+ ")"
492
+ );
493
+ });
494
+ }
495
+
496
+ /*
497
+ MigrationIndex[] 읽어서 인덱스/유니크 정의하는 구문 생성
498
+ */
499
+ private genIndexDefinitions(
500
+ table: string,
501
+ indexes: MigrationIndex[]
502
+ ): string[] {
503
+ if (indexes.length === 0) {
504
+ return [];
505
+ }
506
+
507
+ const lines = _.uniq(
508
+ indexes.reduce((r, index) => {
509
+ r.push(
510
+ `await db.schema.createIndex("${this.createIndexName(table, index.columns, index.type)}")
511
+ .on("${table}")
512
+ .columns([${index.columns.map((col) => `"${col}"`).join(",")}])${index.type === "unique" ? ".unique()" : ""}
513
+ .execute();`
514
+ );
515
+ return r;
516
+ }, [] as string[])
517
+ );
518
+ return lines;
519
+ }
520
+
521
+ /*
522
+ MigrationForeign[] 읽어서 외부키 constraint 정의하는 구문 생성
523
+ */
524
+ private genForeignDefinitions(
525
+ table: string,
526
+ foreigns: MigrationForeign[]
527
+ ): { up: string[]; down: string[] } {
528
+ return foreigns.reduce(
529
+ (r, foreign) => {
530
+ const [toTable, toColumn] = foreign.to.split(".");
531
+ const name = `${table}_${foreign.columns.join("_")}_foreign`;
532
+ const columnsStringQuote = foreign.columns
533
+ .map((col) => `'${col.replace(`${table}.`, "")}'`)
534
+ .join(",");
535
+
536
+ r.up.push(
537
+ "// create fk",
538
+ `await db.schema.alterTable("${table}")`,
539
+ `.addForeignKeyConstraint("${name}", [${columnsStringQuote}], "${toTable}", ["${toColumn}"])`,
540
+ `.onUpdate("${foreign.onUpdate.toLowerCase()}")`,
541
+ `.onDelete("${foreign.onDelete.toLowerCase()}")`,
542
+ `.execute();`
543
+ );
544
+ r.down.push(
545
+ "// drop fk",
546
+ `await db.schema.alterTable("${table}")`,
547
+ `.dropConstraint("${name}")`,
548
+ `.execute();`
549
+ );
550
+
551
+ return r;
552
+ },
553
+ {
554
+ up: [] as string[],
555
+ down: [] as string[],
556
+ }
557
+ );
558
+ }
559
+
560
+ private getAlterColumnLinesTo(
561
+ columnsTo: ReturnType<KyselyGenerator["getAlterColumnsTo"]>,
562
+ entityColumns: MigrationColumn[]
563
+ ) {
564
+ let linesTo = {
565
+ add: {
566
+ up: [] as string[],
567
+ down: [] as string[],
568
+ },
569
+ drop: {
570
+ up: [] as string[],
571
+ down: [] as string[],
572
+ },
573
+ alter: {
574
+ up: [] as string[],
575
+ down: [] as string[],
576
+ },
577
+ };
578
+
579
+ linesTo.add = {
580
+ up: ["// add", ...this.genColumnDefinitions(columnsTo.add)],
581
+ down: [
582
+ "// rollback - add",
583
+ ...columnsTo.add.map((col) => `.dropColumn("${col.name}")`),
584
+ ],
585
+ };
586
+ linesTo.drop = {
587
+ up: [
588
+ "// drop",
589
+ ...columnsTo.drop.map((col) => `.dropColumn("${col.name}")`),
590
+ ],
591
+ down: [
592
+ "// rollback - drop",
593
+ ...this.genColumnDefinitions(columnsTo.drop),
594
+ ],
595
+ };
596
+ linesTo.alter = columnsTo.alter.reduce(
597
+ (r, dbColumn) => {
598
+ const entityColumn = entityColumns.find(
599
+ (col) => col.name == dbColumn.name
600
+ );
601
+ if (entityColumn === undefined) {
602
+ return r;
603
+ }
604
+
605
+ // 컬럼 변경사항
606
+ const columnDiffUp = _.difference(
607
+ this.genColumnDefinitions([entityColumn]),
608
+ this.genColumnDefinitions([dbColumn])
609
+ );
610
+ const columnDiffDown = _.difference(
611
+ this.genColumnDefinitions([dbColumn]),
612
+ this.genColumnDefinitions([entityColumn])
613
+ );
614
+ if (columnDiffUp.length > 0) {
615
+ r.up = [
616
+ ...r.up,
617
+ "// alter column",
618
+ ...columnDiffUp.map((l) =>
619
+ l.replace(".addColumn", ".modifyColumn")
620
+ ),
621
+ ];
622
+ r.down = [
623
+ ...r.down,
624
+ "// rollback - alter column",
625
+ ...columnDiffDown.map((l) =>
626
+ l.replace(".addColumn", ".modifyColumn")
627
+ ),
628
+ ];
629
+ }
630
+
631
+ return r;
632
+ },
633
+ {
634
+ up: [] as string[],
635
+ down: [] as string[],
636
+ }
637
+ );
638
+
639
+ return linesTo;
640
+ }
641
+
642
+ private getAlterIndexLinesTo(
643
+ table: string,
644
+ indexesTo: ReturnType<KyselyGenerator["getAlterIndexesTo"]>,
645
+ columnsTo: ReturnType<KyselyGenerator["getAlterColumnsTo"]>
646
+ ) {
647
+ let linesTo = {
648
+ add: {
649
+ up: [] as string[],
650
+ down: [] as string[],
651
+ },
652
+ drop: {
653
+ up: [] as string[],
654
+ down: [] as string[],
655
+ },
656
+ };
657
+
658
+ // 인덱스가 추가되는 경우, 컬럼과 같이 추가된 케이스에는 drop에서 제외해야함!
659
+ linesTo.add = {
660
+ up: ["// add indexes", ...this.genIndexDefinitions(table, indexesTo.add)],
661
+ down: [
662
+ "// rollback - add indexes",
663
+ ...indexesTo.add
664
+ .filter(
665
+ (index) =>
666
+ index.columns.every((colName) =>
667
+ columnsTo.add.map((col) => col.name).includes(colName)
668
+ ) === false
669
+ )
670
+ .map(
671
+ (index) => `await db.schema.alterTable("${table}")
672
+ .dropIndex("${this.createIndexName(table, index.columns, index.type)}")
673
+ .execute();`
674
+ ),
675
+ ],
676
+ };
677
+ // 인덱스가 삭제되는 경우, 컬럼과 같이 삭제된 케이스에는 drop에서 제외해야함!
678
+ linesTo.drop = {
679
+ up: [
680
+ ...indexesTo.drop
681
+ .filter(
682
+ (index) =>
683
+ index.columns.every((colName) =>
684
+ columnsTo.drop.map((col) => col.name).includes(colName)
685
+ ) === false
686
+ )
687
+ .map(
688
+ (index) => `await db.schema.alterTable("${table}")
689
+ .dropIndex("${this.createIndexName(table, index.columns, index.type)}")
690
+ .execute();`
691
+ ),
692
+ ],
693
+ down: [
694
+ "// rollback - drop indexes",
695
+ ...this.genIndexDefinitions(table, indexesTo.drop),
696
+ ],
697
+ };
698
+
699
+ return linesTo;
700
+ }
701
+
702
+ private createIndexName(
703
+ table: string,
704
+ columns: string[],
705
+ type: string
706
+ ): string {
707
+ if (columns[0].split(".").length > 1) {
708
+ table = columns[0].split(".")[0];
709
+ columns = columns.map((col) => col.split(".")[1]);
710
+ }
711
+
712
+ return `${table}_${columns.join("_")}_${type}`;
713
+ }
714
+ }