shokupan 0.2.0 → 0.4.4
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 +197 -24
- package/dist/benchmarking/advanced-cases/elysia.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/express.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/fastify.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/hapi.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/hono.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/koa.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/nest.d.ts +1 -0
- package/dist/benchmarking/advanced-cases/shokupan.d.ts +1 -0
- package/dist/benchmarking/advanced-data.d.ts +33 -0
- package/dist/benchmarking/advanced-runner.d.ts +1 -0
- package/dist/benchmarking/advanced-worker.d.ts +0 -0
- package/dist/benchmarking/cases/elysia.d.ts +1 -0
- package/dist/benchmarking/cases/express.d.ts +1 -0
- package/dist/benchmarking/cases/fastify.d.ts +1 -0
- package/dist/benchmarking/cases/hapi.d.ts +1 -0
- package/dist/benchmarking/cases/hono.d.ts +1 -0
- package/dist/benchmarking/cases/koa.d.ts +1 -0
- package/dist/benchmarking/cases/nest.d.ts +1 -0
- package/dist/benchmarking/cases/shokupan.d.ts +1 -0
- package/dist/benchmarking/data.d.ts +15 -0
- package/dist/benchmarking/quick_bench.d.ts +1 -0
- package/dist/benchmarking/runner.d.ts +1 -0
- package/dist/benchmarking/worker.d.ts +0 -0
- package/dist/buntest.d.ts +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +17 -7
- package/dist/decorators.d.ts +47 -0
- package/dist/index.cjs +939 -385
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +931 -379
- package/dist/index.js.map +1 -1
- package/dist/{openapi-analyzer-BTExMLX4.js → openapi-analyzer-BtIaHIfe.js} +11 -6
- package/dist/openapi-analyzer-BtIaHIfe.js.map +1 -0
- package/dist/{openapi-analyzer-BN0wFCML.cjs → openapi-analyzer-D9YB3IkV.cjs} +11 -6
- package/dist/openapi-analyzer-D9YB3IkV.cjs.map +1 -0
- package/dist/plugins/auth.d.ts +1 -1
- package/dist/plugins/debugview/plugin.d.ts +7 -12
- package/dist/plugins/failed-request-recorder.d.ts +14 -0
- package/dist/plugins/idempotency/plugin.d.ts +14 -0
- package/dist/plugins/openapi-validator.d.ts +28 -0
- package/dist/plugins/rate-limit.d.ts +3 -1
- package/dist/plugins/serve-static.d.ts +2 -3
- package/dist/router/trie.d.ts +14 -0
- package/dist/router.d.ts +60 -4
- package/dist/{server-adapter-CnQFr4P7.js → server-adapter-BWrEJbKL.js} +5 -5
- package/dist/server-adapter-BWrEJbKL.js.map +1 -0
- package/dist/{server-adapter-BD6oKEto.cjs → server-adapter-fVKP60e0.cjs} +5 -5
- package/dist/server-adapter-fVKP60e0.cjs.map +1 -0
- package/dist/shokupan.d.ts +14 -3
- package/dist/types.d.ts +100 -4
- package/dist/util/cpu-monitor.d.ts +11 -0
- package/dist/util/stack.d.ts +8 -0
- package/package.json +12 -5
- package/dist/openapi-analyzer-BN0wFCML.cjs.map +0 -1
- package/dist/openapi-analyzer-BTExMLX4.js.map +0 -1
- package/dist/server-adapter-BD6oKEto.cjs.map +0 -1
- package/dist/server-adapter-CnQFr4P7.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { Eta } from "eta";
|
|
2
|
-
import { stat, readdir } from "fs/promises";
|
|
3
|
+
import { stat, readdir, readFile as readFile$1 } from "fs/promises";
|
|
3
4
|
import { resolve, join, basename } from "path";
|
|
4
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
import { createNodeEngines } from "@surrealdb/node";
|
|
7
|
+
import { Surreal, RecordId } from "surrealdb";
|
|
5
8
|
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
9
|
+
import * as os from "node:os";
|
|
6
10
|
import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
|
|
7
11
|
import * as jose from "jose";
|
|
12
|
+
import * as zlib from "node:zlib";
|
|
8
13
|
import Ajv from "ajv";
|
|
9
14
|
import addFormats from "ajv-formats";
|
|
10
15
|
import { plainToInstance } from "class-transformer";
|
|
11
16
|
import { validateOrReject } from "class-validator";
|
|
12
|
-
import { OpenAPIAnalyzer } from "./openapi-analyzer-
|
|
17
|
+
import { OpenAPIAnalyzer } from "./openapi-analyzer-BtIaHIfe.js";
|
|
13
18
|
import { randomUUID, createHmac } from "crypto";
|
|
14
19
|
import { EventEmitter } from "events";
|
|
15
20
|
class ShokupanResponse {
|
|
@@ -76,10 +81,12 @@ class ShokupanResponse {
|
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
class ShokupanContext {
|
|
79
|
-
|
|
84
|
+
// Raw body for compression optimization
|
|
85
|
+
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
80
86
|
this.request = request;
|
|
81
87
|
this.server = server;
|
|
82
88
|
this.app = app;
|
|
89
|
+
this.signal = signal;
|
|
83
90
|
this.state = state || {};
|
|
84
91
|
if (enableMiddlewareTracking) {
|
|
85
92
|
const self = this;
|
|
@@ -103,7 +110,9 @@ class ShokupanContext {
|
|
|
103
110
|
state;
|
|
104
111
|
handlerStack = [];
|
|
105
112
|
response;
|
|
113
|
+
_debug;
|
|
106
114
|
_finalResponse;
|
|
115
|
+
_rawBody;
|
|
107
116
|
get url() {
|
|
108
117
|
if (!this._url) {
|
|
109
118
|
const urlString = this.request.url || "http://localhost/";
|
|
@@ -152,7 +161,17 @@ class ShokupanContext {
|
|
|
152
161
|
* Request query params
|
|
153
162
|
*/
|
|
154
163
|
get query() {
|
|
155
|
-
|
|
164
|
+
const q = {};
|
|
165
|
+
for (const [key, value] of this.url.searchParams) {
|
|
166
|
+
if (q[key] === void 0) {
|
|
167
|
+
q[key] = value;
|
|
168
|
+
} else if (Array.isArray(q[key])) {
|
|
169
|
+
q[key].push(value);
|
|
170
|
+
} else {
|
|
171
|
+
q[key] = [q[key], value];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return q;
|
|
156
175
|
}
|
|
157
176
|
/**
|
|
158
177
|
* Client IP address
|
|
@@ -227,17 +246,36 @@ class ShokupanContext {
|
|
|
227
246
|
setCookie(name, value, options = {}) {
|
|
228
247
|
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
229
248
|
if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
249
|
+
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
250
|
+
if (options.path) cookie += `; Path=${options.path || "/"}`;
|
|
230
251
|
if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
231
252
|
if (options.httpOnly) cookie += `; HttpOnly`;
|
|
232
253
|
if (options.secure) cookie += `; Secure`;
|
|
233
|
-
|
|
234
|
-
if (
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
254
|
+
let sameSite = options.sameSite;
|
|
255
|
+
if (sameSite === true) sameSite = "Strict";
|
|
256
|
+
if (sameSite === void 0 || sameSite === false) ;
|
|
257
|
+
else {
|
|
258
|
+
const stringSameSite = typeof sameSite === "string" ? sameSite.toLowerCase() : sameSite;
|
|
259
|
+
switch (stringSameSite) {
|
|
260
|
+
case "lax":
|
|
261
|
+
cookie += "; SameSite=Lax";
|
|
262
|
+
break;
|
|
263
|
+
case "strict":
|
|
264
|
+
cookie += "; SameSite=Strict";
|
|
265
|
+
break;
|
|
266
|
+
case "none":
|
|
267
|
+
cookie += "; SameSite=None";
|
|
268
|
+
break;
|
|
269
|
+
default:
|
|
270
|
+
cookie += "; SameSite=Lax";
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
238
273
|
}
|
|
239
274
|
if (options.priority) {
|
|
240
|
-
|
|
275
|
+
const p = options.priority.toLowerCase();
|
|
276
|
+
if (p === "low") cookie += "; Priority=Low";
|
|
277
|
+
else if (p === "medium") cookie += "; Priority=Medium";
|
|
278
|
+
else if (p === "high") cookie += "; Priority=High";
|
|
241
279
|
}
|
|
242
280
|
this.response.append("Set-Cookie", cookie);
|
|
243
281
|
return this;
|
|
@@ -274,6 +312,9 @@ class ShokupanContext {
|
|
|
274
312
|
send(body, options) {
|
|
275
313
|
const headers = this.mergeHeaders(options?.headers);
|
|
276
314
|
const status = options?.status ?? this.response.status;
|
|
315
|
+
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
316
|
+
this._rawBody = body;
|
|
317
|
+
}
|
|
277
318
|
this._finalResponse = new Response(body, { status, headers });
|
|
278
319
|
return this._finalResponse;
|
|
279
320
|
}
|
|
@@ -281,11 +322,11 @@ class ShokupanContext {
|
|
|
281
322
|
* Read request body
|
|
282
323
|
*/
|
|
283
324
|
async body() {
|
|
284
|
-
const contentType = this.request.headers.get("content-type");
|
|
285
|
-
if (contentType
|
|
325
|
+
const contentType = this.request.headers.get("content-type") || "";
|
|
326
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
286
327
|
return this.request.json();
|
|
287
328
|
}
|
|
288
|
-
if (contentType
|
|
329
|
+
if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
289
330
|
return this.request.formData();
|
|
290
331
|
}
|
|
291
332
|
return this.request.text();
|
|
@@ -296,6 +337,7 @@ class ShokupanContext {
|
|
|
296
337
|
json(data, status, headers) {
|
|
297
338
|
const finalStatus = status ?? this.response.status;
|
|
298
339
|
const jsonString = JSON.stringify(data);
|
|
340
|
+
this._rawBody = jsonString;
|
|
299
341
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
300
342
|
this._finalResponse = new Response(jsonString, {
|
|
301
343
|
status: finalStatus,
|
|
@@ -313,15 +355,16 @@ class ShokupanContext {
|
|
|
313
355
|
*/
|
|
314
356
|
text(data, status, headers) {
|
|
315
357
|
const finalStatus = status ?? this.response.status;
|
|
358
|
+
this._rawBody = data;
|
|
316
359
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
317
360
|
this._finalResponse = new Response(data, {
|
|
318
361
|
status: finalStatus,
|
|
319
|
-
headers: { "content-type": "text/plain" }
|
|
362
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
320
363
|
});
|
|
321
364
|
return this._finalResponse;
|
|
322
365
|
}
|
|
323
366
|
const finalHeaders = this.mergeHeaders(headers);
|
|
324
|
-
finalHeaders.set("content-type", "text/plain");
|
|
367
|
+
finalHeaders.set("content-type", "text/plain; charset=utf-8");
|
|
325
368
|
this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
|
|
326
369
|
return this._finalResponse;
|
|
327
370
|
}
|
|
@@ -329,9 +372,10 @@ class ShokupanContext {
|
|
|
329
372
|
* Respond with HTML content
|
|
330
373
|
*/
|
|
331
374
|
html(html, status, headers) {
|
|
332
|
-
const finalHeaders = this.mergeHeaders(headers);
|
|
333
|
-
finalHeaders.set("content-type", "text/html");
|
|
334
375
|
const finalStatus = status ?? this.response.status;
|
|
376
|
+
const finalHeaders = this.mergeHeaders(headers);
|
|
377
|
+
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
378
|
+
this._rawBody = html;
|
|
335
379
|
this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
|
|
336
380
|
return this._finalResponse;
|
|
337
381
|
}
|
|
@@ -356,11 +400,20 @@ class ShokupanContext {
|
|
|
356
400
|
/**
|
|
357
401
|
* Respond with a file
|
|
358
402
|
*/
|
|
359
|
-
file(path, fileOptions, responseOptions) {
|
|
403
|
+
async file(path, fileOptions, responseOptions) {
|
|
360
404
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
361
405
|
const status = responseOptions?.status ?? this.response.status;
|
|
362
|
-
|
|
363
|
-
|
|
406
|
+
if (typeof Bun !== "undefined") {
|
|
407
|
+
this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
|
|
408
|
+
return this._finalResponse;
|
|
409
|
+
} else {
|
|
410
|
+
const fileBuffer = await readFile(path);
|
|
411
|
+
if (fileOptions?.type) {
|
|
412
|
+
headers.set("content-type", fileOptions.type);
|
|
413
|
+
}
|
|
414
|
+
this._finalResponse = new Response(fileBuffer, { status, headers });
|
|
415
|
+
return this._finalResponse;
|
|
416
|
+
}
|
|
364
417
|
}
|
|
365
418
|
/**
|
|
366
419
|
* JSX Rendering Function
|
|
@@ -380,6 +433,74 @@ class ShokupanContext {
|
|
|
380
433
|
return this.html(html, status, headers);
|
|
381
434
|
}
|
|
382
435
|
}
|
|
436
|
+
function RateLimitMiddleware(options = {}) {
|
|
437
|
+
const windowMs = options.windowMs || 60 * 1e3;
|
|
438
|
+
const max = options.limit || options.max || 5;
|
|
439
|
+
const message = options.message || "Too many requests, please try again later.";
|
|
440
|
+
const statusCode = options.statusCode || 429;
|
|
441
|
+
const headers = options.headers !== false;
|
|
442
|
+
const mode = options.mode || "user";
|
|
443
|
+
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
444
|
+
if (mode === "absolute") {
|
|
445
|
+
return "global";
|
|
446
|
+
}
|
|
447
|
+
return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
|
|
448
|
+
});
|
|
449
|
+
const skip = options.skip || (() => false);
|
|
450
|
+
const hits = /* @__PURE__ */ new Map();
|
|
451
|
+
const interval = setInterval(() => {
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
for (const [key, record] of hits.entries()) {
|
|
454
|
+
if (record.resetTime <= now) {
|
|
455
|
+
hits.delete(key);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}, windowMs);
|
|
459
|
+
if (interval.unref) interval.unref();
|
|
460
|
+
const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
|
|
461
|
+
if (skip(ctx)) return next();
|
|
462
|
+
const key = keyGenerator(ctx);
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
let record = hits.get(key);
|
|
465
|
+
if (!record || record.resetTime <= now) {
|
|
466
|
+
record = {
|
|
467
|
+
hits: 0,
|
|
468
|
+
resetTime: now + windowMs
|
|
469
|
+
};
|
|
470
|
+
hits.set(key, record);
|
|
471
|
+
}
|
|
472
|
+
record.hits++;
|
|
473
|
+
const remaining = Math.max(0, max - record.hits);
|
|
474
|
+
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
475
|
+
const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
|
|
476
|
+
const setHeaders = (res) => {
|
|
477
|
+
if (!headers || !res || !res.headers) return;
|
|
478
|
+
try {
|
|
479
|
+
res.headers.set("X-RateLimit-Limit", String(max));
|
|
480
|
+
res.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
481
|
+
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
482
|
+
} catch (e) {
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
if (record.hits > max) {
|
|
486
|
+
typeof message === "object" ? JSON.stringify(message) : String(message);
|
|
487
|
+
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
488
|
+
if (headers) {
|
|
489
|
+
setHeaders(res);
|
|
490
|
+
res.headers.set("Retry-After", String(retryAfter));
|
|
491
|
+
}
|
|
492
|
+
return res;
|
|
493
|
+
}
|
|
494
|
+
const response = await next();
|
|
495
|
+
if (response instanceof Response && headers) {
|
|
496
|
+
setHeaders(response);
|
|
497
|
+
}
|
|
498
|
+
return response;
|
|
499
|
+
};
|
|
500
|
+
rateLimitMiddleware.isBuiltin = true;
|
|
501
|
+
rateLimitMiddleware.pluginName = "RateLimit";
|
|
502
|
+
return rateLimitMiddleware;
|
|
503
|
+
}
|
|
383
504
|
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
384
505
|
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
385
506
|
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
@@ -476,6 +597,9 @@ const Patch = createMethodDecorator("PATCH");
|
|
|
476
597
|
const Options = createMethodDecorator("OPTIONS");
|
|
477
598
|
const Head = createMethodDecorator("HEAD");
|
|
478
599
|
const All = createMethodDecorator("ALL");
|
|
600
|
+
function RateLimit(options) {
|
|
601
|
+
return Use(RateLimitMiddleware(options));
|
|
602
|
+
}
|
|
479
603
|
class Container {
|
|
480
604
|
static services = /* @__PURE__ */ new Map();
|
|
481
605
|
static register(target, instance) {
|
|
@@ -517,17 +641,31 @@ const compose = (middleware) => {
|
|
|
517
641
|
}
|
|
518
642
|
return function dispatch(context2, next) {
|
|
519
643
|
let index = -1;
|
|
520
|
-
function runner(i) {
|
|
644
|
+
async function runner(i) {
|
|
521
645
|
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
522
646
|
index = i;
|
|
523
647
|
if (i >= middleware.length) {
|
|
524
648
|
return next ? next() : Promise.resolve();
|
|
525
649
|
}
|
|
526
650
|
const fn = middleware[i];
|
|
651
|
+
if (!context2._debug) {
|
|
652
|
+
return fn(context2, () => runner(i + 1));
|
|
653
|
+
}
|
|
654
|
+
const debug = context2._debug;
|
|
655
|
+
const debugId = fn._debugId || fn.name || "anonymous";
|
|
656
|
+
const previousNode = debug.getCurrentNode();
|
|
657
|
+
debug.trackEdge(previousNode, debugId);
|
|
658
|
+
debug.setNode(debugId);
|
|
659
|
+
const start = performance.now();
|
|
527
660
|
try {
|
|
528
|
-
|
|
661
|
+
const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
|
|
662
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "success");
|
|
663
|
+
return res;
|
|
529
664
|
} catch (err) {
|
|
665
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
|
|
530
666
|
return Promise.reject(err);
|
|
667
|
+
} finally {
|
|
668
|
+
if (previousNode) debug.setNode(previousNode);
|
|
531
669
|
}
|
|
532
670
|
}
|
|
533
671
|
return runner(0);
|
|
@@ -589,6 +727,15 @@ function deepMerge(target, ...sources) {
|
|
|
589
727
|
}
|
|
590
728
|
return deepMerge(target, ...sources);
|
|
591
729
|
}
|
|
730
|
+
const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
|
|
731
|
+
const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
|
|
732
|
+
const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
|
|
733
|
+
const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
|
|
734
|
+
const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
|
|
735
|
+
const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
|
|
736
|
+
const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
|
|
737
|
+
const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
|
|
738
|
+
const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
|
|
592
739
|
function analyzeHandler(handler) {
|
|
593
740
|
const handlerSource = handler.toString();
|
|
594
741
|
const inferredSpec = {};
|
|
@@ -598,46 +745,28 @@ function analyzeHandler(handler) {
|
|
|
598
745
|
};
|
|
599
746
|
}
|
|
600
747
|
const queryParams = /* @__PURE__ */ new Map();
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
queryIntMatch.forEach((match) => {
|
|
604
|
-
const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
|
|
605
|
-
if (paramName) queryParams.set(paramName, { type: "integer", format: "int32" });
|
|
606
|
-
});
|
|
748
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
|
|
749
|
+
if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
|
|
607
750
|
}
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
queryFloatMatch.forEach((match) => {
|
|
611
|
-
const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
|
|
612
|
-
if (paramName) queryParams.set(paramName, { type: "number", format: "float" });
|
|
613
|
-
});
|
|
751
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
|
|
752
|
+
if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
|
|
614
753
|
}
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (paramName && !queryParams.has(paramName)) {
|
|
620
|
-
queryParams.set(paramName, { type: "number" });
|
|
621
|
-
}
|
|
622
|
-
});
|
|
754
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
|
|
755
|
+
if (match[1] && !queryParams.has(match[1])) {
|
|
756
|
+
queryParams.set(match[1], { type: "number" });
|
|
757
|
+
}
|
|
623
758
|
}
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
queryParams.set(paramName, { type: "boolean" });
|
|
630
|
-
}
|
|
631
|
-
});
|
|
759
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
|
|
760
|
+
const name = match[1] || match[2];
|
|
761
|
+
if (name && !queryParams.has(name)) {
|
|
762
|
+
queryParams.set(name, { type: "boolean" });
|
|
763
|
+
}
|
|
632
764
|
}
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
queryParams.set(paramName, { type: "string" });
|
|
639
|
-
}
|
|
640
|
-
});
|
|
765
|
+
for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
|
|
766
|
+
const name = match[1];
|
|
767
|
+
if (name && !queryParams.has(name)) {
|
|
768
|
+
queryParams.set(name, { type: "string" });
|
|
769
|
+
}
|
|
641
770
|
}
|
|
642
771
|
if (queryParams.size > 0) {
|
|
643
772
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
@@ -650,19 +779,11 @@ function analyzeHandler(handler) {
|
|
|
650
779
|
});
|
|
651
780
|
}
|
|
652
781
|
const pathParams = /* @__PURE__ */ new Map();
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
paramIntMatch.forEach((match) => {
|
|
656
|
-
const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
|
|
657
|
-
if (paramName) pathParams.set(paramName, { type: "integer", format: "int32" });
|
|
658
|
-
});
|
|
782
|
+
for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
|
|
783
|
+
if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
|
|
659
784
|
}
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
paramFloatMatch.forEach((match) => {
|
|
663
|
-
const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
|
|
664
|
-
if (paramName) pathParams.set(paramName, { type: "number", format: "float" });
|
|
665
|
-
});
|
|
785
|
+
for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
|
|
786
|
+
if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
|
|
666
787
|
}
|
|
667
788
|
if (pathParams.size > 0) {
|
|
668
789
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
@@ -675,76 +796,55 @@ function analyzeHandler(handler) {
|
|
|
675
796
|
});
|
|
676
797
|
});
|
|
677
798
|
}
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
schema: { type: "string" }
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
});
|
|
799
|
+
for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
|
|
800
|
+
if (match[1]) {
|
|
801
|
+
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
802
|
+
inferredSpec.parameters.push({
|
|
803
|
+
name: match[1],
|
|
804
|
+
in: "header",
|
|
805
|
+
schema: { type: "string" }
|
|
806
|
+
});
|
|
807
|
+
}
|
|
691
808
|
}
|
|
692
809
|
const responses = {};
|
|
693
810
|
if (handlerSource.includes("ctx.json(")) {
|
|
694
811
|
responses["200"] = {
|
|
695
812
|
description: "Successful response",
|
|
696
|
-
content: {
|
|
697
|
-
"application/json": { schema: { type: "object" } }
|
|
698
|
-
}
|
|
813
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
699
814
|
};
|
|
700
815
|
}
|
|
701
816
|
if (handlerSource.includes("ctx.html(")) {
|
|
702
817
|
responses["200"] = {
|
|
703
818
|
description: "Successful response",
|
|
704
|
-
content: {
|
|
705
|
-
"text/html": { schema: { type: "string" } }
|
|
706
|
-
}
|
|
819
|
+
content: { "text/html": { schema: { type: "string" } } }
|
|
707
820
|
};
|
|
708
821
|
}
|
|
709
822
|
if (handlerSource.includes("ctx.text(")) {
|
|
710
823
|
responses["200"] = {
|
|
711
824
|
description: "Successful response",
|
|
712
|
-
content: {
|
|
713
|
-
"text/plain": { schema: { type: "string" } }
|
|
714
|
-
}
|
|
825
|
+
content: { "text/plain": { schema: { type: "string" } } }
|
|
715
826
|
};
|
|
716
827
|
}
|
|
717
828
|
if (handlerSource.includes("ctx.file(")) {
|
|
718
829
|
responses["200"] = {
|
|
719
830
|
description: "File download",
|
|
720
|
-
content: {
|
|
721
|
-
"application/octet-stream": { schema: { type: "string", format: "binary" } }
|
|
722
|
-
}
|
|
831
|
+
content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } }
|
|
723
832
|
};
|
|
724
833
|
}
|
|
725
834
|
if (handlerSource.includes("ctx.redirect(")) {
|
|
726
|
-
responses["302"] = {
|
|
727
|
-
description: "Redirect"
|
|
728
|
-
};
|
|
835
|
+
responses["302"] = { description: "Redirect" };
|
|
729
836
|
}
|
|
730
837
|
if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
|
|
731
838
|
responses["200"] = {
|
|
732
839
|
description: "Successful response",
|
|
733
|
-
content: {
|
|
734
|
-
"application/json": { schema: { type: "object" } }
|
|
735
|
-
}
|
|
840
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
736
841
|
};
|
|
737
842
|
}
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
responses[statusCode] = {
|
|
744
|
-
description: `Error response (${statusCode})`
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
});
|
|
843
|
+
for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
|
|
844
|
+
const statusCode = match[1];
|
|
845
|
+
if (statusCode && statusCode !== "200") {
|
|
846
|
+
responses[statusCode] = { description: `Error response (${statusCode})` };
|
|
847
|
+
}
|
|
748
848
|
}
|
|
749
849
|
if (Object.keys(responses).length > 0) {
|
|
750
850
|
inferredSpec.responses = responses;
|
|
@@ -758,7 +858,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
758
858
|
const defaultTagName = options.defaultTag || "Application";
|
|
759
859
|
let astRoutes = [];
|
|
760
860
|
try {
|
|
761
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-
|
|
861
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-BtIaHIfe.js");
|
|
762
862
|
const analyzer = new OpenAPIAnalyzer2(process.cwd());
|
|
763
863
|
const { applications } = await analyzer.analyze();
|
|
764
864
|
const appMap = /* @__PURE__ */ new Map();
|
|
@@ -1028,10 +1128,10 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1028
1128
|
};
|
|
1029
1129
|
}
|
|
1030
1130
|
const eta$1 = new Eta();
|
|
1031
|
-
function serveStatic(
|
|
1131
|
+
function serveStatic(config, prefix) {
|
|
1032
1132
|
const rootPath = resolve(config.root || ".");
|
|
1033
1133
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
1034
|
-
|
|
1134
|
+
const serveStaticMiddleware = async (ctx) => {
|
|
1035
1135
|
let relative = ctx.path.slice(normalizedPrefix.length);
|
|
1036
1136
|
if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
|
|
1037
1137
|
if (relative.length === 0) relative = "/";
|
|
@@ -1148,16 +1248,158 @@ function serveStatic(ctx, config, prefix) {
|
|
|
1148
1248
|
}
|
|
1149
1249
|
}
|
|
1150
1250
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1251
|
+
let response;
|
|
1252
|
+
if (typeof Bun !== "undefined") {
|
|
1253
|
+
response = new Response(Bun.file(finalPath));
|
|
1254
|
+
} else {
|
|
1255
|
+
const fileBuffer = await readFile$1(finalPath);
|
|
1256
|
+
response = new Response(fileBuffer);
|
|
1257
|
+
}
|
|
1153
1258
|
if (config.hooks?.onResponse) {
|
|
1154
1259
|
const hooked = await config.hooks.onResponse(ctx, response);
|
|
1155
1260
|
if (hooked) response = hooked;
|
|
1156
1261
|
}
|
|
1157
1262
|
return response;
|
|
1158
1263
|
};
|
|
1264
|
+
serveStaticMiddleware.isBuiltin = true;
|
|
1265
|
+
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1266
|
+
return serveStaticMiddleware;
|
|
1267
|
+
}
|
|
1268
|
+
class RouterTrie {
|
|
1269
|
+
root;
|
|
1270
|
+
constructor() {
|
|
1271
|
+
this.root = this.createNode();
|
|
1272
|
+
}
|
|
1273
|
+
createNode() {
|
|
1274
|
+
return {
|
|
1275
|
+
children: {}
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
insert(method, path, handler) {
|
|
1279
|
+
let node = this.root;
|
|
1280
|
+
const segments = this.splitPath(path);
|
|
1281
|
+
for (const segment of segments) {
|
|
1282
|
+
if (segment === "**") {
|
|
1283
|
+
if (!node.recursiveChild) {
|
|
1284
|
+
node.recursiveChild = this.createNode();
|
|
1285
|
+
}
|
|
1286
|
+
node = node.recursiveChild;
|
|
1287
|
+
} else if (segment === "*") {
|
|
1288
|
+
if (!node.wildcardChild) {
|
|
1289
|
+
node.wildcardChild = this.createNode();
|
|
1290
|
+
}
|
|
1291
|
+
node = node.wildcardChild;
|
|
1292
|
+
} else if (segment.startsWith(":")) {
|
|
1293
|
+
const paramName = segment.slice(1);
|
|
1294
|
+
if (!node.paramChild) {
|
|
1295
|
+
node.paramChild = this.createNode();
|
|
1296
|
+
node.paramChild.paramName = paramName;
|
|
1297
|
+
}
|
|
1298
|
+
node = node.paramChild;
|
|
1299
|
+
node.paramName = paramName;
|
|
1300
|
+
} else {
|
|
1301
|
+
if (!node.children[segment]) {
|
|
1302
|
+
node.children[segment] = this.createNode();
|
|
1303
|
+
}
|
|
1304
|
+
node = node.children[segment];
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (!node.handlers) {
|
|
1308
|
+
node.handlers = {};
|
|
1309
|
+
}
|
|
1310
|
+
node.handlers[method] = handler;
|
|
1311
|
+
}
|
|
1312
|
+
search(method, path) {
|
|
1313
|
+
const segments = this.splitPath(path);
|
|
1314
|
+
const params = {};
|
|
1315
|
+
const match = this.findNode(this.root, segments, 0, params);
|
|
1316
|
+
if (match && match.handlers) {
|
|
1317
|
+
const handler = match.handlers[method] || match.handlers["ALL"];
|
|
1318
|
+
if (handler) {
|
|
1319
|
+
return { handler, params };
|
|
1320
|
+
}
|
|
1321
|
+
if (method === "HEAD" && match.handlers["GET"]) {
|
|
1322
|
+
return { handler: match.handlers["GET"], params };
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
findNode(node, segments, index, params) {
|
|
1328
|
+
if (index === segments.length) {
|
|
1329
|
+
if (node.handlers) return node;
|
|
1330
|
+
if (node.recursiveChild && node.recursiveChild.handlers) {
|
|
1331
|
+
return node.recursiveChild;
|
|
1332
|
+
}
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
const segment = segments[index];
|
|
1336
|
+
const child = node.children[segment];
|
|
1337
|
+
if (child) {
|
|
1338
|
+
const result = this.findNode(child, segments, index + 1, params);
|
|
1339
|
+
if (result) return result;
|
|
1340
|
+
}
|
|
1341
|
+
if (node.paramChild) {
|
|
1342
|
+
params[node.paramChild.paramName] = segment;
|
|
1343
|
+
const result = this.findNode(node.paramChild, segments, index + 1, params);
|
|
1344
|
+
if (result) return result;
|
|
1345
|
+
delete params[node.paramChild.paramName];
|
|
1346
|
+
}
|
|
1347
|
+
if (node.wildcardChild) {
|
|
1348
|
+
const result = this.findNode(node.wildcardChild, segments, index + 1, params);
|
|
1349
|
+
if (result) return result;
|
|
1350
|
+
}
|
|
1351
|
+
if (node.recursiveChild) {
|
|
1352
|
+
const remaining = segments.length - index;
|
|
1353
|
+
for (let k = 0; k <= remaining; k++) {
|
|
1354
|
+
const result = this.findNode(node.recursiveChild, segments, index + k, params);
|
|
1355
|
+
if (result) return result;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
splitPath(path) {
|
|
1361
|
+
if (path === "/" || path === "") return [];
|
|
1362
|
+
const s = path.startsWith("/") ? path.slice(1) : path;
|
|
1363
|
+
if (s === "") return [];
|
|
1364
|
+
return s.split("/");
|
|
1365
|
+
}
|
|
1159
1366
|
}
|
|
1160
1367
|
const asyncContext = new AsyncLocalStorage();
|
|
1368
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1369
|
+
const db = new Surreal({
|
|
1370
|
+
engines: createNodeEngines()
|
|
1371
|
+
});
|
|
1372
|
+
const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
|
|
1373
|
+
return db.query(`
|
|
1374
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1375
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1376
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1377
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1378
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1379
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1380
|
+
`);
|
|
1381
|
+
});
|
|
1382
|
+
const datastore = {
|
|
1383
|
+
get(store, key) {
|
|
1384
|
+
return db.select(new RecordId(store, key));
|
|
1385
|
+
},
|
|
1386
|
+
set(store, key, value) {
|
|
1387
|
+
return db.create(new RecordId(store, key)).content(value);
|
|
1388
|
+
},
|
|
1389
|
+
async query(query, vars) {
|
|
1390
|
+
try {
|
|
1391
|
+
const r = await db.query(query, vars).collect();
|
|
1392
|
+
return r;
|
|
1393
|
+
} catch (e) {
|
|
1394
|
+
console.error("DS ERROR:", e);
|
|
1395
|
+
throw e;
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
ready
|
|
1399
|
+
};
|
|
1400
|
+
process.on("exit", async () => {
|
|
1401
|
+
await db.close();
|
|
1402
|
+
});
|
|
1161
1403
|
const tracer = trace.getTracer("shokupan.middleware");
|
|
1162
1404
|
function traceHandler(fn, name) {
|
|
1163
1405
|
return async function(...args) {
|
|
@@ -1181,6 +1423,35 @@ function traceHandler(fn, name) {
|
|
|
1181
1423
|
});
|
|
1182
1424
|
};
|
|
1183
1425
|
}
|
|
1426
|
+
function getCallerInfo(skipFrames = 1) {
|
|
1427
|
+
let file = "unknown";
|
|
1428
|
+
let line = 0;
|
|
1429
|
+
try {
|
|
1430
|
+
const err = new Error();
|
|
1431
|
+
const stack = err.stack?.split("\n") || [];
|
|
1432
|
+
let found = 0;
|
|
1433
|
+
for (let i = 1; i < stack.length; i++) {
|
|
1434
|
+
const l = stack[i];
|
|
1435
|
+
if (!l.includes(":")) continue;
|
|
1436
|
+
if (l.includes("node_modules")) continue;
|
|
1437
|
+
if (l.includes("bun:main")) continue;
|
|
1438
|
+
if (l.includes("src/util/stack.ts")) continue;
|
|
1439
|
+
if (l.includes("src/router.ts")) continue;
|
|
1440
|
+
if (l.includes("src/shokupan.ts")) continue;
|
|
1441
|
+
found++;
|
|
1442
|
+
if (found >= skipFrames) {
|
|
1443
|
+
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
1444
|
+
if (match) {
|
|
1445
|
+
file = match[1];
|
|
1446
|
+
line = parseInt(match[2], 10);
|
|
1447
|
+
return { file, line };
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
} catch (e) {
|
|
1452
|
+
}
|
|
1453
|
+
return { file, line };
|
|
1454
|
+
}
|
|
1184
1455
|
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1185
1456
|
const ShokupanApplicationTree = {};
|
|
1186
1457
|
class ShokupanRouter {
|
|
@@ -1200,6 +1471,7 @@ class ShokupanRouter {
|
|
|
1200
1471
|
[$parent] = null;
|
|
1201
1472
|
[$childRouters] = [];
|
|
1202
1473
|
[$childControllers] = [];
|
|
1474
|
+
middleware = [];
|
|
1203
1475
|
get rootConfig() {
|
|
1204
1476
|
return this[$appRoot]?.applicationConfig;
|
|
1205
1477
|
}
|
|
@@ -1208,7 +1480,66 @@ class ShokupanRouter {
|
|
|
1208
1480
|
}
|
|
1209
1481
|
[$routes] = [];
|
|
1210
1482
|
// Public via Symbol for OpenAPI generator
|
|
1483
|
+
trie = new RouterTrie();
|
|
1484
|
+
metadata;
|
|
1485
|
+
// Metadata for the router itself
|
|
1211
1486
|
currentGuards = [];
|
|
1487
|
+
// Registry Accessor
|
|
1488
|
+
getComponentRegistry() {
|
|
1489
|
+
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
1490
|
+
const localRoutes = [];
|
|
1491
|
+
for (const r of this[$routes]) {
|
|
1492
|
+
const entry = {
|
|
1493
|
+
type: "route",
|
|
1494
|
+
path: r.path,
|
|
1495
|
+
method: r.method,
|
|
1496
|
+
metadata: r.metadata,
|
|
1497
|
+
handlerName: r.handler.name,
|
|
1498
|
+
tags: r.handlerSpec?.tags,
|
|
1499
|
+
order: r.order,
|
|
1500
|
+
_fn: r.handler
|
|
1501
|
+
};
|
|
1502
|
+
if (r.controller) {
|
|
1503
|
+
if (!controllerRoutesMap.has(r.controller)) {
|
|
1504
|
+
controllerRoutesMap.set(r.controller, []);
|
|
1505
|
+
}
|
|
1506
|
+
controllerRoutesMap.get(r.controller).push(entry);
|
|
1507
|
+
} else {
|
|
1508
|
+
localRoutes.push(entry);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
const mw = this.middleware;
|
|
1512
|
+
const middleware = mw ? mw.map((m) => ({
|
|
1513
|
+
name: m.name || "middleware",
|
|
1514
|
+
metadata: m.metadata,
|
|
1515
|
+
order: m.order,
|
|
1516
|
+
_fn: m
|
|
1517
|
+
// Expose function for debugging instrumentation
|
|
1518
|
+
})) : [];
|
|
1519
|
+
const routers = this[$childRouters].map((r) => ({
|
|
1520
|
+
type: "router",
|
|
1521
|
+
path: r[$mountPath],
|
|
1522
|
+
metadata: r.metadata,
|
|
1523
|
+
children: r.getComponentRegistry()
|
|
1524
|
+
}));
|
|
1525
|
+
const controllers = this[$childControllers].map((c) => {
|
|
1526
|
+
const routes = controllerRoutesMap.get(c) || [];
|
|
1527
|
+
return {
|
|
1528
|
+
type: "controller",
|
|
1529
|
+
path: c[$mountPath] || "/",
|
|
1530
|
+
name: c.constructor.name,
|
|
1531
|
+
metadata: c.metadata,
|
|
1532
|
+
children: { routes }
|
|
1533
|
+
};
|
|
1534
|
+
});
|
|
1535
|
+
return {
|
|
1536
|
+
metadata: this.metadata,
|
|
1537
|
+
middleware,
|
|
1538
|
+
routes: localRoutes,
|
|
1539
|
+
routers,
|
|
1540
|
+
controllers
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1212
1543
|
isRouterInstance(target) {
|
|
1213
1544
|
return typeof target === "object" && target !== null && $isRouter in target;
|
|
1214
1545
|
}
|
|
@@ -1234,6 +1565,14 @@ class ShokupanRouter {
|
|
|
1234
1565
|
throw new Error("Router is already mounted");
|
|
1235
1566
|
}
|
|
1236
1567
|
controller[$mountPath] = prefix;
|
|
1568
|
+
if (!controller.metadata) {
|
|
1569
|
+
const info = getCallerInfo();
|
|
1570
|
+
controller.metadata = {
|
|
1571
|
+
file: info.file,
|
|
1572
|
+
line: info.line,
|
|
1573
|
+
name: "MountedRouter"
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1237
1576
|
this[$childRouters].push(controller);
|
|
1238
1577
|
controller[$parent] = this;
|
|
1239
1578
|
const setRouterContext = (router) => {
|
|
@@ -1266,6 +1605,12 @@ class ShokupanRouter {
|
|
|
1266
1605
|
}
|
|
1267
1606
|
}
|
|
1268
1607
|
instance[$mountPath] = prefix;
|
|
1608
|
+
const info = getCallerInfo();
|
|
1609
|
+
instance.metadata = {
|
|
1610
|
+
file: info.file,
|
|
1611
|
+
line: info.line,
|
|
1612
|
+
name: instance.constructor.name
|
|
1613
|
+
};
|
|
1269
1614
|
this[$childControllers].push(instance);
|
|
1270
1615
|
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
1271
1616
|
const proto = Object.getPrototypeOf(instance);
|
|
@@ -1280,7 +1625,7 @@ class ShokupanRouter {
|
|
|
1280
1625
|
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
1281
1626
|
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
1282
1627
|
let routesAttached = 0;
|
|
1283
|
-
for (const name of methods) {
|
|
1628
|
+
for (const name of Array.from(methods)) {
|
|
1284
1629
|
if (name === "constructor") continue;
|
|
1285
1630
|
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
1286
1631
|
const originalHandler = instance[name];
|
|
@@ -1349,14 +1694,39 @@ class ShokupanRouter {
|
|
|
1349
1694
|
for (const arg of sortedArgs) {
|
|
1350
1695
|
switch (arg.type) {
|
|
1351
1696
|
case RouteParamType.BODY:
|
|
1352
|
-
|
|
1697
|
+
try {
|
|
1698
|
+
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
1699
|
+
args[arg.index] = await ctx.req.json();
|
|
1700
|
+
} else {
|
|
1701
|
+
const text = await ctx.req.text();
|
|
1702
|
+
if (!text) {
|
|
1703
|
+
args[arg.index] = {};
|
|
1704
|
+
} else {
|
|
1705
|
+
args[arg.index] = JSON.parse(text);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
} catch (e) {
|
|
1709
|
+
const err = new Error("Invalid JSON body");
|
|
1710
|
+
err.status = 400;
|
|
1711
|
+
throw err;
|
|
1712
|
+
}
|
|
1353
1713
|
break;
|
|
1354
1714
|
case RouteParamType.PARAM:
|
|
1355
1715
|
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
1356
1716
|
break;
|
|
1357
1717
|
case RouteParamType.QUERY: {
|
|
1358
1718
|
const url = new URL(ctx.req.url);
|
|
1359
|
-
|
|
1719
|
+
if (arg.name) {
|
|
1720
|
+
const vals = url.searchParams.getAll(arg.name);
|
|
1721
|
+
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1722
|
+
} else {
|
|
1723
|
+
const query = {};
|
|
1724
|
+
for (const key of url.searchParams.keys()) {
|
|
1725
|
+
const vals = url.searchParams.getAll(key);
|
|
1726
|
+
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1727
|
+
}
|
|
1728
|
+
args[arg.index] = query;
|
|
1729
|
+
}
|
|
1360
1730
|
break;
|
|
1361
1731
|
}
|
|
1362
1732
|
case RouteParamType.HEADER:
|
|
@@ -1389,7 +1759,7 @@ class ShokupanRouter {
|
|
|
1389
1759
|
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
1390
1760
|
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
1391
1761
|
const spec = { tags: [tagName], ...userSpec };
|
|
1392
|
-
this.add({ method, path: normalizedPath, handler: finalHandler, spec });
|
|
1762
|
+
this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
|
|
1393
1763
|
}
|
|
1394
1764
|
}
|
|
1395
1765
|
if (routesAttached === 0) {
|
|
@@ -1504,30 +1874,59 @@ class ShokupanRouter {
|
|
|
1504
1874
|
data: result
|
|
1505
1875
|
};
|
|
1506
1876
|
}
|
|
1507
|
-
|
|
1877
|
+
applyRouterHooks(match) {
|
|
1508
1878
|
if (!this.config?.hooks) return match;
|
|
1509
1879
|
const hooks = this.config.hooks;
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1880
|
+
return {
|
|
1881
|
+
...match,
|
|
1882
|
+
handler: this.wrapWithHooks(match.handler, hooks)
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
wrapWithHooks(handler, hooks) {
|
|
1886
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
1887
|
+
const hasStart = hookList.some((h) => !!h.onRequestStart);
|
|
1888
|
+
const hasEnd = hookList.some((h) => !!h.onRequestEnd);
|
|
1889
|
+
const hasError = hookList.some((h) => !!h.onError);
|
|
1890
|
+
if (!hasStart && !hasEnd && !hasError) return handler;
|
|
1891
|
+
const originalHandler = handler;
|
|
1892
|
+
const wrapped = async (ctx) => {
|
|
1893
|
+
if (hasStart) {
|
|
1894
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1895
|
+
const h = hookList[i];
|
|
1896
|
+
if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
const debug = ctx._debug;
|
|
1900
|
+
let debugId;
|
|
1901
|
+
let previousNode;
|
|
1902
|
+
if (debug) {
|
|
1903
|
+
debugId = originalHandler._debugId || originalHandler.name || "handler";
|
|
1904
|
+
previousNode = debug.getCurrentNode();
|
|
1905
|
+
debug.trackEdge(previousNode, debugId);
|
|
1906
|
+
debug.setNode(debugId);
|
|
1907
|
+
}
|
|
1908
|
+
const start = performance.now();
|
|
1514
1909
|
try {
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
|
|
1910
|
+
const res = await originalHandler(ctx);
|
|
1911
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "success");
|
|
1912
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1913
|
+
const h = hookList[i];
|
|
1914
|
+
if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
|
|
1915
|
+
}
|
|
1916
|
+
return res;
|
|
1518
1917
|
} catch (err) {
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
console.error("Error in router onError hook:", e);
|
|
1524
|
-
}
|
|
1918
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
|
|
1919
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1920
|
+
const h = hookList[i];
|
|
1921
|
+
if (typeof h.onError === "function") await h.onError(err, ctx);
|
|
1525
1922
|
}
|
|
1526
1923
|
throw err;
|
|
1924
|
+
} finally {
|
|
1925
|
+
if (debug && previousNode) debug.setNode(previousNode);
|
|
1527
1926
|
}
|
|
1528
1927
|
};
|
|
1529
|
-
|
|
1530
|
-
return
|
|
1928
|
+
wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
|
|
1929
|
+
return wrapped;
|
|
1531
1930
|
}
|
|
1532
1931
|
/**
|
|
1533
1932
|
* Find a route matching the given method and path.
|
|
@@ -1536,24 +1935,10 @@ class ShokupanRouter {
|
|
|
1536
1935
|
* @returns Route handler and parameters if found, otherwise null
|
|
1537
1936
|
*/
|
|
1538
1937
|
find(method, path) {
|
|
1539
|
-
|
|
1540
|
-
for (const route of routes) {
|
|
1541
|
-
if (route.method !== "ALL" && route.method !== m) continue;
|
|
1542
|
-
const match = route.regex.exec(path);
|
|
1543
|
-
if (match) {
|
|
1544
|
-
const params = {};
|
|
1545
|
-
route.keys.forEach((key, index) => {
|
|
1546
|
-
params[key] = match[index + 1];
|
|
1547
|
-
});
|
|
1548
|
-
return this.applyHooks({ handler: route.handler, params });
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
return null;
|
|
1552
|
-
};
|
|
1553
|
-
let result = findInRoutes(this[$routes], method);
|
|
1938
|
+
let result = this.trie.search(method, path);
|
|
1554
1939
|
if (result) return result;
|
|
1555
1940
|
if (method === "HEAD") {
|
|
1556
|
-
result =
|
|
1941
|
+
result = this.trie.search("GET", path);
|
|
1557
1942
|
if (result) return result;
|
|
1558
1943
|
}
|
|
1559
1944
|
for (const child of this[$childRouters]) {
|
|
@@ -1561,13 +1946,13 @@ class ShokupanRouter {
|
|
|
1561
1946
|
if (path === prefix || path.startsWith(prefix + "/")) {
|
|
1562
1947
|
const subPath = path.slice(prefix.length) || "/";
|
|
1563
1948
|
const match = child.find(method, subPath);
|
|
1564
|
-
if (match) return this.
|
|
1949
|
+
if (match) return this.applyRouterHooks(match);
|
|
1565
1950
|
}
|
|
1566
1951
|
if (prefix.endsWith("/")) {
|
|
1567
1952
|
if (path.startsWith(prefix)) {
|
|
1568
1953
|
const subPath = path.slice(prefix.length) || "/";
|
|
1569
1954
|
const match = child.find(method, subPath);
|
|
1570
|
-
if (match) return this.
|
|
1955
|
+
if (match) return this.applyRouterHooks(match);
|
|
1571
1956
|
}
|
|
1572
1957
|
}
|
|
1573
1958
|
}
|
|
@@ -1578,7 +1963,7 @@ class ShokupanRouter {
|
|
|
1578
1963
|
const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
1579
1964
|
keys.push(key);
|
|
1580
1965
|
return "([^/]+)";
|
|
1581
|
-
}).replace(
|
|
1966
|
+
}).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
|
|
1582
1967
|
return {
|
|
1583
1968
|
regex: new RegExp(`^${pattern}$`),
|
|
1584
1969
|
keys
|
|
@@ -1595,8 +1980,23 @@ class ShokupanRouter {
|
|
|
1595
1980
|
* @param handler - Route handler function
|
|
1596
1981
|
* @param requestTimeout - Timeout for this route in milliseconds
|
|
1597
1982
|
*/
|
|
1598
|
-
add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer }) {
|
|
1983
|
+
add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
1599
1984
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path);
|
|
1985
|
+
if (this.currentGuards.length > 0) {
|
|
1986
|
+
spec = spec || {};
|
|
1987
|
+
for (const guard of this.currentGuards) {
|
|
1988
|
+
if (guard.spec) {
|
|
1989
|
+
if (guard.spec.responses) {
|
|
1990
|
+
spec.responses = spec.responses || {};
|
|
1991
|
+
Object.assign(spec.responses, guard.spec.responses);
|
|
1992
|
+
}
|
|
1993
|
+
if (guard.spec.security) {
|
|
1994
|
+
spec.security = spec.security || [];
|
|
1995
|
+
spec.security.push(...guard.spec.security);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1600
2000
|
let wrappedHandler = handler;
|
|
1601
2001
|
const routeGuards = [...this.currentGuards];
|
|
1602
2002
|
const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
|
|
@@ -1647,47 +2047,85 @@ class ShokupanRouter {
|
|
|
1647
2047
|
return innerHandler(ctx);
|
|
1648
2048
|
};
|
|
1649
2049
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
try {
|
|
1653
|
-
const err = new Error();
|
|
1654
|
-
const stack = err.stack?.split("\n") || [];
|
|
1655
|
-
const callerLine = stack.find(
|
|
1656
|
-
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
1657
|
-
);
|
|
1658
|
-
if (callerLine) {
|
|
1659
|
-
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1660
|
-
if (match) {
|
|
1661
|
-
file = match[1];
|
|
1662
|
-
line = parseInt(match[2], 10);
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
} catch (e) {
|
|
1666
|
-
}
|
|
1667
|
-
const trackedHandler = wrappedHandler;
|
|
2050
|
+
const { file, line } = getCallerInfo();
|
|
2051
|
+
const trackingHandler = wrappedHandler;
|
|
1668
2052
|
wrappedHandler = async (ctx) => {
|
|
1669
|
-
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1670
|
-
ctx
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
2053
|
+
if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2054
|
+
return trackingHandler(ctx);
|
|
2055
|
+
}
|
|
2056
|
+
const startTime = performance.now();
|
|
2057
|
+
let error = void 0;
|
|
2058
|
+
try {
|
|
2059
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2060
|
+
ctx.handlerStack.push({
|
|
2061
|
+
name: handler.name || "anonymous",
|
|
2062
|
+
file,
|
|
2063
|
+
line
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
return await trackingHandler(ctx);
|
|
2067
|
+
} catch (e) {
|
|
2068
|
+
error = e;
|
|
2069
|
+
throw e;
|
|
2070
|
+
} finally {
|
|
2071
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2072
|
+
const duration = performance.now() - startTime;
|
|
2073
|
+
const config = ctx.app.applicationConfig;
|
|
2074
|
+
try {
|
|
2075
|
+
const timestamp = Date.now();
|
|
2076
|
+
const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
|
|
2077
|
+
await datastore.set("middleware_tracking", key, {
|
|
2078
|
+
name: handler.name || "anonymous",
|
|
2079
|
+
path: ctx.path,
|
|
2080
|
+
timestamp,
|
|
2081
|
+
duration,
|
|
2082
|
+
file,
|
|
2083
|
+
line,
|
|
2084
|
+
error: error ? String(error) : void 0,
|
|
2085
|
+
metadata: {
|
|
2086
|
+
isBuiltin: handler.isBuiltin,
|
|
2087
|
+
pluginName: handler.pluginName
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
2090
|
+
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2091
|
+
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2092
|
+
const cutoff = Date.now() - ttl;
|
|
2093
|
+
await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2094
|
+
const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2095
|
+
if (results && results[0] && results[0].count > maxCapacity) {
|
|
2096
|
+
const toDelete = results[0].count - maxCapacity;
|
|
2097
|
+
await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2098
|
+
}
|
|
2099
|
+
} catch (datastoreError) {
|
|
2100
|
+
console.error("Failed to store middleware tracking:", datastoreError);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
1675
2103
|
}
|
|
1676
|
-
return trackedHandler(ctx);
|
|
1677
2104
|
};
|
|
1678
|
-
wrappedHandler.originalHandler =
|
|
2105
|
+
wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
|
|
2106
|
+
let bakedHandler = wrappedHandler;
|
|
2107
|
+
if (this.config?.hooks) {
|
|
2108
|
+
bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
|
|
2109
|
+
}
|
|
1679
2110
|
this[$routes].push({
|
|
1680
2111
|
method,
|
|
1681
2112
|
path,
|
|
1682
|
-
regex,
|
|
1683
|
-
keys,
|
|
1684
|
-
handler
|
|
2113
|
+
regex: regex ?? new RegExp(""),
|
|
2114
|
+
keys: keys ?? [],
|
|
2115
|
+
handler,
|
|
2116
|
+
bakedHandler,
|
|
1685
2117
|
handlerSpec: spec,
|
|
1686
2118
|
group,
|
|
1687
|
-
|
|
1688
|
-
requestTimeout
|
|
1689
|
-
|
|
2119
|
+
hooks: this.config?.hooks,
|
|
2120
|
+
requestTimeout,
|
|
2121
|
+
renderer,
|
|
2122
|
+
metadata: {
|
|
2123
|
+
file,
|
|
2124
|
+
line
|
|
2125
|
+
},
|
|
2126
|
+
controller
|
|
1690
2127
|
});
|
|
2128
|
+
this.trie.insert(method, path, bakedHandler);
|
|
1691
2129
|
return this;
|
|
1692
2130
|
}
|
|
1693
2131
|
get(path, ...args) {
|
|
@@ -1761,10 +2199,10 @@ class ShokupanRouter {
|
|
|
1761
2199
|
const config = typeof options === "string" ? { root: options } : options;
|
|
1762
2200
|
const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
|
|
1763
2201
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
1764
|
-
serveStatic(
|
|
2202
|
+
const handlerMiddleware = serveStatic(config, prefix);
|
|
1765
2203
|
const routeHandler = async (ctx) => {
|
|
1766
|
-
|
|
1767
|
-
|
|
2204
|
+
return handlerMiddleware(ctx, async () => {
|
|
2205
|
+
});
|
|
1768
2206
|
};
|
|
1769
2207
|
let groupName = "Static";
|
|
1770
2208
|
const segments = normalizedPrefix.split("/").filter(Boolean);
|
|
@@ -1825,6 +2263,49 @@ class ShokupanRouter {
|
|
|
1825
2263
|
return generateOpenApi(this, options);
|
|
1826
2264
|
}
|
|
1827
2265
|
}
|
|
2266
|
+
class SystemCpuMonitor {
|
|
2267
|
+
constructor(intervalMs = 1e3) {
|
|
2268
|
+
this.intervalMs = intervalMs;
|
|
2269
|
+
}
|
|
2270
|
+
interval = null;
|
|
2271
|
+
lastCpus = [];
|
|
2272
|
+
currentUsage = 0;
|
|
2273
|
+
start() {
|
|
2274
|
+
if (this.interval) return;
|
|
2275
|
+
this.lastCpus = os.cpus();
|
|
2276
|
+
this.interval = setInterval(() => this.update(), this.intervalMs);
|
|
2277
|
+
}
|
|
2278
|
+
stop() {
|
|
2279
|
+
if (this.interval) {
|
|
2280
|
+
clearInterval(this.interval);
|
|
2281
|
+
this.interval = null;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
getUsage() {
|
|
2285
|
+
return this.currentUsage;
|
|
2286
|
+
}
|
|
2287
|
+
update() {
|
|
2288
|
+
const cpus = os.cpus();
|
|
2289
|
+
let idle = 0;
|
|
2290
|
+
let total = 0;
|
|
2291
|
+
for (let i = 0; i < cpus.length; i++) {
|
|
2292
|
+
const cpu = cpus[i];
|
|
2293
|
+
const prev = this.lastCpus[i];
|
|
2294
|
+
let type;
|
|
2295
|
+
for (type in cpu.times) {
|
|
2296
|
+
const ticks = cpu.times[type];
|
|
2297
|
+
const prevTicks = prev.times[type];
|
|
2298
|
+
const diff = ticks - prevTicks;
|
|
2299
|
+
total += diff;
|
|
2300
|
+
if (type === "idle") {
|
|
2301
|
+
idle += diff;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
this.lastCpus = cpus;
|
|
2306
|
+
this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
1828
2309
|
const defaults = {
|
|
1829
2310
|
port: 3e3,
|
|
1830
2311
|
hostname: "localhost",
|
|
@@ -1836,51 +2317,67 @@ trace.getTracer("shokupan.application");
|
|
|
1836
2317
|
class Shokupan extends ShokupanRouter {
|
|
1837
2318
|
applicationConfig = {};
|
|
1838
2319
|
openApiSpec;
|
|
1839
|
-
middleware = [];
|
|
1840
2320
|
composedMiddleware;
|
|
2321
|
+
cpuMonitor;
|
|
2322
|
+
hookCache = /* @__PURE__ */ new Map();
|
|
2323
|
+
hooksInitialized = false;
|
|
1841
2324
|
get logger() {
|
|
1842
2325
|
return this.applicationConfig.logger;
|
|
1843
2326
|
}
|
|
1844
2327
|
constructor(applicationConfig = {}) {
|
|
1845
|
-
|
|
2328
|
+
const config = Object.assign({}, defaults, applicationConfig);
|
|
2329
|
+
const { hooks, ...routerConfig } = config;
|
|
2330
|
+
super(routerConfig);
|
|
1846
2331
|
this[$isApplication] = true;
|
|
1847
2332
|
this[$appRoot] = this;
|
|
1848
|
-
|
|
2333
|
+
this.applicationConfig = config;
|
|
2334
|
+
const { file, line } = getCallerInfo();
|
|
2335
|
+
this.metadata = {
|
|
2336
|
+
file,
|
|
2337
|
+
line,
|
|
2338
|
+
name: "ShokupanApplication"
|
|
2339
|
+
};
|
|
1849
2340
|
}
|
|
1850
2341
|
/**
|
|
1851
2342
|
* Adds middleware to the application.
|
|
1852
2343
|
*/
|
|
1853
2344
|
use(middleware) {
|
|
1854
2345
|
let trackedMiddleware = middleware;
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
if (callerLine) {
|
|
1865
|
-
const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
|
|
1866
|
-
if (match) {
|
|
1867
|
-
file = match[1];
|
|
1868
|
-
line = parseInt(match[2], 10);
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
} catch (e) {
|
|
2346
|
+
const { file, line } = getCallerInfo();
|
|
2347
|
+
if (!middleware.metadata) {
|
|
2348
|
+
middleware.metadata = {
|
|
2349
|
+
file,
|
|
2350
|
+
line,
|
|
2351
|
+
name: middleware.name || "middleware",
|
|
2352
|
+
isBuiltin: middleware.isBuiltin,
|
|
2353
|
+
pluginName: middleware.pluginName
|
|
2354
|
+
};
|
|
1872
2355
|
}
|
|
1873
2356
|
trackedMiddleware = async (ctx, next) => {
|
|
1874
2357
|
const c = ctx;
|
|
1875
2358
|
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2359
|
+
const metadata = middleware.metadata || {};
|
|
2360
|
+
const start = performance.now();
|
|
2361
|
+
const item = {
|
|
2362
|
+
name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
|
|
2363
|
+
file: metadata.file || file,
|
|
2364
|
+
line: metadata.line || line,
|
|
2365
|
+
isBuiltin: metadata.isBuiltin,
|
|
2366
|
+
startTime: start,
|
|
2367
|
+
duration: -1
|
|
2368
|
+
};
|
|
2369
|
+
c.handlerStack.push(item);
|
|
2370
|
+
try {
|
|
2371
|
+
return await middleware(ctx, next);
|
|
2372
|
+
} finally {
|
|
2373
|
+
item.duration = performance.now() - start;
|
|
2374
|
+
}
|
|
1881
2375
|
}
|
|
1882
2376
|
return middleware(ctx, next);
|
|
1883
2377
|
};
|
|
2378
|
+
trackedMiddleware.metadata = middleware.metadata;
|
|
2379
|
+
Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
|
|
2380
|
+
trackedMiddleware.order = this.middleware.length;
|
|
1884
2381
|
this.middleware.push(trackedMiddleware);
|
|
1885
2382
|
return this;
|
|
1886
2383
|
}
|
|
@@ -1892,6 +2389,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
1892
2389
|
this.startupHooks.push(callback);
|
|
1893
2390
|
return this;
|
|
1894
2391
|
}
|
|
2392
|
+
specAvailableHooks = [];
|
|
2393
|
+
/**
|
|
2394
|
+
* Registers a callback to be executed when the OpenAPI spec is available.
|
|
2395
|
+
* This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
|
|
2396
|
+
*/
|
|
2397
|
+
onSpecAvailable(callback) {
|
|
2398
|
+
this.specAvailableHooks.push(callback);
|
|
2399
|
+
return this;
|
|
2400
|
+
}
|
|
1895
2401
|
/**
|
|
1896
2402
|
* Starts the application server.
|
|
1897
2403
|
*
|
|
@@ -1908,8 +2414,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
1908
2414
|
}
|
|
1909
2415
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
1910
2416
|
this.openApiSpec = await generateOpenApi(this);
|
|
2417
|
+
for (const hook of this.specAvailableHooks) {
|
|
2418
|
+
await hook(this.openApiSpec);
|
|
2419
|
+
}
|
|
1911
2420
|
}
|
|
1912
2421
|
if (port === 0 && process.platform === "linux") ;
|
|
2422
|
+
if (this.applicationConfig.autoBackpressureFeedback) {
|
|
2423
|
+
this.cpuMonitor = new SystemCpuMonitor();
|
|
2424
|
+
this.cpuMonitor.start();
|
|
2425
|
+
}
|
|
1913
2426
|
const serveOptions = {
|
|
1914
2427
|
port: finalPort,
|
|
1915
2428
|
hostname: this.applicationConfig.hostname,
|
|
@@ -1934,7 +2447,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
1934
2447
|
};
|
|
1935
2448
|
let factory = this.applicationConfig.serverFactory;
|
|
1936
2449
|
if (!factory && typeof Bun === "undefined") {
|
|
1937
|
-
const { createHttpServer } = await import("./server-adapter-
|
|
2450
|
+
const { createHttpServer } = await import("./server-adapter-BWrEJbKL.js");
|
|
1938
2451
|
factory = createHttpServer();
|
|
1939
2452
|
}
|
|
1940
2453
|
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
@@ -2019,11 +2532,18 @@ class Shokupan extends ShokupanRouter {
|
|
|
2019
2532
|
}
|
|
2020
2533
|
async handleRequest(req, server) {
|
|
2021
2534
|
const request = req;
|
|
2022
|
-
const
|
|
2535
|
+
const controller = new AbortController();
|
|
2536
|
+
const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
|
|
2023
2537
|
const handle = async () => {
|
|
2538
|
+
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
2539
|
+
const msg = "Too Many Requests (CPU Backpressure)";
|
|
2540
|
+
const res = ctx.text(msg, 429);
|
|
2541
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2542
|
+
return res;
|
|
2543
|
+
}
|
|
2024
2544
|
try {
|
|
2025
|
-
if (this.
|
|
2026
|
-
await this.
|
|
2545
|
+
if (this.hasHook("onRequestStart")) {
|
|
2546
|
+
await this.executeHook("onRequestStart", ctx);
|
|
2027
2547
|
}
|
|
2028
2548
|
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2029
2549
|
const result = await fn(ctx, async () => {
|
|
@@ -2039,23 +2559,24 @@ class Shokupan extends ShokupanRouter {
|
|
|
2039
2559
|
response = result;
|
|
2040
2560
|
} else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
|
|
2041
2561
|
response = ctx._finalResponse;
|
|
2042
|
-
} else if ((result === null || result === void 0) && ctx.response.status === 404) {
|
|
2043
|
-
const span = asyncContext.getStore()?.get("span");
|
|
2044
|
-
if (span) span.setAttribute("http.status_code", 404);
|
|
2045
|
-
response = ctx.text("Not Found", 404);
|
|
2046
2562
|
} else if (result === null || result === void 0) {
|
|
2047
|
-
if (ctx._finalResponse
|
|
2048
|
-
|
|
2563
|
+
if (ctx._finalResponse instanceof Response) {
|
|
2564
|
+
response = ctx._finalResponse;
|
|
2565
|
+
} else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
|
|
2566
|
+
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2567
|
+
} else {
|
|
2568
|
+
response = ctx.text("Not Found", 404);
|
|
2569
|
+
}
|
|
2049
2570
|
} else if (typeof result === "object") {
|
|
2050
2571
|
response = ctx.json(result);
|
|
2051
2572
|
} else {
|
|
2052
2573
|
response = ctx.text(String(result));
|
|
2053
2574
|
}
|
|
2054
|
-
if (this.
|
|
2055
|
-
await this.
|
|
2575
|
+
if (this.hasHook("onRequestEnd")) {
|
|
2576
|
+
await this.executeHook("onRequestEnd", ctx);
|
|
2056
2577
|
}
|
|
2057
|
-
if (this.
|
|
2058
|
-
await this.
|
|
2578
|
+
if (this.hasHook("onResponseStart")) {
|
|
2579
|
+
await this.executeHook("onResponseStart", ctx, response);
|
|
2059
2580
|
}
|
|
2060
2581
|
return response;
|
|
2061
2582
|
} catch (err) {
|
|
@@ -2065,28 +2586,21 @@ class Shokupan extends ShokupanRouter {
|
|
|
2065
2586
|
const status = err.status || err.statusCode || 500;
|
|
2066
2587
|
const body = { error: err.message || "Internal Server Error" };
|
|
2067
2588
|
if (err.errors) body.errors = err.errors;
|
|
2068
|
-
if (this.
|
|
2069
|
-
|
|
2070
|
-
await this.applicationConfig.hooks.onError(err, ctx);
|
|
2071
|
-
} catch (hookErr) {
|
|
2072
|
-
console.error("Error in onError hook:", hookErr);
|
|
2073
|
-
}
|
|
2589
|
+
if (this.hasHook("onError")) {
|
|
2590
|
+
await this.executeHook("onError", err, ctx);
|
|
2074
2591
|
}
|
|
2075
2592
|
return ctx.json(body, status);
|
|
2076
2593
|
}
|
|
2077
2594
|
};
|
|
2078
2595
|
let executionPromise = handle();
|
|
2079
2596
|
const timeoutMs = this.applicationConfig.requestTimeout;
|
|
2080
|
-
if (timeoutMs && timeoutMs > 0
|
|
2597
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
2081
2598
|
let timeoutId;
|
|
2082
2599
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2083
2600
|
timeoutId = setTimeout(async () => {
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
}
|
|
2088
|
-
} catch (e) {
|
|
2089
|
-
console.error("Error in onRequestTimeout hook:", e);
|
|
2601
|
+
controller.abort();
|
|
2602
|
+
if (this.hasHook("onRequestTimeout")) {
|
|
2603
|
+
await this.executeHook("onRequestTimeout", ctx);
|
|
2090
2604
|
}
|
|
2091
2605
|
reject(new Error("Request Timeout"));
|
|
2092
2606
|
}, timeoutMs);
|
|
@@ -2100,12 +2614,56 @@ class Shokupan extends ShokupanRouter {
|
|
|
2100
2614
|
console.error("Unexpected error in request execution:", err);
|
|
2101
2615
|
return ctx.text("Internal Server Error", 500);
|
|
2102
2616
|
}).then(async (res) => {
|
|
2103
|
-
if (this.
|
|
2104
|
-
await this.
|
|
2617
|
+
if (this.hasHook("onResponseEnd")) {
|
|
2618
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2105
2619
|
}
|
|
2106
2620
|
return res;
|
|
2107
2621
|
});
|
|
2108
2622
|
}
|
|
2623
|
+
ensureHooksInitialized() {
|
|
2624
|
+
const hooks = this.applicationConfig.hooks;
|
|
2625
|
+
if (hooks) {
|
|
2626
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2627
|
+
const hookTypes = [
|
|
2628
|
+
"onRequestStart",
|
|
2629
|
+
"onRequestEnd",
|
|
2630
|
+
"onResponseStart",
|
|
2631
|
+
"onResponseEnd",
|
|
2632
|
+
"onError",
|
|
2633
|
+
"beforeValidate",
|
|
2634
|
+
"afterValidate",
|
|
2635
|
+
"onRequestTimeout",
|
|
2636
|
+
"onReadTimeout",
|
|
2637
|
+
"onWriteTimeout"
|
|
2638
|
+
];
|
|
2639
|
+
for (const type of hookTypes) {
|
|
2640
|
+
const fns = [];
|
|
2641
|
+
for (const h of hookList) {
|
|
2642
|
+
if (h[type]) fns.push(h[type]);
|
|
2643
|
+
}
|
|
2644
|
+
if (fns.length > 0) {
|
|
2645
|
+
this.hookCache.set(type, fns);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
this.hooksInitialized = true;
|
|
2650
|
+
}
|
|
2651
|
+
async executeHook(name, ...args) {
|
|
2652
|
+
if (!this.hooksInitialized) {
|
|
2653
|
+
this.ensureHooksInitialized();
|
|
2654
|
+
}
|
|
2655
|
+
const fns = this.hookCache.get(name);
|
|
2656
|
+
if (!fns) return;
|
|
2657
|
+
for (const fn of fns) {
|
|
2658
|
+
await fn(...args);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
hasHook(name) {
|
|
2662
|
+
if (!this.hooksInitialized) {
|
|
2663
|
+
this.ensureHooksInitialized();
|
|
2664
|
+
}
|
|
2665
|
+
return this.hookCache.has(name);
|
|
2666
|
+
}
|
|
2109
2667
|
}
|
|
2110
2668
|
class AuthPlugin extends ShokupanRouter {
|
|
2111
2669
|
constructor(authConfig) {
|
|
@@ -2310,7 +2868,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2310
2868
|
/**
|
|
2311
2869
|
* Middleware to verify JWT
|
|
2312
2870
|
*/
|
|
2313
|
-
|
|
2871
|
+
getMiddleware() {
|
|
2314
2872
|
return async (ctx, next) => {
|
|
2315
2873
|
const authHeader = ctx.req.headers.get("Authorization");
|
|
2316
2874
|
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
@@ -2331,12 +2889,16 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2331
2889
|
}
|
|
2332
2890
|
function Compression(options = {}) {
|
|
2333
2891
|
const threshold = options.threshold ?? 512;
|
|
2334
|
-
|
|
2892
|
+
const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
|
|
2335
2893
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
2336
2894
|
let method = null;
|
|
2337
2895
|
if (acceptEncoding.includes("br")) method = "br";
|
|
2338
|
-
else if (acceptEncoding.includes("zstd"))
|
|
2339
|
-
|
|
2896
|
+
else if (acceptEncoding.includes("zstd")) {
|
|
2897
|
+
if (typeof Bun === "undefined") {
|
|
2898
|
+
throw new Error("zstd compression is only available in Bun runtime. Client requested zstd but server is running on Node.js.");
|
|
2899
|
+
}
|
|
2900
|
+
method = "zstd";
|
|
2901
|
+
} else if (acceptEncoding.includes("gzip")) method = "gzip";
|
|
2340
2902
|
else if (acceptEncoding.includes("deflate")) method = "deflate";
|
|
2341
2903
|
if (!method) return next();
|
|
2342
2904
|
let response = await next();
|
|
@@ -2345,18 +2907,34 @@ function Compression(options = {}) {
|
|
|
2345
2907
|
}
|
|
2346
2908
|
if (response instanceof Response) {
|
|
2347
2909
|
if (response.headers.has("Content-Encoding")) return response;
|
|
2348
|
-
|
|
2349
|
-
|
|
2910
|
+
let body;
|
|
2911
|
+
let bodySize;
|
|
2912
|
+
if (ctx._rawBody !== void 0) {
|
|
2913
|
+
if (typeof ctx._rawBody === "string") {
|
|
2914
|
+
const encoded = new TextEncoder().encode(ctx._rawBody);
|
|
2915
|
+
body = encoded;
|
|
2916
|
+
bodySize = encoded.byteLength;
|
|
2917
|
+
} else if (ctx._rawBody instanceof Uint8Array) {
|
|
2918
|
+
body = ctx._rawBody;
|
|
2919
|
+
bodySize = ctx._rawBody.byteLength;
|
|
2920
|
+
} else {
|
|
2921
|
+
body = ctx._rawBody;
|
|
2922
|
+
bodySize = body.byteLength;
|
|
2923
|
+
}
|
|
2924
|
+
} else {
|
|
2925
|
+
body = await response.arrayBuffer();
|
|
2926
|
+
bodySize = body.byteLength;
|
|
2927
|
+
}
|
|
2928
|
+
if (bodySize < threshold) {
|
|
2350
2929
|
return new Response(body, {
|
|
2351
2930
|
status: response.status,
|
|
2352
2931
|
statusText: response.statusText,
|
|
2353
|
-
headers: response.headers
|
|
2932
|
+
headers: new Headers(response.headers)
|
|
2354
2933
|
});
|
|
2355
2934
|
}
|
|
2356
2935
|
let compressed;
|
|
2357
2936
|
switch (method) {
|
|
2358
2937
|
case "br":
|
|
2359
|
-
const zlib = require("node:zlib");
|
|
2360
2938
|
compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
|
|
2361
2939
|
params: {
|
|
2362
2940
|
[zlib.constants.BROTLI_PARAM_QUALITY]: 4
|
|
@@ -2367,13 +2945,19 @@ function Compression(options = {}) {
|
|
|
2367
2945
|
}));
|
|
2368
2946
|
break;
|
|
2369
2947
|
case "gzip":
|
|
2370
|
-
compressed =
|
|
2948
|
+
compressed = await new Promise((res, rej) => zlib.gzip(body, (err, data) => {
|
|
2949
|
+
if (err) return rej(err);
|
|
2950
|
+
res(data);
|
|
2951
|
+
}));
|
|
2371
2952
|
break;
|
|
2372
2953
|
case "zstd":
|
|
2373
2954
|
compressed = await Bun.zstdCompress(body);
|
|
2374
2955
|
break;
|
|
2375
2956
|
default:
|
|
2376
|
-
compressed =
|
|
2957
|
+
compressed = await new Promise((res, rej) => zlib.deflate(body, (err, data) => {
|
|
2958
|
+
if (err) return rej(err);
|
|
2959
|
+
res(data);
|
|
2960
|
+
}));
|
|
2377
2961
|
break;
|
|
2378
2962
|
}
|
|
2379
2963
|
const headers = new Headers(response.headers);
|
|
@@ -2387,6 +2971,9 @@ function Compression(options = {}) {
|
|
|
2387
2971
|
}
|
|
2388
2972
|
return response;
|
|
2389
2973
|
};
|
|
2974
|
+
compressionMiddleware.isBuiltin = true;
|
|
2975
|
+
compressionMiddleware.pluginName = "Compression";
|
|
2976
|
+
return compressionMiddleware;
|
|
2390
2977
|
}
|
|
2391
2978
|
function Cors(options = {}) {
|
|
2392
2979
|
const defaults2 = {
|
|
@@ -2396,7 +2983,7 @@ function Cors(options = {}) {
|
|
|
2396
2983
|
optionsSuccessStatus: 204
|
|
2397
2984
|
};
|
|
2398
2985
|
const opts = { ...defaults2, ...options };
|
|
2399
|
-
|
|
2986
|
+
const corsMiddleware = async function CorsMiddleware(ctx, next) {
|
|
2400
2987
|
const headers = new Headers();
|
|
2401
2988
|
const origin = ctx.headers.get("origin");
|
|
2402
2989
|
const set = (k, v) => headers.set(k, v);
|
|
@@ -2458,6 +3045,9 @@ function Cors(options = {}) {
|
|
|
2458
3045
|
}
|
|
2459
3046
|
return response;
|
|
2460
3047
|
};
|
|
3048
|
+
corsMiddleware.isBuiltin = true;
|
|
3049
|
+
corsMiddleware.pluginName = "Cors";
|
|
3050
|
+
return corsMiddleware;
|
|
2461
3051
|
}
|
|
2462
3052
|
function useExpress(expressMiddleware) {
|
|
2463
3053
|
return async (ctx, next) => {
|
|
@@ -2625,7 +3215,33 @@ const safelyGetBody = async (ctx) => {
|
|
|
2625
3215
|
return {};
|
|
2626
3216
|
}
|
|
2627
3217
|
};
|
|
3218
|
+
function getValidator(schema) {
|
|
3219
|
+
if (isZod(schema)) {
|
|
3220
|
+
return (data) => validateZod(schema, data);
|
|
3221
|
+
}
|
|
3222
|
+
if (isTypeBox(schema)) {
|
|
3223
|
+
return (data) => validateTypeBox(schema, data);
|
|
3224
|
+
}
|
|
3225
|
+
if (isAjv(schema)) {
|
|
3226
|
+
return (data) => validateAjv(schema, data);
|
|
3227
|
+
}
|
|
3228
|
+
if (isValibotWrapper(schema)) {
|
|
3229
|
+
return (data) => validateValibotWrapper(schema, data);
|
|
3230
|
+
}
|
|
3231
|
+
if (isClass(schema)) {
|
|
3232
|
+
return (data) => validateClassValidator(schema, data);
|
|
3233
|
+
}
|
|
3234
|
+
if (typeof schema === "function") {
|
|
3235
|
+
return schema;
|
|
3236
|
+
}
|
|
3237
|
+
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
3238
|
+
}
|
|
2628
3239
|
function validate(config) {
|
|
3240
|
+
const validators = {};
|
|
3241
|
+
if (config.params) validators.params = getValidator(config.params);
|
|
3242
|
+
if (config.query) validators.query = getValidator(config.query);
|
|
3243
|
+
if (config.headers) validators.headers = getValidator(config.headers);
|
|
3244
|
+
if (config.body) validators.body = getValidator(config.body);
|
|
2629
3245
|
return async (ctx, next) => {
|
|
2630
3246
|
const dataToValidate = {};
|
|
2631
3247
|
if (config.params) dataToValidate.params = ctx.params;
|
|
@@ -2644,21 +3260,21 @@ function validate(config) {
|
|
|
2644
3260
|
if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
|
|
2645
3261
|
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
2646
3262
|
}
|
|
2647
|
-
if (
|
|
2648
|
-
ctx.params = await
|
|
3263
|
+
if (validators.params) {
|
|
3264
|
+
ctx.params = await validators.params(ctx.params);
|
|
2649
3265
|
}
|
|
2650
3266
|
let validQuery;
|
|
2651
|
-
if (
|
|
2652
|
-
validQuery = await
|
|
3267
|
+
if (validators.query && queryObj) {
|
|
3268
|
+
validQuery = await validators.query(queryObj);
|
|
2653
3269
|
}
|
|
2654
|
-
if (
|
|
3270
|
+
if (validators.headers) {
|
|
2655
3271
|
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
2656
|
-
await
|
|
3272
|
+
await validators.headers(headersObj);
|
|
2657
3273
|
}
|
|
2658
3274
|
let validBody;
|
|
2659
|
-
if (
|
|
3275
|
+
if (validators.body) {
|
|
2660
3276
|
const b = body ?? await safelyGetBody(ctx);
|
|
2661
|
-
validBody = await
|
|
3277
|
+
validBody = await validators.body(b);
|
|
2662
3278
|
const req = ctx.req;
|
|
2663
3279
|
req._bodyValue = validBody;
|
|
2664
3280
|
Object.defineProperty(req, "json", {
|
|
@@ -2677,36 +3293,6 @@ function validate(config) {
|
|
|
2677
3293
|
return next();
|
|
2678
3294
|
};
|
|
2679
3295
|
}
|
|
2680
|
-
async function runValidation(schema, data) {
|
|
2681
|
-
if (isZod(schema)) {
|
|
2682
|
-
return validateZod(schema, data);
|
|
2683
|
-
}
|
|
2684
|
-
if (isTypeBox(schema)) {
|
|
2685
|
-
return validateTypeBox(schema, data);
|
|
2686
|
-
}
|
|
2687
|
-
if (isAjv(schema)) {
|
|
2688
|
-
return validateAjv(schema, data);
|
|
2689
|
-
}
|
|
2690
|
-
if (isValibotWrapper(schema)) {
|
|
2691
|
-
return validateValibotWrapper(schema, data);
|
|
2692
|
-
}
|
|
2693
|
-
if (isClass(schema)) {
|
|
2694
|
-
return validateClassValidator(schema, data);
|
|
2695
|
-
}
|
|
2696
|
-
if (isTypeBox(schema)) {
|
|
2697
|
-
return validateTypeBox(schema, data);
|
|
2698
|
-
}
|
|
2699
|
-
if (isAjv(schema)) {
|
|
2700
|
-
return validateAjv(schema, data);
|
|
2701
|
-
}
|
|
2702
|
-
if (isValibotWrapper(schema)) {
|
|
2703
|
-
return validateValibotWrapper(schema, data);
|
|
2704
|
-
}
|
|
2705
|
-
if (typeof schema === "function") {
|
|
2706
|
-
return schema(data);
|
|
2707
|
-
}
|
|
2708
|
-
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
2709
|
-
}
|
|
2710
3296
|
const ajv = new Ajv({ coerceTypes: true, allErrors: true });
|
|
2711
3297
|
addFormats(ajv);
|
|
2712
3298
|
const compiledValidators = /* @__PURE__ */ new WeakMap();
|
|
@@ -2721,17 +3307,18 @@ function openApiValidator() {
|
|
|
2721
3307
|
cache = compileValidators(app.openApiSpec);
|
|
2722
3308
|
compiledValidators.set(app, cache);
|
|
2723
3309
|
}
|
|
2724
|
-
const method = ctx.req.method.toLowerCase();
|
|
2725
3310
|
let matchPath;
|
|
2726
|
-
|
|
3311
|
+
let matchParams = {};
|
|
3312
|
+
if (cache.validators.has(ctx.path)) {
|
|
2727
3313
|
matchPath = ctx.path;
|
|
2728
3314
|
} else {
|
|
2729
|
-
for (const
|
|
2730
|
-
const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
|
|
2731
|
-
const regex = new RegExp(regexStr);
|
|
3315
|
+
for (const [path, { regex, paramNames }] of cache.paths) {
|
|
2732
3316
|
const match = regex.exec(ctx.path);
|
|
2733
3317
|
if (match) {
|
|
2734
|
-
matchPath =
|
|
3318
|
+
matchPath = path;
|
|
3319
|
+
paramNames.forEach((name, i) => {
|
|
3320
|
+
matchParams[name] = match[i + 1];
|
|
3321
|
+
});
|
|
2735
3322
|
break;
|
|
2736
3323
|
}
|
|
2737
3324
|
}
|
|
@@ -2739,7 +3326,8 @@ function openApiValidator() {
|
|
|
2739
3326
|
if (!matchPath) {
|
|
2740
3327
|
return next();
|
|
2741
3328
|
}
|
|
2742
|
-
const
|
|
3329
|
+
const method = ctx.req.method.toLowerCase();
|
|
3330
|
+
const validators = cache.validators.get(matchPath)?.[method];
|
|
2743
3331
|
if (!validators) {
|
|
2744
3332
|
return next();
|
|
2745
3333
|
}
|
|
@@ -2764,21 +3352,7 @@ function openApiValidator() {
|
|
|
2764
3352
|
}
|
|
2765
3353
|
}
|
|
2766
3354
|
if (validators.params) {
|
|
2767
|
-
|
|
2768
|
-
if (Object.keys(params).length === 0 && matchPath) {
|
|
2769
|
-
const paramNames = (matchPath.match(/{([^}]+)}/g) || []).map((s) => s.slice(1, -1));
|
|
2770
|
-
if (paramNames.length > 0) {
|
|
2771
|
-
const regexStr = "^" + matchPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
|
|
2772
|
-
const regex = new RegExp(regexStr);
|
|
2773
|
-
const match = regex.exec(ctx.path);
|
|
2774
|
-
if (match) {
|
|
2775
|
-
params = {};
|
|
2776
|
-
paramNames.forEach((name, i) => {
|
|
2777
|
-
params[name] = match[i + 1];
|
|
2778
|
-
});
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
3355
|
+
const params = { ...matchParams, ...ctx.params };
|
|
2782
3356
|
const valid = validators.params(params);
|
|
2783
3357
|
if (!valid && validators.params.errors) {
|
|
2784
3358
|
errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
|
|
@@ -2798,15 +3372,27 @@ function openApiValidator() {
|
|
|
2798
3372
|
};
|
|
2799
3373
|
}
|
|
2800
3374
|
function compileValidators(spec) {
|
|
2801
|
-
const
|
|
3375
|
+
const validators = /* @__PURE__ */ new Map();
|
|
3376
|
+
const paths = /* @__PURE__ */ new Map();
|
|
2802
3377
|
for (const [path, pathItem] of Object.entries(spec.paths || {})) {
|
|
3378
|
+
if (path.includes("{")) {
|
|
3379
|
+
const paramNames = [];
|
|
3380
|
+
const regexStr = "^" + path.replace(/{([^}]+)}/g, (_, name) => {
|
|
3381
|
+
paramNames.push(name);
|
|
3382
|
+
return "([^/]+)";
|
|
3383
|
+
}) + "$";
|
|
3384
|
+
paths.set(path, {
|
|
3385
|
+
regex: new RegExp(regexStr),
|
|
3386
|
+
paramNames
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
2803
3389
|
const pathValidators = {};
|
|
2804
3390
|
for (const [method, operation] of Object.entries(pathItem)) {
|
|
2805
3391
|
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
2806
3392
|
const oper = operation;
|
|
2807
|
-
const
|
|
3393
|
+
const opValidators = {};
|
|
2808
3394
|
if (oper.requestBody?.content?.["application/json"]?.schema) {
|
|
2809
|
-
|
|
3395
|
+
opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
|
|
2810
3396
|
}
|
|
2811
3397
|
const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
|
|
2812
3398
|
const queryProps = {};
|
|
@@ -2828,85 +3414,41 @@ function compileValidators(spec) {
|
|
|
2828
3414
|
}
|
|
2829
3415
|
}
|
|
2830
3416
|
if (Object.keys(queryProps).length > 0) {
|
|
2831
|
-
|
|
3417
|
+
opValidators.query = ajv.compile({
|
|
2832
3418
|
type: "object",
|
|
2833
3419
|
properties: queryProps,
|
|
2834
3420
|
required: queryRequired.length > 0 ? queryRequired : void 0
|
|
2835
3421
|
});
|
|
2836
3422
|
}
|
|
2837
3423
|
if (Object.keys(pathProps).length > 0) {
|
|
2838
|
-
|
|
3424
|
+
opValidators.params = ajv.compile({
|
|
2839
3425
|
type: "object",
|
|
2840
3426
|
properties: pathProps,
|
|
2841
3427
|
required: pathRequired.length > 0 ? pathRequired : void 0
|
|
2842
3428
|
});
|
|
2843
3429
|
}
|
|
2844
3430
|
if (Object.keys(headerProps).length > 0) {
|
|
2845
|
-
|
|
3431
|
+
opValidators.headers = ajv.compile({
|
|
2846
3432
|
type: "object",
|
|
2847
3433
|
properties: headerProps,
|
|
2848
3434
|
required: headerRequired.length > 0 ? headerRequired : void 0
|
|
2849
3435
|
});
|
|
2850
3436
|
}
|
|
2851
|
-
pathValidators[method] =
|
|
3437
|
+
pathValidators[method] = opValidators;
|
|
2852
3438
|
}
|
|
2853
|
-
|
|
3439
|
+
validators.set(path, pathValidators);
|
|
2854
3440
|
}
|
|
2855
|
-
return
|
|
3441
|
+
return { paths, validators };
|
|
2856
3442
|
}
|
|
2857
|
-
function
|
|
2858
|
-
const
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
3443
|
+
function precompileValidators(app, spec) {
|
|
3444
|
+
const cache = compileValidators(spec);
|
|
3445
|
+
compiledValidators.set(app, cache);
|
|
3446
|
+
}
|
|
3447
|
+
function enableOpenApiValidation(app) {
|
|
3448
|
+
app.use(openApiValidator());
|
|
3449
|
+
app.onSpecAvailable((spec) => {
|
|
3450
|
+
precompileValidators(app, spec);
|
|
2865
3451
|
});
|
|
2866
|
-
const skip = options.skip || (() => false);
|
|
2867
|
-
const hits = /* @__PURE__ */ new Map();
|
|
2868
|
-
const interval = setInterval(() => {
|
|
2869
|
-
const now = Date.now();
|
|
2870
|
-
for (const [key, record] of hits.entries()) {
|
|
2871
|
-
if (record.resetTime <= now) {
|
|
2872
|
-
hits.delete(key);
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
}, windowMs);
|
|
2876
|
-
interval.unref?.();
|
|
2877
|
-
return async (ctx, next) => {
|
|
2878
|
-
if (skip(ctx)) return next();
|
|
2879
|
-
const key = keyGenerator(ctx);
|
|
2880
|
-
const now = Date.now();
|
|
2881
|
-
let record = hits.get(key);
|
|
2882
|
-
if (!record || record.resetTime <= now) {
|
|
2883
|
-
record = {
|
|
2884
|
-
hits: 0,
|
|
2885
|
-
resetTime: now + windowMs
|
|
2886
|
-
};
|
|
2887
|
-
hits.set(key, record);
|
|
2888
|
-
}
|
|
2889
|
-
record.hits++;
|
|
2890
|
-
const remaining = Math.max(0, max - record.hits);
|
|
2891
|
-
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
2892
|
-
if (record.hits > max) {
|
|
2893
|
-
if (headers) {
|
|
2894
|
-
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2895
|
-
res.headers.set("X-RateLimit-Limit", String(max));
|
|
2896
|
-
res.headers.set("X-RateLimit-Remaining", "0");
|
|
2897
|
-
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2898
|
-
return res;
|
|
2899
|
-
}
|
|
2900
|
-
return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
2901
|
-
}
|
|
2902
|
-
const response = await next();
|
|
2903
|
-
if (response instanceof Response && headers) {
|
|
2904
|
-
response.headers.set("X-RateLimit-Limit", String(max));
|
|
2905
|
-
response.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
2906
|
-
response.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
2907
|
-
}
|
|
2908
|
-
return response;
|
|
2909
|
-
};
|
|
2910
3452
|
}
|
|
2911
3453
|
const eta = new Eta();
|
|
2912
3454
|
class ScalarPlugin extends ShokupanRouter {
|
|
@@ -2983,7 +3525,7 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
2983
3525
|
}
|
|
2984
3526
|
}
|
|
2985
3527
|
function SecurityHeaders(options = {}) {
|
|
2986
|
-
|
|
3528
|
+
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
2987
3529
|
const headers = {};
|
|
2988
3530
|
const set = (k, v) => headers[k] = v;
|
|
2989
3531
|
if (options.dnsPrefetchControl !== false) {
|
|
@@ -3037,6 +3579,9 @@ function SecurityHeaders(options = {}) {
|
|
|
3037
3579
|
}
|
|
3038
3580
|
return response;
|
|
3039
3581
|
};
|
|
3582
|
+
securityHeadersMiddleware.isBuiltin = true;
|
|
3583
|
+
securityHeadersMiddleware.pluginName = "SecurityHeaders";
|
|
3584
|
+
return securityHeadersMiddleware;
|
|
3040
3585
|
}
|
|
3041
3586
|
class Cookie {
|
|
3042
3587
|
maxAge;
|
|
@@ -3150,7 +3695,7 @@ function Session(options) {
|
|
|
3150
3695
|
const resave = options.resave === void 0 ? true : options.resave;
|
|
3151
3696
|
const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
|
|
3152
3697
|
const rolling = options.rolling || false;
|
|
3153
|
-
|
|
3698
|
+
const sessionMiddleware = async function SessionMiddleware(ctx, next) {
|
|
3154
3699
|
let reqSessionId = null;
|
|
3155
3700
|
const cookieHeader = ctx.req.headers.get("cookie");
|
|
3156
3701
|
const cookies = {};
|
|
@@ -3286,6 +3831,9 @@ function Session(options) {
|
|
|
3286
3831
|
}
|
|
3287
3832
|
return result;
|
|
3288
3833
|
};
|
|
3834
|
+
sessionMiddleware.isBuiltin = true;
|
|
3835
|
+
sessionMiddleware.pluginName = "Session";
|
|
3836
|
+
return sessionMiddleware;
|
|
3289
3837
|
}
|
|
3290
3838
|
export {
|
|
3291
3839
|
$appRoot,
|
|
@@ -3326,6 +3874,7 @@ export {
|
|
|
3326
3874
|
Put,
|
|
3327
3875
|
Query,
|
|
3328
3876
|
RateLimit,
|
|
3877
|
+
RateLimitMiddleware,
|
|
3329
3878
|
Req,
|
|
3330
3879
|
RouteParamType,
|
|
3331
3880
|
RouterRegistry,
|
|
@@ -3341,8 +3890,11 @@ export {
|
|
|
3341
3890
|
Spec,
|
|
3342
3891
|
Use,
|
|
3343
3892
|
ValidationError,
|
|
3893
|
+
compileValidators,
|
|
3344
3894
|
compose,
|
|
3895
|
+
enableOpenApiValidation,
|
|
3345
3896
|
openApiValidator,
|
|
3897
|
+
precompileValidators,
|
|
3346
3898
|
useExpress,
|
|
3347
3899
|
valibot,
|
|
3348
3900
|
validate
|