shokupan 0.4.5 → 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 +10 -9
- package/dist/analysis/openapi-analyzer.d.ts +0 -4
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +30 -8
- package/dist/index.cjs +692 -461
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +635 -426
- 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-D9YB3IkV.cjs → openapi-analyzer-Bei1sVWp.cjs} +63 -49
- package/dist/openapi-analyzer-Bei1sVWp.cjs.map +1 -0
- package/dist/{openapi-analyzer-BtIaHIfe.js → openapi-analyzer-Ce_7JxZh.js} +63 -49
- 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 +4 -8
- package/dist/types.d.ts +32 -3
- 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/benchmarking/advanced-cases/elysia.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/express.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/fastify.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/hapi.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/hono.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/koa.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/nest.d.ts +0 -1
- package/dist/benchmarking/advanced-cases/shokupan.d.ts +0 -1
- package/dist/benchmarking/advanced-data.d.ts +0 -33
- package/dist/benchmarking/advanced-runner.d.ts +0 -1
- package/dist/benchmarking/advanced-worker.d.ts +0 -0
- package/dist/benchmarking/cases/elysia.d.ts +0 -1
- package/dist/benchmarking/cases/express.d.ts +0 -1
- package/dist/benchmarking/cases/fastify.d.ts +0 -1
- package/dist/benchmarking/cases/hapi.d.ts +0 -1
- package/dist/benchmarking/cases/hono.d.ts +0 -1
- package/dist/benchmarking/cases/koa.d.ts +0 -1
- package/dist/benchmarking/cases/nest.d.ts +0 -1
- package/dist/benchmarking/cases/shokupan.d.ts +0 -1
- package/dist/benchmarking/data.d.ts +0 -15
- package/dist/benchmarking/quick_bench.d.ts +0 -1
- package/dist/benchmarking/runner.d.ts +0 -1
- package/dist/benchmarking/worker.d.ts +0 -0
- package/dist/buntest.d.ts +0 -1
- package/dist/openapi-analyzer-BtIaHIfe.js.map +0 -1
- package/dist/openapi-analyzer-D9YB3IkV.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.js
CHANGED
|
@@ -3,8 +3,6 @@ import { Eta } from "eta";
|
|
|
3
3
|
import { stat, readdir, readFile as readFile$1 } from "fs/promises";
|
|
4
4
|
import { resolve, join, basename } from "path";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
-
import { createNodeEngines } from "@surrealdb/node";
|
|
7
|
-
import { Surreal, RecordId } from "surrealdb";
|
|
8
6
|
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
9
7
|
import * as os from "node:os";
|
|
10
8
|
import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
|
|
@@ -12,9 +10,7 @@ import * as jose from "jose";
|
|
|
12
10
|
import * as zlib from "node:zlib";
|
|
13
11
|
import Ajv from "ajv";
|
|
14
12
|
import addFormats from "ajv-formats";
|
|
15
|
-
import {
|
|
16
|
-
import { validateOrReject } from "class-validator";
|
|
17
|
-
import { OpenAPIAnalyzer } from "./openapi-analyzer-BtIaHIfe.js";
|
|
13
|
+
import { OpenAPIAnalyzer } from "./openapi-analyzer-Ce_7JxZh.js";
|
|
18
14
|
import { randomUUID, createHmac } from "crypto";
|
|
19
15
|
import { EventEmitter } from "events";
|
|
20
16
|
class ShokupanResponse {
|
|
@@ -80,8 +76,73 @@ class ShokupanResponse {
|
|
|
80
76
|
return this._headers !== null;
|
|
81
77
|
}
|
|
82
78
|
}
|
|
79
|
+
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
80
|
+
100,
|
|
81
|
+
101,
|
|
82
|
+
102,
|
|
83
|
+
103,
|
|
84
|
+
200,
|
|
85
|
+
201,
|
|
86
|
+
202,
|
|
87
|
+
203,
|
|
88
|
+
204,
|
|
89
|
+
205,
|
|
90
|
+
206,
|
|
91
|
+
207,
|
|
92
|
+
208,
|
|
93
|
+
226,
|
|
94
|
+
300,
|
|
95
|
+
301,
|
|
96
|
+
302,
|
|
97
|
+
303,
|
|
98
|
+
304,
|
|
99
|
+
305,
|
|
100
|
+
306,
|
|
101
|
+
307,
|
|
102
|
+
308,
|
|
103
|
+
400,
|
|
104
|
+
401,
|
|
105
|
+
402,
|
|
106
|
+
403,
|
|
107
|
+
404,
|
|
108
|
+
405,
|
|
109
|
+
406,
|
|
110
|
+
407,
|
|
111
|
+
408,
|
|
112
|
+
409,
|
|
113
|
+
410,
|
|
114
|
+
411,
|
|
115
|
+
412,
|
|
116
|
+
413,
|
|
117
|
+
414,
|
|
118
|
+
415,
|
|
119
|
+
416,
|
|
120
|
+
417,
|
|
121
|
+
418,
|
|
122
|
+
421,
|
|
123
|
+
422,
|
|
124
|
+
423,
|
|
125
|
+
424,
|
|
126
|
+
425,
|
|
127
|
+
426,
|
|
128
|
+
428,
|
|
129
|
+
429,
|
|
130
|
+
431,
|
|
131
|
+
451,
|
|
132
|
+
500,
|
|
133
|
+
501,
|
|
134
|
+
502,
|
|
135
|
+
503,
|
|
136
|
+
504,
|
|
137
|
+
505,
|
|
138
|
+
506,
|
|
139
|
+
507,
|
|
140
|
+
508,
|
|
141
|
+
510,
|
|
142
|
+
511
|
|
143
|
+
]);
|
|
144
|
+
const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
83
145
|
class ShokupanContext {
|
|
84
|
-
// Raw body for compression optimization
|
|
85
146
|
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
86
147
|
this.request = request;
|
|
87
148
|
this.server = server;
|
|
@@ -104,7 +165,6 @@ class ShokupanContext {
|
|
|
104
165
|
}
|
|
105
166
|
this.response = new ShokupanResponse();
|
|
106
167
|
}
|
|
107
|
-
_url;
|
|
108
168
|
params = {};
|
|
109
169
|
// Router assigns this, but default to empty object
|
|
110
170
|
state;
|
|
@@ -113,6 +173,19 @@ class ShokupanContext {
|
|
|
113
173
|
_debug;
|
|
114
174
|
_finalResponse;
|
|
115
175
|
_rawBody;
|
|
176
|
+
// Raw body for compression optimization
|
|
177
|
+
// Body caching to avoid double parsing
|
|
178
|
+
_url;
|
|
179
|
+
_cachedBody;
|
|
180
|
+
_bodyType;
|
|
181
|
+
_bodyParsed = false;
|
|
182
|
+
_bodyParseError;
|
|
183
|
+
// Cached URL properties to avoid repeated parsing
|
|
184
|
+
_cachedHostname;
|
|
185
|
+
_cachedProtocol;
|
|
186
|
+
_cachedHost;
|
|
187
|
+
_cachedOrigin;
|
|
188
|
+
_cachedQuery;
|
|
116
189
|
get url() {
|
|
117
190
|
if (!this._url) {
|
|
118
191
|
const urlString = this.request.url || "http://localhost/";
|
|
@@ -161,8 +234,11 @@ class ShokupanContext {
|
|
|
161
234
|
* Request query params
|
|
162
235
|
*/
|
|
163
236
|
get query() {
|
|
237
|
+
if (this._cachedQuery) return this._cachedQuery;
|
|
164
238
|
const q = {};
|
|
165
|
-
|
|
239
|
+
const entries = Object.entries(this.url.searchParams);
|
|
240
|
+
for (let i = 0; i < entries.length; i++) {
|
|
241
|
+
const [key, value] = entries[i];
|
|
166
242
|
if (q[key] === void 0) {
|
|
167
243
|
q[key] = value;
|
|
168
244
|
} else if (Array.isArray(q[key])) {
|
|
@@ -171,6 +247,7 @@ class ShokupanContext {
|
|
|
171
247
|
q[key] = [q[key], value];
|
|
172
248
|
}
|
|
173
249
|
}
|
|
250
|
+
this._cachedQuery = q;
|
|
174
251
|
return q;
|
|
175
252
|
}
|
|
176
253
|
/**
|
|
@@ -183,31 +260,31 @@ class ShokupanContext {
|
|
|
183
260
|
* Request hostname (e.g. "localhost")
|
|
184
261
|
*/
|
|
185
262
|
get hostname() {
|
|
186
|
-
return this.url.hostname;
|
|
263
|
+
return this._cachedHostname ??= this.url.hostname;
|
|
187
264
|
}
|
|
188
265
|
/**
|
|
189
266
|
* Request host (e.g. "localhost:3000")
|
|
190
267
|
*/
|
|
191
268
|
get host() {
|
|
192
|
-
return this.url.host;
|
|
269
|
+
return this._cachedHost ??= this.url.host;
|
|
193
270
|
}
|
|
194
271
|
/**
|
|
195
272
|
* Request protocol (e.g. "http:", "https:")
|
|
196
273
|
*/
|
|
197
274
|
get protocol() {
|
|
198
|
-
return this.url.protocol;
|
|
275
|
+
return this._cachedProtocol ??= this.url.protocol;
|
|
199
276
|
}
|
|
200
277
|
/**
|
|
201
278
|
* Whether request is secure (https)
|
|
202
279
|
*/
|
|
203
280
|
get secure() {
|
|
204
|
-
return this.
|
|
281
|
+
return this.protocol === "https:";
|
|
205
282
|
}
|
|
206
283
|
/**
|
|
207
284
|
* Request origin (e.g. "http://localhost:3000")
|
|
208
285
|
*/
|
|
209
286
|
get origin() {
|
|
210
|
-
return this.url.origin;
|
|
287
|
+
return this._cachedOrigin ??= this.url.origin;
|
|
211
288
|
}
|
|
212
289
|
/**
|
|
213
290
|
* Request headers
|
|
@@ -303,6 +380,91 @@ class ShokupanContext {
|
|
|
303
380
|
}
|
|
304
381
|
return h;
|
|
305
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Read request body with caching to avoid double parsing.
|
|
385
|
+
* The body is only parsed once and cached for subsequent reads.
|
|
386
|
+
*/
|
|
387
|
+
async body() {
|
|
388
|
+
if (this._bodyParseError) {
|
|
389
|
+
throw this._bodyParseError;
|
|
390
|
+
}
|
|
391
|
+
if (this._bodyParsed) {
|
|
392
|
+
return this._cachedBody;
|
|
393
|
+
}
|
|
394
|
+
const contentType = this.request.headers.get("content-type") || "";
|
|
395
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
396
|
+
const rawText = await this.readRawBody();
|
|
397
|
+
const parserType = this.app?.applicationConfig?.jsonParser || "native";
|
|
398
|
+
if (parserType === "native") {
|
|
399
|
+
this._cachedBody = JSON.parse(rawText);
|
|
400
|
+
} else {
|
|
401
|
+
const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
|
|
402
|
+
const parser = getJSONParser(parserType);
|
|
403
|
+
this._cachedBody = parser(rawText);
|
|
404
|
+
}
|
|
405
|
+
this._bodyType = "json";
|
|
406
|
+
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
407
|
+
this._cachedBody = await this.request.formData();
|
|
408
|
+
this._bodyType = "formData";
|
|
409
|
+
} else {
|
|
410
|
+
this._cachedBody = await this.readRawBody();
|
|
411
|
+
this._bodyType = "text";
|
|
412
|
+
}
|
|
413
|
+
this._bodyParsed = true;
|
|
414
|
+
return this._cachedBody;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Pre-parse the request body before handler execution.
|
|
418
|
+
* This improves performance and enables Node.js compatibility for large payloads.
|
|
419
|
+
* Errors are deferred until the body is actually accessed in the handler.
|
|
420
|
+
*/
|
|
421
|
+
async parseBody() {
|
|
422
|
+
if (this._bodyParsed) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (this.request.method === "GET" || this.request.method === "HEAD") {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
await this.body();
|
|
430
|
+
} catch (error) {
|
|
431
|
+
this._bodyParseError = error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Read raw body from ReadableStream efficiently.
|
|
436
|
+
* This is much faster than request.text() for large payloads.
|
|
437
|
+
* Also handles the case where body is already a string (e.g., in tests).
|
|
438
|
+
*/
|
|
439
|
+
async readRawBody() {
|
|
440
|
+
if (typeof this.request.body === "string") {
|
|
441
|
+
return this.request.body;
|
|
442
|
+
}
|
|
443
|
+
const reader = this.request.body?.getReader();
|
|
444
|
+
if (!reader) {
|
|
445
|
+
return "";
|
|
446
|
+
}
|
|
447
|
+
const chunks = [];
|
|
448
|
+
let totalSize = 0;
|
|
449
|
+
try {
|
|
450
|
+
while (true) {
|
|
451
|
+
const { done, value } = await reader.read();
|
|
452
|
+
if (done) break;
|
|
453
|
+
chunks.push(value);
|
|
454
|
+
totalSize += value.length;
|
|
455
|
+
}
|
|
456
|
+
} finally {
|
|
457
|
+
reader.releaseLock();
|
|
458
|
+
}
|
|
459
|
+
const result = new Uint8Array(totalSize);
|
|
460
|
+
let offset = 0;
|
|
461
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
462
|
+
const chunk = chunks[i];
|
|
463
|
+
result.set(chunk, offset);
|
|
464
|
+
offset += chunk.length;
|
|
465
|
+
}
|
|
466
|
+
return new TextDecoder().decode(result);
|
|
467
|
+
}
|
|
306
468
|
/**
|
|
307
469
|
* Send a response
|
|
308
470
|
* @param body Response body
|
|
@@ -311,31 +473,24 @@ class ShokupanContext {
|
|
|
311
473
|
*/
|
|
312
474
|
send(body, options) {
|
|
313
475
|
const headers = this.mergeHeaders(options?.headers);
|
|
314
|
-
const status = options?.status ?? this.response.status;
|
|
476
|
+
const status = options?.status ?? this.response.status ?? 200;
|
|
477
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
478
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
479
|
+
}
|
|
315
480
|
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
316
481
|
this._rawBody = body;
|
|
317
482
|
}
|
|
318
483
|
this._finalResponse = new Response(body, { status, headers });
|
|
319
484
|
return this._finalResponse;
|
|
320
485
|
}
|
|
321
|
-
/**
|
|
322
|
-
* Read request body
|
|
323
|
-
*/
|
|
324
|
-
async body() {
|
|
325
|
-
const contentType = this.request.headers.get("content-type") || "";
|
|
326
|
-
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
327
|
-
return this.request.json();
|
|
328
|
-
}
|
|
329
|
-
if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
330
|
-
return this.request.formData();
|
|
331
|
-
}
|
|
332
|
-
return this.request.text();
|
|
333
|
-
}
|
|
334
486
|
/**
|
|
335
487
|
* Respond with a JSON object
|
|
336
488
|
*/
|
|
337
489
|
json(data, status, headers) {
|
|
338
|
-
const finalStatus = status ?? this.response.status;
|
|
490
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
491
|
+
if (!VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
492
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
493
|
+
}
|
|
339
494
|
const jsonString = JSON.stringify(data);
|
|
340
495
|
this._rawBody = jsonString;
|
|
341
496
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
@@ -354,7 +509,10 @@ class ShokupanContext {
|
|
|
354
509
|
* Respond with a text string
|
|
355
510
|
*/
|
|
356
511
|
text(data, status, headers) {
|
|
357
|
-
const finalStatus = status ?? this.response.status;
|
|
512
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
513
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
514
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
515
|
+
}
|
|
358
516
|
this._rawBody = data;
|
|
359
517
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
360
518
|
this._finalResponse = new Response(data, {
|
|
@@ -372,7 +530,10 @@ class ShokupanContext {
|
|
|
372
530
|
* Respond with HTML content
|
|
373
531
|
*/
|
|
374
532
|
html(html, status, headers) {
|
|
375
|
-
const finalStatus = status ?? this.response.status;
|
|
533
|
+
const finalStatus = status ?? this.response.status ?? 200;
|
|
534
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
535
|
+
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
536
|
+
}
|
|
376
537
|
const finalHeaders = this.mergeHeaders(headers);
|
|
377
538
|
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
378
539
|
this._rawBody = html;
|
|
@@ -383,6 +544,9 @@ class ShokupanContext {
|
|
|
383
544
|
* Respond with a redirect
|
|
384
545
|
*/
|
|
385
546
|
redirect(url, status = 302) {
|
|
547
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
|
|
548
|
+
throw new Error(`Invalid redirect status code: ${status}`);
|
|
549
|
+
}
|
|
386
550
|
const headers = this.mergeHeaders();
|
|
387
551
|
headers.set("Location", url);
|
|
388
552
|
this._finalResponse = new Response(null, { status, headers });
|
|
@@ -393,6 +557,9 @@ class ShokupanContext {
|
|
|
393
557
|
* DOES NOT CHAIN!
|
|
394
558
|
*/
|
|
395
559
|
status(status) {
|
|
560
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
561
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
562
|
+
}
|
|
396
563
|
const headers = this.mergeHeaders();
|
|
397
564
|
this._finalResponse = new Response(null, { status, headers });
|
|
398
565
|
return this._finalResponse;
|
|
@@ -403,6 +570,9 @@ class ShokupanContext {
|
|
|
403
570
|
async file(path, fileOptions, responseOptions) {
|
|
404
571
|
const headers = this.mergeHeaders(responseOptions?.headers);
|
|
405
572
|
const status = responseOptions?.status ?? this.response.status;
|
|
573
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
574
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
575
|
+
}
|
|
406
576
|
if (typeof Bun !== "undefined") {
|
|
407
577
|
this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
|
|
408
578
|
return this._finalResponse;
|
|
@@ -426,6 +596,10 @@ class ShokupanContext {
|
|
|
426
596
|
* @param headers HTTP Headers
|
|
427
597
|
*/
|
|
428
598
|
async jsx(element, args, status, headers) {
|
|
599
|
+
status ??= 200;
|
|
600
|
+
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
601
|
+
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
602
|
+
}
|
|
429
603
|
if (!this.renderer) {
|
|
430
604
|
throw new Error("No JSX renderer configured");
|
|
431
605
|
}
|
|
@@ -450,7 +624,9 @@ function RateLimitMiddleware(options = {}) {
|
|
|
450
624
|
const hits = /* @__PURE__ */ new Map();
|
|
451
625
|
const interval = setInterval(() => {
|
|
452
626
|
const now = Date.now();
|
|
453
|
-
|
|
627
|
+
const entries = Array.from(hits.entries());
|
|
628
|
+
for (let i = 0; i < entries.length; i++) {
|
|
629
|
+
const [key, record] = entries[i];
|
|
454
630
|
if (record.resetTime <= now) {
|
|
455
631
|
hits.delete(key);
|
|
456
632
|
}
|
|
@@ -703,7 +879,9 @@ function deepMerge(target, ...sources) {
|
|
|
703
879
|
if (!sources.length) return target;
|
|
704
880
|
const source = sources.shift();
|
|
705
881
|
if (isObject(target) && isObject(source)) {
|
|
706
|
-
|
|
882
|
+
const sourceKeys = Object.keys(source);
|
|
883
|
+
for (let i = 0; i < sourceKeys.length; i++) {
|
|
884
|
+
const key = sourceKeys[i];
|
|
707
885
|
if (isObject(source[key])) {
|
|
708
886
|
if (!target[key]) Object.assign(target, { [key]: {} });
|
|
709
887
|
deepMerge(target[key], source[key]);
|
|
@@ -727,15 +905,17 @@ function deepMerge(target, ...sources) {
|
|
|
727
905
|
}
|
|
728
906
|
return deepMerge(target, ...sources);
|
|
729
907
|
}
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
908
|
+
const REGEX_PATTERNS = {
|
|
909
|
+
QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
|
|
910
|
+
QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
|
|
911
|
+
QUERY_NUMBER: /Number\(ctx\.query\.(\w+)\)/g,
|
|
912
|
+
QUERY_BOOL: /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g,
|
|
913
|
+
QUERY_GENERIC: /ctx\.query\.(\w+)/g,
|
|
914
|
+
PARAM_INT: /parseInt\(ctx\.params\.(\w+)\)/g,
|
|
915
|
+
PARAM_FLOAT: /parseFloat\(ctx\.params\.(\w+)\)/g,
|
|
916
|
+
HEADER_GET: /ctx\.get\(['"](\w+)['"]\)/g,
|
|
917
|
+
ERROR_STATUS: /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g
|
|
918
|
+
};
|
|
739
919
|
function analyzeHandler(handler) {
|
|
740
920
|
const handlerSource = handler.toString();
|
|
741
921
|
const inferredSpec = {};
|
|
@@ -745,29 +925,20 @@ function analyzeHandler(handler) {
|
|
|
745
925
|
};
|
|
746
926
|
}
|
|
747
927
|
const queryParams = /* @__PURE__ */ new Map();
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
if (match[1] && !queryParams.has(match[1])) {
|
|
756
|
-
queryParams.set(match[1], { type: "number" });
|
|
757
|
-
}
|
|
758
|
-
}
|
|
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
|
-
}
|
|
764
|
-
}
|
|
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" });
|
|
928
|
+
const processMatches = (regex, type, format) => {
|
|
929
|
+
const matches = Array.from(handlerSource.matchAll(regex));
|
|
930
|
+
for (const match of matches) {
|
|
931
|
+
const name = match[1] || match[2];
|
|
932
|
+
if (name && !queryParams.has(name)) {
|
|
933
|
+
queryParams.set(name, { type, format });
|
|
934
|
+
}
|
|
769
935
|
}
|
|
770
|
-
}
|
|
936
|
+
};
|
|
937
|
+
processMatches(REGEX_PATTERNS.QUERY_INT, "integer", "int32");
|
|
938
|
+
processMatches(REGEX_PATTERNS.QUERY_FLOAT, "number", "float");
|
|
939
|
+
processMatches(REGEX_PATTERNS.QUERY_NUMBER, "number");
|
|
940
|
+
processMatches(REGEX_PATTERNS.QUERY_BOOL, "boolean");
|
|
941
|
+
processMatches(REGEX_PATTERNS.QUERY_GENERIC, "string");
|
|
771
942
|
if (queryParams.size > 0) {
|
|
772
943
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
773
944
|
queryParams.forEach((schema, paramName) => {
|
|
@@ -779,12 +950,15 @@ function analyzeHandler(handler) {
|
|
|
779
950
|
});
|
|
780
951
|
}
|
|
781
952
|
const pathParams = /* @__PURE__ */ new Map();
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
953
|
+
const processPathMatches = (regex, type, format) => {
|
|
954
|
+
const matches = Array.from(handlerSource.matchAll(regex));
|
|
955
|
+
for (const match of matches) {
|
|
956
|
+
const name = match[1];
|
|
957
|
+
if (name) pathParams.set(name, { type, format });
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
processPathMatches(REGEX_PATTERNS.PARAM_INT, "integer", "int32");
|
|
961
|
+
processPathMatches(REGEX_PATTERNS.PARAM_FLOAT, "number", "float");
|
|
788
962
|
if (pathParams.size > 0) {
|
|
789
963
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
790
964
|
pathParams.forEach((schema, paramName) => {
|
|
@@ -796,7 +970,8 @@ function analyzeHandler(handler) {
|
|
|
796
970
|
});
|
|
797
971
|
});
|
|
798
972
|
}
|
|
799
|
-
|
|
973
|
+
const headerMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.HEADER_GET));
|
|
974
|
+
for (const match of headerMatches) {
|
|
800
975
|
if (match[1]) {
|
|
801
976
|
if (!inferredSpec.parameters) inferredSpec.parameters = [];
|
|
802
977
|
inferredSpec.parameters.push({
|
|
@@ -815,13 +990,19 @@ function analyzeHandler(handler) {
|
|
|
815
990
|
}
|
|
816
991
|
if (handlerSource.includes("ctx.html(")) {
|
|
817
992
|
responses["200"] = {
|
|
818
|
-
description: "Successful response",
|
|
993
|
+
description: "Successful HTML response",
|
|
994
|
+
content: { "text/html": { schema: { type: "string" } } }
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
if (handlerSource.includes("ctx.jsx(")) {
|
|
998
|
+
responses["200"] = {
|
|
999
|
+
description: "Successful HTML response (Rendered JSX)",
|
|
819
1000
|
content: { "text/html": { schema: { type: "string" } } }
|
|
820
1001
|
};
|
|
821
1002
|
}
|
|
822
1003
|
if (handlerSource.includes("ctx.text(")) {
|
|
823
1004
|
responses["200"] = {
|
|
824
|
-
description: "Successful response",
|
|
1005
|
+
description: "Successful text response",
|
|
825
1006
|
content: { "text/plain": { schema: { type: "string" } } }
|
|
826
1007
|
};
|
|
827
1008
|
}
|
|
@@ -832,7 +1013,18 @@ function analyzeHandler(handler) {
|
|
|
832
1013
|
};
|
|
833
1014
|
}
|
|
834
1015
|
if (handlerSource.includes("ctx.redirect(")) {
|
|
835
|
-
|
|
1016
|
+
let hasSpecificRedirect = false;
|
|
1017
|
+
const redirectMatches = Array.from(handlerSource.matchAll(/ctx\.redirect\([^,]+,\s*(\d{3})\)/g));
|
|
1018
|
+
for (const match of redirectMatches) {
|
|
1019
|
+
const status = match[1];
|
|
1020
|
+
if (/^30[12378]$/.test(status)) {
|
|
1021
|
+
responses[status] = { description: `Redirect (${status})` };
|
|
1022
|
+
hasSpecificRedirect = true;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (!hasSpecificRedirect) {
|
|
1026
|
+
responses["302"] = { description: "Redirect" };
|
|
1027
|
+
}
|
|
836
1028
|
}
|
|
837
1029
|
if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
|
|
838
1030
|
responses["200"] = {
|
|
@@ -840,7 +1032,8 @@ function analyzeHandler(handler) {
|
|
|
840
1032
|
content: { "application/json": { schema: { type: "object" } } }
|
|
841
1033
|
};
|
|
842
1034
|
}
|
|
843
|
-
|
|
1035
|
+
const errorStatusMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.ERROR_STATUS));
|
|
1036
|
+
for (const match of errorStatusMatches) {
|
|
844
1037
|
const statusCode = match[1];
|
|
845
1038
|
if (statusCode && statusCode !== "200") {
|
|
846
1039
|
responses[statusCode] = { description: `Error response (${statusCode})` };
|
|
@@ -851,6 +1044,52 @@ function analyzeHandler(handler) {
|
|
|
851
1044
|
}
|
|
852
1045
|
return { inferredSpec };
|
|
853
1046
|
}
|
|
1047
|
+
async function getAstRoutes(applications) {
|
|
1048
|
+
const astRoutes = [];
|
|
1049
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
1050
|
+
if (seen.has(app.name)) return [];
|
|
1051
|
+
const newSeen = new Set(seen);
|
|
1052
|
+
newSeen.add(app.name);
|
|
1053
|
+
const expanded = [];
|
|
1054
|
+
for (const route of app.routes) {
|
|
1055
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1056
|
+
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1057
|
+
let joined = cleanPrefix + cleanPath;
|
|
1058
|
+
if (joined.length > 1 && joined.endsWith("/")) {
|
|
1059
|
+
joined = joined.slice(0, -1);
|
|
1060
|
+
}
|
|
1061
|
+
expanded.push({
|
|
1062
|
+
...route,
|
|
1063
|
+
path: joined || "/"
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
if (app.mounted) {
|
|
1067
|
+
for (const mount of app.mounted) {
|
|
1068
|
+
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
1069
|
+
if (targetApp) {
|
|
1070
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1071
|
+
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
1072
|
+
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return expanded;
|
|
1077
|
+
};
|
|
1078
|
+
applications.forEach((app) => {
|
|
1079
|
+
astRoutes.push(...getExpandedRoutes(app));
|
|
1080
|
+
});
|
|
1081
|
+
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
1082
|
+
for (const route of astRoutes) {
|
|
1083
|
+
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
1084
|
+
let score = 0;
|
|
1085
|
+
if (route.responseSchema) score += 10;
|
|
1086
|
+
if (route.handlerSource) score += 5;
|
|
1087
|
+
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
1088
|
+
dedupedRoutes.set(key, { route, score });
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1092
|
+
}
|
|
854
1093
|
async function generateOpenApi(rootRouter, options = {}) {
|
|
855
1094
|
const paths = {};
|
|
856
1095
|
const tagGroups = /* @__PURE__ */ new Map();
|
|
@@ -858,61 +1097,11 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
858
1097
|
const defaultTagName = options.defaultTag || "Application";
|
|
859
1098
|
let astRoutes = [];
|
|
860
1099
|
try {
|
|
861
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-
|
|
1100
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-Ce_7JxZh.js");
|
|
862
1101
|
const analyzer = new OpenAPIAnalyzer2(process.cwd());
|
|
863
1102
|
const { applications } = await analyzer.analyze();
|
|
864
|
-
|
|
865
|
-
applications.forEach((app) => {
|
|
866
|
-
appMap.set(app.name, app);
|
|
867
|
-
if (app.name !== app.className) {
|
|
868
|
-
appMap.set(app.className, app);
|
|
869
|
-
}
|
|
870
|
-
});
|
|
871
|
-
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
872
|
-
if (seen.has(app.name)) return [];
|
|
873
|
-
const newSeen = new Set(seen);
|
|
874
|
-
newSeen.add(app.name);
|
|
875
|
-
const expanded = [];
|
|
876
|
-
for (const route of app.routes) {
|
|
877
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
878
|
-
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
879
|
-
let joined = cleanPrefix + cleanPath;
|
|
880
|
-
if (joined.length > 1 && joined.endsWith("/")) {
|
|
881
|
-
joined = joined.slice(0, -1);
|
|
882
|
-
}
|
|
883
|
-
expanded.push({
|
|
884
|
-
...route,
|
|
885
|
-
path: joined || "/"
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
if (app.mounted) {
|
|
889
|
-
for (const mount of app.mounted) {
|
|
890
|
-
const targetApp = appMap.get(mount.target);
|
|
891
|
-
if (targetApp) {
|
|
892
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
893
|
-
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
894
|
-
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
return expanded;
|
|
899
|
-
};
|
|
900
|
-
applications.forEach((app) => {
|
|
901
|
-
astRoutes.push(...getExpandedRoutes(app));
|
|
902
|
-
});
|
|
903
|
-
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
904
|
-
for (const route of astRoutes) {
|
|
905
|
-
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
906
|
-
let score = 0;
|
|
907
|
-
if (route.responseSchema) score += 10;
|
|
908
|
-
if (route.handlerSource) score += 5;
|
|
909
|
-
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
910
|
-
dedupedRoutes.set(key, { route, score });
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
astRoutes = Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1103
|
+
astRoutes = await getAstRoutes(applications);
|
|
914
1104
|
} catch (e) {
|
|
915
|
-
console.warn("OpenAPI AST analysis failed or skipped:", e);
|
|
916
1105
|
}
|
|
917
1106
|
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
|
|
918
1107
|
let group = currentGroup;
|
|
@@ -969,32 +1158,14 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
969
1158
|
(r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
|
|
970
1159
|
);
|
|
971
1160
|
if (!astMatch) {
|
|
972
|
-
|
|
973
|
-
if (route.handler.originalHandler) {
|
|
974
|
-
runtimeSource = route.handler.originalHandler.toString();
|
|
975
|
-
}
|
|
1161
|
+
const runtimeSource = (route.handler.originalHandler || route.handler).toString();
|
|
976
1162
|
const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
|
|
977
1163
|
const sameMethodRoutes = astRoutes.filter((r) => r.method.toUpperCase() === route.method.toUpperCase());
|
|
978
1164
|
astMatch = sameMethodRoutes.find((r) => {
|
|
979
1165
|
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
980
1166
|
if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
|
|
981
|
-
|
|
982
|
-
return match;
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
const potentialMatches = astRoutes.filter(
|
|
986
|
-
(r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
|
|
987
|
-
);
|
|
988
|
-
if (potentialMatches.length > 1) {
|
|
989
|
-
const runtimeHandlerSrc = route.handler.toString().replace(/\s+/g, " ");
|
|
990
|
-
const preciseMatch = potentialMatches.find((r) => {
|
|
991
|
-
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
992
|
-
const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
993
|
-
return match;
|
|
1167
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
994
1168
|
});
|
|
995
|
-
if (preciseMatch) {
|
|
996
|
-
astMatch = preciseMatch;
|
|
997
|
-
}
|
|
998
1169
|
}
|
|
999
1170
|
if (astMatch) {
|
|
1000
1171
|
if (astMatch.summary) operation.summary = astMatch.summary;
|
|
@@ -1003,25 +1174,19 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1003
1174
|
if (astMatch.operationId) operation.operationId = astMatch.operationId;
|
|
1004
1175
|
if (astMatch.requestTypes?.body) {
|
|
1005
1176
|
operation.requestBody = {
|
|
1006
|
-
content: {
|
|
1007
|
-
"application/json": { schema: astMatch.requestTypes.body }
|
|
1008
|
-
}
|
|
1177
|
+
content: { "application/json": { schema: astMatch.requestTypes.body } }
|
|
1009
1178
|
};
|
|
1010
1179
|
}
|
|
1011
1180
|
if (astMatch.responseSchema) {
|
|
1012
1181
|
operation.responses["200"] = {
|
|
1013
1182
|
description: "Successful response",
|
|
1014
|
-
content: {
|
|
1015
|
-
"application/json": { schema: astMatch.responseSchema }
|
|
1016
|
-
}
|
|
1183
|
+
content: { "application/json": { schema: astMatch.responseSchema } }
|
|
1017
1184
|
};
|
|
1018
1185
|
} else if (astMatch.responseType) {
|
|
1019
1186
|
const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
|
|
1020
1187
|
operation.responses["200"] = {
|
|
1021
1188
|
description: "Successful response",
|
|
1022
|
-
content: {
|
|
1023
|
-
[contentType]: { schema: { type: astMatch.responseType } }
|
|
1024
|
-
}
|
|
1189
|
+
content: { [contentType]: { schema: { type: astMatch.responseType } } }
|
|
1025
1190
|
};
|
|
1026
1191
|
}
|
|
1027
1192
|
const params = [];
|
|
@@ -1072,15 +1237,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1072
1237
|
deepMerge(operation, inferredSpec);
|
|
1073
1238
|
}
|
|
1074
1239
|
if (route.handlerSpec) {
|
|
1075
|
-
|
|
1076
|
-
if (spec.summary) operation.summary = spec.summary;
|
|
1077
|
-
if (spec.description) operation.description = spec.description;
|
|
1078
|
-
if (spec.operationId) operation.operationId = spec.operationId;
|
|
1079
|
-
if (spec.tags) operation.tags = spec.tags;
|
|
1080
|
-
if (spec.security) operation.security = spec.security;
|
|
1081
|
-
if (spec.responses) {
|
|
1082
|
-
operation.responses = { ...operation.responses, ...spec.responses };
|
|
1083
|
-
}
|
|
1240
|
+
deepMerge(operation, route.handlerSpec);
|
|
1084
1241
|
}
|
|
1085
1242
|
if (!operation.tags || operation.tags.length === 0) operation.tags = [tag];
|
|
1086
1243
|
if (operation.tags) {
|
|
@@ -1099,11 +1256,13 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1099
1256
|
paths[fullPath][methodLower] = operation;
|
|
1100
1257
|
}
|
|
1101
1258
|
}
|
|
1102
|
-
|
|
1259
|
+
const controllers = router[$childControllers];
|
|
1260
|
+
for (const controller of controllers) {
|
|
1103
1261
|
const controllerName = controller.constructor.name || "UnknownController";
|
|
1104
1262
|
tagGroups.get(group)?.add(controllerName);
|
|
1105
1263
|
}
|
|
1106
|
-
|
|
1264
|
+
const childRouters = router[$childRouters];
|
|
1265
|
+
for (const child of childRouters) {
|
|
1107
1266
|
const mountPath = child[$mountPath];
|
|
1108
1267
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1109
1268
|
const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
|
|
@@ -1113,7 +1272,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1113
1272
|
};
|
|
1114
1273
|
collect(rootRouter);
|
|
1115
1274
|
const xTagGroups = [];
|
|
1116
|
-
for (const [name, tags] of tagGroups) {
|
|
1275
|
+
for (const [name, tags] of tagGroups.entries()) {
|
|
1117
1276
|
xTagGroups.push({ name, tags: Array.from(tags).sort() });
|
|
1118
1277
|
}
|
|
1119
1278
|
return {
|
|
@@ -1148,7 +1307,8 @@ function serveStatic(config, prefix) {
|
|
|
1148
1307
|
if (res) return res;
|
|
1149
1308
|
}
|
|
1150
1309
|
if (config.exclude) {
|
|
1151
|
-
for (
|
|
1310
|
+
for (let i = 0; i < config.exclude.length; i++) {
|
|
1311
|
+
const pattern = config.exclude[i];
|
|
1152
1312
|
if (pattern instanceof RegExp) {
|
|
1153
1313
|
if (pattern.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
|
|
1154
1314
|
} else if (typeof pattern === "string") {
|
|
@@ -1167,7 +1327,8 @@ function serveStatic(config, prefix) {
|
|
|
1167
1327
|
stats = await stat(requestPath);
|
|
1168
1328
|
} catch (e) {
|
|
1169
1329
|
if (config.extensions) {
|
|
1170
|
-
for (
|
|
1330
|
+
for (let i = 0; i < config.extensions.length; i++) {
|
|
1331
|
+
const ext = config.extensions[i];
|
|
1171
1332
|
const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
|
|
1172
1333
|
try {
|
|
1173
1334
|
const s = await stat(p);
|
|
@@ -1196,7 +1357,8 @@ function serveStatic(config, prefix) {
|
|
|
1196
1357
|
indexes = [config.index];
|
|
1197
1358
|
}
|
|
1198
1359
|
let foundIndex = false;
|
|
1199
|
-
for (
|
|
1360
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
1361
|
+
const idx = indexes[i];
|
|
1200
1362
|
const idxPath = join(finalPath, idx);
|
|
1201
1363
|
try {
|
|
1202
1364
|
const idxStats = await stat(idxPath);
|
|
@@ -1278,7 +1440,8 @@ class RouterTrie {
|
|
|
1278
1440
|
insert(method, path, handler) {
|
|
1279
1441
|
let node = this.root;
|
|
1280
1442
|
const segments = this.splitPath(path);
|
|
1281
|
-
for (
|
|
1443
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1444
|
+
const segment = segments[i];
|
|
1282
1445
|
if (segment === "**") {
|
|
1283
1446
|
if (!node.recursiveChild) {
|
|
1284
1447
|
node.recursiveChild = this.createNode();
|
|
@@ -1365,40 +1528,68 @@ class RouterTrie {
|
|
|
1365
1528
|
}
|
|
1366
1529
|
}
|
|
1367
1530
|
const asyncContext = new AsyncLocalStorage();
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
return
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1531
|
+
let db;
|
|
1532
|
+
let dbPromise = null;
|
|
1533
|
+
let RecordId;
|
|
1534
|
+
async function ensureDb() {
|
|
1535
|
+
if (db) return db;
|
|
1536
|
+
if (dbPromise) return dbPromise;
|
|
1537
|
+
dbPromise = (async () => {
|
|
1538
|
+
try {
|
|
1539
|
+
const { createNodeEngines } = await import("@surrealdb/node");
|
|
1540
|
+
const surreal = await import("surrealdb");
|
|
1541
|
+
const Surreal = surreal.Surreal;
|
|
1542
|
+
RecordId = surreal.RecordId;
|
|
1543
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1544
|
+
const _db = new Surreal({
|
|
1545
|
+
engines: createNodeEngines()
|
|
1546
|
+
});
|
|
1547
|
+
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1548
|
+
await _db.query(`
|
|
1549
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1550
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1551
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1552
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1553
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1554
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1555
|
+
`);
|
|
1556
|
+
db = _db;
|
|
1557
|
+
return db;
|
|
1558
|
+
} catch (e) {
|
|
1559
|
+
dbPromise = null;
|
|
1560
|
+
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1561
|
+
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1562
|
+
}
|
|
1563
|
+
throw e;
|
|
1564
|
+
}
|
|
1565
|
+
})();
|
|
1566
|
+
return dbPromise;
|
|
1567
|
+
}
|
|
1382
1568
|
const datastore = {
|
|
1383
|
-
get(store, key) {
|
|
1569
|
+
async get(store, key) {
|
|
1570
|
+
await ensureDb();
|
|
1384
1571
|
return db.select(new RecordId(store, key));
|
|
1385
1572
|
},
|
|
1386
|
-
set(store, key, value) {
|
|
1573
|
+
async set(store, key, value) {
|
|
1574
|
+
await ensureDb();
|
|
1387
1575
|
return db.create(new RecordId(store, key)).content(value);
|
|
1388
1576
|
},
|
|
1389
1577
|
async query(query, vars) {
|
|
1578
|
+
await ensureDb();
|
|
1390
1579
|
try {
|
|
1391
|
-
const r = await db.query(query, vars)
|
|
1392
|
-
return r;
|
|
1580
|
+
const r = await db.query(query, vars);
|
|
1581
|
+
return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
|
|
1393
1582
|
} catch (e) {
|
|
1394
1583
|
console.error("DS ERROR:", e);
|
|
1395
1584
|
throw e;
|
|
1396
1585
|
}
|
|
1397
1586
|
},
|
|
1398
|
-
ready
|
|
1587
|
+
get ready() {
|
|
1588
|
+
return ensureDb().then(() => void 0);
|
|
1589
|
+
}
|
|
1399
1590
|
};
|
|
1400
1591
|
process.on("exit", async () => {
|
|
1401
|
-
await db.close();
|
|
1592
|
+
if (db) await db.close();
|
|
1402
1593
|
});
|
|
1403
1594
|
const tracer = trace.getTracer("shokupan.middleware");
|
|
1404
1595
|
function traceHandler(fn, name) {
|
|
@@ -1471,6 +1662,8 @@ class ShokupanRouter {
|
|
|
1471
1662
|
[$parent] = null;
|
|
1472
1663
|
[$childRouters] = [];
|
|
1473
1664
|
[$childControllers] = [];
|
|
1665
|
+
hookCache = /* @__PURE__ */ new Map();
|
|
1666
|
+
hooksInitialized = false;
|
|
1474
1667
|
middleware = [];
|
|
1475
1668
|
get rootConfig() {
|
|
1476
1669
|
return this[$appRoot]?.applicationConfig;
|
|
@@ -1488,7 +1681,8 @@ class ShokupanRouter {
|
|
|
1488
1681
|
getComponentRegistry() {
|
|
1489
1682
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
1490
1683
|
const localRoutes = [];
|
|
1491
|
-
for (
|
|
1684
|
+
for (let i = 0; i < this[$routes].length; i++) {
|
|
1685
|
+
const r = this[$routes][i];
|
|
1492
1686
|
const entry = {
|
|
1493
1687
|
type: "route",
|
|
1494
1688
|
path: r.path,
|
|
@@ -1625,7 +1819,8 @@ class ShokupanRouter {
|
|
|
1625
1819
|
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
1626
1820
|
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
1627
1821
|
let routesAttached = 0;
|
|
1628
|
-
for (
|
|
1822
|
+
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
1823
|
+
const name = Array.from(methods)[i];
|
|
1629
1824
|
if (name === "constructor") continue;
|
|
1630
1825
|
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
1631
1826
|
const originalHandler = instance[name];
|
|
@@ -1637,7 +1832,8 @@ class ShokupanRouter {
|
|
|
1637
1832
|
method = config.method;
|
|
1638
1833
|
subPath = config.path;
|
|
1639
1834
|
} else {
|
|
1640
|
-
for (
|
|
1835
|
+
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
1836
|
+
const m = HTTPMethods[j];
|
|
1641
1837
|
if (name.toUpperCase().startsWith(m)) {
|
|
1642
1838
|
method = m;
|
|
1643
1839
|
const rest = name.slice(m.length);
|
|
@@ -1652,8 +1848,8 @@ class ShokupanRouter {
|
|
|
1652
1848
|
buffer = "";
|
|
1653
1849
|
}
|
|
1654
1850
|
};
|
|
1655
|
-
for (let
|
|
1656
|
-
const char = rest[
|
|
1851
|
+
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
1852
|
+
const char = rest[i2];
|
|
1657
1853
|
if (char === "$") {
|
|
1658
1854
|
flush();
|
|
1659
1855
|
subPath += "/:";
|
|
@@ -1691,7 +1887,8 @@ class ShokupanRouter {
|
|
|
1691
1887
|
if (routeArgs?.length > 0) {
|
|
1692
1888
|
args = [];
|
|
1693
1889
|
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
1694
|
-
for (
|
|
1890
|
+
for (let k = 0; k < sortedArgs.length; k++) {
|
|
1891
|
+
const arg = sortedArgs[k];
|
|
1695
1892
|
switch (arg.type) {
|
|
1696
1893
|
case RouteParamType.BODY:
|
|
1697
1894
|
try {
|
|
@@ -1721,7 +1918,9 @@ class ShokupanRouter {
|
|
|
1721
1918
|
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1722
1919
|
} else {
|
|
1723
1920
|
const query = {};
|
|
1724
|
-
|
|
1921
|
+
const keys = Object.keys(url.searchParams);
|
|
1922
|
+
for (let k2 = 0; k2 < keys.length; k2++) {
|
|
1923
|
+
const key = keys[k2];
|
|
1725
1924
|
const vals = url.searchParams.getAll(key);
|
|
1726
1925
|
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1727
1926
|
}
|
|
@@ -1778,9 +1977,11 @@ class ShokupanRouter {
|
|
|
1778
1977
|
path: r.path,
|
|
1779
1978
|
handler: r.handler
|
|
1780
1979
|
}));
|
|
1781
|
-
for (
|
|
1980
|
+
for (let i = 0; i < this[$childRouters].length; i++) {
|
|
1981
|
+
const child = this[$childRouters][i];
|
|
1782
1982
|
const childRoutes = child.getRoutes();
|
|
1783
|
-
for (
|
|
1983
|
+
for (let j = 0; j < childRoutes.length; j++) {
|
|
1984
|
+
const route = childRoutes[j];
|
|
1784
1985
|
const cleanPrefix = child[$mountPath].endsWith("/") ? child[$mountPath].slice(0, -1) : child[$mountPath];
|
|
1785
1986
|
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1786
1987
|
const fullPath = cleanPrefix + cleanPath || "/";
|
|
@@ -1794,12 +1995,12 @@ class ShokupanRouter {
|
|
|
1794
1995
|
return routes;
|
|
1795
1996
|
}
|
|
1796
1997
|
/**
|
|
1797
|
-
* Makes
|
|
1798
|
-
* This is useful for
|
|
1998
|
+
* Makes an internal request through this router's full routing pipeline.
|
|
1999
|
+
* This is useful for calling other routes internally and supports streaming responses.
|
|
1799
2000
|
* @param options The request options.
|
|
1800
|
-
* @returns The
|
|
2001
|
+
* @returns The raw Response object.
|
|
1801
2002
|
*/
|
|
1802
|
-
async
|
|
2003
|
+
async internalRequest(arg) {
|
|
1803
2004
|
const options = typeof arg === "string" ? { path: arg } : arg;
|
|
1804
2005
|
const store = asyncContext.getStore();
|
|
1805
2006
|
store?.get("req");
|
|
@@ -1818,9 +2019,10 @@ class ShokupanRouter {
|
|
|
1818
2019
|
return this.root[$dispatch](req);
|
|
1819
2020
|
}
|
|
1820
2021
|
/**
|
|
1821
|
-
* Processes a request
|
|
2022
|
+
* Processes a request for testing purposes.
|
|
2023
|
+
* Returns a simplified { status, headers, data } object instead of a Response.
|
|
1822
2024
|
*/
|
|
1823
|
-
async
|
|
2025
|
+
async testRequest(options) {
|
|
1824
2026
|
let url = options.url || options.path || "/";
|
|
1825
2027
|
if (!url.startsWith("http")) {
|
|
1826
2028
|
const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig?.port || 3e3}`;
|
|
@@ -1829,7 +2031,9 @@ class ShokupanRouter {
|
|
|
1829
2031
|
}
|
|
1830
2032
|
if (options.query) {
|
|
1831
2033
|
const u = new URL(url);
|
|
1832
|
-
|
|
2034
|
+
const entries = Object.entries(options.query);
|
|
2035
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2036
|
+
const [k, v] = entries[i];
|
|
1833
2037
|
u.searchParams.set(k, v);
|
|
1834
2038
|
}
|
|
1835
2039
|
url = u.toString();
|
|
@@ -1874,28 +2078,17 @@ class ShokupanRouter {
|
|
|
1874
2078
|
data: result
|
|
1875
2079
|
};
|
|
1876
2080
|
}
|
|
1877
|
-
|
|
1878
|
-
if (!this.
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
}
|
|
1885
|
-
wrapWithHooks(handler, hooks) {
|
|
1886
|
-
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
1887
|
-
const hasStart = hookList.some((h) => !!h.onRequestStart);
|
|
1888
|
-
const hasEnd = hookList.some((h) => !!h.onRequestEnd);
|
|
1889
|
-
const hasError = hookList.some((h) => !!h.onError);
|
|
2081
|
+
wrapWithHooks(handler) {
|
|
2082
|
+
if (!this.hooksInitialized) {
|
|
2083
|
+
this.ensureHooksInitialized();
|
|
2084
|
+
}
|
|
2085
|
+
const hasStart = this.hookCache.get("onRequestStart")?.length > 0;
|
|
2086
|
+
const hasEnd = this.hookCache.get("onRequestEnd")?.length > 0;
|
|
2087
|
+
const hasError = this.hookCache.get("onError")?.length > 0;
|
|
1890
2088
|
if (!hasStart && !hasEnd && !hasError) return handler;
|
|
1891
2089
|
const originalHandler = handler;
|
|
1892
2090
|
const wrapped = async (ctx) => {
|
|
1893
|
-
|
|
1894
|
-
for (let i = 0; i < hookList.length; i++) {
|
|
1895
|
-
const h = hookList[i];
|
|
1896
|
-
if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
2091
|
+
await this.runHooks("onRequestStart", ctx);
|
|
1899
2092
|
const debug = ctx._debug;
|
|
1900
2093
|
let debugId;
|
|
1901
2094
|
let previousNode;
|
|
@@ -1909,17 +2102,11 @@ class ShokupanRouter {
|
|
|
1909
2102
|
try {
|
|
1910
2103
|
const res = await originalHandler(ctx);
|
|
1911
2104
|
debug?.trackStep(debugId, "handler", performance.now() - start, "success");
|
|
1912
|
-
|
|
1913
|
-
const h = hookList[i];
|
|
1914
|
-
if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
|
|
1915
|
-
}
|
|
2105
|
+
await this.runHooks("onRequestEnd", ctx);
|
|
1916
2106
|
return res;
|
|
1917
2107
|
} catch (err) {
|
|
1918
2108
|
debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
|
|
1919
|
-
|
|
1920
|
-
const h = hookList[i];
|
|
1921
|
-
if (typeof h.onError === "function") await h.onError(err, ctx);
|
|
1922
|
-
}
|
|
2109
|
+
await this.runHooks("onError", ctx, err);
|
|
1923
2110
|
throw err;
|
|
1924
2111
|
} finally {
|
|
1925
2112
|
if (debug && previousNode) debug.setNode(previousNode);
|
|
@@ -1941,18 +2128,19 @@ class ShokupanRouter {
|
|
|
1941
2128
|
result = this.trie.search("GET", path);
|
|
1942
2129
|
if (result) return result;
|
|
1943
2130
|
}
|
|
1944
|
-
for (
|
|
2131
|
+
for (let i = 0; i < this[$childRouters].length; i++) {
|
|
2132
|
+
const child = this[$childRouters][i];
|
|
1945
2133
|
const prefix = child[$mountPath];
|
|
1946
2134
|
if (path === prefix || path.startsWith(prefix + "/")) {
|
|
1947
2135
|
const subPath = path.slice(prefix.length) || "/";
|
|
1948
2136
|
const match = child.find(method, subPath);
|
|
1949
|
-
if (match) return
|
|
2137
|
+
if (match) return match;
|
|
1950
2138
|
}
|
|
1951
2139
|
if (prefix.endsWith("/")) {
|
|
1952
2140
|
if (path.startsWith(prefix)) {
|
|
1953
2141
|
const subPath = path.slice(prefix.length) || "/";
|
|
1954
2142
|
const match = child.find(method, subPath);
|
|
1955
|
-
if (match) return
|
|
2143
|
+
if (match) return match;
|
|
1956
2144
|
}
|
|
1957
2145
|
}
|
|
1958
2146
|
}
|
|
@@ -1974,17 +2162,23 @@ class ShokupanRouter {
|
|
|
1974
2162
|
/**
|
|
1975
2163
|
* Adds a route to the router.
|
|
1976
2164
|
*
|
|
1977
|
-
* @param
|
|
1978
|
-
* @param
|
|
1979
|
-
* @param
|
|
1980
|
-
* @param
|
|
1981
|
-
* @param
|
|
2165
|
+
* @param arg - Route configuration object
|
|
2166
|
+
* @param arg.method - HTTP method
|
|
2167
|
+
* @param arg.path - URL path
|
|
2168
|
+
* @param arg.spec - OpenAPI specification for the route
|
|
2169
|
+
* @param arg.handler - Route handler function
|
|
2170
|
+
* @param arg.regex - Custom regex for path matching
|
|
2171
|
+
* @param arg.group - Group for the route
|
|
2172
|
+
* @param arg.requestTimeout - Timeout for this route in milliseconds
|
|
2173
|
+
* @param arg.renderer - JSX renderer for the route
|
|
2174
|
+
* @param arg.controller - Controller for the route
|
|
1982
2175
|
*/
|
|
1983
2176
|
add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
1984
2177
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path);
|
|
1985
2178
|
if (this.currentGuards.length > 0) {
|
|
1986
2179
|
spec = spec || {};
|
|
1987
|
-
for (
|
|
2180
|
+
for (let i = 0; i < this.currentGuards.length; i++) {
|
|
2181
|
+
const guard = this.currentGuards[i];
|
|
1988
2182
|
if (guard.spec) {
|
|
1989
2183
|
if (guard.spec.responses) {
|
|
1990
2184
|
spec.responses = spec.responses || {};
|
|
@@ -2013,7 +2207,8 @@ class ShokupanRouter {
|
|
|
2013
2207
|
if (routeGuards.length > 0) {
|
|
2014
2208
|
const innerHandler = wrappedHandler;
|
|
2015
2209
|
wrappedHandler = async (ctx) => {
|
|
2016
|
-
for (
|
|
2210
|
+
for (let i = 0; i < routeGuards.length; i++) {
|
|
2211
|
+
const guard = routeGuards[i];
|
|
2017
2212
|
let guardPassed = false;
|
|
2018
2213
|
let nextCalled = false;
|
|
2019
2214
|
const next = () => {
|
|
@@ -2071,41 +2266,43 @@ class ShokupanRouter {
|
|
|
2071
2266
|
if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2072
2267
|
const duration = performance.now() - startTime;
|
|
2073
2268
|
const config = ctx.app.applicationConfig;
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2269
|
+
Promise.resolve().then(async () => {
|
|
2270
|
+
try {
|
|
2271
|
+
const timestamp = Date.now();
|
|
2272
|
+
const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
|
|
2273
|
+
await datastore.set("middleware_tracking", key, {
|
|
2274
|
+
name: handler.name || "anonymous",
|
|
2275
|
+
path: ctx.path,
|
|
2276
|
+
timestamp,
|
|
2277
|
+
duration,
|
|
2278
|
+
file,
|
|
2279
|
+
line,
|
|
2280
|
+
error: error ? String(error) : void 0,
|
|
2281
|
+
metadata: {
|
|
2282
|
+
isBuiltin: handler.isBuiltin,
|
|
2283
|
+
pluginName: handler.pluginName
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2287
|
+
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2288
|
+
const cutoff = Date.now() - ttl;
|
|
2289
|
+
await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2290
|
+
const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2291
|
+
if (results && results[0] && results[0].count > maxCapacity) {
|
|
2292
|
+
const toDelete = results[0].count - maxCapacity;
|
|
2293
|
+
await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2088
2294
|
}
|
|
2089
|
-
})
|
|
2090
|
-
|
|
2091
|
-
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2092
|
-
const cutoff = Date.now() - ttl;
|
|
2093
|
-
await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2094
|
-
const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2095
|
-
if (results && results[0] && results[0].count > maxCapacity) {
|
|
2096
|
-
const toDelete = results[0].count - maxCapacity;
|
|
2097
|
-
await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2295
|
+
} catch (datastoreError) {
|
|
2296
|
+
console.error("Failed to store middleware tracking:", datastoreError);
|
|
2098
2297
|
}
|
|
2099
|
-
}
|
|
2100
|
-
console.error("Failed to store middleware tracking:", datastoreError);
|
|
2101
|
-
}
|
|
2298
|
+
});
|
|
2102
2299
|
}
|
|
2103
2300
|
}
|
|
2104
2301
|
};
|
|
2105
2302
|
wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
|
|
2106
2303
|
let bakedHandler = wrappedHandler;
|
|
2107
2304
|
if (this.config?.hooks) {
|
|
2108
|
-
bakedHandler = this.wrapWithHooks(wrappedHandler
|
|
2305
|
+
bakedHandler = this.wrapWithHooks(wrappedHandler);
|
|
2109
2306
|
}
|
|
2110
2307
|
this[$routes].push({
|
|
2111
2308
|
method,
|
|
@@ -2262,6 +2459,67 @@ class ShokupanRouter {
|
|
|
2262
2459
|
generateApiSpec(options = {}) {
|
|
2263
2460
|
return generateOpenApi(this, options);
|
|
2264
2461
|
}
|
|
2462
|
+
ensureHooksInitialized() {
|
|
2463
|
+
const hooks = this.config?.hooks;
|
|
2464
|
+
if (hooks) {
|
|
2465
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2466
|
+
const hookTypes = [
|
|
2467
|
+
"onRequestStart",
|
|
2468
|
+
"onRequestEnd",
|
|
2469
|
+
"onResponseStart",
|
|
2470
|
+
"onResponseEnd",
|
|
2471
|
+
"onError",
|
|
2472
|
+
"beforeValidate",
|
|
2473
|
+
"afterValidate",
|
|
2474
|
+
"onRequestTimeout",
|
|
2475
|
+
"onReadTimeout",
|
|
2476
|
+
"onWriteTimeout"
|
|
2477
|
+
];
|
|
2478
|
+
for (let i = 0; i < hookTypes.length; i++) {
|
|
2479
|
+
const type = hookTypes[i];
|
|
2480
|
+
const fns = [];
|
|
2481
|
+
for (let j = 0; j < hookList.length; j++) {
|
|
2482
|
+
const h = hookList[j];
|
|
2483
|
+
if (h[type]) fns.push(h[type]);
|
|
2484
|
+
}
|
|
2485
|
+
if (fns.length > 0) {
|
|
2486
|
+
this.hookCache.set(type, fns);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
this.hooksInitialized = true;
|
|
2491
|
+
}
|
|
2492
|
+
async runHooks(name, ...args) {
|
|
2493
|
+
if (!this.hooksInitialized) {
|
|
2494
|
+
this.ensureHooksInitialized();
|
|
2495
|
+
}
|
|
2496
|
+
const fns = this.hookCache.get(name);
|
|
2497
|
+
if (!fns) return;
|
|
2498
|
+
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2499
|
+
const debug = ctx?._debug;
|
|
2500
|
+
if (debug) {
|
|
2501
|
+
await Promise.all(fns.map(async (fn, index) => {
|
|
2502
|
+
const hookId = `hook_${name}_${fn.name || index}`;
|
|
2503
|
+
const previousNode = debug.getCurrentNode();
|
|
2504
|
+
debug.trackEdge(previousNode, hookId);
|
|
2505
|
+
debug.setNode(hookId);
|
|
2506
|
+
const start = performance.now();
|
|
2507
|
+
try {
|
|
2508
|
+
await fn(...args);
|
|
2509
|
+
const duration = performance.now() - start;
|
|
2510
|
+
debug.trackStep(hookId, "hook", duration, "success");
|
|
2511
|
+
} catch (error) {
|
|
2512
|
+
const duration = performance.now() - start;
|
|
2513
|
+
debug.trackStep(hookId, "hook", duration, "error", error);
|
|
2514
|
+
throw error;
|
|
2515
|
+
} finally {
|
|
2516
|
+
if (previousNode) debug.setNode(previousNode);
|
|
2517
|
+
}
|
|
2518
|
+
}));
|
|
2519
|
+
} else {
|
|
2520
|
+
await Promise.all(fns.map((fn) => fn(...args)));
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2265
2523
|
}
|
|
2266
2524
|
class SystemCpuMonitor {
|
|
2267
2525
|
constructor(intervalMs = 1e3) {
|
|
@@ -2319,15 +2577,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2319
2577
|
openApiSpec;
|
|
2320
2578
|
composedMiddleware;
|
|
2321
2579
|
cpuMonitor;
|
|
2322
|
-
hookCache = /* @__PURE__ */ new Map();
|
|
2323
|
-
hooksInitialized = false;
|
|
2324
2580
|
get logger() {
|
|
2325
2581
|
return this.applicationConfig.logger;
|
|
2326
2582
|
}
|
|
2327
2583
|
constructor(applicationConfig = {}) {
|
|
2328
2584
|
const config = Object.assign({}, defaults, applicationConfig);
|
|
2329
2585
|
const { hooks, ...routerConfig } = config;
|
|
2330
|
-
super(routerConfig);
|
|
2586
|
+
super({ ...routerConfig, hooks });
|
|
2331
2587
|
this[$isApplication] = true;
|
|
2332
2588
|
this[$appRoot] = this;
|
|
2333
2589
|
this.applicationConfig = config;
|
|
@@ -2342,7 +2598,6 @@ class Shokupan extends ShokupanRouter {
|
|
|
2342
2598
|
* Adds middleware to the application.
|
|
2343
2599
|
*/
|
|
2344
2600
|
use(middleware) {
|
|
2345
|
-
let trackedMiddleware = middleware;
|
|
2346
2601
|
const { file, line } = getCallerInfo();
|
|
2347
2602
|
if (!middleware.metadata) {
|
|
2348
2603
|
middleware.metadata = {
|
|
@@ -2353,32 +2608,36 @@ class Shokupan extends ShokupanRouter {
|
|
|
2353
2608
|
pluginName: middleware.pluginName
|
|
2354
2609
|
};
|
|
2355
2610
|
}
|
|
2356
|
-
|
|
2357
|
-
const
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2611
|
+
if (this.applicationConfig.enableMiddlewareTracking) {
|
|
2612
|
+
const trackedMiddleware = async (ctx, next) => {
|
|
2613
|
+
const c = ctx;
|
|
2614
|
+
if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
|
|
2615
|
+
const metadata = middleware.metadata || {};
|
|
2616
|
+
const start = performance.now();
|
|
2617
|
+
const item = {
|
|
2618
|
+
name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
|
|
2619
|
+
file: metadata.file || file,
|
|
2620
|
+
line: metadata.line || line,
|
|
2621
|
+
isBuiltin: metadata.isBuiltin,
|
|
2622
|
+
startTime: start,
|
|
2623
|
+
duration: -1
|
|
2624
|
+
};
|
|
2625
|
+
c.handlerStack.push(item);
|
|
2626
|
+
try {
|
|
2627
|
+
return await middleware(ctx, next);
|
|
2628
|
+
} finally {
|
|
2629
|
+
item.duration = performance.now() - start;
|
|
2630
|
+
}
|
|
2374
2631
|
}
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2632
|
+
return middleware(ctx, next);
|
|
2633
|
+
};
|
|
2634
|
+
trackedMiddleware.metadata = middleware.metadata;
|
|
2635
|
+
Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
|
|
2636
|
+
trackedMiddleware.order = this.middleware.length;
|
|
2637
|
+
this.middleware.push(trackedMiddleware);
|
|
2638
|
+
} else {
|
|
2639
|
+
this.middleware.push(middleware);
|
|
2640
|
+
}
|
|
2382
2641
|
return this;
|
|
2383
2642
|
}
|
|
2384
2643
|
startupHooks = [];
|
|
@@ -2409,17 +2668,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2409
2668
|
if (finalPort < 0 || finalPort > 65535) {
|
|
2410
2669
|
throw new Error("Invalid port number");
|
|
2411
2670
|
}
|
|
2412
|
-
|
|
2413
|
-
await hook();
|
|
2414
|
-
}
|
|
2671
|
+
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
2415
2672
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
2416
2673
|
this.openApiSpec = await generateOpenApi(this);
|
|
2417
|
-
|
|
2418
|
-
await hook(this.openApiSpec);
|
|
2419
|
-
}
|
|
2674
|
+
await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
|
|
2420
2675
|
}
|
|
2421
2676
|
if (port === 0 && process.platform === "linux") ;
|
|
2422
|
-
if (this.applicationConfig.autoBackpressureFeedback) {
|
|
2677
|
+
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
2423
2678
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
2424
2679
|
this.cpuMonitor.start();
|
|
2425
2680
|
}
|
|
@@ -2447,11 +2702,11 @@ class Shokupan extends ShokupanRouter {
|
|
|
2447
2702
|
};
|
|
2448
2703
|
let factory = this.applicationConfig.serverFactory;
|
|
2449
2704
|
if (!factory && typeof Bun === "undefined") {
|
|
2450
|
-
const { createHttpServer } = await import("./server-adapter-
|
|
2705
|
+
const { createHttpServer } = await import("./server-adapter-0xH174zz.js");
|
|
2451
2706
|
factory = createHttpServer();
|
|
2452
2707
|
}
|
|
2453
2708
|
const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
2454
|
-
console.log(`Shokupan server listening on http://${
|
|
2709
|
+
console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
|
|
2455
2710
|
return server;
|
|
2456
2711
|
}
|
|
2457
2712
|
[$dispatch](req) {
|
|
@@ -2460,7 +2715,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2460
2715
|
/**
|
|
2461
2716
|
* Processes a request by wrapping the standard fetch method.
|
|
2462
2717
|
*/
|
|
2463
|
-
async
|
|
2718
|
+
async testRequest(options) {
|
|
2464
2719
|
let url = options.url || options.path || "/";
|
|
2465
2720
|
if (!url.startsWith("http")) {
|
|
2466
2721
|
const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
|
|
@@ -2469,7 +2724,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
2469
2724
|
}
|
|
2470
2725
|
if (options.query) {
|
|
2471
2726
|
const u = new URL(url);
|
|
2472
|
-
|
|
2727
|
+
const entries = Object.entries(options.query);
|
|
2728
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2729
|
+
const [k, v] = entries[i];
|
|
2473
2730
|
u.searchParams.set(k, v);
|
|
2474
2731
|
}
|
|
2475
2732
|
url = u.toString();
|
|
@@ -2538,18 +2795,18 @@ class Shokupan extends ShokupanRouter {
|
|
|
2538
2795
|
if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
|
|
2539
2796
|
const msg = "Too Many Requests (CPU Backpressure)";
|
|
2540
2797
|
const res = ctx.text(msg, 429);
|
|
2541
|
-
await this.
|
|
2798
|
+
await this.runHooks("onResponseEnd", ctx, res);
|
|
2542
2799
|
return res;
|
|
2543
2800
|
}
|
|
2544
2801
|
try {
|
|
2545
|
-
|
|
2546
|
-
await this.executeHook("onRequestStart", ctx);
|
|
2547
|
-
}
|
|
2802
|
+
await this.runHooks("onRequestStart", ctx);
|
|
2548
2803
|
const fn = this.composedMiddleware ??= compose(this.middleware);
|
|
2549
2804
|
const result = await fn(ctx, async () => {
|
|
2805
|
+
const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
|
|
2550
2806
|
const match = this.find(req.method, ctx.path);
|
|
2551
2807
|
if (match) {
|
|
2552
2808
|
ctx.params = match.params;
|
|
2809
|
+
await bodyParsing;
|
|
2553
2810
|
return match.handler(ctx);
|
|
2554
2811
|
}
|
|
2555
2812
|
return null;
|
|
@@ -2572,12 +2829,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
2572
2829
|
} else {
|
|
2573
2830
|
response = ctx.text(String(result));
|
|
2574
2831
|
}
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
}
|
|
2578
|
-
if (this.hasHook("onResponseStart")) {
|
|
2579
|
-
await this.executeHook("onResponseStart", ctx, response);
|
|
2580
|
-
}
|
|
2832
|
+
await this.runHooks("onRequestEnd", ctx);
|
|
2833
|
+
await this.runHooks("onResponseStart", ctx, response);
|
|
2581
2834
|
return response;
|
|
2582
2835
|
} catch (err) {
|
|
2583
2836
|
console.error(err);
|
|
@@ -2586,9 +2839,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2586
2839
|
const status = err.status || err.statusCode || 500;
|
|
2587
2840
|
const body = { error: err.message || "Internal Server Error" };
|
|
2588
2841
|
if (err.errors) body.errors = err.errors;
|
|
2589
|
-
|
|
2590
|
-
await this.executeHook("onError", err, ctx);
|
|
2591
|
-
}
|
|
2842
|
+
await this.runHooks("onError", ctx, err);
|
|
2592
2843
|
return ctx.json(body, status);
|
|
2593
2844
|
}
|
|
2594
2845
|
};
|
|
@@ -2599,9 +2850,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2599
2850
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2600
2851
|
timeoutId = setTimeout(async () => {
|
|
2601
2852
|
controller.abort();
|
|
2602
|
-
|
|
2603
|
-
await this.executeHook("onRequestTimeout", ctx);
|
|
2604
|
-
}
|
|
2853
|
+
await this.runHooks("onRequestTimeout", ctx);
|
|
2605
2854
|
reject(new Error("Request Timeout"));
|
|
2606
2855
|
}, timeoutMs);
|
|
2607
2856
|
});
|
|
@@ -2614,56 +2863,10 @@ class Shokupan extends ShokupanRouter {
|
|
|
2614
2863
|
console.error("Unexpected error in request execution:", err);
|
|
2615
2864
|
return ctx.text("Internal Server Error", 500);
|
|
2616
2865
|
}).then(async (res) => {
|
|
2617
|
-
|
|
2618
|
-
await this.executeHook("onResponseEnd", ctx, res);
|
|
2619
|
-
}
|
|
2866
|
+
await this.runHooks("onResponseEnd", ctx, res);
|
|
2620
2867
|
return res;
|
|
2621
2868
|
});
|
|
2622
2869
|
}
|
|
2623
|
-
ensureHooksInitialized() {
|
|
2624
|
-
const hooks = this.applicationConfig.hooks;
|
|
2625
|
-
if (hooks) {
|
|
2626
|
-
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2627
|
-
const hookTypes = [
|
|
2628
|
-
"onRequestStart",
|
|
2629
|
-
"onRequestEnd",
|
|
2630
|
-
"onResponseStart",
|
|
2631
|
-
"onResponseEnd",
|
|
2632
|
-
"onError",
|
|
2633
|
-
"beforeValidate",
|
|
2634
|
-
"afterValidate",
|
|
2635
|
-
"onRequestTimeout",
|
|
2636
|
-
"onReadTimeout",
|
|
2637
|
-
"onWriteTimeout"
|
|
2638
|
-
];
|
|
2639
|
-
for (const type of hookTypes) {
|
|
2640
|
-
const fns = [];
|
|
2641
|
-
for (const h of hookList) {
|
|
2642
|
-
if (h[type]) fns.push(h[type]);
|
|
2643
|
-
}
|
|
2644
|
-
if (fns.length > 0) {
|
|
2645
|
-
this.hookCache.set(type, fns);
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
this.hooksInitialized = true;
|
|
2650
|
-
}
|
|
2651
|
-
async executeHook(name, ...args) {
|
|
2652
|
-
if (!this.hooksInitialized) {
|
|
2653
|
-
this.ensureHooksInitialized();
|
|
2654
|
-
}
|
|
2655
|
-
const fns = this.hookCache.get(name);
|
|
2656
|
-
if (!fns) return;
|
|
2657
|
-
for (const fn of fns) {
|
|
2658
|
-
await fn(...args);
|
|
2659
|
-
}
|
|
2660
|
-
}
|
|
2661
|
-
hasHook(name) {
|
|
2662
|
-
if (!this.hooksInitialized) {
|
|
2663
|
-
this.ensureHooksInitialized();
|
|
2664
|
-
}
|
|
2665
|
-
return this.hookCache.has(name);
|
|
2666
|
-
}
|
|
2667
2870
|
}
|
|
2668
2871
|
class AuthPlugin extends ShokupanRouter {
|
|
2669
2872
|
constructor(authConfig) {
|
|
@@ -2711,7 +2914,9 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2711
2914
|
return jwt;
|
|
2712
2915
|
}
|
|
2713
2916
|
init() {
|
|
2714
|
-
|
|
2917
|
+
const providerEntries = Object.entries(this.authConfig.providers);
|
|
2918
|
+
for (let i = 0; i < providerEntries.length; i++) {
|
|
2919
|
+
const [providerName, providerConfig] = providerEntries[i];
|
|
2715
2920
|
if (!providerConfig) continue;
|
|
2716
2921
|
const provider = this.getProviderInstance(providerName, providerConfig);
|
|
2717
2922
|
if (!provider) {
|
|
@@ -3039,7 +3244,9 @@ function Cors(options = {}) {
|
|
|
3039
3244
|
}
|
|
3040
3245
|
const response = await next();
|
|
3041
3246
|
if (response instanceof Response) {
|
|
3042
|
-
|
|
3247
|
+
const headerEntries = Array.from(headers.entries());
|
|
3248
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
3249
|
+
const [key, value] = headerEntries[i];
|
|
3043
3250
|
response.headers.set(key, value);
|
|
3044
3251
|
}
|
|
3045
3252
|
}
|
|
@@ -3109,6 +3316,8 @@ function useExpress(expressMiddleware) {
|
|
|
3109
3316
|
});
|
|
3110
3317
|
};
|
|
3111
3318
|
}
|
|
3319
|
+
let plainToInstance;
|
|
3320
|
+
let validateOrReject;
|
|
3112
3321
|
class ValidationError extends Error {
|
|
3113
3322
|
constructor(errors) {
|
|
3114
3323
|
super("Validation Error");
|
|
@@ -3173,6 +3382,18 @@ function isClass(schema) {
|
|
|
3173
3382
|
}
|
|
3174
3383
|
}
|
|
3175
3384
|
async function validateClassValidator(schema, data) {
|
|
3385
|
+
if (!plainToInstance || !validateOrReject) {
|
|
3386
|
+
try {
|
|
3387
|
+
const ct = await import("class-transformer");
|
|
3388
|
+
const cv = await import("class-validator");
|
|
3389
|
+
plainToInstance = ct.plainToInstance;
|
|
3390
|
+
validateOrReject = cv.validateOrReject;
|
|
3391
|
+
} catch (e) {
|
|
3392
|
+
throw new Error(
|
|
3393
|
+
"class-transformer and class-validator are required for class-based validation. Install them with: bun add class-transformer class-validator reflect-metadata"
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3176
3397
|
const object = plainToInstance(schema, data);
|
|
3177
3398
|
try {
|
|
3178
3399
|
await validateOrReject(object);
|
|
@@ -3187,30 +3408,8 @@ async function validateClassValidator(schema, data) {
|
|
|
3187
3408
|
}
|
|
3188
3409
|
}
|
|
3189
3410
|
const safelyGetBody = async (ctx) => {
|
|
3190
|
-
const req = ctx.req;
|
|
3191
|
-
if (req._bodyParsed) {
|
|
3192
|
-
return req._bodyValue;
|
|
3193
|
-
}
|
|
3194
3411
|
try {
|
|
3195
|
-
|
|
3196
|
-
if (typeof req.json === "function") {
|
|
3197
|
-
data = await req.json();
|
|
3198
|
-
} else {
|
|
3199
|
-
data = req.body;
|
|
3200
|
-
if (typeof data === "string") {
|
|
3201
|
-
try {
|
|
3202
|
-
data = JSON.parse(data);
|
|
3203
|
-
} catch {
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
|
-
}
|
|
3207
|
-
req._bodyParsed = true;
|
|
3208
|
-
req._bodyValue = data;
|
|
3209
|
-
Object.defineProperty(req, "json", {
|
|
3210
|
-
value: async () => req._bodyValue,
|
|
3211
|
-
configurable: true
|
|
3212
|
-
});
|
|
3213
|
-
return data;
|
|
3412
|
+
return await ctx.body();
|
|
3214
3413
|
} catch (e) {
|
|
3215
3414
|
return {};
|
|
3216
3415
|
}
|
|
@@ -3257,9 +3456,7 @@ function validate(config) {
|
|
|
3257
3456
|
body = await safelyGetBody(ctx);
|
|
3258
3457
|
dataToValidate.body = body;
|
|
3259
3458
|
}
|
|
3260
|
-
|
|
3261
|
-
await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
|
|
3262
|
-
}
|
|
3459
|
+
await ctx.app.runHooks("beforeValidate", ctx, dataToValidate);
|
|
3263
3460
|
if (validators.params) {
|
|
3264
3461
|
ctx.params = await validators.params(ctx.params);
|
|
3265
3462
|
}
|
|
@@ -3275,21 +3472,20 @@ function validate(config) {
|
|
|
3275
3472
|
if (validators.body) {
|
|
3276
3473
|
const b = body ?? await safelyGetBody(ctx);
|
|
3277
3474
|
validBody = await validators.body(b);
|
|
3475
|
+
ctx._cachedBody = validBody;
|
|
3278
3476
|
const req = ctx.req;
|
|
3279
|
-
req._bodyValue = validBody;
|
|
3280
3477
|
Object.defineProperty(req, "json", {
|
|
3281
3478
|
value: async () => validBody,
|
|
3479
|
+
writable: true,
|
|
3282
3480
|
configurable: true
|
|
3283
3481
|
});
|
|
3284
3482
|
ctx.body = validBody;
|
|
3285
3483
|
}
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
|
|
3292
|
-
}
|
|
3484
|
+
const validatedData = { ...dataToValidate };
|
|
3485
|
+
if (config.params) validatedData.params = ctx.params;
|
|
3486
|
+
if (config.query) validatedData.query = validQuery;
|
|
3487
|
+
if (config.body) validatedData.body = validBody;
|
|
3488
|
+
await ctx.app.runHooks("afterValidate", ctx, validatedData);
|
|
3293
3489
|
return next();
|
|
3294
3490
|
};
|
|
3295
3491
|
}
|
|
@@ -3312,12 +3508,14 @@ function openApiValidator() {
|
|
|
3312
3508
|
if (cache.validators.has(ctx.path)) {
|
|
3313
3509
|
matchPath = ctx.path;
|
|
3314
3510
|
} else {
|
|
3315
|
-
|
|
3511
|
+
const pathEntries = Array.from(cache.paths.entries());
|
|
3512
|
+
for (let i = 0; i < pathEntries.length; i++) {
|
|
3513
|
+
const [path, { regex, paramNames }] = pathEntries[i];
|
|
3316
3514
|
const match = regex.exec(ctx.path);
|
|
3317
3515
|
if (match) {
|
|
3318
3516
|
matchPath = path;
|
|
3319
|
-
paramNames.forEach((name,
|
|
3320
|
-
matchParams[name] = match[
|
|
3517
|
+
paramNames.forEach((name, i2) => {
|
|
3518
|
+
matchParams[name] = match[i2 + 1];
|
|
3321
3519
|
});
|
|
3322
3520
|
break;
|
|
3323
3521
|
}
|
|
@@ -3374,7 +3572,9 @@ function openApiValidator() {
|
|
|
3374
3572
|
function compileValidators(spec) {
|
|
3375
3573
|
const validators = /* @__PURE__ */ new Map();
|
|
3376
3574
|
const paths = /* @__PURE__ */ new Map();
|
|
3377
|
-
|
|
3575
|
+
const pathEntries = Object.entries(spec.paths || {});
|
|
3576
|
+
for (let i = 0; i < pathEntries.length; i++) {
|
|
3577
|
+
const [path, pathItem] = pathEntries[i];
|
|
3378
3578
|
if (path.includes("{")) {
|
|
3379
3579
|
const paramNames = [];
|
|
3380
3580
|
const regexStr = "^" + path.replace(/{([^}]+)}/g, (_, name) => {
|
|
@@ -3387,7 +3587,9 @@ function compileValidators(spec) {
|
|
|
3387
3587
|
});
|
|
3388
3588
|
}
|
|
3389
3589
|
const pathValidators = {};
|
|
3390
|
-
|
|
3590
|
+
const methodEntries = Object.entries(pathItem);
|
|
3591
|
+
for (let k = 0; k < methodEntries.length; k++) {
|
|
3592
|
+
const [method, operation] = methodEntries[k];
|
|
3391
3593
|
if (method === "parameters" || method === "summary" || method === "description") continue;
|
|
3392
3594
|
const oper = operation;
|
|
3393
3595
|
const opValidators = {};
|
|
@@ -3401,7 +3603,8 @@ function compileValidators(spec) {
|
|
|
3401
3603
|
const queryRequired = [];
|
|
3402
3604
|
const pathRequired = [];
|
|
3403
3605
|
const headerRequired = [];
|
|
3404
|
-
for (
|
|
3606
|
+
for (let j = 0; j < parameters.length; j++) {
|
|
3607
|
+
const param = parameters[j];
|
|
3405
3608
|
if (param.in === "query") {
|
|
3406
3609
|
queryProps[param.name] = param.schema || {};
|
|
3407
3610
|
if (param.required) queryRequired.push(param.name);
|
|
@@ -3472,8 +3675,7 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
3472
3675
|
|
|
3473
3676
|
<body>
|
|
3474
3677
|
<div id="app"></div>
|
|
3475
|
-
|
|
3476
|
-
<script src="<%= it.path %>scalar.js"><\/script>
|
|
3678
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
|
|
3477
3679
|
<script>
|
|
3478
3680
|
Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
|
|
3479
3681
|
url: "<%= it.path %>openapi.json",
|
|
@@ -3484,9 +3686,6 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
3484
3686
|
|
|
3485
3687
|
</html>`, { path, config: this.pluginOptions }));
|
|
3486
3688
|
});
|
|
3487
|
-
this.get("/scalar.js", (ctx) => {
|
|
3488
|
-
return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
|
|
3489
|
-
});
|
|
3490
3689
|
this.get("/openapi.json", async (ctx) => {
|
|
3491
3690
|
let spec;
|
|
3492
3691
|
if (this.root.openApiSpec) {
|
|
@@ -3566,14 +3765,18 @@ function SecurityHeaders(options = {}) {
|
|
|
3566
3765
|
if (opt === void 0 || opt === true) {
|
|
3567
3766
|
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");
|
|
3568
3767
|
} else if (typeof opt === "object") {
|
|
3569
|
-
|
|
3768
|
+
const optEntries = Object.entries(opt);
|
|
3769
|
+
for (let i = 0; i < optEntries.length; i++) {
|
|
3770
|
+
const [key, val] = optEntries[i];
|
|
3570
3771
|
}
|
|
3571
3772
|
}
|
|
3572
3773
|
}
|
|
3573
3774
|
if (options.hidePoweredBy !== false) ;
|
|
3574
3775
|
const response = await next();
|
|
3575
3776
|
if (response instanceof Response) {
|
|
3576
|
-
|
|
3777
|
+
const headerEntries = Object.entries(headers);
|
|
3778
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
3779
|
+
const [k, v] = headerEntries[i];
|
|
3577
3780
|
response.headers.set(k, v);
|
|
3578
3781
|
}
|
|
3579
3782
|
return response;
|
|
@@ -3659,7 +3862,9 @@ class MemoryStore extends EventEmitter {
|
|
|
3659
3862
|
}
|
|
3660
3863
|
all(cb) {
|
|
3661
3864
|
const result = {};
|
|
3662
|
-
|
|
3865
|
+
const sessionKeys = Object.keys(this.sessions);
|
|
3866
|
+
for (let i = 0; i < sessionKeys.length; i++) {
|
|
3867
|
+
const sid = sessionKeys[i];
|
|
3663
3868
|
try {
|
|
3664
3869
|
result[sid] = JSON.parse(this.sessions[sid]);
|
|
3665
3870
|
} catch {
|
|
@@ -3745,7 +3950,9 @@ function Session(options) {
|
|
|
3745
3950
|
sessObj.regenerate = (cb) => {
|
|
3746
3951
|
store.destroy(sessObj.id, (err) => {
|
|
3747
3952
|
sessionID = generateId(ctx);
|
|
3748
|
-
|
|
3953
|
+
const keys = Object.keys(sessObj);
|
|
3954
|
+
for (let i = 0; i < keys.length; i++) {
|
|
3955
|
+
const key = keys[i];
|
|
3749
3956
|
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
3750
3957
|
delete sessObj[key];
|
|
3751
3958
|
}
|
|
@@ -3760,7 +3967,9 @@ function Session(options) {
|
|
|
3760
3967
|
store.get(sessObj.id, (err, sess2) => {
|
|
3761
3968
|
if (err) return cb(err);
|
|
3762
3969
|
if (!sess2) return cb(new Error("Session not found"));
|
|
3763
|
-
|
|
3970
|
+
const keys = Object.keys(sessObj);
|
|
3971
|
+
for (let i = 0; i < keys.length; i++) {
|
|
3972
|
+
const key = keys[i];
|
|
3764
3973
|
if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
|
|
3765
3974
|
delete sessObj[key];
|
|
3766
3975
|
}
|