qhttpx 1.8.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/.eslintrc.json +22 -0
- package/.github/workflows/ci.yml +32 -0
- package/.github/workflows/npm-publish.yml +37 -0
- package/.github/workflows/release.yml +21 -0
- package/.prettierrc +7 -0
- package/CHANGELOG.md +145 -0
- package/LICENSE +21 -0
- package/README.md +343 -0
- package/dist/package.json +61 -0
- package/dist/src/benchmarks/compare-frameworks.js +119 -0
- package/dist/src/benchmarks/quantam-users.js +56 -0
- package/dist/src/benchmarks/simple-json.js +58 -0
- package/dist/src/benchmarks/ultra-mode.js +122 -0
- package/dist/src/cli/index.js +200 -0
- package/dist/src/client/index.js +72 -0
- package/dist/src/core/batch.js +97 -0
- package/dist/src/core/body-parser.js +121 -0
- package/dist/src/core/buffer-pool.js +70 -0
- package/dist/src/core/config.js +50 -0
- package/dist/src/core/fusion.js +183 -0
- package/dist/src/core/logger.js +49 -0
- package/dist/src/core/metrics.js +111 -0
- package/dist/src/core/resources.js +25 -0
- package/dist/src/core/scheduler.js +85 -0
- package/dist/src/core/scope.js +68 -0
- package/dist/src/core/serializer.js +44 -0
- package/dist/src/core/server.js +905 -0
- package/dist/src/core/stream.js +71 -0
- package/dist/src/core/tasks.js +87 -0
- package/dist/src/core/types.js +19 -0
- package/dist/src/core/websocket.js +86 -0
- package/dist/src/core/worker-queue.js +73 -0
- package/dist/src/database/adapters/memory.js +90 -0
- package/dist/src/database/adapters/mongo.js +141 -0
- package/dist/src/database/adapters/postgres.js +111 -0
- package/dist/src/database/adapters/sqlite.js +42 -0
- package/dist/src/database/coalescer.js +134 -0
- package/dist/src/database/manager.js +87 -0
- package/dist/src/database/types.js +2 -0
- package/dist/src/index.js +61 -0
- package/dist/src/middleware/compression.js +133 -0
- package/dist/src/middleware/cors.js +66 -0
- package/dist/src/middleware/presets.js +33 -0
- package/dist/src/middleware/rate-limit.js +77 -0
- package/dist/src/middleware/security.js +69 -0
- package/dist/src/middleware/static.js +191 -0
- package/dist/src/openapi/generator.js +149 -0
- package/dist/src/router/radix-router.js +89 -0
- package/dist/src/router/radix-tree.js +81 -0
- package/dist/src/router/router.js +146 -0
- package/dist/src/testing/index.js +84 -0
- package/dist/src/utils/cookies.js +59 -0
- package/dist/src/utils/logger.js +45 -0
- package/dist/src/utils/signals.js +31 -0
- package/dist/src/utils/sse.js +32 -0
- package/dist/src/validation/index.js +19 -0
- package/dist/src/validation/simple.js +102 -0
- package/dist/src/validation/types.js +12 -0
- package/dist/src/validation/zod.js +18 -0
- package/dist/src/views/index.js +17 -0
- package/dist/src/views/types.js +2 -0
- package/dist/tests/adapters.test.js +106 -0
- package/dist/tests/batch.test.js +117 -0
- package/dist/tests/body-parser.test.js +52 -0
- package/dist/tests/compression-sse.test.js +87 -0
- package/dist/tests/cookies.test.js +63 -0
- package/dist/tests/cors.test.js +55 -0
- package/dist/tests/database.test.js +80 -0
- package/dist/tests/dx.test.js +64 -0
- package/dist/tests/ecosystem.test.js +133 -0
- package/dist/tests/features.test.js +47 -0
- package/dist/tests/fusion.test.js +92 -0
- package/dist/tests/http-basic.test.js +124 -0
- package/dist/tests/logger.test.js +33 -0
- package/dist/tests/middleware.test.js +109 -0
- package/dist/tests/observability.test.js +59 -0
- package/dist/tests/openapi.test.js +64 -0
- package/dist/tests/plugin.test.js +65 -0
- package/dist/tests/plugins.test.js +71 -0
- package/dist/tests/rate-limit.test.js +77 -0
- package/dist/tests/resources.test.js +44 -0
- package/dist/tests/scheduler.test.js +46 -0
- package/dist/tests/schema-routes.test.js +77 -0
- package/dist/tests/security.test.js +83 -0
- package/dist/tests/server-db.test.js +72 -0
- package/dist/tests/smoke.test.js +10 -0
- package/dist/tests/sqlite-fusion.test.js +92 -0
- package/dist/tests/static.test.js +102 -0
- package/dist/tests/stream.test.js +44 -0
- package/dist/tests/task-metrics.test.js +53 -0
- package/dist/tests/tasks.test.js +62 -0
- package/dist/tests/testing.test.js +47 -0
- package/dist/tests/validation.test.js +107 -0
- package/dist/tests/websocket.test.js +146 -0
- package/dist/vitest.config.js +9 -0
- package/docs/AEGIS.md +76 -0
- package/docs/BENCHMARKS.md +36 -0
- package/docs/CAPABILITIES.md +70 -0
- package/docs/CLI.md +43 -0
- package/docs/DATABASE.md +142 -0
- package/docs/ECOSYSTEM.md +146 -0
- package/docs/NEXT_STEPS.md +99 -0
- package/docs/OPENAPI.md +99 -0
- package/docs/PLUGINS.md +59 -0
- package/docs/REAL_WORLD_EXAMPLES.md +109 -0
- package/docs/ROADMAP.md +366 -0
- package/docs/VALIDATION.md +136 -0
- package/eslint.config.cjs +26 -0
- package/examples/api-server.ts +254 -0
- package/package.json +61 -0
- package/src/benchmarks/compare-frameworks.ts +149 -0
- package/src/benchmarks/quantam-users.ts +70 -0
- package/src/benchmarks/simple-json.ts +71 -0
- package/src/benchmarks/ultra-mode.ts +159 -0
- package/src/cli/index.ts +214 -0
- package/src/client/index.ts +93 -0
- package/src/core/batch.ts +110 -0
- package/src/core/body-parser.ts +151 -0
- package/src/core/buffer-pool.ts +96 -0
- package/src/core/config.ts +60 -0
- package/src/core/fusion.ts +210 -0
- package/src/core/logger.ts +70 -0
- package/src/core/metrics.ts +166 -0
- package/src/core/resources.ts +38 -0
- package/src/core/scheduler.ts +126 -0
- package/src/core/scope.ts +87 -0
- package/src/core/serializer.ts +41 -0
- package/src/core/server.ts +1113 -0
- package/src/core/stream.ts +111 -0
- package/src/core/tasks.ts +138 -0
- package/src/core/types.ts +178 -0
- package/src/core/websocket.ts +112 -0
- package/src/core/worker-queue.ts +90 -0
- package/src/database/adapters/memory.ts +99 -0
- package/src/database/adapters/mongo.ts +116 -0
- package/src/database/adapters/postgres.ts +86 -0
- package/src/database/adapters/sqlite.ts +44 -0
- package/src/database/coalescer.ts +153 -0
- package/src/database/manager.ts +97 -0
- package/src/database/types.ts +24 -0
- package/src/index.ts +42 -0
- package/src/middleware/compression.ts +147 -0
- package/src/middleware/cors.ts +98 -0
- package/src/middleware/presets.ts +50 -0
- package/src/middleware/rate-limit.ts +106 -0
- package/src/middleware/security.ts +109 -0
- package/src/middleware/static.ts +216 -0
- package/src/openapi/generator.ts +167 -0
- package/src/router/radix-router.ts +119 -0
- package/src/router/radix-tree.ts +106 -0
- package/src/router/router.ts +190 -0
- package/src/testing/index.ts +104 -0
- package/src/utils/cookies.ts +67 -0
- package/src/utils/logger.ts +59 -0
- package/src/utils/signals.ts +45 -0
- package/src/utils/sse.ts +41 -0
- package/src/validation/index.ts +3 -0
- package/src/validation/simple.ts +93 -0
- package/src/validation/types.ts +38 -0
- package/src/validation/zod.ts +14 -0
- package/src/views/index.ts +1 -0
- package/src/views/types.ts +4 -0
- package/tests/adapters.test.ts +120 -0
- package/tests/batch.test.ts +139 -0
- package/tests/body-parser.test.ts +83 -0
- package/tests/compression-sse.test.ts +98 -0
- package/tests/cookies.test.ts +74 -0
- package/tests/cors.test.ts +79 -0
- package/tests/database.test.ts +90 -0
- package/tests/dx.test.ts +78 -0
- package/tests/ecosystem.test.ts +156 -0
- package/tests/features.test.ts +51 -0
- package/tests/fusion.test.ts +121 -0
- package/tests/http-basic.test.ts +161 -0
- package/tests/logger.test.ts +48 -0
- package/tests/middleware.test.ts +137 -0
- package/tests/observability.test.ts +91 -0
- package/tests/openapi.test.ts +74 -0
- package/tests/plugin.test.ts +85 -0
- package/tests/plugins.test.ts +93 -0
- package/tests/rate-limit.test.ts +97 -0
- package/tests/resources.test.ts +64 -0
- package/tests/scheduler.test.ts +71 -0
- package/tests/schema-routes.test.ts +89 -0
- package/tests/security.test.ts +128 -0
- package/tests/server-db.test.ts +72 -0
- package/tests/smoke.test.ts +9 -0
- package/tests/sqlite-fusion.test.ts +106 -0
- package/tests/static.test.ts +111 -0
- package/tests/stream.test.ts +58 -0
- package/tests/task-metrics.test.ts +78 -0
- package/tests/tasks.test.ts +90 -0
- package/tests/testing.test.ts +53 -0
- package/tests/validation.test.ts +126 -0
- package/tests/websocket.test.ts +132 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSecurityHeadersMiddleware = createSecurityHeadersMiddleware;
|
|
4
|
+
exports.createSecureDefaults = createSecureDefaults;
|
|
5
|
+
exports.createRateLimitMiddleware = createRateLimitMiddleware;
|
|
6
|
+
const cors_1 = require("./cors");
|
|
7
|
+
function createSecurityHeadersMiddleware(options = {}) {
|
|
8
|
+
const { contentSecurityPolicy = "default-src 'self'", referrerPolicy = 'no-referrer', xFrameOptions = 'SAMEORIGIN', xContentTypeOptions = 'nosniff', xXssProtection = '1; mode=block', strictTransportSecurity, } = options;
|
|
9
|
+
return async (ctx, next) => {
|
|
10
|
+
if (contentSecurityPolicy) {
|
|
11
|
+
ctx.res.setHeader('content-security-policy', contentSecurityPolicy);
|
|
12
|
+
}
|
|
13
|
+
if (referrerPolicy) {
|
|
14
|
+
ctx.res.setHeader('referrer-policy', referrerPolicy);
|
|
15
|
+
}
|
|
16
|
+
if (xFrameOptions) {
|
|
17
|
+
ctx.res.setHeader('x-frame-options', xFrameOptions);
|
|
18
|
+
}
|
|
19
|
+
if (xContentTypeOptions) {
|
|
20
|
+
ctx.res.setHeader('x-content-type-options', xContentTypeOptions);
|
|
21
|
+
}
|
|
22
|
+
if (xXssProtection) {
|
|
23
|
+
ctx.res.setHeader('x-xss-protection', xXssProtection);
|
|
24
|
+
}
|
|
25
|
+
if (strictTransportSecurity) {
|
|
26
|
+
ctx.res.setHeader('strict-transport-security', strictTransportSecurity);
|
|
27
|
+
}
|
|
28
|
+
await next();
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function createSecureDefaults(options = {}) {
|
|
32
|
+
const middlewares = [];
|
|
33
|
+
middlewares.push((0, cors_1.createCorsMiddleware)(options.cors));
|
|
34
|
+
middlewares.push(createSecurityHeadersMiddleware(options.securityHeaders));
|
|
35
|
+
return middlewares;
|
|
36
|
+
}
|
|
37
|
+
function createRateLimitMiddleware(options) {
|
|
38
|
+
const maxRequests = options.maxRequests;
|
|
39
|
+
const windowMs = options.windowMs;
|
|
40
|
+
const keyGenerator = options.keyGenerator ??
|
|
41
|
+
((ctx) => {
|
|
42
|
+
const addr = ctx.req.socket.remoteAddress;
|
|
43
|
+
return addr || 'anonymous';
|
|
44
|
+
});
|
|
45
|
+
const buckets = new Map();
|
|
46
|
+
return async (ctx, next) => {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const key = keyGenerator(ctx);
|
|
49
|
+
const existing = buckets.get(key);
|
|
50
|
+
let bucket = existing;
|
|
51
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
52
|
+
bucket = {
|
|
53
|
+
count: 0,
|
|
54
|
+
resetAt: now + windowMs,
|
|
55
|
+
};
|
|
56
|
+
buckets.set(key, bucket);
|
|
57
|
+
}
|
|
58
|
+
bucket.count += 1;
|
|
59
|
+
if (bucket.count > maxRequests) {
|
|
60
|
+
if (!ctx.res.headersSent) {
|
|
61
|
+
ctx.res.statusCode = 429;
|
|
62
|
+
ctx.res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
63
|
+
}
|
|
64
|
+
ctx.res.end('Too Many Requests');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await next();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createStaticMiddleware = createStaticMiddleware;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function guessContentType(filePath) {
|
|
10
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
11
|
+
const types = {
|
|
12
|
+
'.html': 'text/html; charset=utf-8',
|
|
13
|
+
'.htm': 'text/html; charset=utf-8',
|
|
14
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
15
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
16
|
+
'.css': 'text/css; charset=utf-8',
|
|
17
|
+
'.json': 'application/json; charset=utf-8',
|
|
18
|
+
'.png': 'image/png',
|
|
19
|
+
'.jpg': 'image/jpeg',
|
|
20
|
+
'.jpeg': 'image/jpeg',
|
|
21
|
+
'.gif': 'image/gif',
|
|
22
|
+
'.svg': 'image/svg+xml',
|
|
23
|
+
'.ico': 'image/x-icon',
|
|
24
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
25
|
+
'.mp4': 'video/mp4',
|
|
26
|
+
'.webm': 'video/webm',
|
|
27
|
+
'.mp3': 'audio/mpeg',
|
|
28
|
+
'.wav': 'audio/wav',
|
|
29
|
+
'.pdf': 'application/pdf',
|
|
30
|
+
'.zip': 'application/zip',
|
|
31
|
+
};
|
|
32
|
+
return types[ext] || 'application/octet-stream';
|
|
33
|
+
}
|
|
34
|
+
function generateETag(stat) {
|
|
35
|
+
const mtime = stat.mtimeMs.toString(16);
|
|
36
|
+
const size = stat.size.toString(16);
|
|
37
|
+
return `W/"${size}-${mtime}"`;
|
|
38
|
+
}
|
|
39
|
+
function createStaticMiddleware(options) {
|
|
40
|
+
const root = path_1.default.resolve(options.root);
|
|
41
|
+
const indexFile = options.index ?? 'index.html';
|
|
42
|
+
const fallthrough = options.fallthrough ?? false;
|
|
43
|
+
const etag = options.etag ?? true;
|
|
44
|
+
const lastModified = options.lastModified ?? true;
|
|
45
|
+
const maxAge = options.maxAge ?? 0;
|
|
46
|
+
const immutable = options.immutable ?? false;
|
|
47
|
+
return async (ctx, next) => {
|
|
48
|
+
const method = ctx.req.method || 'GET';
|
|
49
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
50
|
+
if (fallthrough) {
|
|
51
|
+
await next();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
ctx.res.statusCode = 405;
|
|
55
|
+
ctx.res.setHeader('Allow', 'GET, HEAD');
|
|
56
|
+
ctx.res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
57
|
+
ctx.res.end('Method Not Allowed');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let requestPath = ctx.url.pathname || '/';
|
|
61
|
+
// Prevent directory traversal
|
|
62
|
+
requestPath = requestPath.replace(/^(\.\.(\/|\\|$))+/, '');
|
|
63
|
+
// Normalize path
|
|
64
|
+
let safePath = path_1.default.normalize(requestPath);
|
|
65
|
+
if (safePath.startsWith('..'))
|
|
66
|
+
safePath = '/'; // Extra safety
|
|
67
|
+
// Handle root/directory requests
|
|
68
|
+
let filePath = path_1.default.join(root, safePath);
|
|
69
|
+
let stat;
|
|
70
|
+
try {
|
|
71
|
+
stat = await fs_1.default.promises.stat(filePath);
|
|
72
|
+
if (stat.isDirectory()) {
|
|
73
|
+
filePath = path_1.default.join(filePath, indexFile);
|
|
74
|
+
stat = await fs_1.default.promises.stat(filePath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
if (fallthrough) {
|
|
79
|
+
await next();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
ctx.res.statusCode = 404;
|
|
83
|
+
ctx.res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
84
|
+
ctx.res.end('Not Found');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!stat.isFile()) {
|
|
88
|
+
if (fallthrough) {
|
|
89
|
+
await next();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
ctx.res.statusCode = 404;
|
|
93
|
+
ctx.res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
94
|
+
ctx.res.end('Not Found');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Caching Headers
|
|
98
|
+
if (maxAge > 0 || immutable) {
|
|
99
|
+
let cacheControl = `public, max-age=${maxAge}`;
|
|
100
|
+
if (immutable)
|
|
101
|
+
cacheControl += ', immutable';
|
|
102
|
+
ctx.res.setHeader('Cache-Control', cacheControl);
|
|
103
|
+
}
|
|
104
|
+
if (lastModified) {
|
|
105
|
+
ctx.res.setHeader('Last-Modified', stat.mtime.toUTCString());
|
|
106
|
+
}
|
|
107
|
+
if (etag) {
|
|
108
|
+
const tag = generateETag(stat);
|
|
109
|
+
ctx.res.setHeader('ETag', tag);
|
|
110
|
+
// ETag Freshness Check
|
|
111
|
+
const ifNoneMatch = ctx.req.headers['if-none-match'];
|
|
112
|
+
if (ifNoneMatch === tag) {
|
|
113
|
+
ctx.res.statusCode = 304;
|
|
114
|
+
ctx.res.end();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Last-Modified Freshness Check
|
|
119
|
+
const ifModifiedSince = ctx.req.headers['if-modified-since'];
|
|
120
|
+
if (ifModifiedSince) {
|
|
121
|
+
const since = new Date(ifModifiedSince).getTime();
|
|
122
|
+
// Round down mtime to seconds for comparison
|
|
123
|
+
const mtime = Math.floor(stat.mtimeMs / 1000) * 1000;
|
|
124
|
+
if (mtime <= since) {
|
|
125
|
+
ctx.res.statusCode = 304;
|
|
126
|
+
ctx.res.end();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const contentType = guessContentType(filePath);
|
|
131
|
+
ctx.res.setHeader('Content-Type', contentType);
|
|
132
|
+
ctx.res.setHeader('Accept-Ranges', 'bytes');
|
|
133
|
+
// Range Support
|
|
134
|
+
const range = ctx.req.headers['range'];
|
|
135
|
+
let start = 0;
|
|
136
|
+
let end = stat.size - 1;
|
|
137
|
+
// let isRange = false;
|
|
138
|
+
if (range) {
|
|
139
|
+
const parts = range.replace(/bytes=/, '').split('-');
|
|
140
|
+
const partialStart = parts[0];
|
|
141
|
+
const partialEnd = parts[1];
|
|
142
|
+
const parsedStart = parseInt(partialStart, 10);
|
|
143
|
+
const parsedEnd = partialEnd ? parseInt(partialEnd, 10) : end;
|
|
144
|
+
if (!isNaN(parsedStart) && !isNaN(parsedEnd) && parsedStart <= parsedEnd && parsedEnd < stat.size) {
|
|
145
|
+
start = parsedStart;
|
|
146
|
+
end = parsedEnd;
|
|
147
|
+
// isRange = true;
|
|
148
|
+
ctx.res.statusCode = 206;
|
|
149
|
+
ctx.res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
|
|
150
|
+
ctx.res.setHeader('Content-Length', end - start + 1);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Invalid range
|
|
154
|
+
ctx.res.statusCode = 416;
|
|
155
|
+
ctx.res.setHeader('Content-Range', `bytes */${stat.size}`);
|
|
156
|
+
ctx.res.end();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
ctx.res.statusCode = 200;
|
|
162
|
+
ctx.res.setHeader('Content-Length', stat.size);
|
|
163
|
+
}
|
|
164
|
+
if (method === 'HEAD') {
|
|
165
|
+
ctx.res.end();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const stream = fs_1.default.createReadStream(filePath, { start, end });
|
|
169
|
+
// Disable auto-end if we are streaming?
|
|
170
|
+
// Actually, we can just pipe. But we need to make sure the server doesn't call res.end()
|
|
171
|
+
// The middleware architecture awaits next(), then server calls end().
|
|
172
|
+
// If we handle the response here, we should probably set a flag or just return without calling next() (which we do).
|
|
173
|
+
// But the server might still try to end it if we don't tell it we are done.
|
|
174
|
+
// In QHTTPX, if a handler returns, server checks `res.writableEnded`.
|
|
175
|
+
// Pipe calls end() by default.
|
|
176
|
+
// We need to wait for the stream to finish before returning from middleware
|
|
177
|
+
// so that the server doesn't race.
|
|
178
|
+
await new Promise((resolve, reject) => {
|
|
179
|
+
stream.pipe(ctx.res);
|
|
180
|
+
stream.on('end', resolve);
|
|
181
|
+
stream.on('error', (err) => {
|
|
182
|
+
stream.destroy();
|
|
183
|
+
reject(err);
|
|
184
|
+
});
|
|
185
|
+
ctx.res.on('close', () => {
|
|
186
|
+
stream.destroy();
|
|
187
|
+
resolve();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenAPIGenerator = void 0;
|
|
4
|
+
class OpenAPIGenerator {
|
|
5
|
+
constructor(router, options) {
|
|
6
|
+
this.router = router;
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
generate() {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
const paths = {};
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
const schemas = {};
|
|
14
|
+
const methodMap = this.router.getRoutes();
|
|
15
|
+
for (const [method, routes] of methodMap.entries()) {
|
|
16
|
+
for (const route of routes) {
|
|
17
|
+
// Convert /users/:id to /users/{id}
|
|
18
|
+
const pathKey = route.path.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
|
|
19
|
+
if (!paths[pathKey]) {
|
|
20
|
+
paths[pathKey] = {};
|
|
21
|
+
}
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
const operation = {
|
|
24
|
+
responses: {}
|
|
25
|
+
};
|
|
26
|
+
const schema = route.schema;
|
|
27
|
+
if (schema) {
|
|
28
|
+
// Parameters (Query & Path)
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
const parameters = [];
|
|
31
|
+
// Path Params
|
|
32
|
+
// We can infer path params from the URL segments
|
|
33
|
+
const pathParams = route.path.match(/:([a-zA-Z0-9_]+)/g);
|
|
34
|
+
if (pathParams) {
|
|
35
|
+
pathParams.forEach(p => {
|
|
36
|
+
const name = p.substring(1);
|
|
37
|
+
// Check if we have specific schema for this param
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
const paramSchema = schema.params?.properties?.[name];
|
|
40
|
+
parameters.push({
|
|
41
|
+
name,
|
|
42
|
+
in: 'path',
|
|
43
|
+
required: true,
|
|
44
|
+
schema: paramSchema ? this.convertSchema(paramSchema) : { type: 'string' }
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// Query Params
|
|
49
|
+
if (schema.query) {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
const queryProps = schema.query.properties || {};
|
|
52
|
+
for (const [key, prop] of Object.entries(queryProps)) {
|
|
53
|
+
parameters.push({
|
|
54
|
+
name: key,
|
|
55
|
+
in: 'query',
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
required: schema.query.required !== false && prop.required !== false, // simplified
|
|
58
|
+
schema: this.convertSchema(prop)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (parameters.length > 0) {
|
|
63
|
+
operation.parameters = parameters;
|
|
64
|
+
}
|
|
65
|
+
// Request Body
|
|
66
|
+
if (schema.body) {
|
|
67
|
+
operation.requestBody = {
|
|
68
|
+
content: {
|
|
69
|
+
'application/json': {
|
|
70
|
+
schema: this.convertSchema(schema.body)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Responses
|
|
76
|
+
// For now, default to 200 OK
|
|
77
|
+
operation.responses['200'] = {
|
|
78
|
+
description: 'Successful response',
|
|
79
|
+
content: {
|
|
80
|
+
'application/json': {
|
|
81
|
+
schema: schema.response ? this.convertSchema(schema.response) : {}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
// 400 Bad Request if validation exists
|
|
86
|
+
if (schema.body || schema.query || schema.params) {
|
|
87
|
+
operation.responses['400'] = {
|
|
88
|
+
description: 'Validation Error'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// No schema, generic response
|
|
94
|
+
operation.responses['200'] = {
|
|
95
|
+
description: 'Successful response'
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
paths[pathKey][method.toLowerCase()] = operation;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
openapi: '3.0.0',
|
|
103
|
+
info: this.options.info,
|
|
104
|
+
servers: this.options.servers,
|
|
105
|
+
paths,
|
|
106
|
+
components: {
|
|
107
|
+
schemas
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
|
+
convertSchema(schema) {
|
|
113
|
+
if (!schema)
|
|
114
|
+
return {};
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
const res = { type: schema.type };
|
|
117
|
+
if (schema.properties) {
|
|
118
|
+
res.properties = {};
|
|
119
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
120
|
+
res.properties[key] = this.convertSchema(prop);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (schema.items) {
|
|
124
|
+
res.items = this.convertSchema(schema.items);
|
|
125
|
+
}
|
|
126
|
+
if (schema.min !== undefined) {
|
|
127
|
+
if (schema.type === 'string')
|
|
128
|
+
res.minLength = schema.min;
|
|
129
|
+
if (schema.type === 'number')
|
|
130
|
+
res.minimum = schema.min;
|
|
131
|
+
if (schema.type === 'array')
|
|
132
|
+
res.minItems = schema.min;
|
|
133
|
+
}
|
|
134
|
+
if (schema.max !== undefined) {
|
|
135
|
+
if (schema.type === 'string')
|
|
136
|
+
res.maxLength = schema.max;
|
|
137
|
+
if (schema.type === 'number')
|
|
138
|
+
res.maximum = schema.max;
|
|
139
|
+
if (schema.type === 'array')
|
|
140
|
+
res.maxItems = schema.max;
|
|
141
|
+
}
|
|
142
|
+
if (schema.pattern)
|
|
143
|
+
res.pattern = schema.pattern;
|
|
144
|
+
if (schema.enum)
|
|
145
|
+
res.enum = schema.enum;
|
|
146
|
+
return res;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.OpenAPIGenerator = OpenAPIGenerator;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Radix tree router compiled into a flat array structure for zero-allocation matching.
|
|
4
|
+
* Built once at server startup (frozen).
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.RadixRouter = void 0;
|
|
8
|
+
class RadixRouter {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.roots = new Map();
|
|
11
|
+
this.isFrozen = false;
|
|
12
|
+
}
|
|
13
|
+
register(method, path, handler) {
|
|
14
|
+
if (this.isFrozen) {
|
|
15
|
+
console.warn(`Radix router is frozen. Late route registration (${method} ${path}) may not be optimized.`);
|
|
16
|
+
}
|
|
17
|
+
if (!this.roots.has(method)) {
|
|
18
|
+
this.roots.set(method, {});
|
|
19
|
+
}
|
|
20
|
+
const root = this.roots.get(method);
|
|
21
|
+
const segments = this.normalize(path);
|
|
22
|
+
let node = root;
|
|
23
|
+
for (const segment of segments) {
|
|
24
|
+
if (!node.children) {
|
|
25
|
+
node.children = new Map();
|
|
26
|
+
}
|
|
27
|
+
const isParam = segment.startsWith(':');
|
|
28
|
+
const key = isParam ? segment.slice(1) : segment;
|
|
29
|
+
if (!node.children.has(key)) {
|
|
30
|
+
node.children.set(key, {
|
|
31
|
+
paramKey: isParam ? key : undefined,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
node = node.children.get(key);
|
|
35
|
+
}
|
|
36
|
+
node.handler = handler;
|
|
37
|
+
}
|
|
38
|
+
match(method, path) {
|
|
39
|
+
const root = this.roots.get(method);
|
|
40
|
+
if (!root) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const segments = this.normalize(path);
|
|
44
|
+
const params = {};
|
|
45
|
+
let node = root;
|
|
46
|
+
for (const segment of segments) {
|
|
47
|
+
if (!node.children) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
// Try exact match first
|
|
51
|
+
let child = node.children.get(segment);
|
|
52
|
+
// If no exact match, try param match
|
|
53
|
+
if (!child) {
|
|
54
|
+
for (const [key, candidate] of node.children.entries()) {
|
|
55
|
+
if (candidate.paramKey && typeof key === 'string') {
|
|
56
|
+
params[key] = segment;
|
|
57
|
+
child = candidate;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!child) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
node = child;
|
|
66
|
+
}
|
|
67
|
+
if (!node.handler) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return { handler: node.handler, params };
|
|
71
|
+
}
|
|
72
|
+
freeze() {
|
|
73
|
+
if (this.isFrozen) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.isFrozen = true;
|
|
77
|
+
// Future: compile to flat array structure here
|
|
78
|
+
}
|
|
79
|
+
isFrozenRouter() {
|
|
80
|
+
return this.isFrozen;
|
|
81
|
+
}
|
|
82
|
+
normalize(path) {
|
|
83
|
+
if (!path || path === '/') {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
return path.split('/').filter((segment) => segment.length > 0);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.RadixRouter = RadixRouter;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RadixTree = void 0;
|
|
4
|
+
class Node {
|
|
5
|
+
constructor() {
|
|
6
|
+
// Map segment -> Node
|
|
7
|
+
this.children = new Map();
|
|
8
|
+
// Parameter child node (only one allowed per level for simplicity)
|
|
9
|
+
this.paramChild = null;
|
|
10
|
+
this.paramName = null;
|
|
11
|
+
// Data if this node is a route end
|
|
12
|
+
this.data = null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
class RadixTree {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.root = new Node();
|
|
18
|
+
}
|
|
19
|
+
insert(segments, handler, priority) {
|
|
20
|
+
let node = this.root;
|
|
21
|
+
for (const segment of segments) {
|
|
22
|
+
if (segment.startsWith(':')) {
|
|
23
|
+
const paramName = segment.slice(1);
|
|
24
|
+
if (!node.paramChild) {
|
|
25
|
+
node.paramChild = new Node();
|
|
26
|
+
node.paramChild.paramName = paramName;
|
|
27
|
+
}
|
|
28
|
+
else if (node.paramChild.paramName !== paramName) {
|
|
29
|
+
throw new Error(`Cannot have two different parameter names at the same level: "${node.paramChild.paramName}" and "${paramName}"`);
|
|
30
|
+
}
|
|
31
|
+
node = node.paramChild;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (!node.children.has(segment)) {
|
|
35
|
+
node.children.set(segment, new Node());
|
|
36
|
+
}
|
|
37
|
+
node = node.children.get(segment);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
node.data = { handler, priority };
|
|
41
|
+
}
|
|
42
|
+
lookup(segments) {
|
|
43
|
+
// Use a stack-based approach or recursion.
|
|
44
|
+
// For simplicity and correctness with backtracking, we'll use recursion.
|
|
45
|
+
// To minimize allocations, we pass the same params object and only copy on success?
|
|
46
|
+
// Actually, creating a params object is inevitable for the result.
|
|
47
|
+
return this.find(this.root, segments, 0);
|
|
48
|
+
}
|
|
49
|
+
find(node, segments, index) {
|
|
50
|
+
if (index === segments.length) {
|
|
51
|
+
if (node.data) {
|
|
52
|
+
return {
|
|
53
|
+
handler: node.data.handler,
|
|
54
|
+
priority: node.data.priority,
|
|
55
|
+
params: {},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const segment = segments[index];
|
|
61
|
+
// 1. Try exact match
|
|
62
|
+
const child = node.children.get(segment);
|
|
63
|
+
if (child) {
|
|
64
|
+
const result = this.find(child, segments, index + 1);
|
|
65
|
+
if (result) {
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// 2. Try param match
|
|
70
|
+
if (node.paramChild) {
|
|
71
|
+
const result = this.find(node.paramChild, segments, index + 1);
|
|
72
|
+
if (result) {
|
|
73
|
+
// If match found down this path, add current param to it
|
|
74
|
+
result.params[node.paramChild.paramName] = segment;
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.RadixTree = RadixTree;
|