sonamu 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pnp.cjs +15552 -0
- package/.pnp.loader.mjs +285 -0
- package/.vscode/extensions.json +6 -0
- package/.vscode/settings.json +9 -0
- package/.yarnrc.yml +5 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +123 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/index.js +34 -0
- package/package.json +60 -0
- package/src/api/caster.ts +72 -0
- package/src/api/code-converters.ts +552 -0
- package/src/api/context.ts +20 -0
- package/src/api/decorators.ts +63 -0
- package/src/api/index.ts +5 -0
- package/src/api/init.ts +128 -0
- package/src/bin/cli.ts +115 -0
- package/src/database/base-model.ts +287 -0
- package/src/database/db.ts +95 -0
- package/src/database/knex-plugins/knex-on-duplicate-update.ts +41 -0
- package/src/database/upsert-builder.ts +231 -0
- package/src/exceptions/error-handler.ts +29 -0
- package/src/exceptions/so-exceptions.ts +91 -0
- package/src/index.ts +17 -0
- package/src/shared/web.shared.ts.txt +119 -0
- package/src/smd/migrator.ts +1462 -0
- package/src/smd/smd-manager.ts +141 -0
- package/src/smd/smd-utils.ts +266 -0
- package/src/smd/smd.ts +533 -0
- package/src/syncer/index.ts +1 -0
- package/src/syncer/syncer.ts +1283 -0
- package/src/templates/base-template.ts +19 -0
- package/src/templates/generated.template.ts +247 -0
- package/src/templates/generated_http.template.ts +114 -0
- package/src/templates/index.ts +1 -0
- package/src/templates/init_enums.template.ts +71 -0
- package/src/templates/init_generated.template.ts +44 -0
- package/src/templates/init_types.template.ts +38 -0
- package/src/templates/model.template.ts +168 -0
- package/src/templates/model_test.template.ts +39 -0
- package/src/templates/service.template.ts +263 -0
- package/src/templates/smd.template.ts +49 -0
- package/src/templates/view_enums_buttonset.template.ts +34 -0
- package/src/templates/view_enums_dropdown.template.ts +67 -0
- package/src/templates/view_enums_select.template.ts +60 -0
- package/src/templates/view_form.template.ts +397 -0
- package/src/templates/view_id_all_select.template.ts +34 -0
- package/src/templates/view_id_async_select.template.ts +113 -0
- package/src/templates/view_list.template.ts +652 -0
- package/src/templates/view_list_columns.template.ts +59 -0
- package/src/templates/view_search_input.template.ts +67 -0
- package/src/testing/fixture-manager.ts +271 -0
- package/src/types/types.ts +668 -0
- package/src/typings/knex.d.ts +24 -0
- package/src/utils/controller.ts +21 -0
- package/src/utils/lodash-able.ts +11 -0
- package/src/utils/model.ts +33 -0
- package/src/utils/utils.ts +28 -0
- package/tsconfig.json +47 -0
package/src/api/init.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
|
3
|
+
import { IncomingMessage, Server, ServerResponse } from "http";
|
|
4
|
+
import { ZodError } from "zod";
|
|
5
|
+
import { getZodObjectFromApi } from "./code-converters";
|
|
6
|
+
import { Context } from "./context";
|
|
7
|
+
import { BadRequestException } from "../exceptions/so-exceptions";
|
|
8
|
+
import { SMDManager } from "../smd/smd-manager";
|
|
9
|
+
import { fastifyCaster } from "../api/caster";
|
|
10
|
+
import { ApiParamType } from "../types/types";
|
|
11
|
+
import { Syncer } from "../syncer/syncer";
|
|
12
|
+
import { isLocal } from "../utils/controller";
|
|
13
|
+
import { DB } from "../database/db";
|
|
14
|
+
import { BaseModel } from "../database/base-model";
|
|
15
|
+
|
|
16
|
+
export type SonamuInitConfig = {
|
|
17
|
+
prefix: string;
|
|
18
|
+
appRootPath: string;
|
|
19
|
+
syncTargets: string[];
|
|
20
|
+
contextProvider: (
|
|
21
|
+
defaultContext: Pick<Context, "headers" | "reply">,
|
|
22
|
+
request: FastifyRequest,
|
|
23
|
+
reply: FastifyReply
|
|
24
|
+
) => Context;
|
|
25
|
+
};
|
|
26
|
+
export async function init(
|
|
27
|
+
server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
|
|
28
|
+
config: SonamuInitConfig
|
|
29
|
+
) {
|
|
30
|
+
// 전체 라우팅 리스트
|
|
31
|
+
server.get(
|
|
32
|
+
`${config.prefix}/routes`,
|
|
33
|
+
async (_request, _reply): Promise<any> => {
|
|
34
|
+
return apis;
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Healthcheck API
|
|
39
|
+
server.get(
|
|
40
|
+
`${config.prefix}/healthcheck`,
|
|
41
|
+
async (_request, _reply): Promise<string> => {
|
|
42
|
+
return "ok";
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Syncer
|
|
47
|
+
const syncer = Syncer.getInstance({
|
|
48
|
+
appRootPath: config.appRootPath,
|
|
49
|
+
targets: config.syncTargets,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// DB 설정파일 확인
|
|
53
|
+
await DB.readKnexfile();
|
|
54
|
+
console.log(chalk.green("DB Config Loaded!"));
|
|
55
|
+
|
|
56
|
+
// Autoload: SMD / Models / Types / APIs
|
|
57
|
+
console.time(chalk.cyan("autoload&sync:"));
|
|
58
|
+
await SMDManager.autoload();
|
|
59
|
+
const importedModels = await syncer.autoloadModels(config.appRootPath);
|
|
60
|
+
const references = await syncer.autoloadTypes(config.appRootPath);
|
|
61
|
+
const apis = await syncer.autoloadApis(config.appRootPath);
|
|
62
|
+
if (isLocal()) {
|
|
63
|
+
await syncer.sync();
|
|
64
|
+
}
|
|
65
|
+
console.timeEnd(chalk.cyan("autoload&sync:"));
|
|
66
|
+
|
|
67
|
+
// API 라우팅 등록
|
|
68
|
+
apis.map((api) => {
|
|
69
|
+
// model
|
|
70
|
+
if (importedModels[api.modelName] === undefined) {
|
|
71
|
+
throw new Error(`정의되지 않은 모델에 접근 ${api.modelName}`);
|
|
72
|
+
}
|
|
73
|
+
const model = importedModels[api.modelName];
|
|
74
|
+
|
|
75
|
+
// 파라미터 정보로 zod 스키마 빌드
|
|
76
|
+
const ReqType = getZodObjectFromApi(api, references);
|
|
77
|
+
|
|
78
|
+
// route
|
|
79
|
+
server.route({
|
|
80
|
+
method: api.options.httpMethod!,
|
|
81
|
+
url: config.prefix + api.path,
|
|
82
|
+
handler: async (request, reply): Promise<unknown> => {
|
|
83
|
+
const which = api.options.httpMethod === "GET" ? "query" : "body";
|
|
84
|
+
let reqBody: {
|
|
85
|
+
[key: string]: unknown;
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
reqBody = fastifyCaster(ReqType).parse(request[which] ?? {});
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (e instanceof ZodError) {
|
|
91
|
+
// TODO: BadRequest 에러 핸들링 (ZodError issues를 humanize하여 출력하는 로직 필요)
|
|
92
|
+
throw new BadRequestException(
|
|
93
|
+
`${(e as ZodError).issues[0].message}`,
|
|
94
|
+
e.errors
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = await (model as any)[api.methodName].apply(
|
|
102
|
+
model,
|
|
103
|
+
api.parameters.map((param) => {
|
|
104
|
+
// Context 인젝션
|
|
105
|
+
if (ApiParamType.isContext(param.type)) {
|
|
106
|
+
return config.contextProvider(
|
|
107
|
+
{
|
|
108
|
+
headers: request.headers,
|
|
109
|
+
reply,
|
|
110
|
+
},
|
|
111
|
+
request,
|
|
112
|
+
reply
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
return reqBody[param.name];
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
reply.type(api.options.contentType ?? "application/json");
|
|
120
|
+
return result;
|
|
121
|
+
},
|
|
122
|
+
}); // END server.route
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function destroy(): Promise<void> {
|
|
127
|
+
await BaseModel.destroy();
|
|
128
|
+
}
|
package/src/bin/cli.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/* Global Begin */
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
console.log(chalk.bgBlue(`BEGIN ${new Date()}`));
|
|
4
|
+
|
|
5
|
+
import dotenv from "dotenv";
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { BaseModel } from "../database/base-model";
|
|
10
|
+
import { DB } from "../database/db";
|
|
11
|
+
import { SMDManager } from "../smd/smd-manager";
|
|
12
|
+
import { Migrator } from "../smd/migrator";
|
|
13
|
+
import { Syncer } from "../syncer/syncer";
|
|
14
|
+
import { FixtureManager } from "../testing/fixture-manager";
|
|
15
|
+
|
|
16
|
+
let migrator: Migrator;
|
|
17
|
+
let fixtureManager: FixtureManager;
|
|
18
|
+
|
|
19
|
+
async function bootstrap() {
|
|
20
|
+
// appRootPath 셋업
|
|
21
|
+
const appRootPath = path.resolve(process.cwd(), "..");
|
|
22
|
+
Syncer.getInstance({
|
|
23
|
+
appRootPath,
|
|
24
|
+
});
|
|
25
|
+
await DB.readKnexfile();
|
|
26
|
+
|
|
27
|
+
const [_0, _1, action, ...args] = process.argv;
|
|
28
|
+
switch (action) {
|
|
29
|
+
case "migrate":
|
|
30
|
+
await migrate(args[0] as MigrateSubAction);
|
|
31
|
+
break;
|
|
32
|
+
case "fixture":
|
|
33
|
+
await fixture(args[0] as FixtureSubAction, args.slice(1));
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
throw new Error(`Unknown action ${action}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
bootstrap().finally(async () => {
|
|
40
|
+
if (migrator) {
|
|
41
|
+
await migrator.destroy();
|
|
42
|
+
}
|
|
43
|
+
if (fixtureManager) {
|
|
44
|
+
await fixtureManager.destory();
|
|
45
|
+
}
|
|
46
|
+
await BaseModel.destroy();
|
|
47
|
+
|
|
48
|
+
/* Global End */
|
|
49
|
+
console.log(chalk.bgBlue(`END ${new Date()}\n`));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
type MigrateSubAction = "run" | "rollback" | "reset" | "clear";
|
|
53
|
+
async function migrate(subAction: MigrateSubAction) {
|
|
54
|
+
await SMDManager.autoload();
|
|
55
|
+
|
|
56
|
+
// migrator
|
|
57
|
+
migrator = new Migrator({
|
|
58
|
+
appRootPath: Syncer.getInstance().config.appRootPath,
|
|
59
|
+
knexfile: DB.getKnexfile(),
|
|
60
|
+
mode: "dev",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
switch (subAction) {
|
|
64
|
+
case "run":
|
|
65
|
+
await migrator.cleanUpDist();
|
|
66
|
+
await migrator.run();
|
|
67
|
+
break;
|
|
68
|
+
case "rollback":
|
|
69
|
+
await migrator.rollback();
|
|
70
|
+
break;
|
|
71
|
+
case "clear":
|
|
72
|
+
await migrator.clearPendingList();
|
|
73
|
+
break;
|
|
74
|
+
case "reset":
|
|
75
|
+
await migrator.resetAll();
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(`Unknown subAction - ${subAction}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type FixtureSubAction = "import" | "sync";
|
|
83
|
+
async function fixture(subAction: FixtureSubAction, extras?: string[]) {
|
|
84
|
+
await SMDManager.autoload();
|
|
85
|
+
|
|
86
|
+
fixtureManager = new FixtureManager();
|
|
87
|
+
|
|
88
|
+
switch (subAction) {
|
|
89
|
+
case "import":
|
|
90
|
+
if (!extras || Array.isArray(extras) === false || extras.length !== 2) {
|
|
91
|
+
throw new Error("Import 대상 smdId와 id가 필요합니다.");
|
|
92
|
+
}
|
|
93
|
+
const [smdId, idsString] = extras;
|
|
94
|
+
let ids: number[] = [];
|
|
95
|
+
if (idsString.includes(",")) {
|
|
96
|
+
ids = idsString
|
|
97
|
+
.split(",")
|
|
98
|
+
.map((idString) => Number(idString))
|
|
99
|
+
.filter((id) => Number.isNaN(id) === false);
|
|
100
|
+
} else {
|
|
101
|
+
ids = [Number(idsString)];
|
|
102
|
+
}
|
|
103
|
+
if (smdId === undefined || idsString === undefined || ids.length === 0) {
|
|
104
|
+
throw new Error("잘못된 입력");
|
|
105
|
+
}
|
|
106
|
+
await fixtureManager.importFixture(smdId, ids);
|
|
107
|
+
await fixtureManager.sync();
|
|
108
|
+
break;
|
|
109
|
+
case "sync":
|
|
110
|
+
await fixtureManager.sync();
|
|
111
|
+
break;
|
|
112
|
+
default:
|
|
113
|
+
throw new Error(`Unknown subAction - ${subAction}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
import { Knex } from "knex";
|
|
3
|
+
import { chunk, groupBy, isObject, omit, set, uniq } from "lodash";
|
|
4
|
+
import { attachOnDuplicateUpdate } from "./knex-plugins/knex-on-duplicate-update";
|
|
5
|
+
attachOnDuplicateUpdate();
|
|
6
|
+
import { DBPreset, DB } from "./db";
|
|
7
|
+
import { isCustomJoinClause, SubsetQuery } from "../types/types";
|
|
8
|
+
import { BaseListParams } from "../utils/model";
|
|
9
|
+
import { pluralize, underscore } from "inflection";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { UpsertBuilder } from "./upsert-builder";
|
|
12
|
+
|
|
13
|
+
export class BaseModelClass {
|
|
14
|
+
public modelName: string = "Unknown";
|
|
15
|
+
|
|
16
|
+
/* DB 인스턴스 get, destroy */
|
|
17
|
+
getDB(which: DBPreset): Knex {
|
|
18
|
+
return DB.getDB(which);
|
|
19
|
+
}
|
|
20
|
+
async destroy() {
|
|
21
|
+
return DB.destroy();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
myNow(timestamp?: number): string {
|
|
25
|
+
const dt: DateTime =
|
|
26
|
+
timestamp === undefined
|
|
27
|
+
? DateTime.local()
|
|
28
|
+
: DateTime.fromSeconds(timestamp);
|
|
29
|
+
return dt.toFormat("yyyy-MM-dd HH:mm:ss");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getInsertedIds(
|
|
33
|
+
wdb: Knex,
|
|
34
|
+
rows: any[],
|
|
35
|
+
tableName: string,
|
|
36
|
+
unqKeyFields: string[],
|
|
37
|
+
chunkSize: number = 500
|
|
38
|
+
) {
|
|
39
|
+
if (!wdb) {
|
|
40
|
+
wdb = this.getDB("w");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let unqKeys: string[];
|
|
44
|
+
let whereInField: any, selectField: string;
|
|
45
|
+
if (unqKeyFields.length > 1) {
|
|
46
|
+
whereInField = wdb.raw(`CONCAT_WS('_', '${unqKeyFields.join(",")}')`);
|
|
47
|
+
selectField = `${whereInField} as tmpUid`;
|
|
48
|
+
unqKeys = rows.map((row) =>
|
|
49
|
+
unqKeyFields.map((field) => row[field]).join("_")
|
|
50
|
+
);
|
|
51
|
+
} else {
|
|
52
|
+
whereInField = unqKeyFields[0];
|
|
53
|
+
selectField = unqKeyFields[0];
|
|
54
|
+
unqKeys = rows.map((row) => row[unqKeyFields[0]]);
|
|
55
|
+
}
|
|
56
|
+
const chunks = chunk(unqKeys, chunkSize);
|
|
57
|
+
|
|
58
|
+
let resultIds: number[] = [];
|
|
59
|
+
for (let chunk of chunks) {
|
|
60
|
+
const dbRows = await wdb(tableName)
|
|
61
|
+
.select("id", wdb.raw(selectField))
|
|
62
|
+
.whereIn(whereInField, chunk);
|
|
63
|
+
resultIds = resultIds.concat(
|
|
64
|
+
dbRows.map((dbRow: any) => parseInt(dbRow.id))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return resultIds;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async useLoaders(db: Knex, rows: any[], loaders: SubsetQuery["loaders"]) {
|
|
72
|
+
if (loaders.length > 0) {
|
|
73
|
+
for (let loader of loaders) {
|
|
74
|
+
let subQ: any;
|
|
75
|
+
let subRows: any[];
|
|
76
|
+
let toCol: string;
|
|
77
|
+
|
|
78
|
+
const fromIds = rows.map((row) => row[loader.manyJoin.idField]);
|
|
79
|
+
|
|
80
|
+
if (loader.manyJoin.through === undefined) {
|
|
81
|
+
// HasMany
|
|
82
|
+
subQ = db(loader.manyJoin.toTable)
|
|
83
|
+
.whereIn(loader.manyJoin.toCol, fromIds)
|
|
84
|
+
.select([...loader.select, loader.manyJoin.toCol]);
|
|
85
|
+
|
|
86
|
+
loader.oneJoins.map((join) => {
|
|
87
|
+
if (join.join == "inner") {
|
|
88
|
+
subQ.innerJoin(
|
|
89
|
+
`${join.table} as ${join.as}`,
|
|
90
|
+
this.getJoinClause(db, join)
|
|
91
|
+
);
|
|
92
|
+
} else if (join.join == "outer") {
|
|
93
|
+
subQ.leftOuterJoin(
|
|
94
|
+
`${join.table} as ${join.as}`,
|
|
95
|
+
this.getJoinClause(db, join)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
toCol = loader.manyJoin.toCol;
|
|
100
|
+
} else {
|
|
101
|
+
// ManyToMany
|
|
102
|
+
subQ = db(loader.manyJoin.through.table)
|
|
103
|
+
.join(
|
|
104
|
+
loader.manyJoin.toTable,
|
|
105
|
+
`${loader.manyJoin.through.table}.${loader.manyJoin.through.toCol}`,
|
|
106
|
+
`${loader.manyJoin.toTable}.${loader.manyJoin.toCol}`
|
|
107
|
+
)
|
|
108
|
+
.whereIn(loader.manyJoin.through.fromCol, fromIds)
|
|
109
|
+
.select(uniq([...loader.select, loader.manyJoin.through.fromCol]));
|
|
110
|
+
toCol = loader.manyJoin.through.fromCol;
|
|
111
|
+
}
|
|
112
|
+
subRows = await subQ;
|
|
113
|
+
|
|
114
|
+
if (loader.loaders) {
|
|
115
|
+
// 추가 -Many 케이스가 있는 경우 recursion 처리
|
|
116
|
+
subRows = await this.useLoaders(db, subRows, loader.loaders);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 불러온 row들을 참조ID 기준으로 분류 배치
|
|
120
|
+
const subRowGroups = groupBy(subRows, toCol);
|
|
121
|
+
rows = rows.map((row) => {
|
|
122
|
+
row[loader.as] = (
|
|
123
|
+
subRowGroups[row[loader.manyJoin.idField]] ?? []
|
|
124
|
+
).map((r) => omit(r, toCol));
|
|
125
|
+
return row;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return rows;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
hydrate<T>(rows: T[]): T[] {
|
|
133
|
+
return rows.map((row: any) => {
|
|
134
|
+
// nullable relation인 경우 관련된 필드가 전부 null로 생성되는 것 방지하는 코드
|
|
135
|
+
const nestedKeys = Object.keys(row).filter((key) => key.includes("__"));
|
|
136
|
+
const groups = groupBy(nestedKeys, (key) => key.split("__")[0]);
|
|
137
|
+
const nullKeys = Object.keys(groups).filter(
|
|
138
|
+
(key) =>
|
|
139
|
+
groups[key].length > 1 &&
|
|
140
|
+
groups[key].every((field) => row[field] === null)
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const hydrated = Object.keys(row).reduce((r, field) => {
|
|
144
|
+
if (!field.includes("__")) {
|
|
145
|
+
if (Array.isArray(row[field]) && isObject(row[field][0])) {
|
|
146
|
+
r[field] = this.hydrate(row[field]);
|
|
147
|
+
return r;
|
|
148
|
+
} else {
|
|
149
|
+
r[field] = row[field];
|
|
150
|
+
return r;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const parts = field.split("__");
|
|
155
|
+
const objPath =
|
|
156
|
+
parts[0] +
|
|
157
|
+
parts
|
|
158
|
+
.slice(1)
|
|
159
|
+
.map((part) => `[${part}]`)
|
|
160
|
+
.join("");
|
|
161
|
+
set(r, objPath, row[field]);
|
|
162
|
+
|
|
163
|
+
return r;
|
|
164
|
+
}, {} as any);
|
|
165
|
+
nullKeys.map((nullKey) => (hydrated[nullKey] = null));
|
|
166
|
+
|
|
167
|
+
return hydrated;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async runSubsetQuery<T extends BaseListParams, U extends string>({
|
|
172
|
+
params,
|
|
173
|
+
baseTable,
|
|
174
|
+
subset,
|
|
175
|
+
subsetQuery,
|
|
176
|
+
build,
|
|
177
|
+
debug,
|
|
178
|
+
}: {
|
|
179
|
+
subset: U;
|
|
180
|
+
params: T;
|
|
181
|
+
subsetQuery: SubsetQuery;
|
|
182
|
+
build: (buildParams: {
|
|
183
|
+
qb: Knex.QueryBuilder;
|
|
184
|
+
db: Knex;
|
|
185
|
+
select: string[];
|
|
186
|
+
joins: SubsetQuery["joins"];
|
|
187
|
+
virtual: string[];
|
|
188
|
+
}) => Knex.QueryBuilder;
|
|
189
|
+
baseTable?: string;
|
|
190
|
+
debug?: boolean | "list" | "count";
|
|
191
|
+
}): Promise<{
|
|
192
|
+
rows: any[];
|
|
193
|
+
total?: number | undefined;
|
|
194
|
+
subsetQuery: SubsetQuery;
|
|
195
|
+
}> {
|
|
196
|
+
const db = this.getDB(subset.startsWith("A") ? "w" : "r");
|
|
197
|
+
baseTable = baseTable ?? pluralize(underscore(this.modelName));
|
|
198
|
+
|
|
199
|
+
const { select, virtual, joins, loaders } = subsetQuery;
|
|
200
|
+
const qb = build({
|
|
201
|
+
qb: db.from(baseTable),
|
|
202
|
+
db,
|
|
203
|
+
select,
|
|
204
|
+
joins,
|
|
205
|
+
virtual,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// join
|
|
209
|
+
joins.map((join) => {
|
|
210
|
+
if (join.join == "inner") {
|
|
211
|
+
qb.innerJoin(
|
|
212
|
+
`${join.table} as ${join.as}`,
|
|
213
|
+
this.getJoinClause(db, join)
|
|
214
|
+
);
|
|
215
|
+
} else if (join.join == "outer") {
|
|
216
|
+
qb.leftOuterJoin(
|
|
217
|
+
`${join.table} as ${join.as}`,
|
|
218
|
+
this.getJoinClause(db, join)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// count
|
|
224
|
+
let countQuery;
|
|
225
|
+
if (params.id === undefined && params.withoutCount !== true) {
|
|
226
|
+
countQuery = qb.clone().clearOrder().count("*", { as: "total" });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// limit, offset
|
|
230
|
+
if (params.num !== 0) {
|
|
231
|
+
qb.limit(params.num!);
|
|
232
|
+
qb.offset(params.num! * (params.page! - 1));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// select, rows
|
|
236
|
+
qb.select(select);
|
|
237
|
+
const listQuery = qb.clone();
|
|
238
|
+
|
|
239
|
+
// debug: listQuery
|
|
240
|
+
if (debug === true || debug === "list") {
|
|
241
|
+
console.debug(
|
|
242
|
+
"DEBUG: list query",
|
|
243
|
+
chalk.blue(listQuery.toQuery().toString())
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// listQuery
|
|
248
|
+
let rows = await listQuery;
|
|
249
|
+
rows = await this.useLoaders(db, rows, loaders);
|
|
250
|
+
rows = this.hydrate(rows);
|
|
251
|
+
|
|
252
|
+
// countQuery
|
|
253
|
+
let total = 0;
|
|
254
|
+
if (countQuery) {
|
|
255
|
+
const countResult = await countQuery;
|
|
256
|
+
if (countResult && countResult[0] && countResult[0].total) {
|
|
257
|
+
total = countResult[0].total;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// debug: countQuery
|
|
261
|
+
if (debug === true || debug === "count") {
|
|
262
|
+
console.debug(
|
|
263
|
+
"DEBUG: count query",
|
|
264
|
+
chalk.blue(countQuery.toQuery().toString())
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { rows, total, subsetQuery };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
getJoinClause(
|
|
273
|
+
db: Knex<any, unknown>,
|
|
274
|
+
join: SubsetQuery["joins"][number]
|
|
275
|
+
): Knex.Raw<any> {
|
|
276
|
+
if (!isCustomJoinClause(join)) {
|
|
277
|
+
return db.raw(`${join.from} = ${join.to}`);
|
|
278
|
+
} else {
|
|
279
|
+
return db.raw(join.custom);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
getUpsertBuilder(): UpsertBuilder {
|
|
284
|
+
return new UpsertBuilder();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export const BaseModel = new BaseModelClass();
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type DBPreset = "w" | "r";
|
|
2
|
+
import knex, { Knex } from "knex";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { ServiceUnavailableException } from "../exceptions/so-exceptions";
|
|
5
|
+
import { Syncer } from "../syncer";
|
|
6
|
+
|
|
7
|
+
export type SonamuDBConfig = {
|
|
8
|
+
development_master: Knex.Config;
|
|
9
|
+
development_slave: Knex.Config;
|
|
10
|
+
test: Knex.Config;
|
|
11
|
+
fixture_local: Knex.Config;
|
|
12
|
+
fixture_remote: Knex.Config;
|
|
13
|
+
production_master: Knex.Config;
|
|
14
|
+
production_slave: Knex.Config;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
class DBClass {
|
|
18
|
+
private wdb?: Knex;
|
|
19
|
+
private rdb?: Knex;
|
|
20
|
+
private knexfile?: SonamuDBConfig;
|
|
21
|
+
|
|
22
|
+
async readKnexfile() {
|
|
23
|
+
const configPath: string = path.join(
|
|
24
|
+
Syncer.getInstance().config.appRootPath,
|
|
25
|
+
"/api/dist/configs/db"
|
|
26
|
+
);
|
|
27
|
+
try {
|
|
28
|
+
const knexfileModule = await import(configPath);
|
|
29
|
+
this.knexfile = knexfileModule.default as SonamuDBConfig;
|
|
30
|
+
return this.knexfile;
|
|
31
|
+
} catch {}
|
|
32
|
+
|
|
33
|
+
throw new ServiceUnavailableException(
|
|
34
|
+
`DB설정 파일을 찾을 수 없습니다. ${configPath}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getKnexfile(): SonamuDBConfig {
|
|
39
|
+
if (this.knexfile) {
|
|
40
|
+
return this.knexfile;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw new ServiceUnavailableException("DB설정 파일을 찾을 수 없습니다.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getDB(which: DBPreset): Knex {
|
|
47
|
+
const knexfile = this.getKnexfile();
|
|
48
|
+
|
|
49
|
+
const instanceName = which === "w" ? "wdb" : "rdb";
|
|
50
|
+
|
|
51
|
+
if (!this[instanceName]) {
|
|
52
|
+
let config: Knex.Config;
|
|
53
|
+
switch (process.env.NODE_ENV ?? "development") {
|
|
54
|
+
case "development":
|
|
55
|
+
case "staging":
|
|
56
|
+
config =
|
|
57
|
+
which === "w"
|
|
58
|
+
? knexfile["development_master"]
|
|
59
|
+
: knexfile["development_slave"] ?? knexfile["development_master"];
|
|
60
|
+
break;
|
|
61
|
+
case "production":
|
|
62
|
+
config =
|
|
63
|
+
which === "w"
|
|
64
|
+
? knexfile["production_master"]
|
|
65
|
+
: knexfile["production_slave"] ?? knexfile["production_master"];
|
|
66
|
+
break;
|
|
67
|
+
case "test":
|
|
68
|
+
config = knexfile["test"];
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
throw new Error(
|
|
72
|
+
`현재 ENV ${process.env.NODE_ENV}에는 설정 가능한 DB설정이 없습니다.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
this[instanceName] = knex(config);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return this[instanceName]!;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async destroy(): Promise<void> {
|
|
82
|
+
if (this.wdb !== undefined) {
|
|
83
|
+
if (this.wdb.destroy === undefined) {
|
|
84
|
+
console.log(this.wdb);
|
|
85
|
+
}
|
|
86
|
+
await this.wdb.destroy();
|
|
87
|
+
this.wdb = undefined;
|
|
88
|
+
}
|
|
89
|
+
if (this.rdb !== undefined) {
|
|
90
|
+
await this.rdb.destroy();
|
|
91
|
+
this.rdb = undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export const DB = new DBClass();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import knex from "knex";
|
|
2
|
+
|
|
3
|
+
export function attachOnDuplicateUpdate() {
|
|
4
|
+
knex.QueryBuilder.extend("onDuplicateUpdate", function (...columns) {
|
|
5
|
+
if (columns.length === 0) {
|
|
6
|
+
// 업데이트 할 컬럼이 없으면 onDuplicateUpdate 구문 처리 패스
|
|
7
|
+
const { sql: originalSQL, bindings: originalBindings } = this.toSQL();
|
|
8
|
+
return this.client.raw(originalSQL, originalBindings);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { placeholders, bindings } = columns.reduce(
|
|
12
|
+
(result, column) => {
|
|
13
|
+
if (typeof column === "string") {
|
|
14
|
+
result.placeholders.push(`?? = Values(??)`);
|
|
15
|
+
result.bindings.push(column, column);
|
|
16
|
+
} else if (column && typeof column === "object") {
|
|
17
|
+
Object.keys(column).forEach((key) => {
|
|
18
|
+
result.placeholders.push(`?? = ?`);
|
|
19
|
+
result.bindings.push(key, column[key]);
|
|
20
|
+
});
|
|
21
|
+
} else {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"onDuplicateUpdate error: expected column name to be string or object."
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return result;
|
|
28
|
+
},
|
|
29
|
+
{ placeholders: [], bindings: [] }
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const { sql: originalSQL, bindings: originalBindings } = this.toSQL();
|
|
33
|
+
|
|
34
|
+
const newBindings = [...originalBindings, ...bindings];
|
|
35
|
+
|
|
36
|
+
return this.client.raw(
|
|
37
|
+
`${originalSQL} ON DUPLICATE KEY UPDATE ${placeholders.join(", ")}`,
|
|
38
|
+
newBindings
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
}
|