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.
Files changed (60) hide show
  1. package/.pnp.cjs +15552 -0
  2. package/.pnp.loader.mjs +285 -0
  3. package/.vscode/extensions.json +6 -0
  4. package/.vscode/settings.json +9 -0
  5. package/.yarnrc.yml +5 -0
  6. package/dist/bin/cli.d.ts +2 -0
  7. package/dist/bin/cli.d.ts.map +1 -0
  8. package/dist/bin/cli.js +123 -0
  9. package/dist/bin/cli.js.map +1 -0
  10. package/dist/index.js +34 -0
  11. package/package.json +60 -0
  12. package/src/api/caster.ts +72 -0
  13. package/src/api/code-converters.ts +552 -0
  14. package/src/api/context.ts +20 -0
  15. package/src/api/decorators.ts +63 -0
  16. package/src/api/index.ts +5 -0
  17. package/src/api/init.ts +128 -0
  18. package/src/bin/cli.ts +115 -0
  19. package/src/database/base-model.ts +287 -0
  20. package/src/database/db.ts +95 -0
  21. package/src/database/knex-plugins/knex-on-duplicate-update.ts +41 -0
  22. package/src/database/upsert-builder.ts +231 -0
  23. package/src/exceptions/error-handler.ts +29 -0
  24. package/src/exceptions/so-exceptions.ts +91 -0
  25. package/src/index.ts +17 -0
  26. package/src/shared/web.shared.ts.txt +119 -0
  27. package/src/smd/migrator.ts +1462 -0
  28. package/src/smd/smd-manager.ts +141 -0
  29. package/src/smd/smd-utils.ts +266 -0
  30. package/src/smd/smd.ts +533 -0
  31. package/src/syncer/index.ts +1 -0
  32. package/src/syncer/syncer.ts +1283 -0
  33. package/src/templates/base-template.ts +19 -0
  34. package/src/templates/generated.template.ts +247 -0
  35. package/src/templates/generated_http.template.ts +114 -0
  36. package/src/templates/index.ts +1 -0
  37. package/src/templates/init_enums.template.ts +71 -0
  38. package/src/templates/init_generated.template.ts +44 -0
  39. package/src/templates/init_types.template.ts +38 -0
  40. package/src/templates/model.template.ts +168 -0
  41. package/src/templates/model_test.template.ts +39 -0
  42. package/src/templates/service.template.ts +263 -0
  43. package/src/templates/smd.template.ts +49 -0
  44. package/src/templates/view_enums_buttonset.template.ts +34 -0
  45. package/src/templates/view_enums_dropdown.template.ts +67 -0
  46. package/src/templates/view_enums_select.template.ts +60 -0
  47. package/src/templates/view_form.template.ts +397 -0
  48. package/src/templates/view_id_all_select.template.ts +34 -0
  49. package/src/templates/view_id_async_select.template.ts +113 -0
  50. package/src/templates/view_list.template.ts +652 -0
  51. package/src/templates/view_list_columns.template.ts +59 -0
  52. package/src/templates/view_search_input.template.ts +67 -0
  53. package/src/testing/fixture-manager.ts +271 -0
  54. package/src/types/types.ts +668 -0
  55. package/src/typings/knex.d.ts +24 -0
  56. package/src/utils/controller.ts +21 -0
  57. package/src/utils/lodash-able.ts +11 -0
  58. package/src/utils/model.ts +33 -0
  59. package/src/utils/utils.ts +28 -0
  60. package/tsconfig.json +47 -0
@@ -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
+ }