shokupan 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +27 -5
- package/dist/index.cjs +662 -429
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +605 -394
- package/dist/index.js.map +1 -1
- package/dist/json-parser-B3dnQmCC.js +35 -0
- package/dist/json-parser-B3dnQmCC.js.map +1 -0
- package/dist/json-parser-COdZ0fqY.cjs +35 -0
- package/dist/json-parser-COdZ0fqY.cjs.map +1 -0
- package/dist/{openapi-analyzer-z-7AoFRC.cjs → openapi-analyzer-Bei1sVWp.cjs} +33 -16
- package/dist/openapi-analyzer-Bei1sVWp.cjs.map +1 -0
- package/dist/{openapi-analyzer-D7y6Qa38.js → openapi-analyzer-Ce_7JxZh.js} +33 -16
- package/dist/openapi-analyzer-Ce_7JxZh.js.map +1 -0
- package/dist/plugins/scalar.d.ts +1 -1
- package/dist/router.d.ts +33 -22
- package/dist/{server-adapter-BWrEJbKL.js → server-adapter-0xH174zz.js} +4 -2
- package/dist/server-adapter-0xH174zz.js.map +1 -0
- package/dist/{server-adapter-fVKP60e0.cjs → server-adapter-DFhwlK8e.cjs} +4 -2
- package/dist/server-adapter-DFhwlK8e.cjs.map +1 -0
- package/dist/shokupan.d.ts +2 -7
- package/dist/types.d.ts +30 -2
- package/dist/util/datastore.d.ts +6 -0
- package/dist/util/json-parser.d.ts +12 -0
- package/dist/util/plugin-deps.d.ts +25 -0
- package/package.json +74 -14
- package/dist/buntest.d.ts +0 -1
- package/dist/openapi-analyzer-D7y6Qa38.js.map +0 -1
- package/dist/openapi-analyzer-z-7AoFRC.cjs.map +0 -1
- package/dist/server-adapter-BWrEJbKL.js.map +0 -1
- package/dist/server-adapter-fVKP60e0.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
2
24
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
25
|
const promises = require("node:fs/promises");
|
|
4
26
|
const eta$2 = require("eta");
|
|
5
27
|
const promises$1 = require("fs/promises");
|
|
6
28
|
const path = require("path");
|
|
7
29
|
const node_async_hooks = require("node:async_hooks");
|
|
8
|
-
const node = require("@surrealdb/node");
|
|
9
|
-
const surrealdb = require("surrealdb");
|
|
10
30
|
const api = require("@opentelemetry/api");
|
|
11
31
|
const os = require("node:os");
|
|
12
32
|
const arctic = require("arctic");
|
|
@@ -14,9 +34,7 @@ const jose = require("jose");
|
|
|
14
34
|
const zlib = require("node:zlib");
|
|
15
35
|
const Ajv = require("ajv");
|
|
16
36
|
const addFormats = require("ajv-formats");
|
|
17
|
-
const
|
|
18
|
-
const classValidator = require("class-validator");
|
|
19
|
-
const openapiAnalyzer = require("./openapi-analyzer-z-7AoFRC.cjs");
|
|
37
|
+
const openapiAnalyzer = require("./openapi-analyzer-Bei1sVWp.cjs");
|
|
20
38
|
const crypto = require("crypto");
|
|
21
39
|
const events = require("events");
|
|
22
40
|
function _interopNamespaceDefault(e) {
|
|
@@ -101,8 +119,73 @@ class ShokupanResponse {
|
|
|
101
119
|
return this._headers !== null;
|
|
102
120
|
}
|
|
103
121
|
}
|
|
122
|
+
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
123
|
+
100,
|
|
124
|
+
101,
|
|
125
|
+
102,
|
|
126
|
+
103,
|
|
127
|
+
200,
|
|
128
|
+
201,
|
|
129
|
+
202,
|
|
130
|
+
203,
|
|
131
|
+
204,
|
|
132
|
+
205,
|
|
133
|
+
206,
|
|
134
|
+
207,
|
|
135
|
+
208,
|
|
136
|
+
226,
|
|
137
|
+
300,
|
|
138
|
+
301,
|
|
139
|
+
302,
|
|
140
|
+
303,
|
|
141
|
+
304,
|
|
142
|
+
305,
|
|
143
|
+
306,
|
|
144
|
+
307,
|
|
145
|
+
308,
|
|
146
|
+
400,
|
|
147
|
+
401,
|
|
148
|
+
402,
|
|
149
|
+
403,
|
|
150
|
+
404,
|
|
151
|
+
405,
|
|
152
|
+
406,
|
|
153
|
+
407,
|
|
154
|
+
408,
|
|
155
|
+
409,
|
|
156
|
+
410,
|
|
157
|
+
411,
|
|
158
|
+
412,
|
|
159
|
+
413,
|
|
160
|
+
414,
|
|
161
|
+
415,
|
|
162
|
+
416,
|
|
163
|
+
417,
|
|
164
|
+
418,
|
|
165
|
+
421,
|
|
166
|
+
422,
|
|
167
|
+
423,
|
|
168
|
+
424,
|
|
169
|
+
425,
|
|
170
|
+
426,
|
|
171
|
+
428,
|
|
172
|
+
429,
|
|
173
|
+
431,
|
|
174
|
+
451,
|
|
175
|
+
500,
|
|
176
|
+
501,
|
|
177
|
+
502,
|
|
178
|
+
503,
|
|
179
|
+
504,
|
|
180
|
+
505,
|
|
181
|
+
506,
|
|
182
|
+
507,
|
|
183
|
+
508,
|
|
184
|
+
510,
|
|
185
|
+
511
|
|
186
|
+
]);
|
|
187
|
+
const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
104
188
|
class ShokupanContext {
|
|
105
|
-
// Raw body for compression optimization
|
|
106
189
|
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
107
190
|
this.request = request;
|
|
108
191
|
this.server = server;
|
|
@@ -125,7 +208,6 @@ class ShokupanContext {
|
|
|
125
208
|
}
|
|
126
209
|
this.response = new ShokupanResponse();
|
|
127
210
|
}
|
|
128
|
-
_url;
|
|
129
211
|
params = {};
|
|
130
212
|
// Router assigns this, but default to empty object
|
|
131
213
|
state;
|
|
@@ -134,6 +216,19 @@ class ShokupanContext {
|
|
|
134
216
|
_debug;
|
|
135
217
|
_finalResponse;
|
|
136
218
|
_rawBody;
|
|
219
|
+
// Raw body for compression optimization
|
|
220
|
+
// Body caching to avoid double parsing
|
|
221
|
+
_url;
|
|
222
|
+
_cachedBody;
|
|
223
|
+
_bodyType;
|
|
224
|
+
_bodyParsed = false;
|
|
225
|
+
_bodyParseError;
|
|
226
|
+
// Cached URL properties to avoid repeated parsing
|
|
227
|
+
_cachedHostname;
|
|
228
|
+
_cachedProtocol;
|
|
229
|
+
_cachedHost;
|
|
230
|
+
_cachedOrigin;
|
|
231
|
+
_cachedQuery;
|
|
137
232
|
get url() {
|
|
138
233
|
if (!this._url) {
|
|
139
234
|
const urlString = this.request.url || "http://localhost/";
|
|
@@ -182,8 +277,11 @@ class ShokupanContext {
|
|
|
182
277
|
* Request query params
|
|
183
278
|
*/
|
|
184
279
|
get query() {
|
|
280
|
+
if (this._cachedQuery) return this._cachedQuery;
|
|
185
281
|
const q = {};
|
|
186
|
-
|
|
282
|
+
const entries = Object.entries(this.url.searchParams);
|
|
283
|
+
for (let i = 0; i < entries.length; i++) {
|
|
284
|
+
const [key, value] = entries[i];
|
|
187
285
|
if (q[key] === void 0) {
|
|
188
286
|
q[key] = value;
|
|
189
287
|
} else if (Array.isArray(q[key])) {
|
|
@@ -192,6 +290,7 @@ class ShokupanContext {
|
|
|
192
290
|
q[key] = [q[key], value];
|
|
193
291
|
}
|
|
194
292
|
}
|
|
293
|
+
this._cachedQuery = q;
|
|
195
294
|
return q;
|
|
196
295
|
}
|
|
197
296
|
/**
|
|
@@ -204,31 +303,31 @@ class ShokupanContext {
|
|
|
204
303
|
* Request hostname (e.g. "localhost")
|
|
205
304
|
*/
|
|
206
305
|
get hostname() {
|
|
207
|
-
return this.url.hostname;
|
|
306
|
+
return this._cachedHostname ??= this.url.hostname;
|
|
208
307
|
}
|
|
209
308
|
/**
|
|
210
309
|
* Request host (e.g. "localhost:3000")
|
|
211
310
|
*/
|
|
212
311
|
get host() {
|
|
213
|
-
return this.url.host;
|
|
312
|
+
return this._cachedHost ??= this.url.host;
|
|
214
313
|
}
|
|
215
314
|
/**
|
|
216
315
|
* Request protocol (e.g. "http:", "https:")
|
|
217
316
|
*/
|
|
218
317
|
get protocol() {
|
|
219
|
-
return this.url.protocol;
|
|
318
|
+
return this._cachedProtocol ??= this.url.protocol;
|
|
220
319
|
}
|
|
221
320
|
/**
|
|
222
321
|
* Whether request is secure (https)
|
|
223
322
|
*/
|
|
224
323
|
get secure() {
|
|
225
|
-
return this.
|
|
324
|
+
return this.protocol === "https:";
|
|
226
325
|
}
|
|
227
326
|
/**
|
|
228
327
|
* Request origin (e.g. "http://localhost:3000")
|
|
229
328
|
*/
|
|
230
329
|
get origin() {
|
|
231
|
-
return this.url.origin;
|
|
330
|
+
return this._cachedOrigin ??= this.url.origin;
|
|
232
331
|
}
|
|
233
332
|
/**
|
|
234
333
|
* Request headers
|
|
@@ -324,6 +423,91 @@ class ShokupanContext {
|
|
|
324
423
|
}
|
|
325
424
|
return h;
|
|
326
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Read request body with caching to avoid double parsing.
|
|
428
|
+
* The body is only parsed once and cached for subsequent reads.
|
|
429
|
+
*/
|
|
430
|
+
async body() {
|
|
431
|
+
if (this._bodyParseError) {
|
|
432
|
+
throw this._bodyParseError;
|
|
433
|
+
}
|
|
434
|
+
if (this._bodyParsed) {
|
|
435
|
+
return this._cachedBody;
|
|
436
|
+
}
|
|
437
|
+
const contentType = this.request.headers.get("content-type") || "";
|
|
438
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
439
|
+
const rawText = await this.readRawBody();
|
|
440
|
+
const parserType = this.app?.applicationConfig?.jsonParser || "native";
|
|
441
|
+
if (parserType === "native") {
|
|
442
|
+
this._cachedBody = JSON.parse(rawText);
|
|
443
|
+
} else {
|
|
444
|
+
const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
|
|
445
|
+
const parser = getJSONParser(parserType);
|
|
446
|
+
this._cachedBody = parser(rawText);
|
|
447
|
+
}
|
|
448
|
+
this._bodyType = "json";
|
|
449
|
+
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
450
|
+
this._cachedBody = await this.request.formData();
|
|
451
|
+
this._bodyType = "formData";
|
|
452
|
+
} else {
|
|
453
|
+
this._cachedBody = await this.readRawBody();
|
|
454
|
+
this._bodyType = "text";
|
|
455
|
+
}
|
|
456
|
+
this._bodyParsed = true;
|
|
457
|
+
return this._cachedBody;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Pre-parse the request body before handler execution.
|
|
461
|
+
* This improves performance and enables Node.js compatibility for large payloads.
|
|
462
|
+
* Errors are deferred until the body is actually accessed in the handler.
|
|
463
|
+
*/
|
|
464
|
+
async parseBody() {
|
|
465
|
+
if (this._bodyParsed) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (this.request.method === "GET" || this.request.method === "HEAD") {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
await this.body();
|
|
473
|
+
} catch (error) {
|
|
474
|
+
this._bodyParseError = error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Read raw body from ReadableStream efficiently.
|
|
479
|
+
* This is much faster than request.text() for large payloads.
|
|
480
|
+
* Also handles the case where body is already a string (e.g., in tests).
|
|
481
|
+
*/
|
|
482
|
+
async readRawBody() {
|
|
483
|
+
if (typeof this.request.body === "string") {
|
|
484
|
+
return this.request.body;
|
|
485
|
+
}
|
|
486
|
+
const reader = this.request.body?.getReader();
|
|
487
|
+
if (!reader) {
|
|
488
|
+
return "";
|
|
489
|
+
}
|
|
490
|
+
const chunks = [];
|
|
491
|
+
let totalSize = 0;
|
|
492
|
+
try {
|
|
493
|
+
while (true) {
|
|
494
|
+
const { done, value } = await reader.read();
|
|
495
|
+
if (done) break;
|
|
496
|
+
chunks.push(value);
|
|
497
|
+
totalSize += value.length;
|
|
498
|
+
}
|
|
499
|
+
} finally {
|
|
500
|
+
reader.releaseLock();
|
|
501
|
+
}
|
|
502
|
+
const result = new Uint8Array(totalSize);
|
|
503
|
+
let offset = 0;
|
|
504
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
505
|
+
const chunk = chunks[i];
|
|
506
|
+
result.set(chunk, offset);
|
|
507
|
+
offset += chunk.length;
|
|
508
|
+
}
|
|
509
|
+
return new TextDecoder().decode(result);
|
|
510
|
+
}
|
|
327
511
|
/**
|
|
328
512
|
* Send a response
|
|
329
513
|
* @param body Response body
|
|
@@ -332,31 +516,24 @@ class ShokupanContext {
|
|
|
332
516
|
*/
|
|
333
517
|
send(body, options) {
|
|
334
518
|
const headers = this.mergeHeaders(options?.headers);
|
|
335
|
-
const status = options?.status ?? this.response.status;
|
|
519
|
+
const status = options?.status ?? this.response.status ?? 200;
|
|
520
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
521
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
522
|
+
}
|
|
336
523
|
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
337
524
|
this._rawBody = body;
|
|
338
525
|
}
|
|
339
526
|
this._finalResponse = new Response(body, { status, headers });
|
|
340
527
|
return this._finalResponse;
|
|
341
528
|
}
|
|
342
|
-
/**
|
|
343
|
-
* Read request body
|
|
344
|
-
*/
|
|
345
|
-
async body() {
|
|
346
|
-
const contentType = this.request.headers.get("content-type") || "";
|
|
347
|
-
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
348
|
-
return this.request.json();
|
|
349
|
-
}
|
|
350
|
-
if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
351
|
-
return this.request.formData();
|
|
352
|
-
}
|
|
353
|
-
return this.request.text();
|
|
354
|
-
}
|
|
355
529
|
/**
|
|
356
530
|
* Respond with a JSON object
|
|
357
531
|
*/
|
|
358
532
|
json(data, status, headers) {
|
|
359
|
-
const finalStatus = status ?? this.response.status;
|
|
533
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
534
|
+
if (!VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
535
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
536
|
+
}
|
|
360
537
|
const jsonString = JSON.stringify(data);
|
|
361
538
|
this._rawBody = jsonString;
|
|
362
539
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
@@ -375,7 +552,10 @@ class ShokupanContext {
|
|
|
375
552
|
* Respond with a text string
|
|
376
553
|
*/
|
|
377
554
|
text(data, status, headers) {
|
|
378
|
-
const finalStatus = status ?? this.response.status;
|
|
555
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
556
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
557
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
558
|
+
}
|
|
379
559
|
this._rawBody = data;
|
|
380
560
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
381
561
|
this._finalResponse = new Response(data, {
|
|
@@ -393,7 +573,10 @@ class ShokupanContext {
|
|
|
393
573
|
* Respond with HTML content
|
|
394
574
|
*/
|
|
395
575
|
html(html, status, headers) {
|
|
396
|
-
const finalStatus = status ?? this.response.status;
|
|
576
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
577
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
578
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
579
|
+
}
|
|
397
580
|
const finalHeaders = this.mergeHeaders(headers);
|
|
398
581
|
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
399
582
|
this._rawBody = html;
|
|
@@ -404,6 +587,9 @@ class ShokupanContext {
|
|
|
404
587
|
* Respond with a redirect
|
|
405
588
|
*/
|
|
406
589
|
redirect(url, status = 302) {
|
|
590
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
|
|
591
|
+
throw new Error(`Invalid redirect status code: ${status}`);
|
|
592
|
+
}
|
|
407
593
|
const headers = this.mergeHeaders();
|
|
408
594
|
headers.set("Location", url);
|
|
409
595
|
this._finalResponse = new Response(null, { status, headers });
|
|
@@ -414,6 +600,9 @@ class ShokupanContext {
|
|
|
414
600
|
* DOES NOT CHAIN!
|
|
415
601
|
*/
|
|
416
602
|
status(status) {
|
|
603
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
604
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
605
|
+
}
|
|
417
606
|
const headers = this.mergeHeaders();
|
|
418
607
|
this._finalResponse = new Response(null, { status, headers });
|
|
419
608
|
return this._finalResponse;
|
|
@@ -424,6 +613,9 @@ class ShokupanContext {
|
|
|
424
613
|
async file(path2, fileOptions, responseOptions) {
|
|
425
614
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
426
615
|
const status = responseOptions?.status ?? this.response.status;
|
|
616
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
617
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
618
|
+
}
|
|
427
619
|
if (typeof Bun !== "undefined") {
|
|
428
620
|
this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
|
|
429
621
|
return this._finalResponse;
|
|
@@ -447,6 +639,10 @@ class ShokupanContext {
|
|
|
447
639
|
* @param headers HTTP Headers
|
|
448
640
|
*/
|
|
449
641
|
async jsx(element, args, status, headers) {
|
|
642
|
+
status ??= 200;
|
|
643
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
644
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
645
|
+
}
|
|
450
646
|
if (!this.renderer) {
|
|
451
647
|
throw new Error("No JSX renderer configured");
|
|
452
648
|
}
|
|
@@ -471,7 +667,9 @@ function RateLimitMiddleware(options = {}) {
|
|
|
471
667
|
const hits = /* @__PURE__ */ new Map();
|
|
472
668
|
const interval = setInterval(() => {
|
|
473
669
|
const now = Date.now();
|
|
474
|
-
|
|
670
|
+
const entries = Array.from(hits.entries());
|
|
671
|
+
for (let i = 0; i < entries.length; i++) {
|
|
672
|
+
const [key, record] = entries[i];
|
|
475
673
|
if (record.resetTime <= now) {
|
|
476
674
|
hits.delete(key);
|
|
477
675
|
}
|
|
@@ -724,7 +922,9 @@ function deepMerge(target, ...sources) {
|
|
|
724
922
|
if (!sources.length) return target;
|
|
725
923
|
const source = sources.shift();
|
|
726
924
|
if (isObject(target) && isObject(source)) {
|
|
727
|
-
|
|
925
|
+
const sourceKeys = Object.keys(source);
|
|
926
|
+
for (let i = 0; i < sourceKeys.length; i++) {
|
|
927
|
+
const key = sourceKeys[i];
|
|
728
928
|
if (isObject(source[key])) {
|
|
729
929
|
if (!target[key]) Object.assign(target, { [key]: {} });
|
|
730
930
|
deepMerge(target[key], source[key]);
|
|
@@ -748,15 +948,17 @@ function deepMerge(target, ...sources) {
|
|
|
748
948
|
}
|
|
749
949
|
return deepMerge(target, ...sources);
|
|
750
950
|
}
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
951
|
+
const REGEX_PATTERNS = {
|
|
952
|
+
QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
|
|
953
|
+
QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
|
|
954
|
+
QUERY_NUMBER: /Number\(ctx\.query\.(\w+)\)/g,
|
|
955
|
+
QUERY_BOOL: /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g,
|
|
956
|
+
QUERY_GENERIC: /ctx\.query\.(\w+)/g,
|
|
957
|
+
PARAM_INT: /parseInt\(ctx\.params\.(\w+)\)/g,
|
|
958
|
+
PARAM_FLOAT: /parseFloat\(ctx\.params\.(\w+)\)/g,
|
|
959
|
+
HEADER_GET: /ctx\.get\(['"](\w+)['"]\)/g,
|
|
960
|
+
ERROR_STATUS: /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g
|
|
961
|
+
};
|
|
760
962
|
function analyzeHandler(handler) {
|
|
761
963
|
const handlerSource = handler.toString();
|
|
762
964
|
const inferredSpec = {};
|
|
@@ -766,29 +968,20 @@ function analyzeHandler(handler) {
|
|
|
766
968
|
};
|
|
767
969
|
}
|
|
768
970
|
const queryParams = /* @__PURE__ */ new Map();
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (match[1] && !queryParams.has(match[1])) {
|
|
777
|
-
queryParams.set(match[1], { type: "number" });
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
|
|
781
|
-
const name = match[1] || match[2];
|
|
782
|
-
if (name && !queryParams.has(name)) {
|
|
783
|
-
queryParams.set(name, { type: "boolean" });
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
|
|
787
|
-
const name = match[1];
|
|
788
|
-
if (name && !queryParams.has(name)) {
|
|
789
|
-
queryParams.set(name, { type: "string" });
|
|
971
|
+
const processMatches = (regex, type, format) => {
|
|
972
|
+
const matches = Array.from(handlerSource.matchAll(regex));
|
|
973
|
+
for (const match of matches) {
|
|
974
|
+
const name = match[1] || match[2];
|
|
975
|
+
if (name && !queryParams.has(name)) {
|
|
976
|
+
queryParams.set(name, { type, format });
|
|
977
|
+
}
|
|
790
978
|
}
|
|
791
|
-
}
|
|
979
|
+
};
|
|
980
|
+
processMatches(REGEX_PATTERNS.QUERY_INT, "integer", "int32");
|
|
981
|
+
processMatches(REGEX_PATTERNS.QUERY_FLOAT, "number", "float");
|
|
982
|
+
processMatches(REGEX_PATTERNS.QUERY_NUMBER, "number");
|
|
983
|
+
processMatches(REGEX_PATTERNS.QUERY_BOOL, "boolean");
|
|
984
|
+
processMatches(REGEX_PATTERNS.QUERY_GENERIC, "string");
|
|
792
985
|
if (queryParams.size > 0) {
|
|
793
986
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
794
987
|
queryParams.forEach((schema, paramName) => {
|
|
@@ -800,12 +993,15 @@ function analyzeHandler(handler) {
|
|
|
800
993
|
});
|
|
801
994
|
}
|
|
802
995
|
const pathParams = /* @__PURE__ */ new Map();
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
996
|
+
const processPathMatches = (regex, type, format) => {
|
|
997
|
+
const matches = Array.from(handlerSource.matchAll(regex));
|
|
998
|
+
for (const match of matches) {
|
|
999
|
+
const name = match[1];
|
|
1000
|
+
if (name) pathParams.set(name, { type, format });
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
processPathMatches(REGEX_PATTERNS.PARAM_INT, "integer", "int32");
|
|
1004
|
+
processPathMatches(REGEX_PATTERNS.PARAM_FLOAT, "number", "float");
|
|
809
1005
|
if (pathParams.size > 0) {
|
|
810
1006
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
811
1007
|
pathParams.forEach((schema, paramName) => {
|
|
@@ -817,7 +1013,8 @@ function analyzeHandler(handler) {
|
|
|
817
1013
|
});
|
|
818
1014
|
});
|
|
819
1015
|
}
|
|
820
|
-
|
|
1016
|
+
const headerMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.HEADER_GET));
|
|
1017
|
+
for (const match of headerMatches) {
|
|
821
1018
|
if (match[1]) {
|
|
822
1019
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
823
1020
|
inferredSpec.parameters.push({
|
|
@@ -836,13 +1033,19 @@ function analyzeHandler(handler) {
|
|
|
836
1033
|
}
|
|
837
1034
|
if (handlerSource.includes("ctx.html(")) {
|
|
838
1035
|
responses["200"] = {
|
|
839
|
-
description: "Successful response",
|
|
1036
|
+
description: "Successful HTML response",
|
|
1037
|
+
content: { "text/html": { schema: { type: "string" } } }
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (handlerSource.includes("ctx.jsx(")) {
|
|
1041
|
+
responses["200"] = {
|
|
1042
|
+
description: "Successful HTML response (Rendered JSX)",
|
|
840
1043
|
content: { "text/html": { schema: { type: "string" } } }
|
|
841
1044
|
};
|
|
842
1045
|
}
|
|
843
1046
|
if (handlerSource.includes("ctx.text(")) {
|
|
844
1047
|
responses["200"] = {
|
|
845
|
-
description: "Successful response",
|
|
1048
|
+
description: "Successful text response",
|
|
846
1049
|
content: { "text/plain": { schema: { type: "string" } } }
|
|
847
1050
|
};
|
|
848
1051
|
}
|
|
@@ -853,7 +1056,18 @@ function analyzeHandler(handler) {
|
|
|
853
1056
|
};
|
|
854
1057
|
}
|
|
855
1058
|
if (handlerSource.includes("ctx.redirect(")) {
|
|
856
|
-
|
|
1059
|
+
let hasSpecificRedirect = false;
|
|
1060
|
+
const redirectMatches = Array.from(handlerSource.matchAll(/ctx\.redirect\([^,]+,\s*(\d{3})\)/g));
|
|
1061
|
+
for (const match of redirectMatches) {
|
|
1062
|
+
const status = match[1];
|
|
1063
|
+
if (/^30[12378]$/.test(status)) {
|
|
1064
|
+
responses[status] = { description: `Redirect (${status})` };
|
|
1065
|
+
hasSpecificRedirect = true;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (!hasSpecificRedirect) {
|
|
1069
|
+
responses["302"] = { description: "Redirect" };
|
|
1070
|
+
}
|
|
857
1071
|
}
|
|
858
1072
|
if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
|
|
859
1073
|
responses["200"] = {
|
|
@@ -861,7 +1075,8 @@ function analyzeHandler(handler) {
|
|
|
861
1075
|
content: { "application/json": { schema: { type: "object" } } }
|
|
862
1076
|
};
|
|
863
1077
|
}
|
|
864
|
-
|
|
1078
|
+
const errorStatusMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.ERROR_STATUS));
|
|
1079
|
+
for (const match of errorStatusMatches) {
|
|
865
1080
|
const statusCode = match[1];
|
|
866
1081
|
if (statusCode && statusCode !== "200") {
|
|
867
1082
|
responses[statusCode] = { description: `Error response (${statusCode})` };
|
|
@@ -872,6 +1087,52 @@ function analyzeHandler(handler) {
|
|
|
872
1087
|
}
|
|
873
1088
|
return { inferredSpec };
|
|
874
1089
|
}
|
|
1090
|
+
async function getAstRoutes(applications) {
|
|
1091
|
+
const astRoutes = [];
|
|
1092
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
1093
|
+
if (seen.has(app.name)) return [];
|
|
1094
|
+
const newSeen = new Set(seen);
|
|
1095
|
+
newSeen.add(app.name);
|
|
1096
|
+
const expanded = [];
|
|
1097
|
+
for (const route of app.routes) {
|
|
1098
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1099
|
+
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1100
|
+
let joined = cleanPrefix + cleanPath;
|
|
1101
|
+
if (joined.length > 1 && joined.endsWith("/")) {
|
|
1102
|
+
joined = joined.slice(0, -1);
|
|
1103
|
+
}
|
|
1104
|
+
expanded.push({
|
|
1105
|
+
...route,
|
|
1106
|
+
path: joined || "/"
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
if (app.mounted) {
|
|
1110
|
+
for (const mount of app.mounted) {
|
|
1111
|
+
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
1112
|
+
if (targetApp) {
|
|
1113
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1114
|
+
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
1115
|
+
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return expanded;
|
|
1120
|
+
};
|
|
1121
|
+
applications.forEach((app) => {
|
|
1122
|
+
astRoutes.push(...getExpandedRoutes(app));
|
|
1123
|
+
});
|
|
1124
|
+
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
1125
|
+
for (const route of astRoutes) {
|
|
1126
|
+
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
1127
|
+
let score = 0;
|
|
1128
|
+
if (route.responseSchema) score += 10;
|
|
1129
|
+
if (route.handlerSource) score += 5;
|
|
1130
|
+
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
1131
|
+
dedupedRoutes.set(key, { route, score });
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1135
|
+
}
|
|
875
1136
|
async function generateOpenApi(rootRouter, options = {}) {
|
|
876
1137
|
const paths = {};
|
|
877
1138
|
const tagGroups = /* @__PURE__ */ new Map();
|
|
@@ -879,61 +1140,11 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
879
1140
|
const defaultTagName = options.defaultTag || "Application";
|
|
880
1141
|
let astRoutes = [];
|
|
881
1142
|
try {
|
|
882
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-
|
|
1143
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-Bei1sVWp.cjs"));
|
|
883
1144
|
const analyzer = new OpenAPIAnalyzer(process.cwd());
|
|
884
1145
|
const { applications } = await analyzer.analyze();
|
|
885
|
-
|
|
886
|
-
applications.forEach((app) => {
|
|
887
|
-
appMap.set(app.name, app);
|
|
888
|
-
if (app.name !== app.className) {
|
|
889
|
-
appMap.set(app.className, app);
|
|
890
|
-
}
|
|
891
|
-
});
|
|
892
|
-
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
893
|
-
if (seen.has(app.name)) return [];
|
|
894
|
-
const newSeen = new Set(seen);
|
|
895
|
-
newSeen.add(app.name);
|
|
896
|
-
const expanded = [];
|
|
897
|
-
for (const route of app.routes) {
|
|
898
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
899
|
-
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
900
|
-
let joined = cleanPrefix + cleanPath;
|
|
901
|
-
if (joined.length > 1 && joined.endsWith("/")) {
|
|
902
|
-
joined = joined.slice(0, -1);
|
|
903
|
-
}
|
|
904
|
-
expanded.push({
|
|
905
|
-
...route,
|
|
906
|
-
path: joined || "/"
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
if (app.mounted) {
|
|
910
|
-
for (const mount of app.mounted) {
|
|
911
|
-
const targetApp = appMap.get(mount.target);
|
|
912
|
-
if (targetApp) {
|
|
913
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
914
|
-
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
915
|
-
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
return expanded;
|
|
920
|
-
};
|
|
921
|
-
applications.forEach((app) => {
|
|
922
|
-
astRoutes.push(...getExpandedRoutes(app));
|
|
923
|
-
});
|
|
924
|
-
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
925
|
-
for (const route of astRoutes) {
|
|
926
|
-
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
927
|
-
let score = 0;
|
|
928
|
-
if (route.responseSchema) score += 10;
|
|
929
|
-
if (route.handlerSource) score += 5;
|
|
930
|
-
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
931
|
-
dedupedRoutes.set(key, { route, score });
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
astRoutes = Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1146
|
+
astRoutes = await getAstRoutes(applications);
|
|
935
1147
|
} catch (e) {
|
|
936
|
-
console.warn("OpenAPI AST analysis failed or skipped:", e);
|
|
937
1148
|
}
|
|
938
1149
|
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
|
|
939
1150
|
let group = currentGroup;
|
|
@@ -990,33 +1201,15 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
990
1201
|
(r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
|
|
991
1202
|
);
|
|
992
1203
|
if (!astMatch) {
|
|
993
|
-
|
|
994
|
-
if (route.handler.originalHandler) {
|
|
995
|
-
runtimeSource = route.handler.originalHandler.toString();
|
|
996
|
-
}
|
|
1204
|
+
const runtimeSource = (route.handler.originalHandler || route.handler).toString();
|
|
997
1205
|
const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
|
|
998
1206
|
const sameMethodRoutes = astRoutes.filter((r) => r.method.toUpperCase() === route.method.toUpperCase());
|
|
999
1207
|
astMatch = sameMethodRoutes.find((r) => {
|
|
1000
1208
|
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
1001
1209
|
if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
|
|
1002
|
-
|
|
1003
|
-
return match;
|
|
1210
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
1004
1211
|
});
|
|
1005
1212
|
}
|
|
1006
|
-
const potentialMatches = astRoutes.filter(
|
|
1007
|
-
(r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
|
|
1008
|
-
);
|
|
1009
|
-
if (potentialMatches.length > 1) {
|
|
1010
|
-
const runtimeHandlerSrc = route.handler.toString().replace(/\s+/g, " ");
|
|
1011
|
-
const preciseMatch = potentialMatches.find((r) => {
|
|
1012
|
-
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
1013
|
-
const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
1014
|
-
return match;
|
|
1015
|
-
});
|
|
1016
|
-
if (preciseMatch) {
|
|
1017
|
-
astMatch = preciseMatch;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
1213
|
if (astMatch) {
|
|
1021
1214
|
if (astMatch.summary) operation.summary = astMatch.summary;
|
|
1022
1215
|
if (astMatch.description) operation.description = astMatch.description;
|
|
@@ -1024,25 +1217,19 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1024
1217
|
if (astMatch.operationId) operation.operationId = astMatch.operationId;
|
|
1025
1218
|
if (astMatch.requestTypes?.body) {
|
|
1026
1219
|
operation.requestBody = {
|
|
1027
|
-
content: {
|
|
1028
|
-
"application/json": { schema: astMatch.requestTypes.body }
|
|
1029
|
-
}
|
|
1220
|
+
content: { "application/json": { schema: astMatch.requestTypes.body } }
|
|
1030
1221
|
};
|
|
1031
1222
|
}
|
|
1032
1223
|
if (astMatch.responseSchema) {
|
|
1033
1224
|
operation.responses["200"] = {
|
|
1034
1225
|
description: "Successful response",
|
|
1035
|
-
content: {
|
|
1036
|
-
"application/json": { schema: astMatch.responseSchema }
|
|
1037
|
-
}
|
|
1226
|
+
content: { "application/json": { schema: astMatch.responseSchema } }
|
|
1038
1227
|
};
|
|
1039
1228
|
} else if (astMatch.responseType) {
|
|
1040
1229
|
const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
|
|
1041
1230
|
operation.responses["200"] = {
|
|
1042
1231
|
description: "Successful response",
|
|
1043
|
-
content: {
|
|
1044
|
-
[contentType]: { schema: { type: astMatch.responseType } }
|
|
1045
|
-
}
|
|
1232
|
+
content: { [contentType]: { schema: { type: astMatch.responseType } } }
|
|
1046
1233
|
};
|
|
1047
1234
|
}
|
|
1048
1235
|
const params = [];
|
|
@@ -1093,15 +1280,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1093
1280
|
deepMerge(operation, inferredSpec);
|
|
1094
1281
|
}
|
|
1095
1282
|
if (route.handlerSpec) {
|
|
1096
|
-
|
|
1097
|
-
if (spec.summary) operation.summary = spec.summary;
|
|
1098
|
-
if (spec.description) operation.description = spec.description;
|
|
1099
|
-
if (spec.operationId) operation.operationId = spec.operationId;
|
|
1100
|
-
if (spec.tags) operation.tags = spec.tags;
|
|
1101
|
-
if (spec.security) operation.security = spec.security;
|
|
1102
|
-
if (spec.responses) {
|
|
1103
|
-
operation.responses = { ...operation.responses, ...spec.responses };
|
|
1104
|
-
}
|
|
1283
|
+
deepMerge(operation, route.handlerSpec);
|
|
1105
1284
|
}
|
|
1106
1285
|
if (!operation.tags || operation.tags.length === 0) operation.tags = [tag];
|
|
1107
1286
|
if (operation.tags) {
|
|
@@ -1120,11 +1299,13 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1120
1299
|
paths[fullPath][methodLower] = operation;
|
|
1121
1300
|
}
|
|
1122
1301
|
}
|
|
1123
|
-
|
|
1302
|
+
const controllers = router[$childControllers];
|
|
1303
|
+
for (const controller of controllers) {
|
|
1124
1304
|
const controllerName = controller.constructor.name || "UnknownController";
|
|
1125
1305
|
tagGroups.get(group)?.add(controllerName);
|
|
1126
1306
|
}
|
|
1127
|
-
|
|
1307
|
+
const childRouters = router[$childRouters];
|
|
1308
|
+
for (const child of childRouters) {
|
|
1128
1309
|
const mountPath = child[$mountPath];
|
|
1129
1310
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1130
1311
|
const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
|
|
@@ -1134,7 +1315,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1134
1315
|
};
|
|
1135
1316
|
collect(rootRouter);
|
|
1136
1317
|
const xTagGroups = [];
|
|
1137
|
-
for (const [name, tags] of tagGroups) {
|
|
1318
|
+
for (const [name, tags] of tagGroups.entries()) {
|
|
1138
1319
|
xTagGroups.push({ name, tags: Array.from(tags).sort() });
|
|
1139
1320
|
}
|
|
1140
1321
|
return {
|
|
@@ -1169,7 +1350,8 @@ function serveStatic(config, prefix) {
|
|
|
1169
1350
|
if (res) return res;
|
|
1170
1351
|
}
|
|
1171
1352
|
if (config.exclude) {
|
|
1172
|
-
for (
|
|
1353
|
+
for (let i = 0; i < config.exclude.length; i++) {
|
|
1354
|
+
const pattern = config.exclude[i];
|
|
1173
1355
|
if (pattern instanceof RegExp) {
|
|
1174
1356
|
if (pattern.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
|
|
1175
1357
|
} else if (typeof pattern === "string") {
|
|
@@ -1188,7 +1370,8 @@ function serveStatic(config, prefix) {
|
|
|
1188
1370
|
stats = await promises$1.stat(requestPath);
|
|
1189
1371
|
} catch (e) {
|
|
1190
1372
|
if (config.extensions) {
|
|
1191
|
-
for (
|
|
1373
|
+
for (let i = 0; i < config.extensions.length; i++) {
|
|
1374
|
+
const ext = config.extensions[i];
|
|
1192
1375
|
const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
|
|
1193
1376
|
try {
|
|
1194
1377
|
const s = await promises$1.stat(p);
|
|
@@ -1217,7 +1400,8 @@ function serveStatic(config, prefix) {
|
|
|
1217
1400
|
indexes = [config.index];
|
|
1218
1401
|
}
|
|
1219
1402
|
let foundIndex = false;
|
|
1220
|
-
for (
|
|
1403
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
1404
|
+
const idx = indexes[i];
|
|
1221
1405
|
const idxPath = path.join(finalPath, idx);
|
|
1222
1406
|
try {
|
|
1223
1407
|
const idxStats = await promises$1.stat(idxPath);
|
|
@@ -1297,38 +1481,39 @@ class RouterTrie {
|
|
|
1297
1481
|
};
|
|
1298
1482
|
}
|
|
1299
1483
|
insert(method, path2, handler) {
|
|
1300
|
-
let
|
|
1484
|
+
let node = this.root;
|
|
1301
1485
|
const segments = this.splitPath(path2);
|
|
1302
|
-
for (
|
|
1486
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1487
|
+
const segment = segments[i];
|
|
1303
1488
|
if (segment === "**") {
|
|
1304
|
-
if (!
|
|
1305
|
-
|
|
1489
|
+
if (!node.recursiveChild) {
|
|
1490
|
+
node.recursiveChild = this.createNode();
|
|
1306
1491
|
}
|
|
1307
|
-
|
|
1492
|
+
node = node.recursiveChild;
|
|
1308
1493
|
} else if (segment === "*") {
|
|
1309
|
-
if (!
|
|
1310
|
-
|
|
1494
|
+
if (!node.wildcardChild) {
|
|
1495
|
+
node.wildcardChild = this.createNode();
|
|
1311
1496
|
}
|
|
1312
|
-
|
|
1497
|
+
node = node.wildcardChild;
|
|
1313
1498
|
} else if (segment.startsWith(":")) {
|
|
1314
1499
|
const paramName = segment.slice(1);
|
|
1315
|
-
if (!
|
|
1316
|
-
|
|
1317
|
-
|
|
1500
|
+
if (!node.paramChild) {
|
|
1501
|
+
node.paramChild = this.createNode();
|
|
1502
|
+
node.paramChild.paramName = paramName;
|
|
1318
1503
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1504
|
+
node = node.paramChild;
|
|
1505
|
+
node.paramName = paramName;
|
|
1321
1506
|
} else {
|
|
1322
|
-
if (!
|
|
1323
|
-
|
|
1507
|
+
if (!node.children[segment]) {
|
|
1508
|
+
node.children[segment] = this.createNode();
|
|
1324
1509
|
}
|
|
1325
|
-
|
|
1510
|
+
node = node.children[segment];
|
|
1326
1511
|
}
|
|
1327
1512
|
}
|
|
1328
|
-
if (!
|
|
1329
|
-
|
|
1513
|
+
if (!node.handlers) {
|
|
1514
|
+
node.handlers = {};
|
|
1330
1515
|
}
|
|
1331
|
-
|
|
1516
|
+
node.handlers[method] = handler;
|
|
1332
1517
|
}
|
|
1333
1518
|
search(method, path2) {
|
|
1334
1519
|
const segments = this.splitPath(path2);
|
|
@@ -1345,34 +1530,34 @@ class RouterTrie {
|
|
|
1345
1530
|
}
|
|
1346
1531
|
return null;
|
|
1347
1532
|
}
|
|
1348
|
-
findNode(
|
|
1533
|
+
findNode(node, segments, index, params) {
|
|
1349
1534
|
if (index === segments.length) {
|
|
1350
|
-
if (
|
|
1351
|
-
if (
|
|
1352
|
-
return
|
|
1535
|
+
if (node.handlers) return node;
|
|
1536
|
+
if (node.recursiveChild && node.recursiveChild.handlers) {
|
|
1537
|
+
return node.recursiveChild;
|
|
1353
1538
|
}
|
|
1354
1539
|
return null;
|
|
1355
1540
|
}
|
|
1356
1541
|
const segment = segments[index];
|
|
1357
|
-
const child =
|
|
1542
|
+
const child = node.children[segment];
|
|
1358
1543
|
if (child) {
|
|
1359
1544
|
const result = this.findNode(child, segments, index + 1, params);
|
|
1360
1545
|
if (result) return result;
|
|
1361
1546
|
}
|
|
1362
|
-
if (
|
|
1363
|
-
params[
|
|
1364
|
-
const result = this.findNode(
|
|
1547
|
+
if (node.paramChild) {
|
|
1548
|
+
params[node.paramChild.paramName] = segment;
|
|
1549
|
+
const result = this.findNode(node.paramChild, segments, index + 1, params);
|
|
1365
1550
|
if (result) return result;
|
|
1366
|
-
delete params[
|
|
1551
|
+
delete params[node.paramChild.paramName];
|
|
1367
1552
|
}
|
|
1368
|
-
if (
|
|
1369
|
-
const result = this.findNode(
|
|
1553
|
+
if (node.wildcardChild) {
|
|
1554
|
+
const result = this.findNode(node.wildcardChild, segments, index + 1, params);
|
|
1370
1555
|
if (result) return result;
|
|
1371
1556
|
}
|
|
1372
|
-
if (
|
|
1557
|
+
if (node.recursiveChild) {
|
|
1373
1558
|
const remaining = segments.length - index;
|
|
1374
1559
|
for (let k = 0; k <= remaining; k++) {
|
|
1375
|
-
const result = this.findNode(
|
|
1560
|
+
const result = this.findNode(node.recursiveChild, segments, index + k, params);
|
|
1376
1561
|
if (result) return result;
|
|
1377
1562
|
}
|
|
1378
1563
|
}
|
|
@@ -1386,40 +1571,68 @@ class RouterTrie {
|
|
|
1386
1571
|
}
|
|
1387
1572
|
}
|
|
1388
1573
|
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
return
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1574
|
+
let db;
|
|
1575
|
+
let dbPromise = null;
|
|
1576
|
+
let RecordId;
|
|
1577
|
+
async function ensureDb() {
|
|
1578
|
+
if (db) return db;
|
|
1579
|
+
if (dbPromise) return dbPromise;
|
|
1580
|
+
dbPromise = (async () => {
|
|
1581
|
+
try {
|
|
1582
|
+
const { createNodeEngines } = await import("@surrealdb/node");
|
|
1583
|
+
const surreal = await import("surrealdb");
|
|
1584
|
+
const Surreal = surreal.Surreal;
|
|
1585
|
+
RecordId = surreal.RecordId;
|
|
1586
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1587
|
+
const _db = new Surreal({
|
|
1588
|
+
engines: createNodeEngines()
|
|
1589
|
+
});
|
|
1590
|
+
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1591
|
+
await _db.query(`
|
|
1592
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1593
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1594
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1595
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1596
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1597
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1598
|
+
`);
|
|
1599
|
+
db = _db;
|
|
1600
|
+
return db;
|
|
1601
|
+
} catch (e) {
|
|
1602
|
+
dbPromise = null;
|
|
1603
|
+
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1604
|
+
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1605
|
+
}
|
|
1606
|
+
throw e;
|
|
1607
|
+
}
|
|
1608
|
+
})();
|
|
1609
|
+
return dbPromise;
|
|
1610
|
+
}
|
|
1403
1611
|
const datastore = {
|
|
1404
|
-
get(store, key) {
|
|
1405
|
-
|
|
1612
|
+
async get(store, key) {
|
|
1613
|
+
await ensureDb();
|
|
1614
|
+
return db.select(new RecordId(store, key));
|
|
1406
1615
|
},
|
|
1407
|
-
set(store, key, value) {
|
|
1408
|
-
|
|
1616
|
+
async set(store, key, value) {
|
|
1617
|
+
await ensureDb();
|
|
1618
|
+
return db.create(new RecordId(store, key)).content(value);
|
|
1409
1619
|
},
|
|
1410
1620
|
async query(query, vars) {
|
|
1621
|
+
await ensureDb();
|
|
1411
1622
|
try {
|
|
1412
|
-
const r = await db.query(query, vars)
|
|
1413
|
-
return r;
|
|
1623
|
+
const r = await db.query(query, vars);
|
|
1624
|
+
return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
|
|
1414
1625
|
} catch (e) {
|
|
1415
1626
|
console.error("DS ERROR:", e);
|
|
1416
1627
|
throw e;
|
|
1417
1628
|
}
|
|
1418
1629
|
},
|
|
1419
|
-
ready
|
|
1630
|
+
get ready() {
|
|
1631
|
+
return ensureDb().then(() => void 0);
|
|
1632
|
+
}
|
|
1420
1633
|
};
|
|
1421
1634
|
process.on("exit", async () => {
|
|
1422
|
-
await db.close();
|
|
1635
|
+
if (db) await db.close();
|
|
1423
1636
|
});
|
|
1424
1637
|
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
1425
1638
|
function traceHandler(fn, name) {
|
|
@@ -1492,6 +1705,8 @@ class ShokupanRouter {
|
|
|
1492
1705
|
[$parent] = null;
|
|
1493
1706
|
[$childRouters] = [];
|
|
1494
1707
|
[$childControllers] = [];
|
|
1708
|
+
hookCache = /* @__PURE__ */ new Map();
|
|
1709
|
+
hooksInitialized = false;
|
|
1495
1710
|
middleware = [];
|
|
1496
1711
|
get rootConfig() {
|
|
1497
1712
|
return this[$appRoot]?.applicationConfig;
|
|
@@ -1509,7 +1724,8 @@ class ShokupanRouter {
|
|
|
1509
1724
|
getComponentRegistry() {
|
|
1510
1725
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
1511
1726
|
const localRoutes = [];
|
|
1512
|
-
for (
|
|
1727
|
+
for (let i = 0; i < this[$routes].length; i++) {
|
|
1728
|
+
const r = this[$routes][i];
|
|
1513
1729
|
const entry = {
|
|
1514
1730
|
type: "route",
|
|
1515
1731
|
path: r.path,
|
|
@@ -1646,7 +1862,8 @@ class ShokupanRouter {
|
|
|
1646
1862
|
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
1647
1863
|
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
1648
1864
|
let routesAttached = 0;
|
|
1649
|
-
for (
|
|
1865
|
+
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
1866
|
+
const name = Array.from(methods)[i];
|
|
1650
1867
|
if (name === "constructor") continue;
|
|
1651
1868
|
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
1652
1869
|
const originalHandler = instance[name];
|
|
@@ -1658,7 +1875,8 @@ class ShokupanRouter {
|
|
|
1658
1875
|
method = config.method;
|
|
1659
1876
|
subPath = config.path;
|
|
1660
1877
|
} else {
|
|
1661
|
-
for (
|
|
1878
|
+
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
1879
|
+
const m = HTTPMethods[j];
|
|
1662
1880
|
if (name.toUpperCase().startsWith(m)) {
|
|
1663
1881
|
method = m;
|
|
1664
1882
|
const rest = name.slice(m.length);
|
|
@@ -1673,8 +1891,8 @@ class ShokupanRouter {
|
|
|
1673
1891
|
buffer = "";
|
|
1674
1892
|
}
|
|
1675
1893
|
};
|
|
1676
|
-
for (let
|
|
1677
|
-
const char = rest[
|
|
1894
|
+
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
1895
|
+
const char = rest[i2];
|
|
1678
1896
|
if (char === "$") {
|
|
1679
1897
|
flush();
|
|
1680
1898
|
subPath += "/:";
|
|
@@ -1712,7 +1930,8 @@ class ShokupanRouter {
|
|
|
1712
1930
|
if (routeArgs?.length > 0) {
|
|
1713
1931
|
args = [];
|
|
1714
1932
|
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
1715
|
-
for (
|
|
1933
|
+
for (let k = 0; k < sortedArgs.length; k++) {
|
|
1934
|
+
const arg = sortedArgs[k];
|
|
1716
1935
|
switch (arg.type) {
|
|
1717
1936
|
case RouteParamType.BODY:
|
|
1718
1937
|
try {
|
|
@@ -1742,7 +1961,9 @@ class ShokupanRouter {
|
|
|
1742
1961
|
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1743
1962
|
} else {
|
|
1744
1963
|
const query = {};
|
|
1745
|
-
|
|
1964
|
+
const keys = Object.keys(url.searchParams);
|
|
1965
|
+
for (let k2 = 0; k2 < keys.length; k2++) {
|
|
1966
|
+
const key = keys[k2];
|
|
1746
1967
|
const vals = url.searchParams.getAll(key);
|
|
1747
1968
|
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1748
1969
|
}
|
|
@@ -1799,9 +2020,11 @@ class ShokupanRouter {
|
|
|
1799
2020
|
path: r.path,
|
|
1800
2021
|
handler: r.handler
|
|
1801
2022
|
}));
|
|
1802
|
-
for (
|
|
2023
|
+
for (let i = 0; i < this[$childRouters].length; i++) {
|
|
2024
|
+
const child = this[$childRouters][i];
|
|
1803
2025
|
const childRoutes = child.getRoutes();
|
|
1804
|
-
for (
|
|
2026
|
+
for (let j = 0; j < childRoutes.length; j++) {
|
|
2027
|
+
const route = childRoutes[j];
|
|
1805
2028
|
const cleanPrefix = child[$mountPath].endsWith("/") ? child[$mountPath].slice(0, -1) : child[$mountPath];
|
|
1806
2029
|
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1807
2030
|
const fullPath = cleanPrefix + cleanPath || "/";
|
|
@@ -1815,12 +2038,12 @@ class ShokupanRouter {
|
|
|
1815
2038
|
return routes;
|
|
1816
2039
|
}
|
|
1817
2040
|
/**
|
|
1818
|
-
* Makes
|
|
1819
|
-
* This is useful for
|
|
2041
|
+
* Makes an internal request through this router's full routing pipeline.
|
|
2042
|
+
* This is useful for calling other routes internally and supports streaming responses.
|
|
1820
2043
|
* @param options The request options.
|
|
1821
|
-
* @returns The
|
|
2044
|
+
* @returns The raw Response object.
|
|
1822
2045
|
*/
|
|
1823
|
-
async
|
|
2046
|
+
async internalRequest(arg) {
|
|
1824
2047
|
const options = typeof arg === "string" ? { path: arg } : arg;
|
|
1825
2048
|
const store = asyncContext.getStore();
|
|
1826
2049
|
store?.get("req");
|
|
@@ -1839,9 +2062,10 @@ class ShokupanRouter {
|
|
|
1839
2062
|
return this.root[$dispatch](req);
|
|
1840
2063
|
}
|
|
1841
2064
|
/**
|
|
1842
|
-
* Processes a request
|
|
2065
|
+
* Processes a request for testing purposes.
|
|
2066
|
+
* Returns a simplified { status, headers, data } object instead of a Response.
|
|
1843
2067
|
*/
|
|
1844
|
-
async
|
|
2068
|
+
async testRequest(options) {
|
|
1845
2069
|
let url = options.url || options.path || "/";
|
|
1846
2070
|
if (!url.startsWith("http")) {
|
|
1847
2071
|
const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig?.port || 3e3}`;
|
|
@@ -1850,7 +2074,9 @@ class ShokupanRouter {
|
|
|
1850
2074
|
}
|
|
1851
2075
|
if (options.query) {
|
|
1852
2076
|
const u = new URL(url);
|
|
1853
|
-
|
|
2077
|
+
const entries = Object.entries(options.query);
|
|
2078
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2079
|
+
const [k, v] = entries[i];
|
|
1854
2080
|
u.searchParams.set(k, v);
|
|
1855
2081
|
}
|
|
1856
2082
|
url = u.toString();
|
|
@@ -1895,28 +2121,17 @@ class ShokupanRouter {
|
|
|
1895
2121
|
data: result
|
|
1896
2122
|
};
|
|
1897
2123
|
}
|
|
1898
|
-
|
|
1899
|
-
if (!this.
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
}
|
|
1906
|
-
wrapWithHooks(handler, hooks) {
|
|
1907
|
-
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
1908
|
-
const hasStart = hookList.some((h) => !!h.onRequestStart);
|
|
1909
|
-
const hasEnd = hookList.some((h) => !!h.onRequestEnd);
|
|
1910
|
-
const hasError = hookList.some((h) => !!h.onError);
|
|
2124
|
+
wrapWithHooks(handler) {
|
|
2125
|
+
if (!this.hooksInitialized) {
|
|
2126
|
+
this.ensureHooksInitialized();
|
|
2127
|
+
}
|
|
2128
|
+
const hasStart = this.hookCache.get("onRequestStart")?.length > 0;
|
|
2129
|
+
const hasEnd = this.hookCache.get("onRequestEnd")?.length > 0;
|
|
2130
|
+
const hasError = this.hookCache.get("onError")?.length > 0;
|
|
1911
2131
|
if (!hasStart && !hasEnd && !hasError) return handler;
|
|
1912
2132
|
const originalHandler = handler;
|
|
1913
2133
|
const wrapped = async (ctx) => {
|
|
1914
|
-
|
|
1915
|
-
for (let i = 0; i < hookList.length; i++) {
|
|
1916
|
-
const h = hookList[i];
|
|
1917
|
-
if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
2134
|
+
await this.runHooks("onRequestStart", ctx);
|
|
1920
2135
|
const debug = ctx._debug;
|
|
1921
2136
|
let debugId;
|
|
1922
2137
|
let previousNode;
|
|
@@ -1930,17 +2145,11 @@ class ShokupanRouter {
|
|
|
1930
2145
|
try {
|
|
1931
2146
|
const res = await originalHandler(ctx);
|
|
1932
2147
|
debug?.trackStep(debugId, "handler", performance.now() - start, "success");
|
|
1933
|
-
|
|
1934
|
-
const h = hookList[i];
|
|
1935
|
-
if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
|
|
1936
|
-
}
|
|
2148
|
+
await this.runHooks("onRequestEnd", ctx);
|
|
1937
2149
|
return res;
|
|
1938
2150
|
} catch (err) {
|
|
1939
2151
|
debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
|
|
1940
|
-
|
|
1941
|
-
const h = hookList[i];
|
|
1942
|
-
if (typeof h.onError === "function") await h.onError(err, ctx);
|
|
1943
|
-
}
|
|
2152
|
+
await this.runHooks("onError", ctx, err);
|
|
1944
2153
|
throw err;
|
|
1945
2154
|
} finally {
|
|
1946
2155
|
if (debug && previousNode) debug.setNode(previousNode);
|
|
@@ -1962,18 +2171,19 @@ class ShokupanRouter {
|
|
|
1962
2171
|
result = this.trie.search("GET", path2);
|
|
1963
2172
|
if (result) return result;
|
|
1964
2173
|
}
|
|
1965
|
-
for (
|
|
2174
|
+
for (let i = 0; i < this[$childRouters].length; i++) {
|
|
2175
|
+
const child = this[$childRouters][i];
|
|
1966
2176
|
const prefix = child[$mountPath];
|
|
1967
2177
|
if (path2 === prefix || path2.startsWith(prefix + "/")) {
|
|
1968
2178
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1969
2179
|
const match = child.find(method, subPath);
|
|
1970
|
-
if (match) return
|
|
2180
|
+
if (match) return match;
|
|
1971
2181
|
}
|
|
1972
2182
|
if (prefix.endsWith("/")) {
|
|
1973
2183
|
if (path2.startsWith(prefix)) {
|
|
1974
2184
|
const subPath = path2.slice(prefix.length) || "/";
|
|
1975
2185
|
const match = child.find(method, subPath);
|
|
1976
|
-
if (match) return
|
|
2186
|
+
if (match) return match;
|
|
1977
2187
|
}
|
|
1978
2188
|
}
|
|
1979
2189
|
}
|
|
@@ -1995,17 +2205,23 @@ class ShokupanRouter {
|
|
|
1995
2205
|
/**
|
|
1996
2206
|
* Adds a route to the router.
|
|
1997
2207
|
*
|
|
1998
|
-
* @param
|
|
1999
|
-
* @param
|
|
2000
|
-
* @param
|
|
2001
|
-
* @param
|
|
2002
|
-
* @param
|
|
2208
|
+
* @param arg - Route configuration object
|
|
2209
|
+
* @param arg.method - HTTP method
|
|
2210
|
+
* @param arg.path - URL path
|
|
2211
|
+
* @param arg.spec - OpenAPI specification for the route
|
|
2212
|
+
* @param arg.handler - Route handler function
|
|
2213
|
+
* @param arg.regex - Custom regex for path matching
|
|
2214
|
+
* @param arg.group - Group for the route
|
|
2215
|
+
* @param arg.requestTimeout - Timeout for this route in milliseconds
|
|
2216
|
+
* @param arg.renderer - JSX renderer for the route
|
|
2217
|
+
* @param arg.controller - Controller for the route
|
|
2003
2218
|
*/
|
|
2004
2219
|
add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
2005
2220
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
|
|
2006
2221
|
if (this.currentGuards.length > 0) {
|
|
2007
2222
|
spec = spec || {};
|
|
2008
|
-
for (
|
|
2223
|
+
for (let i = 0; i < this.currentGuards.length; i++) {
|
|
2224
|
+
const guard = this.currentGuards[i];
|
|
2009
2225
|
if (guard.spec) {
|
|
2010
2226
|
if (guard.spec.responses) {
|
|
2011
2227
|
spec.responses = spec.responses || {};
|
|
@@ -2034,7 +2250,8 @@ class ShokupanRouter {
|
|
|
2034
2250
|
if (routeGuards.length > 0) {
|
|
2035
2251
|
const innerHandler = wrappedHandler;
|
|
2036
2252
|
wrappedHandler = async (ctx) => {
|
|
2037
|
-
for (
|
|
2253
|
+
for (let i = 0; i < routeGuards.length; i++) {
|
|
2254
|
+
const guard = routeGuards[i];
|
|
2038
2255
|
let guardPassed = false;
|
|
2039
2256
|
let nextCalled = false;
|
|
2040
2257
|
const next = () => {
|
|
@@ -2128,7 +2345,7 @@ class ShokupanRouter {
|
|
|
2128
2345
|
wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
|
|
2129
2346
|
let bakedHandler = wrappedHandler;
|
|
2130
2347
|
if (this.config?.hooks) {
|
|
2131
|
-
bakedHandler = this.wrapWithHooks(wrappedHandler
|
|
2348
|
+
bakedHandler = this.wrapWithHooks(wrappedHandler);
|
|
2132
2349
|
}
|
|
2133
2350
|
this[$routes].push({
|
|
2134
2351
|
method,
|
|
@@ -2285,6 +2502,67 @@ class ShokupanRouter {
|
|
|
2285
2502
|
generateApiSpec(options = {}) {
|
|
2286
2503
|
return generateOpenApi(this, options);
|
|
2287
2504
|
}
|
|
2505
|
+
ensureHooksInitialized() {
|
|
2506
|
+
const hooks = this.config?.hooks;
|
|
2507
|
+
if (hooks) {
|
|
2508
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2509
|
+
const hookTypes = [
|
|
2510
|
+
"onRequestStart",
|
|
2511
|
+
"onRequestEnd",
|
|
2512
|
+
"onResponseStart",
|
|
2513
|
+
"onResponseEnd",
|
|
2514
|
+
"onError",
|
|
2515
|
+
"beforeValidate",
|
|
2516
|
+
"afterValidate",
|
|
2517
|
+
"onRequestTimeout",
|
|
2518
|
+
"onReadTimeout",
|
|
2519
|
+
"onWriteTimeout"
|
|
2520
|
+
];
|
|
2521
|
+
for (let i = 0; i < hookTypes.length; i++) {
|
|
2522
|
+
const type = hookTypes[i];
|
|
2523
|
+
const fns = [];
|
|
2524
|
+
for (let j = 0; j < hookList.length; j++) {
|
|
2525
|
+
const h = hookList[j];
|
|
2526
|
+
if (h[type]) fns.push(h[type]);
|
|
2527
|
+
}
|
|
2528
|
+
if (fns.length > 0) {
|
|
2529
|
+
this.hookCache.set(type, fns);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
this.hooksInitialized = true;
|
|
2534
|
+
}
|
|
2535
|
+
async runHooks(name, ...args) {
|
|
2536
|
+
if (!this.hooksInitialized) {
|
|
2537
|
+
this.ensureHooksInitialized();
|
|
2538
|
+
}
|
|
2539
|
+
const fns = this.hookCache.get(name);
|
|
2540
|
+
if (!fns) return;
|
|
2541
|
+
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2542
|
+
const debug = ctx?._debug;
|
|
2543
|
+
if (debug) {
|
|
2544
|
+
await Promise.all(fns.map(async (fn, index) => {
|
|
2545
|
+
const hookId = `hook_${name}_${fn.name || index}`;
|
|
2546
|
+
const previousNode = debug.getCurrentNode();
|
|
2547
|
+
debug.trackEdge(previousNode, hookId);
|
|
2548
|
+
debug.setNode(hookId);
|
|
2549
|
+
const start = performance.now();
|
|
2550
|
+
try {
|
|
2551
|
+
await fn(...args);
|
|
2552
|
+
const duration = performance.now() - start;
|
|
2553
|
+
debug.trackStep(hookId, "hook", duration, "success");
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
const duration = performance.now() - start;
|
|
2556
|
+
debug.trackStep(hookId, "hook", duration, "error", error);
|
|
2557
|
+
throw error;
|
|
2558
|
+
} finally {
|
|
2559
|
+
if (previousNode) debug.setNode(previousNode);
|
|
2560
|
+
}
|
|
2561
|
+
}));
|
|
2562
|
+
} else {
|
|
2563
|
+
await Promise.all(fns.map((fn) => fn(...args)));
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2288
2566
|
}
|
|
2289
2567
|
class SystemCpuMonitor {
|
|
2290
2568
|
constructor(intervalMs = 1e3) {
|
|
@@ -2342,15 +2620,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2342
2620
|
openApiSpec;
|
|
2343
2621
|
composedMiddleware;
|
|
2344
2622
|
cpuMonitor;
|
|
2345
|
-
hookCache = /* @__PURE__ */ new Map();
|
|
2346
|
-
hooksInitialized = false;
|
|
2347
2623
|
get logger() {
|
|
2348
2624
|
return this.applicationConfig.logger;
|
|
2349
2625
|
}
|
|
2350
2626
|
constructor(applicationConfig = {}) {
|
|
2351
2627
|
const config = Object.assign({}, defaults, applicationConfig);
|
|
2352
2628
|
const { hooks, ...routerConfig } = config;
|
|
2353
|
-
super(routerConfig);
|
|
2629
|
+
super({ ...routerConfig, hooks });
|
|
2354
2630
|
this[$isApplication] = true;
|
|
2355
2631
|
this[$appRoot] = this;
|
|
2356
2632
|
this.applicationConfig = config;
|
|
@@ -2365,7 +2641,6 @@ class Shokupan extends ShokupanRouter {
|
|
|
2365
2641
|
* Adds middleware to the application.
|
|
2366
2642
|
*/
|
|
2367
2643
|
use(middleware) {
|
|
2368
|
-
let trackedMiddleware = middleware;
|
|
2369
2644
|
const { file, line } = getCallerInfo();
|
|
2370
2645
|
if (!middleware.metadata) {
|
|
2371
2646
|
middleware.metadata = {
|
|
@@ -2376,32 +2651,36 @@ class Shokupan extends ShokupanRouter {
|
|
|
2376
2651
|
pluginName: middleware.pluginName
|
|
2377
2652
|
};
|
|
2378
2653
|
}
|
|
2379
|
-
|
|
2380
|
-
const
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2654
|
+
if (this.applicationConfig.enableMiddlewareTracking) {
|
|
2655
|
+
const trackedMiddleware = async (ctx, next) => {
|
|
2656
|
+
const c = ctx;
|
|
2657
|
+
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2658
|
+
const metadata = middleware.metadata || {};
|
|
2659
|
+
const start = performance.now();
|
|
2660
|
+
const item = {
|
|
2661
|
+
name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
|
|
2662
|
+
file: metadata.file || file,
|
|
2663
|
+
line: metadata.line || line,
|
|
2664
|
+
isBuiltin: metadata.isBuiltin,
|
|
2665
|
+
startTime: start,
|
|
2666
|
+
duration: -1
|
|
2667
|
+
};
|
|
2668
|
+
c.handlerStack.push(item);
|
|
2669
|
+
try {
|
|
2670
|
+
return await middleware(ctx, next);
|
|
2671
|
+
} finally {
|
|
2672
|
+
item.duration = performance.now() - start;
|
|
2673
|
+
}
|
|
2397
2674
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2675
|
+
return middleware(ctx, next);
|
|
2676
|
+
};
|
|
2677
|
+
trackedMiddleware.metadata = middleware.metadata;
|
|
2678
|
+
Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
|
|
2679
|
+
trackedMiddleware.order = this.middleware.length;
|
|
2680
|
+
this.middleware.push(trackedMiddleware);
|
|
2681
|
+
} else {
|
|
2682
|
+
this.middleware.push(middleware);
|
|
2683
|
+
}
|
|
2405
2684
|
return this;
|
|
2406
2685
|
}
|
|
2407
2686
|
startupHooks = [];
|
|
@@ -2432,17 +2711,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2432
2711
|
if (finalPort < 0 || finalPort > 65535) {
|
|
2433
2712
|
throw new Error("Invalid port number");
|
|
2434
2713
|
}
|
|
2435
|
-
|
|
2436
|
-
await hook();
|
|
2437
|
-
}
|
|
2714
|
+
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
2438
2715
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
2439
2716
|
this.openApiSpec = await generateOpenApi(this);
|
|
2440
|
-
|
|
2441
|
-
await hook(this.openApiSpec);
|
|
2442
|
-
}
|
|
2717
|
+
await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
|
|
2443
2718
|
}
|
|
2444
2719
|
if (port === 0 && process.platform === "linux") ;
|
|
2445
|
-
if (this.applicationConfig.autoBackpressureFeedback) {
|
|
2720
|
+
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
2446
2721
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
2447
2722
|
this.cpuMonitor.start();
|
|
2448
2723
|
}
|
|
@@ -2470,7 +2745,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2470
2745
|
};
|
|
2471
2746
|
let factory = this.applicationConfig.serverFactory;
|
|
2472
2747
|
if (!factory && typeof Bun === "undefined") {
|
|
2473
|
-
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-
|
|
2748
|
+
const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-DFhwlK8e.cjs"));
|
|
2474
2749
|
factory = createHttpServer();
|
|
2475
2750
|
}
|
|
2476
2751
|
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
@@ -2483,7 +2758,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2483
2758
|
/**
|
|
2484
2759
|
* Processes a request by wrapping the standard fetch method.
|
|
2485
2760
|
*/
|
|
2486
|
-
async
|
|
2761
|
+
async testRequest(options) {
|
|
2487
2762
|
let url = options.url || options.path || "/";
|
|
2488
2763
|
if (!url.startsWith("http")) {
|
|
2489
2764
|
const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
|
|
@@ -2492,7 +2767,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
2492
2767
|
}
|
|
2493
2768
|
if (options.query) {
|
|
2494
2769
|
const u = new URL(url);
|
|
2495
|
-
|
|
2770
|
+
const entries = Object.entries(options.query);
|
|
2771
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2772
|
+
const [k, v] = entries[i];
|
|
2496
2773
|
u.searchParams.set(k, v);
|
|
2497
2774
|
}
|
|
2498
2775
|
url = u.toString();
|
|
@@ -2561,18 +2838,18 @@ class Shokupan extends ShokupanRouter {
|
|
|
2561
2838
|
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
2562
2839
|
const msg = "Too Many Requests (CPU Backpressure)";
|
|
2563
2840
|
const res = ctx.text(msg, 429);
|
|
2564
|
-
await this.
|
|
2841
|
+
await this.runHooks("onResponseEnd", ctx, res);
|
|
2565
2842
|
return res;
|
|
2566
2843
|
}
|
|
2567
2844
|
try {
|
|
2568
|
-
|
|
2569
|
-
await this.executeHook("onRequestStart", ctx);
|
|
2570
|
-
}
|
|
2845
|
+
await this.runHooks("onRequestStart", ctx);
|
|
2571
2846
|
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2572
2847
|
const result = await fn(ctx, async () => {
|
|
2848
|
+
const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
|
|
2573
2849
|
const match = this.find(req.method, ctx.path);
|
|
2574
2850
|
if (match) {
|
|
2575
2851
|
ctx.params = match.params;
|
|
2852
|
+
await bodyParsing;
|
|
2576
2853
|
return match.handler(ctx);
|
|
2577
2854
|
}
|
|
2578
2855
|
return null;
|
|
@@ -2595,12 +2872,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
2595
2872
|
} else {
|
|
2596
2873
|
response = ctx.text(String(result));
|
|
2597
2874
|
}
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
}
|
|
2601
|
-
if (this.hasHook("onResponseStart")) {
|
|
2602
|
-
await this.executeHook("onResponseStart", ctx, response);
|
|
2603
|
-
}
|
|
2875
|
+
await this.runHooks("onRequestEnd", ctx);
|
|
2876
|
+
await this.runHooks("onResponseStart", ctx, response);
|
|
2604
2877
|
return response;
|
|
2605
2878
|
} catch (err) {
|
|
2606
2879
|
console.error(err);
|
|
@@ -2609,9 +2882,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2609
2882
|
const status = err.status || err.statusCode || 500;
|
|
2610
2883
|
const body = { error: err.message || "Internal Server Error" };
|
|
2611
2884
|
if (err.errors) body.errors = err.errors;
|
|
2612
|
-
|
|
2613
|
-
await this.executeHook("onError", err, ctx);
|
|
2614
|
-
}
|
|
2885
|
+
await this.runHooks("onError", ctx, err);
|
|
2615
2886
|
return ctx.json(body, status);
|
|
2616
2887
|
}
|
|
2617
2888
|
};
|
|
@@ -2622,9 +2893,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2622
2893
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2623
2894
|
timeoutId = setTimeout(async () => {
|
|
2624
2895
|
controller.abort();
|
|
2625
|
-
|
|
2626
|
-
await this.executeHook("onRequestTimeout", ctx);
|
|
2627
|
-
}
|
|
2896
|
+
await this.runHooks("onRequestTimeout", ctx);
|
|
2628
2897
|
reject(new Error("Request Timeout"));
|
|
2629
2898
|
}, timeoutMs);
|
|
2630
2899
|
});
|
|
@@ -2637,56 +2906,10 @@ class Shokupan extends ShokupanRouter {
|
|
|
2637
2906
|
console.error("Unexpected error in request execution:", err);
|
|
2638
2907
|
return ctx.text("Internal Server Error", 500);
|
|
2639
2908
|
}).then(async (res) => {
|
|
2640
|
-
|
|
2641
|
-
await this.executeHook("onResponseEnd", ctx, res);
|
|
2642
|
-
}
|
|
2909
|
+
await this.runHooks("onResponseEnd", ctx, res);
|
|
2643
2910
|
return res;
|
|
2644
2911
|
});
|
|
2645
2912
|
}
|
|
2646
|
-
ensureHooksInitialized() {
|
|
2647
|
-
const hooks = this.applicationConfig.hooks;
|
|
2648
|
-
if (hooks) {
|
|
2649
|
-
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2650
|
-
const hookTypes = [
|
|
2651
|
-
"onRequestStart",
|
|
2652
|
-
"onRequestEnd",
|
|
2653
|
-
"onResponseStart",
|
|
2654
|
-
"onResponseEnd",
|
|
2655
|
-
"onError",
|
|
2656
|
-
"beforeValidate",
|
|
2657
|
-
"afterValidate",
|
|
2658
|
-
"onRequestTimeout",
|
|
2659
|
-
"onReadTimeout",
|
|
2660
|
-
"onWriteTimeout"
|
|
2661
|
-
];
|
|
2662
|
-
for (const type of hookTypes) {
|
|
2663
|
-
const fns = [];
|
|
2664
|
-
for (const h of hookList) {
|
|
2665
|
-
if (h[type]) fns.push(h[type]);
|
|
2666
|
-
}
|
|
2667
|
-
if (fns.length > 0) {
|
|
2668
|
-
this.hookCache.set(type, fns);
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
this.hooksInitialized = true;
|
|
2673
|
-
}
|
|
2674
|
-
async executeHook(name, ...args) {
|
|
2675
|
-
if (!this.hooksInitialized) {
|
|
2676
|
-
this.ensureHooksInitialized();
|
|
2677
|
-
}
|
|
2678
|
-
const fns = this.hookCache.get(name);
|
|
2679
|
-
if (!fns) return;
|
|
2680
|
-
for (const fn of fns) {
|
|
2681
|
-
await fn(...args);
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
hasHook(name) {
|
|
2685
|
-
if (!this.hooksInitialized) {
|
|
2686
|
-
this.ensureHooksInitialized();
|
|
2687
|
-
}
|
|
2688
|
-
return this.hookCache.has(name);
|
|
2689
|
-
}
|
|
2690
2913
|
}
|
|
2691
2914
|
class AuthPlugin extends ShokupanRouter {
|
|
2692
2915
|
constructor(authConfig) {
|
|
@@ -2734,7 +2957,9 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2734
2957
|
return jwt;
|
|
2735
2958
|
}
|
|
2736
2959
|
init() {
|
|
2737
|
-
|
|
2960
|
+
const providerEntries = Object.entries(this.authConfig.providers);
|
|
2961
|
+
for (let i = 0; i < providerEntries.length; i++) {
|
|
2962
|
+
const [providerName, providerConfig] = providerEntries[i];
|
|
2738
2963
|
if (!providerConfig) continue;
|
|
2739
2964
|
const provider = this.getProviderInstance(providerName, providerConfig);
|
|
2740
2965
|
if (!provider) {
|
|
@@ -3062,7 +3287,9 @@ function Cors(options = {}) {
|
|
|
3062
3287
|
}
|
|
3063
3288
|
const response = await next();
|
|
3064
3289
|
if (response instanceof Response) {
|
|
3065
|
-
|
|
3290
|
+
const headerEntries = Array.from(headers.entries());
|
|
3291
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
3292
|
+
const [key, value] = headerEntries[i];
|
|
3066
3293
|
response.headers.set(key, value);
|
|
3067
3294
|
}
|
|
3068
3295
|
}
|
|
@@ -3132,6 +3359,8 @@ function useExpress(expressMiddleware) {
|
|
|
3132
3359
|
});
|
|
3133
3360
|
};
|
|
3134
3361
|
}
|
|
3362
|
+
let plainToInstance;
|
|
3363
|
+
let validateOrReject;
|
|
3135
3364
|
class ValidationError extends Error {
|
|
3136
3365
|
constructor(errors) {
|
|
3137
3366
|
super("Validation Error");
|
|
@@ -3196,9 +3425,21 @@ function isClass(schema) {
|
|
|
3196
3425
|
}
|
|
3197
3426
|
}
|
|
3198
3427
|
async function validateClassValidator(schema, data) {
|
|
3199
|
-
|
|
3428
|
+
if (!plainToInstance || !validateOrReject) {
|
|
3429
|
+
try {
|
|
3430
|
+
const ct = await import("class-transformer");
|
|
3431
|
+
const cv = await import("class-validator");
|
|
3432
|
+
plainToInstance = ct.plainToInstance;
|
|
3433
|
+
validateOrReject = cv.validateOrReject;
|
|
3434
|
+
} catch (e) {
|
|
3435
|
+
throw new Error(
|
|
3436
|
+
"class-transformer and class-validator are required for class-based validation. Install them with: bun add class-transformer class-validator reflect-metadata"
|
|
3437
|
+
);
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
const object = plainToInstance(schema, data);
|
|
3200
3441
|
try {
|
|
3201
|
-
await
|
|
3442
|
+
await validateOrReject(object);
|
|
3202
3443
|
return object;
|
|
3203
3444
|
} catch (errors) {
|
|
3204
3445
|
const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
|
|
@@ -3210,30 +3451,8 @@ async function validateClassValidator(schema, data) {
|
|
|
3210
3451
|
}
|
|
3211
3452
|
}
|
|
3212
3453
|
const safelyGetBody = async (ctx) => {
|
|
3213
|
-
const req = ctx.req;
|
|
3214
|
-
if (req._bodyParsed) {
|
|
3215
|
-
return req._bodyValue;
|
|
3216
|
-
}
|
|
3217
3454
|
try {
|
|
3218
|
-
|
|
3219
|
-
if (typeof req.json === "function") {
|
|
3220
|
-
data = await req.json();
|
|
3221
|
-
} else {
|
|
3222
|
-
data = req.body;
|
|
3223
|
-
if (typeof data === "string") {
|
|
3224
|
-
try {
|
|
3225
|
-
data = JSON.parse(data);
|
|
3226
|
-
} catch {
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
}
|
|
3230
|
-
req._bodyParsed = true;
|
|
3231
|
-
req._bodyValue = data;
|
|
3232
|
-
Object.defineProperty(req, "json", {
|
|
3233
|
-
value: async () => req._bodyValue,
|
|
3234
|
-
configurable: true
|
|
3235
|
-
});
|
|
3236
|
-
return data;
|
|
3455
|
+
return await ctx.body();
|
|
3237
3456
|
} catch (e) {
|
|
3238
3457
|
return {};
|
|
3239
3458
|
}
|
|
@@ -3280,9 +3499,7 @@ function validate(config) {
|
|
|
3280
3499
|
body = await safelyGetBody(ctx);
|
|
3281
3500
|
dataToValidate.body = body;
|
|
3282
3501
|
}
|
|
3283
|
-
|
|
3284
|
-
await ctx.app.executeHook("beforeValidate", ctx, dataToValidate);
|
|
3285
|
-
}
|
|
3502
|
+
await ctx.app.runHooks("beforeValidate", ctx, dataToValidate);
|
|
3286
3503
|
if (validators.params) {
|
|
3287
3504
|
ctx.params = await validators.params(ctx.params);
|
|
3288
3505
|
}
|
|
@@ -3298,21 +3515,20 @@ function validate(config) {
|
|
|
3298
3515
|
if (validators.body) {
|
|
3299
3516
|
const b = body ?? await safelyGetBody(ctx);
|
|
3300
3517
|
validBody = await validators.body(b);
|
|
3518
|
+
ctx._cachedBody = validBody;
|
|
3301
3519
|
const req = ctx.req;
|
|
3302
|
-
req._bodyValue = validBody;
|
|
3303
3520
|
Object.defineProperty(req, "json", {
|
|
3304
3521
|
value: async () => validBody,
|
|
3522
|
+
writable: true,
|
|
3305
3523
|
configurable: true
|
|
3306
3524
|
});
|
|
3307
3525
|
ctx.body = validBody;
|
|
3308
3526
|
}
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
await ctx.app?.executeHook("afterValidate", ctx, validatedData);
|
|
3315
|
-
}
|
|
3527
|
+
const validatedData = { ...dataToValidate };
|
|
3528
|
+
if (config.params) validatedData.params = ctx.params;
|
|
3529
|
+
if (config.query) validatedData.query = validQuery;
|
|
3530
|
+
if (config.body) validatedData.body = validBody;
|
|
3531
|
+
await ctx.app.runHooks("afterValidate", ctx, validatedData);
|
|
3316
3532
|
return next();
|
|
3317
3533
|
};
|
|
3318
3534
|
}
|
|
@@ -3335,12 +3551,14 @@ function openApiValidator() {
|
|
|
3335
3551
|
if (cache.validators.has(ctx.path)) {
|
|
3336
3552
|
matchPath = ctx.path;
|
|
3337
3553
|
} else {
|
|
3338
|
-
|
|
3554
|
+
const pathEntries = Array.from(cache.paths.entries());
|
|
3555
|
+
for (let i = 0; i < pathEntries.length; i++) {
|
|
3556
|
+
const [path2, { regex, paramNames }] = pathEntries[i];
|
|
3339
3557
|
const match = regex.exec(ctx.path);
|
|
3340
3558
|
if (match) {
|
|
3341
3559
|
matchPath = path2;
|
|
3342
|
-
paramNames.forEach((name,
|
|
3343
|
-
matchParams[name] = match[
|
|
3560
|
+
paramNames.forEach((name, i2) => {
|
|
3561
|
+
matchParams[name] = match[i2 + 1];
|
|
3344
3562
|
});
|
|
3345
3563
|
break;
|
|
3346
3564
|
}
|
|
@@ -3397,7 +3615,9 @@ function openApiValidator() {
|
|
|
3397
3615
|
function compileValidators(spec) {
|
|
3398
3616
|
const validators = /* @__PURE__ */ new Map();
|
|
3399
3617
|
const paths = /* @__PURE__ */ new Map();
|
|
3400
|
-
|
|
3618
|
+
const pathEntries = Object.entries(spec.paths || {});
|
|
3619
|
+
for (let i = 0; i < pathEntries.length; i++) {
|
|
3620
|
+
const [path2, pathItem] = pathEntries[i];
|
|
3401
3621
|
if (path2.includes("{")) {
|
|
3402
3622
|
const paramNames = [];
|
|
3403
3623
|
const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
|
|
@@ -3410,7 +3630,9 @@ function compileValidators(spec) {
|
|
|
3410
3630
|
});
|
|
3411
3631
|
}
|
|
3412
3632
|
const pathValidators = {};
|
|
3413
|
-
|
|
3633
|
+
const methodEntries = Object.entries(pathItem);
|
|
3634
|
+
for (let k = 0; k < methodEntries.length; k++) {
|
|
3635
|
+
const [method, operation] = methodEntries[k];
|
|
3414
3636
|
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
3415
3637
|
const oper = operation;
|
|
3416
3638
|
const opValidators = {};
|
|
@@ -3424,7 +3646,8 @@ function compileValidators(spec) {
|
|
|
3424
3646
|
const queryRequired = [];
|
|
3425
3647
|
const pathRequired = [];
|
|
3426
3648
|
const headerRequired = [];
|
|
3427
|
-
for (
|
|
3649
|
+
for (let j = 0; j < parameters.length; j++) {
|
|
3650
|
+
const param = parameters[j];
|
|
3428
3651
|
if (param.in === "query") {
|
|
3429
3652
|
queryProps[param.name] = param.schema || {};
|
|
3430
3653
|
if (param.required) queryRequired.push(param.name);
|
|
@@ -3585,14 +3808,18 @@ function SecurityHeaders(options = {}) {
|
|
|
3585
3808
|
if (opt === void 0 || opt === true) {
|
|
3586
3809
|
set("Content-Security-Policy", "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests");
|
|
3587
3810
|
} else if (typeof opt === "object") {
|
|
3588
|
-
|
|
3811
|
+
const optEntries = Object.entries(opt);
|
|
3812
|
+
for (let i = 0; i < optEntries.length; i++) {
|
|
3813
|
+
const [key, val] = optEntries[i];
|
|
3589
3814
|
}
|
|
3590
3815
|
}
|
|
3591
3816
|
}
|
|
3592
3817
|
if (options.hidePoweredBy !== false) ;
|
|
3593
3818
|
const response = await next();
|
|
3594
3819
|
if (response instanceof Response) {
|
|
3595
|
-
|
|
3820
|
+
const headerEntries = Object.entries(headers);
|
|
3821
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
3822
|
+
const [k, v] = headerEntries[i];
|
|
3596
3823
|
response.headers.set(k, v);
|
|
3597
3824
|
}
|
|
3598
3825
|
return response;
|
|
@@ -3678,7 +3905,9 @@ class MemoryStore extends events.EventEmitter {
|
|
|
3678
3905
|
}
|
|
3679
3906
|
all(cb) {
|
|
3680
3907
|
const result = {};
|
|
3681
|
-
|
|
3908
|
+
const sessionKeys = Object.keys(this.sessions);
|
|
3909
|
+
for (let i = 0; i < sessionKeys.length; i++) {
|
|
3910
|
+
const sid = sessionKeys[i];
|
|
3682
3911
|
try {
|
|
3683
3912
|
result[sid] = JSON.parse(this.sessions[sid]);
|
|
3684
3913
|
} catch {
|
|
@@ -3764,7 +3993,9 @@ function Session(options) {
|
|
|
3764
3993
|
sessObj.regenerate = (cb) => {
|
|
3765
3994
|
store.destroy(sessObj.id, (err) => {
|
|
3766
3995
|
sessionID = generateId(ctx);
|
|
3767
|
-
|
|
3996
|
+
const keys = Object.keys(sessObj);
|
|
3997
|
+
for (let i = 0; i < keys.length; i++) {
|
|
3998
|
+
const key = keys[i];
|
|
3768
3999
|
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
3769
4000
|
delete sessObj[key];
|
|
3770
4001
|
}
|
|
@@ -3779,7 +4010,9 @@ function Session(options) {
|
|
|
3779
4010
|
store.get(sessObj.id, (err, sess2) => {
|
|
3780
4011
|
if (err) return cb(err);
|
|
3781
4012
|
if (!sess2) return cb(new Error("Session not found"));
|
|
3782
|
-
|
|
4013
|
+
const keys = Object.keys(sessObj);
|
|
4014
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4015
|
+
const key = keys[i];
|
|
3783
4016
|
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
3784
4017
|
delete sessObj[key];
|
|
3785
4018
|
}
|