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
@@ -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 _tdb: Knex | null = null;
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
- if (this._tdb !== null) {
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 [tables] = await this.tdb.raw(
90
- "SHOW TABLE STATUS WHERE Engine IS NOT NULL"
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 this.tdb.raw(`SET FOREIGN_KEY_CHECKS = 0`);
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 [[fdbChecksumRow]] = await this.fdb.raw(
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 [[tdbChecksumRow]] = await this.tdb.raw(
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 this.tdb(tableName).truncate();
113
- const rawQuery = `INSERT INTO ${
114
- (Sonamu.dbConfig.test.connection as Knex.ConnectionConfig).database
115
- }.${tableName}
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 this.tdb.raw(`SET FOREIGN_KEY_CHECKS = 1`);
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: Knex, tableName: string) {
131
- const [[checksumRow]] = await db.raw(`CHECKSUM TABLE ${tableName}`);
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 = knex(Sonamu.dbConfig.fixture_remote);
82
+ const frdb = DB.getClient("fixture_remote");
137
83
 
138
- const [tables] = await this.fdb.raw(
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("knex_migrations")) {
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(this.fdb, tableName);
99
+ const localChecksum = await this.getChecksum(DB.fdb, tableName);
154
100
 
155
101
  if (remoteChecksum !== localChecksum) {
156
- await this.fdb.transaction(async (transaction) => {
102
+ await DB.fdb.trx(async (transaction) => {
157
103
  await transaction.raw(`SET FOREIGN_KEY_CHECKS = 0`);
158
- await transaction(tableName).truncate();
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
- .insert(
168
- rows.map((row) => {
169
- Object.keys(row).map((key) => {
170
- if (Array.isArray(row[key])) {
171
- row[key] = JSON.stringify(row[key]);
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 = BaseModel.getDB("w");
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 = BaseModel.getDB("w");
157
+ const wdb = DB.toClient(DB.getDB("w"));
218
158
 
219
159
  // 여기서 실DB의 row 가져옴
220
- const [row] = await wdb(entity.table).where(field, id).limit(1);
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 = (Sonamu.dbConfig.fixture_remote.connection as any)
227
- .database;
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
- if (this._tdb) {
277
- await this._tdb.destroy();
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 = knex(Sonamu.dbConfig[sourceDBName]);
293
- const targetDB = knex(Sonamu.dbConfig[targetDBName]);
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(entity.table).where("id", fixture.id).first();
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
- // targetDB에 해당 레코드가 존재하지 않는 경우, unique 제약을 위반하는지 확인
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?: Knex;
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
- visitedEntities.add(fixtureId);
378
-
309
+ ): Promise<FixtureRecord[]> {
379
310
  const records: FixtureRecord[] = [];
380
- const record: FixtureRecord = {
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
- for (const prop of entity.props) {
390
- if (isVirtualProp(prop)) {
391
- continue;
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
- record.columns[prop.name] = {
395
- prop: prop,
396
- value: row[prop.name],
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 db = options?._db ?? BaseModel.getDB("w");
400
- if (isManyToManyRelationProp(prop)) {
401
- const relatedEntity = EntityManager.get(prop.with);
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
- } else if (isRelationProp(prop)) {
428
- const relatedId = row[`${prop.name}_id`];
429
- record.columns[prop.name].value = relatedId;
430
- if (relatedId) {
431
- record.belongsRecords.push(`${prop.with}#${relatedId}`);
432
- }
433
- if (!options?.singleRecord && relatedId) {
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 relatedRow = await db(relatedEntity.table)
436
- .where("id", relatedId)
437
- .first();
438
- if (relatedRow) {
439
- const newRecords = await this.createFixtureRecord(
440
- relatedEntity,
441
- relatedRow,
442
- options,
443
- visitedEntities
444
- );
445
- records.push(...newRecords);
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
- records.push(record);
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.buildDependencyGraph(fixtures);
462
- const insertionOrder = this.getInsertionOrder();
463
- const db = knex(Sonamu.dbConfig[dbName]);
412
+ this.relationGraph.buildGraph(fixtures);
413
+ const insertionOrder = this.relationGraph.getInsertionOrder();
414
+ const db = DB.getClient(dbName);
464
415
 
465
- await db.transaction(async (trx) => {
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(entity.table).where("id", r.id).first();
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 buildDependencyGraph(fixtures: FixtureRecord[]) {
591
- this.dependencyGraph.clear();
592
-
593
- // 1. 노드 추가
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(entity.table).where("id", fixture.id).first();
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
- const q = db.insert(insertData).into(entity.table);
658
- await q.onDuplicateUpdate.apply(q, Object.keys(insertData));
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: Knex,
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(joinTable)
696
- .where({
697
- [`${inflection.singularize(entity.table)}_id`]: fixture.id,
698
- [`${inflection.singularize(relatedEntity.table)}_id`]: relatedId,
699
- })
700
- .limit(1);
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(joinTable).insert({
706
- [`${inflection.singularize(entity.table)}_id`]: fixture.id,
707
- [`${inflection.singularize(relatedEntity.table)}_id`]: relatedId,
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: Knex,
617
+ db: KnexClient | KyselyClient,
743
618
  entity: Entity,
744
619
  fixture: FixtureRecord
745
620
  ) {
746
- const uniqueIndexes = entity.indexes.filter((i) => i.type === "unique");
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
- for (const index of uniqueIndexes) {
753
- // 컬럼 중 하나라도 null이면 유니크 제약을 위반하지 않기 때문에 해당 인덱스는 무시
754
- if (
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
- qb.whereIn(column, fixture.columns[field].value);
646
+ return [c, "in", fixture.columns[field].value];
768
647
  } else {
769
- qb.andWhere(column, fixture.columns[field].value);
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
- const [uniqueFound] = await uniqueQuery;
657
+
658
+ const [uniqueFound] = await uniqueQuery.execute();
775
659
  return uniqueFound;
776
660
  }
777
661
  }