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.
- package/.pnp.cjs +11 -0
- package/dist/base-model-BzMJ2E_I.d.mts +43 -0
- package/dist/base-model-CWRKUX49.d.ts +43 -0
- package/dist/bin/cli.js +118 -89
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/cli.mjs +74 -45
- package/dist/bin/cli.mjs.map +1 -1
- package/dist/chunk-FLPD24HS.mjs +231 -0
- package/dist/chunk-FLPD24HS.mjs.map +1 -0
- package/dist/chunk-I2MMJRJN.mjs +1550 -0
- package/dist/chunk-I2MMJRJN.mjs.map +1 -0
- package/dist/{chunk-MPXE4IHO.mjs → chunk-PP2PSSAG.mjs} +5284 -5617
- package/dist/chunk-PP2PSSAG.mjs.map +1 -0
- package/dist/chunk-QK5XXJUX.mjs +280 -0
- package/dist/chunk-QK5XXJUX.mjs.map +1 -0
- package/dist/chunk-U636LQJJ.js +231 -0
- package/dist/chunk-U636LQJJ.js.map +1 -0
- package/dist/chunk-W7KDVJLQ.js +280 -0
- package/dist/chunk-W7KDVJLQ.js.map +1 -0
- package/dist/{chunk-YXILRRDT.js → chunk-XT6LHCX5.js} +5252 -5585
- package/dist/chunk-XT6LHCX5.js.map +1 -0
- package/dist/chunk-Z2P7XTXE.js +1550 -0
- package/dist/chunk-Z2P7XTXE.js.map +1 -0
- package/dist/database/drivers/knex/base-model.d.mts +16 -0
- package/dist/database/drivers/knex/base-model.d.ts +16 -0
- package/dist/database/drivers/knex/base-model.js +55 -0
- package/dist/database/drivers/knex/base-model.js.map +1 -0
- package/dist/database/drivers/knex/base-model.mjs +56 -0
- package/dist/database/drivers/knex/base-model.mjs.map +1 -0
- package/dist/database/drivers/kysely/base-model.d.mts +22 -0
- package/dist/database/drivers/kysely/base-model.d.ts +22 -0
- package/dist/database/drivers/kysely/base-model.js +64 -0
- package/dist/database/drivers/kysely/base-model.js.map +1 -0
- package/dist/database/drivers/kysely/base-model.mjs +65 -0
- package/dist/database/drivers/kysely/base-model.mjs.map +1 -0
- package/dist/index.d.mts +220 -926
- package/dist/index.d.ts +220 -926
- package/dist/index.js +13 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -31
- package/dist/index.mjs.map +1 -1
- package/dist/model-CAH_4oQh.d.mts +1042 -0
- package/dist/model-CAH_4oQh.d.ts +1042 -0
- package/import-to-require.js +27 -0
- package/package.json +23 -2
- package/src/api/caster.ts +6 -0
- package/src/api/code-converters.ts +3 -1
- package/src/api/sonamu.ts +41 -22
- package/src/bin/cli.ts +78 -46
- package/src/database/_batch_update.ts +16 -11
- package/src/database/base-model.abstract.ts +97 -0
- package/src/database/base-model.ts +214 -280
- package/src/database/code-generator.ts +72 -0
- package/src/database/db.abstract.ts +75 -0
- package/src/database/db.ts +21 -82
- package/src/database/drivers/knex/base-model.ts +55 -0
- package/src/database/drivers/knex/client.ts +209 -0
- package/src/database/drivers/knex/db.ts +227 -0
- package/src/database/drivers/knex/generator.ts +659 -0
- package/src/database/drivers/kysely/base-model.ts +89 -0
- package/src/database/drivers/kysely/client.ts +309 -0
- package/src/database/drivers/kysely/db.ts +238 -0
- package/src/database/drivers/kysely/generator.ts +714 -0
- package/src/database/types.ts +117 -0
- package/src/database/upsert-builder.ts +31 -18
- package/src/entity/entity-utils.ts +1 -1
- package/src/entity/migrator.ts +98 -693
- package/src/index.ts +1 -1
- package/src/syncer/syncer.ts +69 -27
- package/src/templates/generated_http.template.ts +14 -0
- package/src/templates/kysely_types.template.ts +205 -0
- package/src/templates/model.template.ts +2 -139
- package/src/templates/service.template.ts +3 -1
- package/src/testing/_relation-graph.ts +111 -0
- package/src/testing/fixture-manager.ts +216 -332
- package/src/types/types.ts +56 -6
- package/src/utils/utils.ts +56 -4
- package/src/utils/zod-error.ts +189 -0
- package/tsconfig.json +2 -2
- package/tsup.config.js +11 -10
- package/dist/chunk-MPXE4IHO.mjs.map +0 -1
- package/dist/chunk-YXILRRDT.js.map +0 -1
- /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
|
|
13
|
+
export abstract class BaseModelClassAbstract<D extends DatabaseDriver> {
|
|
14
14
|
public modelName: string = "Unknown";
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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:
|
|
212
|
-
db:
|
|
213
|
-
select:
|
|
41
|
+
qb: DriverSpec[D]["queryBuilder"];
|
|
42
|
+
db: DriverSpec[D]["core"];
|
|
43
|
+
select: SubsetQuery["select"];
|
|
214
44
|
joins: SubsetQuery["joins"];
|
|
215
45
|
virtual: string[];
|
|
216
|
-
}) =>
|
|
217
|
-
baseTable?:
|
|
46
|
+
}) => DriverSpec[D]["queryBuilder"];
|
|
47
|
+
baseTable?: DriverSpec[D]["table"];
|
|
218
48
|
debug?: boolean | "list" | "count";
|
|
219
|
-
db?:
|
|
49
|
+
db?: DriverSpec[D]["core"];
|
|
220
50
|
optimizeCountQuery?: boolean;
|
|
221
51
|
}): Promise<{
|
|
222
52
|
rows: any[];
|
|
223
|
-
total?: number
|
|
53
|
+
total?: number;
|
|
224
54
|
subsetQuery: SubsetQuery;
|
|
225
|
-
qb:
|
|
55
|
+
qb: DriverSpec[D]["queryBuilder"];
|
|
226
56
|
}> {
|
|
227
|
-
const db = _db ??
|
|
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
|
|
235
|
-
qb:
|
|
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
|
|
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.
|
|
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
|
-
|
|
93
|
+
|
|
94
|
+
this.applyJoins(
|
|
281
95
|
clonedQb,
|
|
282
96
|
joins.filter((j) => needToJoin.includes(j.table))
|
|
283
97
|
);
|
|
284
98
|
} else {
|
|
285
|
-
|
|
99
|
+
this.applyJoins(clonedQb, joins);
|
|
286
100
|
}
|
|
287
101
|
|
|
288
|
-
const parsedQuery = parser.astify(clonedQb.
|
|
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
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
+
let listQb = qb;
|
|
325
127
|
if (params.num !== 0) {
|
|
326
|
-
|
|
327
|
-
|
|
128
|
+
listQb = listQb
|
|
129
|
+
.limit(params.num!)
|
|
130
|
+
.offset(params.num! * (params.page! - 1));
|
|
328
131
|
}
|
|
329
132
|
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
346
|
-
rows = this.
|
|
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 {
|
|
145
|
+
return {
|
|
146
|
+
rows,
|
|
147
|
+
total,
|
|
148
|
+
subsetQuery,
|
|
149
|
+
qb: dbClient.qb,
|
|
150
|
+
};
|
|
351
151
|
}
|
|
352
152
|
|
|
353
|
-
|
|
354
|
-
db:
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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
|
+
}
|