@spfn/core 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +580 -0
- package/dist/auto-loader-C44TcLmM.d.ts +125 -0
- package/dist/bind-pssq1NRT.d.ts +34 -0
- package/dist/client/index.d.ts +174 -0
- package/dist/client/index.js +179 -0
- package/dist/client/index.js.map +1 -0
- package/dist/codegen/index.d.ts +126 -0
- package/dist/codegen/index.js +970 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/db/index.d.ts +83 -0
- package/dist/db/index.js +2099 -0
- package/dist/db/index.js.map +1 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.js +13042 -0
- package/dist/index.js.map +1 -0
- package/dist/postgres-errors-CY_Es8EJ.d.ts +1703 -0
- package/dist/route/index.d.ts +72 -0
- package/dist/route/index.js +442 -0
- package/dist/route/index.js.map +1 -0
- package/dist/scripts/index.d.ts +24 -0
- package/dist/scripts/index.js +1157 -0
- package/dist/scripts/index.js.map +1 -0
- package/dist/scripts/templates/api-index.template.txt +10 -0
- package/dist/scripts/templates/api-tag.template.txt +11 -0
- package/dist/scripts/templates/contract.template.txt +87 -0
- package/dist/scripts/templates/entity-type.template.txt +31 -0
- package/dist/scripts/templates/entity.template.txt +19 -0
- package/dist/scripts/templates/index.template.txt +10 -0
- package/dist/scripts/templates/repository.template.txt +37 -0
- package/dist/scripts/templates/routes-id.template.txt +59 -0
- package/dist/scripts/templates/routes-index.template.txt +44 -0
- package/dist/server/index.d.ts +303 -0
- package/dist/server/index.js +12923 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types-SlzTr8ZO.d.ts +143 -0
- package/package.json +119 -0
package/dist/db/index.js
ADDED
|
@@ -0,0 +1,2099 @@
|
|
|
1
|
+
import { config } from 'dotenv';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
3
|
+
import postgres from 'postgres';
|
|
4
|
+
import pino from 'pino';
|
|
5
|
+
import { existsSync, mkdirSync, createWriteStream } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
8
|
+
import { createMiddleware } from 'hono/factory';
|
|
9
|
+
import { and, desc, asc, sql, isNull, isNotNull, notInArray, inArray, like, lte, lt, gte, gt, ne, eq } from 'drizzle-orm';
|
|
10
|
+
import { bigserial, timestamp } from 'drizzle-orm/pg-core';
|
|
11
|
+
|
|
12
|
+
// src/db/manager/factory.ts
|
|
13
|
+
var PinoAdapter = class _PinoAdapter {
|
|
14
|
+
logger;
|
|
15
|
+
constructor(config2) {
|
|
16
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
17
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
18
|
+
const fileLoggingEnabled = process.env.LOGGER_FILE_ENABLED === "true";
|
|
19
|
+
const targets = [];
|
|
20
|
+
if (!isProduction && isDevelopment) {
|
|
21
|
+
targets.push({
|
|
22
|
+
target: "pino-pretty",
|
|
23
|
+
level: "debug",
|
|
24
|
+
options: {
|
|
25
|
+
colorize: true,
|
|
26
|
+
translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l",
|
|
27
|
+
ignore: "pid,hostname"
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (fileLoggingEnabled && isProduction) {
|
|
32
|
+
const logDir = process.env.LOG_DIR || "./logs";
|
|
33
|
+
const maxFileSize = process.env.LOG_MAX_FILE_SIZE || "10M";
|
|
34
|
+
const maxFiles = parseInt(process.env.LOG_MAX_FILES || "10", 10);
|
|
35
|
+
targets.push({
|
|
36
|
+
target: "pino-roll",
|
|
37
|
+
level: "info",
|
|
38
|
+
options: {
|
|
39
|
+
file: `${logDir}/app.log`,
|
|
40
|
+
frequency: "daily",
|
|
41
|
+
size: maxFileSize,
|
|
42
|
+
limit: { count: maxFiles },
|
|
43
|
+
mkdir: true
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
this.logger = pino({
|
|
48
|
+
level: config2.level,
|
|
49
|
+
// Transport 설정 (targets가 있으면 사용, 없으면 기본 stdout)
|
|
50
|
+
transport: targets.length > 0 ? { targets } : void 0,
|
|
51
|
+
// 기본 필드
|
|
52
|
+
base: config2.module ? { module: config2.module } : void 0
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
child(module) {
|
|
56
|
+
const childLogger = new _PinoAdapter({ level: this.logger.level, module });
|
|
57
|
+
childLogger.logger = this.logger.child({ module });
|
|
58
|
+
return childLogger;
|
|
59
|
+
}
|
|
60
|
+
debug(message, context) {
|
|
61
|
+
this.logger.debug(context || {}, message);
|
|
62
|
+
}
|
|
63
|
+
info(message, context) {
|
|
64
|
+
this.logger.info(context || {}, message);
|
|
65
|
+
}
|
|
66
|
+
warn(message, errorOrContext, context) {
|
|
67
|
+
if (errorOrContext instanceof Error) {
|
|
68
|
+
this.logger.warn({ err: errorOrContext, ...context }, message);
|
|
69
|
+
} else {
|
|
70
|
+
this.logger.warn(errorOrContext || {}, message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
error(message, errorOrContext, context) {
|
|
74
|
+
if (errorOrContext instanceof Error) {
|
|
75
|
+
this.logger.error({ err: errorOrContext, ...context }, message);
|
|
76
|
+
} else {
|
|
77
|
+
this.logger.error(errorOrContext || {}, message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
fatal(message, errorOrContext, context) {
|
|
81
|
+
if (errorOrContext instanceof Error) {
|
|
82
|
+
this.logger.fatal({ err: errorOrContext, ...context }, message);
|
|
83
|
+
} else {
|
|
84
|
+
this.logger.fatal(errorOrContext || {}, message);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async close() {
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/logger/logger.ts
|
|
92
|
+
var Logger = class _Logger {
|
|
93
|
+
config;
|
|
94
|
+
module;
|
|
95
|
+
constructor(config2) {
|
|
96
|
+
this.config = config2;
|
|
97
|
+
this.module = config2.module;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get current log level
|
|
101
|
+
*/
|
|
102
|
+
get level() {
|
|
103
|
+
return this.config.level;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Create child logger (per module)
|
|
107
|
+
*/
|
|
108
|
+
child(module) {
|
|
109
|
+
return new _Logger({
|
|
110
|
+
...this.config,
|
|
111
|
+
module
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Debug log
|
|
116
|
+
*/
|
|
117
|
+
debug(message, context) {
|
|
118
|
+
this.log("debug", message, void 0, context);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Info log
|
|
122
|
+
*/
|
|
123
|
+
info(message, context) {
|
|
124
|
+
this.log("info", message, void 0, context);
|
|
125
|
+
}
|
|
126
|
+
warn(message, errorOrContext, context) {
|
|
127
|
+
if (errorOrContext instanceof Error) {
|
|
128
|
+
this.log("warn", message, errorOrContext, context);
|
|
129
|
+
} else {
|
|
130
|
+
this.log("warn", message, void 0, errorOrContext);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
error(message, errorOrContext, context) {
|
|
134
|
+
if (errorOrContext instanceof Error) {
|
|
135
|
+
this.log("error", message, errorOrContext, context);
|
|
136
|
+
} else {
|
|
137
|
+
this.log("error", message, void 0, errorOrContext);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
fatal(message, errorOrContext, context) {
|
|
141
|
+
if (errorOrContext instanceof Error) {
|
|
142
|
+
this.log("fatal", message, errorOrContext, context);
|
|
143
|
+
} else {
|
|
144
|
+
this.log("fatal", message, void 0, errorOrContext);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Log processing (internal)
|
|
149
|
+
*/
|
|
150
|
+
log(level, message, error, context) {
|
|
151
|
+
const metadata = {
|
|
152
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
153
|
+
level,
|
|
154
|
+
message,
|
|
155
|
+
module: this.module,
|
|
156
|
+
error,
|
|
157
|
+
context
|
|
158
|
+
};
|
|
159
|
+
this.processTransports(metadata);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Process Transports
|
|
163
|
+
*/
|
|
164
|
+
processTransports(metadata) {
|
|
165
|
+
const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
|
|
166
|
+
Promise.all(promises).catch((error) => {
|
|
167
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
168
|
+
process.stderr.write(`[Logger] Transport error: ${errorMessage}
|
|
169
|
+
`);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Transport log (error-safe)
|
|
174
|
+
*/
|
|
175
|
+
async safeTransportLog(transport, metadata) {
|
|
176
|
+
try {
|
|
177
|
+
await transport.log(metadata);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
180
|
+
process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
|
|
181
|
+
`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Close all Transports
|
|
186
|
+
*/
|
|
187
|
+
async close() {
|
|
188
|
+
const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
|
|
189
|
+
await Promise.all(closePromises);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/logger/types.ts
|
|
194
|
+
var LOG_LEVEL_PRIORITY = {
|
|
195
|
+
debug: 0,
|
|
196
|
+
info: 1,
|
|
197
|
+
warn: 2,
|
|
198
|
+
error: 3,
|
|
199
|
+
fatal: 4
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/logger/formatters.ts
|
|
203
|
+
var COLORS = {
|
|
204
|
+
reset: "\x1B[0m",
|
|
205
|
+
bright: "\x1B[1m",
|
|
206
|
+
dim: "\x1B[2m",
|
|
207
|
+
// 로그 레벨 컬러
|
|
208
|
+
debug: "\x1B[36m",
|
|
209
|
+
// cyan
|
|
210
|
+
info: "\x1B[32m",
|
|
211
|
+
// green
|
|
212
|
+
warn: "\x1B[33m",
|
|
213
|
+
// yellow
|
|
214
|
+
error: "\x1B[31m",
|
|
215
|
+
// red
|
|
216
|
+
fatal: "\x1B[35m",
|
|
217
|
+
// magenta
|
|
218
|
+
// 추가 컬러
|
|
219
|
+
gray: "\x1B[90m"
|
|
220
|
+
};
|
|
221
|
+
function colorizeLevel(level) {
|
|
222
|
+
const color = COLORS[level];
|
|
223
|
+
const levelStr = level.toUpperCase().padEnd(5);
|
|
224
|
+
return `${color}${levelStr}${COLORS.reset}`;
|
|
225
|
+
}
|
|
226
|
+
function formatTimestamp(date) {
|
|
227
|
+
return date.toISOString();
|
|
228
|
+
}
|
|
229
|
+
function formatTimestampHuman(date) {
|
|
230
|
+
const year = date.getFullYear();
|
|
231
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
232
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
233
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
234
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
235
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
236
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
237
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
|
|
238
|
+
}
|
|
239
|
+
function formatError(error) {
|
|
240
|
+
const lines = [];
|
|
241
|
+
lines.push(`${error.name}: ${error.message}`);
|
|
242
|
+
if (error.stack) {
|
|
243
|
+
const stackLines = error.stack.split("\n").slice(1);
|
|
244
|
+
lines.push(...stackLines);
|
|
245
|
+
}
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
function formatContext(context) {
|
|
249
|
+
try {
|
|
250
|
+
return JSON.stringify(context, null, 2);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return "[Context serialization failed]";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function formatConsole(metadata, colorize = true) {
|
|
256
|
+
const parts = [];
|
|
257
|
+
const timestamp2 = formatTimestampHuman(metadata.timestamp);
|
|
258
|
+
if (colorize) {
|
|
259
|
+
parts.push(`${COLORS.gray}${timestamp2}${COLORS.reset}`);
|
|
260
|
+
} else {
|
|
261
|
+
parts.push(timestamp2);
|
|
262
|
+
}
|
|
263
|
+
if (colorize) {
|
|
264
|
+
parts.push(colorizeLevel(metadata.level));
|
|
265
|
+
} else {
|
|
266
|
+
parts.push(metadata.level.toUpperCase().padEnd(5));
|
|
267
|
+
}
|
|
268
|
+
if (metadata.module) {
|
|
269
|
+
if (colorize) {
|
|
270
|
+
parts.push(`${COLORS.dim}[${metadata.module}]${COLORS.reset}`);
|
|
271
|
+
} else {
|
|
272
|
+
parts.push(`[${metadata.module}]`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
parts.push(metadata.message);
|
|
276
|
+
let output = parts.join(" ");
|
|
277
|
+
if (metadata.context && Object.keys(metadata.context).length > 0) {
|
|
278
|
+
output += "\n" + formatContext(metadata.context);
|
|
279
|
+
}
|
|
280
|
+
if (metadata.error) {
|
|
281
|
+
output += "\n" + formatError(metadata.error);
|
|
282
|
+
}
|
|
283
|
+
return output;
|
|
284
|
+
}
|
|
285
|
+
function formatJSON(metadata) {
|
|
286
|
+
const obj = {
|
|
287
|
+
timestamp: formatTimestamp(metadata.timestamp),
|
|
288
|
+
level: metadata.level,
|
|
289
|
+
message: metadata.message
|
|
290
|
+
};
|
|
291
|
+
if (metadata.module) {
|
|
292
|
+
obj.module = metadata.module;
|
|
293
|
+
}
|
|
294
|
+
if (metadata.context) {
|
|
295
|
+
obj.context = metadata.context;
|
|
296
|
+
}
|
|
297
|
+
if (metadata.error) {
|
|
298
|
+
obj.error = {
|
|
299
|
+
name: metadata.error.name,
|
|
300
|
+
message: metadata.error.message,
|
|
301
|
+
stack: metadata.error.stack
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return JSON.stringify(obj);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/logger/transports/console.ts
|
|
308
|
+
var ConsoleTransport = class {
|
|
309
|
+
name = "console";
|
|
310
|
+
level;
|
|
311
|
+
enabled;
|
|
312
|
+
colorize;
|
|
313
|
+
constructor(config2) {
|
|
314
|
+
this.level = config2.level;
|
|
315
|
+
this.enabled = config2.enabled;
|
|
316
|
+
this.colorize = config2.colorize ?? true;
|
|
317
|
+
}
|
|
318
|
+
async log(metadata) {
|
|
319
|
+
if (!this.enabled) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const message = formatConsole(metadata, this.colorize);
|
|
326
|
+
if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
|
|
327
|
+
console.error(message);
|
|
328
|
+
} else {
|
|
329
|
+
console.log(message);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
var FileTransport = class {
|
|
334
|
+
name = "file";
|
|
335
|
+
level;
|
|
336
|
+
enabled;
|
|
337
|
+
logDir;
|
|
338
|
+
currentStream = null;
|
|
339
|
+
currentFilename = null;
|
|
340
|
+
constructor(config2) {
|
|
341
|
+
this.level = config2.level;
|
|
342
|
+
this.enabled = config2.enabled;
|
|
343
|
+
this.logDir = config2.logDir;
|
|
344
|
+
if (!existsSync(this.logDir)) {
|
|
345
|
+
mkdirSync(this.logDir, { recursive: true });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async log(metadata) {
|
|
349
|
+
if (!this.enabled) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const message = formatJSON(metadata);
|
|
356
|
+
const filename = this.getLogFilename(metadata.timestamp);
|
|
357
|
+
if (this.currentFilename !== filename) {
|
|
358
|
+
await this.rotateStream(filename);
|
|
359
|
+
}
|
|
360
|
+
if (this.currentStream) {
|
|
361
|
+
return new Promise((resolve, reject) => {
|
|
362
|
+
this.currentStream.write(message + "\n", "utf-8", (error) => {
|
|
363
|
+
if (error) {
|
|
364
|
+
process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
|
|
365
|
+
`);
|
|
366
|
+
reject(error);
|
|
367
|
+
} else {
|
|
368
|
+
resolve();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* 스트림 교체 (날짜 변경 시)
|
|
376
|
+
*/
|
|
377
|
+
async rotateStream(filename) {
|
|
378
|
+
if (this.currentStream) {
|
|
379
|
+
await this.closeStream();
|
|
380
|
+
}
|
|
381
|
+
const filepath = join(this.logDir, filename);
|
|
382
|
+
this.currentStream = createWriteStream(filepath, {
|
|
383
|
+
flags: "a",
|
|
384
|
+
// append mode
|
|
385
|
+
encoding: "utf-8"
|
|
386
|
+
});
|
|
387
|
+
this.currentFilename = filename;
|
|
388
|
+
this.currentStream.on("error", (error) => {
|
|
389
|
+
process.stderr.write(`[FileTransport] Stream error: ${error.message}
|
|
390
|
+
`);
|
|
391
|
+
this.currentStream = null;
|
|
392
|
+
this.currentFilename = null;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* 현재 스트림 닫기
|
|
397
|
+
*/
|
|
398
|
+
async closeStream() {
|
|
399
|
+
if (!this.currentStream) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
return new Promise((resolve, reject) => {
|
|
403
|
+
this.currentStream.end((error) => {
|
|
404
|
+
if (error) {
|
|
405
|
+
reject(error);
|
|
406
|
+
} else {
|
|
407
|
+
this.currentStream = null;
|
|
408
|
+
this.currentFilename = null;
|
|
409
|
+
resolve();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* 날짜별 로그 파일명 생성
|
|
416
|
+
*/
|
|
417
|
+
getLogFilename(date) {
|
|
418
|
+
const year = date.getFullYear();
|
|
419
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
420
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
421
|
+
return `${year}-${month}-${day}.log`;
|
|
422
|
+
}
|
|
423
|
+
async close() {
|
|
424
|
+
await this.closeStream();
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// src/logger/config.ts
|
|
429
|
+
function getDefaultLogLevel() {
|
|
430
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
431
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
432
|
+
if (isDevelopment) {
|
|
433
|
+
return "debug";
|
|
434
|
+
}
|
|
435
|
+
if (isProduction) {
|
|
436
|
+
return "info";
|
|
437
|
+
}
|
|
438
|
+
return "warn";
|
|
439
|
+
}
|
|
440
|
+
function getConsoleConfig() {
|
|
441
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
442
|
+
return {
|
|
443
|
+
level: "debug",
|
|
444
|
+
enabled: true,
|
|
445
|
+
colorize: !isProduction
|
|
446
|
+
// Dev: colored output, Production: plain text
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function getFileConfig() {
|
|
450
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
451
|
+
return {
|
|
452
|
+
level: "info",
|
|
453
|
+
enabled: isProduction,
|
|
454
|
+
// File logging in production only
|
|
455
|
+
logDir: process.env.LOG_DIR || "./logs",
|
|
456
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
457
|
+
// 10MB
|
|
458
|
+
maxFiles: 10
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/logger/adapters/custom.ts
|
|
463
|
+
function initializeTransports() {
|
|
464
|
+
const transports = [];
|
|
465
|
+
const consoleConfig = getConsoleConfig();
|
|
466
|
+
transports.push(new ConsoleTransport(consoleConfig));
|
|
467
|
+
const fileConfig = getFileConfig();
|
|
468
|
+
if (fileConfig.enabled) {
|
|
469
|
+
transports.push(new FileTransport(fileConfig));
|
|
470
|
+
}
|
|
471
|
+
return transports;
|
|
472
|
+
}
|
|
473
|
+
var CustomAdapter = class _CustomAdapter {
|
|
474
|
+
logger;
|
|
475
|
+
constructor(config2) {
|
|
476
|
+
this.logger = new Logger({
|
|
477
|
+
level: config2.level,
|
|
478
|
+
module: config2.module,
|
|
479
|
+
transports: initializeTransports()
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
child(module) {
|
|
483
|
+
const adapter = new _CustomAdapter({ level: this.logger.level, module });
|
|
484
|
+
adapter.logger = this.logger.child(module);
|
|
485
|
+
return adapter;
|
|
486
|
+
}
|
|
487
|
+
debug(message, context) {
|
|
488
|
+
this.logger.debug(message, context);
|
|
489
|
+
}
|
|
490
|
+
info(message, context) {
|
|
491
|
+
this.logger.info(message, context);
|
|
492
|
+
}
|
|
493
|
+
warn(message, errorOrContext, context) {
|
|
494
|
+
if (errorOrContext instanceof Error) {
|
|
495
|
+
this.logger.warn(message, errorOrContext, context);
|
|
496
|
+
} else {
|
|
497
|
+
this.logger.warn(message, errorOrContext);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
error(message, errorOrContext, context) {
|
|
501
|
+
if (errorOrContext instanceof Error) {
|
|
502
|
+
this.logger.error(message, errorOrContext, context);
|
|
503
|
+
} else {
|
|
504
|
+
this.logger.error(message, errorOrContext);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
fatal(message, errorOrContext, context) {
|
|
508
|
+
if (errorOrContext instanceof Error) {
|
|
509
|
+
this.logger.fatal(message, errorOrContext, context);
|
|
510
|
+
} else {
|
|
511
|
+
this.logger.fatal(message, errorOrContext);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async close() {
|
|
515
|
+
await this.logger.close();
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// src/logger/adapter-factory.ts
|
|
520
|
+
function createAdapter(type) {
|
|
521
|
+
const level = getDefaultLogLevel();
|
|
522
|
+
switch (type) {
|
|
523
|
+
case "pino":
|
|
524
|
+
return new PinoAdapter({ level });
|
|
525
|
+
case "custom":
|
|
526
|
+
return new CustomAdapter({ level });
|
|
527
|
+
default:
|
|
528
|
+
return new PinoAdapter({ level });
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function getAdapterType() {
|
|
532
|
+
const adapterEnv = process.env.LOGGER_ADAPTER;
|
|
533
|
+
if (adapterEnv === "custom" || adapterEnv === "pino") {
|
|
534
|
+
return adapterEnv;
|
|
535
|
+
}
|
|
536
|
+
return "pino";
|
|
537
|
+
}
|
|
538
|
+
var logger = createAdapter(getAdapterType());
|
|
539
|
+
|
|
540
|
+
// src/errors/database-errors.ts
|
|
541
|
+
var DatabaseError = class extends Error {
|
|
542
|
+
statusCode;
|
|
543
|
+
details;
|
|
544
|
+
timestamp;
|
|
545
|
+
constructor(message, statusCode = 500, details) {
|
|
546
|
+
super(message);
|
|
547
|
+
this.name = "DatabaseError";
|
|
548
|
+
this.statusCode = statusCode;
|
|
549
|
+
this.details = details;
|
|
550
|
+
this.timestamp = /* @__PURE__ */ new Date();
|
|
551
|
+
Error.captureStackTrace(this, this.constructor);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Serialize error for API response
|
|
555
|
+
*/
|
|
556
|
+
toJSON() {
|
|
557
|
+
return {
|
|
558
|
+
name: this.name,
|
|
559
|
+
message: this.message,
|
|
560
|
+
statusCode: this.statusCode,
|
|
561
|
+
details: this.details,
|
|
562
|
+
timestamp: this.timestamp.toISOString()
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
var ConnectionError = class extends DatabaseError {
|
|
567
|
+
constructor(message, details) {
|
|
568
|
+
super(message, 503, details);
|
|
569
|
+
this.name = "ConnectionError";
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
var QueryError = class extends DatabaseError {
|
|
573
|
+
constructor(message, statusCode = 500, details) {
|
|
574
|
+
super(message, statusCode, details);
|
|
575
|
+
this.name = "QueryError";
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
var ValidationError = class extends QueryError {
|
|
579
|
+
constructor(message, details) {
|
|
580
|
+
super(message, 400, details);
|
|
581
|
+
this.name = "ValidationError";
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
var TransactionError = class extends DatabaseError {
|
|
585
|
+
constructor(message, statusCode = 500, details) {
|
|
586
|
+
super(message, statusCode, details);
|
|
587
|
+
this.name = "TransactionError";
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
var DeadlockError = class extends TransactionError {
|
|
591
|
+
constructor(message, details) {
|
|
592
|
+
super(message, 409, details);
|
|
593
|
+
this.name = "DeadlockError";
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
var DuplicateEntryError = class extends QueryError {
|
|
597
|
+
constructor(field, value) {
|
|
598
|
+
super(`${field} '${value}' already exists`, 409, { field, value });
|
|
599
|
+
this.name = "DuplicateEntryError";
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// src/db/postgres-errors.ts
|
|
604
|
+
function parseUniqueViolation(message) {
|
|
605
|
+
const patterns = [
|
|
606
|
+
// Standard format: Key (field)=(value)
|
|
607
|
+
/Key \(([^)]+)\)=\(([^)]+)\)/i,
|
|
608
|
+
// With quotes: Key ("field")=('value')
|
|
609
|
+
/Key \(["']?([^)"']+)["']?\)=\(["']?([^)"']+)["']?\)/i,
|
|
610
|
+
// Alternative format
|
|
611
|
+
/Key `([^`]+)`=`([^`]+)`/i
|
|
612
|
+
];
|
|
613
|
+
for (const pattern of patterns) {
|
|
614
|
+
const match = message.match(pattern);
|
|
615
|
+
if (match) {
|
|
616
|
+
const field = match[1].trim().replace(/["'`]/g, "");
|
|
617
|
+
const value = match[2].trim().replace(/["'`]/g, "");
|
|
618
|
+
return { field, value };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
function fromPostgresError(error) {
|
|
624
|
+
const code = error?.code;
|
|
625
|
+
const message = error?.message || "Database error occurred";
|
|
626
|
+
switch (code) {
|
|
627
|
+
// Class 08 — Connection Exception
|
|
628
|
+
case "08000":
|
|
629
|
+
// connection_exception
|
|
630
|
+
case "08001":
|
|
631
|
+
// sqlclient_unable_to_establish_sqlconnection
|
|
632
|
+
case "08003":
|
|
633
|
+
// connection_does_not_exist
|
|
634
|
+
case "08004":
|
|
635
|
+
// sqlserver_rejected_establishment_of_sqlconnection
|
|
636
|
+
case "08006":
|
|
637
|
+
// connection_failure
|
|
638
|
+
case "08007":
|
|
639
|
+
// transaction_resolution_unknown
|
|
640
|
+
case "08P01":
|
|
641
|
+
return new ConnectionError(message, { code });
|
|
642
|
+
// Class 23 — Integrity Constraint Violation
|
|
643
|
+
case "23000":
|
|
644
|
+
// integrity_constraint_violation
|
|
645
|
+
case "23001":
|
|
646
|
+
return new ValidationError(message, { code, constraint: "integrity" });
|
|
647
|
+
case "23502":
|
|
648
|
+
return new ValidationError(message, { code, constraint: "not_null" });
|
|
649
|
+
case "23503":
|
|
650
|
+
return new ValidationError(message, { code, constraint: "foreign_key" });
|
|
651
|
+
case "23505":
|
|
652
|
+
const parsed = parseUniqueViolation(message);
|
|
653
|
+
if (parsed) {
|
|
654
|
+
return new DuplicateEntryError(parsed.field, parsed.value);
|
|
655
|
+
}
|
|
656
|
+
return new DuplicateEntryError("field", "value");
|
|
657
|
+
case "23514":
|
|
658
|
+
return new ValidationError(message, { code, constraint: "check" });
|
|
659
|
+
// Class 40 — Transaction Rollback
|
|
660
|
+
case "40000":
|
|
661
|
+
// transaction_rollback
|
|
662
|
+
case "40001":
|
|
663
|
+
// serialization_failure
|
|
664
|
+
case "40002":
|
|
665
|
+
// transaction_integrity_constraint_violation
|
|
666
|
+
case "40003":
|
|
667
|
+
return new TransactionError(message, 500, { code });
|
|
668
|
+
case "40P01":
|
|
669
|
+
return new DeadlockError(message, { code });
|
|
670
|
+
// Class 42 — Syntax Error or Access Rule Violation
|
|
671
|
+
case "42000":
|
|
672
|
+
// syntax_error_or_access_rule_violation
|
|
673
|
+
case "42601":
|
|
674
|
+
// syntax_error
|
|
675
|
+
case "42501":
|
|
676
|
+
// insufficient_privilege
|
|
677
|
+
case "42602":
|
|
678
|
+
// invalid_name
|
|
679
|
+
case "42622":
|
|
680
|
+
// name_too_long
|
|
681
|
+
case "42701":
|
|
682
|
+
// duplicate_column
|
|
683
|
+
case "42702":
|
|
684
|
+
// ambiguous_column
|
|
685
|
+
case "42703":
|
|
686
|
+
// undefined_column
|
|
687
|
+
case "42704":
|
|
688
|
+
// undefined_object
|
|
689
|
+
case "42P01":
|
|
690
|
+
// undefined_table
|
|
691
|
+
case "42P02":
|
|
692
|
+
return new QueryError(message, 400, { code });
|
|
693
|
+
// Class 53 — Insufficient Resources
|
|
694
|
+
case "53000":
|
|
695
|
+
// insufficient_resources
|
|
696
|
+
case "53100":
|
|
697
|
+
// disk_full
|
|
698
|
+
case "53200":
|
|
699
|
+
// out_of_memory
|
|
700
|
+
case "53300":
|
|
701
|
+
return new ConnectionError(message, { code });
|
|
702
|
+
// Class 57 — Operator Intervention
|
|
703
|
+
case "57000":
|
|
704
|
+
// operator_intervention
|
|
705
|
+
case "57014":
|
|
706
|
+
// query_canceled
|
|
707
|
+
case "57P01":
|
|
708
|
+
// admin_shutdown
|
|
709
|
+
case "57P02":
|
|
710
|
+
// crash_shutdown
|
|
711
|
+
case "57P03":
|
|
712
|
+
return new ConnectionError(message, { code });
|
|
713
|
+
// Default: Unknown error
|
|
714
|
+
default:
|
|
715
|
+
return new QueryError(message, 500, { code });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/db/manager/connection.ts
|
|
720
|
+
var dbLogger = logger.child("database");
|
|
721
|
+
function delay(ms) {
|
|
722
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
723
|
+
}
|
|
724
|
+
async function createDatabaseConnection(connectionString, poolConfig, retryConfig) {
|
|
725
|
+
let lastError;
|
|
726
|
+
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
|
|
727
|
+
try {
|
|
728
|
+
const client = postgres(connectionString, {
|
|
729
|
+
max: poolConfig.max,
|
|
730
|
+
idle_timeout: poolConfig.idleTimeout
|
|
731
|
+
});
|
|
732
|
+
await client`SELECT 1 as test`;
|
|
733
|
+
if (attempt > 0) {
|
|
734
|
+
dbLogger.info(`Database connected successfully after ${attempt} retries`);
|
|
735
|
+
} else {
|
|
736
|
+
dbLogger.info("Database connected successfully");
|
|
737
|
+
}
|
|
738
|
+
return client;
|
|
739
|
+
} catch (error) {
|
|
740
|
+
lastError = fromPostgresError(error);
|
|
741
|
+
if (attempt < retryConfig.maxRetries) {
|
|
742
|
+
const delayMs = Math.min(
|
|
743
|
+
retryConfig.initialDelay * Math.pow(retryConfig.factor, attempt),
|
|
744
|
+
retryConfig.maxDelay
|
|
745
|
+
);
|
|
746
|
+
dbLogger.warn(
|
|
747
|
+
`Connection failed (attempt ${attempt + 1}/${retryConfig.maxRetries + 1}), retrying in ${delayMs}ms...`,
|
|
748
|
+
lastError,
|
|
749
|
+
{
|
|
750
|
+
attempt: attempt + 1,
|
|
751
|
+
maxRetries: retryConfig.maxRetries + 1,
|
|
752
|
+
delayMs
|
|
753
|
+
}
|
|
754
|
+
);
|
|
755
|
+
await delay(delayMs);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const errorMessage = `Failed to connect to database after ${retryConfig.maxRetries + 1} attempts: ${lastError?.message || "Unknown error"}`;
|
|
760
|
+
throw new ConnectionError(errorMessage);
|
|
761
|
+
}
|
|
762
|
+
async function checkConnection(client) {
|
|
763
|
+
try {
|
|
764
|
+
await client`SELECT 1 as health_check`;
|
|
765
|
+
return true;
|
|
766
|
+
} catch (error) {
|
|
767
|
+
dbLogger.error("Database health check failed", error);
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/db/manager/config.ts
|
|
773
|
+
function getPoolConfig(options) {
|
|
774
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
775
|
+
const max = options?.max ?? (parseInt(process.env.DB_POOL_MAX || "", 10) || (isProduction ? 20 : 10));
|
|
776
|
+
const idleTimeout = options?.idleTimeout ?? (parseInt(process.env.DB_POOL_IDLE_TIMEOUT || "", 10) || (isProduction ? 30 : 20));
|
|
777
|
+
return { max, idleTimeout };
|
|
778
|
+
}
|
|
779
|
+
function getRetryConfig() {
|
|
780
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
781
|
+
return {
|
|
782
|
+
maxRetries: isProduction ? 5 : 3,
|
|
783
|
+
// 프로덕션: 5회, 개발: 3회
|
|
784
|
+
initialDelay: 1e3,
|
|
785
|
+
// 1초
|
|
786
|
+
maxDelay: 16e3,
|
|
787
|
+
// 16초
|
|
788
|
+
factor: 2
|
|
789
|
+
// 2배씩 증가 (1s → 2s → 4s → 8s → 16s)
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/db/manager/factory.ts
|
|
794
|
+
var dbLogger2 = logger.child("database");
|
|
795
|
+
function hasDatabaseConfig() {
|
|
796
|
+
return !!(process.env.DATABASE_URL || process.env.DATABASE_WRITE_URL || process.env.DATABASE_READ_URL);
|
|
797
|
+
}
|
|
798
|
+
async function createDatabaseFromEnv(options) {
|
|
799
|
+
if (!hasDatabaseConfig()) {
|
|
800
|
+
config({ path: ".env.local" });
|
|
801
|
+
}
|
|
802
|
+
if (!hasDatabaseConfig()) {
|
|
803
|
+
return { write: void 0, read: void 0 };
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
const poolConfig = getPoolConfig(options?.pool);
|
|
807
|
+
const retryConfig = getRetryConfig();
|
|
808
|
+
if (process.env.DATABASE_WRITE_URL && process.env.DATABASE_READ_URL) {
|
|
809
|
+
const writeClient2 = await createDatabaseConnection(
|
|
810
|
+
process.env.DATABASE_WRITE_URL,
|
|
811
|
+
poolConfig,
|
|
812
|
+
retryConfig
|
|
813
|
+
);
|
|
814
|
+
const readClient2 = await createDatabaseConnection(
|
|
815
|
+
process.env.DATABASE_READ_URL,
|
|
816
|
+
poolConfig,
|
|
817
|
+
retryConfig
|
|
818
|
+
);
|
|
819
|
+
return {
|
|
820
|
+
write: drizzle(writeClient2),
|
|
821
|
+
read: drizzle(readClient2),
|
|
822
|
+
writeClient: writeClient2,
|
|
823
|
+
readClient: readClient2
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
if (process.env.DATABASE_URL && process.env.DATABASE_REPLICA_URL) {
|
|
827
|
+
const writeClient2 = await createDatabaseConnection(
|
|
828
|
+
process.env.DATABASE_URL,
|
|
829
|
+
poolConfig,
|
|
830
|
+
retryConfig
|
|
831
|
+
);
|
|
832
|
+
const readClient2 = await createDatabaseConnection(
|
|
833
|
+
process.env.DATABASE_REPLICA_URL,
|
|
834
|
+
poolConfig,
|
|
835
|
+
retryConfig
|
|
836
|
+
);
|
|
837
|
+
return {
|
|
838
|
+
write: drizzle(writeClient2),
|
|
839
|
+
read: drizzle(readClient2),
|
|
840
|
+
writeClient: writeClient2,
|
|
841
|
+
readClient: readClient2
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
if (process.env.DATABASE_URL) {
|
|
845
|
+
const client = await createDatabaseConnection(
|
|
846
|
+
process.env.DATABASE_URL,
|
|
847
|
+
poolConfig,
|
|
848
|
+
retryConfig
|
|
849
|
+
);
|
|
850
|
+
const db2 = drizzle(client);
|
|
851
|
+
return {
|
|
852
|
+
write: db2,
|
|
853
|
+
read: db2,
|
|
854
|
+
writeClient: client,
|
|
855
|
+
readClient: client
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
if (process.env.DATABASE_WRITE_URL) {
|
|
859
|
+
const client = await createDatabaseConnection(
|
|
860
|
+
process.env.DATABASE_WRITE_URL,
|
|
861
|
+
poolConfig,
|
|
862
|
+
retryConfig
|
|
863
|
+
);
|
|
864
|
+
const db2 = drizzle(client);
|
|
865
|
+
return {
|
|
866
|
+
write: db2,
|
|
867
|
+
read: db2,
|
|
868
|
+
writeClient: client,
|
|
869
|
+
readClient: client
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
return { write: void 0, read: void 0 };
|
|
873
|
+
} catch (error) {
|
|
874
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
875
|
+
dbLogger2.error("Failed to create database connection", {
|
|
876
|
+
error: message,
|
|
877
|
+
stage: "initialization",
|
|
878
|
+
hasWriteUrl: !!process.env.DATABASE_WRITE_URL,
|
|
879
|
+
hasReadUrl: !!process.env.DATABASE_READ_URL,
|
|
880
|
+
hasUrl: !!process.env.DATABASE_URL,
|
|
881
|
+
hasReplicaUrl: !!process.env.DATABASE_REPLICA_URL
|
|
882
|
+
});
|
|
883
|
+
return { write: void 0, read: void 0 };
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/db/manager/manager.ts
|
|
888
|
+
var dbLogger3 = logger.child("database");
|
|
889
|
+
var writeInstance;
|
|
890
|
+
var readInstance;
|
|
891
|
+
var writeClient;
|
|
892
|
+
var readClient;
|
|
893
|
+
var healthCheckInterval;
|
|
894
|
+
var monitoringConfig;
|
|
895
|
+
function getDatabase(type) {
|
|
896
|
+
if (type === "read") {
|
|
897
|
+
return readInstance ?? writeInstance;
|
|
898
|
+
}
|
|
899
|
+
return writeInstance;
|
|
900
|
+
}
|
|
901
|
+
function setDatabase(write, read) {
|
|
902
|
+
writeInstance = write;
|
|
903
|
+
readInstance = read ?? write;
|
|
904
|
+
}
|
|
905
|
+
function getHealthCheckConfig(options) {
|
|
906
|
+
const parseBoolean = (value, defaultValue) => {
|
|
907
|
+
if (value === void 0) return defaultValue;
|
|
908
|
+
return value.toLowerCase() === "true";
|
|
909
|
+
};
|
|
910
|
+
return {
|
|
911
|
+
enabled: options?.enabled ?? parseBoolean(process.env.DB_HEALTH_CHECK_ENABLED, true),
|
|
912
|
+
interval: options?.interval ?? (parseInt(process.env.DB_HEALTH_CHECK_INTERVAL || "", 10) || 6e4),
|
|
913
|
+
reconnect: options?.reconnect ?? parseBoolean(process.env.DB_HEALTH_CHECK_RECONNECT, true),
|
|
914
|
+
maxRetries: options?.maxRetries ?? (parseInt(process.env.DB_HEALTH_CHECK_MAX_RETRIES || "", 10) || 3),
|
|
915
|
+
retryInterval: options?.retryInterval ?? (parseInt(process.env.DB_HEALTH_CHECK_RETRY_INTERVAL || "", 10) || 5e3)
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function getMonitoringConfig(options) {
|
|
919
|
+
const isDevelopment = process.env.NODE_ENV !== "production";
|
|
920
|
+
const parseBoolean = (value, defaultValue) => {
|
|
921
|
+
if (value === void 0) return defaultValue;
|
|
922
|
+
return value.toLowerCase() === "true";
|
|
923
|
+
};
|
|
924
|
+
return {
|
|
925
|
+
enabled: options?.enabled ?? parseBoolean(process.env.DB_MONITORING_ENABLED, isDevelopment),
|
|
926
|
+
slowThreshold: options?.slowThreshold ?? (parseInt(process.env.DB_MONITORING_SLOW_THRESHOLD || "", 10) || 1e3),
|
|
927
|
+
logQueries: options?.logQueries ?? parseBoolean(process.env.DB_MONITORING_LOG_QUERIES, false)
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
async function initDatabase(options) {
|
|
931
|
+
if (writeInstance) {
|
|
932
|
+
dbLogger3.debug("Database already initialized");
|
|
933
|
+
return { write: writeInstance, read: readInstance };
|
|
934
|
+
}
|
|
935
|
+
const result = await createDatabaseFromEnv(options);
|
|
936
|
+
if (result.write) {
|
|
937
|
+
try {
|
|
938
|
+
await result.write.execute("SELECT 1");
|
|
939
|
+
if (result.read && result.read !== result.write) {
|
|
940
|
+
await result.read.execute("SELECT 1");
|
|
941
|
+
}
|
|
942
|
+
writeInstance = result.write;
|
|
943
|
+
readInstance = result.read;
|
|
944
|
+
writeClient = result.writeClient;
|
|
945
|
+
readClient = result.readClient;
|
|
946
|
+
const hasReplica = result.read && result.read !== result.write;
|
|
947
|
+
dbLogger3.info(
|
|
948
|
+
hasReplica ? "Database connected (Primary + Replica)" : "Database connected"
|
|
949
|
+
);
|
|
950
|
+
const healthCheckConfig = getHealthCheckConfig(options?.healthCheck);
|
|
951
|
+
if (healthCheckConfig.enabled) {
|
|
952
|
+
startHealthCheck(healthCheckConfig);
|
|
953
|
+
}
|
|
954
|
+
monitoringConfig = getMonitoringConfig(options?.monitoring);
|
|
955
|
+
if (monitoringConfig.enabled) {
|
|
956
|
+
dbLogger3.info("Database query monitoring enabled", {
|
|
957
|
+
slowThreshold: `${monitoringConfig.slowThreshold}ms`,
|
|
958
|
+
logQueries: monitoringConfig.logQueries
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
} catch (error) {
|
|
962
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
963
|
+
dbLogger3.error("Database connection failed", { error: message });
|
|
964
|
+
await closeDatabase();
|
|
965
|
+
return { write: void 0, read: void 0 };
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
dbLogger3.warn("No database configuration found");
|
|
969
|
+
dbLogger3.warn("Set DATABASE_URL environment variable to enable database");
|
|
970
|
+
}
|
|
971
|
+
return { write: writeInstance, read: readInstance };
|
|
972
|
+
}
|
|
973
|
+
async function closeDatabase() {
|
|
974
|
+
if (!writeInstance && !readInstance) {
|
|
975
|
+
dbLogger3.debug("No database connections to close");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
stopHealthCheck();
|
|
979
|
+
try {
|
|
980
|
+
const closePromises = [];
|
|
981
|
+
if (writeClient) {
|
|
982
|
+
dbLogger3.debug("Closing write connection...");
|
|
983
|
+
closePromises.push(
|
|
984
|
+
writeClient.end({ timeout: 5 }).then(() => dbLogger3.debug("Write connection closed")).catch((err) => dbLogger3.error("Error closing write connection", err))
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
if (readClient && readClient !== writeClient) {
|
|
988
|
+
dbLogger3.debug("Closing read connection...");
|
|
989
|
+
closePromises.push(
|
|
990
|
+
readClient.end({ timeout: 5 }).then(() => dbLogger3.debug("Read connection closed")).catch((err) => dbLogger3.error("Error closing read connection", err))
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
await Promise.all(closePromises);
|
|
994
|
+
dbLogger3.info("All database connections closed");
|
|
995
|
+
} catch (error) {
|
|
996
|
+
dbLogger3.error("Error during database cleanup", error);
|
|
997
|
+
throw error;
|
|
998
|
+
} finally {
|
|
999
|
+
writeInstance = void 0;
|
|
1000
|
+
readInstance = void 0;
|
|
1001
|
+
writeClient = void 0;
|
|
1002
|
+
readClient = void 0;
|
|
1003
|
+
monitoringConfig = void 0;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function getDatabaseInfo() {
|
|
1007
|
+
return {
|
|
1008
|
+
hasWrite: !!writeInstance,
|
|
1009
|
+
hasRead: !!readInstance,
|
|
1010
|
+
isReplica: !!(readInstance && readInstance !== writeInstance)
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function startHealthCheck(config2) {
|
|
1014
|
+
if (healthCheckInterval) {
|
|
1015
|
+
dbLogger3.debug("Health check already running");
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
dbLogger3.info("Starting database health check", {
|
|
1019
|
+
interval: `${config2.interval}ms`,
|
|
1020
|
+
reconnect: config2.reconnect
|
|
1021
|
+
});
|
|
1022
|
+
healthCheckInterval = setInterval(async () => {
|
|
1023
|
+
try {
|
|
1024
|
+
const write = getDatabase("write");
|
|
1025
|
+
const read = getDatabase("read");
|
|
1026
|
+
if (write) {
|
|
1027
|
+
await write.execute("SELECT 1");
|
|
1028
|
+
}
|
|
1029
|
+
if (read && read !== write) {
|
|
1030
|
+
await read.execute("SELECT 1");
|
|
1031
|
+
}
|
|
1032
|
+
dbLogger3.debug("Database health check passed");
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1035
|
+
dbLogger3.error("Database health check failed", { error: message });
|
|
1036
|
+
if (config2.reconnect) {
|
|
1037
|
+
await attemptReconnection(config2);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}, config2.interval);
|
|
1041
|
+
}
|
|
1042
|
+
async function attemptReconnection(config2) {
|
|
1043
|
+
dbLogger3.warn("Attempting database reconnection", {
|
|
1044
|
+
maxRetries: config2.maxRetries,
|
|
1045
|
+
retryInterval: `${config2.retryInterval}ms`
|
|
1046
|
+
});
|
|
1047
|
+
for (let attempt = 1; attempt <= config2.maxRetries; attempt++) {
|
|
1048
|
+
try {
|
|
1049
|
+
dbLogger3.debug(`Reconnection attempt ${attempt}/${config2.maxRetries}`);
|
|
1050
|
+
await closeDatabase();
|
|
1051
|
+
await new Promise((resolve) => setTimeout(resolve, config2.retryInterval));
|
|
1052
|
+
const result = await createDatabaseFromEnv();
|
|
1053
|
+
if (result.write) {
|
|
1054
|
+
await result.write.execute("SELECT 1");
|
|
1055
|
+
writeInstance = result.write;
|
|
1056
|
+
readInstance = result.read;
|
|
1057
|
+
writeClient = result.writeClient;
|
|
1058
|
+
readClient = result.readClient;
|
|
1059
|
+
dbLogger3.info("Database reconnection successful", { attempt });
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1064
|
+
dbLogger3.error(`Reconnection attempt ${attempt} failed`, {
|
|
1065
|
+
error: message,
|
|
1066
|
+
attempt,
|
|
1067
|
+
maxRetries: config2.maxRetries
|
|
1068
|
+
});
|
|
1069
|
+
if (attempt === config2.maxRetries) {
|
|
1070
|
+
dbLogger3.error("Max reconnection attempts reached, giving up");
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function stopHealthCheck() {
|
|
1076
|
+
if (healthCheckInterval) {
|
|
1077
|
+
clearInterval(healthCheckInterval);
|
|
1078
|
+
healthCheckInterval = void 0;
|
|
1079
|
+
dbLogger3.info("Database health check stopped");
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
function getDatabaseMonitoringConfig() {
|
|
1083
|
+
return monitoringConfig;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/db/manager/instance.ts
|
|
1087
|
+
var db = new Proxy({}, {
|
|
1088
|
+
get(_target, prop) {
|
|
1089
|
+
const instance = getDatabase("write");
|
|
1090
|
+
if (!instance) {
|
|
1091
|
+
throw new Error(
|
|
1092
|
+
"Database not initialized. Set DATABASE_URL environment variable or call initDatabase() first."
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
return instance[prop];
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
function getRawDb(type = "write") {
|
|
1099
|
+
const instance = getDatabase(type);
|
|
1100
|
+
if (!instance) {
|
|
1101
|
+
throw new Error(
|
|
1102
|
+
"Database not initialized. Set DATABASE_URL environment variable or call initDatabase() first."
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
return instance;
|
|
1106
|
+
}
|
|
1107
|
+
var asyncContext = new AsyncLocalStorage();
|
|
1108
|
+
function getTransaction() {
|
|
1109
|
+
const context = asyncContext.getStore();
|
|
1110
|
+
return context?.tx ?? null;
|
|
1111
|
+
}
|
|
1112
|
+
function runWithTransaction(tx, callback) {
|
|
1113
|
+
return asyncContext.run({ tx }, callback);
|
|
1114
|
+
}
|
|
1115
|
+
function Transactional(options = {}) {
|
|
1116
|
+
const defaultTimeout = parseInt(process.env.TRANSACTION_TIMEOUT || "30000", 10);
|
|
1117
|
+
const {
|
|
1118
|
+
slowThreshold = 1e3,
|
|
1119
|
+
enableLogging = true,
|
|
1120
|
+
timeout = defaultTimeout
|
|
1121
|
+
} = options;
|
|
1122
|
+
const txLogger = logger.child("transaction");
|
|
1123
|
+
return createMiddleware(async (c, next) => {
|
|
1124
|
+
const txId = `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1125
|
+
const startTime = Date.now();
|
|
1126
|
+
const route = `${c.req.method} ${c.req.path}`;
|
|
1127
|
+
if (enableLogging) {
|
|
1128
|
+
txLogger.debug("Transaction started", { txId, route });
|
|
1129
|
+
}
|
|
1130
|
+
try {
|
|
1131
|
+
const transactionPromise = db.transaction(async (tx) => {
|
|
1132
|
+
await runWithTransaction(tx, async () => {
|
|
1133
|
+
await next();
|
|
1134
|
+
const contextWithError = c;
|
|
1135
|
+
if (contextWithError.error) {
|
|
1136
|
+
throw contextWithError.error;
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
if (timeout > 0) {
|
|
1141
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1142
|
+
setTimeout(() => {
|
|
1143
|
+
reject(
|
|
1144
|
+
new TransactionError(
|
|
1145
|
+
`Transaction timeout after ${timeout}ms`,
|
|
1146
|
+
500,
|
|
1147
|
+
{
|
|
1148
|
+
txId,
|
|
1149
|
+
route,
|
|
1150
|
+
timeout: `${timeout}ms`
|
|
1151
|
+
}
|
|
1152
|
+
)
|
|
1153
|
+
);
|
|
1154
|
+
}, timeout);
|
|
1155
|
+
});
|
|
1156
|
+
await Promise.race([transactionPromise, timeoutPromise]);
|
|
1157
|
+
} else {
|
|
1158
|
+
await transactionPromise;
|
|
1159
|
+
}
|
|
1160
|
+
const duration = Date.now() - startTime;
|
|
1161
|
+
if (enableLogging) {
|
|
1162
|
+
if (duration >= slowThreshold) {
|
|
1163
|
+
txLogger.warn("Slow transaction committed", {
|
|
1164
|
+
txId,
|
|
1165
|
+
route,
|
|
1166
|
+
duration: `${duration}ms`,
|
|
1167
|
+
threshold: `${slowThreshold}ms`
|
|
1168
|
+
});
|
|
1169
|
+
} else {
|
|
1170
|
+
txLogger.debug("Transaction committed", {
|
|
1171
|
+
txId,
|
|
1172
|
+
route,
|
|
1173
|
+
duration: `${duration}ms`
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
const duration = Date.now() - startTime;
|
|
1179
|
+
const customError = error instanceof TransactionError ? error : fromPostgresError(error);
|
|
1180
|
+
if (enableLogging) {
|
|
1181
|
+
txLogger.error("Transaction rolled back", {
|
|
1182
|
+
txId,
|
|
1183
|
+
route,
|
|
1184
|
+
duration: `${duration}ms`,
|
|
1185
|
+
error: customError.message,
|
|
1186
|
+
errorType: customError.name
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
throw customError;
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
function buildFilters(filters, table) {
|
|
1194
|
+
const conditions = [];
|
|
1195
|
+
for (const [field, filterCondition] of Object.entries(filters)) {
|
|
1196
|
+
const column = table[field];
|
|
1197
|
+
if (!column) {
|
|
1198
|
+
console.warn(`[buildFilters] Unknown field: ${field}`);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
for (const [operator, value] of Object.entries(filterCondition)) {
|
|
1202
|
+
const condition = buildCondition(column, operator, value);
|
|
1203
|
+
if (condition) {
|
|
1204
|
+
conditions.push(condition);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return conditions.length > 0 ? and(...conditions) : void 0;
|
|
1209
|
+
}
|
|
1210
|
+
function buildCondition(column, operator, value) {
|
|
1211
|
+
switch (operator) {
|
|
1212
|
+
case "eq":
|
|
1213
|
+
return eq(column, value);
|
|
1214
|
+
case "ne":
|
|
1215
|
+
return ne(column, value);
|
|
1216
|
+
case "gt":
|
|
1217
|
+
return gt(column, value);
|
|
1218
|
+
case "gte":
|
|
1219
|
+
return gte(column, value);
|
|
1220
|
+
case "lt":
|
|
1221
|
+
return lt(column, value);
|
|
1222
|
+
case "lte":
|
|
1223
|
+
return lte(column, value);
|
|
1224
|
+
case "like":
|
|
1225
|
+
return like(column, `%${value}%`);
|
|
1226
|
+
case "in":
|
|
1227
|
+
if (Array.isArray(value)) {
|
|
1228
|
+
return inArray(column, value);
|
|
1229
|
+
}
|
|
1230
|
+
console.warn(`[buildCondition] 'in' operator requires array value`);
|
|
1231
|
+
return void 0;
|
|
1232
|
+
case "nin":
|
|
1233
|
+
if (Array.isArray(value)) {
|
|
1234
|
+
return notInArray(column, value);
|
|
1235
|
+
}
|
|
1236
|
+
console.warn(`[buildCondition] 'nin' operator requires array value`);
|
|
1237
|
+
return void 0;
|
|
1238
|
+
case "is":
|
|
1239
|
+
if (value === "null") return isNull(column);
|
|
1240
|
+
if (value === "notnull") return isNotNull(column);
|
|
1241
|
+
console.warn(`[buildCondition] 'is' operator requires 'null' or 'notnull'`);
|
|
1242
|
+
return void 0;
|
|
1243
|
+
default:
|
|
1244
|
+
console.warn(`[buildCondition] Unknown operator: ${operator}`);
|
|
1245
|
+
return void 0;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function buildSort(sortConditions, table) {
|
|
1249
|
+
const orderByClauses = [];
|
|
1250
|
+
for (const { field, direction } of sortConditions) {
|
|
1251
|
+
const column = table[field];
|
|
1252
|
+
if (!column) {
|
|
1253
|
+
console.warn(`[buildSort] Unknown field: ${field}`);
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
const clause = direction === "desc" ? desc(column) : asc(column);
|
|
1257
|
+
orderByClauses.push(clause);
|
|
1258
|
+
}
|
|
1259
|
+
return orderByClauses;
|
|
1260
|
+
}
|
|
1261
|
+
function applyPagination(pagination) {
|
|
1262
|
+
const { page, limit } = pagination;
|
|
1263
|
+
const offset = (page - 1) * limit;
|
|
1264
|
+
return { offset, limit };
|
|
1265
|
+
}
|
|
1266
|
+
function createPaginationMeta(pagination, total) {
|
|
1267
|
+
const { page, limit } = pagination;
|
|
1268
|
+
const totalPages = Math.ceil(total / limit);
|
|
1269
|
+
return {
|
|
1270
|
+
page,
|
|
1271
|
+
limit,
|
|
1272
|
+
total,
|
|
1273
|
+
totalPages,
|
|
1274
|
+
hasNext: page < totalPages,
|
|
1275
|
+
hasPrev: page > 1
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
async function countTotal(db2, table, whereCondition) {
|
|
1279
|
+
const query = db2.select({ count: sql`count(*)::int` }).from(table);
|
|
1280
|
+
if (whereCondition) {
|
|
1281
|
+
query.where(whereCondition);
|
|
1282
|
+
}
|
|
1283
|
+
const [result] = await query;
|
|
1284
|
+
return result?.count || 0;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/db/repository/query-builder.ts
|
|
1288
|
+
var QueryBuilder = class {
|
|
1289
|
+
db;
|
|
1290
|
+
table;
|
|
1291
|
+
filterConditions = [];
|
|
1292
|
+
sortConditions = [];
|
|
1293
|
+
limitValue;
|
|
1294
|
+
offsetValue;
|
|
1295
|
+
constructor(db2, table) {
|
|
1296
|
+
this.db = db2;
|
|
1297
|
+
this.table = table;
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Add WHERE conditions
|
|
1301
|
+
*
|
|
1302
|
+
* Multiple where() calls are combined with AND logic.
|
|
1303
|
+
*
|
|
1304
|
+
* @param filters - Filter conditions
|
|
1305
|
+
* @returns QueryBuilder for chaining
|
|
1306
|
+
*
|
|
1307
|
+
* @example
|
|
1308
|
+
* ```typescript
|
|
1309
|
+
* query
|
|
1310
|
+
* .where({ status: 'active' })
|
|
1311
|
+
* .where({ role: 'admin' }) // AND condition
|
|
1312
|
+
* ```
|
|
1313
|
+
*/
|
|
1314
|
+
where(filters) {
|
|
1315
|
+
this.filterConditions.push(filters);
|
|
1316
|
+
return this;
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Add ORDER BY clause
|
|
1320
|
+
*
|
|
1321
|
+
* Multiple orderBy() calls create multi-column sorting.
|
|
1322
|
+
*
|
|
1323
|
+
* @param field - Field name to sort by
|
|
1324
|
+
* @param direction - Sort direction ('asc' or 'desc')
|
|
1325
|
+
* @returns QueryBuilder for chaining
|
|
1326
|
+
*
|
|
1327
|
+
* @example
|
|
1328
|
+
* ```typescript
|
|
1329
|
+
* query
|
|
1330
|
+
* .orderBy('isPremium', 'desc')
|
|
1331
|
+
* .orderBy('createdAt', 'desc')
|
|
1332
|
+
* ```
|
|
1333
|
+
*/
|
|
1334
|
+
orderBy(field, direction = "asc") {
|
|
1335
|
+
this.sortConditions.push({ field, direction });
|
|
1336
|
+
return this;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Set LIMIT clause
|
|
1340
|
+
*
|
|
1341
|
+
* @param limit - Maximum number of records to return
|
|
1342
|
+
* @returns QueryBuilder for chaining
|
|
1343
|
+
*
|
|
1344
|
+
* @example
|
|
1345
|
+
* ```typescript
|
|
1346
|
+
* query.limit(10)
|
|
1347
|
+
* ```
|
|
1348
|
+
*/
|
|
1349
|
+
limit(limit) {
|
|
1350
|
+
this.limitValue = limit;
|
|
1351
|
+
return this;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Set OFFSET clause
|
|
1355
|
+
*
|
|
1356
|
+
* @param offset - Number of records to skip
|
|
1357
|
+
* @returns QueryBuilder for chaining
|
|
1358
|
+
*
|
|
1359
|
+
* @example
|
|
1360
|
+
* ```typescript
|
|
1361
|
+
* query.offset(20)
|
|
1362
|
+
* ```
|
|
1363
|
+
*/
|
|
1364
|
+
offset(offset) {
|
|
1365
|
+
this.offsetValue = offset;
|
|
1366
|
+
return this;
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Execute query and return multiple records
|
|
1370
|
+
*
|
|
1371
|
+
* @returns Array of records
|
|
1372
|
+
*
|
|
1373
|
+
* @example
|
|
1374
|
+
* ```typescript
|
|
1375
|
+
* const users = await query
|
|
1376
|
+
* .where({ status: 'active' })
|
|
1377
|
+
* .orderBy('createdAt', 'desc')
|
|
1378
|
+
* .limit(10)
|
|
1379
|
+
* .findMany();
|
|
1380
|
+
* ```
|
|
1381
|
+
*/
|
|
1382
|
+
async findMany() {
|
|
1383
|
+
const mergedFilters = this.mergeFilters();
|
|
1384
|
+
const whereCondition = buildFilters(mergedFilters, this.table);
|
|
1385
|
+
const orderBy = buildSort(this.sortConditions, this.table);
|
|
1386
|
+
let query = this.db.select().from(this.table).where(whereCondition).orderBy(...orderBy);
|
|
1387
|
+
if (this.limitValue !== void 0) {
|
|
1388
|
+
query = query.limit(this.limitValue);
|
|
1389
|
+
}
|
|
1390
|
+
if (this.offsetValue !== void 0) {
|
|
1391
|
+
query = query.offset(this.offsetValue);
|
|
1392
|
+
}
|
|
1393
|
+
return query;
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Execute query and return first record
|
|
1397
|
+
*
|
|
1398
|
+
* @returns First matching record or null
|
|
1399
|
+
*
|
|
1400
|
+
* @example
|
|
1401
|
+
* ```typescript
|
|
1402
|
+
* const user = await query
|
|
1403
|
+
* .where({ email: 'john@example.com' })
|
|
1404
|
+
* .findOne();
|
|
1405
|
+
* ```
|
|
1406
|
+
*/
|
|
1407
|
+
async findOne() {
|
|
1408
|
+
const results = await this.limit(1).findMany();
|
|
1409
|
+
return results[0] ?? null;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Execute query and return count
|
|
1413
|
+
*
|
|
1414
|
+
* @returns Number of matching records
|
|
1415
|
+
*
|
|
1416
|
+
* @example
|
|
1417
|
+
* ```typescript
|
|
1418
|
+
* const count = await query
|
|
1419
|
+
* .where({ status: 'active' })
|
|
1420
|
+
* .count();
|
|
1421
|
+
* ```
|
|
1422
|
+
*/
|
|
1423
|
+
async count() {
|
|
1424
|
+
const mergedFilters = this.mergeFilters();
|
|
1425
|
+
const whereCondition = buildFilters(mergedFilters, this.table);
|
|
1426
|
+
const { count } = await import('drizzle-orm');
|
|
1427
|
+
const result = await this.db.select({ count: count() }).from(this.table).where(whereCondition);
|
|
1428
|
+
return Number(result[0]?.count ?? 0);
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Merge multiple filter conditions into single object
|
|
1432
|
+
*
|
|
1433
|
+
* Combines all where() calls into one filter object.
|
|
1434
|
+
*/
|
|
1435
|
+
mergeFilters() {
|
|
1436
|
+
if (this.filterConditions.length === 0) {
|
|
1437
|
+
return {};
|
|
1438
|
+
}
|
|
1439
|
+
return this.filterConditions.reduce((merged, current) => {
|
|
1440
|
+
return { ...merged, ...current };
|
|
1441
|
+
}, {});
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
// src/db/repository/repository.ts
|
|
1446
|
+
var Repository = class {
|
|
1447
|
+
db;
|
|
1448
|
+
table;
|
|
1449
|
+
useReplica;
|
|
1450
|
+
explicitDb;
|
|
1451
|
+
// Track if db was explicitly provided
|
|
1452
|
+
autoUpdateField;
|
|
1453
|
+
// Field name to auto-update (e.g., 'updatedAt', 'modifiedAt')
|
|
1454
|
+
constructor(dbOrTable, tableOrUseReplica, useReplica = true) {
|
|
1455
|
+
if ("name" in dbOrTable && typeof dbOrTable.name === "string") {
|
|
1456
|
+
this.db = getRawDb("write");
|
|
1457
|
+
this.table = dbOrTable;
|
|
1458
|
+
this.useReplica = typeof tableOrUseReplica === "boolean" ? tableOrUseReplica : true;
|
|
1459
|
+
this.explicitDb = void 0;
|
|
1460
|
+
} else {
|
|
1461
|
+
this.db = dbOrTable;
|
|
1462
|
+
this.table = tableOrUseReplica;
|
|
1463
|
+
this.useReplica = useReplica;
|
|
1464
|
+
this.explicitDb = this.db;
|
|
1465
|
+
}
|
|
1466
|
+
this.autoUpdateField = this.detectAutoUpdateField();
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Detect which field (if any) should be auto-updated
|
|
1470
|
+
*
|
|
1471
|
+
* Checks all table columns for __autoUpdate metadata flag.
|
|
1472
|
+
* Set by autoUpdateTimestamp() or timestamps({ autoUpdate: true }) helpers.
|
|
1473
|
+
*
|
|
1474
|
+
* @returns Field name to auto-update, or undefined if none found
|
|
1475
|
+
*/
|
|
1476
|
+
detectAutoUpdateField() {
|
|
1477
|
+
if (!this.table || typeof this.table !== "object") {
|
|
1478
|
+
return void 0;
|
|
1479
|
+
}
|
|
1480
|
+
const tableColumns = this.table;
|
|
1481
|
+
for (const [fieldName, column] of Object.entries(tableColumns)) {
|
|
1482
|
+
if (fieldName.startsWith("_") || fieldName.startsWith("$")) {
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (column && typeof column === "object" && column.__autoUpdate === true) {
|
|
1486
|
+
return fieldName;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return void 0;
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Inject auto-update timestamp if configured
|
|
1493
|
+
*
|
|
1494
|
+
* Only injects if:
|
|
1495
|
+
* 1. Table has an auto-update field configured (via autoUpdateTimestamp() or timestamps({ autoUpdate: true }))
|
|
1496
|
+
* 2. The field is not already explicitly provided in the data
|
|
1497
|
+
*
|
|
1498
|
+
* @param data - Update data object
|
|
1499
|
+
* @returns Data with auto-update timestamp injected (if applicable)
|
|
1500
|
+
*/
|
|
1501
|
+
injectAutoUpdateTimestamp(data) {
|
|
1502
|
+
if (!this.autoUpdateField) {
|
|
1503
|
+
return data;
|
|
1504
|
+
}
|
|
1505
|
+
if (data && this.autoUpdateField in data) {
|
|
1506
|
+
return data;
|
|
1507
|
+
}
|
|
1508
|
+
return {
|
|
1509
|
+
...data,
|
|
1510
|
+
[this.autoUpdateField]: /* @__PURE__ */ new Date()
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Get id column from table
|
|
1515
|
+
*
|
|
1516
|
+
* Helper method to reduce code duplication across methods that need id column.
|
|
1517
|
+
*
|
|
1518
|
+
* @returns The id column object
|
|
1519
|
+
* @throws {QueryError} If table does not have an id column
|
|
1520
|
+
*/
|
|
1521
|
+
getIdColumn() {
|
|
1522
|
+
const idColumn = this.table.id;
|
|
1523
|
+
if (!idColumn) {
|
|
1524
|
+
throw new QueryError("Table does not have an id column");
|
|
1525
|
+
}
|
|
1526
|
+
return idColumn;
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Get read-only DB
|
|
1530
|
+
*
|
|
1531
|
+
* Automatically detects and uses transaction context if available.
|
|
1532
|
+
* When in transaction, uses transaction DB to ensure read consistency.
|
|
1533
|
+
* Priority: explicitDb > transaction > replica/primary DB
|
|
1534
|
+
*/
|
|
1535
|
+
getReadDb() {
|
|
1536
|
+
if (this.explicitDb) {
|
|
1537
|
+
return this.explicitDb;
|
|
1538
|
+
}
|
|
1539
|
+
const tx = getTransaction();
|
|
1540
|
+
if (tx) {
|
|
1541
|
+
return tx;
|
|
1542
|
+
}
|
|
1543
|
+
return this.useReplica ? getRawDb("read") : this.db;
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Get write-only DB
|
|
1547
|
+
*
|
|
1548
|
+
* Automatically detects and uses transaction context if available.
|
|
1549
|
+
* Priority: explicitDb > transaction > primary DB
|
|
1550
|
+
*/
|
|
1551
|
+
getWriteDb() {
|
|
1552
|
+
if (this.explicitDb) {
|
|
1553
|
+
return this.explicitDb;
|
|
1554
|
+
}
|
|
1555
|
+
const tx = getTransaction();
|
|
1556
|
+
if (tx) {
|
|
1557
|
+
return tx;
|
|
1558
|
+
}
|
|
1559
|
+
return getRawDb("write");
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Execute operation with performance monitoring
|
|
1563
|
+
*
|
|
1564
|
+
* Wraps database operations with timing and logging for slow queries.
|
|
1565
|
+
* Only logs if monitoring is enabled and query exceeds threshold.
|
|
1566
|
+
*
|
|
1567
|
+
* @param operation - Name of the operation (for logging)
|
|
1568
|
+
* @param fn - Async function to execute
|
|
1569
|
+
* @returns Result of the operation
|
|
1570
|
+
*/
|
|
1571
|
+
async executeWithMonitoring(operation, fn) {
|
|
1572
|
+
const config2 = getDatabaseMonitoringConfig();
|
|
1573
|
+
if (!config2?.enabled) {
|
|
1574
|
+
return fn();
|
|
1575
|
+
}
|
|
1576
|
+
const startTime = performance.now();
|
|
1577
|
+
try {
|
|
1578
|
+
const result = await fn();
|
|
1579
|
+
const duration = performance.now() - startTime;
|
|
1580
|
+
if (duration >= config2.slowThreshold) {
|
|
1581
|
+
const dbLogger4 = logger.child("database");
|
|
1582
|
+
const logData = {
|
|
1583
|
+
operation,
|
|
1584
|
+
table: this.table._.name,
|
|
1585
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
1586
|
+
threshold: `${config2.slowThreshold}ms`
|
|
1587
|
+
};
|
|
1588
|
+
dbLogger4.warn("Slow query detected", logData);
|
|
1589
|
+
}
|
|
1590
|
+
return result;
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
const duration = performance.now() - startTime;
|
|
1593
|
+
const dbLogger4 = logger.child("database");
|
|
1594
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1595
|
+
dbLogger4.error("Query failed", {
|
|
1596
|
+
operation,
|
|
1597
|
+
table: this.table._.name,
|
|
1598
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
1599
|
+
error: message
|
|
1600
|
+
});
|
|
1601
|
+
throw error;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Find all records (uses Replica)
|
|
1606
|
+
*
|
|
1607
|
+
* @example
|
|
1608
|
+
* const users = await userRepo.findAll();
|
|
1609
|
+
*/
|
|
1610
|
+
async findAll() {
|
|
1611
|
+
return this.executeWithMonitoring("findAll", async () => {
|
|
1612
|
+
const readDb = this.getReadDb();
|
|
1613
|
+
return readDb.select().from(this.table);
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Find with pagination (uses Replica)
|
|
1618
|
+
*
|
|
1619
|
+
* @example
|
|
1620
|
+
* const result = await userRepo.findPage({
|
|
1621
|
+
* filters: { email: { like: 'john' } },
|
|
1622
|
+
* sort: [{ field: 'createdAt', direction: 'desc' }],
|
|
1623
|
+
* pagination: { page: 1, limit: 20 }
|
|
1624
|
+
* });
|
|
1625
|
+
*/
|
|
1626
|
+
async findPage(pageable) {
|
|
1627
|
+
return this.executeWithMonitoring("findPage", async () => {
|
|
1628
|
+
const { filters = {}, sort = [], pagination = { page: 1, limit: 20 } } = pageable;
|
|
1629
|
+
const whereCondition = buildFilters(filters, this.table);
|
|
1630
|
+
const orderBy = buildSort(sort, this.table);
|
|
1631
|
+
const { offset, limit } = applyPagination(pagination);
|
|
1632
|
+
const readDb = this.getReadDb();
|
|
1633
|
+
const data = await readDb.select().from(this.table).where(whereCondition).orderBy(...orderBy).limit(limit).offset(offset);
|
|
1634
|
+
const total = await countTotal(readDb, this.table, whereCondition);
|
|
1635
|
+
const meta = createPaginationMeta(pagination, total);
|
|
1636
|
+
return { data, meta };
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Find one record by ID (uses Replica)
|
|
1641
|
+
*
|
|
1642
|
+
* @example
|
|
1643
|
+
* const user = await userRepo.findById(1);
|
|
1644
|
+
*/
|
|
1645
|
+
async findById(id2) {
|
|
1646
|
+
return this.executeWithMonitoring("findById", async () => {
|
|
1647
|
+
const idColumn = this.getIdColumn();
|
|
1648
|
+
const { eq: eq2 } = await import('drizzle-orm');
|
|
1649
|
+
const readDb = this.getReadDb();
|
|
1650
|
+
const [result] = await readDb.select().from(this.table).where(eq2(idColumn, id2));
|
|
1651
|
+
return result ?? null;
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Find one record by condition (uses Replica)
|
|
1656
|
+
*
|
|
1657
|
+
* @example
|
|
1658
|
+
* const user = await userRepo.findOne(eq(users.email, 'john@example.com'));
|
|
1659
|
+
*/
|
|
1660
|
+
async findOne(where) {
|
|
1661
|
+
return this.executeWithMonitoring("findOne", async () => {
|
|
1662
|
+
const readDb = this.getReadDb();
|
|
1663
|
+
const [result] = await readDb.select().from(this.table).where(where);
|
|
1664
|
+
return result ?? null;
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Create a new record (uses Primary)
|
|
1669
|
+
*
|
|
1670
|
+
* @example
|
|
1671
|
+
* const user = await userRepo.save({ email: 'john@example.com', name: 'John' });
|
|
1672
|
+
*/
|
|
1673
|
+
async save(data) {
|
|
1674
|
+
return this.executeWithMonitoring("save", async () => {
|
|
1675
|
+
const writeDb = this.getWriteDb();
|
|
1676
|
+
const [result] = await writeDb.insert(this.table).values(data).returning();
|
|
1677
|
+
return result;
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Update a record (uses Primary)
|
|
1682
|
+
*
|
|
1683
|
+
* Automatically injects current timestamp if table has auto-update field configured.
|
|
1684
|
+
*
|
|
1685
|
+
* @example
|
|
1686
|
+
* const user = await userRepo.update(1, { name: 'Jane' });
|
|
1687
|
+
*/
|
|
1688
|
+
async update(id2, data) {
|
|
1689
|
+
return this.executeWithMonitoring("update", async () => {
|
|
1690
|
+
const idColumn = this.getIdColumn();
|
|
1691
|
+
const updateData = this.injectAutoUpdateTimestamp(data);
|
|
1692
|
+
const { eq: eq2 } = await import('drizzle-orm');
|
|
1693
|
+
const writeDb = this.getWriteDb();
|
|
1694
|
+
const [result] = await writeDb.update(this.table).set(updateData).where(eq2(idColumn, id2)).returning();
|
|
1695
|
+
return result ?? null;
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Delete a record (uses Primary)
|
|
1700
|
+
*
|
|
1701
|
+
* @example
|
|
1702
|
+
* const deleted = await userRepo.delete(1);
|
|
1703
|
+
*/
|
|
1704
|
+
async delete(id2) {
|
|
1705
|
+
return this.executeWithMonitoring("delete", async () => {
|
|
1706
|
+
const idColumn = this.getIdColumn();
|
|
1707
|
+
const { eq: eq2 } = await import('drizzle-orm');
|
|
1708
|
+
const writeDb = this.getWriteDb();
|
|
1709
|
+
const [result] = await writeDb.delete(this.table).where(eq2(idColumn, id2)).returning();
|
|
1710
|
+
return result ?? null;
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Count records (uses Replica)
|
|
1715
|
+
*
|
|
1716
|
+
* @example
|
|
1717
|
+
* const count = await userRepo.count();
|
|
1718
|
+
*/
|
|
1719
|
+
async count(where) {
|
|
1720
|
+
return this.executeWithMonitoring("count", async () => {
|
|
1721
|
+
const readDb = this.getReadDb();
|
|
1722
|
+
return countTotal(readDb, this.table, where);
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Find records by filters (uses Replica)
|
|
1727
|
+
*
|
|
1728
|
+
* @example
|
|
1729
|
+
* const users = await userRepo.findWhere({ email: { like: '@gmail.com' }, status: 'active' });
|
|
1730
|
+
*/
|
|
1731
|
+
async findWhere(filters) {
|
|
1732
|
+
return this.executeWithMonitoring("findWhere", async () => {
|
|
1733
|
+
const whereCondition = buildFilters(filters, this.table);
|
|
1734
|
+
const readDb = this.getReadDb();
|
|
1735
|
+
return readDb.select().from(this.table).where(whereCondition);
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Find one record by filters (uses Replica)
|
|
1740
|
+
*
|
|
1741
|
+
* @example
|
|
1742
|
+
* const user = await userRepo.findOneWhere({ email: 'john@example.com' });
|
|
1743
|
+
*/
|
|
1744
|
+
async findOneWhere(filters) {
|
|
1745
|
+
return this.executeWithMonitoring("findOneWhere", async () => {
|
|
1746
|
+
const whereCondition = buildFilters(filters, this.table);
|
|
1747
|
+
const readDb = this.getReadDb();
|
|
1748
|
+
const [result] = await readDb.select().from(this.table).where(whereCondition);
|
|
1749
|
+
return result ?? null;
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Check if record exists by ID (uses Replica)
|
|
1754
|
+
*
|
|
1755
|
+
* @example
|
|
1756
|
+
* const exists = await userRepo.exists(1);
|
|
1757
|
+
*/
|
|
1758
|
+
async exists(id2) {
|
|
1759
|
+
return this.executeWithMonitoring("exists", async () => {
|
|
1760
|
+
const idColumn = this.getIdColumn();
|
|
1761
|
+
const { eq: eq2 } = await import('drizzle-orm');
|
|
1762
|
+
const readDb = this.getReadDb();
|
|
1763
|
+
const [result] = await readDb.select().from(this.table).where(eq2(idColumn, id2)).limit(1);
|
|
1764
|
+
return !!result;
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Check if record exists by filters (uses Replica)
|
|
1769
|
+
*
|
|
1770
|
+
* @example
|
|
1771
|
+
* const exists = await userRepo.existsBy({ email: 'john@example.com' });
|
|
1772
|
+
*/
|
|
1773
|
+
async existsBy(filters) {
|
|
1774
|
+
return this.executeWithMonitoring("existsBy", async () => {
|
|
1775
|
+
const whereCondition = buildFilters(filters, this.table);
|
|
1776
|
+
const readDb = this.getReadDb();
|
|
1777
|
+
const [result] = await readDb.select().from(this.table).where(whereCondition).limit(1);
|
|
1778
|
+
return !!result;
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Count records by filters (uses Replica)
|
|
1783
|
+
*
|
|
1784
|
+
* @example
|
|
1785
|
+
* const count = await userRepo.countBy({ status: 'active' });
|
|
1786
|
+
*/
|
|
1787
|
+
async countBy(filters) {
|
|
1788
|
+
return this.executeWithMonitoring("countBy", async () => {
|
|
1789
|
+
const whereCondition = buildFilters(filters, this.table);
|
|
1790
|
+
const readDb = this.getReadDb();
|
|
1791
|
+
return countTotal(readDb, this.table, whereCondition);
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Create multiple records (uses Primary)
|
|
1796
|
+
*
|
|
1797
|
+
* @example
|
|
1798
|
+
* const users = await userRepo.saveMany([
|
|
1799
|
+
* { email: 'user1@example.com', name: 'User 1' },
|
|
1800
|
+
* { email: 'user2@example.com', name: 'User 2' }
|
|
1801
|
+
* ]);
|
|
1802
|
+
*/
|
|
1803
|
+
async saveMany(data) {
|
|
1804
|
+
return this.executeWithMonitoring("saveMany", async () => {
|
|
1805
|
+
const writeDb = this.getWriteDb();
|
|
1806
|
+
return writeDb.insert(this.table).values(data).returning();
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Update multiple records by filters (uses Primary)
|
|
1811
|
+
*
|
|
1812
|
+
* Automatically injects current timestamp if table has auto-update field configured.
|
|
1813
|
+
*
|
|
1814
|
+
* @example
|
|
1815
|
+
* const count = await userRepo.updateWhere({ status: 'inactive' }, { status: 'archived' });
|
|
1816
|
+
*/
|
|
1817
|
+
async updateWhere(filters, data) {
|
|
1818
|
+
return this.executeWithMonitoring("updateWhere", async () => {
|
|
1819
|
+
const updateData = this.injectAutoUpdateTimestamp(data);
|
|
1820
|
+
const whereCondition = buildFilters(filters, this.table);
|
|
1821
|
+
const writeDb = this.getWriteDb();
|
|
1822
|
+
const results = await writeDb.update(this.table).set(updateData).where(whereCondition).returning();
|
|
1823
|
+
return results.length;
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Delete multiple records by filters (uses Primary)
|
|
1828
|
+
*
|
|
1829
|
+
* @example
|
|
1830
|
+
* const count = await userRepo.deleteWhere({ status: 'banned' });
|
|
1831
|
+
*/
|
|
1832
|
+
async deleteWhere(filters) {
|
|
1833
|
+
return this.executeWithMonitoring("deleteWhere", async () => {
|
|
1834
|
+
const whereCondition = buildFilters(filters, this.table);
|
|
1835
|
+
const writeDb = this.getWriteDb();
|
|
1836
|
+
const results = await writeDb.delete(this.table).where(whereCondition).returning();
|
|
1837
|
+
return results.length;
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
// ============================================================
|
|
1841
|
+
// Query Builder (Fluent Interface)
|
|
1842
|
+
// ============================================================
|
|
1843
|
+
/**
|
|
1844
|
+
* Start a chainable query builder (uses Replica)
|
|
1845
|
+
*
|
|
1846
|
+
* Returns a QueryBuilder instance for building complex queries with method chaining.
|
|
1847
|
+
*
|
|
1848
|
+
* @returns QueryBuilder instance for chaining
|
|
1849
|
+
*
|
|
1850
|
+
* @example
|
|
1851
|
+
* ```typescript
|
|
1852
|
+
* // Simple chaining
|
|
1853
|
+
* const users = await userRepo
|
|
1854
|
+
* .query()
|
|
1855
|
+
* .where({ status: 'active' })
|
|
1856
|
+
* .orderBy('createdAt', 'desc')
|
|
1857
|
+
* .limit(10)
|
|
1858
|
+
* .findMany();
|
|
1859
|
+
*
|
|
1860
|
+
* // Multiple conditions
|
|
1861
|
+
* const admins = await userRepo
|
|
1862
|
+
* .query()
|
|
1863
|
+
* .where({ role: 'admin' })
|
|
1864
|
+
* .where({ status: 'active' }) // AND condition
|
|
1865
|
+
* .findMany();
|
|
1866
|
+
*
|
|
1867
|
+
* // Reusable query
|
|
1868
|
+
* const activeQuery = userRepo.query().where({ status: 'active' });
|
|
1869
|
+
* const users = await activeQuery.findMany();
|
|
1870
|
+
* const count = await activeQuery.count();
|
|
1871
|
+
* ```
|
|
1872
|
+
*/
|
|
1873
|
+
query() {
|
|
1874
|
+
const readDb = this.getReadDb();
|
|
1875
|
+
return new QueryBuilder(readDb, this.table);
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1879
|
+
// src/db/repository/factory.ts
|
|
1880
|
+
var repositoryCache = /* @__PURE__ */ new Map();
|
|
1881
|
+
function getCacheKey(table, RepositoryClass) {
|
|
1882
|
+
const tableName = table[Symbol.for("drizzle:Name")] || table.name || table.toString();
|
|
1883
|
+
const className = RepositoryClass?.name || "Repository";
|
|
1884
|
+
return `${tableName}:${className}`;
|
|
1885
|
+
}
|
|
1886
|
+
function getRepository(table, RepositoryClass) {
|
|
1887
|
+
const cacheKey = getCacheKey(table, RepositoryClass);
|
|
1888
|
+
let repo = repositoryCache.get(cacheKey);
|
|
1889
|
+
if (!repo) {
|
|
1890
|
+
if (RepositoryClass) {
|
|
1891
|
+
repo = new RepositoryClass(table);
|
|
1892
|
+
} else {
|
|
1893
|
+
repo = new Repository(table);
|
|
1894
|
+
}
|
|
1895
|
+
repositoryCache.set(cacheKey, repo);
|
|
1896
|
+
}
|
|
1897
|
+
return repo;
|
|
1898
|
+
}
|
|
1899
|
+
function clearRepositoryCache() {
|
|
1900
|
+
repositoryCache.clear();
|
|
1901
|
+
}
|
|
1902
|
+
function getRepositoryCacheSize() {
|
|
1903
|
+
return repositoryCache.size;
|
|
1904
|
+
}
|
|
1905
|
+
var repositoryStorage = new AsyncLocalStorage();
|
|
1906
|
+
function getCacheKey2(table, RepositoryClass) {
|
|
1907
|
+
const tableName = table[Symbol.for("drizzle:Name")] || table.name || table.toString();
|
|
1908
|
+
const className = RepositoryClass?.name || "Repository";
|
|
1909
|
+
return `${tableName}:${className}`;
|
|
1910
|
+
}
|
|
1911
|
+
function withRepositoryScope(fn) {
|
|
1912
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1913
|
+
return repositoryStorage.run(cache, fn);
|
|
1914
|
+
}
|
|
1915
|
+
function getScopedRepository(table, RepositoryClass) {
|
|
1916
|
+
const cache = repositoryStorage.getStore();
|
|
1917
|
+
if (!cache) {
|
|
1918
|
+
return RepositoryClass ? new RepositoryClass(table) : new Repository(table);
|
|
1919
|
+
}
|
|
1920
|
+
const key = getCacheKey2(table, RepositoryClass);
|
|
1921
|
+
let repo = cache.get(key);
|
|
1922
|
+
if (!repo) {
|
|
1923
|
+
repo = RepositoryClass ? new RepositoryClass(table) : new Repository(table);
|
|
1924
|
+
cache.set(key, repo);
|
|
1925
|
+
}
|
|
1926
|
+
return repo;
|
|
1927
|
+
}
|
|
1928
|
+
function RepositoryScope() {
|
|
1929
|
+
return async (_c, next) => {
|
|
1930
|
+
return withRepositoryScope(() => next());
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
function getScopedCacheSize() {
|
|
1934
|
+
const cache = repositoryStorage.getStore();
|
|
1935
|
+
return cache?.size ?? 0;
|
|
1936
|
+
}
|
|
1937
|
+
function isInRepositoryScope() {
|
|
1938
|
+
return repositoryStorage.getStore() !== void 0;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// src/db/repository/relation-registry.ts
|
|
1942
|
+
var tableNameCache = /* @__PURE__ */ new WeakMap();
|
|
1943
|
+
function getTableName(table) {
|
|
1944
|
+
const cached = tableNameCache.get(table);
|
|
1945
|
+
if (cached) {
|
|
1946
|
+
return cached;
|
|
1947
|
+
}
|
|
1948
|
+
const name = table[Symbol.for("drizzle:Name")] || table.constructor.name;
|
|
1949
|
+
tableNameCache.set(table, name);
|
|
1950
|
+
return name;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/db/manager/wrapped-db.ts
|
|
1954
|
+
var WrappedDb = class {
|
|
1955
|
+
constructor(db2) {
|
|
1956
|
+
this.db = db2;
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Repository 패턴으로 테이블 접근
|
|
1960
|
+
*
|
|
1961
|
+
* @example
|
|
1962
|
+
* const db = getDb();
|
|
1963
|
+
* const userRepo = db.for(users);
|
|
1964
|
+
* const result = await userRepo.findPage(pageable);
|
|
1965
|
+
*/
|
|
1966
|
+
for(table) {
|
|
1967
|
+
return new Repository(this.db, table);
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Drizzle의 모든 메서드를 프록시
|
|
1971
|
+
*
|
|
1972
|
+
* select, insert, update, delete, transaction 등 모든 Drizzle 메서드 사용 가능
|
|
1973
|
+
*/
|
|
1974
|
+
get select() {
|
|
1975
|
+
return this.db.select.bind(this.db);
|
|
1976
|
+
}
|
|
1977
|
+
get insert() {
|
|
1978
|
+
return this.db.insert.bind(this.db);
|
|
1979
|
+
}
|
|
1980
|
+
get update() {
|
|
1981
|
+
return this.db.update.bind(this.db);
|
|
1982
|
+
}
|
|
1983
|
+
get delete() {
|
|
1984
|
+
return this.db.delete.bind(this.db);
|
|
1985
|
+
}
|
|
1986
|
+
get execute() {
|
|
1987
|
+
return this.db.execute.bind(this.db);
|
|
1988
|
+
}
|
|
1989
|
+
get transaction() {
|
|
1990
|
+
return this.db.transaction.bind(this.db);
|
|
1991
|
+
}
|
|
1992
|
+
get query() {
|
|
1993
|
+
return this.db.query;
|
|
1994
|
+
}
|
|
1995
|
+
get $with() {
|
|
1996
|
+
return this.db.$with.bind(this.db);
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Raw Drizzle DB 접근 (필요시)
|
|
2000
|
+
*/
|
|
2001
|
+
get raw() {
|
|
2002
|
+
return this.db;
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
|
|
2006
|
+
// src/db/manager/context.ts
|
|
2007
|
+
function getDb(type) {
|
|
2008
|
+
const tx = getTransaction();
|
|
2009
|
+
if (tx) {
|
|
2010
|
+
return new WrappedDb(tx);
|
|
2011
|
+
}
|
|
2012
|
+
const rawDb = getDatabase(type);
|
|
2013
|
+
if (!rawDb) {
|
|
2014
|
+
throw new Error(
|
|
2015
|
+
"Database not initialized. Set DATABASE_URL environment variable or call initDatabase() first."
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
return new WrappedDb(rawDb);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// src/db/manager/config-generator.ts
|
|
2022
|
+
function detectDialect(url) {
|
|
2023
|
+
if (url.startsWith("postgres://") || url.startsWith("postgresql://")) {
|
|
2024
|
+
return "postgresql";
|
|
2025
|
+
}
|
|
2026
|
+
if (url.startsWith("mysql://")) {
|
|
2027
|
+
return "mysql";
|
|
2028
|
+
}
|
|
2029
|
+
if (url.startsWith("sqlite://") || url.includes(".db") || url.includes(".sqlite")) {
|
|
2030
|
+
return "sqlite";
|
|
2031
|
+
}
|
|
2032
|
+
throw new Error(
|
|
2033
|
+
`Unsupported database URL format: ${url}. Supported: postgresql://, mysql://, sqlite://`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
function getDrizzleConfig(options = {}) {
|
|
2037
|
+
const databaseUrl = options.databaseUrl ?? process.env.DATABASE_URL;
|
|
2038
|
+
if (!databaseUrl) {
|
|
2039
|
+
throw new Error(
|
|
2040
|
+
"DATABASE_URL is required. Set it in .env or pass it to getDrizzleConfig()"
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
const dialect = options.dialect ?? detectDialect(databaseUrl);
|
|
2044
|
+
const schema = options.schema ?? "./src/server/entities/*.ts";
|
|
2045
|
+
const out = options.out ?? "./drizzle/migrations";
|
|
2046
|
+
return {
|
|
2047
|
+
schema,
|
|
2048
|
+
out,
|
|
2049
|
+
dialect,
|
|
2050
|
+
dbCredentials: getDbCredentials(dialect, databaseUrl)
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
function getDbCredentials(dialect, url) {
|
|
2054
|
+
switch (dialect) {
|
|
2055
|
+
case "postgresql":
|
|
2056
|
+
case "mysql":
|
|
2057
|
+
return { url };
|
|
2058
|
+
case "sqlite":
|
|
2059
|
+
const dbPath = url.replace("sqlite://", "").replace("sqlite:", "");
|
|
2060
|
+
return { url: dbPath };
|
|
2061
|
+
default:
|
|
2062
|
+
throw new Error(`Unsupported dialect: ${dialect}`);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
function generateDrizzleConfigFile(options = {}) {
|
|
2066
|
+
const config2 = getDrizzleConfig(options);
|
|
2067
|
+
return `import { defineConfig } from 'drizzle-kit';
|
|
2068
|
+
|
|
2069
|
+
export default defineConfig({
|
|
2070
|
+
schema: '${config2.schema}',
|
|
2071
|
+
out: '${config2.out}',
|
|
2072
|
+
dialect: '${config2.dialect}',
|
|
2073
|
+
dbCredentials: ${JSON.stringify(config2.dbCredentials, null, 4)},
|
|
2074
|
+
});
|
|
2075
|
+
`;
|
|
2076
|
+
}
|
|
2077
|
+
function id() {
|
|
2078
|
+
return bigserial("id", { mode: "number" }).primaryKey();
|
|
2079
|
+
}
|
|
2080
|
+
function timestamps(options) {
|
|
2081
|
+
const updatedAtColumn = timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull();
|
|
2082
|
+
if (options?.autoUpdate) {
|
|
2083
|
+
updatedAtColumn.__autoUpdate = true;
|
|
2084
|
+
}
|
|
2085
|
+
return {
|
|
2086
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
|
|
2087
|
+
updatedAt: updatedAtColumn
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
function foreignKey(name, reference, options) {
|
|
2091
|
+
return bigserial(`${name}_id`, { mode: "number" }).notNull().references(reference, { onDelete: options?.onDelete ?? "cascade" });
|
|
2092
|
+
}
|
|
2093
|
+
function optionalForeignKey(name, reference, options) {
|
|
2094
|
+
return bigserial(`${name}_id`, { mode: "number" }).references(reference, { onDelete: options?.onDelete ?? "set null" });
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
export { QueryBuilder, Repository, RepositoryScope, Transactional, WrappedDb, checkConnection, clearRepositoryCache, closeDatabase, createDatabaseConnection, createDatabaseFromEnv, db, detectDialect, foreignKey, fromPostgresError, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDb, getDrizzleConfig, getRawDb, getRepository, getRepositoryCacheSize, getScopedCacheSize, getScopedRepository, getTableName, getTransaction, id, initDatabase, isInRepositoryScope, optionalForeignKey, runWithTransaction, setDatabase, timestamps, withRepositoryScope };
|
|
2098
|
+
//# sourceMappingURL=index.js.map
|
|
2099
|
+
//# sourceMappingURL=index.js.map
|