@spfn/core 0.1.0-alpha.88 → 0.2.0-beta.10
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 +298 -466
- package/dist/boss-DI1r4kTS.d.ts +244 -0
- package/dist/cache/index.d.ts +13 -33
- package/dist/cache/index.js +14 -703
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +214 -17
- package/dist/codegen/index.js +231 -1420
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +1227 -0
- package/dist/config/index.js +273 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/index.d.ts +741 -59
- package/dist/db/index.js +1063 -1226
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +658 -308
- package/dist/env/index.js +503 -928
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +87 -0
- package/dist/env/loader.js +70 -0
- package/dist/env/loader.js.map +1 -0
- package/dist/errors/index.d.ts +417 -29
- package/dist/errors/index.js +359 -98
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +41 -0
- package/dist/event/index.js +131 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/sse/client.d.ts +82 -0
- package/dist/event/sse/client.js +115 -0
- package/dist/event/sse/client.js.map +1 -0
- package/dist/event/sse/index.d.ts +40 -0
- package/dist/event/sse/index.js +92 -0
- package/dist/event/sse/index.js.map +1 -0
- package/dist/job/index.d.ts +218 -0
- package/dist/job/index.js +410 -0
- package/dist/job/index.js.map +1 -0
- package/dist/logger/index.d.ts +20 -79
- package/dist/logger/index.js +82 -387
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +102 -20
- package/dist/middleware/index.js +51 -705
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +120 -0
- package/dist/nextjs/index.js +448 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/{client/nextjs/index.d.ts → nextjs/server.d.ts} +335 -262
- package/dist/nextjs/server.js +637 -0
- package/dist/nextjs/server.js.map +1 -0
- package/dist/route/index.d.ts +879 -25
- package/dist/route/index.js +697 -1271
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +9 -0
- package/dist/route/types.js +3 -0
- package/dist/route/types.js.map +1 -0
- package/dist/router-Di7ENoah.d.ts +151 -0
- package/dist/server/index.d.ts +345 -64
- package/dist/server/index.js +1174 -3233
- package/dist/server/index.js.map +1 -1
- package/dist/types-B-e_f2dQ.d.ts +121 -0
- package/dist/types-BGl4QL1w.d.ts +77 -0
- package/dist/types-BOPTApC2.d.ts +245 -0
- package/docs/cache.md +133 -0
- package/docs/codegen.md +74 -0
- package/docs/database.md +346 -0
- package/docs/entity.md +539 -0
- package/docs/env.md +477 -0
- package/docs/errors.md +319 -0
- package/docs/event.md +116 -0
- package/docs/file-upload.md +717 -0
- package/docs/job.md +131 -0
- package/docs/logger.md +108 -0
- package/docs/middleware.md +337 -0
- package/docs/nextjs.md +241 -0
- package/docs/repository.md +496 -0
- package/docs/route.md +497 -0
- package/docs/server.md +307 -0
- package/package.json +68 -48
- package/dist/auto-loader-JFaZ9gON.d.ts +0 -80
- package/dist/client/index.d.ts +0 -358
- package/dist/client/index.js +0 -357
- package/dist/client/index.js.map +0 -1
- package/dist/client/nextjs/index.js +0 -371
- package/dist/client/nextjs/index.js.map +0 -1
- package/dist/codegen/generators/index.d.ts +0 -19
- package/dist/codegen/generators/index.js +0 -1404
- package/dist/codegen/generators/index.js.map +0 -1
- package/dist/database-errors-BNNmLTJE.d.ts +0 -86
- package/dist/events/index.d.ts +0 -183
- package/dist/events/index.js +0 -77
- package/dist/events/index.js.map +0 -1
- package/dist/index-DHiAqhKv.d.ts +0 -101
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -3674
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -121
- package/dist/types/index.js +0 -38
- package/dist/types/index.js.map +0 -1
- package/dist/types-BXibIEyj.d.ts +0 -60
package/dist/route/index.js
CHANGED
|
@@ -1,1354 +1,780 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { readdir, stat } from 'fs/promises';
|
|
1
|
+
import { logger } from '@spfn/core/logger';
|
|
2
|
+
import { FormatRegistry, Type, Kind } from '@sinclair/typebox';
|
|
4
3
|
import { Value } from '@sinclair/typebox/value';
|
|
5
|
-
import {
|
|
6
|
-
import { Type } from '@sinclair/typebox';
|
|
4
|
+
import { ValidationError } from '@spfn/core/errors';
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
6
|
+
// src/route/route-builder.ts
|
|
7
|
+
var RouteBuilder = class _RouteBuilder {
|
|
8
|
+
_method;
|
|
9
|
+
_path;
|
|
10
|
+
_input;
|
|
11
|
+
_interceptor;
|
|
12
|
+
_middlewares;
|
|
13
|
+
_skipMiddlewares;
|
|
14
|
+
/**
|
|
15
|
+
* Create a new RouteBuilder with copied properties and optional overrides
|
|
16
|
+
*/
|
|
17
|
+
clone(overrides) {
|
|
18
|
+
const builder = new _RouteBuilder();
|
|
19
|
+
builder._method = this._method;
|
|
20
|
+
builder._path = this._path;
|
|
21
|
+
builder._input = overrides?.input ?? this._input;
|
|
22
|
+
builder._interceptor = overrides?.interceptor ?? this._interceptor;
|
|
23
|
+
builder._middlewares = overrides?.middlewares ?? this._middlewares;
|
|
24
|
+
builder._skipMiddlewares = overrides?.skipMiddlewares ?? this._skipMiddlewares;
|
|
25
|
+
return builder;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Define input schemas
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* route.get('/users/:id')
|
|
33
|
+
* .input({
|
|
34
|
+
* params: Type.Object({ id: Type.String() }),
|
|
35
|
+
* query: Type.Object({ page: Type.Number() }),
|
|
36
|
+
* headers: Type.Object({ authorization: Type.String() })
|
|
37
|
+
* })
|
|
38
|
+
* .handler(async (c) => {
|
|
39
|
+
* const { params, query, headers } = await c.data();
|
|
40
|
+
* // params = { id: string }
|
|
41
|
+
* // query = { page: number }
|
|
42
|
+
* // headers = { authorization: string }
|
|
43
|
+
* })
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
input(input) {
|
|
47
|
+
return this.clone({ input });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Define fields injected by interceptors
|
|
51
|
+
*
|
|
52
|
+
* These fields are:
|
|
53
|
+
* - Available in the handler (merged with input)
|
|
54
|
+
* - Excluded from client types (codegen uses only input)
|
|
55
|
+
* - Not validated by route input schema (injected by middleware)
|
|
56
|
+
*
|
|
57
|
+
* Use this when middleware/interceptors add fields to the request
|
|
58
|
+
* before it reaches the handler.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* // Auth interceptor injects crypto key fields
|
|
63
|
+
* route.post('/_auth/login')
|
|
64
|
+
* .input({
|
|
65
|
+
* body: Type.Object({
|
|
66
|
+
* email: Type.String(),
|
|
67
|
+
* password: Type.String()
|
|
68
|
+
* })
|
|
69
|
+
* })
|
|
70
|
+
* .interceptor({
|
|
71
|
+
* body: Type.Object({
|
|
72
|
+
* publicKey: Type.String(),
|
|
73
|
+
* keyId: Type.String(),
|
|
74
|
+
* fingerprint: Type.String()
|
|
75
|
+
* })
|
|
76
|
+
* })
|
|
77
|
+
* .handler(async (c) => {
|
|
78
|
+
* const { body } = await c.data();
|
|
79
|
+
* // body type: { email, password, publicKey, keyId, fingerprint }
|
|
80
|
+
* // Client only sees: { email, password }
|
|
81
|
+
* return loginService(body);
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
interceptor(interceptor) {
|
|
86
|
+
return this.clone({ interceptor });
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Add middlewares to the route
|
|
90
|
+
*
|
|
91
|
+
* Accepts both regular middleware handlers and named middlewares (NamedMiddleware).
|
|
92
|
+
* Named middlewares that are already registered globally will be automatically
|
|
93
|
+
* deduplicated to prevent double execution.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* import { authenticate } from '@spfn/auth/server/middleware';
|
|
98
|
+
*
|
|
99
|
+
* // With NamedMiddleware (auto-deduped if registered globally)
|
|
100
|
+
* route.get('/users')
|
|
101
|
+
* .use([authenticate, RateLimitMiddleware()])
|
|
102
|
+
*
|
|
103
|
+
* // With regular middleware handlers
|
|
104
|
+
* route.get('/users')
|
|
105
|
+
* .use([AuthMiddleware(), RateLimitMiddleware()])
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
middleware(middlewares) {
|
|
109
|
+
return this.clone({ middlewares });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Add middlewares to the route (alias for `.middleware()`)
|
|
113
|
+
*
|
|
114
|
+
* Accepts both regular middleware handlers and named middlewares (NamedMiddleware).
|
|
115
|
+
* Named middlewares that are already registered globally will be automatically
|
|
116
|
+
* deduplicated to prevent double execution.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* import { authenticate } from '@spfn/auth/server/middleware';
|
|
121
|
+
*
|
|
122
|
+
* // With NamedMiddleware (auto-deduped if registered globally)
|
|
123
|
+
* route.get('/users')
|
|
124
|
+
* .use([authenticate, RateLimitMiddleware()])
|
|
125
|
+
*
|
|
126
|
+
* // With regular middleware handlers
|
|
127
|
+
* route.get('/users')
|
|
128
|
+
* .use([AuthMiddleware(), RateLimitMiddleware()])
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
use(middlewares) {
|
|
132
|
+
return this.middleware(middlewares);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Skip server-level named middlewares
|
|
136
|
+
*
|
|
137
|
+
* Useful for public endpoints that should bypass auth or rate limiting
|
|
138
|
+
*
|
|
139
|
+
* @param middlewareNames - Array of middleware names to skip, or '*' to skip all
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* // Skip specific middlewares
|
|
144
|
+
* route.get('/health')
|
|
145
|
+
* .skip(['auth', 'rateLimit'])
|
|
146
|
+
* .handler(async (c) => c.json({ status: 'ok' }));
|
|
147
|
+
*
|
|
148
|
+
* // Skip only auth (still apply rate limiting)
|
|
149
|
+
* route.get('/public-data')
|
|
150
|
+
* .skip(['auth'])
|
|
151
|
+
* .handler(async (c) => { ... });
|
|
152
|
+
*
|
|
153
|
+
* // Skip all middlewares
|
|
154
|
+
* route.get('/public-health')
|
|
155
|
+
* .skip('*')
|
|
156
|
+
* .handler(async (c) => c.json({ status: 'ok' }));
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
skip(middlewareNames) {
|
|
160
|
+
return this.clone({ skipMiddlewares: middlewareNames });
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Define handler function
|
|
164
|
+
*
|
|
165
|
+
* Response type is automatically inferred from the return value.
|
|
166
|
+
* Use helper methods like `c.created()`, `c.paginated()` for proper type inference.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* // Direct return - type inferred from data
|
|
171
|
+
* route.get('/users/:id')
|
|
172
|
+
* .input({ params: Type.Object({ id: Type.String() }) })
|
|
173
|
+
* .handler(async (c) => {
|
|
174
|
+
* const { params } = await c.data();
|
|
175
|
+
* return await getUser(params.id); // Type: User
|
|
176
|
+
* })
|
|
177
|
+
*
|
|
178
|
+
* // Using c.created() - returns data with 201 status, type preserved
|
|
179
|
+
* route.post('/users')
|
|
180
|
+
* .input({ body: Type.Object({ name: Type.String() }) })
|
|
181
|
+
* .handler(async (c) => {
|
|
182
|
+
* const { body } = await c.data();
|
|
183
|
+
* return c.created(await createUser(body)); // Type: User
|
|
184
|
+
* })
|
|
185
|
+
*
|
|
186
|
+
* // Using c.paginated() - returns PaginatedResult<T>
|
|
187
|
+
* route.get('/users')
|
|
188
|
+
* .handler(async (c) => {
|
|
189
|
+
* const users = await getUsers();
|
|
190
|
+
* return c.paginated(users, 1, 20, 100); // Type: PaginatedResult<User>
|
|
191
|
+
* })
|
|
192
|
+
*
|
|
193
|
+
* // Using c.noContent() - returns void
|
|
194
|
+
* route.delete('/users/:id')
|
|
195
|
+
* .handler(async (c) => {
|
|
196
|
+
* await deleteUser(params.id);
|
|
197
|
+
* return c.noContent(); // Type: void
|
|
198
|
+
* })
|
|
199
|
+
*
|
|
200
|
+
* // Using c.json() - returns Response (type inference lost)
|
|
201
|
+
* // Use only when you need custom status codes not covered by helpers
|
|
202
|
+
* route.get('/custom')
|
|
203
|
+
* .handler(async (c) => {
|
|
204
|
+
* return c.json({ data }, 418); // Type: Response
|
|
205
|
+
* })
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
handler(fn) {
|
|
209
|
+
return {
|
|
210
|
+
method: this._method,
|
|
211
|
+
path: this._path,
|
|
212
|
+
input: this._input,
|
|
213
|
+
interceptor: this._interceptor,
|
|
214
|
+
middlewares: this._middlewares,
|
|
215
|
+
skipMiddlewares: this._skipMiddlewares,
|
|
216
|
+
handler: fn,
|
|
217
|
+
_input: {},
|
|
218
|
+
_interceptor: {},
|
|
219
|
+
_response: {}
|
|
28
220
|
};
|
|
29
221
|
}
|
|
30
|
-
}
|
|
222
|
+
};
|
|
223
|
+
function createMethodRoute(method) {
|
|
224
|
+
return (path) => {
|
|
225
|
+
const builder = new RouteBuilder();
|
|
226
|
+
builder._method = method;
|
|
227
|
+
builder._path = path;
|
|
228
|
+
return builder;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
var route = {
|
|
232
|
+
get: createMethodRoute("GET"),
|
|
233
|
+
post: createMethodRoute("POST"),
|
|
234
|
+
put: createMethodRoute("PUT"),
|
|
235
|
+
patch: createMethodRoute("PATCH"),
|
|
236
|
+
delete: createMethodRoute("DELETE")
|
|
237
|
+
};
|
|
31
238
|
|
|
32
|
-
// src/
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
239
|
+
// src/route/router.ts
|
|
240
|
+
function createRouterInstance(routes, packageRouters = [], globalMiddlewares = []) {
|
|
241
|
+
return {
|
|
242
|
+
routes,
|
|
243
|
+
_routes: routes,
|
|
244
|
+
_packageRouters: packageRouters,
|
|
245
|
+
_globalMiddlewares: globalMiddlewares,
|
|
246
|
+
packages(routers) {
|
|
247
|
+
const newPackageRouters = [...this._packageRouters, ...routers];
|
|
248
|
+
for (const pkgRouter of routers) {
|
|
249
|
+
if (pkgRouter._packageRouters?.length > 0) {
|
|
250
|
+
newPackageRouters.push(...pkgRouter._packageRouters);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return createRouterInstance(this.routes, newPackageRouters, this._globalMiddlewares);
|
|
254
|
+
},
|
|
255
|
+
use(middlewares) {
|
|
256
|
+
return createRouterInstance(this.routes, this._packageRouters, [...this._globalMiddlewares, ...middlewares]);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function defineRouter(routes) {
|
|
261
|
+
return createRouterInstance(routes);
|
|
262
|
+
}
|
|
263
|
+
function FileSchema(options) {
|
|
264
|
+
return Type.Unsafe({
|
|
265
|
+
[Kind]: "File",
|
|
266
|
+
type: "object",
|
|
267
|
+
fileOptions: options
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function FileArraySchema(options) {
|
|
271
|
+
return Type.Unsafe({
|
|
272
|
+
[Kind]: "FileArray",
|
|
273
|
+
type: "array",
|
|
274
|
+
items: { [Kind]: "File", type: "object" },
|
|
275
|
+
fileOptions: options
|
|
276
|
+
});
|
|
36
277
|
}
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
278
|
+
function OptionalFileSchema(options) {
|
|
279
|
+
return Type.Optional(FileSchema(options));
|
|
280
|
+
}
|
|
281
|
+
function isFileSchema(schema) {
|
|
282
|
+
const kind = schema[Symbol.for("TypeBox.Kind")];
|
|
283
|
+
return kind === "File";
|
|
284
|
+
}
|
|
285
|
+
function isFileArraySchema(schema) {
|
|
286
|
+
const kind = schema[Symbol.for("TypeBox.Kind")];
|
|
287
|
+
return kind === "FileArray";
|
|
288
|
+
}
|
|
289
|
+
function getFileOptions(schema) {
|
|
290
|
+
return schema.fileOptions;
|
|
291
|
+
}
|
|
292
|
+
function formatFileSize(bytes) {
|
|
293
|
+
if (bytes >= 1024 * 1024 * 1024) {
|
|
294
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
|
|
40
295
|
}
|
|
41
|
-
if (
|
|
42
|
-
return
|
|
296
|
+
if (bytes >= 1024 * 1024) {
|
|
297
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
43
298
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
for (const [key, value] of Object.entries(data)) {
|
|
47
|
-
if (isSensitiveKey(key)) {
|
|
48
|
-
masked[key] = MASKED_VALUE;
|
|
49
|
-
} else if (typeof value === "object" && value !== null) {
|
|
50
|
-
masked[key] = maskSensitiveData(value);
|
|
51
|
-
} else {
|
|
52
|
-
masked[key] = value;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return masked;
|
|
299
|
+
if (bytes >= 1024) {
|
|
300
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
56
301
|
}
|
|
57
|
-
return
|
|
302
|
+
return `${bytes}B`;
|
|
58
303
|
}
|
|
59
|
-
|
|
60
|
-
|
|
304
|
+
|
|
305
|
+
// src/route/validation.ts
|
|
306
|
+
FormatRegistry.Set(
|
|
307
|
+
"email",
|
|
308
|
+
(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
309
|
+
);
|
|
310
|
+
FormatRegistry.Set(
|
|
311
|
+
"uri",
|
|
312
|
+
(value) => /^https?:\/\/.+/.test(value)
|
|
313
|
+
);
|
|
314
|
+
FormatRegistry.Set(
|
|
315
|
+
"uuid",
|
|
316
|
+
(value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
|
|
317
|
+
);
|
|
318
|
+
FormatRegistry.Set(
|
|
319
|
+
"date",
|
|
320
|
+
(value) => /^\d{4}-\d{2}-\d{2}$/.test(value)
|
|
321
|
+
);
|
|
322
|
+
FormatRegistry.Set(
|
|
323
|
+
"date-time",
|
|
324
|
+
(value) => !isNaN(Date.parse(value))
|
|
325
|
+
);
|
|
326
|
+
function isFile(value) {
|
|
327
|
+
return value instanceof File || typeof value === "object" && value !== null && "name" in value && "size" in value && "type" in value && typeof value.arrayBuffer === "function";
|
|
61
328
|
}
|
|
62
|
-
function
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
66
|
-
const hours = String(date.getHours()).padStart(2, "0");
|
|
67
|
-
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
68
|
-
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
69
|
-
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
70
|
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
|
|
329
|
+
function isFileSchemaDef(schema) {
|
|
330
|
+
const kind = schema[Symbol.for("TypeBox.Kind")];
|
|
331
|
+
return kind === "File";
|
|
71
332
|
}
|
|
72
|
-
function
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
if (error.stack) {
|
|
76
|
-
const stackLines = error.stack.split("\n").slice(1);
|
|
77
|
-
lines.push(...stackLines);
|
|
78
|
-
}
|
|
79
|
-
return lines.join("\n");
|
|
333
|
+
function isFileArraySchemaDef(schema) {
|
|
334
|
+
const kind = schema[Symbol.for("TypeBox.Kind")];
|
|
335
|
+
return kind === "FileArray";
|
|
80
336
|
}
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
} else {
|
|
93
|
-
parts.push(`[module=${metadata.module}]`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (metadata.context && Object.keys(metadata.context).length > 0) {
|
|
97
|
-
Object.entries(metadata.context).forEach(([key, value]) => {
|
|
98
|
-
let valueStr;
|
|
99
|
-
if (typeof value === "string") {
|
|
100
|
-
valueStr = value;
|
|
101
|
-
} else if (typeof value === "object" && value !== null) {
|
|
102
|
-
try {
|
|
103
|
-
valueStr = JSON.stringify(value);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
valueStr = "[circular]";
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
valueStr = String(value);
|
|
109
|
-
}
|
|
110
|
-
if (colorize) {
|
|
111
|
-
parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
|
|
112
|
-
} else {
|
|
113
|
-
parts.push(`[${key}=${valueStr}]`);
|
|
114
|
-
}
|
|
337
|
+
function getSchemaFileOptions(schema) {
|
|
338
|
+
return schema.fileOptions;
|
|
339
|
+
}
|
|
340
|
+
function validateSingleFile(file, fieldPath, options, errors) {
|
|
341
|
+
if (!options) return;
|
|
342
|
+
const { maxSize, minSize, allowedTypes } = options;
|
|
343
|
+
if (maxSize !== void 0 && file.size > maxSize) {
|
|
344
|
+
errors.push({
|
|
345
|
+
path: fieldPath,
|
|
346
|
+
message: `File size ${formatFileSize(file.size)} exceeds maximum ${formatFileSize(maxSize)}`,
|
|
347
|
+
value: file.size
|
|
115
348
|
});
|
|
116
349
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
if (colorize) {
|
|
125
|
-
parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
|
|
126
|
-
} else {
|
|
127
|
-
parts.push(metadata.message);
|
|
350
|
+
if (minSize !== void 0 && file.size < minSize) {
|
|
351
|
+
errors.push({
|
|
352
|
+
path: fieldPath,
|
|
353
|
+
message: `File size ${formatFileSize(file.size)} is below minimum ${formatFileSize(minSize)}`,
|
|
354
|
+
value: file.size
|
|
355
|
+
});
|
|
128
356
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
357
|
+
if (allowedTypes && allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
|
|
358
|
+
errors.push({
|
|
359
|
+
path: fieldPath,
|
|
360
|
+
message: `File type "${file.type}" is not allowed. Allowed: ${allowedTypes.join(", ")}`,
|
|
361
|
+
value: file.type
|
|
362
|
+
});
|
|
132
363
|
}
|
|
133
|
-
return output;
|
|
134
364
|
}
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
message: metadata.error.message,
|
|
151
|
-
stack: metadata.error.stack
|
|
152
|
-
};
|
|
365
|
+
function validateField(schema, rawValue, fieldName) {
|
|
366
|
+
if (!schema) {
|
|
367
|
+
return {};
|
|
368
|
+
}
|
|
369
|
+
const converted = Value.Convert(schema, rawValue);
|
|
370
|
+
const errors = [...Value.Errors(schema, converted)];
|
|
371
|
+
if (errors.length > 0) {
|
|
372
|
+
throw new ValidationError({
|
|
373
|
+
message: `Invalid ${fieldName}`,
|
|
374
|
+
fields: errors.map((e) => ({
|
|
375
|
+
path: e.path,
|
|
376
|
+
message: e.message,
|
|
377
|
+
value: e.value
|
|
378
|
+
}))
|
|
379
|
+
});
|
|
153
380
|
}
|
|
154
|
-
return
|
|
381
|
+
return converted;
|
|
155
382
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
];
|
|
187
|
-
MASKED_VALUE = "***MASKED***";
|
|
188
|
-
COLORS = {
|
|
189
|
-
reset: "\x1B[0m",
|
|
190
|
-
bright: "\x1B[1m",
|
|
191
|
-
dim: "\x1B[2m",
|
|
192
|
-
// 로그 레벨 컬러
|
|
193
|
-
debug: "\x1B[36m",
|
|
194
|
-
// cyan
|
|
195
|
-
info: "\x1B[32m",
|
|
196
|
-
// green
|
|
197
|
-
warn: "\x1B[33m",
|
|
198
|
-
// yellow
|
|
199
|
-
error: "\x1B[31m",
|
|
200
|
-
// red
|
|
201
|
-
fatal: "\x1B[35m",
|
|
202
|
-
// magenta
|
|
203
|
-
// 추가 컬러
|
|
204
|
-
gray: "\x1B[90m"
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// src/logger/logger.ts
|
|
210
|
-
var Logger;
|
|
211
|
-
var init_logger = __esm({
|
|
212
|
-
"src/logger/logger.ts"() {
|
|
213
|
-
init_types();
|
|
214
|
-
init_formatters();
|
|
215
|
-
Logger = class _Logger {
|
|
216
|
-
config;
|
|
217
|
-
module;
|
|
218
|
-
constructor(config) {
|
|
219
|
-
this.config = config;
|
|
220
|
-
this.module = config.module;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Get current log level
|
|
224
|
-
*/
|
|
225
|
-
get level() {
|
|
226
|
-
return this.config.level;
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Create child logger (per module)
|
|
230
|
-
*/
|
|
231
|
-
child(module) {
|
|
232
|
-
return new _Logger({
|
|
233
|
-
...this.config,
|
|
234
|
-
module
|
|
383
|
+
function validateFormData(schema, rawValue, fieldName) {
|
|
384
|
+
if (!schema) {
|
|
385
|
+
return {};
|
|
386
|
+
}
|
|
387
|
+
const schemaProps = schema.properties;
|
|
388
|
+
if (!schemaProps) {
|
|
389
|
+
return rawValue;
|
|
390
|
+
}
|
|
391
|
+
const result = {};
|
|
392
|
+
const nonFileData = {};
|
|
393
|
+
const nonFileSchema = {};
|
|
394
|
+
const fileErrors = [];
|
|
395
|
+
for (const [key, value] of Object.entries(rawValue)) {
|
|
396
|
+
const propSchema = schemaProps[key];
|
|
397
|
+
if (propSchema && isFileSchemaDef(propSchema)) {
|
|
398
|
+
result[key] = value;
|
|
399
|
+
if (isFile(value)) {
|
|
400
|
+
const fileOptions = getSchemaFileOptions(propSchema);
|
|
401
|
+
validateSingleFile(value, `/${key}`, fileOptions, fileErrors);
|
|
402
|
+
}
|
|
403
|
+
} else if (propSchema && isFileArraySchemaDef(propSchema)) {
|
|
404
|
+
result[key] = value;
|
|
405
|
+
const fileOptions = getSchemaFileOptions(propSchema);
|
|
406
|
+
const files = Array.isArray(value) ? value : [value];
|
|
407
|
+
const fileArray = files.filter(isFile);
|
|
408
|
+
if (fileOptions?.maxFiles !== void 0 && fileArray.length > fileOptions.maxFiles) {
|
|
409
|
+
fileErrors.push({
|
|
410
|
+
path: `/${key}`,
|
|
411
|
+
message: `Too many files. Maximum: ${fileOptions.maxFiles}, received: ${fileArray.length}`,
|
|
412
|
+
value: fileArray.length
|
|
235
413
|
});
|
|
236
414
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Info log
|
|
245
|
-
*/
|
|
246
|
-
info(message, context) {
|
|
247
|
-
this.log("info", message, void 0, context);
|
|
248
|
-
}
|
|
249
|
-
warn(message, errorOrContext, context) {
|
|
250
|
-
if (errorOrContext instanceof Error) {
|
|
251
|
-
this.log("warn", message, errorOrContext, context);
|
|
252
|
-
} else {
|
|
253
|
-
this.log("warn", message, void 0, errorOrContext);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
error(message, errorOrContext, context) {
|
|
257
|
-
if (errorOrContext instanceof Error) {
|
|
258
|
-
this.log("error", message, errorOrContext, context);
|
|
259
|
-
} else {
|
|
260
|
-
this.log("error", message, void 0, errorOrContext);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
fatal(message, errorOrContext, context) {
|
|
264
|
-
if (errorOrContext instanceof Error) {
|
|
265
|
-
this.log("fatal", message, errorOrContext, context);
|
|
266
|
-
} else {
|
|
267
|
-
this.log("fatal", message, void 0, errorOrContext);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Log processing (internal)
|
|
272
|
-
*/
|
|
273
|
-
log(level, message, error, context) {
|
|
274
|
-
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
const metadata = {
|
|
278
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
279
|
-
level,
|
|
280
|
-
message,
|
|
281
|
-
module: this.module,
|
|
282
|
-
error,
|
|
283
|
-
// Mask sensitive information in context to prevent credential leaks
|
|
284
|
-
context: context ? maskSensitiveData(context) : void 0
|
|
285
|
-
};
|
|
286
|
-
this.processTransports(metadata);
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Process Transports
|
|
290
|
-
*/
|
|
291
|
-
processTransports(metadata) {
|
|
292
|
-
const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
|
|
293
|
-
Promise.all(promises).catch((error) => {
|
|
294
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
295
|
-
process.stderr.write(`[Logger] Transport error: ${errorMessage}
|
|
296
|
-
`);
|
|
415
|
+
if (fileOptions?.minFiles !== void 0 && fileArray.length < fileOptions.minFiles) {
|
|
416
|
+
fileErrors.push({
|
|
417
|
+
path: `/${key}`,
|
|
418
|
+
message: `Too few files. Minimum: ${fileOptions.minFiles}, received: ${fileArray.length}`,
|
|
419
|
+
value: fileArray.length
|
|
297
420
|
});
|
|
298
421
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
`);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Close all Transports
|
|
313
|
-
*/
|
|
314
|
-
async close() {
|
|
315
|
-
const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
|
|
316
|
-
await Promise.all(closePromises);
|
|
422
|
+
fileArray.forEach((file, index) => {
|
|
423
|
+
validateSingleFile(file, `/${key}/${index}`, fileOptions, fileErrors);
|
|
424
|
+
});
|
|
425
|
+
} else if (isFile(value) || Array.isArray(value) && value.some(isFile)) {
|
|
426
|
+
result[key] = value;
|
|
427
|
+
} else {
|
|
428
|
+
nonFileData[key] = value;
|
|
429
|
+
if (propSchema) {
|
|
430
|
+
nonFileSchema[key] = propSchema;
|
|
317
431
|
}
|
|
318
|
-
}
|
|
432
|
+
}
|
|
319
433
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
"src/logger/transports/console.ts"() {
|
|
326
|
-
init_types();
|
|
327
|
-
init_formatters();
|
|
328
|
-
ConsoleTransport = class {
|
|
329
|
-
name = "console";
|
|
330
|
-
level;
|
|
331
|
-
enabled;
|
|
332
|
-
colorize;
|
|
333
|
-
constructor(config) {
|
|
334
|
-
this.level = config.level;
|
|
335
|
-
this.enabled = config.enabled;
|
|
336
|
-
this.colorize = config.colorize ?? true;
|
|
337
|
-
}
|
|
338
|
-
async log(metadata) {
|
|
339
|
-
if (!this.enabled) {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const message = formatConsole(metadata, this.colorize);
|
|
346
|
-
if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
|
|
347
|
-
console.error(message);
|
|
348
|
-
} else {
|
|
349
|
-
console.log(message);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
};
|
|
434
|
+
if (fileErrors.length > 0) {
|
|
435
|
+
throw new ValidationError({
|
|
436
|
+
message: `Invalid ${fieldName}`,
|
|
437
|
+
fields: fileErrors
|
|
438
|
+
});
|
|
353
439
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
init_formatters();
|
|
360
|
-
FileTransport = class {
|
|
361
|
-
name = "file";
|
|
362
|
-
level;
|
|
363
|
-
enabled;
|
|
364
|
-
logDir;
|
|
365
|
-
maxFileSize;
|
|
366
|
-
maxFiles;
|
|
367
|
-
currentStream = null;
|
|
368
|
-
currentFilename = null;
|
|
369
|
-
constructor(config) {
|
|
370
|
-
this.level = config.level;
|
|
371
|
-
this.enabled = config.enabled;
|
|
372
|
-
this.logDir = config.logDir;
|
|
373
|
-
this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024;
|
|
374
|
-
this.maxFiles = config.maxFiles ?? 10;
|
|
375
|
-
if (!existsSync(this.logDir)) {
|
|
376
|
-
mkdirSync(this.logDir, { recursive: true });
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
async log(metadata) {
|
|
380
|
-
if (!this.enabled) {
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
const message = formatJSON(metadata);
|
|
387
|
-
const filename = this.getLogFilename(metadata.timestamp);
|
|
388
|
-
if (this.currentFilename !== filename) {
|
|
389
|
-
await this.rotateStream(filename);
|
|
390
|
-
await this.cleanOldFiles();
|
|
391
|
-
} else if (this.currentFilename) {
|
|
392
|
-
await this.checkAndRotateBySize();
|
|
393
|
-
}
|
|
394
|
-
if (this.currentStream) {
|
|
395
|
-
return new Promise((resolve, reject) => {
|
|
396
|
-
this.currentStream.write(message + "\n", "utf-8", (error) => {
|
|
397
|
-
if (error) {
|
|
398
|
-
process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
|
|
399
|
-
`);
|
|
400
|
-
reject(error);
|
|
401
|
-
} else {
|
|
402
|
-
resolve();
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* 스트림 교체 (날짜 변경 시)
|
|
410
|
-
*/
|
|
411
|
-
async rotateStream(filename) {
|
|
412
|
-
if (this.currentStream) {
|
|
413
|
-
await this.closeStream();
|
|
414
|
-
}
|
|
415
|
-
const filepath = join(this.logDir, filename);
|
|
416
|
-
this.currentStream = createWriteStream(filepath, {
|
|
417
|
-
flags: "a",
|
|
418
|
-
// append mode
|
|
419
|
-
encoding: "utf-8"
|
|
420
|
-
});
|
|
421
|
-
this.currentFilename = filename;
|
|
422
|
-
this.currentStream.on("error", (error) => {
|
|
423
|
-
process.stderr.write(`[FileTransport] Stream error: ${error.message}
|
|
424
|
-
`);
|
|
425
|
-
this.currentStream = null;
|
|
426
|
-
this.currentFilename = null;
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* 현재 스트림 닫기
|
|
431
|
-
*/
|
|
432
|
-
async closeStream() {
|
|
433
|
-
if (!this.currentStream) {
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
return new Promise((resolve, reject) => {
|
|
437
|
-
this.currentStream.end((error) => {
|
|
438
|
-
if (error) {
|
|
439
|
-
reject(error);
|
|
440
|
-
} else {
|
|
441
|
-
this.currentStream = null;
|
|
442
|
-
this.currentFilename = null;
|
|
443
|
-
resolve();
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* 파일 크기 체크 및 크기 기반 로테이션
|
|
450
|
-
*/
|
|
451
|
-
async checkAndRotateBySize() {
|
|
452
|
-
if (!this.currentFilename) {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
const filepath = join(this.logDir, this.currentFilename);
|
|
456
|
-
if (!existsSync(filepath)) {
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
try {
|
|
460
|
-
const stats = statSync(filepath);
|
|
461
|
-
if (stats.size >= this.maxFileSize) {
|
|
462
|
-
await this.rotateBySize();
|
|
463
|
-
}
|
|
464
|
-
} catch (error) {
|
|
465
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
466
|
-
process.stderr.write(`[FileTransport] Failed to check file size: ${errorMessage}
|
|
467
|
-
`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* 크기 기반 로테이션 수행
|
|
472
|
-
* 예: 2025-01-01.log -> 2025-01-01.1.log, 2025-01-01.1.log -> 2025-01-01.2.log
|
|
473
|
-
*/
|
|
474
|
-
async rotateBySize() {
|
|
475
|
-
if (!this.currentFilename) {
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
await this.closeStream();
|
|
479
|
-
const baseName = this.currentFilename.replace(/\.log$/, "");
|
|
480
|
-
const files = readdirSync(this.logDir);
|
|
481
|
-
const relatedFiles = files.filter((file) => file.startsWith(baseName) && file.endsWith(".log")).sort().reverse();
|
|
482
|
-
for (const file of relatedFiles) {
|
|
483
|
-
const match = file.match(/\.(\d+)\.log$/);
|
|
484
|
-
if (match) {
|
|
485
|
-
const oldNum = parseInt(match[1], 10);
|
|
486
|
-
const newNum = oldNum + 1;
|
|
487
|
-
const oldPath = join(this.logDir, file);
|
|
488
|
-
const newPath2 = join(this.logDir, `${baseName}.${newNum}.log`);
|
|
489
|
-
try {
|
|
490
|
-
renameSync(oldPath, newPath2);
|
|
491
|
-
} catch (error) {
|
|
492
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
493
|
-
process.stderr.write(`[FileTransport] Failed to rotate file: ${errorMessage}
|
|
494
|
-
`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
const currentPath = join(this.logDir, this.currentFilename);
|
|
499
|
-
const newPath = join(this.logDir, `${baseName}.1.log`);
|
|
500
|
-
try {
|
|
501
|
-
if (existsSync(currentPath)) {
|
|
502
|
-
renameSync(currentPath, newPath);
|
|
503
|
-
}
|
|
504
|
-
} catch (error) {
|
|
505
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
506
|
-
process.stderr.write(`[FileTransport] Failed to rotate current file: ${errorMessage}
|
|
507
|
-
`);
|
|
508
|
-
}
|
|
509
|
-
await this.rotateStream(this.currentFilename);
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* 오래된 로그 파일 정리
|
|
513
|
-
* maxFiles 개수를 초과하는 로그 파일 삭제
|
|
514
|
-
*/
|
|
515
|
-
async cleanOldFiles() {
|
|
516
|
-
try {
|
|
517
|
-
if (!existsSync(this.logDir)) {
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
const files = readdirSync(this.logDir);
|
|
521
|
-
const logFiles = files.filter((file) => file.endsWith(".log")).map((file) => {
|
|
522
|
-
const filepath = join(this.logDir, file);
|
|
523
|
-
const stats = statSync(filepath);
|
|
524
|
-
return { file, mtime: stats.mtime };
|
|
525
|
-
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
526
|
-
if (logFiles.length > this.maxFiles) {
|
|
527
|
-
const filesToDelete = logFiles.slice(this.maxFiles);
|
|
528
|
-
for (const { file } of filesToDelete) {
|
|
529
|
-
const filepath = join(this.logDir, file);
|
|
530
|
-
try {
|
|
531
|
-
unlinkSync(filepath);
|
|
532
|
-
} catch (error) {
|
|
533
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
534
|
-
process.stderr.write(`[FileTransport] Failed to delete old file "${file}": ${errorMessage}
|
|
535
|
-
`);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
} catch (error) {
|
|
540
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
541
|
-
process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage}
|
|
542
|
-
`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* 날짜별 로그 파일명 생성
|
|
547
|
-
*/
|
|
548
|
-
getLogFilename(date) {
|
|
549
|
-
const year = date.getFullYear();
|
|
550
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
551
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
552
|
-
return `${year}-${month}-${day}.log`;
|
|
553
|
-
}
|
|
554
|
-
async close() {
|
|
555
|
-
await this.closeStream();
|
|
556
|
-
}
|
|
440
|
+
if (Object.keys(nonFileSchema).length > 0) {
|
|
441
|
+
const tempSchema = {
|
|
442
|
+
...schema,
|
|
443
|
+
properties: nonFileSchema,
|
|
444
|
+
required: schema.required?.filter((r) => r in nonFileSchema) ?? []
|
|
557
445
|
};
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (isProduction) {
|
|
570
|
-
return "info";
|
|
571
|
-
}
|
|
572
|
-
return "warn";
|
|
573
|
-
}
|
|
574
|
-
function getConsoleConfig() {
|
|
575
|
-
const isProduction = process.env.NODE_ENV === "production";
|
|
576
|
-
return {
|
|
577
|
-
level: "debug",
|
|
578
|
-
enabled: true,
|
|
579
|
-
colorize: !isProduction
|
|
580
|
-
// Dev: colored output, Production: plain text
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
function getFileConfig() {
|
|
584
|
-
const isProduction = process.env.NODE_ENV === "production";
|
|
585
|
-
return {
|
|
586
|
-
level: "info",
|
|
587
|
-
enabled: isProduction,
|
|
588
|
-
// File logging in production only
|
|
589
|
-
logDir: process.env.LOG_DIR || "./logs",
|
|
590
|
-
maxFileSize: 10 * 1024 * 1024,
|
|
591
|
-
// 10MB
|
|
592
|
-
maxFiles: 10
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
function validateDirectoryWritable(dirPath) {
|
|
596
|
-
if (!existsSync(dirPath)) {
|
|
597
|
-
try {
|
|
598
|
-
mkdirSync(dirPath, { recursive: true });
|
|
599
|
-
} catch (error) {
|
|
600
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
601
|
-
throw new Error(`Failed to create log directory "${dirPath}": ${errorMessage}`);
|
|
446
|
+
const converted = Value.Convert(tempSchema, nonFileData);
|
|
447
|
+
const errors = [...Value.Errors(tempSchema, converted)];
|
|
448
|
+
if (errors.length > 0) {
|
|
449
|
+
throw new ValidationError({
|
|
450
|
+
message: `Invalid ${fieldName}`,
|
|
451
|
+
fields: errors.map((e) => ({
|
|
452
|
+
path: e.path,
|
|
453
|
+
message: e.message,
|
|
454
|
+
value: e.value
|
|
455
|
+
}))
|
|
456
|
+
});
|
|
602
457
|
}
|
|
458
|
+
Object.assign(result, converted);
|
|
459
|
+
} else {
|
|
460
|
+
Object.assign(result, nonFileData);
|
|
603
461
|
}
|
|
604
|
-
|
|
605
|
-
accessSync(dirPath, constants.W_OK);
|
|
606
|
-
} catch {
|
|
607
|
-
throw new Error(`Log directory "${dirPath}" is not writable. Please check permissions.`);
|
|
608
|
-
}
|
|
609
|
-
const testFile = join(dirPath, ".logger-write-test");
|
|
610
|
-
try {
|
|
611
|
-
writeFileSync(testFile, "test", "utf-8");
|
|
612
|
-
unlinkSync(testFile);
|
|
613
|
-
} catch (error) {
|
|
614
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
615
|
-
throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
function validateFileConfig() {
|
|
619
|
-
if (!isFileLoggingEnabled()) {
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
const logDir = process.env.LOG_DIR;
|
|
623
|
-
if (!logDir) {
|
|
624
|
-
throw new Error(
|
|
625
|
-
"LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp"
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
validateDirectoryWritable(logDir);
|
|
629
|
-
}
|
|
630
|
-
function validateSlackConfig() {
|
|
631
|
-
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
|
|
632
|
-
if (!webhookUrl) {
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
if (!webhookUrl.startsWith("https://hooks.slack.com/")) {
|
|
636
|
-
throw new Error(
|
|
637
|
-
`Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"`
|
|
638
|
-
);
|
|
639
|
-
}
|
|
462
|
+
return result;
|
|
640
463
|
}
|
|
641
|
-
function
|
|
642
|
-
const
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const missingFields = [];
|
|
651
|
-
if (!smtpHost) missingFields.push("SMTP_HOST");
|
|
652
|
-
if (!smtpPort) missingFields.push("SMTP_PORT");
|
|
653
|
-
if (!emailFrom) missingFields.push("EMAIL_FROM");
|
|
654
|
-
if (!emailTo) missingFields.push("EMAIL_TO");
|
|
655
|
-
if (missingFields.length > 0) {
|
|
656
|
-
throw new Error(
|
|
657
|
-
`Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.`
|
|
658
|
-
);
|
|
659
|
-
}
|
|
660
|
-
const port = parseInt(smtpPort, 10);
|
|
661
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
662
|
-
throw new Error(
|
|
663
|
-
`Invalid SMTP_PORT: "${smtpPort}". Must be a number between 1 and 65535.`
|
|
664
|
-
);
|
|
665
|
-
}
|
|
666
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
667
|
-
if (!emailRegex.test(emailFrom)) {
|
|
668
|
-
throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`);
|
|
669
|
-
}
|
|
670
|
-
const recipients = emailTo.split(",").map((e) => e.trim());
|
|
671
|
-
for (const email of recipients) {
|
|
672
|
-
if (!emailRegex.test(email)) {
|
|
673
|
-
throw new Error(`Invalid email address in EMAIL_TO: "${email}"`);
|
|
464
|
+
function extractQueryParams(c) {
|
|
465
|
+
const url = new URL(c.req.url);
|
|
466
|
+
const queryObj = {};
|
|
467
|
+
url.searchParams.forEach((v, k) => {
|
|
468
|
+
const existing = queryObj[k];
|
|
469
|
+
if (existing) {
|
|
470
|
+
queryObj[k] = Array.isArray(existing) ? [...existing, v] : [existing, v];
|
|
471
|
+
} else {
|
|
472
|
+
queryObj[k] = v;
|
|
674
473
|
}
|
|
675
|
-
}
|
|
474
|
+
});
|
|
475
|
+
return queryObj;
|
|
676
476
|
}
|
|
677
|
-
function
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
477
|
+
function extractHeaders(c) {
|
|
478
|
+
const rawHeaders = {};
|
|
479
|
+
c.req.raw.headers.forEach((value, key) => {
|
|
480
|
+
rawHeaders[key.toLowerCase()] = value;
|
|
481
|
+
});
|
|
482
|
+
return rawHeaders;
|
|
483
|
+
}
|
|
484
|
+
function extractCookies(c) {
|
|
485
|
+
const cookieHeader = c.req.header("cookie");
|
|
486
|
+
const rawCookies = {};
|
|
487
|
+
if (cookieHeader) {
|
|
488
|
+
cookieHeader.split(";").forEach((cookie) => {
|
|
489
|
+
const [key, value] = cookie.trim().split("=");
|
|
490
|
+
if (key && value) {
|
|
491
|
+
rawCookies[key] = decodeURIComponent(value);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
683
494
|
}
|
|
495
|
+
return rawCookies;
|
|
684
496
|
}
|
|
685
|
-
function
|
|
497
|
+
async function parseJsonBody(c) {
|
|
686
498
|
try {
|
|
687
|
-
|
|
688
|
-
validateFileConfig();
|
|
689
|
-
validateSlackConfig();
|
|
690
|
-
validateEmailConfig();
|
|
499
|
+
return await c.req.json();
|
|
691
500
|
} catch (error) {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
// src/logger/factory.ts
|
|
704
|
-
function initializeTransports() {
|
|
705
|
-
const transports = [];
|
|
706
|
-
const consoleConfig = getConsoleConfig();
|
|
707
|
-
transports.push(new ConsoleTransport(consoleConfig));
|
|
708
|
-
const fileConfig = getFileConfig();
|
|
709
|
-
if (fileConfig.enabled) {
|
|
710
|
-
transports.push(new FileTransport(fileConfig));
|
|
501
|
+
throw new ValidationError({
|
|
502
|
+
message: "Invalid JSON body",
|
|
503
|
+
fields: [{
|
|
504
|
+
path: "/",
|
|
505
|
+
message: "Failed to parse JSON",
|
|
506
|
+
value: error instanceof Error ? error.message : "Unknown error"
|
|
507
|
+
}]
|
|
508
|
+
});
|
|
711
509
|
}
|
|
712
|
-
return transports;
|
|
713
510
|
}
|
|
714
|
-
function
|
|
715
|
-
validateConfig();
|
|
716
|
-
return new Logger({
|
|
717
|
-
level: getDefaultLogLevel(),
|
|
718
|
-
transports: initializeTransports()
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
var logger;
|
|
722
|
-
var init_factory = __esm({
|
|
723
|
-
"src/logger/factory.ts"() {
|
|
724
|
-
init_logger();
|
|
725
|
-
init_console();
|
|
726
|
-
init_file();
|
|
727
|
-
init_config();
|
|
728
|
-
logger = initializeLogger();
|
|
729
|
-
}
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
// src/logger/index.ts
|
|
733
|
-
var init_logger2 = __esm({
|
|
734
|
-
"src/logger/index.ts"() {
|
|
735
|
-
init_factory();
|
|
736
|
-
init_logger();
|
|
737
|
-
}
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
// src/route/function-routes.ts
|
|
741
|
-
var function_routes_exports = {};
|
|
742
|
-
__export(function_routes_exports, {
|
|
743
|
-
discoverFunctionRoutes: () => discoverFunctionRoutes
|
|
744
|
-
});
|
|
745
|
-
function discoverFunctionRoutes(cwd = process.cwd()) {
|
|
746
|
-
const functions = [];
|
|
747
|
-
const nodeModulesPath = join(cwd, "node_modules");
|
|
511
|
+
async function parseFormData(c) {
|
|
748
512
|
try {
|
|
749
|
-
const
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
}
|
|
759
|
-
try {
|
|
760
|
-
const pkgPath = join(nodeModulesPath, ...packageName.split("/"), "package.json");
|
|
761
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
762
|
-
if (pkg.spfn?.routes?.dir) {
|
|
763
|
-
const { dir } = pkg.spfn.routes;
|
|
764
|
-
const prefix = pkg.spfn.prefix;
|
|
765
|
-
const packagePath = dirname(pkgPath);
|
|
766
|
-
const routesDir = join(packagePath, dir);
|
|
767
|
-
functions.push({
|
|
768
|
-
packageName,
|
|
769
|
-
routesDir,
|
|
770
|
-
packagePath,
|
|
771
|
-
prefix
|
|
772
|
-
// Include prefix in function info
|
|
773
|
-
});
|
|
774
|
-
routeLogger.debug("Discovered function routes", {
|
|
775
|
-
package: packageName,
|
|
776
|
-
dir,
|
|
777
|
-
prefix: prefix || "(none)"
|
|
778
|
-
});
|
|
513
|
+
const formData = await c.req.formData();
|
|
514
|
+
const result = {};
|
|
515
|
+
formData.forEach((value, key) => {
|
|
516
|
+
const existing = result[key];
|
|
517
|
+
if (existing !== void 0) {
|
|
518
|
+
if (Array.isArray(existing)) {
|
|
519
|
+
existing.push(value);
|
|
520
|
+
} else {
|
|
521
|
+
result[key] = [existing, value];
|
|
779
522
|
}
|
|
780
|
-
}
|
|
523
|
+
} else {
|
|
524
|
+
result[key] = value;
|
|
781
525
|
}
|
|
782
|
-
}
|
|
526
|
+
});
|
|
527
|
+
return result;
|
|
783
528
|
} catch (error) {
|
|
784
|
-
|
|
785
|
-
|
|
529
|
+
throw new ValidationError({
|
|
530
|
+
message: "Invalid form data",
|
|
531
|
+
fields: [{
|
|
532
|
+
path: "/",
|
|
533
|
+
message: "Failed to parse form data",
|
|
534
|
+
value: error instanceof Error ? error.message : "Unknown error"
|
|
535
|
+
}]
|
|
786
536
|
});
|
|
787
537
|
}
|
|
788
|
-
return functions;
|
|
789
538
|
}
|
|
790
|
-
var routeLogger;
|
|
791
|
-
var init_function_routes = __esm({
|
|
792
|
-
"src/route/function-routes.ts"() {
|
|
793
|
-
init_logger2();
|
|
794
|
-
routeLogger = logger.child("function-routes");
|
|
795
|
-
}
|
|
796
|
-
});
|
|
797
539
|
|
|
798
|
-
// src/route/
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
failureCount++;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
const elapsed = Date.now() - startTime;
|
|
825
|
-
const stats = this.getStats();
|
|
826
|
-
if (this.debug) {
|
|
827
|
-
this.logStats(stats, elapsed);
|
|
828
|
-
}
|
|
829
|
-
if (failureCount > 0) {
|
|
830
|
-
routeLogger2.warn("Some routes failed to load", { failureCount });
|
|
831
|
-
}
|
|
832
|
-
return stats;
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* Load routes from an external directory (e.g., from SPFN function packages)
|
|
836
|
-
* Reads package.json spfn.prefix and mounts routes under that prefix
|
|
837
|
-
*
|
|
838
|
-
* @param app - Hono app instance
|
|
839
|
-
* @param routesDir - Directory containing route handlers
|
|
840
|
-
* @param packageName - Name of the package (for logging)
|
|
841
|
-
* @param prefix - Optional prefix to mount routes under (from package.json spfn.prefix)
|
|
842
|
-
* @returns Route statistics
|
|
843
|
-
*/
|
|
844
|
-
async loadExternalRoutes(app, routesDir, packageName, prefix) {
|
|
845
|
-
const startTime = Date.now();
|
|
846
|
-
const tempRoutesDir = this.routesDir;
|
|
847
|
-
this.routesDir = routesDir;
|
|
848
|
-
const files = await this.scanFiles(routesDir);
|
|
849
|
-
if (files.length === 0) {
|
|
850
|
-
routeLogger2.warn("No route files found", { dir: routesDir, package: packageName });
|
|
851
|
-
this.routesDir = tempRoutesDir;
|
|
852
|
-
return this.getStats();
|
|
853
|
-
}
|
|
854
|
-
let successCount = 0;
|
|
855
|
-
let failureCount = 0;
|
|
856
|
-
for (const file of files) {
|
|
857
|
-
const success = await this.loadRoute(app, file, prefix);
|
|
858
|
-
if (success) {
|
|
859
|
-
successCount++;
|
|
860
|
-
} else {
|
|
861
|
-
failureCount++;
|
|
540
|
+
// src/route/register-routes.ts
|
|
541
|
+
function isRouter(value) {
|
|
542
|
+
return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
|
|
543
|
+
}
|
|
544
|
+
function isRouteDef(value) {
|
|
545
|
+
return value !== null && typeof value === "object" && "handler" in value;
|
|
546
|
+
}
|
|
547
|
+
function isNamedMiddleware(value) {
|
|
548
|
+
return value !== null && typeof value === "object" && "name" in value && "handler" in value && "_name" in value;
|
|
549
|
+
}
|
|
550
|
+
function registerRoutes(app, router, namedMiddlewares, collectedRoutes) {
|
|
551
|
+
const routes = collectedRoutes ?? [];
|
|
552
|
+
const allNamedMiddlewares = [
|
|
553
|
+
...namedMiddlewares ?? [],
|
|
554
|
+
...router._globalMiddlewares.map((mw) => ({ name: mw.name, handler: mw.handler }))
|
|
555
|
+
];
|
|
556
|
+
for (const [name, routeOrRouter] of Object.entries(router.routes)) {
|
|
557
|
+
if (isRouter(routeOrRouter)) {
|
|
558
|
+
registerRoutes(app, routeOrRouter, allNamedMiddlewares, routes);
|
|
559
|
+
} else if (isRouteDef(routeOrRouter)) {
|
|
560
|
+
const registered = registerRoute(app, name, routeOrRouter, allNamedMiddlewares);
|
|
561
|
+
if (registered) {
|
|
562
|
+
routes.push(registered);
|
|
862
563
|
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
routeLogger2.info("External routes loaded", {
|
|
867
|
-
package: packageName,
|
|
868
|
-
prefix: prefix || "/",
|
|
869
|
-
total: successCount,
|
|
870
|
-
failed: failureCount,
|
|
871
|
-
elapsed: `${elapsed}ms`
|
|
564
|
+
} else {
|
|
565
|
+
logger.warn(`Unknown route type for "${name}" - skipping`, {
|
|
566
|
+
type: typeof routeOrRouter
|
|
872
567
|
});
|
|
873
568
|
}
|
|
874
|
-
this.routesDir = tempRoutesDir;
|
|
875
|
-
return this.getStats();
|
|
876
569
|
}
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
byPriority: { static: 0, dynamic: 0, catchAll: 0 },
|
|
881
|
-
byTag: {},
|
|
882
|
-
routes: this.routes
|
|
883
|
-
};
|
|
884
|
-
for (const route of this.routes) {
|
|
885
|
-
if (route.priority === 1) stats.byPriority.static++;
|
|
886
|
-
else if (route.priority === 2) stats.byPriority.dynamic++;
|
|
887
|
-
else if (route.priority === 3) stats.byPriority.catchAll++;
|
|
888
|
-
if (route.meta?.tags) {
|
|
889
|
-
for (const tag of route.meta.tags) {
|
|
890
|
-
stats.byTag[tag] = (stats.byTag[tag] || 0) + 1;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
570
|
+
if (router._packageRouters && router._packageRouters.length > 0) {
|
|
571
|
+
for (const pkgRouter of router._packageRouters) {
|
|
572
|
+
registerRoutes(app, pkgRouter, allNamedMiddlewares, routes);
|
|
893
573
|
}
|
|
894
|
-
return stats;
|
|
895
574
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
}
|
|
907
|
-
return files;
|
|
908
|
-
}
|
|
909
|
-
isValidRouteFile(fileName) {
|
|
910
|
-
return fileName === "index.ts" || fileName === "index.js" || fileName === "index.mjs";
|
|
911
|
-
}
|
|
912
|
-
async loadRoute(app, absolutePath, prefix) {
|
|
913
|
-
const relativePath = relative(this.routesDir, absolutePath);
|
|
914
|
-
try {
|
|
915
|
-
const module = await import(absolutePath);
|
|
916
|
-
if (!this.validateModule(module, relativePath)) {
|
|
917
|
-
return false;
|
|
918
|
-
}
|
|
919
|
-
const hasContractMetas = module.default._contractMetas && module.default._contractMetas.size > 0;
|
|
920
|
-
if (!hasContractMetas) {
|
|
921
|
-
routeLogger2.error("Route must use contract-based routing", {
|
|
922
|
-
file: relativePath,
|
|
923
|
-
hint: "Export contracts using satisfies RouteContract and use app.bind()"
|
|
924
|
-
});
|
|
925
|
-
return false;
|
|
926
|
-
}
|
|
927
|
-
const contractPaths = this.extractContractPaths(module);
|
|
928
|
-
if (prefix) {
|
|
929
|
-
const invalidPaths = contractPaths.filter((path) => !path.startsWith(prefix));
|
|
930
|
-
if (invalidPaths.length > 0) {
|
|
931
|
-
routeLogger2.error("Contract paths must include the package prefix", {
|
|
932
|
-
file: relativePath,
|
|
933
|
-
prefix,
|
|
934
|
-
invalidPaths,
|
|
935
|
-
hint: `Contract paths should start with "${prefix}". Example: path: "${prefix}/labels"`
|
|
936
|
-
});
|
|
937
|
-
return false;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
this.registerContractBasedMiddlewares(app, contractPaths, module);
|
|
941
|
-
app.route("/", module.default);
|
|
942
|
-
contractPaths.forEach((path) => {
|
|
943
|
-
this.routes.push({
|
|
944
|
-
path,
|
|
945
|
-
// Use contract path as-is (already includes prefix)
|
|
946
|
-
file: relativePath,
|
|
947
|
-
meta: module.meta,
|
|
948
|
-
priority: this.calculateContractPriority(path)
|
|
949
|
-
});
|
|
950
|
-
if (this.debug) {
|
|
951
|
-
const icon = path.includes("*") ? "\u2B50" : path.includes(":") ? "\u{1F538}" : "\u{1F539}";
|
|
952
|
-
routeLogger2.debug(`Registered route: ${path}`, { icon, file: relativePath });
|
|
953
|
-
}
|
|
954
|
-
});
|
|
955
|
-
return true;
|
|
956
|
-
} catch (error) {
|
|
957
|
-
this.categorizeAndLogError(error, relativePath);
|
|
958
|
-
return false;
|
|
959
|
-
}
|
|
575
|
+
return routes;
|
|
576
|
+
}
|
|
577
|
+
function registerRoute(app, name, routeDef, namedMiddlewares) {
|
|
578
|
+
const { method, path, input, middlewares = [], skipMiddlewares, handler } = routeDef;
|
|
579
|
+
if (!method || !path) {
|
|
580
|
+
logger.warn(`Route "${name}" is missing method or path - skipping`, {
|
|
581
|
+
method,
|
|
582
|
+
path
|
|
583
|
+
});
|
|
584
|
+
return null;
|
|
960
585
|
}
|
|
961
|
-
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
if (path) {
|
|
967
|
-
paths.add(path);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
586
|
+
const wrappedHandler = async (c) => {
|
|
587
|
+
const { context, responseMeta } = await createRouteBuilderContext(c, input || {});
|
|
588
|
+
const result = await handler(context);
|
|
589
|
+
if (result instanceof Response) {
|
|
590
|
+
return result;
|
|
970
591
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
calculateContractPriority(path) {
|
|
974
|
-
if (path.includes("*")) return 3;
|
|
975
|
-
if (path.includes(":")) return 2;
|
|
976
|
-
return 1;
|
|
977
|
-
}
|
|
978
|
-
validateModule(module, relativePath) {
|
|
979
|
-
if (!module.default) {
|
|
980
|
-
routeLogger2.error("Route must export Hono instance as default", { file: relativePath });
|
|
981
|
-
return false;
|
|
592
|
+
if (responseMeta.isEmpty) {
|
|
593
|
+
return c.body(null, responseMeta.status);
|
|
982
594
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
return
|
|
595
|
+
const hasCustomHeaders = Object.keys(responseMeta.headers).length > 0;
|
|
596
|
+
if (hasCustomHeaders) {
|
|
597
|
+
return c.json(result, responseMeta.status, responseMeta.headers);
|
|
986
598
|
}
|
|
987
|
-
return
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
return next();
|
|
1007
|
-
}
|
|
1008
|
-
return middleware.handler(c, next);
|
|
1009
|
-
});
|
|
599
|
+
return c.json(result, responseMeta.status);
|
|
600
|
+
};
|
|
601
|
+
const allMiddlewares = [];
|
|
602
|
+
const registeredNames = /* @__PURE__ */ new Set();
|
|
603
|
+
const registeredHandlers = /* @__PURE__ */ new Set();
|
|
604
|
+
const skipAll = skipMiddlewares === "*";
|
|
605
|
+
if (namedMiddlewares && namedMiddlewares.length > 0) {
|
|
606
|
+
if (skipAll) {
|
|
607
|
+
logger.debug(`\u23ED\uFE0F Skipping all middlewares (*) for route: ${method} ${path}`, { name });
|
|
608
|
+
} else {
|
|
609
|
+
const skipSet = new Set(Array.isArray(skipMiddlewares) ? skipMiddlewares : []);
|
|
610
|
+
for (const middleware of namedMiddlewares) {
|
|
611
|
+
if (!skipSet.has(middleware.name)) {
|
|
612
|
+
allMiddlewares.push(middleware.handler);
|
|
613
|
+
registeredNames.add(middleware.name);
|
|
614
|
+
registeredHandlers.add(middleware.handler);
|
|
615
|
+
} else {
|
|
616
|
+
logger.debug(`\u23ED\uFE0F Skipping middleware '${middleware.name}' for route: ${method} ${path}`, { name });
|
|
617
|
+
}
|
|
1010
618
|
}
|
|
1011
619
|
}
|
|
1012
620
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
});
|
|
1022
|
-
} else if (message.includes("SyntaxError") || stack?.includes("SyntaxError")) {
|
|
1023
|
-
routeLogger2.error("Syntax error", {
|
|
1024
|
-
file: relativePath,
|
|
1025
|
-
error: message,
|
|
1026
|
-
...this.debug && stack && {
|
|
1027
|
-
stack: stack.split("\n").slice(0, 5).join("\n")
|
|
1028
|
-
}
|
|
1029
|
-
});
|
|
1030
|
-
} else if (message.includes("Unexpected token")) {
|
|
1031
|
-
routeLogger2.error("Parse error", {
|
|
1032
|
-
file: relativePath,
|
|
1033
|
-
error: message,
|
|
1034
|
-
hint: "Check for syntax errors or invalid TypeScript"
|
|
1035
|
-
});
|
|
621
|
+
for (const mw of middlewares) {
|
|
622
|
+
if (isNamedMiddleware(mw)) {
|
|
623
|
+
if (registeredNames.has(mw.name)) {
|
|
624
|
+
logger.debug(`\u{1F504} Skipping duplicate middleware '${mw.name}' for route: ${method} ${path}`, { name });
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
registeredNames.add(mw.name);
|
|
628
|
+
allMiddlewares.push(mw.handler);
|
|
1036
629
|
} else {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
...this.debug && stack && { stack }
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
logStats(stats, elapsed) {
|
|
1045
|
-
const tagCounts = Object.entries(stats.byTag).map(([tag, count]) => `${tag}(${count})`).join(", ");
|
|
1046
|
-
routeLogger2.info("Routes loaded successfully", {
|
|
1047
|
-
total: stats.total,
|
|
1048
|
-
priority: {
|
|
1049
|
-
static: stats.byPriority.static,
|
|
1050
|
-
dynamic: stats.byPriority.dynamic,
|
|
1051
|
-
catchAll: stats.byPriority.catchAll
|
|
1052
|
-
},
|
|
1053
|
-
...tagCounts && { tags: tagCounts },
|
|
1054
|
-
elapsed: `${elapsed}ms`
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1057
|
-
};
|
|
1058
|
-
async function loadRoutes(app, options) {
|
|
1059
|
-
const routesDir = options?.routesDir ?? join(process.cwd(), "src", "server", "routes");
|
|
1060
|
-
const debug = options?.debug ?? false;
|
|
1061
|
-
const middlewares = options?.middlewares ?? [];
|
|
1062
|
-
const includeFunctionRoutes = options?.includeFunctionRoutes ?? true;
|
|
1063
|
-
const loader = new AutoRouteLoader(routesDir, debug, middlewares);
|
|
1064
|
-
const stats = await loader.load(app);
|
|
1065
|
-
if (includeFunctionRoutes) {
|
|
1066
|
-
const { discoverFunctionRoutes: discoverFunctionRoutes2 } = await Promise.resolve().then(() => (init_function_routes(), function_routes_exports));
|
|
1067
|
-
const functionRoutes = discoverFunctionRoutes2();
|
|
1068
|
-
if (functionRoutes.length > 0) {
|
|
1069
|
-
routeLogger2.info("Loading function routes", { count: functionRoutes.length });
|
|
1070
|
-
for (const func of functionRoutes) {
|
|
1071
|
-
try {
|
|
1072
|
-
await loader.loadExternalRoutes(app, func.routesDir, func.packageName, func.prefix);
|
|
1073
|
-
routeLogger2.info("Function routes loaded", {
|
|
1074
|
-
package: func.packageName,
|
|
1075
|
-
routesDir: func.routesDir,
|
|
1076
|
-
prefix: func.prefix || "/"
|
|
1077
|
-
});
|
|
1078
|
-
} catch (error) {
|
|
1079
|
-
routeLogger2.error("Failed to load function routes", {
|
|
1080
|
-
package: func.packageName,
|
|
1081
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
630
|
+
if (registeredHandlers.has(mw)) {
|
|
631
|
+
logger.debug(`\u{1F504} Skipping duplicate middleware handler for route: ${method} ${path}`, { name });
|
|
632
|
+
continue;
|
|
1084
633
|
}
|
|
634
|
+
registeredHandlers.add(mw);
|
|
635
|
+
allMiddlewares.push(mw);
|
|
1085
636
|
}
|
|
1086
637
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
statusCode;
|
|
1093
|
-
details;
|
|
1094
|
-
timestamp;
|
|
1095
|
-
constructor(message, statusCode, details) {
|
|
1096
|
-
super(message);
|
|
1097
|
-
this.name = "HttpError";
|
|
1098
|
-
this.statusCode = statusCode;
|
|
1099
|
-
this.details = details;
|
|
1100
|
-
this.timestamp = /* @__PURE__ */ new Date();
|
|
1101
|
-
Error.captureStackTrace(this, this.constructor);
|
|
1102
|
-
}
|
|
1103
|
-
/**
|
|
1104
|
-
* Serialize error for API response
|
|
1105
|
-
*/
|
|
1106
|
-
toJSON() {
|
|
1107
|
-
return {
|
|
1108
|
-
name: this.name,
|
|
1109
|
-
message: this.message,
|
|
1110
|
-
statusCode: this.statusCode,
|
|
1111
|
-
details: this.details,
|
|
1112
|
-
timestamp: this.timestamp.toISOString()
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
};
|
|
1116
|
-
var ValidationError = class extends HttpError {
|
|
1117
|
-
constructor(message, details) {
|
|
1118
|
-
super(message, 400, details);
|
|
1119
|
-
this.name = "ValidationError";
|
|
638
|
+
const methodLower = method.toLowerCase();
|
|
639
|
+
if (allMiddlewares.length > 0) {
|
|
640
|
+
app[methodLower](path, ...allMiddlewares, wrappedHandler);
|
|
641
|
+
} else {
|
|
642
|
+
app[methodLower](path, wrappedHandler);
|
|
1120
643
|
}
|
|
1121
|
-
};
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
}))
|
|
1140
|
-
}
|
|
1141
|
-
);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
const url = new URL(rawContext.req.url);
|
|
1145
|
-
let query = {};
|
|
1146
|
-
url.searchParams.forEach((v, k) => {
|
|
1147
|
-
const existing = query[k];
|
|
1148
|
-
if (existing) {
|
|
1149
|
-
query[k] = Array.isArray(existing) ? [...existing, v] : [existing, v];
|
|
1150
|
-
} else {
|
|
1151
|
-
query[k] = v;
|
|
1152
|
-
}
|
|
1153
|
-
});
|
|
1154
|
-
if (contract.query) {
|
|
1155
|
-
query = Value.Convert(contract.query, query);
|
|
1156
|
-
const errors = [...Value.Errors(contract.query, query)];
|
|
1157
|
-
if (errors.length > 0) {
|
|
1158
|
-
throw new ValidationError(
|
|
1159
|
-
"Invalid query parameters",
|
|
1160
|
-
{
|
|
1161
|
-
fields: errors.map((e) => ({
|
|
1162
|
-
path: e.path,
|
|
1163
|
-
message: e.message,
|
|
1164
|
-
value: e.value
|
|
1165
|
-
}))
|
|
1166
|
-
}
|
|
1167
|
-
);
|
|
1168
|
-
}
|
|
644
|
+
logger.debug(`Registered route: ${method} ${path}`, { name });
|
|
645
|
+
return { method, path, name };
|
|
646
|
+
}
|
|
647
|
+
async function createRouteBuilderContext(c, input) {
|
|
648
|
+
const params = validateField(input.params, c.req.param(), "path parameters");
|
|
649
|
+
const query = validateField(input.query, extractQueryParams(c), "query parameters");
|
|
650
|
+
const headers = validateField(input.headers, extractHeaders(c), "headers");
|
|
651
|
+
const cookies = validateField(input.cookies, extractCookies(c), "cookies");
|
|
652
|
+
let body = {};
|
|
653
|
+
let formData = {};
|
|
654
|
+
if (input.body || input.formData) {
|
|
655
|
+
const contentType = c.req.header("content-type") || "";
|
|
656
|
+
if (contentType.includes("multipart/form-data") && input.formData) {
|
|
657
|
+
const rawFormData = await parseFormData(c);
|
|
658
|
+
formData = validateFormData(input.formData, rawFormData, "form data");
|
|
659
|
+
} else if (input.body) {
|
|
660
|
+
const rawBody = await parseJsonBody(c);
|
|
661
|
+
body = validateField(input.body, rawBody, "request body");
|
|
1169
662
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
body = Value.Convert(contract.body, body);
|
|
1177
|
-
const errors = [...Value.Errors(contract.body, body)];
|
|
1178
|
-
if (errors.length > 0) {
|
|
1179
|
-
throw new ValidationError(
|
|
1180
|
-
"Invalid request body",
|
|
1181
|
-
{
|
|
1182
|
-
fields: errors.map((e) => ({
|
|
1183
|
-
path: e.path,
|
|
1184
|
-
message: e.message,
|
|
1185
|
-
value: e.value
|
|
1186
|
-
}))
|
|
1187
|
-
}
|
|
1188
|
-
);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
return body;
|
|
1192
|
-
},
|
|
1193
|
-
json: (data, status, headers) => {
|
|
1194
|
-
const errorHandlerEnabled = rawContext.get("errorHandlerEnabled");
|
|
1195
|
-
if (errorHandlerEnabled && process.env.NODE_ENV !== "production") {
|
|
1196
|
-
const hasSuccessField = data && typeof data === "object" && "success" in data;
|
|
1197
|
-
if (!hasSuccessField) {
|
|
1198
|
-
logger.warn(
|
|
1199
|
-
"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."
|
|
1200
|
-
);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
return rawContext.json(data, status, headers);
|
|
1204
|
-
},
|
|
1205
|
-
success: (data, meta, status = 200) => {
|
|
1206
|
-
const response = {
|
|
1207
|
-
success: true,
|
|
1208
|
-
data
|
|
1209
|
-
};
|
|
1210
|
-
if (meta) {
|
|
1211
|
-
response.meta = meta;
|
|
1212
|
-
}
|
|
1213
|
-
return rawContext.json(response, status);
|
|
1214
|
-
},
|
|
1215
|
-
paginated: (data, page, limit, total) => {
|
|
1216
|
-
const response = {
|
|
1217
|
-
success: true,
|
|
1218
|
-
data,
|
|
1219
|
-
meta: {
|
|
1220
|
-
pagination: {
|
|
1221
|
-
page,
|
|
1222
|
-
limit,
|
|
1223
|
-
total,
|
|
1224
|
-
totalPages: Math.ceil(total / limit)
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
};
|
|
1228
|
-
return rawContext.json(response, 200);
|
|
1229
|
-
},
|
|
1230
|
-
raw: rawContext
|
|
1231
|
-
};
|
|
1232
|
-
return handler(routeContext);
|
|
663
|
+
}
|
|
664
|
+
let cachedData = null;
|
|
665
|
+
const responseMeta = {
|
|
666
|
+
status: 200,
|
|
667
|
+
headers: {},
|
|
668
|
+
isEmpty: false
|
|
1233
669
|
};
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
670
|
+
const context = {
|
|
671
|
+
data: async () => {
|
|
672
|
+
if (!cachedData) {
|
|
673
|
+
cachedData = { params, query, body, formData, headers, cookies };
|
|
674
|
+
}
|
|
675
|
+
return cachedData;
|
|
676
|
+
},
|
|
677
|
+
json: (data, status, resHeaders) => {
|
|
678
|
+
return c.json(data, status, resHeaders);
|
|
679
|
+
},
|
|
680
|
+
created: (data, location) => {
|
|
681
|
+
responseMeta.status = 201;
|
|
682
|
+
if (location) {
|
|
683
|
+
responseMeta.headers["Location"] = location;
|
|
684
|
+
}
|
|
685
|
+
return data;
|
|
686
|
+
},
|
|
687
|
+
accepted: (data) => {
|
|
688
|
+
responseMeta.status = 202;
|
|
689
|
+
if (data === void 0) {
|
|
690
|
+
responseMeta.isEmpty = true;
|
|
691
|
+
return void 0;
|
|
692
|
+
}
|
|
693
|
+
return data;
|
|
694
|
+
},
|
|
695
|
+
noContent: () => {
|
|
696
|
+
responseMeta.status = 204;
|
|
697
|
+
responseMeta.isEmpty = true;
|
|
698
|
+
},
|
|
699
|
+
notModified: () => {
|
|
700
|
+
responseMeta.status = 304;
|
|
701
|
+
responseMeta.isEmpty = true;
|
|
702
|
+
},
|
|
703
|
+
paginated: (data, page, limit, total) => {
|
|
704
|
+
return {
|
|
705
|
+
items: data,
|
|
706
|
+
pagination: {
|
|
707
|
+
page,
|
|
708
|
+
limit,
|
|
709
|
+
total,
|
|
710
|
+
totalPages: Math.ceil(total / limit)
|
|
711
|
+
}
|
|
1256
712
|
};
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
}
|
|
1263
|
-
errorLogger[logLevel]("Error occurred", logData);
|
|
1264
|
-
}
|
|
1265
|
-
const response = {
|
|
1266
|
-
success: false,
|
|
1267
|
-
error: {
|
|
1268
|
-
message: err.message || "Internal Server Error",
|
|
1269
|
-
type: errorType,
|
|
1270
|
-
statusCode
|
|
1271
|
-
}
|
|
1272
|
-
};
|
|
1273
|
-
if (errorWithCode.details) {
|
|
1274
|
-
response.error.details = errorWithCode.details;
|
|
1275
|
-
}
|
|
1276
|
-
if (includeStack) {
|
|
1277
|
-
response.error.stack = err.stack;
|
|
1278
|
-
}
|
|
1279
|
-
return c.json(response, statusCode);
|
|
713
|
+
},
|
|
714
|
+
redirect: (url, status) => {
|
|
715
|
+
return c.redirect(url, status);
|
|
716
|
+
},
|
|
717
|
+
raw: c
|
|
1280
718
|
};
|
|
719
|
+
return { context, responseMeta };
|
|
1281
720
|
}
|
|
1282
721
|
|
|
1283
|
-
// src/
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
throw new Error(`Unsupported HTTP method: ${contract.method}`);
|
|
722
|
+
// src/route/define-middleware.ts
|
|
723
|
+
function defineMiddleware(name, handlerOrFactory) {
|
|
724
|
+
if (typeof handlerOrFactory === "function") {
|
|
725
|
+
const paramCount = handlerOrFactory.length;
|
|
726
|
+
if (paramCount === 2) {
|
|
727
|
+
return {
|
|
728
|
+
name,
|
|
729
|
+
handler: handlerOrFactory,
|
|
730
|
+
_name: name
|
|
731
|
+
};
|
|
732
|
+
} else {
|
|
733
|
+
const factory = handlerOrFactory;
|
|
734
|
+
const wrapper = (...args) => factory(...args);
|
|
735
|
+
Object.defineProperty(wrapper, "name", {
|
|
736
|
+
value: name,
|
|
737
|
+
writable: false,
|
|
738
|
+
enumerable: false,
|
|
739
|
+
configurable: true
|
|
740
|
+
});
|
|
741
|
+
Object.defineProperty(wrapper, "_name", {
|
|
742
|
+
value: name,
|
|
743
|
+
writable: false,
|
|
744
|
+
enumerable: false,
|
|
745
|
+
configurable: true
|
|
746
|
+
});
|
|
747
|
+
return wrapper;
|
|
1310
748
|
}
|
|
1311
|
-
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
name,
|
|
752
|
+
handler: handlerOrFactory,
|
|
753
|
+
_name: name
|
|
1312
754
|
};
|
|
1313
|
-
return app;
|
|
1314
755
|
}
|
|
1315
|
-
function
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
pagination: Type.Optional(Type.Object({
|
|
1323
|
-
page: Type.Number(),
|
|
1324
|
-
limit: Type.Number(),
|
|
1325
|
-
total: Type.Number(),
|
|
1326
|
-
totalPages: Type.Number()
|
|
1327
|
-
}))
|
|
1328
|
-
}))
|
|
756
|
+
function defineMiddlewareFactory(name, factory) {
|
|
757
|
+
const wrapper = (...args) => factory(...args);
|
|
758
|
+
Object.defineProperty(wrapper, "name", {
|
|
759
|
+
value: name,
|
|
760
|
+
writable: false,
|
|
761
|
+
enumerable: false,
|
|
762
|
+
configurable: true
|
|
1329
763
|
});
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
message: Type.String(),
|
|
1336
|
-
type: Type.String(),
|
|
1337
|
-
statusCode: Type.Number(),
|
|
1338
|
-
stack: Type.Optional(Type.String()),
|
|
1339
|
-
details: Type.Optional(Type.Any())
|
|
1340
|
-
})
|
|
764
|
+
Object.defineProperty(wrapper, "_name", {
|
|
765
|
+
value: name,
|
|
766
|
+
writable: false,
|
|
767
|
+
enumerable: false,
|
|
768
|
+
configurable: true
|
|
1341
769
|
});
|
|
770
|
+
return wrapper;
|
|
1342
771
|
}
|
|
1343
|
-
function ApiResponseSchema(dataSchema) {
|
|
1344
|
-
return ApiSuccessSchema(dataSchema);
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
// src/route/types.ts
|
|
1348
772
|
function isHttpMethod(value) {
|
|
1349
773
|
return typeof value === "string" && ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(value);
|
|
1350
774
|
}
|
|
775
|
+
var Nullable = (schema) => Type.Union([schema, Type.Null()]);
|
|
776
|
+
var OptionalNullable = (schema) => Type.Optional(Type.Union([schema, Type.Null()]));
|
|
1351
777
|
|
|
1352
|
-
export {
|
|
778
|
+
export { FileArraySchema, FileSchema, Nullable, OptionalFileSchema, OptionalNullable, defineMiddleware, defineMiddlewareFactory, defineRouter, formatFileSize, getFileOptions, isFileArraySchema, isFileSchema, isHttpMethod, registerRoutes, route };
|
|
1353
779
|
//# sourceMappingURL=index.js.map
|
|
1354
780
|
//# sourceMappingURL=index.js.map
|