shokupan 0.1.0 → 0.2.0
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 +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +12 -3
- package/dist/index.cjs +819 -452
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +819 -452
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +2 -0
- package/dist/{openapi-analyzer-CFqgSLNK.cjs → openapi-analyzer-BN0wFCML.cjs} +4 -1
- package/dist/openapi-analyzer-BN0wFCML.cjs.map +1 -0
- package/dist/{openapi-analyzer-cjdGeQ5a.js → openapi-analyzer-BTExMLX4.js} +4 -1
- package/dist/openapi-analyzer-BTExMLX4.js.map +1 -0
- package/dist/plugins/debugview/plugin.d.ts +34 -0
- package/dist/plugins/openapi-validator.d.ts +2 -0
- package/dist/plugins/proxy.d.ts +9 -0
- package/dist/response.d.ts +4 -0
- package/dist/router.d.ts +2 -0
- package/dist/server-adapter-BD6oKEto.cjs +81 -0
- package/dist/server-adapter-BD6oKEto.cjs.map +1 -0
- package/dist/server-adapter-CnQFr4P7.js +64 -0
- package/dist/server-adapter-CnQFr4P7.js.map +1 -0
- package/dist/shokupan.d.ts +2 -0
- package/dist/types.d.ts +12 -0
- package/package.json +5 -2
- package/dist/openapi-analyzer-CFqgSLNK.cjs.map +0 -1
- package/dist/openapi-analyzer-cjdGeQ5a.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const api = require("@opentelemetry/api");
|
|
4
3
|
const eta$2 = require("eta");
|
|
5
4
|
const promises = require("fs/promises");
|
|
6
5
|
const path = require("path");
|
|
7
6
|
const node_async_hooks = require("node:async_hooks");
|
|
7
|
+
const api = require("@opentelemetry/api");
|
|
8
8
|
const arctic = require("arctic");
|
|
9
9
|
const jose = require("jose");
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const events = require("events");
|
|
10
|
+
const Ajv = require("ajv");
|
|
11
|
+
const addFormats = require("ajv-formats");
|
|
13
12
|
const classTransformer = require("class-transformer");
|
|
14
13
|
const classValidator = require("class-validator");
|
|
14
|
+
const openapiAnalyzer = require("./openapi-analyzer-BN0wFCML.cjs");
|
|
15
|
+
const crypto = require("crypto");
|
|
16
|
+
const events = require("events");
|
|
15
17
|
function _interopNamespaceDefault(e) {
|
|
16
18
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
17
19
|
if (e) {
|
|
@@ -30,12 +32,13 @@ function _interopNamespaceDefault(e) {
|
|
|
30
32
|
}
|
|
31
33
|
const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
|
|
32
34
|
class ShokupanResponse {
|
|
33
|
-
_headers =
|
|
35
|
+
_headers = null;
|
|
34
36
|
_status = 200;
|
|
35
37
|
/**
|
|
36
38
|
* Get the current headers
|
|
37
39
|
*/
|
|
38
40
|
get headers() {
|
|
41
|
+
if (!this._headers) this._headers = new Headers();
|
|
39
42
|
return this._headers;
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
@@ -56,6 +59,7 @@ class ShokupanResponse {
|
|
|
56
59
|
* @param value Header value
|
|
57
60
|
*/
|
|
58
61
|
set(key, value) {
|
|
62
|
+
if (!this._headers) this._headers = new Headers();
|
|
59
63
|
this._headers.set(key, value);
|
|
60
64
|
return this;
|
|
61
65
|
}
|
|
@@ -65,6 +69,7 @@ class ShokupanResponse {
|
|
|
65
69
|
* @param value Header value
|
|
66
70
|
*/
|
|
67
71
|
append(key, value) {
|
|
72
|
+
if (!this._headers) this._headers = new Headers();
|
|
68
73
|
this._headers.append(key, value);
|
|
69
74
|
return this;
|
|
70
75
|
}
|
|
@@ -73,29 +78,58 @@ class ShokupanResponse {
|
|
|
73
78
|
* @param key Header name
|
|
74
79
|
*/
|
|
75
80
|
get(key) {
|
|
76
|
-
return this._headers
|
|
81
|
+
return this._headers?.get(key) || null;
|
|
77
82
|
}
|
|
78
83
|
/**
|
|
79
84
|
* Check if a header exists
|
|
80
85
|
* @param key Header name
|
|
81
86
|
*/
|
|
82
87
|
has(key) {
|
|
83
|
-
return this._headers
|
|
88
|
+
return this._headers?.has(key) || false;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Internal: check if headers have been initialized/modified
|
|
92
|
+
*/
|
|
93
|
+
get hasPopulatedHeaders() {
|
|
94
|
+
return this._headers !== null;
|
|
84
95
|
}
|
|
85
96
|
}
|
|
86
97
|
class ShokupanContext {
|
|
87
|
-
constructor(request, server, state, app) {
|
|
98
|
+
constructor(request, server, state, app, enableMiddlewareTracking = false) {
|
|
88
99
|
this.request = request;
|
|
89
100
|
this.server = server;
|
|
90
101
|
this.app = app;
|
|
91
|
-
this.url = new URL(request.url);
|
|
92
102
|
this.state = state || {};
|
|
103
|
+
if (enableMiddlewareTracking) {
|
|
104
|
+
const self = this;
|
|
105
|
+
this.state = new Proxy(this.state, {
|
|
106
|
+
set(target, p, newValue, receiver) {
|
|
107
|
+
const result = Reflect.set(target, p, newValue, receiver);
|
|
108
|
+
const currentHandler = self.handlerStack[self.handlerStack.length - 1];
|
|
109
|
+
if (currentHandler) {
|
|
110
|
+
if (!currentHandler.stateChanges) currentHandler.stateChanges = {};
|
|
111
|
+
currentHandler.stateChanges[p] = newValue;
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
93
117
|
this.response = new ShokupanResponse();
|
|
94
118
|
}
|
|
95
|
-
|
|
119
|
+
_url;
|
|
96
120
|
params = {};
|
|
121
|
+
// Router assigns this, but default to empty object
|
|
97
122
|
state;
|
|
123
|
+
handlerStack = [];
|
|
98
124
|
response;
|
|
125
|
+
_finalResponse;
|
|
126
|
+
get url() {
|
|
127
|
+
if (!this._url) {
|
|
128
|
+
const urlString = this.request.url || "http://localhost/";
|
|
129
|
+
this._url = new URL(urlString);
|
|
130
|
+
}
|
|
131
|
+
return this._url;
|
|
132
|
+
}
|
|
99
133
|
/**
|
|
100
134
|
* Base request
|
|
101
135
|
*/
|
|
@@ -112,7 +146,26 @@ class ShokupanContext {
|
|
|
112
146
|
* Request path
|
|
113
147
|
*/
|
|
114
148
|
get path() {
|
|
115
|
-
return this.
|
|
149
|
+
if (this._url) return this._url.pathname;
|
|
150
|
+
const url = this.request.url;
|
|
151
|
+
let queryIndex = url.indexOf("?");
|
|
152
|
+
const end = queryIndex === -1 ? url.length : queryIndex;
|
|
153
|
+
let start = 0;
|
|
154
|
+
const protocolIndex = url.indexOf("://");
|
|
155
|
+
if (protocolIndex !== -1) {
|
|
156
|
+
const hostStart = protocolIndex + 3;
|
|
157
|
+
const pathStart = url.indexOf("/", hostStart);
|
|
158
|
+
if (pathStart !== -1 && pathStart < end) {
|
|
159
|
+
start = pathStart;
|
|
160
|
+
} else {
|
|
161
|
+
return "/";
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
if (url.charCodeAt(0) === 47) {
|
|
165
|
+
start = 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return url.substring(start, end);
|
|
116
169
|
}
|
|
117
170
|
/**
|
|
118
171
|
* Request query params
|
|
@@ -209,9 +262,25 @@ class ShokupanContext {
|
|
|
209
262
|
return this;
|
|
210
263
|
}
|
|
211
264
|
mergeHeaders(headers) {
|
|
212
|
-
|
|
265
|
+
let h;
|
|
266
|
+
if (this.response.hasPopulatedHeaders) {
|
|
267
|
+
h = new Headers(this.response.headers);
|
|
268
|
+
} else {
|
|
269
|
+
h = new Headers();
|
|
270
|
+
}
|
|
213
271
|
if (headers) {
|
|
214
|
-
|
|
272
|
+
if (headers instanceof Headers) {
|
|
273
|
+
headers.forEach((v, k) => h.set(k, v));
|
|
274
|
+
} else if (Array.isArray(headers)) {
|
|
275
|
+
headers.forEach(([k, v]) => h.set(k, v));
|
|
276
|
+
} else {
|
|
277
|
+
const keys = Object.keys(headers);
|
|
278
|
+
for (let i = 0; i < keys.length; i++) {
|
|
279
|
+
const key = keys[i];
|
|
280
|
+
const val = headers[key];
|
|
281
|
+
h.set(key, val);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
215
284
|
}
|
|
216
285
|
return h;
|
|
217
286
|
}
|
|
@@ -224,7 +293,8 @@ class ShokupanContext {
|
|
|
224
293
|
send(body, options) {
|
|
225
294
|
const headers = this.mergeHeaders(options?.headers);
|
|
226
295
|
const status = options?.status ?? this.response.status;
|
|
227
|
-
|
|
296
|
+
this._finalResponse = new Response(body, { status, headers });
|
|
297
|
+
return this._finalResponse;
|
|
228
298
|
}
|
|
229
299
|
/**
|
|
230
300
|
* Read request body
|
|
@@ -243,19 +313,36 @@ class ShokupanContext {
|
|
|
243
313
|
* Respond with a JSON object
|
|
244
314
|
*/
|
|
245
315
|
json(data, status, headers) {
|
|
316
|
+
const finalStatus = status ?? this.response.status;
|
|
317
|
+
const jsonString = JSON.stringify(data);
|
|
318
|
+
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
319
|
+
this._finalResponse = new Response(jsonString, {
|
|
320
|
+
status: finalStatus,
|
|
321
|
+
headers: { "content-type": "application/json" }
|
|
322
|
+
});
|
|
323
|
+
return this._finalResponse;
|
|
324
|
+
}
|
|
246
325
|
const finalHeaders = this.mergeHeaders(headers);
|
|
247
326
|
finalHeaders.set("content-type", "application/json");
|
|
248
|
-
|
|
249
|
-
return
|
|
327
|
+
this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
|
|
328
|
+
return this._finalResponse;
|
|
250
329
|
}
|
|
251
330
|
/**
|
|
252
331
|
* Respond with a text string
|
|
253
332
|
*/
|
|
254
333
|
text(data, status, headers) {
|
|
334
|
+
const finalStatus = status ?? this.response.status;
|
|
335
|
+
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
336
|
+
this._finalResponse = new Response(data, {
|
|
337
|
+
status: finalStatus,
|
|
338
|
+
headers: { "content-type": "text/plain" }
|
|
339
|
+
});
|
|
340
|
+
return this._finalResponse;
|
|
341
|
+
}
|
|
255
342
|
const finalHeaders = this.mergeHeaders(headers);
|
|
256
343
|
finalHeaders.set("content-type", "text/plain");
|
|
257
|
-
|
|
258
|
-
return
|
|
344
|
+
this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
|
|
345
|
+
return this._finalResponse;
|
|
259
346
|
}
|
|
260
347
|
/**
|
|
261
348
|
* Respond with HTML content
|
|
@@ -264,7 +351,8 @@ class ShokupanContext {
|
|
|
264
351
|
const finalHeaders = this.mergeHeaders(headers);
|
|
265
352
|
finalHeaders.set("content-type", "text/html");
|
|
266
353
|
const finalStatus = status ?? this.response.status;
|
|
267
|
-
|
|
354
|
+
this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
|
|
355
|
+
return this._finalResponse;
|
|
268
356
|
}
|
|
269
357
|
/**
|
|
270
358
|
* Respond with a redirect
|
|
@@ -272,7 +360,8 @@ class ShokupanContext {
|
|
|
272
360
|
redirect(url, status = 302) {
|
|
273
361
|
const headers = this.mergeHeaders();
|
|
274
362
|
headers.set("Location", url);
|
|
275
|
-
|
|
363
|
+
this._finalResponse = new Response(null, { status, headers });
|
|
364
|
+
return this._finalResponse;
|
|
276
365
|
}
|
|
277
366
|
/**
|
|
278
367
|
* Respond with a status code
|
|
@@ -280,7 +369,8 @@ class ShokupanContext {
|
|
|
280
369
|
*/
|
|
281
370
|
status(status) {
|
|
282
371
|
const headers = this.mergeHeaders();
|
|
283
|
-
|
|
372
|
+
this._finalResponse = new Response(null, { status, headers });
|
|
373
|
+
return this._finalResponse;
|
|
284
374
|
}
|
|
285
375
|
/**
|
|
286
376
|
* Respond with a file
|
|
@@ -288,7 +378,8 @@ class ShokupanContext {
|
|
|
288
378
|
file(path2, fileOptions, responseOptions) {
|
|
289
379
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
290
380
|
const status = responseOptions?.status ?? this.response.status;
|
|
291
|
-
|
|
381
|
+
this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
|
|
382
|
+
return this._finalResponse;
|
|
292
383
|
}
|
|
293
384
|
/**
|
|
294
385
|
* JSX Rendering Function
|
|
@@ -437,69 +528,29 @@ function Inject(token) {
|
|
|
437
528
|
});
|
|
438
529
|
};
|
|
439
530
|
}
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
return result;
|
|
454
|
-
} catch (err) {
|
|
455
|
-
span.recordException(err);
|
|
456
|
-
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
457
|
-
throw err;
|
|
458
|
-
} finally {
|
|
459
|
-
span.end();
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
function traceHandler(fn, name) {
|
|
465
|
-
return async function(...args) {
|
|
466
|
-
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
467
|
-
kind: api.SpanKind.INTERNAL,
|
|
468
|
-
attributes: {
|
|
469
|
-
"http.route": name,
|
|
470
|
-
"component": "shokupan.route"
|
|
531
|
+
const compose = (middleware) => {
|
|
532
|
+
if (!middleware.length) {
|
|
533
|
+
return (context, next) => {
|
|
534
|
+
return next ? next() : Promise.resolve();
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
return function dispatch(context, next) {
|
|
538
|
+
let index = -1;
|
|
539
|
+
function runner(i) {
|
|
540
|
+
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
541
|
+
index = i;
|
|
542
|
+
if (i >= middleware.length) {
|
|
543
|
+
return next ? next() : Promise.resolve();
|
|
471
544
|
}
|
|
472
|
-
|
|
545
|
+
const fn = middleware[i];
|
|
473
546
|
try {
|
|
474
|
-
|
|
475
|
-
return result;
|
|
547
|
+
return Promise.resolve(fn(context, () => runner(i + 1)));
|
|
476
548
|
} catch (err) {
|
|
477
|
-
|
|
478
|
-
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
479
|
-
throw err;
|
|
480
|
-
} finally {
|
|
481
|
-
span.end();
|
|
549
|
+
return Promise.reject(err);
|
|
482
550
|
}
|
|
483
|
-
});
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
const compose = (middleware) => {
|
|
487
|
-
function fn(context, next) {
|
|
488
|
-
let runner = next || (async () => {
|
|
489
|
-
});
|
|
490
|
-
for (let i = middleware.length - 1; i >= 0; i--) {
|
|
491
|
-
const fn2 = traceMiddleware(middleware[i]);
|
|
492
|
-
const nextStep = runner;
|
|
493
|
-
let called = false;
|
|
494
|
-
runner = async () => {
|
|
495
|
-
if (called) throw new Error("next() called multiple times");
|
|
496
|
-
called = true;
|
|
497
|
-
return fn2(context, nextStep);
|
|
498
|
-
};
|
|
499
551
|
}
|
|
500
|
-
return runner();
|
|
501
|
-
}
|
|
502
|
-
return fn;
|
|
552
|
+
return runner(0);
|
|
553
|
+
};
|
|
503
554
|
};
|
|
504
555
|
class ShokupanRequestBase {
|
|
505
556
|
method;
|
|
@@ -726,7 +777,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
726
777
|
const defaultTagName = options.defaultTag || "Application";
|
|
727
778
|
let astRoutes = [];
|
|
728
779
|
try {
|
|
729
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-
|
|
780
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-BN0wFCML.cjs"));
|
|
730
781
|
const analyzer = new OpenAPIAnalyzer(process.cwd());
|
|
731
782
|
const { applications } = await analyzer.analyze();
|
|
732
783
|
const appMap = /* @__PURE__ */ new Map();
|
|
@@ -1126,6 +1177,29 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1126
1177
|
};
|
|
1127
1178
|
}
|
|
1128
1179
|
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
1180
|
+
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
1181
|
+
function traceHandler(fn, name) {
|
|
1182
|
+
return async function(...args) {
|
|
1183
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
1184
|
+
kind: api.SpanKind.INTERNAL,
|
|
1185
|
+
attributes: {
|
|
1186
|
+
"http.route": name,
|
|
1187
|
+
"component": "shokupan.route"
|
|
1188
|
+
}
|
|
1189
|
+
}, async (span) => {
|
|
1190
|
+
try {
|
|
1191
|
+
const result = await fn.apply(this, args);
|
|
1192
|
+
return result;
|
|
1193
|
+
} catch (err) {
|
|
1194
|
+
span.recordException(err);
|
|
1195
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
1196
|
+
throw err;
|
|
1197
|
+
} finally {
|
|
1198
|
+
span.end();
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1129
1203
|
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1130
1204
|
const ShokupanApplicationTree = {};
|
|
1131
1205
|
class ShokupanRouter {
|
|
@@ -1316,7 +1390,7 @@ class ShokupanRouter {
|
|
|
1316
1390
|
}
|
|
1317
1391
|
}
|
|
1318
1392
|
}
|
|
1319
|
-
const tracedOriginalHandler = traceHandler(originalHandler, normalizedPath);
|
|
1393
|
+
const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
|
|
1320
1394
|
return tracedOriginalHandler.apply(instance, args);
|
|
1321
1395
|
};
|
|
1322
1396
|
let finalHandler = wrappedHandler;
|
|
@@ -1452,6 +1526,7 @@ class ShokupanRouter {
|
|
|
1452
1526
|
applyHooks(match) {
|
|
1453
1527
|
if (!this.config?.hooks) return match;
|
|
1454
1528
|
const hooks = this.config.hooks;
|
|
1529
|
+
if (!hooks.onRequestStart && !hooks.onRequestEnd && !hooks.onError) return match;
|
|
1455
1530
|
const originalHandler = match.handler;
|
|
1456
1531
|
match.handler = async (ctx) => {
|
|
1457
1532
|
if (hooks.onRequestStart) await hooks.onRequestStart(ctx);
|
|
@@ -1480,16 +1555,25 @@ class ShokupanRouter {
|
|
|
1480
1555
|
* @returns Route handler and parameters if found, otherwise null
|
|
1481
1556
|
*/
|
|
1482
1557
|
find(method, path2) {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1558
|
+
const findInRoutes = (routes, m) => {
|
|
1559
|
+
for (const route of routes) {
|
|
1560
|
+
if (route.method !== "ALL" && route.method !== m) continue;
|
|
1561
|
+
const match = route.regex.exec(path2);
|
|
1562
|
+
if (match) {
|
|
1563
|
+
const params = {};
|
|
1564
|
+
route.keys.forEach((key, index) => {
|
|
1565
|
+
params[key] = match[index + 1];
|
|
1566
|
+
});
|
|
1567
|
+
return this.applyHooks({ handler: route.handler, params });
|
|
1568
|
+
}
|
|
1492
1569
|
}
|
|
1570
|
+
return null;
|
|
1571
|
+
};
|
|
1572
|
+
let result = findInRoutes(this[$routes], method);
|
|
1573
|
+
if (result) return result;
|
|
1574
|
+
if (method === "HEAD") {
|
|
1575
|
+
result = findInRoutes(this[$routes], "GET");
|
|
1576
|
+
if (result) return result;
|
|
1493
1577
|
}
|
|
1494
1578
|
for (const child of this[$childRouters]) {
|
|
1495
1579
|
const prefix = child[$mountPath];
|
|
@@ -1582,6 +1666,35 @@ class ShokupanRouter {
|
|
|
1582
1666
|
return innerHandler(ctx);
|
|
1583
1667
|
};
|
|
1584
1668
|
}
|
|
1669
|
+
let file = "unknown";
|
|
1670
|
+
let line = 0;
|
|
1671
|
+
try {
|
|
1672
|
+
const err = new Error();
|
|
1673
|
+
const stack = err.stack?.split("\n") || [];
|
|
1674
|
+
const callerLine = stack.find(
|
|
1675
|
+
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
1676
|
+
);
|
|
1677
|
+
if (callerLine) {
|
|
1678
|
+
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1679
|
+
if (match) {
|
|
1680
|
+
file = match[1];
|
|
1681
|
+
line = parseInt(match[2], 10);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
} catch (e) {
|
|
1685
|
+
}
|
|
1686
|
+
const trackedHandler = wrappedHandler;
|
|
1687
|
+
wrappedHandler = async (ctx) => {
|
|
1688
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1689
|
+
ctx.handlerStack.push({
|
|
1690
|
+
name: handler.name || "anonymous",
|
|
1691
|
+
file,
|
|
1692
|
+
line
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
return trackedHandler(ctx);
|
|
1696
|
+
};
|
|
1697
|
+
wrappedHandler.originalHandler = trackedHandler.originalHandler || trackedHandler;
|
|
1585
1698
|
this[$routes].push({
|
|
1586
1699
|
method,
|
|
1587
1700
|
path: path2,
|
|
@@ -1627,7 +1740,35 @@ class ShokupanRouter {
|
|
|
1627
1740
|
guard(specOrHandler, handler) {
|
|
1628
1741
|
const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
|
|
1629
1742
|
const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
|
|
1630
|
-
|
|
1743
|
+
let file = "unknown";
|
|
1744
|
+
let line = 0;
|
|
1745
|
+
try {
|
|
1746
|
+
const err = new Error();
|
|
1747
|
+
const stack = err.stack?.split("\n") || [];
|
|
1748
|
+
const callerLine = stack.find(
|
|
1749
|
+
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
1750
|
+
);
|
|
1751
|
+
if (callerLine) {
|
|
1752
|
+
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1753
|
+
if (match) {
|
|
1754
|
+
file = match[1];
|
|
1755
|
+
line = parseInt(match[2], 10);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
} catch (e) {
|
|
1759
|
+
}
|
|
1760
|
+
const trackedGuard = async (ctx, next) => {
|
|
1761
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1762
|
+
ctx.handlerStack.push({
|
|
1763
|
+
name: guardHandler.name || "guard",
|
|
1764
|
+
file,
|
|
1765
|
+
line
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
return guardHandler(ctx, next);
|
|
1769
|
+
};
|
|
1770
|
+
trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
|
|
1771
|
+
this.currentGuards.push({ handler: trackedGuard, spec });
|
|
1631
1772
|
return this;
|
|
1632
1773
|
}
|
|
1633
1774
|
/**
|
|
@@ -1715,6 +1856,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
1715
1856
|
applicationConfig = {};
|
|
1716
1857
|
openApiSpec;
|
|
1717
1858
|
middleware = [];
|
|
1859
|
+
composedMiddleware;
|
|
1718
1860
|
get logger() {
|
|
1719
1861
|
return this.applicationConfig.logger;
|
|
1720
1862
|
}
|
|
@@ -1728,7 +1870,37 @@ class Shokupan extends ShokupanRouter {
|
|
|
1728
1870
|
* Adds middleware to the application.
|
|
1729
1871
|
*/
|
|
1730
1872
|
use(middleware) {
|
|
1731
|
-
|
|
1873
|
+
let trackedMiddleware = middleware;
|
|
1874
|
+
let file = "unknown";
|
|
1875
|
+
let line = 0;
|
|
1876
|
+
try {
|
|
1877
|
+
const err = new Error();
|
|
1878
|
+
const stack = err.stack?.split("\n") || [];
|
|
1879
|
+
const callerLine = stack.find(
|
|
1880
|
+
(l) => l.includes(":") && !l.includes("shokupan.ts") && !l.includes("router.ts") && // In case called from router?
|
|
1881
|
+
!l.includes("node_modules") && !l.includes("bun:main")
|
|
1882
|
+
);
|
|
1883
|
+
if (callerLine) {
|
|
1884
|
+
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1885
|
+
if (match) {
|
|
1886
|
+
file = match[1];
|
|
1887
|
+
line = parseInt(match[2], 10);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
} catch (e) {
|
|
1891
|
+
}
|
|
1892
|
+
trackedMiddleware = async (ctx, next) => {
|
|
1893
|
+
const c = ctx;
|
|
1894
|
+
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1895
|
+
c.handlerStack.push({
|
|
1896
|
+
name: middleware.name || "middleware",
|
|
1897
|
+
file,
|
|
1898
|
+
line
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
return middleware(ctx, next);
|
|
1902
|
+
};
|
|
1903
|
+
this.middleware.push(trackedMiddleware);
|
|
1732
1904
|
return this;
|
|
1733
1905
|
}
|
|
1734
1906
|
startupHooks = [];
|
|
@@ -1763,9 +1935,28 @@ class Shokupan extends ShokupanRouter {
|
|
|
1763
1935
|
development: this.applicationConfig.development,
|
|
1764
1936
|
fetch: this.fetch.bind(this),
|
|
1765
1937
|
reusePort: this.applicationConfig.reusePort,
|
|
1766
|
-
idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0
|
|
1938
|
+
idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0,
|
|
1939
|
+
websocket: {
|
|
1940
|
+
open(ws) {
|
|
1941
|
+
ws.data?.handler?.open?.(ws);
|
|
1942
|
+
},
|
|
1943
|
+
message(ws, message) {
|
|
1944
|
+
ws.data?.handler?.message?.(ws, message);
|
|
1945
|
+
},
|
|
1946
|
+
drain(ws) {
|
|
1947
|
+
ws.data?.handler?.drain?.(ws);
|
|
1948
|
+
},
|
|
1949
|
+
close(ws, code, reason) {
|
|
1950
|
+
ws.data?.handler?.close?.(ws, code, reason);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1767
1953
|
};
|
|
1768
|
-
|
|
1954
|
+
let factory = this.applicationConfig.serverFactory;
|
|
1955
|
+
if (!factory && typeof Bun === "undefined") {
|
|
1956
|
+
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-BD6oKEto.cjs"));
|
|
1957
|
+
factory = createHttpServer();
|
|
1958
|
+
}
|
|
1959
|
+
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
1769
1960
|
console.log(`Shokupan server listening on http://${server.hostname}:${server.port}`);
|
|
1770
1961
|
return server;
|
|
1771
1962
|
}
|
|
@@ -1820,108 +2011,118 @@ class Shokupan extends ShokupanRouter {
|
|
|
1820
2011
|
* @returns The response to send.
|
|
1821
2012
|
*/
|
|
1822
2013
|
async fetch(req, server) {
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2014
|
+
if (this.applicationConfig.enableTracing) {
|
|
2015
|
+
const tracer2 = api.trace.getTracer("shokupan.application");
|
|
2016
|
+
const store = asyncContext.getStore();
|
|
2017
|
+
const attrs = {
|
|
2018
|
+
attributes: {
|
|
2019
|
+
"http.url": req.url,
|
|
2020
|
+
"http.method": req.method
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
const parent = store?.get("span");
|
|
2024
|
+
const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
|
|
2025
|
+
return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
|
|
2026
|
+
const ctxMap = /* @__PURE__ */ new Map();
|
|
2027
|
+
ctxMap.set("span", span);
|
|
2028
|
+
ctxMap.set("request", req);
|
|
2029
|
+
return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
if (this.applicationConfig.enableAsyncLocalStorage) {
|
|
1834
2033
|
const ctxMap = /* @__PURE__ */ new Map();
|
|
1835
|
-
ctxMap.set("span", span);
|
|
1836
2034
|
ctxMap.set("request", req);
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
if (result instanceof Response) {
|
|
1856
|
-
response = result;
|
|
1857
|
-
} else if (result === null || result === void 0) {
|
|
1858
|
-
span.setAttribute("http.status_code", 404);
|
|
1859
|
-
response = ctx2.text("Not Found", 404);
|
|
1860
|
-
} else if (typeof result === "object") {
|
|
1861
|
-
response = ctx2.json(result);
|
|
1862
|
-
} else {
|
|
1863
|
-
response = ctx2.text(String(result));
|
|
1864
|
-
}
|
|
1865
|
-
if (this.applicationConfig.hooks?.onRequestEnd) {
|
|
1866
|
-
await this.applicationConfig.hooks.onRequestEnd(ctx2);
|
|
1867
|
-
}
|
|
1868
|
-
if (this.applicationConfig.hooks?.onResponseStart) {
|
|
1869
|
-
await this.applicationConfig.hooks.onResponseStart(ctx2, response);
|
|
1870
|
-
}
|
|
1871
|
-
return response;
|
|
1872
|
-
} catch (err) {
|
|
1873
|
-
console.error(err);
|
|
1874
|
-
span.recordException(err);
|
|
1875
|
-
span.setStatus({ code: 2 });
|
|
1876
|
-
const status = err.status || err.statusCode || 500;
|
|
1877
|
-
const body = { error: err.message || "Internal Server Error" };
|
|
1878
|
-
if (err.errors) body.errors = err.errors;
|
|
1879
|
-
if (this.applicationConfig.hooks?.onError) {
|
|
1880
|
-
try {
|
|
1881
|
-
await this.applicationConfig.hooks.onError(err, ctx2);
|
|
1882
|
-
} catch (hookErr) {
|
|
1883
|
-
console.error("Error in onError hook:", hookErr);
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
return ctx2.json(body, status);
|
|
2035
|
+
return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
|
|
2036
|
+
}
|
|
2037
|
+
return this.handleRequest(req, server);
|
|
2038
|
+
}
|
|
2039
|
+
async handleRequest(req, server) {
|
|
2040
|
+
const request = req;
|
|
2041
|
+
const ctx = new ShokupanContext(request, server, void 0, this, this.applicationConfig.enableMiddlewareTracking);
|
|
2042
|
+
const handle = async () => {
|
|
2043
|
+
try {
|
|
2044
|
+
if (this.applicationConfig.hooks?.onRequestStart) {
|
|
2045
|
+
await this.applicationConfig.hooks.onRequestStart(ctx);
|
|
2046
|
+
}
|
|
2047
|
+
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2048
|
+
const result = await fn(ctx, async () => {
|
|
2049
|
+
const match = this.find(req.method, ctx.path);
|
|
2050
|
+
if (match) {
|
|
2051
|
+
ctx.params = match.params;
|
|
2052
|
+
return match.handler(ctx);
|
|
1887
2053
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
if (
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2054
|
+
return null;
|
|
2055
|
+
});
|
|
2056
|
+
let response;
|
|
2057
|
+
if (result instanceof Response) {
|
|
2058
|
+
response = result;
|
|
2059
|
+
} else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
|
|
2060
|
+
response = ctx._finalResponse;
|
|
2061
|
+
} else if ((result === null || result === void 0) && ctx.response.status === 404) {
|
|
2062
|
+
const span = asyncContext.getStore()?.get("span");
|
|
2063
|
+
if (span) span.setAttribute("http.status_code", 404);
|
|
2064
|
+
response = ctx.text("Not Found", 404);
|
|
2065
|
+
} else if (result === null || result === void 0) {
|
|
2066
|
+
if (ctx._finalResponse) response = ctx._finalResponse;
|
|
2067
|
+
else response = ctx.text("Not Found", 404);
|
|
2068
|
+
} else if (typeof result === "object") {
|
|
2069
|
+
response = ctx.json(result);
|
|
2070
|
+
} else {
|
|
2071
|
+
response = ctx.text(String(result));
|
|
2072
|
+
}
|
|
2073
|
+
if (this.applicationConfig.hooks?.onRequestEnd) {
|
|
2074
|
+
await this.applicationConfig.hooks.onRequestEnd(ctx);
|
|
1906
2075
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2076
|
+
if (this.applicationConfig.hooks?.onResponseStart) {
|
|
2077
|
+
await this.applicationConfig.hooks.onResponseStart(ctx, response);
|
|
2078
|
+
}
|
|
2079
|
+
return response;
|
|
2080
|
+
} catch (err) {
|
|
2081
|
+
console.error(err);
|
|
2082
|
+
const span = asyncContext.getStore()?.get("span");
|
|
2083
|
+
if (span) span.setStatus({ code: 2 });
|
|
2084
|
+
const status = err.status || err.statusCode || 500;
|
|
2085
|
+
const body = { error: err.message || "Internal Server Error" };
|
|
2086
|
+
if (err.errors) body.errors = err.errors;
|
|
2087
|
+
if (this.applicationConfig.hooks?.onError) {
|
|
2088
|
+
try {
|
|
2089
|
+
await this.applicationConfig.hooks.onError(err, ctx);
|
|
2090
|
+
} catch (hookErr) {
|
|
2091
|
+
console.error("Error in onError hook:", hookErr);
|
|
1910
2092
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2093
|
+
}
|
|
2094
|
+
return ctx.json(body, status);
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
let executionPromise = handle();
|
|
2098
|
+
const timeoutMs = this.applicationConfig.requestTimeout;
|
|
2099
|
+
if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
|
|
2100
|
+
let timeoutId;
|
|
2101
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2102
|
+
timeoutId = setTimeout(async () => {
|
|
2103
|
+
try {
|
|
2104
|
+
if (this.applicationConfig.hooks?.onRequestTimeout) {
|
|
2105
|
+
await this.applicationConfig.hooks.onRequestTimeout(ctx);
|
|
2106
|
+
}
|
|
2107
|
+
} catch (e) {
|
|
2108
|
+
console.error("Error in onRequestTimeout hook:", e);
|
|
1916
2109
|
}
|
|
1917
|
-
|
|
1918
|
-
}
|
|
1919
|
-
};
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2110
|
+
reject(new Error("Request Timeout"));
|
|
2111
|
+
}, timeoutMs);
|
|
2112
|
+
});
|
|
2113
|
+
executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
2114
|
+
}
|
|
2115
|
+
return executionPromise.catch((err) => {
|
|
2116
|
+
if (err.message === "Request Timeout") {
|
|
2117
|
+
return ctx.text("Request Timeout", 408);
|
|
2118
|
+
}
|
|
2119
|
+
console.error("Unexpected error in request execution:", err);
|
|
2120
|
+
return ctx.text("Internal Server Error", 500);
|
|
2121
|
+
}).then(async (res) => {
|
|
2122
|
+
if (this.applicationConfig.hooks?.onResponseEnd) {
|
|
2123
|
+
await this.applicationConfig.hooks.onResponseEnd(ctx, res);
|
|
1924
2124
|
}
|
|
2125
|
+
return res;
|
|
1925
2126
|
});
|
|
1926
2127
|
}
|
|
1927
2128
|
}
|
|
@@ -2148,15 +2349,19 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2148
2349
|
}
|
|
2149
2350
|
}
|
|
2150
2351
|
function Compression(options = {}) {
|
|
2151
|
-
const threshold = options.threshold ??
|
|
2352
|
+
const threshold = options.threshold ?? 512;
|
|
2152
2353
|
return async (ctx, next) => {
|
|
2153
2354
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
2154
2355
|
let method = null;
|
|
2155
2356
|
if (acceptEncoding.includes("br")) method = "br";
|
|
2357
|
+
else if (acceptEncoding.includes("zstd")) method = "zstd";
|
|
2156
2358
|
else if (acceptEncoding.includes("gzip")) method = "gzip";
|
|
2157
2359
|
else if (acceptEncoding.includes("deflate")) method = "deflate";
|
|
2158
2360
|
if (!method) return next();
|
|
2159
|
-
|
|
2361
|
+
let response = await next();
|
|
2362
|
+
if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
|
|
2363
|
+
response = ctx._finalResponse;
|
|
2364
|
+
}
|
|
2160
2365
|
if (response instanceof Response) {
|
|
2161
2366
|
if (response.headers.has("Content-Encoding")) return response;
|
|
2162
2367
|
const body = await response.arrayBuffer();
|
|
@@ -2168,17 +2373,31 @@ function Compression(options = {}) {
|
|
|
2168
2373
|
});
|
|
2169
2374
|
}
|
|
2170
2375
|
let compressed;
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2376
|
+
switch (method) {
|
|
2377
|
+
case "br":
|
|
2378
|
+
const zlib = require("node:zlib");
|
|
2379
|
+
compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
|
|
2380
|
+
params: {
|
|
2381
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 4
|
|
2382
|
+
}
|
|
2383
|
+
}, (err, data) => {
|
|
2384
|
+
if (err) return rej(err);
|
|
2385
|
+
res(data);
|
|
2386
|
+
}));
|
|
2387
|
+
break;
|
|
2388
|
+
case "gzip":
|
|
2389
|
+
compressed = Bun.gzipSync(body);
|
|
2390
|
+
break;
|
|
2391
|
+
case "zstd":
|
|
2392
|
+
compressed = await Bun.zstdCompress(body);
|
|
2393
|
+
break;
|
|
2394
|
+
default:
|
|
2395
|
+
compressed = Bun.deflateSync(body);
|
|
2396
|
+
break;
|
|
2177
2397
|
}
|
|
2178
2398
|
const headers = new Headers(response.headers);
|
|
2179
2399
|
headers.set("Content-Encoding", method);
|
|
2180
2400
|
headers.set("Content-Length", String(compressed.length));
|
|
2181
|
-
headers.delete("Content-Length");
|
|
2182
2401
|
return new Response(compressed, {
|
|
2183
2402
|
status: response.status,
|
|
2184
2403
|
statusText: response.statusText,
|
|
@@ -2319,72 +2538,407 @@ function useExpress(expressMiddleware) {
|
|
|
2319
2538
|
});
|
|
2320
2539
|
};
|
|
2321
2540
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
const statusCode = options.statusCode || 429;
|
|
2327
|
-
const headers = options.headers !== false;
|
|
2328
|
-
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
2329
|
-
return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
|
|
2330
|
-
});
|
|
2331
|
-
const skip = options.skip || (() => false);
|
|
2332
|
-
const hits = /* @__PURE__ */ new Map();
|
|
2333
|
-
const interval = setInterval(() => {
|
|
2334
|
-
const now = Date.now();
|
|
2335
|
-
for (const [key, record] of hits.entries()) {
|
|
2336
|
-
if (record.resetTime <= now) {
|
|
2337
|
-
hits.delete(key);
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2340
|
-
}, windowMs);
|
|
2341
|
-
if (interval.unref) interval.unref();
|
|
2342
|
-
return async (ctx, next) => {
|
|
2343
|
-
if (skip(ctx)) return next();
|
|
2344
|
-
const key = keyGenerator(ctx);
|
|
2345
|
-
const now = Date.now();
|
|
2346
|
-
let record = hits.get(key);
|
|
2347
|
-
if (!record || record.resetTime <= now) {
|
|
2348
|
-
record = {
|
|
2349
|
-
hits: 0,
|
|
2350
|
-
resetTime: now + windowMs
|
|
2351
|
-
};
|
|
2352
|
-
hits.set(key, record);
|
|
2353
|
-
}
|
|
2354
|
-
record.hits++;
|
|
2355
|
-
const remaining = Math.max(0, max - record.hits);
|
|
2356
|
-
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
2357
|
-
if (record.hits > max) {
|
|
2358
|
-
if (headers) {
|
|
2359
|
-
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2360
|
-
res.headers.set("X-RateLimit-Limit", String(max));
|
|
2361
|
-
res.headers.set("X-RateLimit-Remaining", "0");
|
|
2362
|
-
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2363
|
-
return res;
|
|
2364
|
-
}
|
|
2365
|
-
return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2366
|
-
}
|
|
2367
|
-
const response = await next();
|
|
2368
|
-
if (response instanceof Response && headers) {
|
|
2369
|
-
response.headers.set("X-RateLimit-Limit", String(max));
|
|
2370
|
-
response.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
2371
|
-
response.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2372
|
-
}
|
|
2373
|
-
return response;
|
|
2374
|
-
};
|
|
2375
|
-
}
|
|
2376
|
-
const eta = new eta$2.Eta();
|
|
2377
|
-
class ScalarPlugin extends ShokupanRouter {
|
|
2378
|
-
constructor(pluginOptions) {
|
|
2379
|
-
super();
|
|
2380
|
-
this.pluginOptions = pluginOptions;
|
|
2381
|
-
this.init();
|
|
2541
|
+
class ValidationError extends Error {
|
|
2542
|
+
constructor(errors) {
|
|
2543
|
+
super("Validation Error");
|
|
2544
|
+
this.errors = errors;
|
|
2382
2545
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2546
|
+
status = 400;
|
|
2547
|
+
}
|
|
2548
|
+
function isZod(schema) {
|
|
2549
|
+
return typeof schema?.safeParse === "function";
|
|
2550
|
+
}
|
|
2551
|
+
async function validateZod(schema, data) {
|
|
2552
|
+
const result = await schema.safeParseAsync(data);
|
|
2553
|
+
if (!result.success) {
|
|
2554
|
+
throw new ValidationError(result.error.errors);
|
|
2555
|
+
}
|
|
2556
|
+
return result.data;
|
|
2557
|
+
}
|
|
2558
|
+
function isTypeBox(schema) {
|
|
2559
|
+
return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
|
|
2560
|
+
}
|
|
2561
|
+
function validateTypeBox(schema, data) {
|
|
2562
|
+
if (!schema.Check(data)) {
|
|
2563
|
+
throw new ValidationError([...schema.Errors(data)]);
|
|
2564
|
+
}
|
|
2565
|
+
return data;
|
|
2566
|
+
}
|
|
2567
|
+
function isAjv(schema) {
|
|
2568
|
+
return typeof schema === "function" && "errors" in schema;
|
|
2569
|
+
}
|
|
2570
|
+
function validateAjv(schema, data) {
|
|
2571
|
+
const valid = schema(data);
|
|
2572
|
+
if (!valid) {
|
|
2573
|
+
throw new ValidationError(schema.errors);
|
|
2574
|
+
}
|
|
2575
|
+
return data;
|
|
2576
|
+
}
|
|
2577
|
+
const valibot = (schema, parser) => {
|
|
2578
|
+
return {
|
|
2579
|
+
_valibot: true,
|
|
2580
|
+
schema,
|
|
2581
|
+
parser
|
|
2582
|
+
};
|
|
2583
|
+
};
|
|
2584
|
+
function isValibotWrapper(schema) {
|
|
2585
|
+
return schema?._valibot === true;
|
|
2586
|
+
}
|
|
2587
|
+
async function validateValibotWrapper(wrapper, data) {
|
|
2588
|
+
const result = await wrapper.parser(wrapper.schema, data);
|
|
2589
|
+
if (!result.success) {
|
|
2590
|
+
throw new ValidationError(result.issues);
|
|
2591
|
+
}
|
|
2592
|
+
return result.output;
|
|
2593
|
+
}
|
|
2594
|
+
function isClass(schema) {
|
|
2595
|
+
try {
|
|
2596
|
+
if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
|
|
2597
|
+
return true;
|
|
2598
|
+
}
|
|
2599
|
+
return typeof schema === "function" && schema.prototype && schema.name;
|
|
2600
|
+
} catch {
|
|
2601
|
+
return false;
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
async function validateClassValidator(schema, data) {
|
|
2605
|
+
const object = classTransformer.plainToInstance(schema, data);
|
|
2606
|
+
try {
|
|
2607
|
+
await classValidator.validateOrReject(object);
|
|
2608
|
+
return object;
|
|
2609
|
+
} catch (errors) {
|
|
2610
|
+
const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
|
|
2611
|
+
property: err.property,
|
|
2612
|
+
constraints: err.constraints,
|
|
2613
|
+
children: err.children
|
|
2614
|
+
})) : errors;
|
|
2615
|
+
throw new ValidationError(formattedErrors);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
const safelyGetBody = async (ctx) => {
|
|
2619
|
+
const req = ctx.req;
|
|
2620
|
+
if (req._bodyParsed) {
|
|
2621
|
+
return req._bodyValue;
|
|
2622
|
+
}
|
|
2623
|
+
try {
|
|
2624
|
+
let data;
|
|
2625
|
+
if (typeof req.json === "function") {
|
|
2626
|
+
data = await req.json();
|
|
2627
|
+
} else {
|
|
2628
|
+
data = req.body;
|
|
2629
|
+
if (typeof data === "string") {
|
|
2630
|
+
try {
|
|
2631
|
+
data = JSON.parse(data);
|
|
2632
|
+
} catch {
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
req._bodyParsed = true;
|
|
2637
|
+
req._bodyValue = data;
|
|
2638
|
+
Object.defineProperty(req, "json", {
|
|
2639
|
+
value: async () => req._bodyValue,
|
|
2640
|
+
configurable: true
|
|
2641
|
+
});
|
|
2642
|
+
return data;
|
|
2643
|
+
} catch (e) {
|
|
2644
|
+
return {};
|
|
2645
|
+
}
|
|
2646
|
+
};
|
|
2647
|
+
function validate(config) {
|
|
2648
|
+
return async (ctx, next) => {
|
|
2649
|
+
const dataToValidate = {};
|
|
2650
|
+
if (config.params) dataToValidate.params = ctx.params;
|
|
2651
|
+
let queryObj;
|
|
2652
|
+
if (config.query) {
|
|
2653
|
+
const url = new URL(ctx.req.url);
|
|
2654
|
+
queryObj = Object.fromEntries(url.searchParams.entries());
|
|
2655
|
+
dataToValidate.query = queryObj;
|
|
2656
|
+
}
|
|
2657
|
+
if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
|
|
2658
|
+
let body;
|
|
2659
|
+
if (config.body) {
|
|
2660
|
+
body = await safelyGetBody(ctx);
|
|
2661
|
+
dataToValidate.body = body;
|
|
2662
|
+
}
|
|
2663
|
+
if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
|
|
2664
|
+
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
2665
|
+
}
|
|
2666
|
+
if (config.params) {
|
|
2667
|
+
ctx.params = await runValidation(config.params, ctx.params);
|
|
2668
|
+
}
|
|
2669
|
+
let validQuery;
|
|
2670
|
+
if (config.query && queryObj) {
|
|
2671
|
+
validQuery = await runValidation(config.query, queryObj);
|
|
2672
|
+
}
|
|
2673
|
+
if (config.headers) {
|
|
2674
|
+
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
2675
|
+
await runValidation(config.headers, headersObj);
|
|
2676
|
+
}
|
|
2677
|
+
let validBody;
|
|
2678
|
+
if (config.body) {
|
|
2679
|
+
const b = body ?? await safelyGetBody(ctx);
|
|
2680
|
+
validBody = await runValidation(config.body, b);
|
|
2681
|
+
const req = ctx.req;
|
|
2682
|
+
req._bodyValue = validBody;
|
|
2683
|
+
Object.defineProperty(req, "json", {
|
|
2684
|
+
value: async () => validBody,
|
|
2685
|
+
configurable: true
|
|
2686
|
+
});
|
|
2687
|
+
ctx.body = validBody;
|
|
2688
|
+
}
|
|
2689
|
+
if (ctx.app?.applicationConfig.hooks?.afterValidate) {
|
|
2690
|
+
const validatedData = { ...dataToValidate };
|
|
2691
|
+
if (config.params) validatedData.params = ctx.params;
|
|
2692
|
+
if (config.query) validatedData.query = validQuery;
|
|
2693
|
+
if (config.body) validatedData.body = validBody;
|
|
2694
|
+
await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
|
|
2695
|
+
}
|
|
2696
|
+
return next();
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
async function runValidation(schema, data) {
|
|
2700
|
+
if (isZod(schema)) {
|
|
2701
|
+
return validateZod(schema, data);
|
|
2702
|
+
}
|
|
2703
|
+
if (isTypeBox(schema)) {
|
|
2704
|
+
return validateTypeBox(schema, data);
|
|
2705
|
+
}
|
|
2706
|
+
if (isAjv(schema)) {
|
|
2707
|
+
return validateAjv(schema, data);
|
|
2708
|
+
}
|
|
2709
|
+
if (isValibotWrapper(schema)) {
|
|
2710
|
+
return validateValibotWrapper(schema, data);
|
|
2711
|
+
}
|
|
2712
|
+
if (isClass(schema)) {
|
|
2713
|
+
return validateClassValidator(schema, data);
|
|
2714
|
+
}
|
|
2715
|
+
if (isTypeBox(schema)) {
|
|
2716
|
+
return validateTypeBox(schema, data);
|
|
2717
|
+
}
|
|
2718
|
+
if (isAjv(schema)) {
|
|
2719
|
+
return validateAjv(schema, data);
|
|
2720
|
+
}
|
|
2721
|
+
if (isValibotWrapper(schema)) {
|
|
2722
|
+
return validateValibotWrapper(schema, data);
|
|
2723
|
+
}
|
|
2724
|
+
if (typeof schema === "function") {
|
|
2725
|
+
return schema(data);
|
|
2726
|
+
}
|
|
2727
|
+
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
2728
|
+
}
|
|
2729
|
+
const ajv = new Ajv({ coerceTypes: true, allErrors: true });
|
|
2730
|
+
addFormats(ajv);
|
|
2731
|
+
const compiledValidators = /* @__PURE__ */ new WeakMap();
|
|
2732
|
+
function openApiValidator() {
|
|
2733
|
+
return async (ctx, next) => {
|
|
2734
|
+
const app = ctx.app;
|
|
2735
|
+
if (!app || !app.openApiSpec) {
|
|
2736
|
+
return next();
|
|
2737
|
+
}
|
|
2738
|
+
let cache = compiledValidators.get(app);
|
|
2739
|
+
if (!cache) {
|
|
2740
|
+
cache = compileValidators(app.openApiSpec);
|
|
2741
|
+
compiledValidators.set(app, cache);
|
|
2742
|
+
}
|
|
2743
|
+
const method = ctx.req.method.toLowerCase();
|
|
2744
|
+
let matchPath;
|
|
2745
|
+
if (cache.has(ctx.path)) {
|
|
2746
|
+
matchPath = ctx.path;
|
|
2747
|
+
} else {
|
|
2748
|
+
for (const specPath of cache.keys()) {
|
|
2749
|
+
const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
|
|
2750
|
+
const regex = new RegExp(regexStr);
|
|
2751
|
+
const match = regex.exec(ctx.path);
|
|
2752
|
+
if (match) {
|
|
2753
|
+
matchPath = specPath;
|
|
2754
|
+
break;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
if (!matchPath) {
|
|
2759
|
+
return next();
|
|
2760
|
+
}
|
|
2761
|
+
const validators = cache.get(matchPath)?.[method];
|
|
2762
|
+
if (!validators) {
|
|
2763
|
+
return next();
|
|
2764
|
+
}
|
|
2765
|
+
const errors = [];
|
|
2766
|
+
if (validators.body) {
|
|
2767
|
+
let body;
|
|
2768
|
+
try {
|
|
2769
|
+
body = await ctx.req.json().catch(() => ({}));
|
|
2770
|
+
} catch {
|
|
2771
|
+
body = {};
|
|
2772
|
+
}
|
|
2773
|
+
const valid = validators.body(body);
|
|
2774
|
+
if (!valid && validators.body.errors) {
|
|
2775
|
+
errors.push(...validators.body.errors.map((e) => ({ ...e, location: "body" })));
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
if (validators.query) {
|
|
2779
|
+
const query = Object.fromEntries(new URL(ctx.req.url).searchParams.entries());
|
|
2780
|
+
const valid = validators.query(query);
|
|
2781
|
+
if (!valid && validators.query.errors) {
|
|
2782
|
+
errors.push(...validators.query.errors.map((e) => ({ ...e, location: "query" })));
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
if (validators.params) {
|
|
2786
|
+
let params = ctx.params;
|
|
2787
|
+
if (Object.keys(params).length === 0 && matchPath) {
|
|
2788
|
+
const paramNames = (matchPath.match(/{([^}]+)}/g) || []).map((s) => s.slice(1, -1));
|
|
2789
|
+
if (paramNames.length > 0) {
|
|
2790
|
+
const regexStr = "^" + matchPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
|
|
2791
|
+
const regex = new RegExp(regexStr);
|
|
2792
|
+
const match = regex.exec(ctx.path);
|
|
2793
|
+
if (match) {
|
|
2794
|
+
params = {};
|
|
2795
|
+
paramNames.forEach((name, i) => {
|
|
2796
|
+
params[name] = match[i + 1];
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
const valid = validators.params(params);
|
|
2802
|
+
if (!valid && validators.params.errors) {
|
|
2803
|
+
errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
if (validators.headers) {
|
|
2807
|
+
const headers = Object.fromEntries(ctx.req.headers.entries());
|
|
2808
|
+
const valid = validators.headers(headers);
|
|
2809
|
+
if (!valid && validators.headers.errors) {
|
|
2810
|
+
errors.push(...validators.headers.errors.map((e) => ({ ...e, location: "header" })));
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
if (errors.length > 0) {
|
|
2814
|
+
throw new ValidationError(errors);
|
|
2815
|
+
}
|
|
2816
|
+
return next();
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
function compileValidators(spec) {
|
|
2820
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2821
|
+
for (const [path2, pathItem] of Object.entries(spec.paths || {})) {
|
|
2822
|
+
const pathValidators = {};
|
|
2823
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
2824
|
+
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
2825
|
+
const oper = operation;
|
|
2826
|
+
const validators = {};
|
|
2827
|
+
if (oper.requestBody?.content?.["application/json"]?.schema) {
|
|
2828
|
+
validators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
|
|
2829
|
+
}
|
|
2830
|
+
const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
|
|
2831
|
+
const queryProps = {};
|
|
2832
|
+
const pathProps = {};
|
|
2833
|
+
const headerProps = {};
|
|
2834
|
+
const queryRequired = [];
|
|
2835
|
+
const pathRequired = [];
|
|
2836
|
+
const headerRequired = [];
|
|
2837
|
+
for (const param of parameters) {
|
|
2838
|
+
if (param.in === "query") {
|
|
2839
|
+
queryProps[param.name] = param.schema || {};
|
|
2840
|
+
if (param.required) queryRequired.push(param.name);
|
|
2841
|
+
} else if (param.in === "path") {
|
|
2842
|
+
pathProps[param.name] = param.schema || {};
|
|
2843
|
+
pathRequired.push(param.name);
|
|
2844
|
+
} else if (param.in === "header") {
|
|
2845
|
+
headerProps[param.name] = param.schema || {};
|
|
2846
|
+
if (param.required) headerRequired.push(param.name);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
if (Object.keys(queryProps).length > 0) {
|
|
2850
|
+
validators.query = ajv.compile({
|
|
2851
|
+
type: "object",
|
|
2852
|
+
properties: queryProps,
|
|
2853
|
+
required: queryRequired.length > 0 ? queryRequired : void 0
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
if (Object.keys(pathProps).length > 0) {
|
|
2857
|
+
validators.params = ajv.compile({
|
|
2858
|
+
type: "object",
|
|
2859
|
+
properties: pathProps,
|
|
2860
|
+
required: pathRequired.length > 0 ? pathRequired : void 0
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2863
|
+
if (Object.keys(headerProps).length > 0) {
|
|
2864
|
+
validators.headers = ajv.compile({
|
|
2865
|
+
type: "object",
|
|
2866
|
+
properties: headerProps,
|
|
2867
|
+
required: headerRequired.length > 0 ? headerRequired : void 0
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
pathValidators[method] = validators;
|
|
2871
|
+
}
|
|
2872
|
+
cache.set(path2, pathValidators);
|
|
2873
|
+
}
|
|
2874
|
+
return cache;
|
|
2875
|
+
}
|
|
2876
|
+
function RateLimit(options = {}) {
|
|
2877
|
+
const windowMs = options.windowMs || 60 * 1e3;
|
|
2878
|
+
const max = options.max || 5;
|
|
2879
|
+
const message = options.message || "Too many requests, please try again later.";
|
|
2880
|
+
const statusCode = options.statusCode || 429;
|
|
2881
|
+
const headers = options.headers !== false;
|
|
2882
|
+
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
2883
|
+
return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
|
|
2884
|
+
});
|
|
2885
|
+
const skip = options.skip || (() => false);
|
|
2886
|
+
const hits = /* @__PURE__ */ new Map();
|
|
2887
|
+
const interval = setInterval(() => {
|
|
2888
|
+
const now = Date.now();
|
|
2889
|
+
for (const [key, record] of hits.entries()) {
|
|
2890
|
+
if (record.resetTime <= now) {
|
|
2891
|
+
hits.delete(key);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}, windowMs);
|
|
2895
|
+
interval.unref?.();
|
|
2896
|
+
return async (ctx, next) => {
|
|
2897
|
+
if (skip(ctx)) return next();
|
|
2898
|
+
const key = keyGenerator(ctx);
|
|
2899
|
+
const now = Date.now();
|
|
2900
|
+
let record = hits.get(key);
|
|
2901
|
+
if (!record || record.resetTime <= now) {
|
|
2902
|
+
record = {
|
|
2903
|
+
hits: 0,
|
|
2904
|
+
resetTime: now + windowMs
|
|
2905
|
+
};
|
|
2906
|
+
hits.set(key, record);
|
|
2907
|
+
}
|
|
2908
|
+
record.hits++;
|
|
2909
|
+
const remaining = Math.max(0, max - record.hits);
|
|
2910
|
+
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
2911
|
+
if (record.hits > max) {
|
|
2912
|
+
if (headers) {
|
|
2913
|
+
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2914
|
+
res.headers.set("X-RateLimit-Limit", String(max));
|
|
2915
|
+
res.headers.set("X-RateLimit-Remaining", "0");
|
|
2916
|
+
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2917
|
+
return res;
|
|
2918
|
+
}
|
|
2919
|
+
return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2920
|
+
}
|
|
2921
|
+
const response = await next();
|
|
2922
|
+
if (response instanceof Response && headers) {
|
|
2923
|
+
response.headers.set("X-RateLimit-Limit", String(max));
|
|
2924
|
+
response.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
2925
|
+
response.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2926
|
+
}
|
|
2927
|
+
return response;
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
const eta = new eta$2.Eta();
|
|
2931
|
+
class ScalarPlugin extends ShokupanRouter {
|
|
2932
|
+
constructor(pluginOptions) {
|
|
2933
|
+
super();
|
|
2934
|
+
this.pluginOptions = pluginOptions;
|
|
2935
|
+
this.init();
|
|
2936
|
+
}
|
|
2937
|
+
init() {
|
|
2938
|
+
this.get("/", (ctx) => {
|
|
2939
|
+
let path2 = ctx.url.toString();
|
|
2940
|
+
if (!path2.endsWith("/")) path2 += "/";
|
|
2941
|
+
return ctx.html(eta.renderString(`<!doctype html>
|
|
2388
2942
|
<html>
|
|
2389
2943
|
<head>
|
|
2390
2944
|
<title>API Reference</title>
|
|
@@ -2752,194 +3306,6 @@ function Session(options) {
|
|
|
2752
3306
|
return result;
|
|
2753
3307
|
};
|
|
2754
3308
|
}
|
|
2755
|
-
class ValidationError extends Error {
|
|
2756
|
-
constructor(errors) {
|
|
2757
|
-
super("Validation Error");
|
|
2758
|
-
this.errors = errors;
|
|
2759
|
-
}
|
|
2760
|
-
status = 400;
|
|
2761
|
-
}
|
|
2762
|
-
function isZod(schema) {
|
|
2763
|
-
return typeof schema?.safeParse === "function";
|
|
2764
|
-
}
|
|
2765
|
-
async function validateZod(schema, data) {
|
|
2766
|
-
const result = await schema.safeParseAsync(data);
|
|
2767
|
-
if (!result.success) {
|
|
2768
|
-
throw new ValidationError(result.error.errors);
|
|
2769
|
-
}
|
|
2770
|
-
return result.data;
|
|
2771
|
-
}
|
|
2772
|
-
function isTypeBox(schema) {
|
|
2773
|
-
return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
|
|
2774
|
-
}
|
|
2775
|
-
function validateTypeBox(schema, data) {
|
|
2776
|
-
if (!schema.Check(data)) {
|
|
2777
|
-
throw new ValidationError([...schema.Errors(data)]);
|
|
2778
|
-
}
|
|
2779
|
-
return data;
|
|
2780
|
-
}
|
|
2781
|
-
function isAjv(schema) {
|
|
2782
|
-
return typeof schema === "function" && "errors" in schema;
|
|
2783
|
-
}
|
|
2784
|
-
function validateAjv(schema, data) {
|
|
2785
|
-
const valid = schema(data);
|
|
2786
|
-
if (!valid) {
|
|
2787
|
-
throw new ValidationError(schema.errors);
|
|
2788
|
-
}
|
|
2789
|
-
return data;
|
|
2790
|
-
}
|
|
2791
|
-
const valibot = (schema, parser) => {
|
|
2792
|
-
return {
|
|
2793
|
-
_valibot: true,
|
|
2794
|
-
schema,
|
|
2795
|
-
parser
|
|
2796
|
-
};
|
|
2797
|
-
};
|
|
2798
|
-
function isValibotWrapper(schema) {
|
|
2799
|
-
return schema?._valibot === true;
|
|
2800
|
-
}
|
|
2801
|
-
async function validateValibotWrapper(wrapper, data) {
|
|
2802
|
-
const result = await wrapper.parser(wrapper.schema, data);
|
|
2803
|
-
if (!result.success) {
|
|
2804
|
-
throw new ValidationError(result.issues);
|
|
2805
|
-
}
|
|
2806
|
-
return result.output;
|
|
2807
|
-
}
|
|
2808
|
-
function isClass(schema) {
|
|
2809
|
-
try {
|
|
2810
|
-
if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
|
|
2811
|
-
return true;
|
|
2812
|
-
}
|
|
2813
|
-
return typeof schema === "function" && schema.prototype && schema.name;
|
|
2814
|
-
} catch {
|
|
2815
|
-
return false;
|
|
2816
|
-
}
|
|
2817
|
-
}
|
|
2818
|
-
async function validateClassValidator(schema, data) {
|
|
2819
|
-
const object = classTransformer.plainToInstance(schema, data);
|
|
2820
|
-
try {
|
|
2821
|
-
await classValidator.validateOrReject(object);
|
|
2822
|
-
return object;
|
|
2823
|
-
} catch (errors) {
|
|
2824
|
-
const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
|
|
2825
|
-
property: err.property,
|
|
2826
|
-
constraints: err.constraints,
|
|
2827
|
-
children: err.children
|
|
2828
|
-
})) : errors;
|
|
2829
|
-
throw new ValidationError(formattedErrors);
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
const safelyGetBody = async (ctx) => {
|
|
2833
|
-
const req = ctx.req;
|
|
2834
|
-
if (req._bodyParsed) {
|
|
2835
|
-
return req._bodyValue;
|
|
2836
|
-
}
|
|
2837
|
-
try {
|
|
2838
|
-
let data;
|
|
2839
|
-
if (typeof req.json === "function") {
|
|
2840
|
-
data = await req.json();
|
|
2841
|
-
} else {
|
|
2842
|
-
data = req.body;
|
|
2843
|
-
if (typeof data === "string") {
|
|
2844
|
-
try {
|
|
2845
|
-
data = JSON.parse(data);
|
|
2846
|
-
} catch {
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
req._bodyParsed = true;
|
|
2851
|
-
req._bodyValue = data;
|
|
2852
|
-
Object.defineProperty(req, "json", {
|
|
2853
|
-
value: async () => req._bodyValue,
|
|
2854
|
-
configurable: true
|
|
2855
|
-
});
|
|
2856
|
-
return data;
|
|
2857
|
-
} catch (e) {
|
|
2858
|
-
return {};
|
|
2859
|
-
}
|
|
2860
|
-
};
|
|
2861
|
-
function validate(config) {
|
|
2862
|
-
return async (ctx, next) => {
|
|
2863
|
-
const dataToValidate = {};
|
|
2864
|
-
if (config.params) dataToValidate.params = ctx.params;
|
|
2865
|
-
let queryObj;
|
|
2866
|
-
if (config.query) {
|
|
2867
|
-
const url = new URL(ctx.req.url);
|
|
2868
|
-
queryObj = Object.fromEntries(url.searchParams.entries());
|
|
2869
|
-
dataToValidate.query = queryObj;
|
|
2870
|
-
}
|
|
2871
|
-
if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
|
|
2872
|
-
let body;
|
|
2873
|
-
if (config.body) {
|
|
2874
|
-
body = await safelyGetBody(ctx);
|
|
2875
|
-
dataToValidate.body = body;
|
|
2876
|
-
}
|
|
2877
|
-
if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
|
|
2878
|
-
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
2879
|
-
}
|
|
2880
|
-
if (config.params) {
|
|
2881
|
-
ctx.params = await runValidation(config.params, ctx.params);
|
|
2882
|
-
}
|
|
2883
|
-
let validQuery;
|
|
2884
|
-
if (config.query && queryObj) {
|
|
2885
|
-
validQuery = await runValidation(config.query, queryObj);
|
|
2886
|
-
}
|
|
2887
|
-
if (config.headers) {
|
|
2888
|
-
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
2889
|
-
await runValidation(config.headers, headersObj);
|
|
2890
|
-
}
|
|
2891
|
-
let validBody;
|
|
2892
|
-
if (config.body) {
|
|
2893
|
-
const b = body ?? await safelyGetBody(ctx);
|
|
2894
|
-
validBody = await runValidation(config.body, b);
|
|
2895
|
-
const req = ctx.req;
|
|
2896
|
-
req._bodyValue = validBody;
|
|
2897
|
-
Object.defineProperty(req, "json", {
|
|
2898
|
-
value: async () => validBody,
|
|
2899
|
-
configurable: true
|
|
2900
|
-
});
|
|
2901
|
-
ctx.body = validBody;
|
|
2902
|
-
}
|
|
2903
|
-
if (ctx.app?.applicationConfig.hooks?.afterValidate) {
|
|
2904
|
-
const validatedData = { ...dataToValidate };
|
|
2905
|
-
if (config.params) validatedData.params = ctx.params;
|
|
2906
|
-
if (config.query) validatedData.query = validQuery;
|
|
2907
|
-
if (config.body) validatedData.body = validBody;
|
|
2908
|
-
await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
|
|
2909
|
-
}
|
|
2910
|
-
return next();
|
|
2911
|
-
};
|
|
2912
|
-
}
|
|
2913
|
-
async function runValidation(schema, data) {
|
|
2914
|
-
if (isZod(schema)) {
|
|
2915
|
-
return validateZod(schema, data);
|
|
2916
|
-
}
|
|
2917
|
-
if (isTypeBox(schema)) {
|
|
2918
|
-
return validateTypeBox(schema, data);
|
|
2919
|
-
}
|
|
2920
|
-
if (isAjv(schema)) {
|
|
2921
|
-
return validateAjv(schema, data);
|
|
2922
|
-
}
|
|
2923
|
-
if (isValibotWrapper(schema)) {
|
|
2924
|
-
return validateValibotWrapper(schema, data);
|
|
2925
|
-
}
|
|
2926
|
-
if (isClass(schema)) {
|
|
2927
|
-
return validateClassValidator(schema, data);
|
|
2928
|
-
}
|
|
2929
|
-
if (isTypeBox(schema)) {
|
|
2930
|
-
return validateTypeBox(schema, data);
|
|
2931
|
-
}
|
|
2932
|
-
if (isAjv(schema)) {
|
|
2933
|
-
return validateAjv(schema, data);
|
|
2934
|
-
}
|
|
2935
|
-
if (isValibotWrapper(schema)) {
|
|
2936
|
-
return validateValibotWrapper(schema, data);
|
|
2937
|
-
}
|
|
2938
|
-
if (typeof schema === "function") {
|
|
2939
|
-
return schema(data);
|
|
2940
|
-
}
|
|
2941
|
-
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
2942
|
-
}
|
|
2943
3309
|
exports.$appRoot = $appRoot;
|
|
2944
3310
|
exports.$childControllers = $childControllers;
|
|
2945
3311
|
exports.$childRouters = $childRouters;
|
|
@@ -2994,6 +3360,7 @@ exports.Spec = Spec;
|
|
|
2994
3360
|
exports.Use = Use;
|
|
2995
3361
|
exports.ValidationError = ValidationError;
|
|
2996
3362
|
exports.compose = compose;
|
|
3363
|
+
exports.openApiValidator = openApiValidator;
|
|
2997
3364
|
exports.useExpress = useExpress;
|
|
2998
3365
|
exports.valibot = valibot;
|
|
2999
3366
|
exports.validate = validate;
|