shokupan 0.2.0 → 0.3.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/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 +14 -6
- package/dist/decorators.d.ts +47 -0
- package/dist/index.cjs +895 -379
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +887 -373
- 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 +6 -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 +48 -3
- 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 +96 -4
- package/dist/util/cpu-monitor.d.ts +11 -0
- package/dist/util/stack.d.ts +8 -0
- package/package.json +4 -2
- 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,157 @@ 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
|
+
`);
|
|
1380
|
+
});
|
|
1381
|
+
const datastore = {
|
|
1382
|
+
get(store, key) {
|
|
1383
|
+
return db.select(new RecordId(store, key));
|
|
1384
|
+
},
|
|
1385
|
+
set(store, key, value) {
|
|
1386
|
+
return db.create(new RecordId(store, key)).content(value);
|
|
1387
|
+
},
|
|
1388
|
+
async query(query, vars) {
|
|
1389
|
+
try {
|
|
1390
|
+
const r = await db.query(query, vars).collect();
|
|
1391
|
+
return r;
|
|
1392
|
+
} catch (e) {
|
|
1393
|
+
console.error("DS ERROR:", e);
|
|
1394
|
+
throw e;
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
ready
|
|
1398
|
+
};
|
|
1399
|
+
process.on("exit", async () => {
|
|
1400
|
+
await db.close();
|
|
1401
|
+
});
|
|
1161
1402
|
const tracer = trace.getTracer("shokupan.middleware");
|
|
1162
1403
|
function traceHandler(fn, name) {
|
|
1163
1404
|
return async function(...args) {
|
|
@@ -1181,6 +1422,35 @@ function traceHandler(fn, name) {
|
|
|
1181
1422
|
});
|
|
1182
1423
|
};
|
|
1183
1424
|
}
|
|
1425
|
+
function getCallerInfo(skipFrames = 1) {
|
|
1426
|
+
let file = "unknown";
|
|
1427
|
+
let line = 0;
|
|
1428
|
+
try {
|
|
1429
|
+
const err = new Error();
|
|
1430
|
+
const stack = err.stack?.split("\n") || [];
|
|
1431
|
+
let found = 0;
|
|
1432
|
+
for (let i = 1; i < stack.length; i++) {
|
|
1433
|
+
const l = stack[i];
|
|
1434
|
+
if (!l.includes(":")) continue;
|
|
1435
|
+
if (l.includes("node_modules")) continue;
|
|
1436
|
+
if (l.includes("bun:main")) continue;
|
|
1437
|
+
if (l.includes("src/util/stack.ts")) continue;
|
|
1438
|
+
if (l.includes("src/router.ts")) continue;
|
|
1439
|
+
if (l.includes("src/shokupan.ts")) continue;
|
|
1440
|
+
found++;
|
|
1441
|
+
if (found >= skipFrames) {
|
|
1442
|
+
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
1443
|
+
if (match) {
|
|
1444
|
+
file = match[1];
|
|
1445
|
+
line = parseInt(match[2], 10);
|
|
1446
|
+
return { file, line };
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
} catch (e) {
|
|
1451
|
+
}
|
|
1452
|
+
return { file, line };
|
|
1453
|
+
}
|
|
1184
1454
|
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1185
1455
|
const ShokupanApplicationTree = {};
|
|
1186
1456
|
class ShokupanRouter {
|
|
@@ -1200,6 +1470,7 @@ class ShokupanRouter {
|
|
|
1200
1470
|
[$parent] = null;
|
|
1201
1471
|
[$childRouters] = [];
|
|
1202
1472
|
[$childControllers] = [];
|
|
1473
|
+
middleware = [];
|
|
1203
1474
|
get rootConfig() {
|
|
1204
1475
|
return this[$appRoot]?.applicationConfig;
|
|
1205
1476
|
}
|
|
@@ -1208,7 +1479,54 @@ class ShokupanRouter {
|
|
|
1208
1479
|
}
|
|
1209
1480
|
[$routes] = [];
|
|
1210
1481
|
// Public via Symbol for OpenAPI generator
|
|
1482
|
+
trie = new RouterTrie();
|
|
1483
|
+
metadata;
|
|
1484
|
+
// Metadata for the router itself
|
|
1211
1485
|
currentGuards = [];
|
|
1486
|
+
// Registry Accessor
|
|
1487
|
+
getComponentRegistry() {
|
|
1488
|
+
const routes = this[$routes].map((r) => ({
|
|
1489
|
+
type: "route",
|
|
1490
|
+
path: r.path,
|
|
1491
|
+
method: r.method,
|
|
1492
|
+
metadata: r.metadata,
|
|
1493
|
+
handlerName: r.handler.name,
|
|
1494
|
+
tags: r.handlerSpec?.tags,
|
|
1495
|
+
order: r.order,
|
|
1496
|
+
_fn: r.handler
|
|
1497
|
+
// Expose handler for debugging instrumentation
|
|
1498
|
+
}));
|
|
1499
|
+
const mw = this.middleware;
|
|
1500
|
+
const middleware = mw ? mw.map((m) => ({
|
|
1501
|
+
name: m.name || "middleware",
|
|
1502
|
+
metadata: m.metadata,
|
|
1503
|
+
order: m.order,
|
|
1504
|
+
_fn: m
|
|
1505
|
+
// Expose function for debugging instrumentation
|
|
1506
|
+
})) : [];
|
|
1507
|
+
const routers = this[$childRouters].map((r) => ({
|
|
1508
|
+
type: "router",
|
|
1509
|
+
path: r[$mountPath],
|
|
1510
|
+
metadata: r.metadata,
|
|
1511
|
+
children: r.getComponentRegistry()
|
|
1512
|
+
}));
|
|
1513
|
+
const controllers = this[$childControllers].map((c) => {
|
|
1514
|
+
return {
|
|
1515
|
+
type: "controller",
|
|
1516
|
+
path: c[$mountPath] || "/",
|
|
1517
|
+
name: c.constructor.name,
|
|
1518
|
+
metadata: c.metadata
|
|
1519
|
+
// Check if we can store this
|
|
1520
|
+
};
|
|
1521
|
+
});
|
|
1522
|
+
return {
|
|
1523
|
+
metadata: this.metadata,
|
|
1524
|
+
middleware,
|
|
1525
|
+
routes,
|
|
1526
|
+
routers,
|
|
1527
|
+
controllers
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1212
1530
|
isRouterInstance(target) {
|
|
1213
1531
|
return typeof target === "object" && target !== null && $isRouter in target;
|
|
1214
1532
|
}
|
|
@@ -1234,6 +1552,14 @@ class ShokupanRouter {
|
|
|
1234
1552
|
throw new Error("Router is already mounted");
|
|
1235
1553
|
}
|
|
1236
1554
|
controller[$mountPath] = prefix;
|
|
1555
|
+
if (!controller.metadata) {
|
|
1556
|
+
const info = getCallerInfo();
|
|
1557
|
+
controller.metadata = {
|
|
1558
|
+
file: info.file,
|
|
1559
|
+
line: info.line,
|
|
1560
|
+
name: "MountedRouter"
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1237
1563
|
this[$childRouters].push(controller);
|
|
1238
1564
|
controller[$parent] = this;
|
|
1239
1565
|
const setRouterContext = (router) => {
|
|
@@ -1266,6 +1592,12 @@ class ShokupanRouter {
|
|
|
1266
1592
|
}
|
|
1267
1593
|
}
|
|
1268
1594
|
instance[$mountPath] = prefix;
|
|
1595
|
+
const info = getCallerInfo();
|
|
1596
|
+
instance.metadata = {
|
|
1597
|
+
file: info.file,
|
|
1598
|
+
line: info.line,
|
|
1599
|
+
name: instance.constructor.name
|
|
1600
|
+
};
|
|
1269
1601
|
this[$childControllers].push(instance);
|
|
1270
1602
|
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
1271
1603
|
const proto = Object.getPrototypeOf(instance);
|
|
@@ -1349,14 +1681,39 @@ class ShokupanRouter {
|
|
|
1349
1681
|
for (const arg of sortedArgs) {
|
|
1350
1682
|
switch (arg.type) {
|
|
1351
1683
|
case RouteParamType.BODY:
|
|
1352
|
-
|
|
1684
|
+
try {
|
|
1685
|
+
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
1686
|
+
args[arg.index] = await ctx.req.json();
|
|
1687
|
+
} else {
|
|
1688
|
+
const text = await ctx.req.text();
|
|
1689
|
+
if (!text) {
|
|
1690
|
+
args[arg.index] = {};
|
|
1691
|
+
} else {
|
|
1692
|
+
args[arg.index] = JSON.parse(text);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
} catch (e) {
|
|
1696
|
+
const err = new Error("Invalid JSON body");
|
|
1697
|
+
err.status = 400;
|
|
1698
|
+
throw err;
|
|
1699
|
+
}
|
|
1353
1700
|
break;
|
|
1354
1701
|
case RouteParamType.PARAM:
|
|
1355
1702
|
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
1356
1703
|
break;
|
|
1357
1704
|
case RouteParamType.QUERY: {
|
|
1358
1705
|
const url = new URL(ctx.req.url);
|
|
1359
|
-
|
|
1706
|
+
if (arg.name) {
|
|
1707
|
+
const vals = url.searchParams.getAll(arg.name);
|
|
1708
|
+
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1709
|
+
} else {
|
|
1710
|
+
const query = {};
|
|
1711
|
+
for (const key of url.searchParams.keys()) {
|
|
1712
|
+
const vals = url.searchParams.getAll(key);
|
|
1713
|
+
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1714
|
+
}
|
|
1715
|
+
args[arg.index] = query;
|
|
1716
|
+
}
|
|
1360
1717
|
break;
|
|
1361
1718
|
}
|
|
1362
1719
|
case RouteParamType.HEADER:
|
|
@@ -1504,30 +1861,59 @@ class ShokupanRouter {
|
|
|
1504
1861
|
data: result
|
|
1505
1862
|
};
|
|
1506
1863
|
}
|
|
1507
|
-
|
|
1864
|
+
applyRouterHooks(match) {
|
|
1508
1865
|
if (!this.config?.hooks) return match;
|
|
1509
1866
|
const hooks = this.config.hooks;
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1867
|
+
return {
|
|
1868
|
+
...match,
|
|
1869
|
+
handler: this.wrapWithHooks(match.handler, hooks)
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
wrapWithHooks(handler, hooks) {
|
|
1873
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
1874
|
+
const hasStart = hookList.some((h) => !!h.onRequestStart);
|
|
1875
|
+
const hasEnd = hookList.some((h) => !!h.onRequestEnd);
|
|
1876
|
+
const hasError = hookList.some((h) => !!h.onError);
|
|
1877
|
+
if (!hasStart && !hasEnd && !hasError) return handler;
|
|
1878
|
+
const originalHandler = handler;
|
|
1879
|
+
const wrapped = async (ctx) => {
|
|
1880
|
+
if (hasStart) {
|
|
1881
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1882
|
+
const h = hookList[i];
|
|
1883
|
+
if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const debug = ctx._debug;
|
|
1887
|
+
let debugId;
|
|
1888
|
+
let previousNode;
|
|
1889
|
+
if (debug) {
|
|
1890
|
+
debugId = originalHandler._debugId || originalHandler.name || "handler";
|
|
1891
|
+
previousNode = debug.getCurrentNode();
|
|
1892
|
+
debug.trackEdge(previousNode, debugId);
|
|
1893
|
+
debug.setNode(debugId);
|
|
1894
|
+
}
|
|
1895
|
+
const start = performance.now();
|
|
1514
1896
|
try {
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
|
|
1897
|
+
const res = await originalHandler(ctx);
|
|
1898
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "success");
|
|
1899
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1900
|
+
const h = hookList[i];
|
|
1901
|
+
if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
|
|
1902
|
+
}
|
|
1903
|
+
return res;
|
|
1518
1904
|
} catch (err) {
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
console.error("Error in router onError hook:", e);
|
|
1524
|
-
}
|
|
1905
|
+
debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
|
|
1906
|
+
for (let i = 0; i < hookList.length; i++) {
|
|
1907
|
+
const h = hookList[i];
|
|
1908
|
+
if (typeof h.onError === "function") await h.onError(err, ctx);
|
|
1525
1909
|
}
|
|
1526
1910
|
throw err;
|
|
1911
|
+
} finally {
|
|
1912
|
+
if (debug && previousNode) debug.setNode(previousNode);
|
|
1527
1913
|
}
|
|
1528
1914
|
};
|
|
1529
|
-
|
|
1530
|
-
return
|
|
1915
|
+
wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
|
|
1916
|
+
return wrapped;
|
|
1531
1917
|
}
|
|
1532
1918
|
/**
|
|
1533
1919
|
* Find a route matching the given method and path.
|
|
@@ -1536,24 +1922,10 @@ class ShokupanRouter {
|
|
|
1536
1922
|
* @returns Route handler and parameters if found, otherwise null
|
|
1537
1923
|
*/
|
|
1538
1924
|
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);
|
|
1925
|
+
let result = this.trie.search(method, path);
|
|
1554
1926
|
if (result) return result;
|
|
1555
1927
|
if (method === "HEAD") {
|
|
1556
|
-
result =
|
|
1928
|
+
result = this.trie.search("GET", path);
|
|
1557
1929
|
if (result) return result;
|
|
1558
1930
|
}
|
|
1559
1931
|
for (const child of this[$childRouters]) {
|
|
@@ -1561,13 +1933,13 @@ class ShokupanRouter {
|
|
|
1561
1933
|
if (path === prefix || path.startsWith(prefix + "/")) {
|
|
1562
1934
|
const subPath = path.slice(prefix.length) || "/";
|
|
1563
1935
|
const match = child.find(method, subPath);
|
|
1564
|
-
if (match) return this.
|
|
1936
|
+
if (match) return this.applyRouterHooks(match);
|
|
1565
1937
|
}
|
|
1566
1938
|
if (prefix.endsWith("/")) {
|
|
1567
1939
|
if (path.startsWith(prefix)) {
|
|
1568
1940
|
const subPath = path.slice(prefix.length) || "/";
|
|
1569
1941
|
const match = child.find(method, subPath);
|
|
1570
|
-
if (match) return this.
|
|
1942
|
+
if (match) return this.applyRouterHooks(match);
|
|
1571
1943
|
}
|
|
1572
1944
|
}
|
|
1573
1945
|
}
|
|
@@ -1578,7 +1950,7 @@ class ShokupanRouter {
|
|
|
1578
1950
|
const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
1579
1951
|
keys.push(key);
|
|
1580
1952
|
return "([^/]+)";
|
|
1581
|
-
}).replace(
|
|
1953
|
+
}).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
|
|
1582
1954
|
return {
|
|
1583
1955
|
regex: new RegExp(`^${pattern}$`),
|
|
1584
1956
|
keys
|
|
@@ -1647,47 +2019,84 @@ class ShokupanRouter {
|
|
|
1647
2019
|
return innerHandler(ctx);
|
|
1648
2020
|
};
|
|
1649
2021
|
}
|
|
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;
|
|
2022
|
+
const { file, line } = getCallerInfo();
|
|
2023
|
+
const trackingHandler = wrappedHandler;
|
|
1668
2024
|
wrappedHandler = async (ctx) => {
|
|
1669
|
-
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
1670
|
-
ctx
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
2025
|
+
if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2026
|
+
return trackingHandler(ctx);
|
|
2027
|
+
}
|
|
2028
|
+
const startTime = performance.now();
|
|
2029
|
+
let error = void 0;
|
|
2030
|
+
try {
|
|
2031
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2032
|
+
ctx.handlerStack.push({
|
|
2033
|
+
name: handler.name || "anonymous",
|
|
2034
|
+
file,
|
|
2035
|
+
line
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
return await trackingHandler(ctx);
|
|
2039
|
+
} catch (e) {
|
|
2040
|
+
error = e;
|
|
2041
|
+
throw e;
|
|
2042
|
+
} finally {
|
|
2043
|
+
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2044
|
+
const duration = performance.now() - startTime;
|
|
2045
|
+
const config = ctx.app.applicationConfig;
|
|
2046
|
+
try {
|
|
2047
|
+
const timestamp = Date.now();
|
|
2048
|
+
const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
|
|
2049
|
+
await datastore.set("middleware_tracking", key, {
|
|
2050
|
+
name: handler.name || "anonymous",
|
|
2051
|
+
path: ctx.path,
|
|
2052
|
+
timestamp,
|
|
2053
|
+
duration,
|
|
2054
|
+
file,
|
|
2055
|
+
line,
|
|
2056
|
+
error: error ? String(error) : void 0,
|
|
2057
|
+
metadata: {
|
|
2058
|
+
isBuiltin: handler.isBuiltin,
|
|
2059
|
+
pluginName: handler.pluginName
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2063
|
+
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2064
|
+
const cutoff = Date.now() - ttl;
|
|
2065
|
+
await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2066
|
+
const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2067
|
+
if (results && results[0] && results[0].count > maxCapacity) {
|
|
2068
|
+
const toDelete = results[0].count - maxCapacity;
|
|
2069
|
+
await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2070
|
+
}
|
|
2071
|
+
} catch (datastoreError) {
|
|
2072
|
+
console.error("Failed to store middleware tracking:", datastoreError);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
1675
2075
|
}
|
|
1676
|
-
return trackedHandler(ctx);
|
|
1677
2076
|
};
|
|
1678
|
-
wrappedHandler.originalHandler =
|
|
2077
|
+
wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
|
|
2078
|
+
let bakedHandler = wrappedHandler;
|
|
2079
|
+
if (this.config?.hooks) {
|
|
2080
|
+
bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
|
|
2081
|
+
}
|
|
1679
2082
|
this[$routes].push({
|
|
1680
2083
|
method,
|
|
1681
2084
|
path,
|
|
1682
|
-
regex,
|
|
1683
|
-
keys,
|
|
1684
|
-
handler
|
|
2085
|
+
regex: regex ?? new RegExp(""),
|
|
2086
|
+
keys: keys ?? [],
|
|
2087
|
+
handler,
|
|
2088
|
+
bakedHandler,
|
|
1685
2089
|
handlerSpec: spec,
|
|
1686
2090
|
group,
|
|
1687
|
-
|
|
1688
|
-
requestTimeout
|
|
1689
|
-
|
|
2091
|
+
hooks: this.config?.hooks,
|
|
2092
|
+
requestTimeout,
|
|
2093
|
+
renderer,
|
|
2094
|
+
metadata: {
|
|
2095
|
+
file,
|
|
2096
|
+
line
|
|
2097
|
+
}
|
|
1690
2098
|
});
|
|
2099
|
+
this.trie.insert(method, path, bakedHandler);
|
|
1691
2100
|
return this;
|
|
1692
2101
|
}
|
|
1693
2102
|
get(path, ...args) {
|
|
@@ -1761,10 +2170,10 @@ class ShokupanRouter {
|
|
|
1761
2170
|
const config = typeof options === "string" ? { root: options } : options;
|
|
1762
2171
|
const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
|
|
1763
2172
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
1764
|
-
serveStatic(
|
|
2173
|
+
const handlerMiddleware = serveStatic(config, prefix);
|
|
1765
2174
|
const routeHandler = async (ctx) => {
|
|
1766
|
-
|
|
1767
|
-
|
|
2175
|
+
return handlerMiddleware(ctx, async () => {
|
|
2176
|
+
});
|
|
1768
2177
|
};
|
|
1769
2178
|
let groupName = "Static";
|
|
1770
2179
|
const segments = normalizedPrefix.split("/").filter(Boolean);
|
|
@@ -1825,6 +2234,49 @@ class ShokupanRouter {
|
|
|
1825
2234
|
return generateOpenApi(this, options);
|
|
1826
2235
|
}
|
|
1827
2236
|
}
|
|
2237
|
+
class SystemCpuMonitor {
|
|
2238
|
+
constructor(intervalMs = 1e3) {
|
|
2239
|
+
this.intervalMs = intervalMs;
|
|
2240
|
+
}
|
|
2241
|
+
interval = null;
|
|
2242
|
+
lastCpus = [];
|
|
2243
|
+
currentUsage = 0;
|
|
2244
|
+
start() {
|
|
2245
|
+
if (this.interval) return;
|
|
2246
|
+
this.lastCpus = os.cpus();
|
|
2247
|
+
this.interval = setInterval(() => this.update(), this.intervalMs);
|
|
2248
|
+
}
|
|
2249
|
+
stop() {
|
|
2250
|
+
if (this.interval) {
|
|
2251
|
+
clearInterval(this.interval);
|
|
2252
|
+
this.interval = null;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
getUsage() {
|
|
2256
|
+
return this.currentUsage;
|
|
2257
|
+
}
|
|
2258
|
+
update() {
|
|
2259
|
+
const cpus = os.cpus();
|
|
2260
|
+
let idle = 0;
|
|
2261
|
+
let total = 0;
|
|
2262
|
+
for (let i = 0; i < cpus.length; i++) {
|
|
2263
|
+
const cpu = cpus[i];
|
|
2264
|
+
const prev = this.lastCpus[i];
|
|
2265
|
+
let type;
|
|
2266
|
+
for (type in cpu.times) {
|
|
2267
|
+
const ticks = cpu.times[type];
|
|
2268
|
+
const prevTicks = prev.times[type];
|
|
2269
|
+
const diff = ticks - prevTicks;
|
|
2270
|
+
total += diff;
|
|
2271
|
+
if (type === "idle") {
|
|
2272
|
+
idle += diff;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
this.lastCpus = cpus;
|
|
2277
|
+
this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
1828
2280
|
const defaults = {
|
|
1829
2281
|
port: 3e3,
|
|
1830
2282
|
hostname: "localhost",
|
|
@@ -1836,51 +2288,58 @@ trace.getTracer("shokupan.application");
|
|
|
1836
2288
|
class Shokupan extends ShokupanRouter {
|
|
1837
2289
|
applicationConfig = {};
|
|
1838
2290
|
openApiSpec;
|
|
1839
|
-
middleware = [];
|
|
1840
2291
|
composedMiddleware;
|
|
2292
|
+
cpuMonitor;
|
|
2293
|
+
hookCache = /* @__PURE__ */ new Map();
|
|
2294
|
+
hooksInitialized = false;
|
|
1841
2295
|
get logger() {
|
|
1842
2296
|
return this.applicationConfig.logger;
|
|
1843
2297
|
}
|
|
1844
2298
|
constructor(applicationConfig = {}) {
|
|
1845
|
-
|
|
2299
|
+
const config = Object.assign({}, defaults, applicationConfig);
|
|
2300
|
+
const { hooks, ...routerConfig } = config;
|
|
2301
|
+
super(routerConfig);
|
|
1846
2302
|
this[$isApplication] = true;
|
|
1847
2303
|
this[$appRoot] = this;
|
|
1848
|
-
|
|
2304
|
+
this.applicationConfig = config;
|
|
2305
|
+
const { file, line } = getCallerInfo();
|
|
2306
|
+
this.metadata = {
|
|
2307
|
+
file,
|
|
2308
|
+
line,
|
|
2309
|
+
name: "ShokupanApplication"
|
|
2310
|
+
};
|
|
1849
2311
|
}
|
|
1850
2312
|
/**
|
|
1851
2313
|
* Adds middleware to the application.
|
|
1852
2314
|
*/
|
|
1853
2315
|
use(middleware) {
|
|
1854
2316
|
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) {
|
|
2317
|
+
const { file, line } = getCallerInfo();
|
|
2318
|
+
if (!middleware.metadata) {
|
|
2319
|
+
middleware.metadata = {
|
|
2320
|
+
file,
|
|
2321
|
+
line,
|
|
2322
|
+
name: middleware.name || "middleware",
|
|
2323
|
+
isBuiltin: middleware.isBuiltin,
|
|
2324
|
+
pluginName: middleware.pluginName
|
|
2325
|
+
};
|
|
1872
2326
|
}
|
|
1873
2327
|
trackedMiddleware = async (ctx, next) => {
|
|
1874
2328
|
const c = ctx;
|
|
1875
2329
|
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2330
|
+
const metadata = middleware.metadata || {};
|
|
1876
2331
|
c.handlerStack.push({
|
|
1877
|
-
name: middleware.name || "middleware",
|
|
1878
|
-
file,
|
|
1879
|
-
line
|
|
2332
|
+
name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
|
|
2333
|
+
file: metadata.file || file,
|
|
2334
|
+
line: metadata.line || line,
|
|
2335
|
+
isBuiltin: metadata.isBuiltin
|
|
1880
2336
|
});
|
|
1881
2337
|
}
|
|
1882
2338
|
return middleware(ctx, next);
|
|
1883
2339
|
};
|
|
2340
|
+
trackedMiddleware.metadata = middleware.metadata;
|
|
2341
|
+
Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
|
|
2342
|
+
trackedMiddleware.order = this.middleware.length;
|
|
1884
2343
|
this.middleware.push(trackedMiddleware);
|
|
1885
2344
|
return this;
|
|
1886
2345
|
}
|
|
@@ -1892,6 +2351,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
1892
2351
|
this.startupHooks.push(callback);
|
|
1893
2352
|
return this;
|
|
1894
2353
|
}
|
|
2354
|
+
specAvailableHooks = [];
|
|
2355
|
+
/**
|
|
2356
|
+
* Registers a callback to be executed when the OpenAPI spec is available.
|
|
2357
|
+
* This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
|
|
2358
|
+
*/
|
|
2359
|
+
onSpecAvailable(callback) {
|
|
2360
|
+
this.specAvailableHooks.push(callback);
|
|
2361
|
+
return this;
|
|
2362
|
+
}
|
|
1895
2363
|
/**
|
|
1896
2364
|
* Starts the application server.
|
|
1897
2365
|
*
|
|
@@ -1908,8 +2376,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
1908
2376
|
}
|
|
1909
2377
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
1910
2378
|
this.openApiSpec = await generateOpenApi(this);
|
|
2379
|
+
for (const hook of this.specAvailableHooks) {
|
|
2380
|
+
await hook(this.openApiSpec);
|
|
2381
|
+
}
|
|
1911
2382
|
}
|
|
1912
2383
|
if (port === 0 && process.platform === "linux") ;
|
|
2384
|
+
if (this.applicationConfig.autoBackpressureFeedback) {
|
|
2385
|
+
this.cpuMonitor = new SystemCpuMonitor();
|
|
2386
|
+
this.cpuMonitor.start();
|
|
2387
|
+
}
|
|
1913
2388
|
const serveOptions = {
|
|
1914
2389
|
port: finalPort,
|
|
1915
2390
|
hostname: this.applicationConfig.hostname,
|
|
@@ -1934,7 +2409,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
1934
2409
|
};
|
|
1935
2410
|
let factory = this.applicationConfig.serverFactory;
|
|
1936
2411
|
if (!factory && typeof Bun === "undefined") {
|
|
1937
|
-
const { createHttpServer } = await import("./server-adapter-
|
|
2412
|
+
const { createHttpServer } = await import("./server-adapter-BWrEJbKL.js");
|
|
1938
2413
|
factory = createHttpServer();
|
|
1939
2414
|
}
|
|
1940
2415
|
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
@@ -2019,11 +2494,18 @@ class Shokupan extends ShokupanRouter {
|
|
|
2019
2494
|
}
|
|
2020
2495
|
async handleRequest(req, server) {
|
|
2021
2496
|
const request = req;
|
|
2022
|
-
const
|
|
2497
|
+
const controller = new AbortController();
|
|
2498
|
+
const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
|
|
2023
2499
|
const handle = async () => {
|
|
2500
|
+
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
2501
|
+
const msg = "Too Many Requests (CPU Backpressure)";
|
|
2502
|
+
const res = ctx.text(msg, 429);
|
|
2503
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2504
|
+
return res;
|
|
2505
|
+
}
|
|
2024
2506
|
try {
|
|
2025
|
-
if (this.
|
|
2026
|
-
await this.
|
|
2507
|
+
if (this.hasHook("onRequestStart")) {
|
|
2508
|
+
await this.executeHook("onRequestStart", ctx);
|
|
2027
2509
|
}
|
|
2028
2510
|
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2029
2511
|
const result = await fn(ctx, async () => {
|
|
@@ -2039,23 +2521,24 @@ class Shokupan extends ShokupanRouter {
|
|
|
2039
2521
|
response = result;
|
|
2040
2522
|
} else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
|
|
2041
2523
|
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
2524
|
} else if (result === null || result === void 0) {
|
|
2047
|
-
if (ctx._finalResponse
|
|
2048
|
-
|
|
2525
|
+
if (ctx._finalResponse instanceof Response) {
|
|
2526
|
+
response = ctx._finalResponse;
|
|
2527
|
+
} else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
|
|
2528
|
+
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2529
|
+
} else {
|
|
2530
|
+
response = ctx.text("Not Found", 404);
|
|
2531
|
+
}
|
|
2049
2532
|
} else if (typeof result === "object") {
|
|
2050
2533
|
response = ctx.json(result);
|
|
2051
2534
|
} else {
|
|
2052
2535
|
response = ctx.text(String(result));
|
|
2053
2536
|
}
|
|
2054
|
-
if (this.
|
|
2055
|
-
await this.
|
|
2537
|
+
if (this.hasHook("onRequestEnd")) {
|
|
2538
|
+
await this.executeHook("onRequestEnd", ctx);
|
|
2056
2539
|
}
|
|
2057
|
-
if (this.
|
|
2058
|
-
await this.
|
|
2540
|
+
if (this.hasHook("onResponseStart")) {
|
|
2541
|
+
await this.executeHook("onResponseStart", ctx, response);
|
|
2059
2542
|
}
|
|
2060
2543
|
return response;
|
|
2061
2544
|
} catch (err) {
|
|
@@ -2065,28 +2548,21 @@ class Shokupan extends ShokupanRouter {
|
|
|
2065
2548
|
const status = err.status || err.statusCode || 500;
|
|
2066
2549
|
const body = { error: err.message || "Internal Server Error" };
|
|
2067
2550
|
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
|
-
}
|
|
2551
|
+
if (this.hasHook("onError")) {
|
|
2552
|
+
await this.executeHook("onError", err, ctx);
|
|
2074
2553
|
}
|
|
2075
2554
|
return ctx.json(body, status);
|
|
2076
2555
|
}
|
|
2077
2556
|
};
|
|
2078
2557
|
let executionPromise = handle();
|
|
2079
2558
|
const timeoutMs = this.applicationConfig.requestTimeout;
|
|
2080
|
-
if (timeoutMs && timeoutMs > 0
|
|
2559
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
2081
2560
|
let timeoutId;
|
|
2082
2561
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2083
2562
|
timeoutId = setTimeout(async () => {
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
}
|
|
2088
|
-
} catch (e) {
|
|
2089
|
-
console.error("Error in onRequestTimeout hook:", e);
|
|
2563
|
+
controller.abort();
|
|
2564
|
+
if (this.hasHook("onRequestTimeout")) {
|
|
2565
|
+
await this.executeHook("onRequestTimeout", ctx);
|
|
2090
2566
|
}
|
|
2091
2567
|
reject(new Error("Request Timeout"));
|
|
2092
2568
|
}, timeoutMs);
|
|
@@ -2100,12 +2576,56 @@ class Shokupan extends ShokupanRouter {
|
|
|
2100
2576
|
console.error("Unexpected error in request execution:", err);
|
|
2101
2577
|
return ctx.text("Internal Server Error", 500);
|
|
2102
2578
|
}).then(async (res) => {
|
|
2103
|
-
if (this.
|
|
2104
|
-
await this.
|
|
2579
|
+
if (this.hasHook("onResponseEnd")) {
|
|
2580
|
+
await this.executeHook("onResponseEnd", ctx, res);
|
|
2105
2581
|
}
|
|
2106
2582
|
return res;
|
|
2107
2583
|
});
|
|
2108
2584
|
}
|
|
2585
|
+
ensureHooksInitialized() {
|
|
2586
|
+
const hooks = this.applicationConfig.hooks;
|
|
2587
|
+
if (hooks) {
|
|
2588
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2589
|
+
const hookTypes = [
|
|
2590
|
+
"onRequestStart",
|
|
2591
|
+
"onRequestEnd",
|
|
2592
|
+
"onResponseStart",
|
|
2593
|
+
"onResponseEnd",
|
|
2594
|
+
"onError",
|
|
2595
|
+
"beforeValidate",
|
|
2596
|
+
"afterValidate",
|
|
2597
|
+
"onRequestTimeout",
|
|
2598
|
+
"onReadTimeout",
|
|
2599
|
+
"onWriteTimeout"
|
|
2600
|
+
];
|
|
2601
|
+
for (const type of hookTypes) {
|
|
2602
|
+
const fns = [];
|
|
2603
|
+
for (const h of hookList) {
|
|
2604
|
+
if (h[type]) fns.push(h[type]);
|
|
2605
|
+
}
|
|
2606
|
+
if (fns.length > 0) {
|
|
2607
|
+
this.hookCache.set(type, fns);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
this.hooksInitialized = true;
|
|
2612
|
+
}
|
|
2613
|
+
async executeHook(name, ...args) {
|
|
2614
|
+
if (!this.hooksInitialized) {
|
|
2615
|
+
this.ensureHooksInitialized();
|
|
2616
|
+
}
|
|
2617
|
+
const fns = this.hookCache.get(name);
|
|
2618
|
+
if (!fns) return;
|
|
2619
|
+
for (const fn of fns) {
|
|
2620
|
+
await fn(...args);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
hasHook(name) {
|
|
2624
|
+
if (!this.hooksInitialized) {
|
|
2625
|
+
this.ensureHooksInitialized();
|
|
2626
|
+
}
|
|
2627
|
+
return this.hookCache.has(name);
|
|
2628
|
+
}
|
|
2109
2629
|
}
|
|
2110
2630
|
class AuthPlugin extends ShokupanRouter {
|
|
2111
2631
|
constructor(authConfig) {
|
|
@@ -2310,7 +2830,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2310
2830
|
/**
|
|
2311
2831
|
* Middleware to verify JWT
|
|
2312
2832
|
*/
|
|
2313
|
-
|
|
2833
|
+
getMiddleware() {
|
|
2314
2834
|
return async (ctx, next) => {
|
|
2315
2835
|
const authHeader = ctx.req.headers.get("Authorization");
|
|
2316
2836
|
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
@@ -2331,12 +2851,16 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2331
2851
|
}
|
|
2332
2852
|
function Compression(options = {}) {
|
|
2333
2853
|
const threshold = options.threshold ?? 512;
|
|
2334
|
-
|
|
2854
|
+
const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
|
|
2335
2855
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
2336
2856
|
let method = null;
|
|
2337
2857
|
if (acceptEncoding.includes("br")) method = "br";
|
|
2338
|
-
else if (acceptEncoding.includes("zstd"))
|
|
2339
|
-
|
|
2858
|
+
else if (acceptEncoding.includes("zstd")) {
|
|
2859
|
+
if (typeof Bun === "undefined") {
|
|
2860
|
+
throw new Error("zstd compression is only available in Bun runtime. Client requested zstd but server is running on Node.js.");
|
|
2861
|
+
}
|
|
2862
|
+
method = "zstd";
|
|
2863
|
+
} else if (acceptEncoding.includes("gzip")) method = "gzip";
|
|
2340
2864
|
else if (acceptEncoding.includes("deflate")) method = "deflate";
|
|
2341
2865
|
if (!method) return next();
|
|
2342
2866
|
let response = await next();
|
|
@@ -2345,8 +2869,25 @@ function Compression(options = {}) {
|
|
|
2345
2869
|
}
|
|
2346
2870
|
if (response instanceof Response) {
|
|
2347
2871
|
if (response.headers.has("Content-Encoding")) return response;
|
|
2348
|
-
|
|
2349
|
-
|
|
2872
|
+
let body;
|
|
2873
|
+
let bodySize;
|
|
2874
|
+
if (ctx._rawBody !== void 0) {
|
|
2875
|
+
if (typeof ctx._rawBody === "string") {
|
|
2876
|
+
const encoded = new TextEncoder().encode(ctx._rawBody);
|
|
2877
|
+
body = encoded.buffer;
|
|
2878
|
+
bodySize = encoded.byteLength;
|
|
2879
|
+
} else if (ctx._rawBody instanceof Uint8Array) {
|
|
2880
|
+
body = ctx._rawBody.buffer;
|
|
2881
|
+
bodySize = ctx._rawBody.byteLength;
|
|
2882
|
+
} else {
|
|
2883
|
+
body = ctx._rawBody;
|
|
2884
|
+
bodySize = ctx._rawBody.byteLength;
|
|
2885
|
+
}
|
|
2886
|
+
} else {
|
|
2887
|
+
body = await response.arrayBuffer();
|
|
2888
|
+
bodySize = body.byteLength;
|
|
2889
|
+
}
|
|
2890
|
+
if (bodySize < threshold) {
|
|
2350
2891
|
return new Response(body, {
|
|
2351
2892
|
status: response.status,
|
|
2352
2893
|
statusText: response.statusText,
|
|
@@ -2356,7 +2897,6 @@ function Compression(options = {}) {
|
|
|
2356
2897
|
let compressed;
|
|
2357
2898
|
switch (method) {
|
|
2358
2899
|
case "br":
|
|
2359
|
-
const zlib = require("node:zlib");
|
|
2360
2900
|
compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
|
|
2361
2901
|
params: {
|
|
2362
2902
|
[zlib.constants.BROTLI_PARAM_QUALITY]: 4
|
|
@@ -2367,13 +2907,19 @@ function Compression(options = {}) {
|
|
|
2367
2907
|
}));
|
|
2368
2908
|
break;
|
|
2369
2909
|
case "gzip":
|
|
2370
|
-
compressed =
|
|
2910
|
+
compressed = await new Promise((res, rej) => zlib.gzip(body, (err, data) => {
|
|
2911
|
+
if (err) return rej(err);
|
|
2912
|
+
res(data);
|
|
2913
|
+
}));
|
|
2371
2914
|
break;
|
|
2372
2915
|
case "zstd":
|
|
2373
2916
|
compressed = await Bun.zstdCompress(body);
|
|
2374
2917
|
break;
|
|
2375
2918
|
default:
|
|
2376
|
-
compressed =
|
|
2919
|
+
compressed = await new Promise((res, rej) => zlib.deflate(body, (err, data) => {
|
|
2920
|
+
if (err) return rej(err);
|
|
2921
|
+
res(data);
|
|
2922
|
+
}));
|
|
2377
2923
|
break;
|
|
2378
2924
|
}
|
|
2379
2925
|
const headers = new Headers(response.headers);
|
|
@@ -2387,6 +2933,9 @@ function Compression(options = {}) {
|
|
|
2387
2933
|
}
|
|
2388
2934
|
return response;
|
|
2389
2935
|
};
|
|
2936
|
+
compressionMiddleware.isBuiltin = true;
|
|
2937
|
+
compressionMiddleware.pluginName = "Compression";
|
|
2938
|
+
return compressionMiddleware;
|
|
2390
2939
|
}
|
|
2391
2940
|
function Cors(options = {}) {
|
|
2392
2941
|
const defaults2 = {
|
|
@@ -2396,7 +2945,7 @@ function Cors(options = {}) {
|
|
|
2396
2945
|
optionsSuccessStatus: 204
|
|
2397
2946
|
};
|
|
2398
2947
|
const opts = { ...defaults2, ...options };
|
|
2399
|
-
|
|
2948
|
+
const corsMiddleware = async function CorsMiddleware(ctx, next) {
|
|
2400
2949
|
const headers = new Headers();
|
|
2401
2950
|
const origin = ctx.headers.get("origin");
|
|
2402
2951
|
const set = (k, v) => headers.set(k, v);
|
|
@@ -2458,6 +3007,9 @@ function Cors(options = {}) {
|
|
|
2458
3007
|
}
|
|
2459
3008
|
return response;
|
|
2460
3009
|
};
|
|
3010
|
+
corsMiddleware.isBuiltin = true;
|
|
3011
|
+
corsMiddleware.pluginName = "Cors";
|
|
3012
|
+
return corsMiddleware;
|
|
2461
3013
|
}
|
|
2462
3014
|
function useExpress(expressMiddleware) {
|
|
2463
3015
|
return async (ctx, next) => {
|
|
@@ -2625,7 +3177,33 @@ const safelyGetBody = async (ctx) => {
|
|
|
2625
3177
|
return {};
|
|
2626
3178
|
}
|
|
2627
3179
|
};
|
|
3180
|
+
function getValidator(schema) {
|
|
3181
|
+
if (isZod(schema)) {
|
|
3182
|
+
return (data) => validateZod(schema, data);
|
|
3183
|
+
}
|
|
3184
|
+
if (isTypeBox(schema)) {
|
|
3185
|
+
return (data) => validateTypeBox(schema, data);
|
|
3186
|
+
}
|
|
3187
|
+
if (isAjv(schema)) {
|
|
3188
|
+
return (data) => validateAjv(schema, data);
|
|
3189
|
+
}
|
|
3190
|
+
if (isValibotWrapper(schema)) {
|
|
3191
|
+
return (data) => validateValibotWrapper(schema, data);
|
|
3192
|
+
}
|
|
3193
|
+
if (isClass(schema)) {
|
|
3194
|
+
return (data) => validateClassValidator(schema, data);
|
|
3195
|
+
}
|
|
3196
|
+
if (typeof schema === "function") {
|
|
3197
|
+
return schema;
|
|
3198
|
+
}
|
|
3199
|
+
throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
|
|
3200
|
+
}
|
|
2628
3201
|
function validate(config) {
|
|
3202
|
+
const validators = {};
|
|
3203
|
+
if (config.params) validators.params = getValidator(config.params);
|
|
3204
|
+
if (config.query) validators.query = getValidator(config.query);
|
|
3205
|
+
if (config.headers) validators.headers = getValidator(config.headers);
|
|
3206
|
+
if (config.body) validators.body = getValidator(config.body);
|
|
2629
3207
|
return async (ctx, next) => {
|
|
2630
3208
|
const dataToValidate = {};
|
|
2631
3209
|
if (config.params) dataToValidate.params = ctx.params;
|
|
@@ -2644,21 +3222,21 @@ function validate(config) {
|
|
|
2644
3222
|
if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
|
|
2645
3223
|
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
2646
3224
|
}
|
|
2647
|
-
if (
|
|
2648
|
-
ctx.params = await
|
|
3225
|
+
if (validators.params) {
|
|
3226
|
+
ctx.params = await validators.params(ctx.params);
|
|
2649
3227
|
}
|
|
2650
3228
|
let validQuery;
|
|
2651
|
-
if (
|
|
2652
|
-
validQuery = await
|
|
3229
|
+
if (validators.query && queryObj) {
|
|
3230
|
+
validQuery = await validators.query(queryObj);
|
|
2653
3231
|
}
|
|
2654
|
-
if (
|
|
3232
|
+
if (validators.headers) {
|
|
2655
3233
|
const headersObj = Object.fromEntries(ctx.req.headers.entries());
|
|
2656
|
-
await
|
|
3234
|
+
await validators.headers(headersObj);
|
|
2657
3235
|
}
|
|
2658
3236
|
let validBody;
|
|
2659
|
-
if (
|
|
3237
|
+
if (validators.body) {
|
|
2660
3238
|
const b = body ?? await safelyGetBody(ctx);
|
|
2661
|
-
validBody = await
|
|
3239
|
+
validBody = await validators.body(b);
|
|
2662
3240
|
const req = ctx.req;
|
|
2663
3241
|
req._bodyValue = validBody;
|
|
2664
3242
|
Object.defineProperty(req, "json", {
|
|
@@ -2677,36 +3255,6 @@ function validate(config) {
|
|
|
2677
3255
|
return next();
|
|
2678
3256
|
};
|
|
2679
3257
|
}
|
|
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
3258
|
const ajv = new Ajv({ coerceTypes: true, allErrors: true });
|
|
2711
3259
|
addFormats(ajv);
|
|
2712
3260
|
const compiledValidators = /* @__PURE__ */ new WeakMap();
|
|
@@ -2721,17 +3269,18 @@ function openApiValidator() {
|
|
|
2721
3269
|
cache = compileValidators(app.openApiSpec);
|
|
2722
3270
|
compiledValidators.set(app, cache);
|
|
2723
3271
|
}
|
|
2724
|
-
const method = ctx.req.method.toLowerCase();
|
|
2725
3272
|
let matchPath;
|
|
2726
|
-
|
|
3273
|
+
let matchParams = {};
|
|
3274
|
+
if (cache.validators.has(ctx.path)) {
|
|
2727
3275
|
matchPath = ctx.path;
|
|
2728
3276
|
} else {
|
|
2729
|
-
for (const
|
|
2730
|
-
const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
|
|
2731
|
-
const regex = new RegExp(regexStr);
|
|
3277
|
+
for (const [path, { regex, paramNames }] of cache.paths) {
|
|
2732
3278
|
const match = regex.exec(ctx.path);
|
|
2733
3279
|
if (match) {
|
|
2734
|
-
matchPath =
|
|
3280
|
+
matchPath = path;
|
|
3281
|
+
paramNames.forEach((name, i) => {
|
|
3282
|
+
matchParams[name] = match[i + 1];
|
|
3283
|
+
});
|
|
2735
3284
|
break;
|
|
2736
3285
|
}
|
|
2737
3286
|
}
|
|
@@ -2739,7 +3288,8 @@ function openApiValidator() {
|
|
|
2739
3288
|
if (!matchPath) {
|
|
2740
3289
|
return next();
|
|
2741
3290
|
}
|
|
2742
|
-
const
|
|
3291
|
+
const method = ctx.req.method.toLowerCase();
|
|
3292
|
+
const validators = cache.validators.get(matchPath)?.[method];
|
|
2743
3293
|
if (!validators) {
|
|
2744
3294
|
return next();
|
|
2745
3295
|
}
|
|
@@ -2764,21 +3314,7 @@ function openApiValidator() {
|
|
|
2764
3314
|
}
|
|
2765
3315
|
}
|
|
2766
3316
|
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
|
-
}
|
|
3317
|
+
const params = { ...matchParams, ...ctx.params };
|
|
2782
3318
|
const valid = validators.params(params);
|
|
2783
3319
|
if (!valid && validators.params.errors) {
|
|
2784
3320
|
errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
|
|
@@ -2798,15 +3334,27 @@ function openApiValidator() {
|
|
|
2798
3334
|
};
|
|
2799
3335
|
}
|
|
2800
3336
|
function compileValidators(spec) {
|
|
2801
|
-
const
|
|
3337
|
+
const validators = /* @__PURE__ */ new Map();
|
|
3338
|
+
const paths = /* @__PURE__ */ new Map();
|
|
2802
3339
|
for (const [path, pathItem] of Object.entries(spec.paths || {})) {
|
|
3340
|
+
if (path.includes("{")) {
|
|
3341
|
+
const paramNames = [];
|
|
3342
|
+
const regexStr = "^" + path.replace(/{([^}]+)}/g, (_, name) => {
|
|
3343
|
+
paramNames.push(name);
|
|
3344
|
+
return "([^/]+)";
|
|
3345
|
+
}) + "$";
|
|
3346
|
+
paths.set(path, {
|
|
3347
|
+
regex: new RegExp(regexStr),
|
|
3348
|
+
paramNames
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
2803
3351
|
const pathValidators = {};
|
|
2804
3352
|
for (const [method, operation] of Object.entries(pathItem)) {
|
|
2805
3353
|
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
2806
3354
|
const oper = operation;
|
|
2807
|
-
const
|
|
3355
|
+
const opValidators = {};
|
|
2808
3356
|
if (oper.requestBody?.content?.["application/json"]?.schema) {
|
|
2809
|
-
|
|
3357
|
+
opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
|
|
2810
3358
|
}
|
|
2811
3359
|
const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
|
|
2812
3360
|
const queryProps = {};
|
|
@@ -2828,85 +3376,41 @@ function compileValidators(spec) {
|
|
|
2828
3376
|
}
|
|
2829
3377
|
}
|
|
2830
3378
|
if (Object.keys(queryProps).length > 0) {
|
|
2831
|
-
|
|
3379
|
+
opValidators.query = ajv.compile({
|
|
2832
3380
|
type: "object",
|
|
2833
3381
|
properties: queryProps,
|
|
2834
3382
|
required: queryRequired.length > 0 ? queryRequired : void 0
|
|
2835
3383
|
});
|
|
2836
3384
|
}
|
|
2837
3385
|
if (Object.keys(pathProps).length > 0) {
|
|
2838
|
-
|
|
3386
|
+
opValidators.params = ajv.compile({
|
|
2839
3387
|
type: "object",
|
|
2840
3388
|
properties: pathProps,
|
|
2841
3389
|
required: pathRequired.length > 0 ? pathRequired : void 0
|
|
2842
3390
|
});
|
|
2843
3391
|
}
|
|
2844
3392
|
if (Object.keys(headerProps).length > 0) {
|
|
2845
|
-
|
|
3393
|
+
opValidators.headers = ajv.compile({
|
|
2846
3394
|
type: "object",
|
|
2847
3395
|
properties: headerProps,
|
|
2848
3396
|
required: headerRequired.length > 0 ? headerRequired : void 0
|
|
2849
3397
|
});
|
|
2850
3398
|
}
|
|
2851
|
-
pathValidators[method] =
|
|
3399
|
+
pathValidators[method] = opValidators;
|
|
2852
3400
|
}
|
|
2853
|
-
|
|
3401
|
+
validators.set(path, pathValidators);
|
|
2854
3402
|
}
|
|
2855
|
-
return
|
|
3403
|
+
return { paths, validators };
|
|
2856
3404
|
}
|
|
2857
|
-
function
|
|
2858
|
-
const
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
3405
|
+
function precompileValidators(app, spec) {
|
|
3406
|
+
const cache = compileValidators(spec);
|
|
3407
|
+
compiledValidators.set(app, cache);
|
|
3408
|
+
}
|
|
3409
|
+
function enableOpenApiValidation(app) {
|
|
3410
|
+
app.use(openApiValidator());
|
|
3411
|
+
app.onSpecAvailable((spec) => {
|
|
3412
|
+
precompileValidators(app, spec);
|
|
2865
3413
|
});
|
|
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
3414
|
}
|
|
2911
3415
|
const eta = new Eta();
|
|
2912
3416
|
class ScalarPlugin extends ShokupanRouter {
|
|
@@ -2983,7 +3487,7 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
2983
3487
|
}
|
|
2984
3488
|
}
|
|
2985
3489
|
function SecurityHeaders(options = {}) {
|
|
2986
|
-
|
|
3490
|
+
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
2987
3491
|
const headers = {};
|
|
2988
3492
|
const set = (k, v) => headers[k] = v;
|
|
2989
3493
|
if (options.dnsPrefetchControl !== false) {
|
|
@@ -3037,6 +3541,9 @@ function SecurityHeaders(options = {}) {
|
|
|
3037
3541
|
}
|
|
3038
3542
|
return response;
|
|
3039
3543
|
};
|
|
3544
|
+
securityHeadersMiddleware.isBuiltin = true;
|
|
3545
|
+
securityHeadersMiddleware.pluginName = "SecurityHeaders";
|
|
3546
|
+
return securityHeadersMiddleware;
|
|
3040
3547
|
}
|
|
3041
3548
|
class Cookie {
|
|
3042
3549
|
maxAge;
|
|
@@ -3150,7 +3657,7 @@ function Session(options) {
|
|
|
3150
3657
|
const resave = options.resave === void 0 ? true : options.resave;
|
|
3151
3658
|
const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
|
|
3152
3659
|
const rolling = options.rolling || false;
|
|
3153
|
-
|
|
3660
|
+
const sessionMiddleware = async function SessionMiddleware(ctx, next) {
|
|
3154
3661
|
let reqSessionId = null;
|
|
3155
3662
|
const cookieHeader = ctx.req.headers.get("cookie");
|
|
3156
3663
|
const cookies = {};
|
|
@@ -3286,6 +3793,9 @@ function Session(options) {
|
|
|
3286
3793
|
}
|
|
3287
3794
|
return result;
|
|
3288
3795
|
};
|
|
3796
|
+
sessionMiddleware.isBuiltin = true;
|
|
3797
|
+
sessionMiddleware.pluginName = "Session";
|
|
3798
|
+
return sessionMiddleware;
|
|
3289
3799
|
}
|
|
3290
3800
|
export {
|
|
3291
3801
|
$appRoot,
|
|
@@ -3326,6 +3836,7 @@ export {
|
|
|
3326
3836
|
Put,
|
|
3327
3837
|
Query,
|
|
3328
3838
|
RateLimit,
|
|
3839
|
+
RateLimitMiddleware,
|
|
3329
3840
|
Req,
|
|
3330
3841
|
RouteParamType,
|
|
3331
3842
|
RouterRegistry,
|
|
@@ -3341,8 +3852,11 @@ export {
|
|
|
3341
3852
|
Spec,
|
|
3342
3853
|
Use,
|
|
3343
3854
|
ValidationError,
|
|
3855
|
+
compileValidators,
|
|
3344
3856
|
compose,
|
|
3857
|
+
enableOpenApiValidation,
|
|
3345
3858
|
openApiValidator,
|
|
3859
|
+
precompileValidators,
|
|
3346
3860
|
useExpress,
|
|
3347
3861
|
valibot,
|
|
3348
3862
|
validate
|