sonamu 0.2.53 → 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.
- package/dist/base-model-BzMJ2E_I.d.mts +43 -0
- package/dist/base-model-CWRKUX49.d.ts +43 -0
- package/dist/bin/cli.js +118 -89
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/cli.mjs +74 -45
- package/dist/bin/cli.mjs.map +1 -1
- package/dist/chunk-4K2F3SOM.mjs +231 -0
- package/dist/chunk-4K2F3SOM.mjs.map +1 -0
- package/dist/chunk-6SP5N5ND.mjs +1579 -0
- package/dist/chunk-6SP5N5ND.mjs.map +1 -0
- package/dist/chunk-EUP6N7EK.js +1579 -0
- package/dist/chunk-EUP6N7EK.js.map +1 -0
- package/dist/chunk-HVVCQLAU.mjs +280 -0
- package/dist/chunk-HVVCQLAU.mjs.map +1 -0
- package/dist/chunk-N6N3LENC.js +231 -0
- package/dist/chunk-N6N3LENC.js.map +1 -0
- package/dist/chunk-UAG3SKFM.js +280 -0
- package/dist/chunk-UAG3SKFM.js.map +1 -0
- package/dist/{chunk-5UCV2JP3.js → chunk-WJGRXAXE.js} +5261 -5570
- package/dist/chunk-WJGRXAXE.js.map +1 -0
- package/dist/{chunk-XTNCGTDO.mjs → chunk-ZFLQLW37.mjs} +5264 -5573
- package/dist/chunk-ZFLQLW37.mjs.map +1 -0
- package/dist/database/drivers/knex/base-model.d.mts +16 -0
- package/dist/database/drivers/knex/base-model.d.ts +16 -0
- package/dist/database/drivers/knex/base-model.js +55 -0
- package/dist/database/drivers/knex/base-model.js.map +1 -0
- package/dist/database/drivers/knex/base-model.mjs +56 -0
- package/dist/database/drivers/knex/base-model.mjs.map +1 -0
- package/dist/database/drivers/kysely/base-model.d.mts +22 -0
- package/dist/database/drivers/kysely/base-model.d.ts +22 -0
- package/dist/database/drivers/kysely/base-model.js +64 -0
- package/dist/database/drivers/kysely/base-model.js.map +1 -0
- package/dist/database/drivers/kysely/base-model.mjs +65 -0
- package/dist/database/drivers/kysely/base-model.mjs.map +1 -0
- package/dist/index.d.mts +226 -931
- package/dist/index.d.ts +226 -931
- package/dist/index.js +13 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -31
- package/dist/index.mjs.map +1 -1
- package/dist/model-CAH_4oQh.d.mts +1042 -0
- package/dist/model-CAH_4oQh.d.ts +1042 -0
- package/package.json +1 -1
- package/src/api/code-converters.ts +20 -1
- package/src/entity/migrator.ts +3 -0
- package/src/types/types.ts +1 -0
- package/dist/chunk-5UCV2JP3.js.map +0 -1
- package/dist/chunk-XTNCGTDO.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
|