sonamu 0.7.1 → 0.7.3
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/ai/agents/types.d.ts +4 -3
- package/dist/ai/agents/types.d.ts.map +1 -1
- package/dist/ai/agents/types.js +1 -1
- package/dist/api/code-converters.js +2 -2
- package/dist/api/config.d.ts +4 -2
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +6 -3
- package/dist/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +3 -2
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +3 -4
- package/dist/bin/cli.js +13 -29
- package/dist/bin/{hot-hook-register.d.ts → hmr-hook-register.d.ts} +3 -3
- package/dist/bin/hmr-hook-register.d.ts.map +1 -0
- package/dist/bin/{hot-hook-register.js → hmr-hook-register.js} +5 -5
- package/dist/bin/ts-loader-register.d.ts +2 -0
- package/dist/bin/ts-loader-register.d.ts.map +1 -0
- package/dist/bin/ts-loader-register.js +34 -0
- package/dist/database/base-model.d.ts +2 -34
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +3 -170
- package/dist/database/base-model.types.d.ts +1 -0
- package/dist/database/base-model.types.d.ts.map +1 -1
- package/dist/database/base-model.types.js +2 -2
- package/dist/database/puri-wrapper.js +7 -3
- package/dist/database/upsert-builder.d.ts +7 -3
- package/dist/database/upsert-builder.d.ts.map +1 -1
- package/dist/database/upsert-builder.js +63 -25
- package/dist/entity/entity-manager.d.ts +1 -1
- package/dist/entity/entity.js +3 -3
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +8 -7
- package/dist/migration/migration-set.d.ts.map +1 -1
- package/dist/migration/migration-set.js +2 -25
- package/dist/migration/migrator.js +2 -2
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +2 -1
- package/dist/syncer/file-patterns.js +2 -2
- package/dist/syncer/syncer.js +3 -3
- package/dist/template/implementations/service.template.d.ts.map +1 -1
- package/dist/template/implementations/service.template.js +3 -2
- package/dist/template/zod-converter.js +4 -2
- package/dist/types/types.d.ts +6 -5
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +2 -2
- package/dist/utils/model.d.ts +9 -2
- package/dist/utils/model.d.ts.map +1 -1
- package/dist/utils/model.js +1 -1
- package/dist/utils/path-utils.d.ts +1 -1
- package/dist/utils/path-utils.d.ts.map +1 -1
- package/dist/utils/path-utils.js +1 -1
- package/package.json +12 -12
- package/src/ai/agents/types.ts +6 -3
- package/src/api/code-converters.ts +2 -2
- package/src/api/config.ts +17 -6
- package/src/api/decorators.ts +2 -1
- package/src/api/sonamu.ts +2 -5
- package/src/bin/cli.ts +13 -30
- package/src/bin/{hot-hook-register.ts → hmr-hook-register.ts} +4 -4
- package/src/bin/{loader-register.ts → ts-loader-register.ts} +2 -2
- package/src/database/base-model.ts +5 -236
- package/src/database/base-model.types.ts +2 -0
- package/src/database/puri-wrapper.ts +2 -2
- package/src/database/upsert-builder.ts +88 -29
- package/src/entity/entity.ts +2 -2
- package/src/migration/code-generation.ts +8 -6
- package/src/migration/migration-set.ts +0 -20
- package/src/migration/migrator.ts +1 -1
- package/src/migration/postgresql-schema-reader.ts +1 -0
- package/src/shared/web.shared.ts.txt +6 -4
- package/src/syncer/file-patterns.ts +1 -1
- package/src/syncer/syncer.ts +2 -2
- package/src/template/implementations/service.template.ts +2 -1
- package/src/template/zod-converter.ts +3 -1
- package/src/types/types.ts +3 -2
- package/src/utils/model.ts +10 -4
- package/src/utils/path-utils.ts +5 -2
- package/dist/bin/hot-hook-register.d.ts.map +0 -1
- package/dist/bin/loader-register.d.ts +0 -2
- package/dist/bin/loader-register.d.ts.map +0 -1
- package/dist/bin/loader-register.js +0 -34
package/src/bin/cli.ts
CHANGED
|
@@ -100,12 +100,12 @@ async function sync() {
|
|
|
100
100
|
* pnpm dev 하면 실행되는 함수입니다.
|
|
101
101
|
* 프로젝트에 대해 HMR 지원하는 개발 서버를 띄워줍니다.
|
|
102
102
|
*
|
|
103
|
-
* TypeScript를 바로 실행할 수 있도록 @sonamu-kit/loader를,
|
|
104
|
-
* HMR을 지원하기 위해 @sonamu-kit/
|
|
103
|
+
* TypeScript를 바로 실행할 수 있도록 @sonamu-kit/ts-loader를,
|
|
104
|
+
* HMR을 지원하기 위해 @sonamu-kit/hmr-hook을 import하며,
|
|
105
105
|
* 소스맵 지원을 위해 --enable-source-maps 플래그를 포함하여 실행합니다.
|
|
106
106
|
*
|
|
107
|
-
* 이때 @sonamu-kit/loader와 @sonamu-kit/
|
|
108
|
-
* 또한 실행에 사용하는 @sonamu-kit/
|
|
107
|
+
* 이때 @sonamu-kit/ts-loader와 @sonamu-kit/hmr-hook는 sonamu가 자체적으로 가지고 있는 dependency입니다.
|
|
108
|
+
* 또한 실행에 사용하는 @sonamu-kit/hmr-runner도 마찬가지로 sonamu가 자체적으로 가지고 있는 dependency입니다.
|
|
109
109
|
* 따라서 사용자 프로젝트에서는 이 세 패키지를 직접 설치할 필요가 없습니다.
|
|
110
110
|
*
|
|
111
111
|
* Sonamu.init 없이 호출될 것을 상정하여 구현되었습니다.
|
|
@@ -116,10 +116,10 @@ async function dev() {
|
|
|
116
116
|
|
|
117
117
|
console.log(chalk.yellow.bold("🚀 Starting Sonamu dev server...\n"));
|
|
118
118
|
|
|
119
|
-
// 이 sonamu 패키지가 dependencies로 가지고 있는 @sonamu-kit/
|
|
120
|
-
// 이 경로(/bin/run.js)는 @sonamu-kit/
|
|
119
|
+
// 이 sonamu 패키지가 dependencies로 가지고 있는 @sonamu-kit/hmr-runner의 bin/run.js를 사용합니다.
|
|
120
|
+
// 이 경로(/bin/run.js)는 @sonamu-kit/hmr-runner의 package.json의 bin 필드에 명시되어 있는 그것과 같습니다.
|
|
121
121
|
const hotRunnerBinPath = createRequire(import.meta.url).resolve(
|
|
122
|
-
"@sonamu-kit/
|
|
122
|
+
"@sonamu-kit/hmr-runner/bin/run.js",
|
|
123
123
|
);
|
|
124
124
|
|
|
125
125
|
const serverProcess = spawn(
|
|
@@ -127,8 +127,8 @@ async function dev() {
|
|
|
127
127
|
[
|
|
128
128
|
hotRunnerBinPath, // 이렇게 해서 hot-runner를 실행하구요
|
|
129
129
|
"--clear-screen=false", // 이하 hot-runner에게 넘겨줄 인자들입니다.
|
|
130
|
-
"--node-args=--import=sonamu/loader-register", // TypeScript 서포트를 위한 로더,
|
|
131
|
-
"--node-args=--import=sonamu/
|
|
130
|
+
"--node-args=--import=sonamu/ts-loader-register", // TypeScript 서포트를 위한 로더,
|
|
131
|
+
"--node-args=--import=sonamu/hmr-hook-register", // HMR을 지원하기 위한 hook,
|
|
132
132
|
"--node-args=--enable-source-maps", // 그리고 소스맵 지원을 위한 플래그입니다.
|
|
133
133
|
"--on-key=r:restart:Restart server", // r 누르면 서버 재시작하게 해줘요.
|
|
134
134
|
`--on-key=f:shell(rm ${path.join(apiRoot, "sonamu.lock")}):restart:Force restart`, // f 누르면 sonamu.lock 파일을 지우고 서버 재시작하게 해줘요.
|
|
@@ -144,7 +144,7 @@ async function dev() {
|
|
|
144
144
|
...process.env,
|
|
145
145
|
NODE_ENV: "development",
|
|
146
146
|
HOT: "yes", // 얘가 있어야 HMR이 활성화됩니다.
|
|
147
|
-
API_ROOT_PATH: apiRoot, // 이 경로가
|
|
147
|
+
API_ROOT_PATH: apiRoot, // 이 경로가 hmr-hook의 루트 디렉토리가 됩니다.
|
|
148
148
|
},
|
|
149
149
|
},
|
|
150
150
|
);
|
|
@@ -216,23 +216,6 @@ async function build() {
|
|
|
216
216
|
process.exit(1);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
// sonamu.config.ts만 따로 빌드합니다.
|
|
220
|
-
// 이 친구는 src에 들어있지 않기 때문에 SWC_BUILD_COMMAND로 빌드되지 않습니다.
|
|
221
|
-
// 따라서 따로 빌드해줍니다.
|
|
222
|
-
try {
|
|
223
|
-
const configPath = path.join(apiRoot, "sonamu.config.ts");
|
|
224
|
-
if (await exists(configPath)) {
|
|
225
|
-
console.log(chalk.blue("Building sonamu.config.ts..."));
|
|
226
|
-
execSync(`swc ${configPath} -o ${BUILD_DIR}/sonamu.config.js`, {
|
|
227
|
-
cwd: apiRoot,
|
|
228
|
-
stdio: "inherit",
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
} catch (error) {
|
|
232
|
-
console.error(chalk.red("Building sonamu.config.ts failed."), error);
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
219
|
// 마지막에는 타입 체크를 해요.
|
|
237
220
|
try {
|
|
238
221
|
console.log(chalk.blue("Checking types with tsc..."));
|
|
@@ -481,14 +464,14 @@ async function ui() {
|
|
|
481
464
|
return;
|
|
482
465
|
}
|
|
483
466
|
|
|
484
|
-
// UI를 별도 프로세스로 실행 (
|
|
467
|
+
// UI를 별도 프로세스로 실행 (hmr-hook 활성화)
|
|
485
468
|
const uiProcess = spawn(
|
|
486
469
|
process.execPath,
|
|
487
470
|
[
|
|
488
471
|
"--import",
|
|
489
|
-
"sonamu/loader-register",
|
|
472
|
+
"sonamu/ts-loader-register",
|
|
490
473
|
"--import",
|
|
491
|
-
"sonamu/
|
|
474
|
+
"sonamu/hmr-hook-register",
|
|
492
475
|
"--enable-source-maps",
|
|
493
476
|
"--no-warnings",
|
|
494
477
|
uiNodePath,
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* hmr-hook 초기화하는 모듈입니다.
|
|
3
3
|
*
|
|
4
4
|
* 이 파일은 --import 플래그로 프로세스 시작 시 로드되어야 합니다.
|
|
5
5
|
*
|
|
6
6
|
* 환경변수:
|
|
7
7
|
* - API_ROOT_PATH: 사용자 프로젝트의 API 루트 경로
|
|
8
|
-
* - HOT: 'yes'일 때만
|
|
8
|
+
* - HOT: 'yes'일 때만 hmr-hook 활성화
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
if (process.env.HOT === "yes" && process.env.API_ROOT_PATH) {
|
|
12
|
-
const { hot } = await import("@sonamu-kit/
|
|
12
|
+
const { hot } = await import("@sonamu-kit/hmr-hook");
|
|
13
13
|
|
|
14
14
|
await hot.init({
|
|
15
15
|
rootDirectory: process.env.API_ROOT_PATH, // 이 친구가 프로젝트 API 경로로 잘 설정되어야 아래 바운더리가 작동합니다.
|
|
16
16
|
boundaries: [`./src/**/*.ts`], // 프로젝트의 이 친구들이 바운더리가 됩니다.
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
console.log("🔥
|
|
19
|
+
console.log("🔥 HMR-hook initialized");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export {};
|
|
@@ -4,7 +4,7 @@ import { exists } from "../utils/fs-utils.js";
|
|
|
4
4
|
import { findApiRootPath } from "../utils/utils.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @sonamu-kit/loader/loader를 등록하는 스크립트입니다.
|
|
7
|
+
* @sonamu-kit/ts-loader/loader를 등록하는 스크립트입니다.
|
|
8
8
|
* 이 스크립트는 sonamu cli로 dev 실행할 때 --import로 실행됩니다.
|
|
9
9
|
*/
|
|
10
10
|
async function setupSwcConfig() {
|
|
@@ -33,6 +33,6 @@ async function setupSwcConfig() {
|
|
|
33
33
|
// swc 설정 파일 경로를 환경 변수로 설정
|
|
34
34
|
await setupSwcConfig();
|
|
35
35
|
|
|
36
|
-
register("@sonamu-kit/loader/loader", {
|
|
36
|
+
register("@sonamu-kit/ts-loader/loader", {
|
|
37
37
|
parentURL: import.meta.url,
|
|
38
38
|
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: Puri의 타입은 개별 모델에서 확정되므로 BaseModel에서는 any를 허용함 */
|
|
2
|
+
|
|
3
3
|
import type { Knex } from "knex";
|
|
4
|
-
import { group, isObject, omit, set
|
|
4
|
+
import { group, isObject, omit, set } from "radashi";
|
|
5
5
|
import { Sonamu } from "../api";
|
|
6
|
-
import {
|
|
7
|
-
import type { BaseListParams } from "../utils/model";
|
|
6
|
+
import type { DatabaseSchemaExtend } from "../types/types";
|
|
8
7
|
import { getJoinTables, getTableNamesFromWhere } from "../utils/sql-parser";
|
|
9
8
|
import { chunk } from "../utils/utils";
|
|
10
9
|
import type {
|
|
@@ -56,7 +55,7 @@ export class BaseModelClass<
|
|
|
56
55
|
|
|
57
56
|
// 트랜잭션이 없으면 새로운 PuriWrapper 반환
|
|
58
57
|
const db = this.getDB(which);
|
|
59
|
-
return new PuriWrapper(db,
|
|
58
|
+
return new PuriWrapper(db, new UpsertBuilder());
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
async destroy() {
|
|
@@ -365,236 +364,6 @@ export class BaseModelClass<
|
|
|
365
364
|
return hydrated;
|
|
366
365
|
}) as T[];
|
|
367
366
|
}
|
|
368
|
-
|
|
369
|
-
// Legacy SubsetQuery 실행 (Puri 도입 전 호환용)
|
|
370
|
-
async runSubsetQuery<T extends BaseListParams, U extends string>({
|
|
371
|
-
params,
|
|
372
|
-
baseTable,
|
|
373
|
-
subset,
|
|
374
|
-
subsetQuery,
|
|
375
|
-
build,
|
|
376
|
-
afterBuild,
|
|
377
|
-
debug,
|
|
378
|
-
db: _db,
|
|
379
|
-
optimizeCountQuery,
|
|
380
|
-
}: {
|
|
381
|
-
subset: U;
|
|
382
|
-
params: T;
|
|
383
|
-
subsetQuery: SubsetQuery;
|
|
384
|
-
build: (buildParams: {
|
|
385
|
-
qb: Knex.QueryBuilder;
|
|
386
|
-
db: Knex;
|
|
387
|
-
select: (string | Knex.Raw)[];
|
|
388
|
-
joins: SubsetQuery["joins"];
|
|
389
|
-
virtual: string[];
|
|
390
|
-
}) => Knex.QueryBuilder;
|
|
391
|
-
afterBuild?: (buildParams: {
|
|
392
|
-
qb: Knex.QueryBuilder;
|
|
393
|
-
db: Knex;
|
|
394
|
-
select: (string | Knex.Raw)[];
|
|
395
|
-
joins: SubsetQuery["joins"];
|
|
396
|
-
virtual: string[];
|
|
397
|
-
}) => Knex.QueryBuilder;
|
|
398
|
-
baseTable?: string;
|
|
399
|
-
debug?: boolean | "list" | "count";
|
|
400
|
-
db?: Knex;
|
|
401
|
-
optimizeCountQuery?: boolean;
|
|
402
|
-
}): Promise<{
|
|
403
|
-
// biome-ignore lint/suspicious/noExplicitAny: Puri 도입 전까지 any로 유지
|
|
404
|
-
rows: any[];
|
|
405
|
-
total?: number | undefined;
|
|
406
|
-
subsetQuery: SubsetQuery;
|
|
407
|
-
qb: Knex.QueryBuilder;
|
|
408
|
-
}> {
|
|
409
|
-
const chalk = (await import("chalk")).default;
|
|
410
|
-
const SqlParser = (await import("node-sql-parser")).default;
|
|
411
|
-
const { getTableName, getTableNamesFromWhere } = await import("../utils/sql-parser");
|
|
412
|
-
|
|
413
|
-
const db = _db ?? this.getDB(subset.startsWith("A") ? "w" : "r");
|
|
414
|
-
baseTable = baseTable ?? inflection.pluralize(inflection.underscore(this.modelName));
|
|
415
|
-
const queryMode = params.queryMode ?? (params.id !== undefined ? "list" : "both");
|
|
416
|
-
|
|
417
|
-
const { select, virtual, joins, loaders } = subsetQuery;
|
|
418
|
-
const qb = build({
|
|
419
|
-
qb: db.from(baseTable),
|
|
420
|
-
db,
|
|
421
|
-
select,
|
|
422
|
-
joins,
|
|
423
|
-
virtual,
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
const applyJoinClause = (qb: Knex.QueryBuilder, joins: SubsetQuery["joins"]) => {
|
|
427
|
-
joins.forEach((join) => {
|
|
428
|
-
if (join.join === "inner") {
|
|
429
|
-
qb.innerJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
|
|
430
|
-
} else if (join.join === "outer") {
|
|
431
|
-
qb.leftOuterJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
// countQuery
|
|
437
|
-
const total = await (async () => {
|
|
438
|
-
if (queryMode === "list") {
|
|
439
|
-
return undefined;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const clonedQb = qb.clone().clear("order").clear("offset").clear("limit");
|
|
443
|
-
const parser = new SqlParser.Parser();
|
|
444
|
-
|
|
445
|
-
if (optimizeCountQuery) {
|
|
446
|
-
const parsedQuery = parser.astify(clonedQb.toQuery(), {
|
|
447
|
-
database: Sonamu.config.database.database,
|
|
448
|
-
});
|
|
449
|
-
const tables = getTableNamesFromWhere(parsedQuery);
|
|
450
|
-
const needToJoin = unique(
|
|
451
|
-
tables.flatMap((table) => table.split("__").map((t) => inflection.pluralize(t))),
|
|
452
|
-
);
|
|
453
|
-
applyJoinClause(
|
|
454
|
-
clonedQb,
|
|
455
|
-
joins.filter((j) => needToJoin.includes(j.table)),
|
|
456
|
-
);
|
|
457
|
-
} else {
|
|
458
|
-
applyJoinClause(clonedQb, joins);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const processedQb = afterBuild?.({ qb: clonedQb, db, select, joins, virtual }) ?? clonedQb;
|
|
462
|
-
|
|
463
|
-
const parsedQuery = parser.astify(processedQb.toQuery(), {
|
|
464
|
-
database: Sonamu.config.database.database,
|
|
465
|
-
});
|
|
466
|
-
const q = Array.isArray(parsedQuery) ? parsedQuery[0] : parsedQuery;
|
|
467
|
-
if (q.type !== "select") {
|
|
468
|
-
throw new Error("Invalid query");
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const countQuery =
|
|
472
|
-
q.distinct !== null
|
|
473
|
-
? clonedQb
|
|
474
|
-
.clear("select")
|
|
475
|
-
.select(
|
|
476
|
-
db.raw(
|
|
477
|
-
`COUNT(DISTINCT \`${getTableName(q.columns[0].expr)}\`.\`${q.columns[0].expr.column}\`) as total`,
|
|
478
|
-
),
|
|
479
|
-
)
|
|
480
|
-
.first()
|
|
481
|
-
: clonedQb.clear("select").count("*", { as: "total" }).first();
|
|
482
|
-
const countRow: { total?: number } = await countQuery;
|
|
483
|
-
|
|
484
|
-
if (debug === true || debug === "count") {
|
|
485
|
-
console.debug("DEBUG: count query", chalk.blue(countQuery.toQuery().toString()));
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return countRow?.total ?? 0;
|
|
489
|
-
})();
|
|
490
|
-
|
|
491
|
-
// listQuery
|
|
492
|
-
const rows = await (async () => {
|
|
493
|
-
if (queryMode === "count") {
|
|
494
|
-
return [];
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (params.num !== 0) {
|
|
498
|
-
assert(params.num);
|
|
499
|
-
qb.limit(params.num);
|
|
500
|
-
qb.offset(params.num * ((params.page ?? 1) - 1));
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const clonedQb = qb.clone().select(select);
|
|
504
|
-
applyJoinClause(clonedQb, joins);
|
|
505
|
-
|
|
506
|
-
const listQuery = afterBuild?.({ qb: clonedQb, db, select, joins, virtual }) ?? clonedQb;
|
|
507
|
-
|
|
508
|
-
let rows = await listQuery;
|
|
509
|
-
if (debug === true || debug === "list") {
|
|
510
|
-
console.debug("DEBUG: list query", chalk.blue(listQuery.toQuery().toString()));
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
rows = await this.useLoaders(db, rows, loaders);
|
|
514
|
-
rows = this.hydrate(rows);
|
|
515
|
-
return rows;
|
|
516
|
-
})();
|
|
517
|
-
|
|
518
|
-
return { rows, total, subsetQuery, qb };
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Legacy Loader 처리 (Puri 도입 전 호환용)
|
|
522
|
-
async useLoaders(db: Knex, rows: UnknownDBRecord[], loaders: SubsetQuery["loaders"]) {
|
|
523
|
-
if (loaders.length === 0) {
|
|
524
|
-
return rows;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
for (const loader of loaders) {
|
|
528
|
-
let subQ: Knex.QueryBuilder;
|
|
529
|
-
let subRows: UnknownDBRecord[];
|
|
530
|
-
let toCol: string;
|
|
531
|
-
|
|
532
|
-
const fromIds = rows.map((row) => row[loader.manyJoin.idField]);
|
|
533
|
-
|
|
534
|
-
if (loader.manyJoin.through === undefined) {
|
|
535
|
-
// HasMany
|
|
536
|
-
const idColumn = `${loader.manyJoin.toTable}.${loader.manyJoin.toCol}`;
|
|
537
|
-
subQ = db(loader.manyJoin.toTable)
|
|
538
|
-
.whereIn(idColumn as string, fromIds as string[])
|
|
539
|
-
.select([...loader.select, idColumn]);
|
|
540
|
-
|
|
541
|
-
loader.oneJoins.forEach((join) => {
|
|
542
|
-
if (join.join === "inner") {
|
|
543
|
-
subQ.innerJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
|
|
544
|
-
} else if (join.join === "outer") {
|
|
545
|
-
subQ.leftOuterJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
toCol = loader.manyJoin.toCol;
|
|
549
|
-
} else {
|
|
550
|
-
// ManyToMany
|
|
551
|
-
const idColumn = `${loader.manyJoin.through.table}.${loader.manyJoin.through.fromCol}`;
|
|
552
|
-
subQ = db(loader.manyJoin.through.table)
|
|
553
|
-
.join(
|
|
554
|
-
loader.manyJoin.toTable,
|
|
555
|
-
`${loader.manyJoin.through.table}.${loader.manyJoin.through.toCol}`,
|
|
556
|
-
`${loader.manyJoin.toTable}.${loader.manyJoin.toCol}`,
|
|
557
|
-
)
|
|
558
|
-
.whereIn(idColumn as string, fromIds as string[])
|
|
559
|
-
.select(unique([...loader.select, idColumn]));
|
|
560
|
-
|
|
561
|
-
loader.oneJoins.forEach((join) => {
|
|
562
|
-
if (join.join === "inner") {
|
|
563
|
-
subQ.innerJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
|
|
564
|
-
} else if (join.join === "outer") {
|
|
565
|
-
subQ.leftOuterJoin(`${join.table} as ${join.as}`, this.getJoinClause(db, join));
|
|
566
|
-
}
|
|
567
|
-
});
|
|
568
|
-
toCol = loader.manyJoin.through.fromCol;
|
|
569
|
-
}
|
|
570
|
-
subRows = await subQ;
|
|
571
|
-
|
|
572
|
-
if (loader.loaders) {
|
|
573
|
-
subRows = await this.useLoaders(db, subRows, loader.loaders);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const subRowGroups = group(subRows, (row) => row[toCol] as string);
|
|
577
|
-
rows = rows.map((row) => {
|
|
578
|
-
row[loader.as] = (subRowGroups[row[loader.manyJoin.idField] as string] ?? []).map((r) =>
|
|
579
|
-
omit(r, [toCol]),
|
|
580
|
-
);
|
|
581
|
-
return row;
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
return rows;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
getJoinClause(db: Knex<any, unknown>, join: SubsetQuery["joins"][number]): Knex.Raw<any> {
|
|
588
|
-
if (!isCustomJoinClause(join)) {
|
|
589
|
-
return db.raw(`${join.from} = ${join.to}`);
|
|
590
|
-
} else {
|
|
591
|
-
return db.raw(join.custom);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
getUpsertBuilder(): UpsertBuilder {
|
|
596
|
-
return new UpsertBuilder();
|
|
597
|
-
}
|
|
598
367
|
}
|
|
599
368
|
|
|
600
369
|
/**
|
|
@@ -148,7 +148,7 @@ export class PuriWrapper<TSchema extends DatabaseSchemaExtend = DatabaseSchemaEx
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
ubUpsert(tableName: TableName<TSchema>, chunkSize?: number): Promise<number[]> {
|
|
151
|
-
return this.upsertBuilder.upsert(this.knex, tableName, chunkSize);
|
|
151
|
+
return this.upsertBuilder.upsert(this.knex, tableName, { chunkSize });
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
ubInsertOnly(tableName: TableName<TSchema>, chunkSize?: number): Promise<number[]> {
|
|
@@ -160,7 +160,7 @@ export class PuriWrapper<TSchema extends DatabaseSchemaExtend = DatabaseSchemaEx
|
|
|
160
160
|
mode: "upsert" | "insert",
|
|
161
161
|
chunkSize?: number,
|
|
162
162
|
): Promise<number[]> {
|
|
163
|
-
return this.upsertBuilder.upsertOrInsert(this.knex, tableName, mode, chunkSize);
|
|
163
|
+
return this.upsertBuilder.upsertOrInsert(this.knex, tableName, mode, { chunkSize });
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
ubUpdateBatch(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import type { Knex } from "knex";
|
|
3
|
-
import { unique } from "radashi";
|
|
3
|
+
import { isArray, unique } from "radashi";
|
|
4
4
|
import { EntityManager } from "../entity/entity-manager";
|
|
5
5
|
import { Naite } from "../naite/naite";
|
|
6
6
|
import { assertDefined, chunk, nonNullable } from "../utils/utils";
|
|
@@ -17,6 +17,10 @@ export type UBRef = {
|
|
|
17
17
|
of: string;
|
|
18
18
|
use?: string;
|
|
19
19
|
};
|
|
20
|
+
type UpsertOptions = {
|
|
21
|
+
chunkSize?: number;
|
|
22
|
+
cleanOrphans?: string | string[]; // FK 컬럼명(들)
|
|
23
|
+
};
|
|
20
24
|
export function isRefField(field: unknown): field is UBRef {
|
|
21
25
|
return (
|
|
22
26
|
field !== undefined &&
|
|
@@ -150,18 +154,37 @@ export class UpsertBuilder {
|
|
|
150
154
|
return result;
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
async upsert(
|
|
154
|
-
|
|
157
|
+
async upsert(
|
|
158
|
+
wdb: Knex,
|
|
159
|
+
tableName: string,
|
|
160
|
+
optionsOrChunkSize?: UpsertOptions,
|
|
161
|
+
): Promise<number[]> {
|
|
162
|
+
// 숫자면 { chunkSize: n } 으로 변환
|
|
163
|
+
const options =
|
|
164
|
+
typeof optionsOrChunkSize === "number"
|
|
165
|
+
? { chunkSize: optionsOrChunkSize }
|
|
166
|
+
: optionsOrChunkSize;
|
|
167
|
+
|
|
168
|
+
return this.upsertOrInsert(wdb, tableName, "upsert", options);
|
|
155
169
|
}
|
|
156
|
-
async insertOnly(
|
|
157
|
-
|
|
170
|
+
async insertOnly(
|
|
171
|
+
wdb: Knex,
|
|
172
|
+
tableName: string,
|
|
173
|
+
optionsOrChunkSize?: UpsertOptions | number,
|
|
174
|
+
): Promise<number[]> {
|
|
175
|
+
const options =
|
|
176
|
+
typeof optionsOrChunkSize === "number"
|
|
177
|
+
? { chunkSize: optionsOrChunkSize }
|
|
178
|
+
: optionsOrChunkSize;
|
|
179
|
+
|
|
180
|
+
return this.upsertOrInsert(wdb, tableName, "insert", options);
|
|
158
181
|
}
|
|
159
182
|
|
|
160
183
|
async upsertOrInsert(
|
|
161
184
|
wdb: Knex,
|
|
162
185
|
tableName: string,
|
|
163
186
|
mode: "upsert" | "insert",
|
|
164
|
-
|
|
187
|
+
options?: UpsertOptions,
|
|
165
188
|
): Promise<number[]> {
|
|
166
189
|
if (this.hasTable(tableName) === false) {
|
|
167
190
|
return [];
|
|
@@ -244,43 +267,47 @@ export class UpsertBuilder {
|
|
|
244
267
|
});
|
|
245
268
|
|
|
246
269
|
// 현재 레벨 upsert
|
|
270
|
+
const chunkSize = options?.chunkSize;
|
|
247
271
|
const levelChunks = chunkSize ? chunk(resolvedRows, chunkSize) : [resolvedRows];
|
|
248
|
-
const selectFields = unique(["
|
|
272
|
+
const selectFields = unique(["id", ...extractFields]);
|
|
249
273
|
|
|
250
274
|
for (const dataChunk of levelChunks) {
|
|
251
275
|
if (dataChunk.length === 0) continue;
|
|
252
276
|
|
|
253
|
-
|
|
277
|
+
// uuid를 별도로 보관하고, DB에 저장할 데이터에서 제거
|
|
278
|
+
const originalUuids = dataChunk.map((r) => r.uuid as string);
|
|
279
|
+
const dataForDb = dataChunk.map(({ uuid, ...rest }) => rest);
|
|
254
280
|
|
|
255
|
-
|
|
256
|
-
// INSERT 모드
|
|
257
|
-
await wdb.insert(dataChunk).into(tableName);
|
|
281
|
+
let resultRows: { id: number; [key: string]: unknown }[];
|
|
258
282
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
.whereIn("uuid", uuids as readonly string[]);
|
|
283
|
+
if (mode === "insert") {
|
|
284
|
+
// INSERT 모드 - RETURNING 사용
|
|
285
|
+
resultRows = await wdb.insert(dataForDb).into(tableName).returning(selectFields);
|
|
263
286
|
} else {
|
|
264
|
-
// UPSERT
|
|
287
|
+
// UPSERT 모드 - onConflict 사용
|
|
265
288
|
const conflictColumns = table.uniqueIndexes[0].columns;
|
|
266
|
-
const updateColumns = Object.keys(
|
|
267
|
-
(col) =>
|
|
289
|
+
const updateColumns = Object.keys(dataForDb[0]).filter(
|
|
290
|
+
(col) => !conflictColumns.includes(col),
|
|
268
291
|
);
|
|
269
292
|
|
|
270
|
-
|
|
293
|
+
// updateColumns가 비어있어도 merge()를 사용하여 모든 행이 RETURNING되도록 보장
|
|
294
|
+
const mergeColumns = updateColumns.length > 0 ? updateColumns : conflictColumns;
|
|
271
295
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
296
|
+
resultRows = await wdb
|
|
297
|
+
.insert(dataForDb)
|
|
298
|
+
.into(tableName)
|
|
299
|
+
.onConflict(conflictColumns)
|
|
300
|
+
.merge(mergeColumns)
|
|
301
|
+
.returning(selectFields);
|
|
278
302
|
}
|
|
279
303
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
304
|
+
if (originalUuids.length !== resultRows.length) {
|
|
305
|
+
throw new Error(`${tableName}: register/returning 불일치`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (let i = 0; i < resultRows.length; i++) {
|
|
309
|
+
uuidMap.set(originalUuids[i], resultRows[i]);
|
|
310
|
+
allIds.push(resultRows[i].id);
|
|
284
311
|
}
|
|
285
312
|
}
|
|
286
313
|
}
|
|
@@ -311,6 +338,38 @@ export class UpsertBuilder {
|
|
|
311
338
|
});
|
|
312
339
|
}
|
|
313
340
|
|
|
341
|
+
if (options?.cleanOrphans) {
|
|
342
|
+
const cleanOrphans = options.cleanOrphans;
|
|
343
|
+
const fkColumns = isArray(cleanOrphans) ? cleanOrphans : [cleanOrphans];
|
|
344
|
+
|
|
345
|
+
// 현재 register된 레코드들의 FK 값들 추출
|
|
346
|
+
const fkConditions = fkColumns.map((fkCol) => {
|
|
347
|
+
const fkValues = [...new Set(table.rows.map((row) => row[fkCol]).filter((v) => v != null))];
|
|
348
|
+
return { column: fkCol, values: fkValues };
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// 모든 FK 컬럼에 값이 있는 경우에만 삭제 실행
|
|
352
|
+
if (fkConditions.every((fc) => fc.values.length > 0)) {
|
|
353
|
+
let deleteQuery = wdb(tableName);
|
|
354
|
+
|
|
355
|
+
// 각 FK 컬럼에 대한 WHERE IN 조건 추가
|
|
356
|
+
for (const { column, values } of fkConditions) {
|
|
357
|
+
deleteQuery = deleteQuery.whereIn(column, values);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 방금 upsert한 ID는 제외
|
|
361
|
+
deleteQuery = deleteQuery.whereNotIn("id", allIds);
|
|
362
|
+
|
|
363
|
+
const deletedCount = await deleteQuery.delete();
|
|
364
|
+
|
|
365
|
+
Naite.t("puri:ub-clean-orphans", {
|
|
366
|
+
tableName,
|
|
367
|
+
cleanOrphans: fkColumns,
|
|
368
|
+
deletedCount,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
314
373
|
// 해당 테이블의 데이터 초기화
|
|
315
374
|
table.rows = [];
|
|
316
375
|
table.references.clear();
|
package/src/entity/entity.ts
CHANGED
|
@@ -524,8 +524,8 @@ export class Entity {
|
|
|
524
524
|
// 일반 prop 처리
|
|
525
525
|
if (key === "") {
|
|
526
526
|
return group.map((propName) => {
|
|
527
|
-
//
|
|
528
|
-
if (propName === "
|
|
527
|
+
// FIXME: 이거 나중에 없애야함
|
|
528
|
+
if (propName === "말도안되는프롭명__이거왜타입처리가꼬여서이러지?") {
|
|
529
529
|
return {
|
|
530
530
|
nodeType: "plain" as const,
|
|
531
531
|
prop: {
|
|
@@ -146,15 +146,15 @@ function genIndexDefinition(index: MigrationIndex, table: string) {
|
|
|
146
146
|
};
|
|
147
147
|
|
|
148
148
|
if (index.type === "fulltext" && index.parser === "ngram") {
|
|
149
|
-
|
|
150
|
-
return `await knex.raw(\`ALTER TABLE ${table} ADD FULLTEXT INDEX ${indexName} (${index.columns.join(
|
|
149
|
+
return `await knex.raw(\`ALTER TABLE ${table} ADD FULLTEXT INDEX ${index.name} (${index.columns.join(
|
|
151
150
|
", ",
|
|
152
151
|
)}) WITH PARSER ngram\`);`;
|
|
153
152
|
}
|
|
154
153
|
|
|
155
154
|
return `table.${methodMap[index.type]}([${index.columns
|
|
156
155
|
.map((col) => `'${col}'`)
|
|
157
|
-
.join(",")}]${index.type === "fulltext" ? ",
|
|
156
|
+
.join(",")}], '${index.name}'${index.type === "fulltext" ? ", 'FULLTEXT'" : ""}
|
|
157
|
+
);`;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
|
@@ -309,6 +309,8 @@ async function generateAlterCode_ColumnAndIndexes(
|
|
|
309
309
|
});
|
|
310
310
|
// Naite.t("migrator:generateAlterCode_ColumnAndIndexes:alterColumnsTo", alterColumnsTo);
|
|
311
311
|
|
|
312
|
+
// TODO: 인덱스명 변경된 경우 처리
|
|
313
|
+
|
|
312
314
|
const lines: string[] = [
|
|
313
315
|
'import { Knex } from "knex";',
|
|
314
316
|
"",
|
|
@@ -536,7 +538,7 @@ function genIndexDropDefinition(index: MigrationIndex) {
|
|
|
536
538
|
|
|
537
539
|
return `table.drop${methodMap[index.type]}([${index.columns
|
|
538
540
|
.map((columnName) => `'${columnName}'`)
|
|
539
|
-
.join(",")}])`;
|
|
541
|
+
.join(",")}], '${index.name}')`;
|
|
540
542
|
}
|
|
541
543
|
|
|
542
544
|
/**
|
|
@@ -723,10 +725,10 @@ export async function generateAlterCode(
|
|
|
723
725
|
*/
|
|
724
726
|
|
|
725
727
|
const entityIndexes = alphabetical(entitySet.indexes, (a) =>
|
|
726
|
-
[a.type, ...a.columns
|
|
728
|
+
[a.type, ...a.columns].join("-"),
|
|
727
729
|
);
|
|
728
730
|
const dbIndexes = alphabetical(dbSet.indexes, (a) =>
|
|
729
|
-
[a.type, ...a.columns
|
|
731
|
+
[a.type, ...a.columns].join("-"),
|
|
730
732
|
);
|
|
731
733
|
|
|
732
734
|
const replaceNoActionOnMySQL = (f: MigrationForeign) => {
|