sonamu 0.3.1 → 0.4.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 (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-FLPD24HS.mjs +231 -0
  9. package/dist/chunk-FLPD24HS.mjs.map +1 -0
  10. package/dist/chunk-I2MMJRJN.mjs +1550 -0
  11. package/dist/chunk-I2MMJRJN.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-U636LQJJ.js +231 -0
  17. package/dist/chunk-U636LQJJ.js.map +1 -0
  18. package/dist/chunk-W7KDVJLQ.js +280 -0
  19. package/dist/chunk-W7KDVJLQ.js.map +1 -0
  20. package/dist/{chunk-YXILRRDT.js → chunk-XT6LHCX5.js} +5252 -5585
  21. package/dist/chunk-XT6LHCX5.js.map +1 -0
  22. package/dist/chunk-Z2P7XTXE.js +1550 -0
  23. package/dist/chunk-Z2P7XTXE.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 +220 -926
  37. package/dist/index.d.ts +220 -926
  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 +23 -2
  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 +78 -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 +98 -693
  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
@@ -1,198 +1,28 @@
1
+ // base-model.ts
1
2
  import { DateTime } from "luxon";
2
- import { Knex } from "knex";
3
3
  import _ from "lodash";
4
- import { DBPreset, DB } from "./db";
5
- import { isCustomJoinClause, SubsetQuery } from "../types/types";
6
- import { BaseListParams } from "../utils/model";
7
- import inflection from "inflection";
8
- import chalk from "chalk";
9
- import { UpsertBuilder } from "./upsert-builder";
10
4
  import SqlParser from "node-sql-parser";
5
+ import chalk from "chalk";
6
+ import inflection from "inflection";
7
+ import { BaseListParams } from "../utils/model";
8
+ import { DBPreset, DriverSpec, DatabaseDriver } from "./types";
9
+ import { SubsetQuery } from "../types/types";
11
10
  import { getTableName, getTableNamesFromWhere } from "../utils/sql-parser";
11
+ import { DB } from "./db";
12
12
 
13
- export class BaseModelClass {
13
+ export abstract class BaseModelClassAbstract<D extends DatabaseDriver> {
14
14
  public modelName: string = "Unknown";
15
15
 
16
- /* DB 인스턴스 get, destroy */
17
- getDB(which: DBPreset): Knex {
18
- return DB.getDB(which);
19
- }
20
- async destroy() {
21
- return DB.destroy();
22
- }
23
-
24
- myNow(timestamp?: number): string {
25
- const dt: DateTime =
26
- timestamp === undefined
27
- ? DateTime.local()
28
- : DateTime.fromSeconds(timestamp);
29
- return dt.toFormat("yyyy-MM-dd HH:mm:ss");
30
- }
31
-
32
- async getInsertedIds(
33
- wdb: Knex,
34
- rows: any[],
35
- tableName: string,
36
- unqKeyFields: string[],
37
- chunkSize: number = 500
38
- ) {
39
- if (!wdb) {
40
- wdb = this.getDB("w");
41
- }
42
-
43
- let unqKeys: string[];
44
- let whereInField: any, selectField: string;
45
- if (unqKeyFields.length > 1) {
46
- whereInField = wdb.raw(`CONCAT_WS('_', '${unqKeyFields.join(",")}')`);
47
- selectField = `${whereInField} as tmpUid`;
48
- unqKeys = rows.map((row) =>
49
- unqKeyFields.map((field) => row[field]).join("_")
50
- );
51
- } else {
52
- whereInField = unqKeyFields[0];
53
- selectField = unqKeyFields[0];
54
- unqKeys = rows.map((row) => row[unqKeyFields[0]]);
55
- }
56
- const chunks = _.chunk(unqKeys, chunkSize);
57
-
58
- let resultIds: number[] = [];
59
- for (let chunk of chunks) {
60
- const dbRows = await wdb(tableName)
61
- .select("id", wdb.raw(selectField))
62
- .whereIn(whereInField, chunk);
63
- resultIds = resultIds.concat(
64
- dbRows.map((dbRow: any) => parseInt(dbRow.id))
65
- );
66
- }
67
-
68
- return resultIds;
69
- }
16
+ protected abstract applyJoins(
17
+ qb: DriverSpec[D]["adapter"],
18
+ joins: SubsetQuery["joins"]
19
+ ): DriverSpec[D]["adapter"];
20
+ protected abstract executeCountQuery(
21
+ qb: DriverSpec[D]["queryBuilder"]
22
+ ): Promise<number>;
70
23
 
71
- async useLoaders(db: Knex, rows: any[], loaders: SubsetQuery["loaders"]) {
72
- if (loaders.length === 0) {
73
- return rows;
74
- }
75
-
76
- for (let loader of loaders) {
77
- let subQ: any;
78
- let subRows: any[];
79
- let toCol: string;
80
-
81
- const fromIds = rows.map((row) => row[loader.manyJoin.idField]);
82
-
83
- if (loader.manyJoin.through === undefined) {
84
- // HasMany
85
- const idColumn = `${loader.manyJoin.toTable}.${loader.manyJoin.toCol}`;
86
- subQ = db(loader.manyJoin.toTable)
87
- .whereIn(idColumn, fromIds)
88
- .select([...loader.select, idColumn]);
89
-
90
- // HasMany에서 OneJoin이 있는 경우
91
- loader.oneJoins.map((join) => {
92
- if (join.join == "inner") {
93
- subQ.innerJoin(
94
- `${join.table} as ${join.as}`,
95
- this.getJoinClause(db, join)
96
- );
97
- } else if (join.join == "outer") {
98
- subQ.leftOuterJoin(
99
- `${join.table} as ${join.as}`,
100
- this.getJoinClause(db, join)
101
- );
102
- }
103
- });
104
- toCol = loader.manyJoin.toCol;
105
- } else {
106
- // ManyToMany
107
- const idColumn = `${loader.manyJoin.through.table}.${loader.manyJoin.through.fromCol}`;
108
- subQ = db(loader.manyJoin.through.table)
109
- .join(
110
- loader.manyJoin.toTable,
111
- `${loader.manyJoin.through.table}.${loader.manyJoin.through.toCol}`,
112
- `${loader.manyJoin.toTable}.${loader.manyJoin.toCol}`
113
- )
114
- .whereIn(idColumn, fromIds)
115
- .select(_.uniq([...loader.select, idColumn]));
116
-
117
- // ManyToMany에서 OneJoin이 있는 경우
118
- loader.oneJoins.map((join) => {
119
- if (join.join == "inner") {
120
- subQ.innerJoin(
121
- `${join.table} as ${join.as}`,
122
- this.getJoinClause(db, join)
123
- );
124
- } else if (join.join == "outer") {
125
- subQ.leftOuterJoin(
126
- `${join.table} as ${join.as}`,
127
- this.getJoinClause(db, join)
128
- );
129
- }
130
- });
131
- toCol = loader.manyJoin.through.fromCol;
132
- }
133
- subRows = await subQ;
134
-
135
- if (loader.loaders) {
136
- // 추가 -Many 케이스가 있는 경우 recursion 처리
137
- subRows = await this.useLoaders(db, subRows, loader.loaders);
138
- }
139
-
140
- // 불러온 row들을 참조ID 기준으로 분류 배치
141
- const subRowGroups = _.groupBy(subRows, toCol);
142
- rows = rows.map((row) => {
143
- row[loader.as] = (subRowGroups[row[loader.manyJoin.idField]] ?? []).map(
144
- (r) => _.omit(r, toCol)
145
- );
146
- return row;
147
- });
148
- }
149
- return rows;
150
- }
151
-
152
- hydrate<T>(rows: T[]): T[] {
153
- return rows.map((row: any) => {
154
- // nullable relation인 경우 관련된 필드가 전부 null로 생성되는 것 방지하는 코드
155
- const nestedKeys = Object.keys(row).filter((key) => key.includes("__"));
156
- const groups = _.groupBy(nestedKeys, (key) => key.split("__")[0]);
157
- const nullKeys = Object.keys(groups).filter(
158
- (key) =>
159
- groups[key].length > 1 &&
160
- groups[key].every((field) => row[field] === null)
161
- );
162
-
163
- const hydrated = Object.keys(row).reduce((r, field) => {
164
- if (!field.includes("__")) {
165
- if (Array.isArray(row[field]) && _.isObject(row[field][0])) {
166
- r[field] = this.hydrate(row[field]);
167
- return r;
168
- } else {
169
- r[field] = row[field];
170
- return r;
171
- }
172
- }
173
-
174
- const parts = field.split("__");
175
- const objPath =
176
- parts[0] +
177
- parts
178
- .slice(1)
179
- .map((part) => `[${part}]`)
180
- .join("");
181
- _.set(
182
- r,
183
- objPath,
184
- row[field] && Array.isArray(row[field]) && _.isObject(row[field][0])
185
- ? this.hydrate(row[field])
186
- : row[field]
187
- );
188
-
189
- return r;
190
- }, {} as any);
191
- nullKeys.map((nullKey) => (hydrated[nullKey] = null));
192
-
193
- return hydrated;
194
- });
195
- }
24
+ abstract getDB(which: DBPreset): DriverSpec[D]["core"];
25
+ abstract destroy(): Promise<void>;
196
26
 
197
27
  async runSubsetQuery<T extends BaseListParams, U extends string>({
198
28
  params,
@@ -208,161 +38,265 @@ export class BaseModelClass {
208
38
  params: T;
209
39
  subsetQuery: SubsetQuery;
210
40
  build: (buildParams: {
211
- qb: Knex.QueryBuilder;
212
- db: Knex;
213
- select: (string | Knex.Raw)[];
41
+ qb: DriverSpec[D]["queryBuilder"];
42
+ db: DriverSpec[D]["core"];
43
+ select: SubsetQuery["select"];
214
44
  joins: SubsetQuery["joins"];
215
45
  virtual: string[];
216
- }) => Knex.QueryBuilder;
217
- baseTable?: string;
46
+ }) => DriverSpec[D]["queryBuilder"];
47
+ baseTable?: DriverSpec[D]["table"];
218
48
  debug?: boolean | "list" | "count";
219
- db?: Knex;
49
+ db?: DriverSpec[D]["core"];
220
50
  optimizeCountQuery?: boolean;
221
51
  }): Promise<{
222
52
  rows: any[];
223
- total?: number | undefined;
53
+ total?: number;
224
54
  subsetQuery: SubsetQuery;
225
- qb: Knex.QueryBuilder;
55
+ qb: DriverSpec[D]["queryBuilder"];
226
56
  }> {
227
- const db = _db ?? this.getDB(subset.startsWith("A") ? "w" : "r");
57
+ const db = _db ?? DB.getDB(subset.startsWith("A") ? "w" : "r");
58
+ const dbClient = DB.toClient(db);
228
59
  baseTable =
229
60
  baseTable ?? inflection.pluralize(inflection.underscore(this.modelName));
230
61
  const queryMode =
231
62
  params.queryMode ?? (params.id !== undefined ? "list" : "both");
232
63
 
233
64
  const { select, virtual, joins, loaders } = subsetQuery;
234
- const qb = build({
235
- qb: db.from(baseTable),
65
+ const _qb = build({
66
+ qb: dbClient.from(baseTable).qb,
236
67
  db,
237
68
  select,
238
69
  joins,
239
70
  virtual,
240
71
  });
72
+ dbClient.qb = _qb;
73
+ const qb = dbClient;
241
74
 
242
- const applyJoinClause = (
243
- qb: Knex.QueryBuilder,
244
- joins: SubsetQuery["joins"]
245
- ) => {
246
- joins.map((join) => {
247
- if (join.join == "inner") {
248
- qb.innerJoin(
249
- `${join.table} as ${join.as}`,
250
- this.getJoinClause(db, join)
251
- );
252
- } else if (join.join == "outer") {
253
- qb.leftOuterJoin(
254
- `${join.table} as ${join.as}`,
255
- this.getJoinClause(db, join)
256
- );
257
- }
258
- });
259
- };
260
-
261
- // countQuery
262
75
  const total = await (async () => {
263
- if (queryMode === "list") {
264
- return undefined;
265
- }
76
+ if (queryMode === "list") return undefined;
266
77
 
267
- const clonedQb = qb.clone().clear("order").clear("offset").clear("limit");
78
+ const clonedQb = qb
79
+ .clone()
80
+ .clearQueryParts(["order", "offset", "limit"])
81
+ .clearSelect()
82
+ .select(`${baseTable}.id`);
268
83
  const parser = new SqlParser.Parser();
269
84
 
270
- // optmizeCountQuery가 true인 경우 다른 clause에 영향을 주지 않는 모든 join을 제외함
271
85
  if (optimizeCountQuery) {
272
- const parsedQuery = parser.astify(clonedQb.toQuery());
86
+ const parsedQuery = parser.astify(clonedQb.sql);
273
87
  const tables = getTableNamesFromWhere(parsedQuery);
274
- // where절에 사용되는 테이블의 조인을 위해 사용되는 테이블
275
88
  const needToJoin = _.uniq(
276
89
  tables.flatMap((table) =>
277
90
  table.split("__").map((t) => inflection.pluralize(t))
278
91
  )
279
92
  );
280
- applyJoinClause(
93
+
94
+ this.applyJoins(
281
95
  clonedQb,
282
96
  joins.filter((j) => needToJoin.includes(j.table))
283
97
  );
284
98
  } else {
285
- applyJoinClause(clonedQb, joins);
99
+ this.applyJoins(clonedQb, joins);
286
100
  }
287
101
 
288
- const parsedQuery = parser.astify(clonedQb.toQuery());
102
+ const parsedQuery = parser.astify(clonedQb.sql);
289
103
  const q = Array.isArray(parsedQuery) ? parsedQuery[0] : parsedQuery;
104
+
290
105
  if (q.type !== "select") {
291
106
  throw new Error("Invalid query");
292
107
  }
293
108
 
294
- const countQuery =
295
- q.distinct !== null
296
- ? clonedQb
297
- .clear("select")
298
- .select(
299
- db.raw(
300
- `COUNT(DISTINCT \`${getTableName(q.columns[0].expr)}\`.\`${q.columns[0].expr.column}\`) as total`
301
- )
302
- )
303
- .first()
304
- : clonedQb.clear("select").count("*", { as: "total" }).first();
305
- const countRow: { total?: number } = await countQuery;
306
-
307
- // debug: countQuery
109
+ const countColumn = `${getTableName(q.columns[0].expr)}.${q.columns[0].expr.column}`;
110
+ clonedQb.clearSelect().count(countColumn, "total").first();
111
+ if (q.distinct) {
112
+ clonedQb.distinct(countColumn);
113
+ }
114
+
308
115
  if (debug === true || debug === "count") {
309
- console.debug(
310
- "DEBUG: count query",
311
- chalk.blue(countQuery.toQuery().toString())
312
- );
116
+ console.debug("DEBUG: count query", chalk.blue(clonedQb.sql));
313
117
  }
314
118
 
315
- return countRow?.total ?? 0;
119
+ const [{ total }] = await clonedQb.execute();
120
+ return total;
316
121
  })();
317
122
 
318
- // listQuery
319
123
  const rows = await (async () => {
320
- if (queryMode === "count") {
321
- return [];
322
- }
124
+ if (queryMode === "count") return [];
323
125
 
324
- // limit, offset
126
+ let listQb = qb;
325
127
  if (params.num !== 0) {
326
- qb.limit(params.num!);
327
- qb.offset(params.num! * (params.page! - 1));
128
+ listQb = listQb
129
+ .limit(params.num!)
130
+ .offset(params.num! * (params.page! - 1));
328
131
  }
329
132
 
330
- // select, rows
331
- const listQuery = qb.clone().select(select);
133
+ listQb.select(select);
134
+ listQb = this.applyJoins(listQb, joins);
332
135
 
333
- // join
334
- applyJoinClause(listQuery, joins);
335
-
336
- let rows = await listQuery;
337
- // debug: listQuery
338
136
  if (debug === true || debug === "list") {
339
- console.debug(
340
- "DEBUG: list query",
341
- chalk.blue(listQuery.toQuery().toString())
342
- );
137
+ console.debug("DEBUG: list query", chalk.blue(listQb.sql));
343
138
  }
344
139
 
345
- rows = await this.useLoaders(db, rows, loaders);
346
- rows = this.hydrate(rows);
347
- return rows;
140
+ let rows = await listQb.execute();
141
+ rows = await this.useLoaders(dbClient, rows, loaders);
142
+ return this.hydrate(rows);
348
143
  })();
349
144
 
350
- return { rows, total, subsetQuery, qb };
145
+ return {
146
+ rows,
147
+ total,
148
+ subsetQuery,
149
+ qb: dbClient.qb,
150
+ };
351
151
  }
352
152
 
353
- getJoinClause(
354
- db: Knex<any, unknown>,
355
- join: SubsetQuery["joins"][number]
356
- ): Knex.Raw<any> {
357
- if (!isCustomJoinClause(join)) {
358
- return db.raw(`${join.from} = ${join.to}`);
359
- } else {
360
- return db.raw(join.custom);
153
+ async useLoaders(
154
+ db: DriverSpec[D]["adapter"],
155
+ rows: any[],
156
+ loaders: SubsetQuery["loaders"]
157
+ ): Promise<any[]> {
158
+ if (loaders.length === 0) return rows;
159
+
160
+ for (const loader of loaders) {
161
+ const fromIds = rows.map((row) => row[loader.manyJoin.idField]);
162
+
163
+ if (!fromIds.length) continue;
164
+
165
+ let subRows: any[];
166
+ let toCol: string;
167
+
168
+ if (loader.manyJoin.through === undefined) {
169
+ // HasMany
170
+ const { subQ, col } = await this.buildHasManyQuery(db, loader, fromIds);
171
+ subRows = await subQ.execute();
172
+ toCol = col;
173
+ } else {
174
+ // ManyToMany
175
+ const { subQ, col } = await this.buildManyToManyQuery(
176
+ db,
177
+ loader,
178
+ fromIds
179
+ );
180
+ subRows = await subQ.execute();
181
+ toCol = col;
182
+ }
183
+
184
+ if (loader.loaders) {
185
+ subRows = await this.useLoaders(db, subRows, loader.loaders);
186
+ }
187
+
188
+ // Group and assign loaded rows
189
+ const subRowGroups = _.groupBy(subRows, toCol);
190
+ rows = rows.map((row) => {
191
+ row[loader.as] = (subRowGroups[row[loader.manyJoin.idField]] ?? []).map(
192
+ (r) => _.omit(r, toCol)
193
+ );
194
+ return row;
195
+ });
361
196
  }
197
+
198
+ return rows;
199
+ }
200
+
201
+ protected async buildHasManyQuery(
202
+ db: DriverSpec[D]["adapter"],
203
+ loader: SubsetQuery["loaders"][number],
204
+ fromIds: any[]
205
+ ) {
206
+ const idColumn = `${loader.manyJoin.toTable}.${loader.manyJoin.toCol}`;
207
+ let qb = db.from(loader.manyJoin.toTable);
208
+
209
+ db.where([idColumn, "in", fromIds]).select([...loader.select, idColumn]);
210
+ qb = this.applyJoins(qb, loader.oneJoins);
211
+
212
+ return {
213
+ subQ: qb,
214
+ col: loader.manyJoin.toCol,
215
+ };
362
216
  }
363
217
 
364
- getUpsertBuilder(): UpsertBuilder {
365
- return new UpsertBuilder();
218
+ protected async buildManyToManyQuery(
219
+ db: DriverSpec[D]["adapter"],
220
+ loader: SubsetQuery["loaders"][number],
221
+ fromIds: any[]
222
+ ) {
223
+ if (!loader.manyJoin.through)
224
+ throw new Error("Through table info missing for many-to-many relation");
225
+
226
+ const idColumn = `${loader.manyJoin.through.table}.${loader.manyJoin.through.fromCol}`;
227
+ let qb = db.from(loader.manyJoin.through.table);
228
+
229
+ const throughTable = loader.manyJoin.through.table;
230
+ const targetTable = loader.manyJoin.toTable;
231
+
232
+ qb = this.applyJoins(qb, [
233
+ {
234
+ join: "inner",
235
+ table: targetTable,
236
+ as: targetTable,
237
+ from: `${throughTable}.${loader.manyJoin.through.toCol}`,
238
+ to: `${targetTable}.${loader.manyJoin.toCol}`,
239
+ },
240
+ ]);
241
+
242
+ qb.where([idColumn, "in", fromIds]).select([...loader.select, idColumn]);
243
+ qb = this.applyJoins(qb, loader.oneJoins);
244
+
245
+ return {
246
+ subQ: qb,
247
+ col: loader.manyJoin.through.fromCol,
248
+ };
249
+ }
250
+
251
+ myNow(timestamp?: number): string {
252
+ const dt: DateTime =
253
+ timestamp === undefined
254
+ ? DateTime.local()
255
+ : DateTime.fromSeconds(timestamp);
256
+ return dt.toFormat("yyyy-MM-dd HH:mm:ss");
257
+ }
258
+
259
+ hydrate<T>(rows: T[]): T[] {
260
+ return rows.map((row: any) => {
261
+ const nestedKeys = Object.keys(row).filter((key) => key.includes("__"));
262
+ const groups = _.groupBy(nestedKeys, (key) => key.split("__")[0]);
263
+ const nullKeys = Object.keys(groups).filter(
264
+ (key) =>
265
+ groups[key].length > 1 &&
266
+ groups[key].every((field) => row[field] === null)
267
+ );
268
+
269
+ const hydrated = Object.keys(row).reduce((r, field) => {
270
+ if (!field.includes("__")) {
271
+ if (Array.isArray(row[field]) && _.isObject(row[field][0])) {
272
+ r[field] = this.hydrate(row[field]);
273
+ } else {
274
+ r[field] = row[field];
275
+ }
276
+ return r;
277
+ }
278
+
279
+ const parts = field.split("__");
280
+ const objPath =
281
+ parts[0] +
282
+ parts
283
+ .slice(1)
284
+ .map((part) => `[${part}]`)
285
+ .join("");
286
+
287
+ _.set(
288
+ r,
289
+ objPath,
290
+ row[field] && Array.isArray(row[field]) && _.isObject(row[field][0])
291
+ ? this.hydrate(row[field])
292
+ : row[field]
293
+ );
294
+
295
+ return r;
296
+ }, {} as any);
297
+
298
+ nullKeys.forEach((nullKey) => (hydrated[nullKey] = null));
299
+ return hydrated;
300
+ });
366
301
  }
367
302
  }
368
- export const BaseModel = new BaseModelClass();
@@ -0,0 +1,72 @@
1
+ import _ from "lodash";
2
+ import equal from "fast-deep-equal";
3
+ import { MigrationColumn, MigrationIndex } from "../types/types";
4
+
5
+ export class CodeGenerator {
6
+ getAlterColumnsTo(
7
+ entityColumns: MigrationColumn[],
8
+ dbColumns: MigrationColumn[]
9
+ ) {
10
+ const columnsTo = {
11
+ add: [] as MigrationColumn[],
12
+ drop: [] as MigrationColumn[],
13
+ alter: [] as MigrationColumn[],
14
+ };
15
+
16
+ // 컬럼명 기준 비교
17
+ const extraColumns = {
18
+ db: _.differenceBy(dbColumns, entityColumns, (col) => col.name),
19
+ entity: _.differenceBy(entityColumns, dbColumns, (col) => col.name),
20
+ };
21
+ if (extraColumns.entity.length > 0) {
22
+ columnsTo.add = columnsTo.add.concat(extraColumns.entity);
23
+ }
24
+ if (extraColumns.db.length > 0) {
25
+ columnsTo.drop = columnsTo.drop.concat(extraColumns.db);
26
+ }
27
+
28
+ // 동일 컬럼명의 세부 필드 비교
29
+ const sameDbColumns = _.intersectionBy(
30
+ dbColumns,
31
+ entityColumns,
32
+ (col) => col.name
33
+ );
34
+ const sameMdColumns = _.intersectionBy(
35
+ entityColumns,
36
+ dbColumns,
37
+ (col) => col.name
38
+ );
39
+ columnsTo.alter = _.differenceWith(sameDbColumns, sameMdColumns, (a, b) =>
40
+ equal(a, b)
41
+ );
42
+
43
+ return columnsTo;
44
+ }
45
+
46
+ getAlterIndexesTo(
47
+ entityIndexes: MigrationIndex[],
48
+ dbIndexes: MigrationIndex[]
49
+ ) {
50
+ // 인덱스 비교
51
+ let indexesTo = {
52
+ add: [] as MigrationIndex[],
53
+ drop: [] as MigrationIndex[],
54
+ };
55
+ const extraIndexes = {
56
+ db: _.differenceBy(dbIndexes, entityIndexes, (col) =>
57
+ [col.type, col.columns.join("-")].join("//")
58
+ ),
59
+ entity: _.differenceBy(entityIndexes, dbIndexes, (col) =>
60
+ [col.type, col.columns.join("-")].join("//")
61
+ ),
62
+ };
63
+ if (extraIndexes.entity.length > 0) {
64
+ indexesTo.add = indexesTo.add.concat(extraIndexes.entity);
65
+ }
66
+ if (extraIndexes.db.length > 0) {
67
+ indexesTo.drop = indexesTo.drop.concat(extraIndexes.db);
68
+ }
69
+
70
+ return indexesTo;
71
+ }
72
+ }