sonamu 0.2.54 → 0.2.55

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 (48) hide show
  1. package/dist/base-model-BzMJ2E_I.d.mts +43 -0
  2. package/dist/base-model-CWRKUX49.d.ts +43 -0
  3. package/dist/bin/cli.js +118 -89
  4. package/dist/bin/cli.js.map +1 -1
  5. package/dist/bin/cli.mjs +74 -45
  6. package/dist/bin/cli.mjs.map +1 -1
  7. package/dist/chunk-4K2F3SOM.mjs +231 -0
  8. package/dist/chunk-4K2F3SOM.mjs.map +1 -0
  9. package/dist/chunk-6SP5N5ND.mjs +1579 -0
  10. package/dist/chunk-6SP5N5ND.mjs.map +1 -0
  11. package/dist/chunk-EUP6N7EK.js +1579 -0
  12. package/dist/chunk-EUP6N7EK.js.map +1 -0
  13. package/dist/chunk-HVVCQLAU.mjs +280 -0
  14. package/dist/chunk-HVVCQLAU.mjs.map +1 -0
  15. package/dist/chunk-N6N3LENC.js +231 -0
  16. package/dist/chunk-N6N3LENC.js.map +1 -0
  17. package/dist/chunk-UAG3SKFM.js +280 -0
  18. package/dist/chunk-UAG3SKFM.js.map +1 -0
  19. package/dist/{chunk-JOHF7PK4.js → chunk-WJGRXAXE.js} +5301 -5623
  20. package/dist/chunk-WJGRXAXE.js.map +1 -0
  21. package/dist/{chunk-L4KELCY7.mjs → chunk-ZFLQLW37.mjs} +5252 -5574
  22. package/dist/chunk-ZFLQLW37.mjs.map +1 -0
  23. package/dist/database/drivers/knex/base-model.d.mts +16 -0
  24. package/dist/database/drivers/knex/base-model.d.ts +16 -0
  25. package/dist/database/drivers/knex/base-model.js +55 -0
  26. package/dist/database/drivers/knex/base-model.js.map +1 -0
  27. package/dist/database/drivers/knex/base-model.mjs +56 -0
  28. package/dist/database/drivers/knex/base-model.mjs.map +1 -0
  29. package/dist/database/drivers/kysely/base-model.d.mts +22 -0
  30. package/dist/database/drivers/kysely/base-model.d.ts +22 -0
  31. package/dist/database/drivers/kysely/base-model.js +64 -0
  32. package/dist/database/drivers/kysely/base-model.js.map +1 -0
  33. package/dist/database/drivers/kysely/base-model.mjs +65 -0
  34. package/dist/database/drivers/kysely/base-model.mjs.map +1 -0
  35. package/dist/index.d.mts +226 -931
  36. package/dist/index.d.ts +226 -931
  37. package/dist/index.js +13 -26
  38. package/dist/index.js.map +1 -1
  39. package/dist/index.mjs +18 -31
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/model-CAH_4oQh.d.mts +1042 -0
  42. package/dist/model-CAH_4oQh.d.ts +1042 -0
  43. package/package.json +1 -1
  44. package/src/api/code-converters.ts +1 -1
  45. package/src/entity/migrator.ts +3 -0
  46. package/src/types/types.ts +1 -0
  47. package/dist/chunk-JOHF7PK4.js.map +0 -1
  48. package/dist/chunk-L4KELCY7.mjs.map +0 -1
@@ -0,0 +1,1579 @@
1
+ import {
2
+ DB,
3
+ EntityManager,
4
+ ServiceUnavailableException,
5
+ Sonamu,
6
+ isBelongsToOneRelationProp,
7
+ isDecimalProp,
8
+ isEnumProp,
9
+ isFloatProp,
10
+ isHasManyRelationProp,
11
+ isIntegerProp,
12
+ isKnexError,
13
+ isManyToManyRelationProp,
14
+ isOneToOneRelationProp,
15
+ isRelationProp,
16
+ isStringProp,
17
+ isTextProp,
18
+ isVirtualProp
19
+ } from "./chunk-ZFLQLW37.mjs";
20
+
21
+ // src/entity/migrator.ts
22
+ import _ from "lodash";
23
+ import chalk from "chalk";
24
+ import { DateTime } from "luxon";
25
+ import fs from "fs-extra";
26
+ import equal from "fast-deep-equal";
27
+ import inflection from "inflection";
28
+ import prompts from "prompts";
29
+ import { execSync } from "child_process";
30
+ import path from "path";
31
+ var Migrator = class {
32
+ mode;
33
+ targets;
34
+ constructor(options) {
35
+ this.mode = options.mode;
36
+ if (this.mode === "dev") {
37
+ const devDB = DB.getClient("development_master");
38
+ const testDB = DB.getClient("test");
39
+ const fixtureLocalDB = DB.getClient("fixture_local");
40
+ const uniqConfigs = DB.getUniqueConfigs([
41
+ "development_master",
42
+ "test",
43
+ "fixture_local",
44
+ "fixture_remote"
45
+ ]);
46
+ const applyDBs = [devDB, testDB, fixtureLocalDB];
47
+ if (uniqConfigs.length === 4) {
48
+ const fixtureRemoteDB = DB.getClient("fixture_remote");
49
+ applyDBs.push(fixtureRemoteDB);
50
+ }
51
+ this.targets = {
52
+ compare: devDB,
53
+ pending: devDB,
54
+ shadow: testDB,
55
+ apply: applyDBs
56
+ };
57
+ } else if (this.mode === "deploy") {
58
+ const productionDB = DB.getClient("production_master");
59
+ const testDB = DB.getClient("test");
60
+ this.targets = {
61
+ pending: productionDB,
62
+ shadow: testDB,
63
+ apply: [productionDB]
64
+ };
65
+ } else {
66
+ throw new Error(`\uC798\uBABB\uB41C \uBAA8\uB4DC ${this.mode} \uC785\uB825`);
67
+ }
68
+ }
69
+ async getMigrationCodes() {
70
+ const srcMigrationsDir = `${Sonamu.apiRootPath}/src/migrations`;
71
+ const distMigrationsDir = `${Sonamu.apiRootPath}/dist/migrations`;
72
+ if (fs.existsSync(srcMigrationsDir) === false) {
73
+ fs.mkdirSync(srcMigrationsDir, {
74
+ recursive: true
75
+ });
76
+ }
77
+ if (fs.existsSync(distMigrationsDir) === false) {
78
+ fs.mkdirSync(distMigrationsDir, {
79
+ recursive: true
80
+ });
81
+ }
82
+ const srcMigrations = fs.readdirSync(srcMigrationsDir).filter((f) => f.endsWith(".ts")).map((f) => f.split(".")[0]);
83
+ const distMigrations = fs.readdirSync(distMigrationsDir).filter((f) => f.endsWith(".js")).map((f) => f.split(".")[0]);
84
+ const normal = _.intersection(srcMigrations, distMigrations).map((filename) => {
85
+ return {
86
+ name: filename,
87
+ path: path.join(srcMigrationsDir, filename) + ".ts"
88
+ };
89
+ }).sort((a, b) => a > b ? 1 : -1);
90
+ const onlyTs = _.difference(srcMigrations, distMigrations).map(
91
+ (filename) => {
92
+ return {
93
+ name: filename,
94
+ path: path.join(srcMigrationsDir, filename) + ".ts"
95
+ };
96
+ }
97
+ );
98
+ const onlyJs = _.difference(distMigrations, srcMigrations).map(
99
+ (filename) => {
100
+ return {
101
+ name: filename,
102
+ path: path.join(distMigrationsDir, filename) + ".js"
103
+ };
104
+ }
105
+ );
106
+ return {
107
+ normal,
108
+ onlyTs,
109
+ onlyJs
110
+ };
111
+ }
112
+ async getStatus() {
113
+ const { normal, onlyTs, onlyJs } = await this.getMigrationCodes();
114
+ if (onlyTs.length > 0) {
115
+ console.debug({ onlyTs });
116
+ throw new ServiceUnavailableException(
117
+ `There are un-compiled TS migration files.
118
+ Please compile them first.
119
+
120
+ ${onlyTs.map((f) => f.name).join("\n")}`
121
+ );
122
+ }
123
+ if (onlyJs.length > 0) {
124
+ console.debug({ onlyJs });
125
+ await Promise.all(
126
+ onlyJs.map(async (f) => {
127
+ execSync(
128
+ `rm -f ${f.path.replace("/src/", "/dist/").replace(".ts", ".js")}`
129
+ );
130
+ })
131
+ );
132
+ }
133
+ const connKeys = Object.keys(DB.fullConfig).filter(
134
+ (key) => key.endsWith("_slave") === false
135
+ );
136
+ const statuses = await Promise.all(
137
+ connKeys.map(async (connKey) => {
138
+ const tConn = DB.getClient(connKey);
139
+ const status = await (async () => {
140
+ try {
141
+ return await tConn.status();
142
+ } catch (err) {
143
+ console.error(err);
144
+ return "error";
145
+ }
146
+ })();
147
+ const pending = await (async () => {
148
+ try {
149
+ return await tConn.getMigrations();
150
+ } catch (err) {
151
+ console.error(err);
152
+ return [];
153
+ }
154
+ })();
155
+ const currentVersion = await (async () => {
156
+ return "error";
157
+ })();
158
+ const info = tConn.connectionInfo;
159
+ await tConn.destroy();
160
+ return {
161
+ name: connKey.replace("_master", ""),
162
+ connKey,
163
+ connString: `mysql2://${info.user ?? ""}@${info.host}:${info.port}/${info.database}`,
164
+ currentVersion,
165
+ status,
166
+ pending
167
+ };
168
+ })
169
+ );
170
+ const preparedCodes = await (async () => {
171
+ const status0conn = statuses.find((status) => status.status === 0);
172
+ if (status0conn === void 0) {
173
+ return [];
174
+ }
175
+ const compareDBconn = DB.getClient(status0conn.connKey);
176
+ const genCodes = await this.compareMigrations(compareDBconn);
177
+ await compareDBconn.destroy();
178
+ return genCodes;
179
+ })();
180
+ return {
181
+ conns: statuses,
182
+ codes: normal,
183
+ preparedCodes
184
+ };
185
+ }
186
+ async runAction(action, targets) {
187
+ const configs = DB.getUniqueConfigs(targets);
188
+ const conns = await Promise.all(
189
+ configs.map(async (config) => ({
190
+ connKey: config.connKey,
191
+ db: DB.getClient(config.connKey)
192
+ }))
193
+ );
194
+ const result = await (async () => {
195
+ switch (action) {
196
+ case "latest":
197
+ return Promise.all(
198
+ conns.map(async ({ connKey, db }) => {
199
+ const [batchNo, applied] = await db.migrate();
200
+ return {
201
+ connKey,
202
+ batchNo,
203
+ applied
204
+ };
205
+ })
206
+ );
207
+ case "rollback":
208
+ return Promise.all(
209
+ conns.map(async ({ connKey, db }) => {
210
+ const [batchNo, applied] = await db.rollback();
211
+ return {
212
+ connKey,
213
+ batchNo,
214
+ applied
215
+ };
216
+ })
217
+ );
218
+ }
219
+ })();
220
+ await Promise.all(
221
+ conns.map(({ db }) => {
222
+ return db.destroy();
223
+ })
224
+ );
225
+ return result;
226
+ }
227
+ async delCodes(codeNames) {
228
+ const { conns } = await this.getStatus();
229
+ if (conns.some((conn) => {
230
+ return codeNames.some(
231
+ (codeName) => conn.pending.includes(codeName) === false
232
+ );
233
+ })) {
234
+ throw new Error(
235
+ "You cannot delete a migration file if there is already applied."
236
+ );
237
+ }
238
+ const delFiles = codeNames.map((codeName) => [
239
+ `${Sonamu.apiRootPath}/src/migrations/${codeName}.ts`,
240
+ `${Sonamu.apiRootPath}/dist/migrations/${codeName}.js`
241
+ ]).flat();
242
+ const res = await Promise.all(
243
+ delFiles.map((delFile) => {
244
+ if (fs.existsSync(delFile)) {
245
+ console.log(chalk.red(`DELETE: ${delFile}`));
246
+ fs.unlinkSync(delFile);
247
+ return delFiles.includes(".ts") ? 1 : 0;
248
+ }
249
+ return 0;
250
+ })
251
+ );
252
+ return _.sum(res);
253
+ }
254
+ async generatePreparedCodes() {
255
+ const { preparedCodes } = await this.getStatus();
256
+ if (preparedCodes.length === 0) {
257
+ console.log(chalk.green("\n\uD604\uC7AC \uBAA8\uB450 \uC2F1\uD06C\uB41C \uC0C1\uD0DC\uC785\uB2C8\uB2E4."));
258
+ return 0;
259
+ }
260
+ const migrationsDir = `${Sonamu.apiRootPath}/src/migrations`;
261
+ preparedCodes.filter((pcode) => pcode.formatted).map((pcode, index) => {
262
+ const dateTag = DateTime.local().plus({ seconds: index }).toFormat("yyyyMMddHHmmss");
263
+ const filePath = `${migrationsDir}/${dateTag}_${pcode.title}.ts`;
264
+ fs.writeFileSync(filePath, pcode.formatted);
265
+ console.log(chalk.green(`MIGRTAION CREATED ${filePath}`));
266
+ });
267
+ return preparedCodes.length;
268
+ }
269
+ async clearPendingList() {
270
+ const pendingList = await this.targets.pending.getMigrations();
271
+ const migrationsDir = `${Sonamu.apiRootPath}/src/migrations`;
272
+ const delList = pendingList.map((df) => {
273
+ return path.join(migrationsDir, `${df}.ts`);
274
+ });
275
+ for (let p of delList) {
276
+ if (fs.existsSync(p)) {
277
+ fs.unlinkSync(p);
278
+ }
279
+ }
280
+ await this.cleanUpDist(true);
281
+ }
282
+ async check() {
283
+ const codes = await this.compareMigrations(this.targets.compare);
284
+ if (codes.length === 0) {
285
+ console.log(chalk.green("\n\uD604\uC7AC \uBAA8\uB450 \uC2F1\uD06C\uB41C \uC0C1\uD0DC\uC785\uB2C8\uB2E4."));
286
+ return;
287
+ }
288
+ console.table(codes, ["type", "title"]);
289
+ console.log(codes[0]);
290
+ }
291
+ async run() {
292
+ const pendingList = await this.targets.pending.getMigrations();
293
+ if (pendingList.length > 0) {
294
+ console.log(
295
+ chalk.red("pending \uB41C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC774 \uC874\uC7AC\uD569\uB2C8\uB2E4."),
296
+ pendingList.map((pending) => pending.file)
297
+ );
298
+ const answer2 = await prompts({
299
+ type: "confirm",
300
+ name: "value",
301
+ message: "Shadow DB \uD14C\uC2A4\uD2B8\uB97C \uC9C4\uD589\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
302
+ initial: true
303
+ });
304
+ if (answer2.value === false) {
305
+ return;
306
+ }
307
+ console.time(chalk.blue("Migrator - runShadowTest"));
308
+ await this.runShadowTest();
309
+ console.timeEnd(chalk.blue("Migrator - runShadowTest"));
310
+ await Promise.all(
311
+ this.targets.apply.map(async (applyDb) => {
312
+ const info = applyDb.connectionInfo;
313
+ const label = chalk.green(`APPLIED ${info.host} ${info.database}`);
314
+ console.time(label);
315
+ await applyDb.migrate();
316
+ console.timeEnd(label);
317
+ })
318
+ );
319
+ }
320
+ const codes = await this.compareMigrations(this.targets.compare);
321
+ if (codes.length === 0) {
322
+ console.log(chalk.green("\n\uD604\uC7AC \uBAA8\uB450 \uC2F1\uD06C\uB41C \uC0C1\uD0DC\uC785\uB2C8\uB2E4."));
323
+ return;
324
+ }
325
+ console.table(codes, ["type", "title"]);
326
+ const answer = await prompts({
327
+ type: "confirm",
328
+ name: "value",
329
+ message: "\uB9C8\uC774\uADF8\uB808\uC774\uC158 \uCF54\uB4DC\uB97C \uC0DD\uC131\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
330
+ initial: false
331
+ });
332
+ if (answer.value === false) {
333
+ return;
334
+ }
335
+ const migrationsDir = `${Sonamu.apiRootPath}/src/migrations`;
336
+ codes.filter((code) => code.formatted).map((code, index) => {
337
+ const dateTag = DateTime.local().plus({ seconds: index }).toFormat("yyyyMMddHHmmss");
338
+ const filePath = `${migrationsDir}/${dateTag}_${code.title}.ts`;
339
+ fs.writeFileSync(filePath, code.formatted);
340
+ console.log(chalk.green(`MIGRTAION CREATED ${filePath}`));
341
+ });
342
+ }
343
+ async rollback() {
344
+ console.time(chalk.red("rollback:"));
345
+ const rollbackAllResult = await Promise.all(
346
+ this.targets.apply.map(async (db) => {
347
+ return db.rollback();
348
+ })
349
+ );
350
+ console.dir({ rollbackAllResult }, { depth: null });
351
+ console.timeEnd(chalk.red("rollback:"));
352
+ }
353
+ async cleanUpDist(force = false) {
354
+ const files = ["src", "dist"].reduce(
355
+ (r, which) => {
356
+ const migrationPath = path.join(
357
+ Sonamu.apiRootPath,
358
+ which,
359
+ "migrations"
360
+ );
361
+ if (fs.existsSync(migrationPath) === false) {
362
+ fs.mkdirSync(migrationPath, {
363
+ recursive: true
364
+ });
365
+ }
366
+ const files2 = fs.readdirSync(migrationPath).filter((filename) => filename.startsWith(".") === false);
367
+ r[which] = files2;
368
+ return r;
369
+ },
370
+ {
371
+ src: [],
372
+ dist: []
373
+ }
374
+ );
375
+ const diffOnSrc = _.differenceBy(
376
+ files.src,
377
+ files.dist,
378
+ (filename) => filename.split(".")[0]
379
+ );
380
+ if (diffOnSrc.length > 0) {
381
+ throw new Error(
382
+ "\uCEF4\uD30C\uC77C \uB418\uC9C0 \uC54A\uC740 \uD30C\uC77C\uC774 \uC788\uC2B5\uB2C8\uB2E4.\n" + diffOnSrc.join("\n")
383
+ );
384
+ }
385
+ const diffOnDist = _.differenceBy(
386
+ files.dist,
387
+ files.src,
388
+ (filename) => filename.split(".")[0]
389
+ );
390
+ if (diffOnDist.length > 0) {
391
+ console.log(chalk.red("\uC6D0\uBCF8 ts\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uB294 js\uD30C\uC77C\uC774 \uC788\uC2B5\uB2C8\uB2E4."));
392
+ console.log(diffOnDist);
393
+ if (!force) {
394
+ const answer = await prompts({
395
+ type: "confirm",
396
+ name: "value",
397
+ message: "\uC0AD\uC81C\uB97C \uC9C4\uD589\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
398
+ initial: true
399
+ });
400
+ if (answer.value === false) {
401
+ return;
402
+ }
403
+ }
404
+ const filesToRm = diffOnDist.map((filename) => {
405
+ return path.join(Sonamu.apiRootPath, "dist", "migrations", filename);
406
+ });
407
+ filesToRm.map((filePath) => {
408
+ fs.unlinkSync(filePath);
409
+ });
410
+ console.log(chalk.green(`${filesToRm.length}\uAC74 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4!`));
411
+ }
412
+ }
413
+ async runShadowTest() {
414
+ const tdb = DB.getClient("test");
415
+ const tdbConn = tdb.connectionInfo;
416
+ const shadowDatabase = tdbConn.database + "__migration_shadow";
417
+ const tmpSqlPath = `/tmp/${shadowDatabase}.sql`;
418
+ console.log(
419
+ chalk.magenta(`${tdbConn.database}\uC758 \uB370\uC774\uD130 ${tmpSqlPath}\uB85C \uB364\uD504`)
420
+ );
421
+ execSync(
422
+ `mysqldump -h${tdbConn.host} -P${tdbConn.port} -u${tdbConn.user} -p'${tdbConn.password}' ${tdbConn.database} --single-transaction --no-create-db --triggers > ${tmpSqlPath};`
423
+ );
424
+ execSync(
425
+ `sed -i'' -e 's/\`${tdbConn.database}\`/\`${shadowDatabase}\`/g' ${tmpSqlPath};`
426
+ );
427
+ console.log(chalk.magenta(`${shadowDatabase} \uB9AC\uC14B`));
428
+ await tdb.raw(`DROP DATABASE IF EXISTS \`${shadowDatabase}\`;`);
429
+ await tdb.raw(`CREATE DATABASE \`${shadowDatabase}\`;`);
430
+ console.log(chalk.magenta(`${shadowDatabase} \uB370\uC774\uD130\uBCA0\uC774\uC2A4 \uC0DD\uC131`));
431
+ execSync(
432
+ `mysql -h${tdbConn.host} -P${tdbConn.port} -u${tdbConn.user} -p'${tdbConn.password}' ${shadowDatabase} < ${tmpSqlPath};`
433
+ );
434
+ try {
435
+ await tdb.raw(`USE \`${shadowDatabase}\`;`);
436
+ const [batchNo, applied] = await tdb.migrate();
437
+ console.log(chalk.green("Shadow DB \uD14C\uC2A4\uD2B8\uC5D0 \uC131\uACF5\uD588\uC2B5\uB2C8\uB2E4!"), {
438
+ batchNo,
439
+ applied
440
+ });
441
+ console.log(chalk.magenta(`${shadowDatabase} \uC0AD\uC81C`));
442
+ await tdb.raw(`DROP DATABASE IF EXISTS \`${shadowDatabase}\`;`);
443
+ return [
444
+ {
445
+ connKey: "shadow",
446
+ batchNo,
447
+ applied
448
+ }
449
+ ];
450
+ } catch (e) {
451
+ console.error(e);
452
+ throw new ServiceUnavailableException("Shadow DB \uD14C\uC2A4\uD2B8 \uC9C4\uD589 \uC911 \uC5D0\uB7EC");
453
+ } finally {
454
+ await tdb.destroy();
455
+ }
456
+ }
457
+ async resetAll() {
458
+ const answer = await prompts({
459
+ type: "confirm",
460
+ name: "value",
461
+ message: "\uBAA8\uB4E0 DB\uB97C \uB864\uBC31\uD558\uACE0 \uC804\uCCB4 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uD30C\uC77C\uC744 \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
462
+ initial: false
463
+ });
464
+ if (answer.value === false) {
465
+ return;
466
+ }
467
+ console.time(chalk.red("rollback-all:"));
468
+ const rollbackAllResult = await Promise.all(
469
+ this.targets.apply.map(async (db) => {
470
+ return db.rollbackAll();
471
+ })
472
+ );
473
+ console.log({ rollbackAllResult });
474
+ console.timeEnd(chalk.red("rollback-all:"));
475
+ const migrationsDir = `${Sonamu.apiRootPath}/src/migrations`;
476
+ console.time(chalk.red("delete migration files"));
477
+ execSync(`rm -f ${migrationsDir}/*`);
478
+ execSync(`rm -f ${migrationsDir.replace("/src/", "/dist/")}/*`);
479
+ console.timeEnd(chalk.red("delete migration files"));
480
+ }
481
+ async compareMigrations(compareDB) {
482
+ const entityIds = EntityManager.getAllIds();
483
+ const entitySetsWithJoinTable = entityIds.filter((entityId) => {
484
+ const entity = EntityManager.get(entityId);
485
+ return entity.props.length > 0;
486
+ }).map((entityId) => {
487
+ const entity = EntityManager.get(entityId);
488
+ return this.getMigrationSetFromEntity(entity);
489
+ });
490
+ const joinTablesWithDup = entitySetsWithJoinTable.map((entitySet) => entitySet.joinTables).flat();
491
+ const joinTables = Object.values(
492
+ _.groupBy(joinTablesWithDup, (jt) => jt.table)
493
+ ).map((tables) => {
494
+ if (tables.length === 1) {
495
+ return tables[0];
496
+ }
497
+ return {
498
+ ...tables[0],
499
+ indexes: _.uniqBy(
500
+ tables.flatMap((t) => t.indexes),
501
+ (index) => [index.type, ...index.columns.sort()].join("-")
502
+ )
503
+ };
504
+ });
505
+ const entitySets = [
506
+ ...entitySetsWithJoinTable,
507
+ ...joinTables
508
+ ];
509
+ const codes = (await Promise.all(
510
+ entitySets.map(async (entitySet) => {
511
+ const dbSet = await this.getMigrationSetFromDB(
512
+ compareDB,
513
+ entitySet.table
514
+ );
515
+ if (dbSet === null) {
516
+ return [
517
+ await DB.generator.generateCreateCode_ColumnAndIndexes(
518
+ entitySet.table,
519
+ entitySet.columns,
520
+ entitySet.indexes
521
+ ),
522
+ ...await DB.generator.generateCreateCode_Foreign(
523
+ entitySet.table,
524
+ entitySet.foreigns
525
+ )
526
+ ];
527
+ }
528
+ const alterCodes = await Promise.all(
529
+ ["columnsAndIndexes", "foreigns"].map((key) => {
530
+ if (key === "columnsAndIndexes") {
531
+ const replaceColumnDefaultTo = (col) => {
532
+ if (col.type === "float" && col.defaultTo && String(col.defaultTo).includes('"') === false) {
533
+ col.defaultTo = `"${Number(col.defaultTo).toFixed(
534
+ col.scale ?? 2
535
+ )}"`;
536
+ }
537
+ if (col.type === "string" && col.defaultTo === "") {
538
+ col.defaultTo = '""';
539
+ }
540
+ return col;
541
+ };
542
+ const entityColumns = _.sortBy(
543
+ entitySet.columns,
544
+ (a) => a.name
545
+ ).map(replaceColumnDefaultTo);
546
+ const dbColumns = _.sortBy(dbSet.columns, (a) => a.name).map(
547
+ replaceColumnDefaultTo
548
+ );
549
+ const entityIndexes = _.sortBy(
550
+ entitySet.indexes,
551
+ (a) => [
552
+ a.type,
553
+ ...a.columns.sort((c1, c2) => c1 > c2 ? 1 : -1)
554
+ ].join("-")
555
+ );
556
+ const dbIndexes = _.sortBy(
557
+ dbSet.indexes,
558
+ (a) => [
559
+ a.type,
560
+ ...a.columns.sort((c1, c2) => c1 > c2 ? 1 : -1)
561
+ ].join("-")
562
+ );
563
+ const isEqualColumns = equal(entityColumns, dbColumns);
564
+ const isEqualIndexes = equal(entityIndexes, dbIndexes);
565
+ if (isEqualColumns && isEqualIndexes) {
566
+ return null;
567
+ } else {
568
+ return DB.generator.generateAlterCode_ColumnAndIndexes(
569
+ entitySet.table,
570
+ entityColumns,
571
+ entityIndexes,
572
+ dbColumns,
573
+ dbIndexes
574
+ );
575
+ }
576
+ } else {
577
+ const replaceNoActionOnMySQL = (f) => {
578
+ const { onDelete, onUpdate } = f;
579
+ return {
580
+ ...f,
581
+ onUpdate: onUpdate === "RESTRICT" ? "NO ACTION" : onUpdate,
582
+ onDelete: onDelete === "RESTRICT" ? "NO ACTION" : onDelete
583
+ };
584
+ };
585
+ const entityForeigns = _.sortBy(
586
+ entitySet.foreigns,
587
+ (a) => [a.to, ...a.columns].join("-")
588
+ ).map((f) => replaceNoActionOnMySQL(f));
589
+ const dbForeigns = _.sortBy(
590
+ dbSet.foreigns,
591
+ (a) => [a.to, ...a.columns].join("-")
592
+ ).map((f) => replaceNoActionOnMySQL(f));
593
+ if (equal(entityForeigns, dbForeigns) === false) {
594
+ return DB.generator.generateAlterCode_Foreigns(
595
+ entitySet.table,
596
+ entityForeigns,
597
+ dbForeigns
598
+ );
599
+ }
600
+ }
601
+ return null;
602
+ })
603
+ );
604
+ if (alterCodes.every((alterCode) => alterCode === null)) {
605
+ return null;
606
+ } else {
607
+ return alterCodes.filter((alterCode) => alterCode !== null).flat();
608
+ }
609
+ })
610
+ )).flat().filter((code) => code !== null);
611
+ codes.sort((codeA, codeB) => {
612
+ if (codeA.type === "foreign" && codeB.type == "normal") {
613
+ return 1;
614
+ } else if (codeA.type === "normal" && codeB.type === "foreign") {
615
+ return -1;
616
+ } else {
617
+ return 0;
618
+ }
619
+ });
620
+ return codes;
621
+ }
622
+ /*
623
+ 기존 테이블 정보 읽어서 MigrationSet 형식으로 리턴
624
+ */
625
+ async getMigrationSetFromDB(compareDB, table) {
626
+ let dbColumns, dbIndexes, dbForeigns;
627
+ try {
628
+ [dbColumns, dbIndexes, dbForeigns] = await this.readTable(
629
+ compareDB,
630
+ table
631
+ );
632
+ } catch (e) {
633
+ if (isKnexError(e) && e.code === "ER_NO_SUCH_TABLE") {
634
+ return null;
635
+ }
636
+ console.error(e);
637
+ return null;
638
+ }
639
+ const columns = dbColumns.map((dbColumn) => {
640
+ const dbColType = this.resolveDBColType(dbColumn.Type, dbColumn.Field);
641
+ return {
642
+ name: dbColumn.Field,
643
+ nullable: dbColumn.Null !== "NO",
644
+ ...dbColType,
645
+ ...(() => {
646
+ if (dbColumn.Default !== null) {
647
+ return {
648
+ defaultTo: dbColumn.Default
649
+ };
650
+ }
651
+ return {};
652
+ })()
653
+ };
654
+ });
655
+ const dbIndexesGroup = _.groupBy(
656
+ dbIndexes.filter(
657
+ (dbIndex) => dbIndex.Key_name !== "PRIMARY" && !dbForeigns.find(
658
+ (dbForeign) => dbForeign.keyName === dbIndex.Key_name
659
+ )
660
+ ),
661
+ (dbIndex) => dbIndex.Key_name
662
+ );
663
+ const indexes = Object.keys(dbIndexesGroup).map(
664
+ (keyName) => {
665
+ const currentIndexes = dbIndexesGroup[keyName];
666
+ return {
667
+ type: currentIndexes[0].Non_unique === 1 ? "index" : "unique",
668
+ columns: currentIndexes.map(
669
+ (currentIndex) => currentIndex.Column_name
670
+ )
671
+ };
672
+ }
673
+ );
674
+ const foreigns = dbForeigns.map((dbForeign) => {
675
+ return {
676
+ columns: [dbForeign.from],
677
+ to: `${dbForeign.referencesTable}.${dbForeign.referencesField}`,
678
+ onUpdate: dbForeign.onUpdate,
679
+ onDelete: dbForeign.onDelete
680
+ };
681
+ });
682
+ return {
683
+ table,
684
+ columns,
685
+ indexes,
686
+ foreigns
687
+ };
688
+ }
689
+ resolveDBColType(colType, colField) {
690
+ let [rawType, unsigned] = colType.split(" ");
691
+ const matched = rawType.match(/\(([0-9]+)\)/);
692
+ let length;
693
+ if (matched !== null && matched[1]) {
694
+ rawType = rawType.replace(/\(([0-9]+)\)/, "");
695
+ length = parseInt(matched[1]);
696
+ }
697
+ if (rawType === "char" && colField === "uuid") {
698
+ return {
699
+ type: "uuid"
700
+ };
701
+ }
702
+ switch (rawType) {
703
+ case "int":
704
+ return {
705
+ type: "integer",
706
+ unsigned: unsigned === "unsigned"
707
+ };
708
+ case "varchar":
709
+ return {
710
+ type: "string",
711
+ ...length !== void 0 && {
712
+ length
713
+ }
714
+ };
715
+ case "text":
716
+ case "mediumtext":
717
+ case "longtext":
718
+ case "timestamp":
719
+ case "json":
720
+ case "date":
721
+ case "time":
722
+ return {
723
+ type: rawType
724
+ };
725
+ case "datetime":
726
+ return {
727
+ type: "datetime"
728
+ };
729
+ case "tinyint":
730
+ return {
731
+ type: "boolean"
732
+ };
733
+ default:
734
+ if (rawType.startsWith("decimal")) {
735
+ const [, precision, scale] = rawType.match(/decimal\(([0-9]+),([0-9]+)\)/) ?? [];
736
+ return {
737
+ type: "decimal",
738
+ precision: parseInt(precision),
739
+ scale: parseInt(scale),
740
+ ...unsigned === "unsigned" && {
741
+ unsigned: true
742
+ }
743
+ };
744
+ } else if (rawType.startsWith("float")) {
745
+ const [, precision, scale] = rawType.match(/float\(([0-9]+),([0-9]+)\)/) ?? [];
746
+ return {
747
+ type: "float",
748
+ precision: parseInt(precision),
749
+ scale: parseInt(scale),
750
+ ...unsigned === "unsigned" && {
751
+ unsigned: true
752
+ }
753
+ };
754
+ }
755
+ throw new Error(`resolve \uBD88\uAC00\uB2A5\uD55C DB\uCEEC\uB7FC \uD0C0\uC785 ${colType} ${rawType}`);
756
+ }
757
+ }
758
+ /*
759
+ 기존 테이블 읽어서 cols, indexes 반환
760
+ */
761
+ async readTable(compareDB, tableName) {
762
+ try {
763
+ const _cols = await compareDB.raw(
764
+ `SHOW FIELDS FROM ${tableName}`
765
+ );
766
+ const cols = _cols.map((col) => ({
767
+ ...col,
768
+ // Default 값은 숫자나 MySQL Expression이 아닌 경우 ""로 감싸줌
769
+ ...col.Default !== null && {
770
+ Default: col.Default.replace(/[0-9]+/g, "").length > 0 && col.Extra !== "DEFAULT_GENERATED" ? `"${col.Default}"` : col.Default
771
+ }
772
+ }));
773
+ const indexes = await compareDB.raw(
774
+ `SHOW INDEX FROM ${tableName}`
775
+ );
776
+ const [row] = await compareDB.raw(`SHOW CREATE TABLE ${tableName}`);
777
+ const ddl = row["Create Table"];
778
+ const matched = ddl.match(/CONSTRAINT .+/g);
779
+ const foreignKeys = (matched ?? []).map((line) => {
780
+ const matched2 = line.match(
781
+ /CONSTRAINT `(.+)` FOREIGN KEY \(`(.+)`\) REFERENCES `(.+)` \(`(.+)`\)( ON [A-Z ]+)*/
782
+ );
783
+ if (!matched2) {
784
+ throw new Error(`\uC778\uC2DD\uD560 \uC218 \uC5C6\uB294 FOREIGN KEY CONSTRAINT ${line}`);
785
+ }
786
+ const [, keyName, from, referencesTable, referencesField, onClause] = matched2;
787
+ const [onUpdateFull, _onUpdate] = (onClause ?? "").match(/ON UPDATE ([A-Z ]+)$/) ?? [];
788
+ const onUpdate = _onUpdate ?? "NO ACTION";
789
+ const onDelete = (onClause ?? "").replace(onUpdateFull ?? "", "").match(/ON DELETE ([A-Z ]+)/)?.[1]?.trim() ?? "NO ACTION";
790
+ return {
791
+ keyName,
792
+ from,
793
+ referencesTable,
794
+ referencesField,
795
+ onDelete,
796
+ onUpdate
797
+ };
798
+ });
799
+ return [cols, indexes, foreignKeys];
800
+ } catch (e) {
801
+ throw e;
802
+ }
803
+ }
804
+ /*
805
+ Entity 내용 읽어서 MigrationSetAndJoinTable 추출
806
+ */
807
+ getMigrationSetFromEntity(entity) {
808
+ const migrationSet = entity.props.reduce(
809
+ (r, prop) => {
810
+ if (isVirtualProp(prop)) {
811
+ return r;
812
+ }
813
+ if (isHasManyRelationProp(prop)) {
814
+ return r;
815
+ }
816
+ if (!isRelationProp(prop)) {
817
+ let type;
818
+ if (isTextProp(prop)) {
819
+ type = prop.textType;
820
+ } else if (isEnumProp(prop)) {
821
+ type = "string";
822
+ } else {
823
+ type = prop.type;
824
+ }
825
+ const column = {
826
+ name: prop.name,
827
+ type,
828
+ ...isIntegerProp(prop) && { unsigned: prop.unsigned === true },
829
+ ...(isStringProp(prop) || isEnumProp(prop)) && {
830
+ length: prop.length
831
+ },
832
+ nullable: prop.nullable === true,
833
+ ...(() => {
834
+ if (prop.dbDefault !== void 0) {
835
+ return {
836
+ defaultTo: prop.dbDefault
837
+ };
838
+ }
839
+ return {};
840
+ })(),
841
+ // FIXME: float(N, M) deprecated
842
+ // Decimal, Float 타입의 경우 precision, scale 추가
843
+ ...(isDecimalProp(prop) || isFloatProp(prop)) && {
844
+ precision: prop.precision ?? 8,
845
+ scale: prop.scale ?? 2
846
+ }
847
+ };
848
+ r.columns.push(column);
849
+ }
850
+ if (isManyToManyRelationProp(prop)) {
851
+ const relMd = EntityManager.get(prop.with);
852
+ const [table1, table2] = prop.joinTable.split("__");
853
+ const join = {
854
+ from: `${entity.table}.id`,
855
+ through: {
856
+ from: `${prop.joinTable}.${inflection.singularize(table1)}_id`,
857
+ to: `${prop.joinTable}.${inflection.singularize(table2)}_id`,
858
+ onUpdate: prop.onUpdate,
859
+ onDelete: prop.onDelete
860
+ },
861
+ to: `${relMd.table}.id`
862
+ };
863
+ const through = join.through;
864
+ const fields = [through.from, through.to];
865
+ r.joinTables.push({
866
+ table: through.from.split(".")[0],
867
+ indexes: [
868
+ {
869
+ type: "unique",
870
+ columns: ["uuid"]
871
+ },
872
+ // 조인 테이블에 걸린 인덱스 찾아와서 연결
873
+ ...entity.indexes.filter(
874
+ (index) => index.columns.find(
875
+ (col) => col.includes(prop.joinTable + ".")
876
+ )
877
+ ).map((index) => ({
878
+ ...index,
879
+ columns: index.columns.map(
880
+ (col) => col.replace(prop.joinTable + ".", "")
881
+ )
882
+ }))
883
+ ],
884
+ columns: [
885
+ {
886
+ name: "id",
887
+ type: "integer",
888
+ nullable: false,
889
+ unsigned: true
890
+ },
891
+ ...fields.map((field) => {
892
+ return {
893
+ name: field.split(".")[1],
894
+ type: "integer",
895
+ nullable: false,
896
+ unsigned: true
897
+ };
898
+ }),
899
+ {
900
+ name: "uuid",
901
+ nullable: true,
902
+ type: "uuid"
903
+ }
904
+ ],
905
+ foreigns: fields.map((field) => {
906
+ const col = field.split(".")[1];
907
+ const to = (() => {
908
+ if (inflection.singularize(join.to.split(".")[0]) + "_id" === col) {
909
+ return join.to;
910
+ } else {
911
+ return join.from;
912
+ }
913
+ })();
914
+ return {
915
+ columns: [col],
916
+ to,
917
+ onUpdate: through.onUpdate,
918
+ onDelete: through.onDelete
919
+ };
920
+ })
921
+ });
922
+ return r;
923
+ } else if (isBelongsToOneRelationProp(prop) || isOneToOneRelationProp(prop) && prop.hasJoinColumn) {
924
+ const idColumnName = prop.name + "_id";
925
+ r.columns.push({
926
+ name: idColumnName,
927
+ type: "integer",
928
+ unsigned: true,
929
+ nullable: prop.nullable ?? false
930
+ });
931
+ r.foreigns.push({
932
+ columns: [idColumnName],
933
+ to: `${inflection.underscore(inflection.pluralize(prop.with)).toLowerCase()}.id`,
934
+ onUpdate: prop.onUpdate,
935
+ onDelete: prop.onDelete
936
+ });
937
+ }
938
+ return r;
939
+ },
940
+ {
941
+ table: entity.table,
942
+ columns: [],
943
+ indexes: [],
944
+ foreigns: [],
945
+ joinTables: []
946
+ }
947
+ );
948
+ migrationSet.indexes = entity.indexes.filter(
949
+ (index) => index.columns.find((col) => col.includes(".") === false)
950
+ );
951
+ migrationSet.columns = migrationSet.columns.concat({
952
+ name: "uuid",
953
+ nullable: true,
954
+ type: "uuid"
955
+ });
956
+ migrationSet.indexes = migrationSet.indexes.concat({
957
+ type: "unique",
958
+ columns: ["uuid"]
959
+ });
960
+ return migrationSet;
961
+ }
962
+ /*
963
+ 마이그레이션 컬럼 배열 비교용 코드
964
+ */
965
+ showMigrationSet(which, migrationSet) {
966
+ const { columns, indexes, foreigns } = migrationSet;
967
+ const styledChalk = which === "Entity" ? chalk.bgGreen.black : chalk.bgBlue.black;
968
+ console.log(
969
+ styledChalk(
970
+ `${which} ${migrationSet.table} Columns `
971
+ )
972
+ );
973
+ console.table(
974
+ columns.map((column) => {
975
+ return {
976
+ ..._.pick(column, [
977
+ "name",
978
+ "type",
979
+ "nullable",
980
+ "unsigned",
981
+ "length",
982
+ "defaultTo",
983
+ "precision",
984
+ "scale"
985
+ ])
986
+ };
987
+ }),
988
+ [
989
+ "name",
990
+ "type",
991
+ "nullable",
992
+ "unsigned",
993
+ "length",
994
+ "defaultTo",
995
+ "precision",
996
+ "scale"
997
+ ]
998
+ );
999
+ if (indexes.length > 0) {
1000
+ console.log(
1001
+ styledChalk(
1002
+ `${which} ${migrationSet.table} Indexes `
1003
+ )
1004
+ );
1005
+ console.table(
1006
+ indexes.map((index) => {
1007
+ return {
1008
+ ..._.pick(index, ["type", "columns", "name"])
1009
+ };
1010
+ })
1011
+ );
1012
+ }
1013
+ if (foreigns.length > 0) {
1014
+ console.log(
1015
+ chalk.bgMagenta.black(
1016
+ `${which} ${migrationSet.table} Foreigns `
1017
+ )
1018
+ );
1019
+ console.table(
1020
+ foreigns.map((foreign) => {
1021
+ return {
1022
+ ..._.pick(foreign, ["columns", "to", "onUpdate", "onDelete"])
1023
+ };
1024
+ })
1025
+ );
1026
+ }
1027
+ }
1028
+ async destroy() {
1029
+ await Promise.all(
1030
+ this.targets.apply.map((db) => {
1031
+ return db.destroy();
1032
+ })
1033
+ );
1034
+ }
1035
+ };
1036
+
1037
+ // src/testing/fixture-manager.ts
1038
+ import chalk2 from "chalk";
1039
+ import _2 from "lodash";
1040
+ import inflection2 from "inflection";
1041
+ import { readFileSync, writeFileSync } from "fs";
1042
+
1043
+ // src/testing/_relation-graph.ts
1044
+ var RelationGraph = class {
1045
+ graph = /* @__PURE__ */ new Map();
1046
+ buildGraph(fixtures) {
1047
+ this.graph.clear();
1048
+ for (const fixture of fixtures) {
1049
+ this.graph.set(fixture.fixtureId, {
1050
+ fixtureId: fixture.fixtureId,
1051
+ entityId: fixture.entityId,
1052
+ related: /* @__PURE__ */ new Set()
1053
+ });
1054
+ }
1055
+ for (const fixture of fixtures) {
1056
+ const node = this.graph.get(fixture.fixtureId);
1057
+ for (const [, column] of Object.entries(fixture.columns)) {
1058
+ const prop = column.prop;
1059
+ if (isRelationProp(prop)) {
1060
+ if (isBelongsToOneRelationProp(prop) || isOneToOneRelationProp(prop) && prop.hasJoinColumn) {
1061
+ const relatedFixtureId = `${prop.with}#${column.value}`;
1062
+ if (this.graph.has(relatedFixtureId)) {
1063
+ node.related.add(relatedFixtureId);
1064
+ }
1065
+ } else if (isManyToManyRelationProp(prop)) {
1066
+ const relatedIds = column.value;
1067
+ for (const relatedId of relatedIds) {
1068
+ const relatedFixtureId = `${prop.with}#${relatedId}`;
1069
+ if (this.graph.has(relatedFixtureId)) {
1070
+ node.related.add(relatedFixtureId);
1071
+ this.graph.get(relatedFixtureId).related.add(fixture.fixtureId);
1072
+ }
1073
+ }
1074
+ }
1075
+ }
1076
+ }
1077
+ }
1078
+ }
1079
+ getInsertionOrder() {
1080
+ const visited = /* @__PURE__ */ new Set();
1081
+ const order = [];
1082
+ const tempVisited = /* @__PURE__ */ new Set();
1083
+ const visit = (fixtureId) => {
1084
+ if (visited.has(fixtureId)) return;
1085
+ if (tempVisited.has(fixtureId)) {
1086
+ console.warn(`Circular dependency detected involving: ${fixtureId}`);
1087
+ return;
1088
+ }
1089
+ tempVisited.add(fixtureId);
1090
+ const node = this.graph.get(fixtureId);
1091
+ const entity = EntityManager.get(node.entityId);
1092
+ for (const depId of node.related) {
1093
+ const depNode = this.graph.get(depId);
1094
+ const relationProp = entity.props.find(
1095
+ (prop) => isRelationProp(prop) && (isBelongsToOneRelationProp(prop) || isOneToOneRelationProp(prop) && prop.hasJoinColumn) && prop.with === depNode.entityId
1096
+ );
1097
+ if (relationProp && !relationProp.nullable) {
1098
+ visit(depId);
1099
+ }
1100
+ }
1101
+ tempVisited.delete(fixtureId);
1102
+ visited.add(fixtureId);
1103
+ order.push(fixtureId);
1104
+ };
1105
+ for (const fixtureId of this.graph.keys()) {
1106
+ visit(fixtureId);
1107
+ }
1108
+ for (const fixtureId of this.graph.keys()) {
1109
+ if (!visited.has(fixtureId)) {
1110
+ order.push(fixtureId);
1111
+ }
1112
+ }
1113
+ return order;
1114
+ }
1115
+ };
1116
+
1117
+ // src/testing/fixture-manager.ts
1118
+ var FixtureManagerClass = class {
1119
+ relationGraph = new RelationGraph();
1120
+ init() {
1121
+ DB.testInit();
1122
+ }
1123
+ async cleanAndSeed(usingTables) {
1124
+ const tableNames = await (async () => {
1125
+ if (usingTables) {
1126
+ return usingTables;
1127
+ }
1128
+ const tables = await DB.tdb.raw(
1129
+ `SHOW TABLE STATUS WHERE Engine IS NOT NULL`
1130
+ );
1131
+ return tables.map((tableInfo) => tableInfo["Name"]);
1132
+ })();
1133
+ await DB.tdb.raw(`SET FOREIGN_KEY_CHECKS = 0`);
1134
+ for await (let tableName of tableNames) {
1135
+ if (tableName == "migrations") {
1136
+ continue;
1137
+ }
1138
+ const [fdbChecksumRow] = await DB.fdb.raw(
1139
+ `CHECKSUM TABLE ${tableName}`
1140
+ );
1141
+ const fdbChecksum = fdbChecksumRow["Checksum"];
1142
+ const [tdbChecksumRow] = await DB.tdb.raw(
1143
+ `CHECKSUM TABLE ${tableName}`
1144
+ );
1145
+ const tdbChecksum = tdbChecksumRow["Checksum"];
1146
+ if (fdbChecksum !== tdbChecksum) {
1147
+ await DB.tdb.truncate(tableName);
1148
+ const rawQuery = `INSERT INTO ${DB.connectionInfo.test.database}.${tableName}
1149
+ SELECT * FROM ${DB.connectionInfo.fixture_local.database}.${tableName}`;
1150
+ await DB.tdb.raw(rawQuery);
1151
+ }
1152
+ }
1153
+ await DB.tdb.raw(`SET FOREIGN_KEY_CHECKS = 1`);
1154
+ }
1155
+ async getChecksum(db, tableName) {
1156
+ const [checksumRow] = await db.raw(
1157
+ `CHECKSUM TABLE ${tableName}`
1158
+ );
1159
+ return checksumRow.Checksum;
1160
+ }
1161
+ async sync() {
1162
+ const frdb = DB.getClient("fixture_remote");
1163
+ const tables = await DB.fdb.raw(
1164
+ "SHOW TABLE STATUS WHERE Engine IS NOT NULL"
1165
+ );
1166
+ const tableNames = tables.map(
1167
+ (table) => table.Name
1168
+ );
1169
+ console.log(chalk2.magenta("SYNC..."));
1170
+ await Promise.all(
1171
+ tableNames.map(async (tableName) => {
1172
+ if (tableName.startsWith(DB.migrationTable)) {
1173
+ return;
1174
+ }
1175
+ const remoteChecksum = await this.getChecksum(frdb, tableName);
1176
+ const localChecksum = await this.getChecksum(DB.fdb, tableName);
1177
+ if (remoteChecksum !== localChecksum) {
1178
+ await DB.fdb.trx(async (transaction) => {
1179
+ await transaction.raw(`SET FOREIGN_KEY_CHECKS = 0`);
1180
+ await transaction.truncate(tableName);
1181
+ const rows = await frdb.raw(`SELECT * FROM ${tableName}`);
1182
+ if (rows.length === 0) {
1183
+ return;
1184
+ }
1185
+ console.log(chalk2.blue(tableName), rows.length);
1186
+ await transaction.raw(
1187
+ `INSERT INTO ${tableName} (${Object.keys(rows[0]).map((k) => `\`${k}\``).join(",")}) VALUES ?`,
1188
+ [
1189
+ rows.map(
1190
+ (row) => Object.values(row).map((v) => {
1191
+ if (v === null) {
1192
+ return null;
1193
+ } else if (typeof v === "boolean") {
1194
+ return v ? 1 : 0;
1195
+ } else if (typeof v === "object") {
1196
+ return JSON.stringify(v);
1197
+ } else {
1198
+ return v;
1199
+ }
1200
+ })
1201
+ )
1202
+ ]
1203
+ );
1204
+ console.log("OK");
1205
+ await transaction.raw(`SET FOREIGN_KEY_CHECKS = 1`);
1206
+ });
1207
+ }
1208
+ })
1209
+ );
1210
+ console.log(chalk2.magenta("DONE!"));
1211
+ await frdb.destroy();
1212
+ }
1213
+ async importFixture(entityId, ids) {
1214
+ const queries = _2.uniq(
1215
+ (await Promise.all(
1216
+ ids.map(async (id) => {
1217
+ return await this.getImportQueries(entityId, "id", id);
1218
+ })
1219
+ )).flat()
1220
+ );
1221
+ const wdb = DB.toClient(DB.getDB("w"));
1222
+ for (let query of queries) {
1223
+ const [rsh] = await wdb.raw(query);
1224
+ console.log({
1225
+ query,
1226
+ info: rsh.info
1227
+ });
1228
+ }
1229
+ }
1230
+ async getImportQueries(entityId, field, id) {
1231
+ console.log({ entityId, field, id });
1232
+ const entity = EntityManager.get(entityId);
1233
+ const wdb = DB.toClient(DB.getDB("w"));
1234
+ const [row] = await wdb.raw(
1235
+ `SELECT * FROM ${entity.table} WHERE ${field} = ${id} LIMIT 1`
1236
+ );
1237
+ if (row === void 0) {
1238
+ throw new Error(`${entityId}#${id} row\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
1239
+ }
1240
+ const fixtureDatabase = DB.connectionInfo.fixture_remote.database;
1241
+ const realDatabase = DB.connectionInfo.production_master.database;
1242
+ const selfQuery = `INSERT IGNORE INTO \`${fixtureDatabase}\`.\`${entity.table}\` (SELECT * FROM \`${realDatabase}\`.\`${entity.table}\` WHERE \`id\` = ${id})`;
1243
+ const args = Object.entries(entity.relations).filter(
1244
+ ([, relation]) => isBelongsToOneRelationProp(relation) || isOneToOneRelationProp(relation) && relation.customJoinClause === void 0
1245
+ ).map(([, relation]) => {
1246
+ let field2;
1247
+ let id2;
1248
+ if (isOneToOneRelationProp(relation) && !relation.hasJoinColumn) {
1249
+ field2 = `${relation.name}_id`;
1250
+ id2 = row["id"];
1251
+ } else {
1252
+ field2 = "id";
1253
+ id2 = row[`${relation.name}_id`];
1254
+ }
1255
+ return {
1256
+ entityId: relation.with,
1257
+ field: field2,
1258
+ id: id2
1259
+ };
1260
+ }).filter((arg) => arg.id !== null);
1261
+ const relQueries = await Promise.all(
1262
+ args.map(async (args2) => {
1263
+ return this.getImportQueries(args2.entityId, args2.field, args2.id);
1264
+ })
1265
+ );
1266
+ return [..._2.uniq(relQueries.reverse().flat()), selfQuery];
1267
+ }
1268
+ async destory() {
1269
+ await DB.testDestroy();
1270
+ await DB.destroy();
1271
+ }
1272
+ async getFixtures(sourceDBName, targetDBName, searchOptions) {
1273
+ const sourceDB = DB.getClient(sourceDBName);
1274
+ const targetDB = DB.getClient(targetDBName);
1275
+ const { entityId, field, value, searchType } = searchOptions;
1276
+ const entity = EntityManager.get(entityId);
1277
+ const column = entity.props.find((prop) => prop.name === field)?.type === "relation" ? `${field}_id` : field;
1278
+ let query = sourceDB.from(entity.table).selectAll();
1279
+ if (searchType === "equals") {
1280
+ query = query.where([column, "=", value]);
1281
+ } else if (searchType === "like") {
1282
+ query = query.where([column, "like", `%${value}%`]);
1283
+ }
1284
+ const rows = await query.execute();
1285
+ if (rows.length === 0) {
1286
+ throw new Error("No records found");
1287
+ }
1288
+ const fixtures = [];
1289
+ for (const row of rows) {
1290
+ const initialRecordsLength = fixtures.length;
1291
+ const newRecords = await this.createFixtureRecord(entity, row);
1292
+ fixtures.push(...newRecords);
1293
+ const currentFixtureRecord = fixtures.find(
1294
+ (r) => r.fixtureId === `${entityId}#${row.id}`
1295
+ );
1296
+ if (currentFixtureRecord) {
1297
+ currentFixtureRecord.fetchedRecords = fixtures.filter((r) => r.fixtureId !== currentFixtureRecord.fixtureId).slice(initialRecordsLength).map((r) => r.fixtureId);
1298
+ }
1299
+ }
1300
+ for await (const fixture of fixtures) {
1301
+ const entity2 = EntityManager.get(fixture.entityId);
1302
+ const [row] = await targetDB.from(entity2.table).selectAll().where(["id", "=", fixture.id]).first().execute();
1303
+ if (row) {
1304
+ const [record] = await this.createFixtureRecord(entity2, row, {
1305
+ singleRecord: true,
1306
+ _db: targetDB
1307
+ });
1308
+ fixture.target = record;
1309
+ continue;
1310
+ }
1311
+ const uniqueRow = await this.checkUniqueViolation(
1312
+ targetDB,
1313
+ entity2,
1314
+ fixture
1315
+ );
1316
+ if (uniqueRow) {
1317
+ const [record] = await this.createFixtureRecord(entity2, uniqueRow, {
1318
+ singleRecord: true,
1319
+ _db: targetDB
1320
+ });
1321
+ fixture.unique = record;
1322
+ }
1323
+ }
1324
+ return _2.uniqBy(fixtures, (f) => f.fixtureId);
1325
+ }
1326
+ async createFixtureRecord(entity, row, options) {
1327
+ const records = [];
1328
+ const visitedEntities = /* @__PURE__ */ new Set();
1329
+ const create = async (entity2, row2) => {
1330
+ const fixtureId = `${entity2.id}#${row2.id}`;
1331
+ if (visitedEntities.has(fixtureId)) {
1332
+ return;
1333
+ }
1334
+ visitedEntities.add(fixtureId);
1335
+ const record = {
1336
+ fixtureId,
1337
+ entityId: entity2.id,
1338
+ id: row2.id,
1339
+ columns: {},
1340
+ fetchedRecords: [],
1341
+ belongsRecords: []
1342
+ };
1343
+ for (const prop of entity2.props) {
1344
+ if (isVirtualProp(prop)) {
1345
+ continue;
1346
+ }
1347
+ record.columns[prop.name] = {
1348
+ prop,
1349
+ value: row2[prop.name]
1350
+ };
1351
+ const db = options?._db ?? DB.toClient(DB.getDB("w"));
1352
+ if (isManyToManyRelationProp(prop)) {
1353
+ const relatedEntity = EntityManager.get(prop.with);
1354
+ const throughTable = prop.joinTable;
1355
+ const fromColumn = `${inflection2.singularize(entity2.table)}_id`;
1356
+ const toColumn = `${inflection2.singularize(relatedEntity.table)}_id`;
1357
+ const _relatedIds = await db.from(throughTable).select(toColumn).where([fromColumn, "=", row2.id]).execute();
1358
+ const relatedIds = _relatedIds.map((r) => parseInt(r[toColumn]));
1359
+ record.columns[prop.name].value = relatedIds;
1360
+ } else if (isHasManyRelationProp(prop)) {
1361
+ const relatedEntity = EntityManager.get(prop.with);
1362
+ const relatedIds = await db.from(relatedEntity.table).select("id").where([prop.joinColumn, "=", row2.id]).pluck("id");
1363
+ record.columns[prop.name].value = relatedIds;
1364
+ } else if (isOneToOneRelationProp(prop) && !prop.hasJoinColumn) {
1365
+ const relatedEntity = EntityManager.get(prop.with);
1366
+ const relatedProp = relatedEntity.props.find(
1367
+ (p) => isRelationProp(p) && p.with === entity2.id
1368
+ );
1369
+ if (relatedProp) {
1370
+ const [relatedRow] = await db.from(relatedEntity.table).select("id").where([relatedProp.name, "=", row2.id]).first().execute();
1371
+ record.columns[prop.name].value = relatedRow?.id;
1372
+ }
1373
+ } else if (isRelationProp(prop)) {
1374
+ const relatedId = row2[`${prop.name}_id`];
1375
+ record.columns[prop.name].value = relatedId;
1376
+ if (relatedId) {
1377
+ record.belongsRecords.push(`${prop.with}#${relatedId}`);
1378
+ }
1379
+ if (!options?.singleRecord && relatedId) {
1380
+ const relatedEntity = EntityManager.get(prop.with);
1381
+ const [relatedRow] = await db.from(relatedEntity.table).selectAll().where(["id", "=", relatedId]).first().execute();
1382
+ if (relatedRow) {
1383
+ await create(relatedEntity, relatedRow);
1384
+ }
1385
+ }
1386
+ }
1387
+ }
1388
+ records.push(record);
1389
+ };
1390
+ await create(entity, row);
1391
+ return records;
1392
+ }
1393
+ async insertFixtures(dbName, _fixtures) {
1394
+ const fixtures = _2.uniqBy(_fixtures, (f) => f.fixtureId);
1395
+ this.relationGraph.buildGraph(fixtures);
1396
+ const insertionOrder = this.relationGraph.getInsertionOrder();
1397
+ const db = DB.getClient(dbName);
1398
+ await db.trx(async (trx) => {
1399
+ await trx.raw(`SET FOREIGN_KEY_CHECKS = 0`);
1400
+ for (const fixtureId of insertionOrder) {
1401
+ const fixture = fixtures.find((f) => f.fixtureId === fixtureId);
1402
+ const result = await this.insertFixture(trx, fixture);
1403
+ if (result.id !== fixture.id) {
1404
+ console.log(
1405
+ chalk2.yellow(
1406
+ `Unique constraint violation: ${fixture.entityId}#${fixture.id} -> ${fixture.entityId}#${result.id}`
1407
+ )
1408
+ );
1409
+ fixtures.forEach((f) => {
1410
+ Object.values(f.columns).forEach((column) => {
1411
+ if (column.prop.type === "relation" && column.prop.with === result.entityId && column.value === fixture.id) {
1412
+ column.value = result.id;
1413
+ }
1414
+ });
1415
+ });
1416
+ fixture.id = result.id;
1417
+ }
1418
+ }
1419
+ for (const fixtureId of insertionOrder) {
1420
+ const fixture = fixtures.find((f) => f.fixtureId === fixtureId);
1421
+ await this.handleManyToManyRelations(trx, fixture, fixtures);
1422
+ }
1423
+ await trx.raw(`SET FOREIGN_KEY_CHECKS = 1`);
1424
+ });
1425
+ const records = [];
1426
+ for await (const r of fixtures) {
1427
+ const entity = EntityManager.get(r.entityId);
1428
+ const [record] = await db.from(entity.table).selectAll().where(["id", "=", r.id]).first().execute();
1429
+ records.push({
1430
+ entityId: r.entityId,
1431
+ data: record
1432
+ });
1433
+ }
1434
+ return _2.uniqBy(records, (r) => `${r.entityId}#${r.data.id}`);
1435
+ }
1436
+ prepareInsertData(fixture) {
1437
+ const insertData = {};
1438
+ for (const [propName, column] of Object.entries(fixture.columns)) {
1439
+ if (isVirtualProp(column.prop)) {
1440
+ continue;
1441
+ }
1442
+ const prop = column.prop;
1443
+ if (!isRelationProp(prop)) {
1444
+ if (prop.type === "json") {
1445
+ insertData[propName] = JSON.stringify(column.value);
1446
+ } else {
1447
+ insertData[propName] = column.value;
1448
+ }
1449
+ } else if (isBelongsToOneRelationProp(prop) || isOneToOneRelationProp(prop) && prop.hasJoinColumn) {
1450
+ insertData[`${propName}_id`] = column.value;
1451
+ }
1452
+ }
1453
+ return insertData;
1454
+ }
1455
+ async insertFixture(db, fixture) {
1456
+ const insertData = this.prepareInsertData(fixture);
1457
+ const entity = EntityManager.get(fixture.entityId);
1458
+ try {
1459
+ const uniqueFound = await this.checkUniqueViolation(db, entity, fixture);
1460
+ if (uniqueFound) {
1461
+ return {
1462
+ entityId: fixture.entityId,
1463
+ id: uniqueFound.id
1464
+ };
1465
+ }
1466
+ const [found] = await db.from(entity.table).select("id").where(["id", "=", fixture.id]).first().execute();
1467
+ if (found && !fixture.override) {
1468
+ return {
1469
+ entityId: fixture.entityId,
1470
+ id: found.id
1471
+ };
1472
+ }
1473
+ await db.upsert(entity.table, [insertData]);
1474
+ return {
1475
+ entityId: fixture.entityId,
1476
+ id: fixture.id
1477
+ };
1478
+ } catch (err) {
1479
+ console.log(err);
1480
+ throw err;
1481
+ }
1482
+ }
1483
+ async handleManyToManyRelations(db, fixture, fixtures) {
1484
+ for (const [, column] of Object.entries(fixture.columns)) {
1485
+ const prop = column.prop;
1486
+ if (isManyToManyRelationProp(prop)) {
1487
+ const joinTable = prop.joinTable;
1488
+ const relatedIds = column.value;
1489
+ for (const relatedId of relatedIds) {
1490
+ if (!fixtures.find((f) => f.fixtureId === `${prop.with}#${relatedId}`)) {
1491
+ continue;
1492
+ }
1493
+ const entity = EntityManager.get(fixture.entityId);
1494
+ const relatedEntity = EntityManager.get(prop.with);
1495
+ if (!entity || !relatedEntity) {
1496
+ throw new Error(
1497
+ `Entity not found: ${fixture.entityId}, ${prop.with}`
1498
+ );
1499
+ }
1500
+ const [found] = await db.from(joinTable).select("id").where([
1501
+ [`${inflection2.singularize(entity.table)}_id`, "=", fixture.id],
1502
+ [
1503
+ `${inflection2.singularize(relatedEntity.table)}_id`,
1504
+ "=",
1505
+ relatedId
1506
+ ]
1507
+ ]).first().execute();
1508
+ if (found) {
1509
+ continue;
1510
+ }
1511
+ const newIds = await db.insert(joinTable, [
1512
+ {
1513
+ [`${inflection2.singularize(entity.table)}_id`]: fixture.id,
1514
+ [`${inflection2.singularize(relatedEntity.table)}_id`]: relatedId
1515
+ }
1516
+ ]);
1517
+ console.log(
1518
+ chalk2.green(
1519
+ `Inserted into ${joinTable}: ${entity.table}(${fixture.id}) - ${relatedEntity.table}(${relatedId}) ID: ${newIds}`
1520
+ )
1521
+ );
1522
+ }
1523
+ }
1524
+ }
1525
+ }
1526
+ async addFixtureLoader(code) {
1527
+ const path2 = Sonamu.apiRootPath + "/src/testing/fixture.ts";
1528
+ let content = readFileSync(path2).toString();
1529
+ const fixtureLoaderStart = content.indexOf("const fixtureLoader = {");
1530
+ const fixtureLoaderEnd = content.indexOf("};", fixtureLoaderStart);
1531
+ if (fixtureLoaderStart !== -1 && fixtureLoaderEnd !== -1) {
1532
+ const newContent = content.slice(0, fixtureLoaderEnd) + " " + code + "\n" + content.slice(fixtureLoaderEnd);
1533
+ writeFileSync(path2, newContent);
1534
+ } else {
1535
+ throw new Error("Failed to find fixtureLoader in fixture.ts");
1536
+ }
1537
+ }
1538
+ // 해당 픽스쳐의 값으로 유니크 제약에 위배되는 레코드가 있는지 확인
1539
+ async checkUniqueViolation(db, entity, fixture) {
1540
+ const _uniqueIndexes = entity.indexes.filter((i) => i.type === "unique");
1541
+ const uniqueIndexes = _uniqueIndexes.filter(
1542
+ (index) => index.columns.every((column) => !column.startsWith(`${entity.table}__`))
1543
+ );
1544
+ if (uniqueIndexes.length === 0) {
1545
+ return null;
1546
+ }
1547
+ let uniqueQuery = db.from(entity.table).selectAll();
1548
+ const whereClauses = uniqueIndexes.map((index) => {
1549
+ const containsNull = index.columns.some((column) => {
1550
+ const field = column.split("_id")[0];
1551
+ return fixture.columns[field].value === null;
1552
+ });
1553
+ if (containsNull) {
1554
+ return;
1555
+ }
1556
+ return index.columns.map((c) => {
1557
+ const field = c.split("_id")[0];
1558
+ if (Array.isArray(fixture.columns[field].value)) {
1559
+ return [c, "in", fixture.columns[field].value];
1560
+ } else {
1561
+ return [c, "=", fixture.columns[field].value];
1562
+ }
1563
+ });
1564
+ }).filter(Boolean);
1565
+ for (const clauses of whereClauses) {
1566
+ uniqueQuery = uniqueQuery.orWhere(clauses);
1567
+ }
1568
+ const [uniqueFound] = await uniqueQuery.execute();
1569
+ return uniqueFound;
1570
+ }
1571
+ };
1572
+ var FixtureManager = new FixtureManagerClass();
1573
+
1574
+ export {
1575
+ Migrator,
1576
+ FixtureManagerClass,
1577
+ FixtureManager
1578
+ };
1579
+ //# sourceMappingURL=chunk-6SP5N5ND.mjs.map