@spfn/core 0.1.0-alpha.8 → 0.1.0-alpha.82
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/README.md +169 -195
- package/dist/auto-loader-JFaZ9gON.d.ts +80 -0
- package/dist/cache/index.d.ts +211 -0
- package/dist/cache/index.js +1013 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/client/index.d.ts +131 -92
- package/dist/client/index.js +93 -85
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/generators/index.d.ts +19 -0
- package/dist/codegen/generators/index.js +1521 -0
- package/dist/codegen/generators/index.js.map +1 -0
- package/dist/codegen/index.d.ts +76 -60
- package/dist/codegen/index.js +1506 -735
- package/dist/codegen/index.js.map +1 -1
- package/dist/database-errors-BNNmLTJE.d.ts +86 -0
- package/dist/db/index.d.ts +844 -44
- package/dist/db/index.js +1281 -1307
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +508 -0
- package/dist/env/index.js +1127 -0
- package/dist/env/index.js.map +1 -0
- package/dist/errors/index.d.ts +136 -0
- package/dist/errors/index.js +172 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index-DHiAqhKv.d.ts +101 -0
- package/dist/index.d.ts +3 -374
- package/dist/index.js +2424 -2178
- package/dist/index.js.map +1 -1
- package/dist/logger/index.d.ts +94 -0
- package/dist/logger/index.js +795 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/middleware/index.d.ts +60 -0
- package/dist/middleware/index.js +918 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/route/index.d.ts +21 -53
- package/dist/route/index.js +1259 -219
- package/dist/route/index.js.map +1 -1
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.js +2419 -2059
- package/dist/server/index.js.map +1 -1
- package/dist/types/index.d.ts +121 -0
- package/dist/types/index.js +38 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types-BXibIEyj.d.ts +60 -0
- package/package.json +67 -17
- package/dist/auto-loader-C44TcLmM.d.ts +0 -125
- package/dist/bind-pssq1NRT.d.ts +0 -34
- package/dist/postgres-errors-CY_Es8EJ.d.ts +0 -1703
- package/dist/scripts/index.d.ts +0 -24
- package/dist/scripts/index.js +0 -1201
- package/dist/scripts/index.js.map +0 -1
- package/dist/scripts/templates/api-index.template.txt +0 -10
- package/dist/scripts/templates/api-tag.template.txt +0 -11
- package/dist/scripts/templates/contract.template.txt +0 -87
- package/dist/scripts/templates/entity-type.template.txt +0 -31
- package/dist/scripts/templates/entity.template.txt +0 -19
- package/dist/scripts/templates/index.template.txt +0 -10
- package/dist/scripts/templates/repository.template.txt +0 -37
- package/dist/scripts/templates/routes-id.template.txt +0 -59
- package/dist/scripts/templates/routes-index.template.txt +0 -44
- package/dist/types-SlzTr8ZO.d.ts +0 -143
package/dist/route/index.js
CHANGED
|
@@ -1,9 +1,932 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync, accessSync, constants, writeFileSync, unlinkSync, createWriteStream, statSync, readdirSync, renameSync } from 'fs';
|
|
3
|
+
import { join, dirname, relative } from 'path';
|
|
1
4
|
import { readdir, stat } from 'fs/promises';
|
|
2
|
-
import { relative, join } from 'path';
|
|
3
5
|
import { Value } from '@sinclair/typebox/value';
|
|
4
6
|
import { Hono } from 'hono';
|
|
7
|
+
import { Type } from '@sinclair/typebox';
|
|
8
|
+
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
11
|
+
var __esm = (fn, res) => function __init() {
|
|
12
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
13
|
+
};
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var PinoAdapter;
|
|
19
|
+
var init_pino = __esm({
|
|
20
|
+
"src/logger/adapters/pino.ts"() {
|
|
21
|
+
PinoAdapter = class _PinoAdapter {
|
|
22
|
+
logger;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
25
|
+
const transport = isDevelopment ? {
|
|
26
|
+
target: "pino-pretty",
|
|
27
|
+
options: {
|
|
28
|
+
colorize: true,
|
|
29
|
+
translateTime: "HH:MM:ss.l",
|
|
30
|
+
ignore: "pid,hostname",
|
|
31
|
+
singleLine: false,
|
|
32
|
+
messageFormat: "{module} {msg}",
|
|
33
|
+
errorLikeObjectKeys: ["err", "error"]
|
|
34
|
+
}
|
|
35
|
+
} : void 0;
|
|
36
|
+
try {
|
|
37
|
+
this.logger = pino({
|
|
38
|
+
level: config.level,
|
|
39
|
+
// 기본 필드
|
|
40
|
+
base: config.module ? { module: config.module } : void 0,
|
|
41
|
+
// Transport (pretty print in development if available)
|
|
42
|
+
transport
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
this.logger = pino({
|
|
46
|
+
level: config.level,
|
|
47
|
+
base: config.module ? { module: config.module } : void 0
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
child(module) {
|
|
52
|
+
const childLogger = new _PinoAdapter({ level: this.logger.level, module });
|
|
53
|
+
childLogger.logger = this.logger.child({ module });
|
|
54
|
+
return childLogger;
|
|
55
|
+
}
|
|
56
|
+
debug(message, context) {
|
|
57
|
+
this.logger.debug(context || {}, message);
|
|
58
|
+
}
|
|
59
|
+
info(message, context) {
|
|
60
|
+
this.logger.info(context || {}, message);
|
|
61
|
+
}
|
|
62
|
+
warn(message, errorOrContext, context) {
|
|
63
|
+
if (errorOrContext instanceof Error) {
|
|
64
|
+
this.logger.warn({ err: errorOrContext, ...context }, message);
|
|
65
|
+
} else {
|
|
66
|
+
this.logger.warn(errorOrContext || {}, message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
error(message, errorOrContext, context) {
|
|
70
|
+
if (errorOrContext instanceof Error) {
|
|
71
|
+
this.logger.error({ err: errorOrContext, ...context }, message);
|
|
72
|
+
} else {
|
|
73
|
+
this.logger.error(errorOrContext || {}, message);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
fatal(message, errorOrContext, context) {
|
|
77
|
+
if (errorOrContext instanceof Error) {
|
|
78
|
+
this.logger.fatal({ err: errorOrContext, ...context }, message);
|
|
79
|
+
} else {
|
|
80
|
+
this.logger.fatal(errorOrContext || {}, message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async close() {
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// src/logger/types.ts
|
|
90
|
+
var LOG_LEVEL_PRIORITY;
|
|
91
|
+
var init_types = __esm({
|
|
92
|
+
"src/logger/types.ts"() {
|
|
93
|
+
LOG_LEVEL_PRIORITY = {
|
|
94
|
+
debug: 0,
|
|
95
|
+
info: 1,
|
|
96
|
+
warn: 2,
|
|
97
|
+
error: 3,
|
|
98
|
+
fatal: 4
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// src/logger/formatters.ts
|
|
104
|
+
function isSensitiveKey(key) {
|
|
105
|
+
const lowerKey = key.toLowerCase();
|
|
106
|
+
return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
|
|
107
|
+
}
|
|
108
|
+
function maskSensitiveData(data) {
|
|
109
|
+
if (data === null || data === void 0) {
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(data)) {
|
|
113
|
+
return data.map((item) => maskSensitiveData(item));
|
|
114
|
+
}
|
|
115
|
+
if (typeof data === "object") {
|
|
116
|
+
const masked = {};
|
|
117
|
+
for (const [key, value] of Object.entries(data)) {
|
|
118
|
+
if (isSensitiveKey(key)) {
|
|
119
|
+
masked[key] = MASKED_VALUE;
|
|
120
|
+
} else if (typeof value === "object" && value !== null) {
|
|
121
|
+
masked[key] = maskSensitiveData(value);
|
|
122
|
+
} else {
|
|
123
|
+
masked[key] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return masked;
|
|
127
|
+
}
|
|
128
|
+
return data;
|
|
129
|
+
}
|
|
130
|
+
function formatTimestamp(date) {
|
|
131
|
+
return date.toISOString();
|
|
132
|
+
}
|
|
133
|
+
function formatTimestampHuman(date) {
|
|
134
|
+
const year = date.getFullYear();
|
|
135
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
136
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
137
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
138
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
139
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
140
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
141
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
|
|
142
|
+
}
|
|
143
|
+
function formatError(error) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push(`${error.name}: ${error.message}`);
|
|
146
|
+
if (error.stack) {
|
|
147
|
+
const stackLines = error.stack.split("\n").slice(1);
|
|
148
|
+
lines.push(...stackLines);
|
|
149
|
+
}
|
|
150
|
+
return lines.join("\n");
|
|
151
|
+
}
|
|
152
|
+
function formatConsole(metadata, colorize = true) {
|
|
153
|
+
const parts = [];
|
|
154
|
+
const timestamp = formatTimestampHuman(metadata.timestamp);
|
|
155
|
+
if (colorize) {
|
|
156
|
+
parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
|
|
157
|
+
} else {
|
|
158
|
+
parts.push(`[${timestamp}]`);
|
|
159
|
+
}
|
|
160
|
+
if (metadata.module) {
|
|
161
|
+
if (colorize) {
|
|
162
|
+
parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
|
|
163
|
+
} else {
|
|
164
|
+
parts.push(`[module=${metadata.module}]`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (metadata.context && Object.keys(metadata.context).length > 0) {
|
|
168
|
+
Object.entries(metadata.context).forEach(([key, value]) => {
|
|
169
|
+
const valueStr = typeof value === "string" ? value : String(value);
|
|
170
|
+
if (colorize) {
|
|
171
|
+
parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
|
|
172
|
+
} else {
|
|
173
|
+
parts.push(`[${key}=${valueStr}]`);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const levelStr = metadata.level.toUpperCase();
|
|
178
|
+
if (colorize) {
|
|
179
|
+
const color = COLORS[metadata.level];
|
|
180
|
+
parts.push(`${color}(${levelStr})${COLORS.reset}:`);
|
|
181
|
+
} else {
|
|
182
|
+
parts.push(`(${levelStr}):`);
|
|
183
|
+
}
|
|
184
|
+
if (colorize) {
|
|
185
|
+
parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
|
|
186
|
+
} else {
|
|
187
|
+
parts.push(metadata.message);
|
|
188
|
+
}
|
|
189
|
+
let output = parts.join(" ");
|
|
190
|
+
if (metadata.error) {
|
|
191
|
+
output += "\n" + formatError(metadata.error);
|
|
192
|
+
}
|
|
193
|
+
return output;
|
|
194
|
+
}
|
|
195
|
+
function formatJSON(metadata) {
|
|
196
|
+
const obj = {
|
|
197
|
+
timestamp: formatTimestamp(metadata.timestamp),
|
|
198
|
+
level: metadata.level,
|
|
199
|
+
message: metadata.message
|
|
200
|
+
};
|
|
201
|
+
if (metadata.module) {
|
|
202
|
+
obj.module = metadata.module;
|
|
203
|
+
}
|
|
204
|
+
if (metadata.context) {
|
|
205
|
+
obj.context = metadata.context;
|
|
206
|
+
}
|
|
207
|
+
if (metadata.error) {
|
|
208
|
+
obj.error = {
|
|
209
|
+
name: metadata.error.name,
|
|
210
|
+
message: metadata.error.message,
|
|
211
|
+
stack: metadata.error.stack
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return JSON.stringify(obj);
|
|
215
|
+
}
|
|
216
|
+
var SENSITIVE_KEYS, MASKED_VALUE, COLORS;
|
|
217
|
+
var init_formatters = __esm({
|
|
218
|
+
"src/logger/formatters.ts"() {
|
|
219
|
+
SENSITIVE_KEYS = [
|
|
220
|
+
"password",
|
|
221
|
+
"passwd",
|
|
222
|
+
"pwd",
|
|
223
|
+
"secret",
|
|
224
|
+
"token",
|
|
225
|
+
"apikey",
|
|
226
|
+
"api_key",
|
|
227
|
+
"accesstoken",
|
|
228
|
+
"access_token",
|
|
229
|
+
"refreshtoken",
|
|
230
|
+
"refresh_token",
|
|
231
|
+
"authorization",
|
|
232
|
+
"auth",
|
|
233
|
+
"cookie",
|
|
234
|
+
"session",
|
|
235
|
+
"sessionid",
|
|
236
|
+
"session_id",
|
|
237
|
+
"privatekey",
|
|
238
|
+
"private_key",
|
|
239
|
+
"creditcard",
|
|
240
|
+
"credit_card",
|
|
241
|
+
"cardnumber",
|
|
242
|
+
"card_number",
|
|
243
|
+
"cvv",
|
|
244
|
+
"ssn",
|
|
245
|
+
"pin"
|
|
246
|
+
];
|
|
247
|
+
MASKED_VALUE = "***MASKED***";
|
|
248
|
+
COLORS = {
|
|
249
|
+
reset: "\x1B[0m",
|
|
250
|
+
bright: "\x1B[1m",
|
|
251
|
+
dim: "\x1B[2m",
|
|
252
|
+
// 로그 레벨 컬러
|
|
253
|
+
debug: "\x1B[36m",
|
|
254
|
+
// cyan
|
|
255
|
+
info: "\x1B[32m",
|
|
256
|
+
// green
|
|
257
|
+
warn: "\x1B[33m",
|
|
258
|
+
// yellow
|
|
259
|
+
error: "\x1B[31m",
|
|
260
|
+
// red
|
|
261
|
+
fatal: "\x1B[35m",
|
|
262
|
+
// magenta
|
|
263
|
+
// 추가 컬러
|
|
264
|
+
gray: "\x1B[90m"
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// src/logger/logger.ts
|
|
270
|
+
var Logger;
|
|
271
|
+
var init_logger = __esm({
|
|
272
|
+
"src/logger/logger.ts"() {
|
|
273
|
+
init_types();
|
|
274
|
+
init_formatters();
|
|
275
|
+
Logger = class _Logger {
|
|
276
|
+
config;
|
|
277
|
+
module;
|
|
278
|
+
constructor(config) {
|
|
279
|
+
this.config = config;
|
|
280
|
+
this.module = config.module;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get current log level
|
|
284
|
+
*/
|
|
285
|
+
get level() {
|
|
286
|
+
return this.config.level;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Create child logger (per module)
|
|
290
|
+
*/
|
|
291
|
+
child(module) {
|
|
292
|
+
return new _Logger({
|
|
293
|
+
...this.config,
|
|
294
|
+
module
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Debug log
|
|
299
|
+
*/
|
|
300
|
+
debug(message, context) {
|
|
301
|
+
this.log("debug", message, void 0, context);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Info log
|
|
305
|
+
*/
|
|
306
|
+
info(message, context) {
|
|
307
|
+
this.log("info", message, void 0, context);
|
|
308
|
+
}
|
|
309
|
+
warn(message, errorOrContext, context) {
|
|
310
|
+
if (errorOrContext instanceof Error) {
|
|
311
|
+
this.log("warn", message, errorOrContext, context);
|
|
312
|
+
} else {
|
|
313
|
+
this.log("warn", message, void 0, errorOrContext);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
error(message, errorOrContext, context) {
|
|
317
|
+
if (errorOrContext instanceof Error) {
|
|
318
|
+
this.log("error", message, errorOrContext, context);
|
|
319
|
+
} else {
|
|
320
|
+
this.log("error", message, void 0, errorOrContext);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
fatal(message, errorOrContext, context) {
|
|
324
|
+
if (errorOrContext instanceof Error) {
|
|
325
|
+
this.log("fatal", message, errorOrContext, context);
|
|
326
|
+
} else {
|
|
327
|
+
this.log("fatal", message, void 0, errorOrContext);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Log processing (internal)
|
|
332
|
+
*/
|
|
333
|
+
log(level, message, error, context) {
|
|
334
|
+
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const metadata = {
|
|
338
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
339
|
+
level,
|
|
340
|
+
message,
|
|
341
|
+
module: this.module,
|
|
342
|
+
error,
|
|
343
|
+
// Mask sensitive information in context to prevent credential leaks
|
|
344
|
+
context: context ? maskSensitiveData(context) : void 0
|
|
345
|
+
};
|
|
346
|
+
this.processTransports(metadata);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Process Transports
|
|
350
|
+
*/
|
|
351
|
+
processTransports(metadata) {
|
|
352
|
+
const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
|
|
353
|
+
Promise.all(promises).catch((error) => {
|
|
354
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
355
|
+
process.stderr.write(`[Logger] Transport error: ${errorMessage}
|
|
356
|
+
`);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Transport log (error-safe)
|
|
361
|
+
*/
|
|
362
|
+
async safeTransportLog(transport, metadata) {
|
|
363
|
+
try {
|
|
364
|
+
await transport.log(metadata);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
367
|
+
process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
|
|
368
|
+
`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Close all Transports
|
|
373
|
+
*/
|
|
374
|
+
async close() {
|
|
375
|
+
const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
|
|
376
|
+
await Promise.all(closePromises);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// src/logger/transports/console.ts
|
|
383
|
+
var ConsoleTransport;
|
|
384
|
+
var init_console = __esm({
|
|
385
|
+
"src/logger/transports/console.ts"() {
|
|
386
|
+
init_types();
|
|
387
|
+
init_formatters();
|
|
388
|
+
ConsoleTransport = class {
|
|
389
|
+
name = "console";
|
|
390
|
+
level;
|
|
391
|
+
enabled;
|
|
392
|
+
colorize;
|
|
393
|
+
constructor(config) {
|
|
394
|
+
this.level = config.level;
|
|
395
|
+
this.enabled = config.enabled;
|
|
396
|
+
this.colorize = config.colorize ?? true;
|
|
397
|
+
}
|
|
398
|
+
async log(metadata) {
|
|
399
|
+
if (!this.enabled) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const message = formatConsole(metadata, this.colorize);
|
|
406
|
+
if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
|
|
407
|
+
console.error(message);
|
|
408
|
+
} else {
|
|
409
|
+
console.log(message);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
var FileTransport;
|
|
416
|
+
var init_file = __esm({
|
|
417
|
+
"src/logger/transports/file.ts"() {
|
|
418
|
+
init_types();
|
|
419
|
+
init_formatters();
|
|
420
|
+
FileTransport = class {
|
|
421
|
+
name = "file";
|
|
422
|
+
level;
|
|
423
|
+
enabled;
|
|
424
|
+
logDir;
|
|
425
|
+
maxFileSize;
|
|
426
|
+
maxFiles;
|
|
427
|
+
currentStream = null;
|
|
428
|
+
currentFilename = null;
|
|
429
|
+
constructor(config) {
|
|
430
|
+
this.level = config.level;
|
|
431
|
+
this.enabled = config.enabled;
|
|
432
|
+
this.logDir = config.logDir;
|
|
433
|
+
this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024;
|
|
434
|
+
this.maxFiles = config.maxFiles ?? 10;
|
|
435
|
+
if (!existsSync(this.logDir)) {
|
|
436
|
+
mkdirSync(this.logDir, { recursive: true });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async log(metadata) {
|
|
440
|
+
if (!this.enabled) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const message = formatJSON(metadata);
|
|
447
|
+
const filename = this.getLogFilename(metadata.timestamp);
|
|
448
|
+
if (this.currentFilename !== filename) {
|
|
449
|
+
await this.rotateStream(filename);
|
|
450
|
+
await this.cleanOldFiles();
|
|
451
|
+
} else if (this.currentFilename) {
|
|
452
|
+
await this.checkAndRotateBySize();
|
|
453
|
+
}
|
|
454
|
+
if (this.currentStream) {
|
|
455
|
+
return new Promise((resolve, reject) => {
|
|
456
|
+
this.currentStream.write(message + "\n", "utf-8", (error) => {
|
|
457
|
+
if (error) {
|
|
458
|
+
process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
|
|
459
|
+
`);
|
|
460
|
+
reject(error);
|
|
461
|
+
} else {
|
|
462
|
+
resolve();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* 스트림 교체 (날짜 변경 시)
|
|
470
|
+
*/
|
|
471
|
+
async rotateStream(filename) {
|
|
472
|
+
if (this.currentStream) {
|
|
473
|
+
await this.closeStream();
|
|
474
|
+
}
|
|
475
|
+
const filepath = join(this.logDir, filename);
|
|
476
|
+
this.currentStream = createWriteStream(filepath, {
|
|
477
|
+
flags: "a",
|
|
478
|
+
// append mode
|
|
479
|
+
encoding: "utf-8"
|
|
480
|
+
});
|
|
481
|
+
this.currentFilename = filename;
|
|
482
|
+
this.currentStream.on("error", (error) => {
|
|
483
|
+
process.stderr.write(`[FileTransport] Stream error: ${error.message}
|
|
484
|
+
`);
|
|
485
|
+
this.currentStream = null;
|
|
486
|
+
this.currentFilename = null;
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* 현재 스트림 닫기
|
|
491
|
+
*/
|
|
492
|
+
async closeStream() {
|
|
493
|
+
if (!this.currentStream) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
return new Promise((resolve, reject) => {
|
|
497
|
+
this.currentStream.end((error) => {
|
|
498
|
+
if (error) {
|
|
499
|
+
reject(error);
|
|
500
|
+
} else {
|
|
501
|
+
this.currentStream = null;
|
|
502
|
+
this.currentFilename = null;
|
|
503
|
+
resolve();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* 파일 크기 체크 및 크기 기반 로테이션
|
|
510
|
+
*/
|
|
511
|
+
async checkAndRotateBySize() {
|
|
512
|
+
if (!this.currentFilename) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const filepath = join(this.logDir, this.currentFilename);
|
|
516
|
+
if (!existsSync(filepath)) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
const stats = statSync(filepath);
|
|
521
|
+
if (stats.size >= this.maxFileSize) {
|
|
522
|
+
await this.rotateBySize();
|
|
523
|
+
}
|
|
524
|
+
} catch (error) {
|
|
525
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
526
|
+
process.stderr.write(`[FileTransport] Failed to check file size: ${errorMessage}
|
|
527
|
+
`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* 크기 기반 로테이션 수행
|
|
532
|
+
* 예: 2025-01-01.log -> 2025-01-01.1.log, 2025-01-01.1.log -> 2025-01-01.2.log
|
|
533
|
+
*/
|
|
534
|
+
async rotateBySize() {
|
|
535
|
+
if (!this.currentFilename) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
await this.closeStream();
|
|
539
|
+
const baseName = this.currentFilename.replace(/\.log$/, "");
|
|
540
|
+
const files = readdirSync(this.logDir);
|
|
541
|
+
const relatedFiles = files.filter((file) => file.startsWith(baseName) && file.endsWith(".log")).sort().reverse();
|
|
542
|
+
for (const file of relatedFiles) {
|
|
543
|
+
const match = file.match(/\.(\d+)\.log$/);
|
|
544
|
+
if (match) {
|
|
545
|
+
const oldNum = parseInt(match[1], 10);
|
|
546
|
+
const newNum = oldNum + 1;
|
|
547
|
+
const oldPath = join(this.logDir, file);
|
|
548
|
+
const newPath2 = join(this.logDir, `${baseName}.${newNum}.log`);
|
|
549
|
+
try {
|
|
550
|
+
renameSync(oldPath, newPath2);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
553
|
+
process.stderr.write(`[FileTransport] Failed to rotate file: ${errorMessage}
|
|
554
|
+
`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const currentPath = join(this.logDir, this.currentFilename);
|
|
559
|
+
const newPath = join(this.logDir, `${baseName}.1.log`);
|
|
560
|
+
try {
|
|
561
|
+
if (existsSync(currentPath)) {
|
|
562
|
+
renameSync(currentPath, newPath);
|
|
563
|
+
}
|
|
564
|
+
} catch (error) {
|
|
565
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
566
|
+
process.stderr.write(`[FileTransport] Failed to rotate current file: ${errorMessage}
|
|
567
|
+
`);
|
|
568
|
+
}
|
|
569
|
+
await this.rotateStream(this.currentFilename);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* 오래된 로그 파일 정리
|
|
573
|
+
* maxFiles 개수를 초과하는 로그 파일 삭제
|
|
574
|
+
*/
|
|
575
|
+
async cleanOldFiles() {
|
|
576
|
+
try {
|
|
577
|
+
if (!existsSync(this.logDir)) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const files = readdirSync(this.logDir);
|
|
581
|
+
const logFiles = files.filter((file) => file.endsWith(".log")).map((file) => {
|
|
582
|
+
const filepath = join(this.logDir, file);
|
|
583
|
+
const stats = statSync(filepath);
|
|
584
|
+
return { file, mtime: stats.mtime };
|
|
585
|
+
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
586
|
+
if (logFiles.length > this.maxFiles) {
|
|
587
|
+
const filesToDelete = logFiles.slice(this.maxFiles);
|
|
588
|
+
for (const { file } of filesToDelete) {
|
|
589
|
+
const filepath = join(this.logDir, file);
|
|
590
|
+
try {
|
|
591
|
+
unlinkSync(filepath);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
594
|
+
process.stderr.write(`[FileTransport] Failed to delete old file "${file}": ${errorMessage}
|
|
595
|
+
`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} catch (error) {
|
|
600
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
601
|
+
process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage}
|
|
602
|
+
`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* 날짜별 로그 파일명 생성
|
|
607
|
+
*/
|
|
608
|
+
getLogFilename(date) {
|
|
609
|
+
const year = date.getFullYear();
|
|
610
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
611
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
612
|
+
return `${year}-${month}-${day}.log`;
|
|
613
|
+
}
|
|
614
|
+
async close() {
|
|
615
|
+
await this.closeStream();
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
function isFileLoggingEnabled() {
|
|
621
|
+
return process.env.LOGGER_FILE_ENABLED === "true";
|
|
622
|
+
}
|
|
623
|
+
function getDefaultLogLevel() {
|
|
624
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
625
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
626
|
+
if (isDevelopment) {
|
|
627
|
+
return "debug";
|
|
628
|
+
}
|
|
629
|
+
if (isProduction) {
|
|
630
|
+
return "info";
|
|
631
|
+
}
|
|
632
|
+
return "warn";
|
|
633
|
+
}
|
|
634
|
+
function getConsoleConfig() {
|
|
635
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
636
|
+
return {
|
|
637
|
+
level: "debug",
|
|
638
|
+
enabled: true,
|
|
639
|
+
colorize: !isProduction
|
|
640
|
+
// Dev: colored output, Production: plain text
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function getFileConfig() {
|
|
644
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
645
|
+
return {
|
|
646
|
+
level: "info",
|
|
647
|
+
enabled: isProduction,
|
|
648
|
+
// File logging in production only
|
|
649
|
+
logDir: process.env.LOG_DIR || "./logs",
|
|
650
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
651
|
+
// 10MB
|
|
652
|
+
maxFiles: 10
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function validateDirectoryWritable(dirPath) {
|
|
656
|
+
if (!existsSync(dirPath)) {
|
|
657
|
+
try {
|
|
658
|
+
mkdirSync(dirPath, { recursive: true });
|
|
659
|
+
} catch (error) {
|
|
660
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
661
|
+
throw new Error(`Failed to create log directory "${dirPath}": ${errorMessage}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
accessSync(dirPath, constants.W_OK);
|
|
666
|
+
} catch {
|
|
667
|
+
throw new Error(`Log directory "${dirPath}" is not writable. Please check permissions.`);
|
|
668
|
+
}
|
|
669
|
+
const testFile = join(dirPath, ".logger-write-test");
|
|
670
|
+
try {
|
|
671
|
+
writeFileSync(testFile, "test", "utf-8");
|
|
672
|
+
unlinkSync(testFile);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
675
|
+
throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function validateFileConfig() {
|
|
679
|
+
if (!isFileLoggingEnabled()) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const logDir = process.env.LOG_DIR;
|
|
683
|
+
if (!logDir) {
|
|
684
|
+
throw new Error(
|
|
685
|
+
"LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp"
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
validateDirectoryWritable(logDir);
|
|
689
|
+
}
|
|
690
|
+
function validateSlackConfig() {
|
|
691
|
+
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
|
|
692
|
+
if (!webhookUrl) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (!webhookUrl.startsWith("https://hooks.slack.com/")) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function validateEmailConfig() {
|
|
702
|
+
const smtpHost = process.env.SMTP_HOST;
|
|
703
|
+
const smtpPort = process.env.SMTP_PORT;
|
|
704
|
+
const emailFrom = process.env.EMAIL_FROM;
|
|
705
|
+
const emailTo = process.env.EMAIL_TO;
|
|
706
|
+
const hasAnyEmailConfig = smtpHost || smtpPort || emailFrom || emailTo;
|
|
707
|
+
if (!hasAnyEmailConfig) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const missingFields = [];
|
|
711
|
+
if (!smtpHost) missingFields.push("SMTP_HOST");
|
|
712
|
+
if (!smtpPort) missingFields.push("SMTP_PORT");
|
|
713
|
+
if (!emailFrom) missingFields.push("EMAIL_FROM");
|
|
714
|
+
if (!emailTo) missingFields.push("EMAIL_TO");
|
|
715
|
+
if (missingFields.length > 0) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const port = parseInt(smtpPort, 10);
|
|
721
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
722
|
+
throw new Error(
|
|
723
|
+
`Invalid SMTP_PORT: "${smtpPort}". Must be a number between 1 and 65535.`
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
727
|
+
if (!emailRegex.test(emailFrom)) {
|
|
728
|
+
throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`);
|
|
729
|
+
}
|
|
730
|
+
const recipients = emailTo.split(",").map((e) => e.trim());
|
|
731
|
+
for (const email of recipients) {
|
|
732
|
+
if (!emailRegex.test(email)) {
|
|
733
|
+
throw new Error(`Invalid email address in EMAIL_TO: "${email}"`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function validateEnvironment() {
|
|
738
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
739
|
+
if (!nodeEnv) {
|
|
740
|
+
process.stderr.write(
|
|
741
|
+
"[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function validateConfig() {
|
|
746
|
+
try {
|
|
747
|
+
validateEnvironment();
|
|
748
|
+
validateFileConfig();
|
|
749
|
+
validateSlackConfig();
|
|
750
|
+
validateEmailConfig();
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (error instanceof Error) {
|
|
753
|
+
throw new Error(`[Logger] Configuration validation failed: ${error.message}`);
|
|
754
|
+
}
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
var init_config = __esm({
|
|
759
|
+
"src/logger/config.ts"() {
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// src/logger/adapters/custom.ts
|
|
764
|
+
function initializeTransports() {
|
|
765
|
+
const transports = [];
|
|
766
|
+
const consoleConfig = getConsoleConfig();
|
|
767
|
+
transports.push(new ConsoleTransport(consoleConfig));
|
|
768
|
+
const fileConfig = getFileConfig();
|
|
769
|
+
if (fileConfig.enabled) {
|
|
770
|
+
transports.push(new FileTransport(fileConfig));
|
|
771
|
+
}
|
|
772
|
+
return transports;
|
|
773
|
+
}
|
|
774
|
+
var CustomAdapter;
|
|
775
|
+
var init_custom = __esm({
|
|
776
|
+
"src/logger/adapters/custom.ts"() {
|
|
777
|
+
init_logger();
|
|
778
|
+
init_console();
|
|
779
|
+
init_file();
|
|
780
|
+
init_config();
|
|
781
|
+
CustomAdapter = class _CustomAdapter {
|
|
782
|
+
logger;
|
|
783
|
+
constructor(config) {
|
|
784
|
+
this.logger = new Logger({
|
|
785
|
+
level: config.level,
|
|
786
|
+
module: config.module,
|
|
787
|
+
transports: initializeTransports()
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
child(module) {
|
|
791
|
+
const adapter = new _CustomAdapter({ level: this.logger.level, module });
|
|
792
|
+
adapter.logger = this.logger.child(module);
|
|
793
|
+
return adapter;
|
|
794
|
+
}
|
|
795
|
+
debug(message, context) {
|
|
796
|
+
this.logger.debug(message, context);
|
|
797
|
+
}
|
|
798
|
+
info(message, context) {
|
|
799
|
+
this.logger.info(message, context);
|
|
800
|
+
}
|
|
801
|
+
warn(message, errorOrContext, context) {
|
|
802
|
+
if (errorOrContext instanceof Error) {
|
|
803
|
+
this.logger.warn(message, errorOrContext, context);
|
|
804
|
+
} else {
|
|
805
|
+
this.logger.warn(message, errorOrContext);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
error(message, errorOrContext, context) {
|
|
809
|
+
if (errorOrContext instanceof Error) {
|
|
810
|
+
this.logger.error(message, errorOrContext, context);
|
|
811
|
+
} else {
|
|
812
|
+
this.logger.error(message, errorOrContext);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
fatal(message, errorOrContext, context) {
|
|
816
|
+
if (errorOrContext instanceof Error) {
|
|
817
|
+
this.logger.fatal(message, errorOrContext, context);
|
|
818
|
+
} else {
|
|
819
|
+
this.logger.fatal(message, errorOrContext);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async close() {
|
|
823
|
+
await this.logger.close();
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// src/logger/adapter-factory.ts
|
|
830
|
+
function createAdapter(type) {
|
|
831
|
+
const level = getDefaultLogLevel();
|
|
832
|
+
switch (type) {
|
|
833
|
+
case "pino":
|
|
834
|
+
return new PinoAdapter({ level });
|
|
835
|
+
case "custom":
|
|
836
|
+
return new CustomAdapter({ level });
|
|
837
|
+
default:
|
|
838
|
+
return new PinoAdapter({ level });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function getAdapterType() {
|
|
842
|
+
const adapterEnv = process.env.LOGGER_ADAPTER;
|
|
843
|
+
if (adapterEnv === "custom" || adapterEnv === "pino") {
|
|
844
|
+
return adapterEnv;
|
|
845
|
+
}
|
|
846
|
+
return "pino";
|
|
847
|
+
}
|
|
848
|
+
function initializeLogger() {
|
|
849
|
+
validateConfig();
|
|
850
|
+
return createAdapter(getAdapterType());
|
|
851
|
+
}
|
|
852
|
+
var logger;
|
|
853
|
+
var init_adapter_factory = __esm({
|
|
854
|
+
"src/logger/adapter-factory.ts"() {
|
|
855
|
+
init_pino();
|
|
856
|
+
init_custom();
|
|
857
|
+
init_config();
|
|
858
|
+
logger = initializeLogger();
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// src/logger/index.ts
|
|
863
|
+
var init_logger2 = __esm({
|
|
864
|
+
"src/logger/index.ts"() {
|
|
865
|
+
init_adapter_factory();
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// src/route/function-routes.ts
|
|
870
|
+
var function_routes_exports = {};
|
|
871
|
+
__export(function_routes_exports, {
|
|
872
|
+
discoverFunctionRoutes: () => discoverFunctionRoutes
|
|
873
|
+
});
|
|
874
|
+
function discoverFunctionRoutes(cwd = process.cwd()) {
|
|
875
|
+
const functions = [];
|
|
876
|
+
const nodeModulesPath = join(cwd, "node_modules");
|
|
877
|
+
try {
|
|
878
|
+
const projectPkgPath = join(cwd, "package.json");
|
|
879
|
+
const projectPkg = JSON.parse(readFileSync(projectPkgPath, "utf-8"));
|
|
880
|
+
const dependencies = {
|
|
881
|
+
...projectPkg.dependencies,
|
|
882
|
+
...projectPkg.devDependencies
|
|
883
|
+
};
|
|
884
|
+
for (const [packageName] of Object.entries(dependencies)) {
|
|
885
|
+
if (!packageName.startsWith("@spfn/") && !packageName.startsWith("spfn-")) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
const pkgPath = join(nodeModulesPath, ...packageName.split("/"), "package.json");
|
|
890
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
891
|
+
if (pkg.spfn?.routes?.dir) {
|
|
892
|
+
const { dir } = pkg.spfn.routes;
|
|
893
|
+
const prefix = pkg.spfn.prefix;
|
|
894
|
+
const packagePath = dirname(pkgPath);
|
|
895
|
+
const routesDir = join(packagePath, dir);
|
|
896
|
+
functions.push({
|
|
897
|
+
packageName,
|
|
898
|
+
routesDir,
|
|
899
|
+
packagePath,
|
|
900
|
+
prefix
|
|
901
|
+
// Include prefix in function info
|
|
902
|
+
});
|
|
903
|
+
routeLogger.debug("Discovered function routes", {
|
|
904
|
+
package: packageName,
|
|
905
|
+
dir,
|
|
906
|
+
prefix: prefix || "(none)"
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
} catch (error) {
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
} catch (error) {
|
|
913
|
+
routeLogger.warn("Failed to discover function routes", {
|
|
914
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
return functions;
|
|
918
|
+
}
|
|
919
|
+
var routeLogger;
|
|
920
|
+
var init_function_routes = __esm({
|
|
921
|
+
"src/route/function-routes.ts"() {
|
|
922
|
+
init_logger2();
|
|
923
|
+
routeLogger = logger.child("function-routes");
|
|
924
|
+
}
|
|
925
|
+
});
|
|
5
926
|
|
|
6
927
|
// src/route/auto-loader.ts
|
|
928
|
+
init_logger2();
|
|
929
|
+
var routeLogger2 = logger.child("route");
|
|
7
930
|
var AutoRouteLoader = class {
|
|
8
931
|
constructor(routesDir, debug = false, middlewares = []) {
|
|
9
932
|
this.routesDir = routesDir;
|
|
@@ -11,36 +934,18 @@ var AutoRouteLoader = class {
|
|
|
11
934
|
this.middlewares = middlewares;
|
|
12
935
|
}
|
|
13
936
|
routes = [];
|
|
14
|
-
registeredRoutes = /* @__PURE__ */ new Map();
|
|
15
|
-
// normalized path → file
|
|
16
937
|
debug;
|
|
17
938
|
middlewares;
|
|
18
|
-
/**
|
|
19
|
-
* Load all routes from directory
|
|
20
|
-
*/
|
|
21
939
|
async load(app) {
|
|
22
940
|
const startTime = Date.now();
|
|
23
941
|
const files = await this.scanFiles(this.routesDir);
|
|
24
942
|
if (files.length === 0) {
|
|
25
|
-
|
|
943
|
+
routeLogger2.warn("No route files found");
|
|
26
944
|
return this.getStats();
|
|
27
945
|
}
|
|
28
|
-
const filesWithPriority = files.map((file) => ({
|
|
29
|
-
path: file,
|
|
30
|
-
priority: this.calculatePriority(relative(this.routesDir, file))
|
|
31
|
-
}));
|
|
32
|
-
filesWithPriority.sort((a, b) => a.priority - b.priority);
|
|
33
|
-
if (this.debug) {
|
|
34
|
-
console.log(`
|
|
35
|
-
\u{1F4CB} Route Registration Order:`);
|
|
36
|
-
console.log(` Priority 1 (Static): ${filesWithPriority.filter((f) => f.priority === 1).length} routes`);
|
|
37
|
-
console.log(` Priority 2 (Dynamic): ${filesWithPriority.filter((f) => f.priority === 2).length} routes`);
|
|
38
|
-
console.log(` Priority 3 (Catch-all): ${filesWithPriority.filter((f) => f.priority === 3).length} routes
|
|
39
|
-
`);
|
|
40
|
-
}
|
|
41
946
|
let failureCount = 0;
|
|
42
|
-
for (const
|
|
43
|
-
const success = await this.loadRoute(app,
|
|
947
|
+
for (const file of files) {
|
|
948
|
+
const success = await this.loadRoute(app, file);
|
|
44
949
|
if (success) ; else {
|
|
45
950
|
failureCount++;
|
|
46
951
|
}
|
|
@@ -51,13 +956,53 @@ var AutoRouteLoader = class {
|
|
|
51
956
|
this.logStats(stats, elapsed);
|
|
52
957
|
}
|
|
53
958
|
if (failureCount > 0) {
|
|
54
|
-
|
|
959
|
+
routeLogger2.warn("Some routes failed to load", { failureCount });
|
|
55
960
|
}
|
|
56
961
|
return stats;
|
|
57
962
|
}
|
|
58
963
|
/**
|
|
59
|
-
*
|
|
964
|
+
* Load routes from an external directory (e.g., from SPFN function packages)
|
|
965
|
+
* Reads package.json spfn.prefix and mounts routes under that prefix
|
|
966
|
+
*
|
|
967
|
+
* @param app - Hono app instance
|
|
968
|
+
* @param routesDir - Directory containing route handlers
|
|
969
|
+
* @param packageName - Name of the package (for logging)
|
|
970
|
+
* @param prefix - Optional prefix to mount routes under (from package.json spfn.prefix)
|
|
971
|
+
* @returns Route statistics
|
|
60
972
|
*/
|
|
973
|
+
async loadExternalRoutes(app, routesDir, packageName, prefix) {
|
|
974
|
+
const startTime = Date.now();
|
|
975
|
+
const tempRoutesDir = this.routesDir;
|
|
976
|
+
this.routesDir = routesDir;
|
|
977
|
+
const files = await this.scanFiles(routesDir);
|
|
978
|
+
if (files.length === 0) {
|
|
979
|
+
routeLogger2.warn("No route files found", { dir: routesDir, package: packageName });
|
|
980
|
+
this.routesDir = tempRoutesDir;
|
|
981
|
+
return this.getStats();
|
|
982
|
+
}
|
|
983
|
+
let successCount = 0;
|
|
984
|
+
let failureCount = 0;
|
|
985
|
+
for (const file of files) {
|
|
986
|
+
const success = await this.loadRoute(app, file, prefix);
|
|
987
|
+
if (success) {
|
|
988
|
+
successCount++;
|
|
989
|
+
} else {
|
|
990
|
+
failureCount++;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const elapsed = Date.now() - startTime;
|
|
994
|
+
if (this.debug) {
|
|
995
|
+
routeLogger2.info("External routes loaded", {
|
|
996
|
+
package: packageName,
|
|
997
|
+
prefix: prefix || "/",
|
|
998
|
+
total: successCount,
|
|
999
|
+
failed: failureCount,
|
|
1000
|
+
elapsed: `${elapsed}ms`
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
this.routesDir = tempRoutesDir;
|
|
1004
|
+
return this.getStats();
|
|
1005
|
+
}
|
|
61
1006
|
getStats() {
|
|
62
1007
|
const stats = {
|
|
63
1008
|
total: this.routes.length,
|
|
@@ -77,12 +1022,6 @@ var AutoRouteLoader = class {
|
|
|
77
1022
|
}
|
|
78
1023
|
return stats;
|
|
79
1024
|
}
|
|
80
|
-
// ========================================================================
|
|
81
|
-
// Private Methods
|
|
82
|
-
// ========================================================================
|
|
83
|
-
/**
|
|
84
|
-
* Recursively scan directory for .ts files
|
|
85
|
-
*/
|
|
86
1025
|
async scanFiles(dir, files = []) {
|
|
87
1026
|
const entries = await readdir(dir);
|
|
88
1027
|
for (const entry of entries) {
|
|
@@ -96,197 +1035,195 @@ var AutoRouteLoader = class {
|
|
|
96
1035
|
}
|
|
97
1036
|
return files;
|
|
98
1037
|
}
|
|
99
|
-
/**
|
|
100
|
-
* Check if file is a valid route file
|
|
101
|
-
*/
|
|
102
1038
|
isValidRouteFile(fileName) {
|
|
103
|
-
return fileName
|
|
1039
|
+
return fileName === "index.ts" || fileName === "index.js" || fileName === "index.mjs";
|
|
104
1040
|
}
|
|
105
|
-
|
|
106
|
-
* Load and register a single route
|
|
107
|
-
* Returns true if successful, false if failed
|
|
108
|
-
*/
|
|
109
|
-
async loadRoute(app, absolutePath) {
|
|
1041
|
+
async loadRoute(app, absolutePath, prefix) {
|
|
110
1042
|
const relativePath = relative(this.routesDir, absolutePath);
|
|
111
1043
|
try {
|
|
112
1044
|
const module = await import(absolutePath);
|
|
113
|
-
if (!module
|
|
114
|
-
console.error(`\u274C ${relativePath}: Must export Hono instance as default`);
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
if (typeof module.default.route !== "function") {
|
|
118
|
-
console.error(`\u274C ${relativePath}: Default export is not a Hono instance`);
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
const urlPath = this.fileToPath(relativePath);
|
|
122
|
-
const priority = this.calculatePriority(relativePath);
|
|
123
|
-
const normalizedPath = this.normalizePath(urlPath);
|
|
124
|
-
const existingFile = this.registeredRoutes.get(normalizedPath);
|
|
125
|
-
if (existingFile) {
|
|
126
|
-
console.warn(`\u26A0\uFE0F Route conflict detected:`);
|
|
127
|
-
console.warn(` Path: ${urlPath} (normalized: ${normalizedPath})`);
|
|
128
|
-
console.warn(` Already registered by: ${existingFile}`);
|
|
129
|
-
console.warn(` Attempted by: ${relativePath}`);
|
|
130
|
-
console.warn(` \u2192 Skipping duplicate registration`);
|
|
1045
|
+
if (!this.validateModule(module, relativePath)) {
|
|
131
1046
|
return false;
|
|
132
1047
|
}
|
|
133
|
-
this.registeredRoutes.set(normalizedPath, relativePath);
|
|
134
1048
|
const hasContractMetas = module.default._contractMetas && module.default._contractMetas.size > 0;
|
|
135
|
-
if (hasContractMetas) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const requestPath = new URL(c.req.url).pathname;
|
|
140
|
-
const relativePath2 = requestPath.startsWith(urlPath) ? requestPath.slice(urlPath.length) || "/" : requestPath;
|
|
141
|
-
const key = `${method} ${relativePath2}`;
|
|
142
|
-
const meta = module.default._contractMetas?.get(key);
|
|
143
|
-
if (meta?.skipMiddlewares) {
|
|
144
|
-
c.set("_skipMiddlewares", meta.skipMiddlewares);
|
|
145
|
-
}
|
|
146
|
-
return next();
|
|
1049
|
+
if (!hasContractMetas) {
|
|
1050
|
+
routeLogger2.error("Route must use contract-based routing", {
|
|
1051
|
+
file: relativePath,
|
|
1052
|
+
hint: "Export contracts using satisfies RouteContract and use app.bind()"
|
|
147
1053
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
const contractPaths = this.extractContractPaths(module);
|
|
1057
|
+
if (prefix) {
|
|
1058
|
+
const invalidPaths = contractPaths.filter((path) => !path.startsWith(prefix));
|
|
1059
|
+
if (invalidPaths.length > 0) {
|
|
1060
|
+
routeLogger2.error("Contract paths must include the package prefix", {
|
|
1061
|
+
file: relativePath,
|
|
1062
|
+
prefix,
|
|
1063
|
+
invalidPaths,
|
|
1064
|
+
hint: `Contract paths should start with "${prefix}". Example: path: "${prefix}/labels"`
|
|
155
1065
|
});
|
|
156
|
-
|
|
157
|
-
} else {
|
|
158
|
-
const skipList = module.meta?.skipMiddlewares || [];
|
|
159
|
-
const activeMiddlewares = this.middlewares.filter((m) => !skipList.includes(m.name));
|
|
160
|
-
for (const middleware of activeMiddlewares) {
|
|
161
|
-
app.use(urlPath, middleware.handler);
|
|
1066
|
+
return false;
|
|
162
1067
|
}
|
|
163
1068
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
1069
|
+
this.registerContractBasedMiddlewares(app, contractPaths, module);
|
|
1070
|
+
app.route("/", module.default);
|
|
1071
|
+
contractPaths.forEach((path) => {
|
|
1072
|
+
this.routes.push({
|
|
1073
|
+
path,
|
|
1074
|
+
// Use contract path as-is (already includes prefix)
|
|
1075
|
+
file: relativePath,
|
|
1076
|
+
meta: module.meta,
|
|
1077
|
+
priority: this.calculateContractPriority(path)
|
|
1078
|
+
});
|
|
1079
|
+
if (this.debug) {
|
|
1080
|
+
const icon = path.includes("*") ? "\u2B50" : path.includes(":") ? "\u{1F538}" : "\u{1F539}";
|
|
1081
|
+
routeLogger2.debug(`Registered route: ${path}`, { icon, file: relativePath });
|
|
1082
|
+
}
|
|
170
1083
|
});
|
|
171
|
-
if (this.debug) {
|
|
172
|
-
const icon = priority === 1 ? "\u{1F539}" : priority === 2 ? "\u{1F538}" : "\u2B50";
|
|
173
|
-
console.log(` ${icon} ${urlPath.padEnd(40)} \u2192 ${relativePath}`);
|
|
174
|
-
}
|
|
175
1084
|
return true;
|
|
176
1085
|
} catch (error) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
stackLines.forEach((line) => console.error(` ${line}`));
|
|
189
|
-
}
|
|
190
|
-
} else if (err.message.includes("Unexpected token")) {
|
|
191
|
-
console.error(`\u274C ${relativePath}: Parse error`);
|
|
192
|
-
console.error(` ${err.message}`);
|
|
193
|
-
console.error(` \u2192 Check for syntax errors or invalid TypeScript`);
|
|
194
|
-
} else {
|
|
195
|
-
console.error(`\u274C ${relativePath}: ${err.message}`);
|
|
196
|
-
if (this.debug && err.stack) {
|
|
197
|
-
console.error(` Stack: ${err.stack}`);
|
|
1086
|
+
this.categorizeAndLogError(error, relativePath);
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
extractContractPaths(module) {
|
|
1091
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1092
|
+
if (module.default._contractMetas) {
|
|
1093
|
+
for (const key of module.default._contractMetas.keys()) {
|
|
1094
|
+
const path = key.split(" ")[1];
|
|
1095
|
+
if (path) {
|
|
1096
|
+
paths.add(path);
|
|
198
1097
|
}
|
|
199
1098
|
}
|
|
200
|
-
return false;
|
|
201
1099
|
}
|
|
1100
|
+
return Array.from(paths);
|
|
202
1101
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
let path = filePath.replace(/\.ts$/, "");
|
|
213
|
-
const segments = path.split("/");
|
|
214
|
-
if (segments[segments.length - 1] === "index") {
|
|
215
|
-
segments.pop();
|
|
1102
|
+
calculateContractPriority(path) {
|
|
1103
|
+
if (path.includes("*")) return 3;
|
|
1104
|
+
if (path.includes(":")) return 2;
|
|
1105
|
+
return 1;
|
|
1106
|
+
}
|
|
1107
|
+
validateModule(module, relativePath) {
|
|
1108
|
+
if (!module.default) {
|
|
1109
|
+
routeLogger2.error("Route must export Hono instance as default", { file: relativePath });
|
|
1110
|
+
return false;
|
|
216
1111
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
1112
|
+
if (typeof module.default.route !== "function") {
|
|
1113
|
+
routeLogger2.error("Default export is not a Hono instance", { file: relativePath });
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
return true;
|
|
1117
|
+
}
|
|
1118
|
+
registerContractBasedMiddlewares(app, contractPaths, module) {
|
|
1119
|
+
app.use("*", (c, next) => {
|
|
1120
|
+
const method = c.req.method;
|
|
1121
|
+
const requestPath = new URL(c.req.url).pathname;
|
|
1122
|
+
const key = `${method} ${requestPath}`;
|
|
1123
|
+
const meta = module.default._contractMetas?.get(key);
|
|
1124
|
+
if (meta?.skipMiddlewares) {
|
|
1125
|
+
c.set("_skipMiddlewares", meta.skipMiddlewares);
|
|
223
1126
|
}
|
|
224
|
-
|
|
225
|
-
|
|
1127
|
+
return next();
|
|
1128
|
+
});
|
|
1129
|
+
for (const contractPath of contractPaths) {
|
|
1130
|
+
const middlewarePath = contractPath === "/" ? "/*" : `${contractPath}/*`;
|
|
1131
|
+
for (const middleware of this.middlewares) {
|
|
1132
|
+
app.use(middlewarePath, async (c, next) => {
|
|
1133
|
+
const skipList = c.get("_skipMiddlewares") || [];
|
|
1134
|
+
if (skipList.includes(middleware.name)) {
|
|
1135
|
+
return next();
|
|
1136
|
+
}
|
|
1137
|
+
return middleware.handler(c, next);
|
|
1138
|
+
});
|
|
226
1139
|
}
|
|
227
|
-
|
|
228
|
-
}).filter((seg) => seg !== null);
|
|
229
|
-
const result = "/" + transformed.join("/");
|
|
230
|
-
return result.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* Calculate route priority
|
|
234
|
-
* 1 = static, 2 = dynamic, 3 = catch-all
|
|
235
|
-
*/
|
|
236
|
-
calculatePriority(path) {
|
|
237
|
-
if (/\[\.\.\.[\w-]+]/.test(path)) return 3;
|
|
238
|
-
if (/\[[\w-]+]/.test(path)) return 2;
|
|
239
|
-
return 1;
|
|
1140
|
+
}
|
|
240
1141
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
1142
|
+
categorizeAndLogError(error, relativePath) {
|
|
1143
|
+
const message = error.message;
|
|
1144
|
+
const stack = error.stack;
|
|
1145
|
+
if (message.includes("Cannot find module") || message.includes("MODULE_NOT_FOUND")) {
|
|
1146
|
+
routeLogger2.error("Missing dependency", {
|
|
1147
|
+
file: relativePath,
|
|
1148
|
+
error: message,
|
|
1149
|
+
hint: "Run: npm install"
|
|
1150
|
+
});
|
|
1151
|
+
} else if (message.includes("SyntaxError") || stack?.includes("SyntaxError")) {
|
|
1152
|
+
routeLogger2.error("Syntax error", {
|
|
1153
|
+
file: relativePath,
|
|
1154
|
+
error: message,
|
|
1155
|
+
...this.debug && stack && {
|
|
1156
|
+
stack: stack.split("\n").slice(0, 5).join("\n")
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
} else if (message.includes("Unexpected token")) {
|
|
1160
|
+
routeLogger2.error("Parse error", {
|
|
1161
|
+
file: relativePath,
|
|
1162
|
+
error: message,
|
|
1163
|
+
hint: "Check for syntax errors or invalid TypeScript"
|
|
1164
|
+
});
|
|
1165
|
+
} else {
|
|
1166
|
+
routeLogger2.error("Route loading failed", {
|
|
1167
|
+
file: relativePath,
|
|
1168
|
+
error: message,
|
|
1169
|
+
...this.debug && stack && { stack }
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
254
1172
|
}
|
|
255
|
-
/**
|
|
256
|
-
* Log statistics
|
|
257
|
-
*/
|
|
258
1173
|
logStats(stats, elapsed) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
\u2705 Routes loaded in ${elapsed}ms
|
|
271
|
-
`);
|
|
1174
|
+
const tagCounts = Object.entries(stats.byTag).map(([tag, count]) => `${tag}(${count})`).join(", ");
|
|
1175
|
+
routeLogger2.info("Routes loaded successfully", {
|
|
1176
|
+
total: stats.total,
|
|
1177
|
+
priority: {
|
|
1178
|
+
static: stats.byPriority.static,
|
|
1179
|
+
dynamic: stats.byPriority.dynamic,
|
|
1180
|
+
catchAll: stats.byPriority.catchAll
|
|
1181
|
+
},
|
|
1182
|
+
...tagCounts && { tags: tagCounts },
|
|
1183
|
+
elapsed: `${elapsed}ms`
|
|
1184
|
+
});
|
|
272
1185
|
}
|
|
273
1186
|
};
|
|
274
1187
|
async function loadRoutes(app, options) {
|
|
275
1188
|
const routesDir = options?.routesDir ?? join(process.cwd(), "src", "server", "routes");
|
|
276
1189
|
const debug = options?.debug ?? false;
|
|
277
1190
|
const middlewares = options?.middlewares ?? [];
|
|
1191
|
+
const includeFunctionRoutes = options?.includeFunctionRoutes ?? true;
|
|
278
1192
|
const loader = new AutoRouteLoader(routesDir, debug, middlewares);
|
|
279
|
-
|
|
1193
|
+
const stats = await loader.load(app);
|
|
1194
|
+
if (includeFunctionRoutes) {
|
|
1195
|
+
const { discoverFunctionRoutes: discoverFunctionRoutes2 } = await Promise.resolve().then(() => (init_function_routes(), function_routes_exports));
|
|
1196
|
+
const functionRoutes = discoverFunctionRoutes2();
|
|
1197
|
+
if (functionRoutes.length > 0) {
|
|
1198
|
+
routeLogger2.info("Loading function routes", { count: functionRoutes.length });
|
|
1199
|
+
for (const func of functionRoutes) {
|
|
1200
|
+
try {
|
|
1201
|
+
await loader.loadExternalRoutes(app, func.routesDir, func.packageName, func.prefix);
|
|
1202
|
+
routeLogger2.info("Function routes loaded", {
|
|
1203
|
+
package: func.packageName,
|
|
1204
|
+
routesDir: func.routesDir,
|
|
1205
|
+
prefix: func.prefix || "/"
|
|
1206
|
+
});
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
routeLogger2.error("Failed to load function routes", {
|
|
1209
|
+
package: func.packageName,
|
|
1210
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return stats;
|
|
280
1217
|
}
|
|
281
1218
|
|
|
282
|
-
// src/errors/
|
|
283
|
-
var
|
|
1219
|
+
// src/errors/http-errors.ts
|
|
1220
|
+
var HttpError = class extends Error {
|
|
284
1221
|
statusCode;
|
|
285
1222
|
details;
|
|
286
1223
|
timestamp;
|
|
287
|
-
constructor(message, statusCode
|
|
1224
|
+
constructor(message, statusCode, details) {
|
|
288
1225
|
super(message);
|
|
289
|
-
this.name = "
|
|
1226
|
+
this.name = "HttpError";
|
|
290
1227
|
this.statusCode = statusCode;
|
|
291
1228
|
this.details = details;
|
|
292
1229
|
this.timestamp = /* @__PURE__ */ new Date();
|
|
@@ -305,13 +1242,7 @@ var DatabaseError = class extends Error {
|
|
|
305
1242
|
};
|
|
306
1243
|
}
|
|
307
1244
|
};
|
|
308
|
-
var
|
|
309
|
-
constructor(message, statusCode = 500, details) {
|
|
310
|
-
super(message, statusCode, details);
|
|
311
|
-
this.name = "QueryError";
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
var ValidationError = class extends QueryError {
|
|
1245
|
+
var ValidationError = class extends HttpError {
|
|
315
1246
|
constructor(message, details) {
|
|
316
1247
|
super(message, 400, details);
|
|
317
1248
|
this.name = "ValidationError";
|
|
@@ -321,8 +1252,9 @@ var ValidationError = class extends QueryError {
|
|
|
321
1252
|
// src/route/bind.ts
|
|
322
1253
|
function bind(contract, handler) {
|
|
323
1254
|
return async (rawContext) => {
|
|
324
|
-
|
|
1255
|
+
let params = rawContext.req.param();
|
|
325
1256
|
if (contract.params) {
|
|
1257
|
+
params = Value.Convert(contract.params, params);
|
|
326
1258
|
const errors = [...Value.Errors(contract.params, params)];
|
|
327
1259
|
if (errors.length > 0) {
|
|
328
1260
|
throw new ValidationError(
|
|
@@ -338,7 +1270,7 @@ function bind(contract, handler) {
|
|
|
338
1270
|
}
|
|
339
1271
|
}
|
|
340
1272
|
const url = new URL(rawContext.req.url);
|
|
341
|
-
|
|
1273
|
+
let query = {};
|
|
342
1274
|
url.searchParams.forEach((v, k) => {
|
|
343
1275
|
const existing = query[k];
|
|
344
1276
|
if (existing) {
|
|
@@ -348,6 +1280,7 @@ function bind(contract, handler) {
|
|
|
348
1280
|
}
|
|
349
1281
|
});
|
|
350
1282
|
if (contract.query) {
|
|
1283
|
+
query = Value.Convert(contract.query, query);
|
|
351
1284
|
const errors = [...Value.Errors(contract.query, query)];
|
|
352
1285
|
if (errors.length > 0) {
|
|
353
1286
|
throw new ValidationError(
|
|
@@ -365,10 +1298,10 @@ function bind(contract, handler) {
|
|
|
365
1298
|
const routeContext = {
|
|
366
1299
|
params,
|
|
367
1300
|
query,
|
|
368
|
-
// data() - validates and returns body
|
|
369
1301
|
data: async () => {
|
|
370
|
-
|
|
1302
|
+
let body = await rawContext.req.json();
|
|
371
1303
|
if (contract.body) {
|
|
1304
|
+
body = Value.Convert(contract.body, body);
|
|
372
1305
|
const errors = [...Value.Errors(contract.body, body)];
|
|
373
1306
|
if (errors.length > 0) {
|
|
374
1307
|
throw new ValidationError(
|
|
@@ -385,58 +1318,165 @@ function bind(contract, handler) {
|
|
|
385
1318
|
}
|
|
386
1319
|
return body;
|
|
387
1320
|
},
|
|
388
|
-
// json() - returns typed response
|
|
389
1321
|
json: (data, status, headers) => {
|
|
1322
|
+
const errorHandlerEnabled = rawContext.get("errorHandlerEnabled");
|
|
1323
|
+
if (errorHandlerEnabled && process.env.NODE_ENV !== "production") {
|
|
1324
|
+
const hasSuccessField = data && typeof data === "object" && "success" in data;
|
|
1325
|
+
if (!hasSuccessField) {
|
|
1326
|
+
console.warn(
|
|
1327
|
+
"[SPFN] Warning: ErrorHandler is enabled but c.json() is being used with non-standard response format.\nConsider using c.success() for consistent API responses, or disable ErrorHandler if you prefer custom formats."
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
390
1331
|
return rawContext.json(data, status, headers);
|
|
391
1332
|
},
|
|
392
|
-
|
|
1333
|
+
success: (data, meta, status = 200) => {
|
|
1334
|
+
const response = {
|
|
1335
|
+
success: true,
|
|
1336
|
+
data
|
|
1337
|
+
};
|
|
1338
|
+
if (meta) {
|
|
1339
|
+
response.meta = meta;
|
|
1340
|
+
}
|
|
1341
|
+
return rawContext.json(response, status);
|
|
1342
|
+
},
|
|
1343
|
+
paginated: (data, page, limit, total) => {
|
|
1344
|
+
const response = {
|
|
1345
|
+
success: true,
|
|
1346
|
+
data,
|
|
1347
|
+
meta: {
|
|
1348
|
+
pagination: {
|
|
1349
|
+
page,
|
|
1350
|
+
limit,
|
|
1351
|
+
total,
|
|
1352
|
+
totalPages: Math.ceil(total / limit)
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
return rawContext.json(response, 200);
|
|
1357
|
+
},
|
|
393
1358
|
raw: rawContext
|
|
394
1359
|
};
|
|
395
1360
|
return handler(routeContext);
|
|
396
1361
|
};
|
|
397
1362
|
}
|
|
1363
|
+
|
|
1364
|
+
// src/middleware/error-handler.ts
|
|
1365
|
+
init_logger2();
|
|
1366
|
+
var errorLogger = logger.child("error-handler");
|
|
1367
|
+
function ErrorHandler(options = {}) {
|
|
1368
|
+
const {
|
|
1369
|
+
includeStack = process.env.NODE_ENV !== "production",
|
|
1370
|
+
enableLogging = true
|
|
1371
|
+
} = options;
|
|
1372
|
+
return (err, c) => {
|
|
1373
|
+
const errorWithCode = err;
|
|
1374
|
+
const statusCode = errorWithCode.statusCode || 500;
|
|
1375
|
+
const errorType = err.name || "Error";
|
|
1376
|
+
if (enableLogging) {
|
|
1377
|
+
const logLevel = statusCode >= 500 ? "error" : "warn";
|
|
1378
|
+
const logData = {
|
|
1379
|
+
type: errorType,
|
|
1380
|
+
message: err.message,
|
|
1381
|
+
statusCode,
|
|
1382
|
+
path: c.req.path,
|
|
1383
|
+
method: c.req.method
|
|
1384
|
+
};
|
|
1385
|
+
if (errorWithCode.details) {
|
|
1386
|
+
logData.details = errorWithCode.details;
|
|
1387
|
+
}
|
|
1388
|
+
if (statusCode >= 500 && includeStack) {
|
|
1389
|
+
logData.stack = err.stack;
|
|
1390
|
+
}
|
|
1391
|
+
errorLogger[logLevel]("Error occurred", logData);
|
|
1392
|
+
}
|
|
1393
|
+
const response = {
|
|
1394
|
+
success: false,
|
|
1395
|
+
error: {
|
|
1396
|
+
message: err.message || "Internal Server Error",
|
|
1397
|
+
type: errorType,
|
|
1398
|
+
statusCode
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
if (errorWithCode.details) {
|
|
1402
|
+
response.error.details = errorWithCode.details;
|
|
1403
|
+
}
|
|
1404
|
+
if (includeStack) {
|
|
1405
|
+
response.error.stack = err.stack;
|
|
1406
|
+
}
|
|
1407
|
+
return c.json(response, statusCode);
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// src/middleware/request-logger.ts
|
|
1412
|
+
init_logger2();
|
|
1413
|
+
|
|
1414
|
+
// src/route/create-app.ts
|
|
398
1415
|
function createApp() {
|
|
399
1416
|
const hono = new Hono();
|
|
400
1417
|
const app = hono;
|
|
401
1418
|
app._contractMetas = /* @__PURE__ */ new Map();
|
|
1419
|
+
app.onError(ErrorHandler());
|
|
1420
|
+
const methodMap = /* @__PURE__ */ new Map([
|
|
1421
|
+
["get", (path, handlers) => hono.get(path, ...handlers)],
|
|
1422
|
+
["post", (path, handlers) => hono.post(path, ...handlers)],
|
|
1423
|
+
["put", (path, handlers) => hono.put(path, ...handlers)],
|
|
1424
|
+
["patch", (path, handlers) => hono.patch(path, ...handlers)],
|
|
1425
|
+
["delete", (path, handlers) => hono.delete(path, ...handlers)]
|
|
1426
|
+
]);
|
|
402
1427
|
app.bind = function(contract, ...args) {
|
|
403
1428
|
const method = contract.method.toLowerCase();
|
|
404
1429
|
const path = contract.path;
|
|
405
1430
|
const [middlewares, handler] = args.length === 1 ? [[], args[0]] : [args[0], args[1]];
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
app._contractMetas.set(key, contract.meta);
|
|
409
|
-
}
|
|
1431
|
+
const key = `${contract.method} ${path}`;
|
|
1432
|
+
app._contractMetas.set(key, contract.meta || {});
|
|
410
1433
|
const boundHandler = bind(contract, handler);
|
|
411
1434
|
const handlers = middlewares.length > 0 ? [...middlewares, boundHandler] : [boundHandler];
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
break;
|
|
416
|
-
case "post":
|
|
417
|
-
hono.post(path, ...handlers);
|
|
418
|
-
break;
|
|
419
|
-
case "put":
|
|
420
|
-
hono.put(path, ...handlers);
|
|
421
|
-
break;
|
|
422
|
-
case "patch":
|
|
423
|
-
hono.patch(path, ...handlers);
|
|
424
|
-
break;
|
|
425
|
-
case "delete":
|
|
426
|
-
hono.delete(path, ...handlers);
|
|
427
|
-
break;
|
|
428
|
-
default:
|
|
429
|
-
throw new Error(`Unsupported HTTP method: ${contract.method}`);
|
|
1435
|
+
const registerMethod = methodMap.get(method);
|
|
1436
|
+
if (!registerMethod) {
|
|
1437
|
+
throw new Error(`Unsupported HTTP method: ${contract.method}`);
|
|
430
1438
|
}
|
|
1439
|
+
registerMethod(path, handlers);
|
|
431
1440
|
};
|
|
432
1441
|
return app;
|
|
433
1442
|
}
|
|
1443
|
+
function ApiSuccessSchema(dataSchema) {
|
|
1444
|
+
return Type.Object({
|
|
1445
|
+
success: Type.Literal(true),
|
|
1446
|
+
data: dataSchema,
|
|
1447
|
+
meta: Type.Optional(Type.Object({
|
|
1448
|
+
timestamp: Type.Optional(Type.String()),
|
|
1449
|
+
requestId: Type.Optional(Type.String()),
|
|
1450
|
+
pagination: Type.Optional(Type.Object({
|
|
1451
|
+
page: Type.Number(),
|
|
1452
|
+
limit: Type.Number(),
|
|
1453
|
+
total: Type.Number(),
|
|
1454
|
+
totalPages: Type.Number()
|
|
1455
|
+
}))
|
|
1456
|
+
}))
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
function ApiErrorSchema() {
|
|
1460
|
+
return Type.Object({
|
|
1461
|
+
success: Type.Literal(false),
|
|
1462
|
+
error: Type.Object({
|
|
1463
|
+
message: Type.String(),
|
|
1464
|
+
type: Type.String(),
|
|
1465
|
+
statusCode: Type.Number(),
|
|
1466
|
+
stack: Type.Optional(Type.String()),
|
|
1467
|
+
details: Type.Optional(Type.Any())
|
|
1468
|
+
})
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
function ApiResponseSchema(dataSchema) {
|
|
1472
|
+
return ApiSuccessSchema(dataSchema);
|
|
1473
|
+
}
|
|
434
1474
|
|
|
435
1475
|
// src/route/types.ts
|
|
436
1476
|
function isHttpMethod(value) {
|
|
437
1477
|
return typeof value === "string" && ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(value);
|
|
438
1478
|
}
|
|
439
1479
|
|
|
440
|
-
export { AutoRouteLoader, bind, createApp, isHttpMethod, loadRoutes };
|
|
1480
|
+
export { ApiErrorSchema, ApiResponseSchema, ApiSuccessSchema, AutoRouteLoader, bind, createApp, isHttpMethod, loadRoutes };
|
|
441
1481
|
//# sourceMappingURL=index.js.map
|
|
442
1482
|
//# sourceMappingURL=index.js.map
|