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.
Files changed (85) hide show
  1. package/.pnp.cjs +11 -0
  2. package/dist/base-model-BzMJ2E_I.d.mts +43 -0
  3. package/dist/base-model-CWRKUX49.d.ts +43 -0
  4. package/dist/bin/cli.js +118 -89
  5. package/dist/bin/cli.js.map +1 -1
  6. package/dist/bin/cli.mjs +74 -45
  7. package/dist/bin/cli.mjs.map +1 -1
  8. package/dist/chunk-FLPD24HS.mjs +231 -0
  9. package/dist/chunk-FLPD24HS.mjs.map +1 -0
  10. package/dist/chunk-I2MMJRJN.mjs +1550 -0
  11. package/dist/chunk-I2MMJRJN.mjs.map +1 -0
  12. package/dist/{chunk-L4KELCY7.mjs → chunk-PP2PSSAG.mjs} +5241 -5571
  13. package/dist/chunk-PP2PSSAG.mjs.map +1 -0
  14. package/dist/chunk-QK5XXJUX.mjs +280 -0
  15. package/dist/chunk-QK5XXJUX.mjs.map +1 -0
  16. package/dist/chunk-U636LQJJ.js +231 -0
  17. package/dist/chunk-U636LQJJ.js.map +1 -0
  18. package/dist/chunk-W7KDVJLQ.js +280 -0
  19. package/dist/chunk-W7KDVJLQ.js.map +1 -0
  20. package/dist/{chunk-JOHF7PK4.js → chunk-XT6LHCX5.js} +5292 -5622
  21. package/dist/chunk-XT6LHCX5.js.map +1 -0
  22. package/dist/chunk-Z2P7XTXE.js +1550 -0
  23. package/dist/chunk-Z2P7XTXE.js.map +1 -0
  24. package/dist/database/drivers/knex/base-model.d.mts +16 -0
  25. package/dist/database/drivers/knex/base-model.d.ts +16 -0
  26. package/dist/database/drivers/knex/base-model.js +55 -0
  27. package/dist/database/drivers/knex/base-model.js.map +1 -0
  28. package/dist/database/drivers/knex/base-model.mjs +56 -0
  29. package/dist/database/drivers/knex/base-model.mjs.map +1 -0
  30. package/dist/database/drivers/kysely/base-model.d.mts +22 -0
  31. package/dist/database/drivers/kysely/base-model.d.ts +22 -0
  32. package/dist/database/drivers/kysely/base-model.js +64 -0
  33. package/dist/database/drivers/kysely/base-model.js.map +1 -0
  34. package/dist/database/drivers/kysely/base-model.mjs +65 -0
  35. package/dist/database/drivers/kysely/base-model.mjs.map +1 -0
  36. package/dist/index.d.mts +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 +23 -2
  46. package/src/api/caster.ts +6 -0
  47. package/src/api/code-converters.ts +3 -1
  48. package/src/api/sonamu.ts +41 -22
  49. package/src/bin/cli.ts +78 -46
  50. package/src/database/_batch_update.ts +16 -11
  51. package/src/database/base-model.abstract.ts +97 -0
  52. package/src/database/base-model.ts +214 -280
  53. package/src/database/code-generator.ts +72 -0
  54. package/src/database/db.abstract.ts +75 -0
  55. package/src/database/db.ts +21 -82
  56. package/src/database/drivers/knex/base-model.ts +55 -0
  57. package/src/database/drivers/knex/client.ts +209 -0
  58. package/src/database/drivers/knex/db.ts +227 -0
  59. package/src/database/drivers/knex/generator.ts +659 -0
  60. package/src/database/drivers/kysely/base-model.ts +89 -0
  61. package/src/database/drivers/kysely/client.ts +309 -0
  62. package/src/database/drivers/kysely/db.ts +238 -0
  63. package/src/database/drivers/kysely/generator.ts +714 -0
  64. package/src/database/types.ts +117 -0
  65. package/src/database/upsert-builder.ts +31 -18
  66. package/src/entity/entity-manager.ts +9 -5
  67. package/src/entity/entity-utils.ts +1 -1
  68. package/src/entity/entity.ts +9 -13
  69. package/src/entity/migrator.ts +98 -693
  70. package/src/index.ts +1 -1
  71. package/src/syncer/syncer.ts +103 -56
  72. package/src/templates/generated_http.template.ts +14 -0
  73. package/src/templates/kysely_types.template.ts +205 -0
  74. package/src/templates/model.template.ts +2 -139
  75. package/src/templates/service.template.ts +3 -1
  76. package/src/testing/_relation-graph.ts +111 -0
  77. package/src/testing/fixture-manager.ts +216 -332
  78. package/src/types/types.ts +56 -6
  79. package/src/utils/utils.ts +56 -4
  80. package/src/utils/zod-error.ts +189 -0
  81. package/tsconfig.json +2 -2
  82. package/tsup.config.js +11 -10
  83. package/dist/chunk-JOHF7PK4.js.map +0 -1
  84. package/dist/chunk-L4KELCY7.mjs.map +0 -1
  85. /package/src/database/{knex-plugins → drivers/knex/plugins}/knex-on-duplicate-update.ts +0 -0
@@ -0,0 +1,1550 @@
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-PP2PSSAG.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 is an 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.getMigrationSetFromMD(entity);
489
+ });
490
+ const joinTables = _.uniqBy(
491
+ entitySetsWithJoinTable.map((entitySet) => entitySet.joinTables).flat(),
492
+ (joinTable) => {
493
+ return joinTable.table;
494
+ }
495
+ );
496
+ const entitySets = [
497
+ ...entitySetsWithJoinTable,
498
+ ...joinTables
499
+ ];
500
+ let codes = (await Promise.all(
501
+ entitySets.map(async (entitySet) => {
502
+ const dbSet = await this.getMigrationSetFromDB(
503
+ compareDB,
504
+ entitySet.table
505
+ );
506
+ if (dbSet === null) {
507
+ return [
508
+ await DB.generator.generateCreateCode_ColumnAndIndexes(
509
+ entitySet.table,
510
+ entitySet.columns,
511
+ entitySet.indexes
512
+ ),
513
+ ...await DB.generator.generateCreateCode_Foreign(
514
+ entitySet.table,
515
+ entitySet.foreigns
516
+ )
517
+ ];
518
+ }
519
+ const alterCodes = await Promise.all(
520
+ ["columnsAndIndexes", "foreigns"].map((key) => {
521
+ if (key === "columnsAndIndexes") {
522
+ const replaceColumnDefaultTo = (col) => {
523
+ if (col.type === "float" && col.defaultTo && String(col.defaultTo).includes('"') === false) {
524
+ col.defaultTo = `"${Number(col.defaultTo).toFixed(
525
+ col.scale ?? 2
526
+ )}"`;
527
+ }
528
+ if (col.type === "string" && col.defaultTo === "") {
529
+ col.defaultTo = '""';
530
+ }
531
+ return col;
532
+ };
533
+ const entityColumns = _.sortBy(
534
+ entitySet.columns,
535
+ (a) => a.name
536
+ ).map(replaceColumnDefaultTo);
537
+ const dbColumns = _.sortBy(dbSet.columns, (a) => a.name).map(
538
+ replaceColumnDefaultTo
539
+ );
540
+ const entityIndexes = _.sortBy(
541
+ entitySet.indexes,
542
+ (a) => [
543
+ a.type,
544
+ ...a.columns.sort((c1, c2) => c1 > c2 ? 1 : -1)
545
+ ].join("-")
546
+ );
547
+ const dbIndexes = _.sortBy(
548
+ dbSet.indexes,
549
+ (a) => [
550
+ a.type,
551
+ ...a.columns.sort((c1, c2) => c1 > c2 ? 1 : -1)
552
+ ].join("-")
553
+ );
554
+ const isEqualColumns = equal(entityColumns, dbColumns);
555
+ const isEqualIndexes = equal(entityIndexes, dbIndexes);
556
+ if (isEqualColumns && isEqualIndexes) {
557
+ return null;
558
+ } else {
559
+ return DB.generator.generateAlterCode_ColumnAndIndexes(
560
+ entitySet.table,
561
+ entityColumns,
562
+ entityIndexes,
563
+ dbColumns,
564
+ dbIndexes
565
+ );
566
+ }
567
+ } else {
568
+ const replaceNoActionOnMySQL = (f) => {
569
+ const { onDelete, onUpdate } = f;
570
+ return {
571
+ ...f,
572
+ onUpdate: onUpdate === "RESTRICT" ? "NO ACTION" : onUpdate,
573
+ onDelete: onDelete === "RESTRICT" ? "NO ACTION" : onDelete
574
+ };
575
+ };
576
+ const entityForeigns = _.sortBy(
577
+ entitySet.foreigns,
578
+ (a) => [a.to, ...a.columns].join("-")
579
+ ).map((f) => replaceNoActionOnMySQL(f));
580
+ const dbForeigns = _.sortBy(
581
+ dbSet.foreigns,
582
+ (a) => [a.to, ...a.columns].join("-")
583
+ ).map((f) => replaceNoActionOnMySQL(f));
584
+ if (equal(entityForeigns, dbForeigns) === false) {
585
+ return DB.generator.generateAlterCode_Foreigns(
586
+ entitySet.table,
587
+ entityForeigns,
588
+ dbForeigns
589
+ );
590
+ }
591
+ }
592
+ return null;
593
+ })
594
+ );
595
+ if (alterCodes.every((alterCode) => alterCode === null)) {
596
+ return null;
597
+ } else {
598
+ return alterCodes.filter((alterCode) => alterCode !== null).flat();
599
+ }
600
+ })
601
+ )).flat().filter((code) => code !== null);
602
+ codes.sort((codeA, codeB) => {
603
+ if (codeA.type === "foreign" && codeB.type == "normal") {
604
+ return 1;
605
+ } else if (codeA.type === "normal" && codeB.type === "foreign") {
606
+ return -1;
607
+ } else {
608
+ return 0;
609
+ }
610
+ });
611
+ return codes;
612
+ }
613
+ /*
614
+ 기존 테이블 정보 읽어서 MigrationSet 형식으로 리턴
615
+ */
616
+ async getMigrationSetFromDB(compareDB, table) {
617
+ let dbColumns, dbIndexes, dbForeigns;
618
+ try {
619
+ [dbColumns, dbIndexes, dbForeigns] = await this.readTable(
620
+ compareDB,
621
+ table
622
+ );
623
+ } catch (e) {
624
+ if (isKnexError(e) && e.code === "ER_NO_SUCH_TABLE") {
625
+ return null;
626
+ }
627
+ console.error(e);
628
+ return null;
629
+ }
630
+ const columns = dbColumns.map((dbColumn) => {
631
+ const dbColType = this.resolveDBColType(dbColumn.Type, dbColumn.Field);
632
+ return {
633
+ name: dbColumn.Field,
634
+ nullable: dbColumn.Null !== "NO",
635
+ ...dbColType,
636
+ ...(() => {
637
+ if (dbColumn.Default !== null) {
638
+ return {
639
+ defaultTo: dbColumn.Default
640
+ };
641
+ }
642
+ return {};
643
+ })()
644
+ };
645
+ });
646
+ const dbIndexesGroup = _.groupBy(
647
+ dbIndexes.filter(
648
+ (dbIndex) => dbIndex.Key_name !== "PRIMARY" && !dbForeigns.find(
649
+ (dbForeign) => dbForeign.keyName === dbIndex.Key_name
650
+ )
651
+ ),
652
+ (dbIndex) => dbIndex.Key_name
653
+ );
654
+ const indexes = Object.keys(dbIndexesGroup).map(
655
+ (keyName) => {
656
+ const currentIndexes = dbIndexesGroup[keyName];
657
+ return {
658
+ type: currentIndexes[0].Non_unique === 1 ? "index" : "unique",
659
+ columns: currentIndexes.map(
660
+ (currentIndex) => currentIndex.Column_name
661
+ )
662
+ };
663
+ }
664
+ );
665
+ const foreigns = dbForeigns.map((dbForeign) => {
666
+ return {
667
+ columns: [dbForeign.from],
668
+ to: `${dbForeign.referencesTable}.${dbForeign.referencesField}`,
669
+ onUpdate: dbForeign.onUpdate,
670
+ onDelete: dbForeign.onDelete
671
+ };
672
+ });
673
+ return {
674
+ table,
675
+ columns,
676
+ indexes,
677
+ foreigns
678
+ };
679
+ }
680
+ resolveDBColType(colType, colField) {
681
+ let [rawType, unsigned] = colType.split(" ");
682
+ const matched = rawType.match(/\(([0-9]+)\)/);
683
+ let length;
684
+ if (matched !== null && matched[1]) {
685
+ rawType = rawType.replace(/\(([0-9]+)\)/, "");
686
+ length = parseInt(matched[1]);
687
+ }
688
+ if (rawType === "char" && colField === "uuid") {
689
+ return {
690
+ type: "uuid"
691
+ };
692
+ }
693
+ switch (rawType) {
694
+ case "int":
695
+ return {
696
+ type: "integer",
697
+ unsigned: unsigned === "unsigned"
698
+ };
699
+ case "varchar":
700
+ return {
701
+ type: "string",
702
+ ...length !== void 0 && {
703
+ length
704
+ }
705
+ };
706
+ case "text":
707
+ case "mediumtext":
708
+ case "longtext":
709
+ case "timestamp":
710
+ case "json":
711
+ case "date":
712
+ case "time":
713
+ return {
714
+ type: rawType
715
+ };
716
+ case "datetime":
717
+ return {
718
+ type: "datetime"
719
+ };
720
+ case "tinyint":
721
+ return {
722
+ type: "boolean"
723
+ };
724
+ default:
725
+ if (rawType.startsWith("decimal")) {
726
+ const [, precision, scale] = rawType.match(/decimal\(([0-9]+),([0-9]+)\)/) ?? [];
727
+ return {
728
+ type: "decimal",
729
+ precision: parseInt(precision),
730
+ scale: parseInt(scale),
731
+ ...unsigned === "unsigned" && {
732
+ unsigned: true
733
+ }
734
+ };
735
+ } else if (rawType.startsWith("float")) {
736
+ const [, precision, scale] = rawType.match(/float\(([0-9]+),([0-9]+)\)/) ?? [];
737
+ return {
738
+ type: "float",
739
+ precision: parseInt(precision),
740
+ scale: parseInt(scale),
741
+ ...unsigned === "unsigned" && {
742
+ unsigned: true
743
+ }
744
+ };
745
+ }
746
+ throw new Error(`resolve \uBD88\uAC00\uB2A5\uD55C DB\uCEEC\uB7FC \uD0C0\uC785 ${colType} ${rawType}`);
747
+ }
748
+ }
749
+ /*
750
+ 기존 테이블 읽어서 cols, indexes 반환
751
+ */
752
+ async readTable(compareDB, tableName) {
753
+ try {
754
+ const _cols = await compareDB.raw(
755
+ `SHOW FIELDS FROM ${tableName}`
756
+ );
757
+ const cols = _cols.map((col) => ({
758
+ ...col,
759
+ // Default 값은 숫자나 MySQL Expression이 아닌 경우 ""로 감싸줌
760
+ ...col.Default !== null && {
761
+ Default: col.Default.replace(/[0-9]+/g, "").length > 0 && col.Extra !== "DEFAULT_GENERATED" ? `"${col.Default}"` : col.Default
762
+ }
763
+ }));
764
+ const indexes = await compareDB.raw(
765
+ `SHOW INDEX FROM ${tableName}`
766
+ );
767
+ const [row] = await compareDB.raw(`SHOW CREATE TABLE ${tableName}`);
768
+ const ddl = row["Create Table"];
769
+ const matched = ddl.match(/CONSTRAINT .+/g);
770
+ const foreignKeys = (matched ?? []).map((line) => {
771
+ const matched2 = line.match(
772
+ /CONSTRAINT `(.+)` FOREIGN KEY \(`(.+)`\) REFERENCES `(.+)` \(`(.+)`\)( ON [A-Z ]+)*/
773
+ );
774
+ if (!matched2) {
775
+ throw new Error(`\uC778\uC2DD\uD560 \uC218 \uC5C6\uB294 FOREIGN KEY CONSTRAINT ${line}`);
776
+ }
777
+ const [, keyName, from, referencesTable, referencesField, onClause] = matched2;
778
+ const [onUpdateFull, _onUpdate] = (onClause ?? "").match(/ON UPDATE ([A-Z ]+)$/) ?? [];
779
+ const onUpdate = _onUpdate ?? "NO ACTION";
780
+ const onDelete = (onClause ?? "").replace(onUpdateFull ?? "", "").match(/ON DELETE ([A-Z ]+)/)?.[1]?.trim() ?? "NO ACTION";
781
+ return {
782
+ keyName,
783
+ from,
784
+ referencesTable,
785
+ referencesField,
786
+ onDelete,
787
+ onUpdate
788
+ };
789
+ });
790
+ return [cols, indexes, foreignKeys];
791
+ } catch (e) {
792
+ throw e;
793
+ }
794
+ }
795
+ /*
796
+ MD 내용 읽어서 MigrationSetAndJoinTable 추출
797
+ */
798
+ getMigrationSetFromMD(entity) {
799
+ const migrationSet = entity.props.reduce(
800
+ (r, prop) => {
801
+ if (isVirtualProp(prop)) {
802
+ return r;
803
+ }
804
+ if (isHasManyRelationProp(prop)) {
805
+ return r;
806
+ }
807
+ if (!isRelationProp(prop)) {
808
+ let type;
809
+ if (isTextProp(prop)) {
810
+ type = prop.textType;
811
+ } else if (isEnumProp(prop)) {
812
+ type = "string";
813
+ } else {
814
+ type = prop.type;
815
+ }
816
+ const column = {
817
+ name: prop.name,
818
+ type,
819
+ ...isIntegerProp(prop) && { unsigned: prop.unsigned === true },
820
+ ...(isStringProp(prop) || isEnumProp(prop)) && {
821
+ length: prop.length
822
+ },
823
+ nullable: prop.nullable === true,
824
+ ...(() => {
825
+ if (prop.dbDefault !== void 0) {
826
+ return {
827
+ defaultTo: prop.dbDefault
828
+ };
829
+ }
830
+ return {};
831
+ })(),
832
+ // FIXME: float(N, M) deprecated
833
+ // Decimal, Float 타입의 경우 precision, scale 추가
834
+ ...(isDecimalProp(prop) || isFloatProp(prop)) && {
835
+ precision: prop.precision ?? 8,
836
+ scale: prop.scale ?? 2
837
+ }
838
+ };
839
+ r.columns.push(column);
840
+ }
841
+ if (isManyToManyRelationProp(prop)) {
842
+ const relMd = EntityManager.get(prop.with);
843
+ const [table1, table2] = prop.joinTable.split("__");
844
+ const join = {
845
+ from: `${entity.table}.id`,
846
+ through: {
847
+ from: `${prop.joinTable}.${inflection.singularize(table1)}_id`,
848
+ to: `${prop.joinTable}.${inflection.singularize(table2)}_id`,
849
+ onUpdate: prop.onUpdate,
850
+ onDelete: prop.onDelete
851
+ },
852
+ to: `${relMd.table}.id`
853
+ };
854
+ const through = join.through;
855
+ const fields = [through.from, through.to];
856
+ r.joinTables.push({
857
+ table: through.from.split(".")[0],
858
+ indexes: [
859
+ {
860
+ type: "unique",
861
+ columns: ["uuid"]
862
+ },
863
+ // 조인 테이블에 걸린 인덱스 찾아와서 연결
864
+ ...entity.indexes.filter(
865
+ (index) => index.columns.find(
866
+ (col) => col.includes(prop.joinTable + ".")
867
+ )
868
+ ).map((index) => ({
869
+ ...index,
870
+ columns: index.columns.map(
871
+ (col) => col.replace(prop.joinTable + ".", "")
872
+ )
873
+ }))
874
+ ],
875
+ columns: [
876
+ {
877
+ name: "id",
878
+ type: "integer",
879
+ nullable: false,
880
+ unsigned: true
881
+ },
882
+ ...fields.map((field) => {
883
+ return {
884
+ name: field.split(".")[1],
885
+ type: "integer",
886
+ nullable: false,
887
+ unsigned: true
888
+ };
889
+ }),
890
+ {
891
+ name: "uuid",
892
+ nullable: true,
893
+ type: "uuid"
894
+ }
895
+ ],
896
+ foreigns: fields.map((field) => {
897
+ return {
898
+ columns: [field.split(".")[1]],
899
+ to: through.to.includes(field) ? join.to : join.from,
900
+ onUpdate: through.onUpdate,
901
+ onDelete: through.onDelete
902
+ };
903
+ })
904
+ });
905
+ return r;
906
+ } else if (isBelongsToOneRelationProp(prop) || isOneToOneRelationProp(prop) && prop.hasJoinColumn) {
907
+ const idColumnName = prop.name + "_id";
908
+ r.columns.push({
909
+ name: idColumnName,
910
+ type: "integer",
911
+ unsigned: true,
912
+ nullable: prop.nullable ?? false
913
+ });
914
+ r.foreigns.push({
915
+ columns: [idColumnName],
916
+ to: `${inflection.underscore(inflection.pluralize(prop.with)).toLowerCase()}.id`,
917
+ onUpdate: prop.onUpdate,
918
+ onDelete: prop.onDelete
919
+ });
920
+ }
921
+ return r;
922
+ },
923
+ {
924
+ table: entity.table,
925
+ columns: [],
926
+ indexes: [],
927
+ foreigns: [],
928
+ joinTables: []
929
+ }
930
+ );
931
+ migrationSet.indexes = entity.indexes.filter(
932
+ (index) => index.columns.find((col) => col.includes(".") === false)
933
+ );
934
+ migrationSet.columns = migrationSet.columns.concat({
935
+ name: "uuid",
936
+ nullable: true,
937
+ type: "uuid"
938
+ });
939
+ migrationSet.indexes = migrationSet.indexes.concat({
940
+ type: "unique",
941
+ columns: ["uuid"]
942
+ });
943
+ return migrationSet;
944
+ }
945
+ /*
946
+ 마이그레이션 컬럼 배열 비교용 코드
947
+ */
948
+ showMigrationSet(which, migrationSet) {
949
+ const { columns, indexes, foreigns } = migrationSet;
950
+ const styledChalk = which === "MD" ? chalk.bgGreen.black : chalk.bgBlue.black;
951
+ console.log(
952
+ styledChalk(
953
+ `${which} ${migrationSet.table} Columns `
954
+ )
955
+ );
956
+ console.table(
957
+ columns.map((column) => {
958
+ return {
959
+ ..._.pick(column, [
960
+ "name",
961
+ "type",
962
+ "nullable",
963
+ "unsigned",
964
+ "length",
965
+ "defaultTo",
966
+ "precision",
967
+ "scale"
968
+ ])
969
+ };
970
+ }),
971
+ [
972
+ "name",
973
+ "type",
974
+ "nullable",
975
+ "unsigned",
976
+ "length",
977
+ "defaultTo",
978
+ "precision",
979
+ "scale"
980
+ ]
981
+ );
982
+ if (indexes.length > 0) {
983
+ console.log(
984
+ styledChalk(
985
+ `${which} ${migrationSet.table} Indexes `
986
+ )
987
+ );
988
+ console.table(
989
+ indexes.map((index) => {
990
+ return {
991
+ ..._.pick(index, ["type", "columns", "name"])
992
+ };
993
+ })
994
+ );
995
+ }
996
+ if (foreigns.length > 0) {
997
+ console.log(
998
+ chalk.bgMagenta.black(
999
+ `${which} ${migrationSet.table} Foreigns `
1000
+ )
1001
+ );
1002
+ console.table(
1003
+ foreigns.map((foreign) => {
1004
+ return {
1005
+ ..._.pick(foreign, ["columns", "to", "onUpdate", "onDelete"])
1006
+ };
1007
+ })
1008
+ );
1009
+ }
1010
+ }
1011
+ async destroy() {
1012
+ await Promise.all(
1013
+ this.targets.apply.map((db) => {
1014
+ return db.destroy();
1015
+ })
1016
+ );
1017
+ }
1018
+ };
1019
+
1020
+ // src/testing/fixture-manager.ts
1021
+ import chalk2 from "chalk";
1022
+ import _2 from "lodash";
1023
+ import inflection2 from "inflection";
1024
+ import { readFileSync, writeFileSync } from "fs";
1025
+
1026
+ // src/testing/_relation-graph.ts
1027
+ var RelationGraph = class {
1028
+ graph = /* @__PURE__ */ new Map();
1029
+ buildGraph(fixtures) {
1030
+ this.graph.clear();
1031
+ for (const fixture of fixtures) {
1032
+ this.graph.set(fixture.fixtureId, {
1033
+ fixtureId: fixture.fixtureId,
1034
+ entityId: fixture.entityId,
1035
+ related: /* @__PURE__ */ new Set()
1036
+ });
1037
+ }
1038
+ for (const fixture of fixtures) {
1039
+ const node = this.graph.get(fixture.fixtureId);
1040
+ for (const [, column] of Object.entries(fixture.columns)) {
1041
+ const prop = column.prop;
1042
+ if (isRelationProp(prop)) {
1043
+ if (isBelongsToOneRelationProp(prop) || isOneToOneRelationProp(prop) && prop.hasJoinColumn) {
1044
+ const relatedFixtureId = `${prop.with}#${column.value}`;
1045
+ if (this.graph.has(relatedFixtureId)) {
1046
+ node.related.add(relatedFixtureId);
1047
+ }
1048
+ } else if (isManyToManyRelationProp(prop)) {
1049
+ const relatedIds = column.value;
1050
+ for (const relatedId of relatedIds) {
1051
+ const relatedFixtureId = `${prop.with}#${relatedId}`;
1052
+ if (this.graph.has(relatedFixtureId)) {
1053
+ node.related.add(relatedFixtureId);
1054
+ this.graph.get(relatedFixtureId).related.add(fixture.fixtureId);
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+ }
1062
+ getInsertionOrder() {
1063
+ const visited = /* @__PURE__ */ new Set();
1064
+ const order = [];
1065
+ const tempVisited = /* @__PURE__ */ new Set();
1066
+ const visit = (fixtureId) => {
1067
+ if (visited.has(fixtureId)) return;
1068
+ if (tempVisited.has(fixtureId)) {
1069
+ console.warn(`Circular dependency detected involving: ${fixtureId}`);
1070
+ return;
1071
+ }
1072
+ tempVisited.add(fixtureId);
1073
+ const node = this.graph.get(fixtureId);
1074
+ const entity = EntityManager.get(node.entityId);
1075
+ for (const depId of node.related) {
1076
+ const depNode = this.graph.get(depId);
1077
+ const relationProp = entity.props.find(
1078
+ (prop) => isRelationProp(prop) && (isBelongsToOneRelationProp(prop) || isOneToOneRelationProp(prop) && prop.hasJoinColumn) && prop.with === depNode.entityId
1079
+ );
1080
+ if (relationProp && !relationProp.nullable) {
1081
+ visit(depId);
1082
+ }
1083
+ }
1084
+ tempVisited.delete(fixtureId);
1085
+ visited.add(fixtureId);
1086
+ order.push(fixtureId);
1087
+ };
1088
+ for (const fixtureId of this.graph.keys()) {
1089
+ visit(fixtureId);
1090
+ }
1091
+ for (const fixtureId of this.graph.keys()) {
1092
+ if (!visited.has(fixtureId)) {
1093
+ order.push(fixtureId);
1094
+ }
1095
+ }
1096
+ return order;
1097
+ }
1098
+ };
1099
+
1100
+ // src/testing/fixture-manager.ts
1101
+ var FixtureManagerClass = class {
1102
+ relationGraph = new RelationGraph();
1103
+ init() {
1104
+ DB.testInit();
1105
+ }
1106
+ async cleanAndSeed(usingTables) {
1107
+ const tableNames = await (async () => {
1108
+ if (usingTables) {
1109
+ return usingTables;
1110
+ }
1111
+ const tables = await DB.tdb.raw(
1112
+ `SHOW TABLE STATUS WHERE Engine IS NOT NULL`
1113
+ );
1114
+ return tables.map((tableInfo) => tableInfo["Name"]);
1115
+ })();
1116
+ await DB.tdb.raw(`SET FOREIGN_KEY_CHECKS = 0`);
1117
+ for await (let tableName of tableNames) {
1118
+ if (tableName == "migrations") {
1119
+ continue;
1120
+ }
1121
+ const [fdbChecksumRow] = await DB.fdb.raw(
1122
+ `CHECKSUM TABLE ${tableName}`
1123
+ );
1124
+ const fdbChecksum = fdbChecksumRow["Checksum"];
1125
+ const [tdbChecksumRow] = await DB.tdb.raw(
1126
+ `CHECKSUM TABLE ${tableName}`
1127
+ );
1128
+ const tdbChecksum = tdbChecksumRow["Checksum"];
1129
+ if (fdbChecksum !== tdbChecksum) {
1130
+ await DB.tdb.truncate(tableName);
1131
+ const rawQuery = `INSERT INTO ${DB.connectionInfo.test.database}.${tableName}
1132
+ SELECT * FROM ${DB.connectionInfo.fixture_local.database}.${tableName}`;
1133
+ await DB.tdb.raw(rawQuery);
1134
+ }
1135
+ }
1136
+ await DB.tdb.raw(`SET FOREIGN_KEY_CHECKS = 1`);
1137
+ }
1138
+ async getChecksum(db, tableName) {
1139
+ const [checksumRow] = await db.raw(
1140
+ `CHECKSUM TABLE ${tableName}`
1141
+ );
1142
+ return checksumRow.Checksum;
1143
+ }
1144
+ async sync() {
1145
+ const frdb = DB.getClient("fixture_remote");
1146
+ const tables = await DB.fdb.raw(
1147
+ "SHOW TABLE STATUS WHERE Engine IS NOT NULL"
1148
+ );
1149
+ const tableNames = tables.map(
1150
+ (table) => table.Name
1151
+ );
1152
+ console.log(chalk2.magenta("SYNC..."));
1153
+ await Promise.all(
1154
+ tableNames.map(async (tableName) => {
1155
+ if (tableName.startsWith(DB.migrationTable)) {
1156
+ return;
1157
+ }
1158
+ const remoteChecksum = await this.getChecksum(frdb, tableName);
1159
+ const localChecksum = await this.getChecksum(DB.fdb, tableName);
1160
+ if (remoteChecksum !== localChecksum) {
1161
+ await DB.fdb.trx(async (transaction) => {
1162
+ await transaction.raw(`SET FOREIGN_KEY_CHECKS = 0`);
1163
+ await transaction.truncate(tableName);
1164
+ const rows = await frdb.raw(`SELECT * FROM ${tableName}`);
1165
+ if (rows.length === 0) {
1166
+ return;
1167
+ }
1168
+ console.log(chalk2.blue(tableName), rows.length);
1169
+ await transaction.raw(
1170
+ `INSERT INTO ${tableName} (${Object.keys(rows[0]).join(
1171
+ ","
1172
+ )}) VALUES ?`,
1173
+ [rows.map((row) => Object.values(row))]
1174
+ );
1175
+ console.log("OK");
1176
+ await transaction.raw(`SET FOREIGN_KEY_CHECKS = 1`);
1177
+ });
1178
+ }
1179
+ })
1180
+ );
1181
+ console.log(chalk2.magenta("DONE!"));
1182
+ await frdb.destroy();
1183
+ }
1184
+ async importFixture(entityId, ids) {
1185
+ const queries = _2.uniq(
1186
+ (await Promise.all(
1187
+ ids.map(async (id) => {
1188
+ return await this.getImportQueries(entityId, "id", id);
1189
+ })
1190
+ )).flat()
1191
+ );
1192
+ const wdb = DB.toClient(DB.getDB("w"));
1193
+ for (let query of queries) {
1194
+ const [rsh] = await wdb.raw(query);
1195
+ console.log({
1196
+ query,
1197
+ info: rsh.info
1198
+ });
1199
+ }
1200
+ }
1201
+ async getImportQueries(entityId, field, id) {
1202
+ console.log({ entityId, field, id });
1203
+ const entity = EntityManager.get(entityId);
1204
+ const wdb = DB.toClient(DB.getDB("w"));
1205
+ const [row] = await wdb.raw(
1206
+ `SELECT * FROM ${entity.table} WHERE ${field} = ${id} LIMIT 1`
1207
+ );
1208
+ if (row === void 0) {
1209
+ throw new Error(`${entityId}#${id} row\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
1210
+ }
1211
+ const fixtureDatabase = DB.connectionInfo.fixture_remote.database;
1212
+ const realDatabase = DB.connectionInfo.production_master.database;
1213
+ const selfQuery = `INSERT IGNORE INTO \`${fixtureDatabase}\`.\`${entity.table}\` (SELECT * FROM \`${realDatabase}\`.\`${entity.table}\` WHERE \`id\` = ${id})`;
1214
+ const args = Object.entries(entity.relations).filter(
1215
+ ([, relation]) => isBelongsToOneRelationProp(relation) || isOneToOneRelationProp(relation) && relation.customJoinClause === void 0
1216
+ ).map(([, relation]) => {
1217
+ let field2;
1218
+ let id2;
1219
+ if (isOneToOneRelationProp(relation) && !relation.hasJoinColumn) {
1220
+ field2 = `${relation.name}_id`;
1221
+ id2 = row["id"];
1222
+ } else {
1223
+ field2 = "id";
1224
+ id2 = row[`${relation.name}_id`];
1225
+ }
1226
+ return {
1227
+ entityId: relation.with,
1228
+ field: field2,
1229
+ id: id2
1230
+ };
1231
+ }).filter((arg) => arg.id !== null);
1232
+ const relQueries = await Promise.all(
1233
+ args.map(async (args2) => {
1234
+ return this.getImportQueries(args2.entityId, args2.field, args2.id);
1235
+ })
1236
+ );
1237
+ return [..._2.uniq(relQueries.reverse().flat()), selfQuery];
1238
+ }
1239
+ async destory() {
1240
+ await DB.testDestroy();
1241
+ await DB.destroy();
1242
+ }
1243
+ async getFixtures(sourceDBName, targetDBName, searchOptions) {
1244
+ const sourceDB = DB.getClient(sourceDBName);
1245
+ const targetDB = DB.getClient(targetDBName);
1246
+ const { entityId, field, value, searchType } = searchOptions;
1247
+ const entity = EntityManager.get(entityId);
1248
+ const column = entity.props.find((prop) => prop.name === field)?.type === "relation" ? `${field}_id` : field;
1249
+ let query = sourceDB.from(entity.table).selectAll();
1250
+ if (searchType === "equals") {
1251
+ query = query.where([column, "=", value]);
1252
+ } else if (searchType === "like") {
1253
+ query = query.where([column, "like", `%${value}%`]);
1254
+ }
1255
+ const rows = await query.execute();
1256
+ if (rows.length === 0) {
1257
+ throw new Error("No records found");
1258
+ }
1259
+ const fixtures = [];
1260
+ for (const row of rows) {
1261
+ const initialRecordsLength = fixtures.length;
1262
+ const newRecords = await this.createFixtureRecord(entity, row);
1263
+ fixtures.push(...newRecords);
1264
+ const currentFixtureRecord = fixtures.find(
1265
+ (r) => r.fixtureId === `${entityId}#${row.id}`
1266
+ );
1267
+ if (currentFixtureRecord) {
1268
+ currentFixtureRecord.fetchedRecords = fixtures.filter((r) => r.fixtureId !== currentFixtureRecord.fixtureId).slice(initialRecordsLength).map((r) => r.fixtureId);
1269
+ }
1270
+ }
1271
+ for await (const fixture of fixtures) {
1272
+ const entity2 = EntityManager.get(fixture.entityId);
1273
+ const [row] = await targetDB.from(entity2.table).selectAll().where(["id", "=", fixture.id]).first().execute();
1274
+ if (row) {
1275
+ const [record] = await this.createFixtureRecord(entity2, row, {
1276
+ singleRecord: true,
1277
+ _db: targetDB
1278
+ });
1279
+ fixture.target = record;
1280
+ continue;
1281
+ }
1282
+ const uniqueRow = await this.checkUniqueViolation(
1283
+ targetDB,
1284
+ entity2,
1285
+ fixture
1286
+ );
1287
+ if (uniqueRow) {
1288
+ const [record] = await this.createFixtureRecord(entity2, uniqueRow, {
1289
+ singleRecord: true,
1290
+ _db: targetDB
1291
+ });
1292
+ fixture.unique = record;
1293
+ }
1294
+ }
1295
+ return _2.uniqBy(fixtures, (f) => f.fixtureId);
1296
+ }
1297
+ async createFixtureRecord(entity, row, options) {
1298
+ const records = [];
1299
+ const visitedEntities = /* @__PURE__ */ new Set();
1300
+ const create = async (entity2, row2) => {
1301
+ const fixtureId = `${entity2.id}#${row2.id}`;
1302
+ if (visitedEntities.has(fixtureId)) {
1303
+ return;
1304
+ }
1305
+ visitedEntities.add(fixtureId);
1306
+ const record = {
1307
+ fixtureId,
1308
+ entityId: entity2.id,
1309
+ id: row2.id,
1310
+ columns: {},
1311
+ fetchedRecords: [],
1312
+ belongsRecords: []
1313
+ };
1314
+ for (const prop of entity2.props) {
1315
+ if (isVirtualProp(prop)) {
1316
+ continue;
1317
+ }
1318
+ record.columns[prop.name] = {
1319
+ prop,
1320
+ value: row2[prop.name]
1321
+ };
1322
+ const db = options?._db ?? DB.toClient(DB.getDB("w"));
1323
+ if (isManyToManyRelationProp(prop)) {
1324
+ const relatedEntity = EntityManager.get(prop.with);
1325
+ const throughTable = prop.joinTable;
1326
+ const fromColumn = `${inflection2.singularize(entity2.table)}_id`;
1327
+ const toColumn = `${inflection2.singularize(relatedEntity.table)}_id`;
1328
+ const _relatedIds = await db.from(throughTable).select(toColumn).where([fromColumn, "=", row2.id]).execute();
1329
+ const relatedIds = _relatedIds.map((r) => parseInt(r[toColumn]));
1330
+ record.columns[prop.name].value = relatedIds;
1331
+ } else if (isHasManyRelationProp(prop)) {
1332
+ const relatedEntity = EntityManager.get(prop.with);
1333
+ const relatedIds = await db.from(relatedEntity.table).select("id").where([prop.joinColumn, "=", row2.id]).pluck("id");
1334
+ record.columns[prop.name].value = relatedIds;
1335
+ } else if (isOneToOneRelationProp(prop) && !prop.hasJoinColumn) {
1336
+ const relatedEntity = EntityManager.get(prop.with);
1337
+ const relatedProp = relatedEntity.props.find(
1338
+ (p) => isRelationProp(p) && p.with === entity2.id
1339
+ );
1340
+ if (relatedProp) {
1341
+ const [relatedRow] = await db.from(relatedEntity.table).select("id").where([relatedProp.name, "=", row2.id]).first().execute();
1342
+ record.columns[prop.name].value = relatedRow?.id;
1343
+ }
1344
+ } else if (isRelationProp(prop)) {
1345
+ const relatedId = row2[`${prop.name}_id`];
1346
+ record.columns[prop.name].value = relatedId;
1347
+ if (relatedId) {
1348
+ record.belongsRecords.push(`${prop.with}#${relatedId}`);
1349
+ }
1350
+ if (!options?.singleRecord && relatedId) {
1351
+ const relatedEntity = EntityManager.get(prop.with);
1352
+ const [relatedRow] = await db.from(relatedEntity.table).selectAll().where(["id", "=", relatedId]).first().execute();
1353
+ if (relatedRow) {
1354
+ await create(relatedEntity, relatedRow);
1355
+ }
1356
+ }
1357
+ }
1358
+ }
1359
+ records.push(record);
1360
+ };
1361
+ await create(entity, row);
1362
+ return records;
1363
+ }
1364
+ async insertFixtures(dbName, _fixtures) {
1365
+ const fixtures = _2.uniqBy(_fixtures, (f) => f.fixtureId);
1366
+ this.relationGraph.buildGraph(fixtures);
1367
+ const insertionOrder = this.relationGraph.getInsertionOrder();
1368
+ const db = DB.getClient(dbName);
1369
+ await db.trx(async (trx) => {
1370
+ await trx.raw(`SET FOREIGN_KEY_CHECKS = 0`);
1371
+ for (const fixtureId of insertionOrder) {
1372
+ const fixture = fixtures.find((f) => f.fixtureId === fixtureId);
1373
+ const result = await this.insertFixture(trx, fixture);
1374
+ if (result.id !== fixture.id) {
1375
+ console.log(
1376
+ chalk2.yellow(
1377
+ `Unique constraint violation: ${fixture.entityId}#${fixture.id} -> ${fixture.entityId}#${result.id}`
1378
+ )
1379
+ );
1380
+ fixtures.forEach((f) => {
1381
+ Object.values(f.columns).forEach((column) => {
1382
+ if (column.prop.type === "relation" && column.prop.with === result.entityId && column.value === fixture.id) {
1383
+ column.value = result.id;
1384
+ }
1385
+ });
1386
+ });
1387
+ fixture.id = result.id;
1388
+ }
1389
+ }
1390
+ for (const fixtureId of insertionOrder) {
1391
+ const fixture = fixtures.find((f) => f.fixtureId === fixtureId);
1392
+ await this.handleManyToManyRelations(trx, fixture, fixtures);
1393
+ }
1394
+ await trx.raw(`SET FOREIGN_KEY_CHECKS = 1`);
1395
+ });
1396
+ const records = [];
1397
+ for await (const r of fixtures) {
1398
+ const entity = EntityManager.get(r.entityId);
1399
+ const [record] = await db.from(entity.table).selectAll().where(["id", "=", r.id]).first().execute();
1400
+ records.push({
1401
+ entityId: r.entityId,
1402
+ data: record
1403
+ });
1404
+ }
1405
+ return _2.uniqBy(records, (r) => `${r.entityId}#${r.data.id}`);
1406
+ }
1407
+ prepareInsertData(fixture) {
1408
+ const insertData = {};
1409
+ for (const [propName, column] of Object.entries(fixture.columns)) {
1410
+ if (isVirtualProp(column.prop)) {
1411
+ continue;
1412
+ }
1413
+ const prop = column.prop;
1414
+ if (!isRelationProp(prop)) {
1415
+ if (prop.type === "json") {
1416
+ insertData[propName] = JSON.stringify(column.value);
1417
+ } else {
1418
+ insertData[propName] = column.value;
1419
+ }
1420
+ } else if (isBelongsToOneRelationProp(prop) || isOneToOneRelationProp(prop) && prop.hasJoinColumn) {
1421
+ insertData[`${propName}_id`] = column.value;
1422
+ }
1423
+ }
1424
+ return insertData;
1425
+ }
1426
+ async insertFixture(db, fixture) {
1427
+ const insertData = this.prepareInsertData(fixture);
1428
+ const entity = EntityManager.get(fixture.entityId);
1429
+ try {
1430
+ const uniqueFound = await this.checkUniqueViolation(db, entity, fixture);
1431
+ if (uniqueFound) {
1432
+ return {
1433
+ entityId: fixture.entityId,
1434
+ id: uniqueFound.id
1435
+ };
1436
+ }
1437
+ const [found] = await db.from(entity.table).select("id").where(["id", "=", fixture.id]).first().execute();
1438
+ if (found && !fixture.override) {
1439
+ return {
1440
+ entityId: fixture.entityId,
1441
+ id: found.id
1442
+ };
1443
+ }
1444
+ await db.upsert(entity.table, [insertData]);
1445
+ return {
1446
+ entityId: fixture.entityId,
1447
+ id: fixture.id
1448
+ };
1449
+ } catch (err) {
1450
+ console.log(err);
1451
+ throw err;
1452
+ }
1453
+ }
1454
+ async handleManyToManyRelations(db, fixture, fixtures) {
1455
+ for (const [, column] of Object.entries(fixture.columns)) {
1456
+ const prop = column.prop;
1457
+ if (isManyToManyRelationProp(prop)) {
1458
+ const joinTable = prop.joinTable;
1459
+ const relatedIds = column.value;
1460
+ for (const relatedId of relatedIds) {
1461
+ if (!fixtures.find((f) => f.fixtureId === `${prop.with}#${relatedId}`)) {
1462
+ continue;
1463
+ }
1464
+ const entity = EntityManager.get(fixture.entityId);
1465
+ const relatedEntity = EntityManager.get(prop.with);
1466
+ if (!entity || !relatedEntity) {
1467
+ throw new Error(
1468
+ `Entity not found: ${fixture.entityId}, ${prop.with}`
1469
+ );
1470
+ }
1471
+ const [found] = await db.from(joinTable).select("id").where([
1472
+ [`${inflection2.singularize(entity.table)}_id`, "=", fixture.id],
1473
+ [
1474
+ `${inflection2.singularize(relatedEntity.table)}_id`,
1475
+ "=",
1476
+ relatedId
1477
+ ]
1478
+ ]).first().execute();
1479
+ if (found) {
1480
+ continue;
1481
+ }
1482
+ const newIds = await db.insert(joinTable, [
1483
+ {
1484
+ [`${inflection2.singularize(entity.table)}_id`]: fixture.id,
1485
+ [`${inflection2.singularize(relatedEntity.table)}_id`]: relatedId
1486
+ }
1487
+ ]);
1488
+ console.log(
1489
+ chalk2.green(
1490
+ `Inserted into ${joinTable}: ${entity.table}(${fixture.id}) - ${relatedEntity.table}(${relatedId}) ID: ${newIds}`
1491
+ )
1492
+ );
1493
+ }
1494
+ }
1495
+ }
1496
+ }
1497
+ async addFixtureLoader(code) {
1498
+ const path2 = Sonamu.apiRootPath + "/src/testing/fixture.ts";
1499
+ let content = readFileSync(path2).toString();
1500
+ const fixtureLoaderStart = content.indexOf("const fixtureLoader = {");
1501
+ const fixtureLoaderEnd = content.indexOf("};", fixtureLoaderStart);
1502
+ if (fixtureLoaderStart !== -1 && fixtureLoaderEnd !== -1) {
1503
+ const newContent = content.slice(0, fixtureLoaderEnd) + " " + code + "\n" + content.slice(fixtureLoaderEnd);
1504
+ writeFileSync(path2, newContent);
1505
+ } else {
1506
+ throw new Error("Failed to find fixtureLoader in fixture.ts");
1507
+ }
1508
+ }
1509
+ // 해당 픽스쳐의 값으로 유니크 제약에 위배되는 레코드가 있는지 확인
1510
+ async checkUniqueViolation(db, entity, fixture) {
1511
+ const _uniqueIndexes = entity.indexes.filter((i) => i.type === "unique");
1512
+ const uniqueIndexes = _uniqueIndexes.filter(
1513
+ (index) => index.columns.every((column) => !column.startsWith(`${entity.table}__`))
1514
+ );
1515
+ if (uniqueIndexes.length === 0) {
1516
+ return null;
1517
+ }
1518
+ let uniqueQuery = db.from(entity.table).selectAll();
1519
+ const whereClauses = uniqueIndexes.map((index) => {
1520
+ const containsNull = index.columns.some((column) => {
1521
+ const field = column.split("_id")[0];
1522
+ return fixture.columns[field].value === null;
1523
+ });
1524
+ if (containsNull) {
1525
+ return;
1526
+ }
1527
+ return index.columns.map((c) => {
1528
+ const field = c.split("_id")[0];
1529
+ if (Array.isArray(fixture.columns[field].value)) {
1530
+ return [c, "in", fixture.columns[field].value];
1531
+ } else {
1532
+ return [c, "=", fixture.columns[field].value];
1533
+ }
1534
+ });
1535
+ }).filter(Boolean);
1536
+ for (const clauses of whereClauses) {
1537
+ uniqueQuery = uniqueQuery.orWhere(clauses);
1538
+ }
1539
+ const [uniqueFound] = await uniqueQuery.execute();
1540
+ return uniqueFound;
1541
+ }
1542
+ };
1543
+ var FixtureManager = new FixtureManagerClass();
1544
+
1545
+ export {
1546
+ Migrator,
1547
+ FixtureManagerClass,
1548
+ FixtureManager
1549
+ };
1550
+ //# sourceMappingURL=chunk-I2MMJRJN.mjs.map