@simplysm/orm-node 13.0.0-beta.6
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/.cache/typecheck-node.tsbuildinfo +1 -0
- package/README.md +418 -0
- package/dist/connections/mssql-db-conn.js +386 -0
- package/dist/connections/mssql-db-conn.js.map +7 -0
- package/dist/connections/mysql-db-conn.js +227 -0
- package/dist/connections/mysql-db-conn.js.map +7 -0
- package/dist/connections/postgresql-db-conn.js +191 -0
- package/dist/connections/postgresql-db-conn.js.map +7 -0
- package/dist/core-common/src/common.types.d.ts +74 -0
- package/dist/core-common/src/common.types.d.ts.map +1 -0
- package/dist/core-common/src/env.d.ts +6 -0
- package/dist/core-common/src/env.d.ts.map +1 -0
- package/dist/core-common/src/errors/argument-error.d.ts +25 -0
- package/dist/core-common/src/errors/argument-error.d.ts.map +1 -0
- package/dist/core-common/src/errors/not-implemented-error.d.ts +29 -0
- package/dist/core-common/src/errors/not-implemented-error.d.ts.map +1 -0
- package/dist/core-common/src/errors/sd-error.d.ts +27 -0
- package/dist/core-common/src/errors/sd-error.d.ts.map +1 -0
- package/dist/core-common/src/errors/timeout-error.d.ts +31 -0
- package/dist/core-common/src/errors/timeout-error.d.ts.map +1 -0
- package/dist/core-common/src/extensions/arr-ext.d.ts +15 -0
- package/dist/core-common/src/extensions/arr-ext.d.ts.map +1 -0
- package/dist/core-common/src/extensions/arr-ext.helpers.d.ts +19 -0
- package/dist/core-common/src/extensions/arr-ext.helpers.d.ts.map +1 -0
- package/dist/core-common/src/extensions/arr-ext.types.d.ts +215 -0
- package/dist/core-common/src/extensions/arr-ext.types.d.ts.map +1 -0
- package/dist/core-common/src/extensions/map-ext.d.ts +57 -0
- package/dist/core-common/src/extensions/map-ext.d.ts.map +1 -0
- package/dist/core-common/src/extensions/set-ext.d.ts +36 -0
- package/dist/core-common/src/extensions/set-ext.d.ts.map +1 -0
- package/dist/core-common/src/features/debounce-queue.d.ts +53 -0
- package/dist/core-common/src/features/debounce-queue.d.ts.map +1 -0
- package/dist/core-common/src/features/event-emitter.d.ts +66 -0
- package/dist/core-common/src/features/event-emitter.d.ts.map +1 -0
- package/dist/core-common/src/features/serial-queue.d.ts +47 -0
- package/dist/core-common/src/features/serial-queue.d.ts.map +1 -0
- package/dist/core-common/src/index.d.ts +32 -0
- package/dist/core-common/src/index.d.ts.map +1 -0
- package/dist/core-common/src/types/date-only.d.ts +152 -0
- package/dist/core-common/src/types/date-only.d.ts.map +1 -0
- package/dist/core-common/src/types/date-time.d.ts +96 -0
- package/dist/core-common/src/types/date-time.d.ts.map +1 -0
- package/dist/core-common/src/types/lazy-gc-map.d.ts +80 -0
- package/dist/core-common/src/types/lazy-gc-map.d.ts.map +1 -0
- package/dist/core-common/src/types/time.d.ts +68 -0
- package/dist/core-common/src/types/time.d.ts.map +1 -0
- package/dist/core-common/src/types/uuid.d.ts +35 -0
- package/dist/core-common/src/types/uuid.d.ts.map +1 -0
- package/dist/core-common/src/utils/bytes.d.ts +51 -0
- package/dist/core-common/src/utils/bytes.d.ts.map +1 -0
- package/dist/core-common/src/utils/date-format.d.ts +90 -0
- package/dist/core-common/src/utils/date-format.d.ts.map +1 -0
- package/dist/core-common/src/utils/json.d.ts +34 -0
- package/dist/core-common/src/utils/json.d.ts.map +1 -0
- package/dist/core-common/src/utils/num.d.ts +60 -0
- package/dist/core-common/src/utils/num.d.ts.map +1 -0
- package/dist/core-common/src/utils/obj.d.ts +258 -0
- package/dist/core-common/src/utils/obj.d.ts.map +1 -0
- package/dist/core-common/src/utils/path.d.ts +23 -0
- package/dist/core-common/src/utils/path.d.ts.map +1 -0
- package/dist/core-common/src/utils/primitive.d.ts +18 -0
- package/dist/core-common/src/utils/primitive.d.ts.map +1 -0
- package/dist/core-common/src/utils/str.d.ts +103 -0
- package/dist/core-common/src/utils/str.d.ts.map +1 -0
- package/dist/core-common/src/utils/template-strings.d.ts +84 -0
- package/dist/core-common/src/utils/template-strings.d.ts.map +1 -0
- package/dist/core-common/src/utils/transferable.d.ts +47 -0
- package/dist/core-common/src/utils/transferable.d.ts.map +1 -0
- package/dist/core-common/src/utils/wait.d.ts +19 -0
- package/dist/core-common/src/utils/wait.d.ts.map +1 -0
- package/dist/core-common/src/utils/xml.d.ts +36 -0
- package/dist/core-common/src/utils/xml.d.ts.map +1 -0
- package/dist/core-common/src/zip/sd-zip.d.ts +80 -0
- package/dist/core-common/src/zip/sd-zip.d.ts.map +1 -0
- package/dist/db-conn-factory.js +88 -0
- package/dist/db-conn-factory.js.map +7 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +7 -0
- package/dist/node-db-context-executor.js +129 -0
- package/dist/node-db-context-executor.js.map +7 -0
- package/dist/orm-common/src/db-context.d.ts +669 -0
- package/dist/orm-common/src/db-context.d.ts.map +1 -0
- package/dist/orm-common/src/errors/db-transaction-error.d.ts +51 -0
- package/dist/orm-common/src/errors/db-transaction-error.d.ts.map +1 -0
- package/dist/orm-common/src/exec/executable.d.ts +79 -0
- package/dist/orm-common/src/exec/executable.d.ts.map +1 -0
- package/dist/orm-common/src/exec/queryable.d.ts +708 -0
- package/dist/orm-common/src/exec/queryable.d.ts.map +1 -0
- package/dist/orm-common/src/exec/search-parser.d.ts +72 -0
- package/dist/orm-common/src/exec/search-parser.d.ts.map +1 -0
- package/dist/orm-common/src/expr/expr-unit.d.ts +25 -0
- package/dist/orm-common/src/expr/expr-unit.d.ts.map +1 -0
- package/dist/orm-common/src/expr/expr.d.ts +1369 -0
- package/dist/orm-common/src/expr/expr.d.ts.map +1 -0
- package/dist/orm-common/src/index.d.ts +32 -0
- package/dist/orm-common/src/index.d.ts.map +1 -0
- package/dist/orm-common/src/models/system-migration.d.ts +10 -0
- package/dist/orm-common/src/models/system-migration.d.ts.map +1 -0
- package/dist/orm-common/src/query-builder/base/expr-renderer-base.d.ts +95 -0
- package/dist/orm-common/src/query-builder/base/expr-renderer-base.d.ts.map +1 -0
- package/dist/orm-common/src/query-builder/base/query-builder-base.d.ts +66 -0
- package/dist/orm-common/src/query-builder/base/query-builder-base.d.ts.map +1 -0
- package/dist/orm-common/src/query-builder/mssql/mssql-expr-renderer.d.ts +84 -0
- package/dist/orm-common/src/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -0
- package/dist/orm-common/src/query-builder/mssql/mssql-query-builder.d.ts +45 -0
- package/dist/orm-common/src/query-builder/mssql/mssql-query-builder.d.ts.map +1 -0
- package/dist/orm-common/src/query-builder/mysql/mysql-expr-renderer.d.ts +84 -0
- package/dist/orm-common/src/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -0
- package/dist/orm-common/src/query-builder/mysql/mysql-query-builder.d.ts +54 -0
- package/dist/orm-common/src/query-builder/mysql/mysql-query-builder.d.ts.map +1 -0
- package/dist/orm-common/src/query-builder/postgresql/postgresql-expr-renderer.d.ts +84 -0
- package/dist/orm-common/src/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -0
- package/dist/orm-common/src/query-builder/postgresql/postgresql-query-builder.d.ts +52 -0
- package/dist/orm-common/src/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -0
- package/dist/orm-common/src/query-builder/query-builder.d.ts +7 -0
- package/dist/orm-common/src/query-builder/query-builder.d.ts.map +1 -0
- package/dist/orm-common/src/schema/factory/column-builder.d.ts +394 -0
- package/dist/orm-common/src/schema/factory/column-builder.d.ts.map +1 -0
- package/dist/orm-common/src/schema/factory/index-builder.d.ts +151 -0
- package/dist/orm-common/src/schema/factory/index-builder.d.ts.map +1 -0
- package/dist/orm-common/src/schema/factory/relation-builder.d.ts +337 -0
- package/dist/orm-common/src/schema/factory/relation-builder.d.ts.map +1 -0
- package/dist/orm-common/src/schema/procedure-builder.d.ts +202 -0
- package/dist/orm-common/src/schema/procedure-builder.d.ts.map +1 -0
- package/dist/orm-common/src/schema/table-builder.d.ts +259 -0
- package/dist/orm-common/src/schema/table-builder.d.ts.map +1 -0
- package/dist/orm-common/src/schema/view-builder.d.ts +183 -0
- package/dist/orm-common/src/schema/view-builder.d.ts.map +1 -0
- package/dist/orm-common/src/types/column.d.ts +172 -0
- package/dist/orm-common/src/types/column.d.ts.map +1 -0
- package/dist/orm-common/src/types/db.d.ts +175 -0
- package/dist/orm-common/src/types/db.d.ts.map +1 -0
- package/dist/orm-common/src/types/expr.d.ts +474 -0
- package/dist/orm-common/src/types/expr.d.ts.map +1 -0
- package/dist/orm-common/src/types/query-def.d.ts +351 -0
- package/dist/orm-common/src/types/query-def.d.ts.map +1 -0
- package/dist/orm-common/src/utils/result-parser.d.ts +38 -0
- package/dist/orm-common/src/utils/result-parser.d.ts.map +1 -0
- package/dist/orm-node/src/connections/mssql-db-conn.d.ts +44 -0
- package/dist/orm-node/src/connections/mssql-db-conn.d.ts.map +1 -0
- package/dist/orm-node/src/connections/mysql-db-conn.d.ts +38 -0
- package/dist/orm-node/src/connections/mysql-db-conn.d.ts.map +1 -0
- package/dist/orm-node/src/connections/postgresql-db-conn.d.ts +39 -0
- package/dist/orm-node/src/connections/postgresql-db-conn.d.ts.map +1 -0
- package/dist/orm-node/src/db-conn-factory.d.ts +25 -0
- package/dist/orm-node/src/db-conn-factory.d.ts.map +1 -0
- package/dist/orm-node/src/index.d.ts +9 -0
- package/dist/orm-node/src/index.d.ts.map +1 -0
- package/dist/orm-node/src/node-db-context-executor.d.ts +77 -0
- package/dist/orm-node/src/node-db-context-executor.d.ts.map +1 -0
- package/dist/orm-node/src/pooled-db-conn.d.ts +79 -0
- package/dist/orm-node/src/pooled-db-conn.d.ts.map +1 -0
- package/dist/orm-node/src/sd-orm.d.ts +78 -0
- package/dist/orm-node/src/sd-orm.d.ts.map +1 -0
- package/dist/orm-node/src/types/db-conn.d.ts +159 -0
- package/dist/orm-node/src/types/db-conn.d.ts.map +1 -0
- package/dist/pooled-db-conn.js +134 -0
- package/dist/pooled-db-conn.js.map +7 -0
- package/dist/sd-orm.js +44 -0
- package/dist/sd-orm.js.map +7 -0
- package/dist/types/db-conn.js +17 -0
- package/dist/types/db-conn.js.map +7 -0
- package/package.json +50 -0
- package/src/connections/mssql-db-conn.ts +483 -0
- package/src/connections/mysql-db-conn.ts +299 -0
- package/src/connections/postgresql-db-conn.ts +254 -0
- package/src/db-conn-factory.ts +114 -0
- package/src/index.ts +13 -0
- package/src/node-db-context-executor.ts +162 -0
- package/src/pooled-db-conn.ts +175 -0
- package/src/sd-orm.ts +102 -0
- package/src/types/db-conn.ts +196 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import type { Connection } from "mysql2/promise";
|
|
6
|
+
import { createConsola } from "consola";
|
|
7
|
+
import {
|
|
8
|
+
bytesToHex,
|
|
9
|
+
DateOnly,
|
|
10
|
+
DateTime,
|
|
11
|
+
SdError,
|
|
12
|
+
EventEmitter,
|
|
13
|
+
strIsNullOrEmpty,
|
|
14
|
+
Time,
|
|
15
|
+
Uuid,
|
|
16
|
+
} from "@simplysm/core-common";
|
|
17
|
+
import type { ColumnMeta, DataType, IsolationLevel } from "@simplysm/orm-common";
|
|
18
|
+
import { DB_CONN_DEFAULT_TIMEOUT, DB_CONN_ERRORS, type DbConn, type MysqlDbConnConfig } from "../types/db-conn";
|
|
19
|
+
|
|
20
|
+
const logger = createConsola().withTag("mysql-db-conn");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* MySQL 데이터베이스 연결 클래스
|
|
24
|
+
*
|
|
25
|
+
* mysql2/promise 라이브러리를 사용하여 MySQL 연결을 관리합니다.
|
|
26
|
+
*/
|
|
27
|
+
export class MysqlDbConn extends EventEmitter<{ close: void }> implements DbConn {
|
|
28
|
+
private static readonly _ROOT_USER = "root";
|
|
29
|
+
private readonly _timeout = DB_CONN_DEFAULT_TIMEOUT;
|
|
30
|
+
|
|
31
|
+
private _conn?: Connection;
|
|
32
|
+
private _connTimeout?: ReturnType<typeof setTimeout>;
|
|
33
|
+
|
|
34
|
+
isConnected = false;
|
|
35
|
+
isOnTransaction = false;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
private readonly _mysql2: typeof import("mysql2/promise"),
|
|
39
|
+
readonly config: MysqlDbConnConfig,
|
|
40
|
+
) {
|
|
41
|
+
super();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async connect(): Promise<void> {
|
|
45
|
+
if (this.isConnected) {
|
|
46
|
+
throw new SdError(DB_CONN_ERRORS.ALREADY_CONNECTED);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const conn = await this._mysql2.createConnection({
|
|
50
|
+
host: this.config.host,
|
|
51
|
+
port: this.config.port,
|
|
52
|
+
user: this.config.username,
|
|
53
|
+
password: this.config.password,
|
|
54
|
+
// root 사용자는 특정 database에 바인딩되지 않고 연결하여
|
|
55
|
+
// 모든 데이터베이스에 접근할 수 있도록 함 (관리 작업용)
|
|
56
|
+
database: this.config.username === MysqlDbConn._ROOT_USER ? undefined : this.config.database,
|
|
57
|
+
multipleStatements: true,
|
|
58
|
+
charset: "utf8mb4",
|
|
59
|
+
infileStreamFactory: (filePath: string) => fs.createReadStream(filePath), // LOAD DATA LOCAL INFILE 지원
|
|
60
|
+
} as Parameters<typeof this._mysql2.createConnection>[0]);
|
|
61
|
+
|
|
62
|
+
conn.on("end", () => {
|
|
63
|
+
this.emit("close");
|
|
64
|
+
this._resetState();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
conn.on("error", (error) => {
|
|
68
|
+
logger.error("DB 연결 오류", error.message);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this._conn = conn;
|
|
72
|
+
this._startTimeout();
|
|
73
|
+
this.isConnected = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async close(): Promise<void> {
|
|
77
|
+
this._stopTimeout();
|
|
78
|
+
|
|
79
|
+
if (this._conn == null || !this.isConnected) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await this._conn.end();
|
|
84
|
+
|
|
85
|
+
this.emit("close");
|
|
86
|
+
this._resetState();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async beginTransaction(isolationLevel?: IsolationLevel): Promise<void> {
|
|
90
|
+
const conn = this._assertConnected();
|
|
91
|
+
|
|
92
|
+
const level = (isolationLevel ?? this.config.defaultIsolationLevel ?? "READ_UNCOMMITTED").replace(/_/g, " ");
|
|
93
|
+
|
|
94
|
+
// 격리 수준을 먼저 설정 (다음 트랜잭션에 적용됨)
|
|
95
|
+
await conn.query({
|
|
96
|
+
sql: `SET SESSION TRANSACTION ISOLATION LEVEL ${level}`,
|
|
97
|
+
timeout: this._timeout,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 그 다음 트랜잭션 시작
|
|
101
|
+
await conn.beginTransaction();
|
|
102
|
+
|
|
103
|
+
this.isOnTransaction = true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async commitTransaction(): Promise<void> {
|
|
107
|
+
const conn = this._assertConnected();
|
|
108
|
+
await conn.commit();
|
|
109
|
+
this.isOnTransaction = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async rollbackTransaction(): Promise<void> {
|
|
113
|
+
const conn = this._assertConnected();
|
|
114
|
+
await conn.rollback();
|
|
115
|
+
this.isOnTransaction = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async execute(queries: string[]): Promise<unknown[][]> {
|
|
119
|
+
const results: unknown[][] = [];
|
|
120
|
+
for (const query of queries.filter((item) => !strIsNullOrEmpty(item))) {
|
|
121
|
+
const resultItems = await this.executeParametrized(query);
|
|
122
|
+
results.push(...resultItems);
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async executeParametrized(query: string, params?: unknown[]): Promise<unknown[][]> {
|
|
128
|
+
const conn = this._assertConnected();
|
|
129
|
+
|
|
130
|
+
logger.debug("쿼리 실행", { queryLength: query.length, params });
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const [queryResults] = await conn.query({
|
|
134
|
+
sql: query,
|
|
135
|
+
timeout: this._timeout,
|
|
136
|
+
values: params,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
this._startTimeout();
|
|
140
|
+
|
|
141
|
+
// MySQL은 INSERT/UPDATE/DELETE 문에 대해 ResultSetHeader를 반환함
|
|
142
|
+
// SELECT 결과만 추출하기 위해 ResultSetHeader 객체를 필터링함
|
|
143
|
+
// ResultSetHeader는 affectedRows, fieldCount 등의 필드를 가지고 있음
|
|
144
|
+
const result: unknown[] = [];
|
|
145
|
+
if (queryResults instanceof Array) {
|
|
146
|
+
for (const queryResult of queryResults.filter(
|
|
147
|
+
(item: unknown) =>
|
|
148
|
+
!(typeof item === "object" && item !== null && "affectedRows" in item && "fieldCount" in item),
|
|
149
|
+
)) {
|
|
150
|
+
result.push(queryResult);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return [result];
|
|
155
|
+
} catch (err) {
|
|
156
|
+
this._startTimeout();
|
|
157
|
+
const error = err as Error & { sql?: string };
|
|
158
|
+
throw new SdError(
|
|
159
|
+
error,
|
|
160
|
+
"쿼리 수행중 오류발생" + (error.sql != null ? "\n-- query\n" + error.sql.trim() + "\n--" : ""),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async bulkInsert(
|
|
166
|
+
tableName: string,
|
|
167
|
+
columnMetas: Record<string, ColumnMeta>,
|
|
168
|
+
records: Record<string, unknown>[],
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
const conn = this._assertConnected();
|
|
171
|
+
|
|
172
|
+
if (records.length === 0) return;
|
|
173
|
+
|
|
174
|
+
const colNames = Object.keys(columnMetas);
|
|
175
|
+
|
|
176
|
+
// 임시 CSV 파일 생성
|
|
177
|
+
const tmpDir = os.tmpdir();
|
|
178
|
+
const tmpFile = path.join(tmpDir, `mysql_bulk_${randomUUID()}.csv`);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// CSV 데이터 생성
|
|
182
|
+
const csvLines: string[] = [];
|
|
183
|
+
for (const record of records) {
|
|
184
|
+
const row = colNames.map((colName) => this._escapeForCsv(record[colName], columnMetas[colName].dataType));
|
|
185
|
+
csvLines.push(row.join("\t"));
|
|
186
|
+
}
|
|
187
|
+
const csvContent = csvLines.join("\n");
|
|
188
|
+
|
|
189
|
+
// 파일 쓰기
|
|
190
|
+
await fs.promises.writeFile(tmpFile, csvContent, "utf8");
|
|
191
|
+
|
|
192
|
+
// UUID/binary 컬럼은 임시 변수로 읽고 SET 절에서 UNHEX() 변환
|
|
193
|
+
const binaryColNames = colNames.filter((c) => {
|
|
194
|
+
const dt = columnMetas[c].dataType.type;
|
|
195
|
+
return dt === "uuid" || dt === "binary";
|
|
196
|
+
});
|
|
197
|
+
const normalCols = colNames.map((c) => {
|
|
198
|
+
if (binaryColNames.includes(c)) return `@_${c}`;
|
|
199
|
+
return `\`${c}\``;
|
|
200
|
+
});
|
|
201
|
+
const setClauses = binaryColNames.map((c) => `\`${c}\` = UNHEX(@_${c})`);
|
|
202
|
+
|
|
203
|
+
// LOAD DATA LOCAL INFILE 실행
|
|
204
|
+
let query = `LOAD DATA LOCAL INFILE ? INTO TABLE ${tableName} FIELDS TERMINATED BY '\\t' LINES TERMINATED BY '\\n' (${normalCols.join(", ")})`;
|
|
205
|
+
if (setClauses.length > 0) {
|
|
206
|
+
query += ` SET ${setClauses.join(", ")}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await conn.query({ sql: query, timeout: this._timeout, values: [tmpFile] });
|
|
210
|
+
} finally {
|
|
211
|
+
// 임시 파일 삭제
|
|
212
|
+
try {
|
|
213
|
+
await fs.promises.unlink(tmpFile);
|
|
214
|
+
} catch {
|
|
215
|
+
// 삭제 실패 무시
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─────────────────────────────────────────────
|
|
221
|
+
// Private helpers
|
|
222
|
+
// ─────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* MySQL LOAD DATA INFILE용 값 이스케이프
|
|
226
|
+
*/
|
|
227
|
+
private _escapeForCsv(value: unknown, dataType: DataType): string {
|
|
228
|
+
if (value == null) {
|
|
229
|
+
return "\\N"; // MySQL NULL 표현
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
switch (dataType.type) {
|
|
233
|
+
case "int":
|
|
234
|
+
case "bigint":
|
|
235
|
+
case "float":
|
|
236
|
+
case "double":
|
|
237
|
+
case "decimal":
|
|
238
|
+
return String(value);
|
|
239
|
+
|
|
240
|
+
case "boolean":
|
|
241
|
+
return (value as boolean) ? "1" : "0";
|
|
242
|
+
|
|
243
|
+
case "varchar":
|
|
244
|
+
case "char":
|
|
245
|
+
case "text": {
|
|
246
|
+
const str = value as string;
|
|
247
|
+
// 탭, 줄바꿈, 백슬래시 이스케이프
|
|
248
|
+
return str.replace(/\\/g, "\\\\").replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case "datetime":
|
|
252
|
+
return (value as DateTime).toFormatString("yyyy-MM-dd HH:mm:ss.fff");
|
|
253
|
+
|
|
254
|
+
case "date":
|
|
255
|
+
return (value as DateOnly).toFormatString("yyyy-MM-dd");
|
|
256
|
+
|
|
257
|
+
case "time":
|
|
258
|
+
return (value as Time).toFormatString("HH:mm:ss");
|
|
259
|
+
|
|
260
|
+
case "uuid":
|
|
261
|
+
return (value as Uuid).toString().replace(/-/g, ""); // BINARY(16) 저장용 hex
|
|
262
|
+
|
|
263
|
+
case "binary":
|
|
264
|
+
return bytesToHex(value as Uint8Array);
|
|
265
|
+
|
|
266
|
+
default:
|
|
267
|
+
throw new SdError(`지원하지 않는 DataType: ${JSON.stringify(dataType)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private _assertConnected(): Connection {
|
|
272
|
+
if (this._conn == null || !this.isConnected) {
|
|
273
|
+
throw new SdError(DB_CONN_ERRORS.NOT_CONNECTED);
|
|
274
|
+
}
|
|
275
|
+
this._startTimeout();
|
|
276
|
+
return this._conn;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private _resetState(): void {
|
|
280
|
+
this.isConnected = false;
|
|
281
|
+
this.isOnTransaction = false;
|
|
282
|
+
this._conn = undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private _stopTimeout(): void {
|
|
286
|
+
if (this._connTimeout != null) {
|
|
287
|
+
clearTimeout(this._connTimeout);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private _startTimeout(): void {
|
|
292
|
+
this._stopTimeout();
|
|
293
|
+
this._connTimeout = setTimeout(() => {
|
|
294
|
+
this.close().catch((err) => {
|
|
295
|
+
logger.error("close error", err instanceof Error ? err.message : String(err));
|
|
296
|
+
});
|
|
297
|
+
}, this._timeout * 2);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Readable } from "stream";
|
|
2
|
+
import { createConsola } from "consola";
|
|
3
|
+
import {
|
|
4
|
+
bytesToHex,
|
|
5
|
+
DateOnly,
|
|
6
|
+
DateTime,
|
|
7
|
+
SdError,
|
|
8
|
+
EventEmitter,
|
|
9
|
+
strIsNullOrEmpty,
|
|
10
|
+
Time,
|
|
11
|
+
Uuid,
|
|
12
|
+
} from "@simplysm/core-common";
|
|
13
|
+
import type { ColumnMeta, DataType, IsolationLevel } from "@simplysm/orm-common";
|
|
14
|
+
import { DB_CONN_DEFAULT_TIMEOUT, DB_CONN_ERRORS, type DbConn, type PostgresqlDbConnConfig } from "../types/db-conn";
|
|
15
|
+
import type { Client } from "pg";
|
|
16
|
+
import type { CopyStreamQuery } from "pg-copy-streams";
|
|
17
|
+
|
|
18
|
+
const logger = createConsola().withTag("postgresql-db-conn");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* PostgreSQL 데이터베이스 연결 클래스
|
|
22
|
+
*
|
|
23
|
+
* pg 라이브러리를 사용하여 PostgreSQL 연결을 관리합니다.
|
|
24
|
+
*/
|
|
25
|
+
export class PostgresqlDbConn extends EventEmitter<{ close: void }> implements DbConn {
|
|
26
|
+
private readonly _timeout = DB_CONN_DEFAULT_TIMEOUT;
|
|
27
|
+
|
|
28
|
+
private _client?: Client;
|
|
29
|
+
private _connTimeout?: ReturnType<typeof setTimeout>;
|
|
30
|
+
|
|
31
|
+
isConnected = false;
|
|
32
|
+
isOnTransaction = false;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly _pg: typeof import("pg"),
|
|
36
|
+
private readonly _pgCopyFrom: (queryText: string) => CopyStreamQuery,
|
|
37
|
+
readonly config: PostgresqlDbConnConfig,
|
|
38
|
+
) {
|
|
39
|
+
super();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async connect(): Promise<void> {
|
|
43
|
+
if (this.isConnected) {
|
|
44
|
+
throw new SdError(DB_CONN_ERRORS.ALREADY_CONNECTED);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const client = new this._pg.Client({
|
|
48
|
+
host: this.config.host,
|
|
49
|
+
port: this.config.port ?? 5432,
|
|
50
|
+
user: this.config.username,
|
|
51
|
+
password: this.config.password,
|
|
52
|
+
database: this.config.database,
|
|
53
|
+
connectionTimeoutMillis: this._timeout,
|
|
54
|
+
query_timeout: this._timeout,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
client.on("end", () => {
|
|
58
|
+
this.emit("close");
|
|
59
|
+
this._resetState();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
client.on("error", (error) => {
|
|
63
|
+
logger.error("DB 연결 오류", error.message);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await client.connect();
|
|
67
|
+
|
|
68
|
+
this._client = client;
|
|
69
|
+
this._startTimeout();
|
|
70
|
+
this.isConnected = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async close(): Promise<void> {
|
|
74
|
+
this._stopTimeout();
|
|
75
|
+
|
|
76
|
+
if (this._client == null || !this.isConnected) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await this._client.end();
|
|
81
|
+
|
|
82
|
+
this.emit("close");
|
|
83
|
+
this._resetState();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async beginTransaction(isolationLevel?: IsolationLevel): Promise<void> {
|
|
87
|
+
this._assertConnected();
|
|
88
|
+
|
|
89
|
+
const level = (isolationLevel ?? this.config.defaultIsolationLevel ?? "READ_UNCOMMITTED").replace(/_/g, " ");
|
|
90
|
+
|
|
91
|
+
await this._client!.query("BEGIN");
|
|
92
|
+
await this._client!.query(`SET TRANSACTION ISOLATION LEVEL ${level}`);
|
|
93
|
+
|
|
94
|
+
this.isOnTransaction = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async commitTransaction(): Promise<void> {
|
|
98
|
+
this._assertConnected();
|
|
99
|
+
await this._client!.query("COMMIT");
|
|
100
|
+
this.isOnTransaction = false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async rollbackTransaction(): Promise<void> {
|
|
104
|
+
this._assertConnected();
|
|
105
|
+
await this._client!.query("ROLLBACK");
|
|
106
|
+
this.isOnTransaction = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async execute(queries: string[]): Promise<unknown[][]> {
|
|
110
|
+
const results: unknown[][] = [];
|
|
111
|
+
for (const query of queries.filter((item) => !strIsNullOrEmpty(item))) {
|
|
112
|
+
const resultItems = await this.executeParametrized(query);
|
|
113
|
+
results.push(...resultItems);
|
|
114
|
+
}
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async executeParametrized(query: string, params?: unknown[]): Promise<unknown[][]> {
|
|
119
|
+
this._assertConnected();
|
|
120
|
+
|
|
121
|
+
logger.debug("쿼리 실행", { queryLength: query.length, params });
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const result = await this._client!.query(query, params);
|
|
125
|
+
|
|
126
|
+
this._startTimeout();
|
|
127
|
+
|
|
128
|
+
// PostgreSQL은 단일 결과셋 반환
|
|
129
|
+
return [result.rows];
|
|
130
|
+
} catch (err) {
|
|
131
|
+
this._startTimeout();
|
|
132
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
133
|
+
throw new SdError(error, "쿼리 수행중 오류발생\n-- query\n" + query.trim() + "\n--");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async bulkInsert(
|
|
138
|
+
tableName: string,
|
|
139
|
+
columnMetas: Record<string, ColumnMeta>,
|
|
140
|
+
records: Record<string, unknown>[],
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
this._assertConnected();
|
|
143
|
+
|
|
144
|
+
if (records.length === 0) return;
|
|
145
|
+
|
|
146
|
+
const colNames = Object.keys(columnMetas);
|
|
147
|
+
const wrappedCols = colNames.map((c) => `"${c}"`).join(", ");
|
|
148
|
+
|
|
149
|
+
// COPY FROM STDIN 스트림 생성
|
|
150
|
+
const copyQuery = `COPY ${tableName} (${wrappedCols}) FROM STDIN WITH (FORMAT csv, NULL '\\N')`;
|
|
151
|
+
const stream = this._client!.query(this._pgCopyFrom(copyQuery));
|
|
152
|
+
|
|
153
|
+
// CSV 데이터 생성
|
|
154
|
+
const csvLines: string[] = [];
|
|
155
|
+
for (const record of records) {
|
|
156
|
+
const row = colNames.map((colName) => this._escapeForCsv(record[colName], columnMetas[colName].dataType));
|
|
157
|
+
csvLines.push(row.join(","));
|
|
158
|
+
}
|
|
159
|
+
const csvContent = csvLines.join("\n") + "\n";
|
|
160
|
+
|
|
161
|
+
// 스트림으로 데이터 전송
|
|
162
|
+
await new Promise<void>((resolve, reject) => {
|
|
163
|
+
const readable = Readable.from([csvContent]);
|
|
164
|
+
|
|
165
|
+
readable.on("error", reject);
|
|
166
|
+
stream.on("error", reject);
|
|
167
|
+
stream.on("finish", resolve);
|
|
168
|
+
|
|
169
|
+
readable.pipe(stream);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─────────────────────────────────────────────
|
|
174
|
+
// Private helpers
|
|
175
|
+
// ─────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* PostgreSQL COPY CSV용 값 이스케이프
|
|
179
|
+
*/
|
|
180
|
+
private _escapeForCsv(value: unknown, dataType: DataType): string {
|
|
181
|
+
if (value == null) {
|
|
182
|
+
return "\\N"; // NULL 표현
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
switch (dataType.type) {
|
|
186
|
+
case "int":
|
|
187
|
+
case "bigint":
|
|
188
|
+
case "float":
|
|
189
|
+
case "double":
|
|
190
|
+
case "decimal":
|
|
191
|
+
return String(value);
|
|
192
|
+
|
|
193
|
+
case "boolean":
|
|
194
|
+
return (value as boolean) ? "true" : "false";
|
|
195
|
+
|
|
196
|
+
case "varchar":
|
|
197
|
+
case "char":
|
|
198
|
+
case "text": {
|
|
199
|
+
const str = value as string;
|
|
200
|
+
// CSV 형식: 쌍따옴표로 감싸고, 내부 쌍따옴표는 두 번
|
|
201
|
+
if (str.includes('"') || str.includes(",") || str.includes("\n") || str.includes("\r")) {
|
|
202
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
203
|
+
}
|
|
204
|
+
return str;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case "datetime":
|
|
208
|
+
return (value as DateTime).toFormatString("yyyy-MM-dd HH:mm:ss.fff");
|
|
209
|
+
|
|
210
|
+
case "date":
|
|
211
|
+
return (value as DateOnly).toFormatString("yyyy-MM-dd");
|
|
212
|
+
|
|
213
|
+
case "time":
|
|
214
|
+
return (value as Time).toFormatString("HH:mm:ss");
|
|
215
|
+
|
|
216
|
+
case "uuid":
|
|
217
|
+
return (value as Uuid).toString();
|
|
218
|
+
|
|
219
|
+
case "binary":
|
|
220
|
+
return '"\\x' + bytesToHex(value as Uint8Array) + '"'; // PostgreSQL bytea hex 형식 (CSV 쌍따옴표로 감쌈)
|
|
221
|
+
|
|
222
|
+
default:
|
|
223
|
+
throw new SdError(`지원하지 않는 DataType: ${JSON.stringify(dataType)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private _assertConnected(): void {
|
|
228
|
+
if (this._client == null || !this.isConnected) {
|
|
229
|
+
throw new SdError(DB_CONN_ERRORS.NOT_CONNECTED);
|
|
230
|
+
}
|
|
231
|
+
this._startTimeout();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private _resetState(): void {
|
|
235
|
+
this.isConnected = false;
|
|
236
|
+
this.isOnTransaction = false;
|
|
237
|
+
this._client = undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private _stopTimeout(): void {
|
|
241
|
+
if (this._connTimeout != null) {
|
|
242
|
+
clearTimeout(this._connTimeout);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private _startTimeout(): void {
|
|
247
|
+
this._stopTimeout();
|
|
248
|
+
this._connTimeout = setTimeout(() => {
|
|
249
|
+
this.close().catch((err) => {
|
|
250
|
+
logger.error("close error", err instanceof Error ? err.message : String(err));
|
|
251
|
+
});
|
|
252
|
+
}, this._timeout * 2);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Pool } from "generic-pool";
|
|
2
|
+
import { createPool } from "generic-pool";
|
|
3
|
+
import type { DbConn, DbConnConfig } from "./types/db-conn";
|
|
4
|
+
import { PooledDbConn } from "./pooled-db-conn";
|
|
5
|
+
import { MysqlDbConn } from "./connections/mysql-db-conn";
|
|
6
|
+
import { MssqlDbConn } from "./connections/mssql-db-conn";
|
|
7
|
+
import { PostgresqlDbConn } from "./connections/postgresql-db-conn";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* DB 연결 팩토리
|
|
11
|
+
*
|
|
12
|
+
* 데이터베이스 연결 인스턴스를 생성하고 풀링을 관리한다.
|
|
13
|
+
* MSSQL, MySQL, PostgreSQL을 지원한다.
|
|
14
|
+
*/
|
|
15
|
+
export class DbConnFactory {
|
|
16
|
+
// 설정별 커넥션 풀 캐싱
|
|
17
|
+
private static readonly _poolMap = new Map<string, Pool<DbConn>>();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* DB 연결 생성
|
|
21
|
+
*
|
|
22
|
+
* 커넥션 풀에서 연결을 획득하여 반환한다.
|
|
23
|
+
* 풀이 없는 경우 새로 생성한다.
|
|
24
|
+
*
|
|
25
|
+
* @param config - 데이터베이스 연결 설정
|
|
26
|
+
* @returns 풀링된 DB 연결 객체
|
|
27
|
+
*/
|
|
28
|
+
static create(config: DbConnConfig): Promise<DbConn> {
|
|
29
|
+
// 1. 풀 가져오기 (없으면 생성)
|
|
30
|
+
const pool = this._getOrCreatePool(config);
|
|
31
|
+
|
|
32
|
+
// 2. 래퍼 객체 반환
|
|
33
|
+
return Promise.resolve(new PooledDbConn(pool, config));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private static _getOrCreatePool(config: DbConnConfig): Pool<DbConn> {
|
|
37
|
+
// 객체를 키로 쓰기 위해 문자열 변환 (중첩 객체도 정렬하여 동일 설정의 일관된 키 보장)
|
|
38
|
+
const configKey = JSON.stringify(config, (_, value: unknown) =>
|
|
39
|
+
value != null && typeof value === "object" && !Array.isArray(value)
|
|
40
|
+
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)))
|
|
41
|
+
: value,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!this._poolMap.has(configKey)) {
|
|
45
|
+
const pool = createPool<DbConn>(
|
|
46
|
+
{
|
|
47
|
+
create: async () => {
|
|
48
|
+
const conn = await this._createRawConnection(config);
|
|
49
|
+
await conn.connect();
|
|
50
|
+
return conn;
|
|
51
|
+
},
|
|
52
|
+
destroy: async (conn) => {
|
|
53
|
+
await conn.close(); // 풀에서 제거될 때 실제 연결 종료
|
|
54
|
+
},
|
|
55
|
+
validate: (conn) => {
|
|
56
|
+
// 획득 시 연결 상태 확인 (끊겨있으면 Pool이 폐기하고 새로 만듦)
|
|
57
|
+
return Promise.resolve(conn.isConnected);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
min: config.pool?.min ?? 1,
|
|
62
|
+
max: config.pool?.max ?? 10,
|
|
63
|
+
acquireTimeoutMillis: config.pool?.acquireTimeoutMillis ?? 30000,
|
|
64
|
+
idleTimeoutMillis: config.pool?.idleTimeoutMillis ?? 30000,
|
|
65
|
+
testOnBorrow: true, // [중요] 빌려줄 때 validate 실행 여부
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
this._poolMap.set(configKey, pool);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return this._poolMap.get(configKey)!;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private static async _createRawConnection(config: DbConnConfig): Promise<DbConn> {
|
|
76
|
+
if (config.dialect === "mysql") {
|
|
77
|
+
const mysql = await this._ensureModule("mysql");
|
|
78
|
+
return new MysqlDbConn(mysql, config);
|
|
79
|
+
} else if (config.dialect === "postgresql") {
|
|
80
|
+
const pg = await this._ensureModule("pg");
|
|
81
|
+
const pgCopyStreams = await this._ensureModule("pgCopyStreams");
|
|
82
|
+
return new PostgresqlDbConn(pg, pgCopyStreams.from, config);
|
|
83
|
+
} else {
|
|
84
|
+
// mssql, mssql-azure
|
|
85
|
+
const tedious = await this._ensureModule("tedious");
|
|
86
|
+
return new MssqlDbConn(tedious, config);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 지연 로딩 모듈 캐시
|
|
91
|
+
private static readonly _modules: {
|
|
92
|
+
tedious?: typeof import("tedious");
|
|
93
|
+
mysql?: typeof import("mysql2/promise");
|
|
94
|
+
pg?: typeof import("pg");
|
|
95
|
+
pgCopyStreams?: typeof import("pg-copy-streams");
|
|
96
|
+
} = {};
|
|
97
|
+
|
|
98
|
+
private static async _ensureModule<K extends keyof typeof this._modules>(
|
|
99
|
+
name: K,
|
|
100
|
+
): Promise<NonNullable<(typeof this._modules)[K]>> {
|
|
101
|
+
if (this._modules[name] == null) {
|
|
102
|
+
if (name === "mysql") {
|
|
103
|
+
this._modules.mysql = await import("mysql2/promise");
|
|
104
|
+
} else if (name === "pg") {
|
|
105
|
+
this._modules.pg = await import("pg");
|
|
106
|
+
} else if (name === "pgCopyStreams") {
|
|
107
|
+
this._modules.pgCopyStreams = await import("pg-copy-streams");
|
|
108
|
+
} else {
|
|
109
|
+
this._modules.tedious = await import("tedious");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return this._modules[name]!;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export * from "./types/db-conn";
|
|
3
|
+
|
|
4
|
+
// Connections
|
|
5
|
+
export * from "./connections/mssql-db-conn";
|
|
6
|
+
export * from "./connections/mysql-db-conn";
|
|
7
|
+
export * from "./connections/postgresql-db-conn";
|
|
8
|
+
|
|
9
|
+
// Core
|
|
10
|
+
export * from "./db-conn-factory";
|
|
11
|
+
export * from "./node-db-context-executor";
|
|
12
|
+
export * from "./pooled-db-conn";
|
|
13
|
+
export * from "./sd-orm";
|