sonamu 0.2.54 → 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-L4KELCY7.mjs → chunk-PP2PSSAG.mjs} +5241 -5571
- 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-JOHF7PK4.js → chunk-XT6LHCX5.js} +5292 -5622
- 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 +222 -928
- package/dist/index.d.ts +222 -928
- 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-manager.ts +9 -5
- package/src/entity/entity-utils.ts +1 -1
- package/src/entity/entity.ts +9 -13
- package/src/entity/migrator.ts +98 -693
- package/src/index.ts +1 -1
- package/src/syncer/syncer.ts +103 -56
- 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-JOHF7PK4.js.map +0 -1
- package/dist/chunk-L4KELCY7.mjs.map +0 -1
- /package/src/database/{knex-plugins → drivers/knex/plugins}/knex-on-duplicate-update.ts +0 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import knex, { Knex } from "knex";
|
|
3
2
|
import _ from "lodash";
|
|
4
3
|
import { Sonamu } from "../api";
|
|
5
|
-
import { BaseModel } from "../database/base-model";
|
|
6
4
|
import { EntityManager } from "../entity/entity-manager";
|
|
7
5
|
import {
|
|
8
6
|
EntityProp,
|
|
@@ -19,65 +17,18 @@ import {
|
|
|
19
17
|
} from "../types/types";
|
|
20
18
|
import { Entity } from "../entity/entity";
|
|
21
19
|
import inflection from "inflection";
|
|
22
|
-
import { SonamuDBConfig } from "../database/db";
|
|
23
20
|
import { readFileSync, writeFileSync } from "fs";
|
|
21
|
+
import { RelationGraph } from "./_relation-graph";
|
|
22
|
+
import { SonamuDBConfig, WhereClause } from "../database/types";
|
|
23
|
+
import { DB } from "../database/db";
|
|
24
|
+
import { KyselyClient } from "../database/drivers/kysely/client";
|
|
25
|
+
import { KnexClient } from "../database/drivers/knex/client";
|
|
24
26
|
|
|
25
27
|
export class FixtureManagerClass {
|
|
26
|
-
private
|
|
27
|
-
set tdb(tdb: Knex) {
|
|
28
|
-
this._tdb = tdb;
|
|
29
|
-
}
|
|
30
|
-
get tdb(): Knex {
|
|
31
|
-
if (this._tdb === null) {
|
|
32
|
-
throw new Error("FixtureManager has not been initialized");
|
|
33
|
-
}
|
|
34
|
-
return this._tdb;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private _fdb: Knex | null = null;
|
|
38
|
-
set fdb(fdb: Knex) {
|
|
39
|
-
this._fdb = fdb;
|
|
40
|
-
}
|
|
41
|
-
get fdb(): Knex {
|
|
42
|
-
if (this._fdb === null) {
|
|
43
|
-
throw new Error("FixtureManager has not been initialized");
|
|
44
|
-
}
|
|
45
|
-
return this._fdb;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private dependencyGraph: Map<
|
|
49
|
-
string,
|
|
50
|
-
{
|
|
51
|
-
fixtureId: string;
|
|
52
|
-
entityId: string;
|
|
53
|
-
dependencies: Set<string>;
|
|
54
|
-
}
|
|
55
|
-
> = new Map();
|
|
28
|
+
private relationGraph = new RelationGraph();
|
|
56
29
|
|
|
57
30
|
init() {
|
|
58
|
-
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (Sonamu.dbConfig.test && Sonamu.dbConfig.production_master) {
|
|
62
|
-
const tConn = Sonamu.dbConfig.test.connection as Knex.ConnectionConfig & {
|
|
63
|
-
port?: number;
|
|
64
|
-
};
|
|
65
|
-
const pConn = Sonamu.dbConfig.production_master
|
|
66
|
-
.connection as Knex.ConnectionConfig & { port?: number };
|
|
67
|
-
if (
|
|
68
|
-
`${tConn.host ?? "localhost"}:${tConn.port ?? 3306}/${
|
|
69
|
-
tConn.database
|
|
70
|
-
}` ===
|
|
71
|
-
`${pConn.host ?? "localhost"}:${pConn.port ?? 3306}/${pConn.database}`
|
|
72
|
-
) {
|
|
73
|
-
throw new Error(
|
|
74
|
-
`테스트DB와 프로덕션DB에 동일한 데이터베이스가 사용되었습니다.`
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.tdb = knex(Sonamu.dbConfig.test);
|
|
80
|
-
this.fdb = knex(Sonamu.dbConfig.fixture_local);
|
|
31
|
+
DB.testInit();
|
|
81
32
|
}
|
|
82
33
|
|
|
83
34
|
async cleanAndSeed(usingTables?: string[]) {
|
|
@@ -86,56 +37,51 @@ export class FixtureManagerClass {
|
|
|
86
37
|
return usingTables;
|
|
87
38
|
}
|
|
88
39
|
|
|
89
|
-
const
|
|
90
|
-
|
|
40
|
+
const tables = await DB.tdb.raw<{ Name: string }>(
|
|
41
|
+
`SHOW TABLE STATUS WHERE Engine IS NOT NULL`
|
|
91
42
|
);
|
|
92
43
|
return tables.map((tableInfo: any) => tableInfo["Name"] as string);
|
|
93
44
|
})();
|
|
94
45
|
|
|
95
|
-
await
|
|
46
|
+
await DB.tdb.raw(`SET FOREIGN_KEY_CHECKS = 0`);
|
|
96
47
|
for await (let tableName of tableNames) {
|
|
97
48
|
if (tableName == "migrations") {
|
|
98
49
|
continue;
|
|
99
50
|
}
|
|
100
51
|
|
|
101
|
-
const [
|
|
52
|
+
const [fdbChecksumRow] = await DB.fdb.raw<{ Checksum: string }>(
|
|
102
53
|
`CHECKSUM TABLE ${tableName}`
|
|
103
54
|
);
|
|
104
55
|
const fdbChecksum = fdbChecksumRow["Checksum"];
|
|
105
56
|
|
|
106
|
-
const [
|
|
57
|
+
const [tdbChecksumRow] = await DB.tdb.raw<{ Checksum: string }>(
|
|
107
58
|
`CHECKSUM TABLE ${tableName}`
|
|
108
59
|
);
|
|
109
60
|
const tdbChecksum = tdbChecksumRow["Checksum"];
|
|
110
61
|
|
|
111
62
|
if (fdbChecksum !== tdbChecksum) {
|
|
112
|
-
await
|
|
113
|
-
const rawQuery = `INSERT INTO ${
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
SELECT * FROM ${
|
|
117
|
-
(
|
|
118
|
-
Sonamu.dbConfig.fixture_local
|
|
119
|
-
.connection as Knex.ConnectionConfig
|
|
120
|
-
).database
|
|
121
|
-
}.${tableName}`;
|
|
122
|
-
await this.tdb.raw(rawQuery);
|
|
63
|
+
await DB.tdb.truncate(tableName);
|
|
64
|
+
const rawQuery = `INSERT INTO ${DB.connectionInfo.test.database}.${tableName}
|
|
65
|
+
SELECT * FROM ${DB.connectionInfo.fixture_local.database}.${tableName}`;
|
|
66
|
+
await DB.tdb.raw(rawQuery);
|
|
123
67
|
}
|
|
124
68
|
}
|
|
125
|
-
await
|
|
69
|
+
await DB.tdb.raw(`SET FOREIGN_KEY_CHECKS = 1`);
|
|
126
70
|
|
|
127
71
|
// console.timeEnd("FIXTURE-CleanAndSeed");
|
|
128
72
|
}
|
|
129
73
|
|
|
130
|
-
async getChecksum(db:
|
|
131
|
-
const [
|
|
74
|
+
async getChecksum(db: KnexClient | KyselyClient, tableName: string) {
|
|
75
|
+
const [checksumRow] = await db.raw<{ Checksum: string }>(
|
|
76
|
+
`CHECKSUM TABLE ${tableName}`
|
|
77
|
+
);
|
|
132
78
|
return checksumRow.Checksum;
|
|
133
79
|
}
|
|
134
80
|
|
|
135
81
|
async sync() {
|
|
136
|
-
const frdb =
|
|
82
|
+
const frdb = DB.getClient("fixture_remote");
|
|
137
83
|
|
|
138
|
-
const
|
|
84
|
+
const tables = await DB.fdb.raw<{ Name: string }>(
|
|
139
85
|
"SHOW TABLE STATUS WHERE Engine IS NOT NULL"
|
|
140
86
|
);
|
|
141
87
|
const tableNames: string[] = tables.map(
|
|
@@ -145,36 +91,30 @@ export class FixtureManagerClass {
|
|
|
145
91
|
console.log(chalk.magenta("SYNC..."));
|
|
146
92
|
await Promise.all(
|
|
147
93
|
tableNames.map(async (tableName) => {
|
|
148
|
-
if (tableName.startsWith(
|
|
94
|
+
if (tableName.startsWith(DB.migrationTable)) {
|
|
149
95
|
return;
|
|
150
96
|
}
|
|
151
97
|
|
|
152
98
|
const remoteChecksum = await this.getChecksum(frdb, tableName);
|
|
153
|
-
const localChecksum = await this.getChecksum(
|
|
99
|
+
const localChecksum = await this.getChecksum(DB.fdb, tableName);
|
|
154
100
|
|
|
155
101
|
if (remoteChecksum !== localChecksum) {
|
|
156
|
-
await
|
|
102
|
+
await DB.fdb.trx(async (transaction) => {
|
|
157
103
|
await transaction.raw(`SET FOREIGN_KEY_CHECKS = 0`);
|
|
158
|
-
await transaction
|
|
104
|
+
await transaction.truncate(tableName);
|
|
159
105
|
|
|
160
|
-
const rows = await frdb(tableName);
|
|
106
|
+
const rows = await frdb.raw(`SELECT * FROM ${tableName}`);
|
|
161
107
|
if (rows.length === 0) {
|
|
162
108
|
return;
|
|
163
109
|
}
|
|
164
110
|
|
|
165
111
|
console.log(chalk.blue(tableName), rows.length);
|
|
166
|
-
await transaction
|
|
167
|
-
.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
return row;
|
|
175
|
-
})
|
|
176
|
-
)
|
|
177
|
-
.into(tableName);
|
|
112
|
+
await transaction.raw(
|
|
113
|
+
`INSERT INTO ${tableName} (${Object.keys(rows[0] as any).join(
|
|
114
|
+
","
|
|
115
|
+
)}) VALUES ?`,
|
|
116
|
+
[rows.map((row: any) => Object.values(row))]
|
|
117
|
+
);
|
|
178
118
|
console.log("OK");
|
|
179
119
|
await transaction.raw(`SET FOREIGN_KEY_CHECKS = 1`);
|
|
180
120
|
});
|
|
@@ -197,9 +137,9 @@ export class FixtureManagerClass {
|
|
|
197
137
|
).flat()
|
|
198
138
|
);
|
|
199
139
|
|
|
200
|
-
const wdb =
|
|
140
|
+
const wdb = DB.toClient(DB.getDB("w"));
|
|
201
141
|
for (let query of queries) {
|
|
202
|
-
const [rsh] = await wdb.raw(query);
|
|
142
|
+
const [rsh] = await wdb.raw<{ info: any }>(query);
|
|
203
143
|
console.log({
|
|
204
144
|
query,
|
|
205
145
|
info: rsh.info,
|
|
@@ -214,19 +154,19 @@ export class FixtureManagerClass {
|
|
|
214
154
|
): Promise<string[]> {
|
|
215
155
|
console.log({ entityId, field, id });
|
|
216
156
|
const entity = EntityManager.get(entityId);
|
|
217
|
-
const wdb =
|
|
157
|
+
const wdb = DB.toClient(DB.getDB("w"));
|
|
218
158
|
|
|
219
159
|
// 여기서 실DB의 row 가져옴
|
|
220
|
-
const [row] = await wdb
|
|
160
|
+
const [row] = await wdb.raw<any>(
|
|
161
|
+
`SELECT * FROM ${entity.table} WHERE ${field} = ${id} LIMIT 1`
|
|
162
|
+
);
|
|
221
163
|
if (row === undefined) {
|
|
222
164
|
throw new Error(`${entityId}#${id} row를 찾을 수 없습니다.`);
|
|
223
165
|
}
|
|
224
166
|
|
|
225
167
|
// 픽스쳐DB, 실DB
|
|
226
|
-
const fixtureDatabase =
|
|
227
|
-
|
|
228
|
-
const realDatabase = (Sonamu.dbConfig.production_master.connection as any)
|
|
229
|
-
.database;
|
|
168
|
+
const fixtureDatabase = DB.connectionInfo.fixture_remote.database;
|
|
169
|
+
const realDatabase = DB.connectionInfo.production_master.database;
|
|
230
170
|
|
|
231
171
|
const selfQuery = `INSERT IGNORE INTO \`${fixtureDatabase}\`.\`${entity.table}\` (SELECT * FROM \`${realDatabase}\`.\`${entity.table}\` WHERE \`id\` = ${id})`;
|
|
232
172
|
|
|
@@ -273,15 +213,8 @@ export class FixtureManagerClass {
|
|
|
273
213
|
}
|
|
274
214
|
|
|
275
215
|
async destory() {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
this._tdb = null;
|
|
279
|
-
}
|
|
280
|
-
if (this._fdb) {
|
|
281
|
-
await this._fdb.destroy();
|
|
282
|
-
this._fdb = null;
|
|
283
|
-
}
|
|
284
|
-
await BaseModel.destroy();
|
|
216
|
+
await DB.testDestroy();
|
|
217
|
+
await DB.destroy();
|
|
285
218
|
}
|
|
286
219
|
|
|
287
220
|
async getFixtures(
|
|
@@ -289,8 +222,8 @@ export class FixtureManagerClass {
|
|
|
289
222
|
targetDBName: keyof SonamuDBConfig,
|
|
290
223
|
searchOptions: FixtureSearchOptions
|
|
291
224
|
) {
|
|
292
|
-
const sourceDB =
|
|
293
|
-
const targetDB =
|
|
225
|
+
const sourceDB = DB.getClient(sourceDBName);
|
|
226
|
+
const targetDB = DB.getClient(targetDBName);
|
|
294
227
|
const { entityId, field, value, searchType } = searchOptions;
|
|
295
228
|
|
|
296
229
|
const entity = EntityManager.get(entityId);
|
|
@@ -299,14 +232,14 @@ export class FixtureManagerClass {
|
|
|
299
232
|
? `${field}_id`
|
|
300
233
|
: field;
|
|
301
234
|
|
|
302
|
-
let query = sourceDB(entity.table);
|
|
235
|
+
let query = sourceDB.from(entity.table).selectAll();
|
|
303
236
|
if (searchType === "equals") {
|
|
304
|
-
query = query.where(column, value);
|
|
237
|
+
query = query.where([column, "=", value]);
|
|
305
238
|
} else if (searchType === "like") {
|
|
306
|
-
query = query.where(column, "like", `%${value}%`);
|
|
239
|
+
query = query.where([column, "like", `%${value}%`]);
|
|
307
240
|
}
|
|
308
241
|
|
|
309
|
-
const rows = await query;
|
|
242
|
+
const rows = await query.execute();
|
|
310
243
|
if (rows.length === 0) {
|
|
311
244
|
throw new Error("No records found");
|
|
312
245
|
}
|
|
@@ -332,8 +265,13 @@ export class FixtureManagerClass {
|
|
|
332
265
|
for await (const fixture of fixtures) {
|
|
333
266
|
const entity = EntityManager.get(fixture.entityId);
|
|
334
267
|
|
|
335
|
-
// targetDB에
|
|
336
|
-
const row = await targetDB
|
|
268
|
+
// ID를 이용하여 targetDB에 레코드가 존재하는지 확인
|
|
269
|
+
const [row] = await targetDB
|
|
270
|
+
.from(entity.table)
|
|
271
|
+
.selectAll()
|
|
272
|
+
.where(["id", "=", fixture.id])
|
|
273
|
+
.first()
|
|
274
|
+
.execute();
|
|
337
275
|
if (row) {
|
|
338
276
|
const [record] = await this.createFixtureRecord(entity, row, {
|
|
339
277
|
singleRecord: true,
|
|
@@ -343,7 +281,7 @@ export class FixtureManagerClass {
|
|
|
343
281
|
continue;
|
|
344
282
|
}
|
|
345
283
|
|
|
346
|
-
//
|
|
284
|
+
// ID를 이용하여 targetDB에서 조회되지 않는 경우, unique 제약을 위반하는지 확인
|
|
347
285
|
const uniqueRow = await this.checkUniqueViolation(
|
|
348
286
|
targetDB,
|
|
349
287
|
entity,
|
|
@@ -358,7 +296,7 @@ export class FixtureManagerClass {
|
|
|
358
296
|
}
|
|
359
297
|
}
|
|
360
298
|
|
|
361
|
-
return fixtures;
|
|
299
|
+
return _.uniqBy(fixtures, (f) => f.fixtureId);
|
|
362
300
|
}
|
|
363
301
|
|
|
364
302
|
async createFixtureRecord(
|
|
@@ -366,89 +304,102 @@ export class FixtureManagerClass {
|
|
|
366
304
|
row: any,
|
|
367
305
|
options?: {
|
|
368
306
|
singleRecord?: boolean;
|
|
369
|
-
_db?:
|
|
370
|
-
},
|
|
371
|
-
visitedEntities = new Set<string>()
|
|
372
|
-
): Promise<FixtureRecord[]> {
|
|
373
|
-
const fixtureId = `${entity.id}#${row.id}`;
|
|
374
|
-
if (visitedEntities.has(fixtureId)) {
|
|
375
|
-
return [];
|
|
307
|
+
_db?: KnexClient | KyselyClient;
|
|
376
308
|
}
|
|
377
|
-
|
|
378
|
-
|
|
309
|
+
): Promise<FixtureRecord[]> {
|
|
379
310
|
const records: FixtureRecord[] = [];
|
|
380
|
-
const
|
|
381
|
-
fixtureId,
|
|
382
|
-
entityId: entity.id,
|
|
383
|
-
id: row.id,
|
|
384
|
-
columns: {},
|
|
385
|
-
fetchedRecords: [],
|
|
386
|
-
belongsRecords: [],
|
|
387
|
-
};
|
|
311
|
+
const visitedEntities = new Set<string>();
|
|
388
312
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
313
|
+
const create = async (entity: Entity, row: any) => {
|
|
314
|
+
const fixtureId = `${entity.id}#${row.id}`;
|
|
315
|
+
if (visitedEntities.has(fixtureId)) {
|
|
316
|
+
return;
|
|
392
317
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
318
|
+
visitedEntities.add(fixtureId);
|
|
319
|
+
|
|
320
|
+
const record: FixtureRecord = {
|
|
321
|
+
fixtureId,
|
|
322
|
+
entityId: entity.id,
|
|
323
|
+
id: row.id,
|
|
324
|
+
columns: {},
|
|
325
|
+
fetchedRecords: [],
|
|
326
|
+
belongsRecords: [],
|
|
397
327
|
};
|
|
398
328
|
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const throughTable = prop.joinTable;
|
|
403
|
-
const fromColumn = `${inflection.singularize(entity.table)}_id`;
|
|
404
|
-
const toColumn = `${inflection.singularize(relatedEntity.table)}_id`;
|
|
405
|
-
|
|
406
|
-
const relatedIds = await db(throughTable)
|
|
407
|
-
.where(fromColumn, row.id)
|
|
408
|
-
.pluck(toColumn);
|
|
409
|
-
record.columns[prop.name].value = relatedIds;
|
|
410
|
-
} else if (isHasManyRelationProp(prop)) {
|
|
411
|
-
const relatedEntity = EntityManager.get(prop.with);
|
|
412
|
-
const relatedIds = await db(relatedEntity.table)
|
|
413
|
-
.where(prop.joinColumn, row.id)
|
|
414
|
-
.pluck("id");
|
|
415
|
-
record.columns[prop.name].value = relatedIds;
|
|
416
|
-
} else if (isOneToOneRelationProp(prop) && !prop.hasJoinColumn) {
|
|
417
|
-
const relatedEntity = EntityManager.get(prop.with);
|
|
418
|
-
const relatedProp = relatedEntity.props.find(
|
|
419
|
-
(p) => p.type === "relation" && p.with === entity.id
|
|
420
|
-
);
|
|
421
|
-
if (relatedProp) {
|
|
422
|
-
const relatedRow = await db(relatedEntity.table)
|
|
423
|
-
.where("id", row.id)
|
|
424
|
-
.first();
|
|
425
|
-
record.columns[prop.name].value = relatedRow?.id;
|
|
329
|
+
for (const prop of entity.props) {
|
|
330
|
+
if (isVirtualProp(prop)) {
|
|
331
|
+
continue;
|
|
426
332
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
333
|
+
|
|
334
|
+
record.columns[prop.name] = {
|
|
335
|
+
prop: prop,
|
|
336
|
+
value: row[prop.name],
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const db = options?._db ?? DB.toClient(DB.getDB("w"));
|
|
340
|
+
if (isManyToManyRelationProp(prop)) {
|
|
434
341
|
const relatedEntity = EntityManager.get(prop.with);
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
342
|
+
const throughTable = prop.joinTable;
|
|
343
|
+
const fromColumn = `${inflection.singularize(entity.table)}_id`;
|
|
344
|
+
const toColumn = `${inflection.singularize(relatedEntity.table)}_id`;
|
|
345
|
+
|
|
346
|
+
const _relatedIds = await db
|
|
347
|
+
.from(throughTable)
|
|
348
|
+
.select(toColumn)
|
|
349
|
+
.where([fromColumn, "=", row.id])
|
|
350
|
+
.execute();
|
|
351
|
+
const relatedIds = _relatedIds.map((r) => parseInt(r[toColumn]));
|
|
352
|
+
|
|
353
|
+
record.columns[prop.name].value = relatedIds;
|
|
354
|
+
} else if (isHasManyRelationProp(prop)) {
|
|
355
|
+
const relatedEntity = EntityManager.get(prop.with);
|
|
356
|
+
const relatedIds = await db
|
|
357
|
+
.from(relatedEntity.table)
|
|
358
|
+
.select("id")
|
|
359
|
+
.where([prop.joinColumn, "=", row.id])
|
|
360
|
+
.pluck("id");
|
|
361
|
+
record.columns[prop.name].value = relatedIds;
|
|
362
|
+
} else if (isOneToOneRelationProp(prop) && !prop.hasJoinColumn) {
|
|
363
|
+
const relatedEntity = EntityManager.get(prop.with);
|
|
364
|
+
const relatedProp = relatedEntity.props.find(
|
|
365
|
+
(p) => isRelationProp(p) && p.with === entity.id
|
|
366
|
+
);
|
|
367
|
+
if (relatedProp) {
|
|
368
|
+
const [relatedRow] = await db
|
|
369
|
+
.from(relatedEntity.table)
|
|
370
|
+
.select("id")
|
|
371
|
+
.where([relatedProp.name, "=", row.id])
|
|
372
|
+
.first()
|
|
373
|
+
.execute();
|
|
374
|
+
|
|
375
|
+
record.columns[prop.name].value = relatedRow?.id;
|
|
376
|
+
}
|
|
377
|
+
} else if (isRelationProp(prop)) {
|
|
378
|
+
const relatedId = row[`${prop.name}_id`];
|
|
379
|
+
record.columns[prop.name].value = relatedId;
|
|
380
|
+
if (relatedId) {
|
|
381
|
+
record.belongsRecords.push(`${prop.with}#${relatedId}`);
|
|
382
|
+
}
|
|
383
|
+
if (!options?.singleRecord && relatedId) {
|
|
384
|
+
const relatedEntity = EntityManager.get(prop.with);
|
|
385
|
+
const [relatedRow] = await db
|
|
386
|
+
.from(relatedEntity.table)
|
|
387
|
+
.selectAll()
|
|
388
|
+
.where(["id", "=", relatedId])
|
|
389
|
+
.first()
|
|
390
|
+
.execute();
|
|
391
|
+
if (relatedRow) {
|
|
392
|
+
await create(relatedEntity, relatedRow);
|
|
393
|
+
}
|
|
446
394
|
}
|
|
447
395
|
}
|
|
448
396
|
}
|
|
449
|
-
}
|
|
450
397
|
|
|
451
|
-
|
|
398
|
+
records.push(record);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
await create(entity, row);
|
|
402
|
+
|
|
452
403
|
return records;
|
|
453
404
|
}
|
|
454
405
|
|
|
@@ -458,16 +409,16 @@ export class FixtureManagerClass {
|
|
|
458
409
|
) {
|
|
459
410
|
const fixtures = _.uniqBy(_fixtures, (f) => f.fixtureId);
|
|
460
411
|
|
|
461
|
-
this.
|
|
462
|
-
const insertionOrder = this.getInsertionOrder();
|
|
463
|
-
const db =
|
|
412
|
+
this.relationGraph.buildGraph(fixtures);
|
|
413
|
+
const insertionOrder = this.relationGraph.getInsertionOrder();
|
|
414
|
+
const db = DB.getClient(dbName);
|
|
464
415
|
|
|
465
|
-
await db.
|
|
416
|
+
await db.trx(async (trx) => {
|
|
466
417
|
await trx.raw(`SET FOREIGN_KEY_CHECKS = 0`);
|
|
467
418
|
|
|
468
419
|
for (const fixtureId of insertionOrder) {
|
|
469
420
|
const fixture = fixtures.find((f) => f.fixtureId === fixtureId)!;
|
|
470
|
-
const result = await this.insertFixture(trx, fixture);
|
|
421
|
+
const result = await this.insertFixture(trx as any, fixture);
|
|
471
422
|
if (result.id !== fixture.id) {
|
|
472
423
|
// ID가 변경된 경우, 다른 fixture에서 참조하는 경우가 찾아서 수정
|
|
473
424
|
console.log(
|
|
@@ -492,7 +443,7 @@ export class FixtureManagerClass {
|
|
|
492
443
|
|
|
493
444
|
for (const fixtureId of insertionOrder) {
|
|
494
445
|
const fixture = fixtures.find((f) => f.fixtureId === fixtureId)!;
|
|
495
|
-
await this.handleManyToManyRelations(trx, fixture, fixtures);
|
|
446
|
+
await this.handleManyToManyRelations(trx as any, fixture, fixtures);
|
|
496
447
|
}
|
|
497
448
|
await trx.raw(`SET FOREIGN_KEY_CHECKS = 1`);
|
|
498
449
|
});
|
|
@@ -501,7 +452,12 @@ export class FixtureManagerClass {
|
|
|
501
452
|
|
|
502
453
|
for await (const r of fixtures) {
|
|
503
454
|
const entity = EntityManager.get(r.entityId);
|
|
504
|
-
const record = await db
|
|
455
|
+
const [record] = await db
|
|
456
|
+
.from(entity.table)
|
|
457
|
+
.selectAll()
|
|
458
|
+
.where(["id", "=", r.id])
|
|
459
|
+
.first()
|
|
460
|
+
.execute();
|
|
505
461
|
records.push({
|
|
506
462
|
entityId: r.entityId,
|
|
507
463
|
data: record,
|
|
@@ -511,58 +467,6 @@ export class FixtureManagerClass {
|
|
|
511
467
|
return _.uniqBy(records, (r) => `${r.entityId}#${r.data.id}`);
|
|
512
468
|
}
|
|
513
469
|
|
|
514
|
-
private getInsertionOrder() {
|
|
515
|
-
const visited = new Set<string>();
|
|
516
|
-
const order: string[] = [];
|
|
517
|
-
const tempVisited = new Set<string>();
|
|
518
|
-
|
|
519
|
-
const visit = (fixtureId: string) => {
|
|
520
|
-
if (visited.has(fixtureId)) return;
|
|
521
|
-
if (tempVisited.has(fixtureId)) {
|
|
522
|
-
console.warn(`Circular dependency detected involving: ${fixtureId}`);
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
tempVisited.add(fixtureId);
|
|
527
|
-
|
|
528
|
-
const node = this.dependencyGraph.get(fixtureId)!;
|
|
529
|
-
const entity = EntityManager.get(node.entityId);
|
|
530
|
-
|
|
531
|
-
for (const depId of node.dependencies) {
|
|
532
|
-
const depNode = this.dependencyGraph.get(depId)!;
|
|
533
|
-
|
|
534
|
-
// BelongsToOne 관계이면서 nullable이 아닌 경우 먼저 방문
|
|
535
|
-
const relationProp = entity.props.find(
|
|
536
|
-
(prop) =>
|
|
537
|
-
isRelationProp(prop) &&
|
|
538
|
-
(isBelongsToOneRelationProp(prop) ||
|
|
539
|
-
(isOneToOneRelationProp(prop) && prop.hasJoinColumn)) &&
|
|
540
|
-
prop.with === depNode.entityId
|
|
541
|
-
);
|
|
542
|
-
if (relationProp && !relationProp.nullable) {
|
|
543
|
-
visit(depId);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
tempVisited.delete(fixtureId);
|
|
548
|
-
visited.add(fixtureId);
|
|
549
|
-
order.push(fixtureId);
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
for (const fixtureId of this.dependencyGraph.keys()) {
|
|
553
|
-
visit(fixtureId);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// circular dependency로 인해 방문되지 않은 fixtureId 추가
|
|
557
|
-
for (const fixtureId of this.dependencyGraph.keys()) {
|
|
558
|
-
if (!visited.has(fixtureId)) {
|
|
559
|
-
order.push(fixtureId);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return order;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
470
|
private prepareInsertData(fixture: FixtureRecord) {
|
|
567
471
|
const insertData: any = {};
|
|
568
472
|
for (const [propName, column] of Object.entries(fixture.columns)) {
|
|
@@ -587,53 +491,10 @@ export class FixtureManagerClass {
|
|
|
587
491
|
return insertData;
|
|
588
492
|
}
|
|
589
493
|
|
|
590
|
-
private
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
for (const fixture of fixtures) {
|
|
595
|
-
this.dependencyGraph.set(fixture.fixtureId, {
|
|
596
|
-
fixtureId: fixture.fixtureId,
|
|
597
|
-
entityId: fixture.entityId,
|
|
598
|
-
dependencies: new Set(),
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// 2. 의존성 추가
|
|
603
|
-
for (const fixture of fixtures) {
|
|
604
|
-
const node = this.dependencyGraph.get(fixture.fixtureId)!;
|
|
605
|
-
|
|
606
|
-
for (const [, column] of Object.entries(fixture.columns)) {
|
|
607
|
-
const prop = column.prop as EntityProp;
|
|
608
|
-
|
|
609
|
-
if (isRelationProp(prop)) {
|
|
610
|
-
if (
|
|
611
|
-
isBelongsToOneRelationProp(prop) ||
|
|
612
|
-
(isOneToOneRelationProp(prop) && prop.hasJoinColumn)
|
|
613
|
-
) {
|
|
614
|
-
const relatedFixtureId = `${prop.with}#${column.value}`;
|
|
615
|
-
if (this.dependencyGraph.has(relatedFixtureId)) {
|
|
616
|
-
node.dependencies.add(relatedFixtureId);
|
|
617
|
-
}
|
|
618
|
-
} else if (isManyToManyRelationProp(prop)) {
|
|
619
|
-
// ManyToMany 관계의 경우 양방향 의존성 추가
|
|
620
|
-
const relatedIds = column.value as number[];
|
|
621
|
-
for (const relatedId of relatedIds) {
|
|
622
|
-
const relatedFixtureId = `${prop.with}#${relatedId}`;
|
|
623
|
-
if (this.dependencyGraph.has(relatedFixtureId)) {
|
|
624
|
-
node.dependencies.add(relatedFixtureId);
|
|
625
|
-
this.dependencyGraph
|
|
626
|
-
.get(relatedFixtureId)!
|
|
627
|
-
.dependencies.add(fixture.fixtureId);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
private async insertFixture(db: Knex, fixture: FixtureRecord) {
|
|
494
|
+
private async insertFixture(
|
|
495
|
+
db: KnexClient | KyselyClient,
|
|
496
|
+
fixture: FixtureRecord
|
|
497
|
+
) {
|
|
637
498
|
const insertData = this.prepareInsertData(fixture);
|
|
638
499
|
const entity = EntityManager.get(fixture.entityId);
|
|
639
500
|
|
|
@@ -646,7 +507,12 @@ export class FixtureManagerClass {
|
|
|
646
507
|
};
|
|
647
508
|
}
|
|
648
509
|
|
|
649
|
-
const found = await db
|
|
510
|
+
const [found] = await db
|
|
511
|
+
.from(entity.table)
|
|
512
|
+
.select("id")
|
|
513
|
+
.where(["id", "=", fixture.id])
|
|
514
|
+
.first()
|
|
515
|
+
.execute();
|
|
650
516
|
if (found && !fixture.override) {
|
|
651
517
|
return {
|
|
652
518
|
entityId: fixture.entityId,
|
|
@@ -654,8 +520,8 @@ export class FixtureManagerClass {
|
|
|
654
520
|
};
|
|
655
521
|
}
|
|
656
522
|
|
|
657
|
-
|
|
658
|
-
|
|
523
|
+
await db.upsert(entity.table, [insertData]);
|
|
524
|
+
|
|
659
525
|
return {
|
|
660
526
|
entityId: fixture.entityId,
|
|
661
527
|
id: fixture.id,
|
|
@@ -667,7 +533,7 @@ export class FixtureManagerClass {
|
|
|
667
533
|
}
|
|
668
534
|
|
|
669
535
|
private async handleManyToManyRelations(
|
|
670
|
-
db:
|
|
536
|
+
db: KnexClient | KyselyClient,
|
|
671
537
|
fixture: FixtureRecord,
|
|
672
538
|
fixtures: FixtureRecord[]
|
|
673
539
|
) {
|
|
@@ -692,20 +558,29 @@ export class FixtureManagerClass {
|
|
|
692
558
|
);
|
|
693
559
|
}
|
|
694
560
|
|
|
695
|
-
const [found] = await db
|
|
696
|
-
.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
561
|
+
const [found] = await db
|
|
562
|
+
.from(joinTable)
|
|
563
|
+
.select("id")
|
|
564
|
+
.where([
|
|
565
|
+
[`${inflection.singularize(entity.table)}_id`, "=", fixture.id],
|
|
566
|
+
[
|
|
567
|
+
`${inflection.singularize(relatedEntity.table)}_id`,
|
|
568
|
+
"=",
|
|
569
|
+
relatedId,
|
|
570
|
+
],
|
|
571
|
+
])
|
|
572
|
+
.first()
|
|
573
|
+
.execute();
|
|
701
574
|
if (found) {
|
|
702
575
|
continue;
|
|
703
576
|
}
|
|
704
577
|
|
|
705
|
-
const newIds = await db
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
578
|
+
const newIds = await db.insert(joinTable, [
|
|
579
|
+
{
|
|
580
|
+
[`${inflection.singularize(entity.table)}_id`]: fixture.id,
|
|
581
|
+
[`${inflection.singularize(relatedEntity.table)}_id`]: relatedId,
|
|
582
|
+
},
|
|
583
|
+
]);
|
|
709
584
|
console.log(
|
|
710
585
|
chalk.green(
|
|
711
586
|
`Inserted into ${joinTable}: ${entity.table}(${fixture.id}) - ${relatedEntity.table}(${relatedId}) ID: ${newIds}`
|
|
@@ -739,39 +614,48 @@ export class FixtureManagerClass {
|
|
|
739
614
|
|
|
740
615
|
// 해당 픽스쳐의 값으로 유니크 제약에 위배되는 레코드가 있는지 확인
|
|
741
616
|
private async checkUniqueViolation(
|
|
742
|
-
db:
|
|
617
|
+
db: KnexClient | KyselyClient,
|
|
743
618
|
entity: Entity,
|
|
744
619
|
fixture: FixtureRecord
|
|
745
620
|
) {
|
|
746
|
-
const
|
|
621
|
+
const _uniqueIndexes = entity.indexes.filter((i) => i.type === "unique");
|
|
622
|
+
|
|
623
|
+
// ManyToMany 관계 테이블의 유니크 제약은 제외
|
|
624
|
+
const uniqueIndexes = _uniqueIndexes.filter((index) =>
|
|
625
|
+
index.columns.every((column) => !column.startsWith(`${entity.table}__`))
|
|
626
|
+
);
|
|
747
627
|
if (uniqueIndexes.length === 0) {
|
|
748
628
|
return null;
|
|
749
629
|
}
|
|
750
630
|
|
|
751
|
-
let uniqueQuery = db(entity.table);
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
index.columns.some(
|
|
756
|
-
(column) => fixture.columns[column.split("_id")[0]].value === null
|
|
757
|
-
)
|
|
758
|
-
) {
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
uniqueQuery = uniqueQuery.orWhere((qb) => {
|
|
763
|
-
for (const column of index.columns) {
|
|
631
|
+
let uniqueQuery = db.from(entity.table).selectAll();
|
|
632
|
+
const whereClauses = uniqueIndexes
|
|
633
|
+
.map((index) => {
|
|
634
|
+
// 컬럼 중 하나라도 null이면 유니크 제약을 위반하지 않기 때문에 해당 인덱스는 무시
|
|
635
|
+
const containsNull = index.columns.some((column) => {
|
|
764
636
|
const field = column.split("_id")[0];
|
|
637
|
+
return fixture.columns[field].value === null;
|
|
638
|
+
});
|
|
639
|
+
if (containsNull) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
765
642
|
|
|
643
|
+
return index.columns.map((c) => {
|
|
644
|
+
const field = c.split("_id")[0];
|
|
766
645
|
if (Array.isArray(fixture.columns[field].value)) {
|
|
767
|
-
|
|
646
|
+
return [c, "in", fixture.columns[field].value];
|
|
768
647
|
} else {
|
|
769
|
-
|
|
648
|
+
return [c, "=", fixture.columns[field].value];
|
|
770
649
|
}
|
|
771
|
-
}
|
|
772
|
-
})
|
|
650
|
+
});
|
|
651
|
+
})
|
|
652
|
+
.filter(Boolean) as WhereClause[];
|
|
653
|
+
|
|
654
|
+
for (const clauses of whereClauses) {
|
|
655
|
+
uniqueQuery = uniqueQuery.orWhere(clauses);
|
|
773
656
|
}
|
|
774
|
-
|
|
657
|
+
|
|
658
|
+
const [uniqueFound] = await uniqueQuery.execute();
|
|
775
659
|
return uniqueFound;
|
|
776
660
|
}
|
|
777
661
|
}
|