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,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Router = void 0;
|
|
4
|
+
const radix_tree_1 = require("./radix-tree");
|
|
5
|
+
const types_1 = require("../core/types");
|
|
6
|
+
class Router {
|
|
7
|
+
constructor() {
|
|
8
|
+
// Per-method route buckets
|
|
9
|
+
this.methodBuckets = new Map([
|
|
10
|
+
['GET', []],
|
|
11
|
+
['POST', []],
|
|
12
|
+
['PUT', []],
|
|
13
|
+
['DELETE', []],
|
|
14
|
+
['PATCH', []],
|
|
15
|
+
['HEAD', []],
|
|
16
|
+
['OPTIONS', []],
|
|
17
|
+
]);
|
|
18
|
+
// Derived structures (built at freeze time)
|
|
19
|
+
this.radixTrees = new Map();
|
|
20
|
+
// Freeze state
|
|
21
|
+
this.isFrozen = false;
|
|
22
|
+
}
|
|
23
|
+
register(method, path, handler, options) {
|
|
24
|
+
if (this.isFrozen) {
|
|
25
|
+
console.warn(`Router is frozen. Late route registration (${method} ${path}) may not be optimized.`);
|
|
26
|
+
}
|
|
27
|
+
const segments = this.normalize(path);
|
|
28
|
+
const bucket = this.methodBuckets.get(method);
|
|
29
|
+
if (bucket) {
|
|
30
|
+
bucket.push({
|
|
31
|
+
path,
|
|
32
|
+
segments,
|
|
33
|
+
handler,
|
|
34
|
+
priority: options?.priority ?? types_1.RoutePriority.STANDARD,
|
|
35
|
+
schema: options?.schema,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
getRoutes() {
|
|
40
|
+
return this.methodBuckets;
|
|
41
|
+
}
|
|
42
|
+
match(method, path) {
|
|
43
|
+
// Fast path for frozen router
|
|
44
|
+
if (this.isFrozen) {
|
|
45
|
+
const tree = this.radixTrees.get(method);
|
|
46
|
+
if (tree) {
|
|
47
|
+
const segments = this.normalize(path);
|
|
48
|
+
const match = tree.lookup(segments);
|
|
49
|
+
if (match) {
|
|
50
|
+
return match;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
// Slow path for unfrozen router (legacy behavior)
|
|
56
|
+
const segments = this.normalize(path);
|
|
57
|
+
const bucket = this.methodBuckets.get(method);
|
|
58
|
+
if (!bucket || bucket.length === 0) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
// Try to match against routes
|
|
62
|
+
for (const route of bucket) {
|
|
63
|
+
if (route.segments.length !== segments.length) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const params = {};
|
|
67
|
+
let matched = true;
|
|
68
|
+
for (let i = 0; i < route.segments.length; i += 1) {
|
|
69
|
+
const pattern = route.segments[i];
|
|
70
|
+
const value = segments[i];
|
|
71
|
+
if (pattern.startsWith(':')) {
|
|
72
|
+
const key = pattern.slice(1);
|
|
73
|
+
params[key] = value;
|
|
74
|
+
}
|
|
75
|
+
else if (pattern !== value) {
|
|
76
|
+
matched = false;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (matched) {
|
|
81
|
+
return {
|
|
82
|
+
handler: route.handler,
|
|
83
|
+
params,
|
|
84
|
+
priority: route.priority,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
getAllowedMethods(path) {
|
|
91
|
+
const segments = this.normalize(path);
|
|
92
|
+
const methods = [];
|
|
93
|
+
for (const [method, routes] of this.methodBuckets.entries()) {
|
|
94
|
+
for (const route of routes) {
|
|
95
|
+
if (route.segments.length !== segments.length) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
let matched = true;
|
|
99
|
+
for (let i = 0; i < route.segments.length; i += 1) {
|
|
100
|
+
const pattern = route.segments[i];
|
|
101
|
+
const value = segments[i];
|
|
102
|
+
if (pattern.startsWith(':')) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (pattern !== value) {
|
|
106
|
+
matched = false;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (matched) {
|
|
111
|
+
methods.push(method);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return methods;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Freeze the router after server starts.
|
|
120
|
+
* Prevents further route registration and builds derived structures for optimized matching.
|
|
121
|
+
*/
|
|
122
|
+
freeze() {
|
|
123
|
+
if (this.isFrozen) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.isFrozen = true;
|
|
127
|
+
// Build derived structures for faster matching
|
|
128
|
+
for (const [method, routes] of this.methodBuckets.entries()) {
|
|
129
|
+
const tree = new radix_tree_1.RadixTree();
|
|
130
|
+
for (const route of routes) {
|
|
131
|
+
tree.insert(route.segments, route.handler, route.priority);
|
|
132
|
+
}
|
|
133
|
+
this.radixTrees.set(method, tree);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
isFrozenRouter() {
|
|
137
|
+
return this.isFrozen;
|
|
138
|
+
}
|
|
139
|
+
normalize(path) {
|
|
140
|
+
if (!path || path === '/') {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
return path.split('/').filter((segment) => segment.length > 0);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.Router = Router;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TestClient = void 0;
|
|
4
|
+
exports.createTestClient = createTestClient;
|
|
5
|
+
class TestClient {
|
|
6
|
+
constructor(app) {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
this.server = null;
|
|
9
|
+
this.baseURL = '';
|
|
10
|
+
this.app = app;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Starts the server on a random port.
|
|
14
|
+
* Automatically called by request methods if not started.
|
|
15
|
+
*/
|
|
16
|
+
async start() {
|
|
17
|
+
if (this.server)
|
|
18
|
+
return;
|
|
19
|
+
const { port } = await this.app.listen(0);
|
|
20
|
+
this.server = this.app.serverInstance;
|
|
21
|
+
this.baseURL = `http://127.0.0.1:${port}`;
|
|
22
|
+
}
|
|
23
|
+
async stop() {
|
|
24
|
+
if (this.server) {
|
|
25
|
+
await this.app.close();
|
|
26
|
+
this.server = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async request(method, path, options = {}) {
|
|
30
|
+
if (!this.server) {
|
|
31
|
+
await this.start();
|
|
32
|
+
}
|
|
33
|
+
const url = new URL(path, this.baseURL);
|
|
34
|
+
if (options.query) {
|
|
35
|
+
Object.entries(options.query).forEach(([k, v]) => {
|
|
36
|
+
url.searchParams.append(k, String(v));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const headers = options.headers || {};
|
|
40
|
+
let bodyPayload;
|
|
41
|
+
if (options.body) {
|
|
42
|
+
if (typeof options.body === 'object' &&
|
|
43
|
+
!(options.body instanceof Uint8Array) &&
|
|
44
|
+
!(options.body instanceof ArrayBuffer) &&
|
|
45
|
+
!(options.body instanceof FormData) &&
|
|
46
|
+
!(options.body instanceof URLSearchParams)) {
|
|
47
|
+
bodyPayload = JSON.stringify(options.body);
|
|
48
|
+
if (!headers['content-type']) {
|
|
49
|
+
headers['content-type'] = 'application/json';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
bodyPayload = options.body;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return fetch(url, {
|
|
57
|
+
method,
|
|
58
|
+
headers,
|
|
59
|
+
body: bodyPayload,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
get(path, options) {
|
|
63
|
+
return this.request('GET', path, options);
|
|
64
|
+
}
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
+
post(path, body, options) {
|
|
67
|
+
return this.request('POST', path, { ...options, body });
|
|
68
|
+
}
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
put(path, body, options) {
|
|
71
|
+
return this.request('PUT', path, { ...options, body });
|
|
72
|
+
}
|
|
73
|
+
delete(path, options) {
|
|
74
|
+
return this.request('DELETE', path, options);
|
|
75
|
+
}
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
patch(path, body, options) {
|
|
78
|
+
return this.request('PATCH', path, { ...options, body });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.TestClient = TestClient;
|
|
82
|
+
function createTestClient(app) {
|
|
83
|
+
return new TestClient(app);
|
|
84
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCookies = parseCookies;
|
|
4
|
+
exports.serializeCookie = serializeCookie;
|
|
5
|
+
function parseCookies(header) {
|
|
6
|
+
const list = {};
|
|
7
|
+
if (!header) {
|
|
8
|
+
return list;
|
|
9
|
+
}
|
|
10
|
+
header.split(';').forEach((cookie) => {
|
|
11
|
+
const parts = cookie.split('=');
|
|
12
|
+
const name = parts.shift()?.trim();
|
|
13
|
+
if (name) {
|
|
14
|
+
const value = parts.join('=');
|
|
15
|
+
list[name] = decodeURIComponent(value);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return list;
|
|
19
|
+
}
|
|
20
|
+
function serializeCookie(name, value, options = {}) {
|
|
21
|
+
let str = `${name}=${encodeURIComponent(value)}`;
|
|
22
|
+
if (options.maxAge) {
|
|
23
|
+
str += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
24
|
+
}
|
|
25
|
+
if (options.domain) {
|
|
26
|
+
str += `; Domain=${options.domain}`;
|
|
27
|
+
}
|
|
28
|
+
if (options.path) {
|
|
29
|
+
str += `; Path=${options.path}`;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
str += '; Path=/';
|
|
33
|
+
}
|
|
34
|
+
if (options.expires) {
|
|
35
|
+
str += `; Expires=${options.expires.toUTCString()}`;
|
|
36
|
+
}
|
|
37
|
+
if (options.httpOnly) {
|
|
38
|
+
str += '; HttpOnly';
|
|
39
|
+
}
|
|
40
|
+
if (options.secure) {
|
|
41
|
+
str += '; Secure';
|
|
42
|
+
}
|
|
43
|
+
if (options.sameSite) {
|
|
44
|
+
switch (options.sameSite) {
|
|
45
|
+
case 'lax':
|
|
46
|
+
str += '; SameSite=Lax';
|
|
47
|
+
break;
|
|
48
|
+
case 'strict':
|
|
49
|
+
str += '; SameSite=Strict';
|
|
50
|
+
break;
|
|
51
|
+
case 'none':
|
|
52
|
+
str += '; SameSite=None';
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return str;
|
|
59
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLoggerMiddleware = createLoggerMiddleware;
|
|
4
|
+
function createLoggerMiddleware(options = {}) {
|
|
5
|
+
const sink = options.sink ??
|
|
6
|
+
((entry) => {
|
|
7
|
+
const status = entry.status;
|
|
8
|
+
let color = '\x1b[37m';
|
|
9
|
+
if (status >= 200 && status < 300) {
|
|
10
|
+
color = '\x1b[32m';
|
|
11
|
+
}
|
|
12
|
+
else if (status === 404) {
|
|
13
|
+
color = '\x1b[33m';
|
|
14
|
+
}
|
|
15
|
+
else if (status >= 500) {
|
|
16
|
+
color = '\x1b[34m';
|
|
17
|
+
}
|
|
18
|
+
else if (status >= 400) {
|
|
19
|
+
color = '\x1b[35m';
|
|
20
|
+
}
|
|
21
|
+
const reset = '\x1b[0m';
|
|
22
|
+
const prefix = entry.requestId ? `${entry.requestId} ` : '';
|
|
23
|
+
const line = `${prefix}${entry.method} ${entry.path} ${status} ${entry.durationMs}ms`;
|
|
24
|
+
console.log(`${color}${line}${reset}`);
|
|
25
|
+
});
|
|
26
|
+
return async (ctx, next) => {
|
|
27
|
+
const start = ctx.requestStart ?? Date.now();
|
|
28
|
+
try {
|
|
29
|
+
await next();
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
const durationMs = Math.max(1, Date.now() - start);
|
|
33
|
+
const method = ctx.req.method || 'GET';
|
|
34
|
+
const path = ctx.url.pathname;
|
|
35
|
+
const status = ctx.res.statusCode || 200;
|
|
36
|
+
sink({
|
|
37
|
+
method,
|
|
38
|
+
path,
|
|
39
|
+
status,
|
|
40
|
+
durationMs,
|
|
41
|
+
requestId: ctx.requestId,
|
|
42
|
+
}, ctx);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachSignalHandlers = attachSignalHandlers;
|
|
4
|
+
function attachSignalHandlers(app, options = {}) {
|
|
5
|
+
const signals = options.signals ?? ['SIGINT', 'SIGTERM'];
|
|
6
|
+
const timeoutMs = options.timeoutMs ?? 10000;
|
|
7
|
+
let shuttingDown = false;
|
|
8
|
+
const handler = async (signal) => {
|
|
9
|
+
if (shuttingDown) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
shuttingDown = true;
|
|
13
|
+
console.log(`Received ${signal}, shutting down...`);
|
|
14
|
+
const timer = setTimeout(() => {
|
|
15
|
+
console.error('Shutdown timed out, forcing exit. (some connections might be lost)');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}, timeoutMs);
|
|
18
|
+
try {
|
|
19
|
+
await app.shutdown();
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error('Error during shutdown:', err);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
for (const signal of signals) {
|
|
29
|
+
process.on(signal, handler);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSSE = createSSE;
|
|
4
|
+
function createSSE(ctx) {
|
|
5
|
+
const res = ctx.res;
|
|
6
|
+
// Disable auto-end so we can keep the connection open
|
|
7
|
+
ctx.disableAutoEnd = true;
|
|
8
|
+
// Only write headers if not already sent
|
|
9
|
+
if (!res.headersSent) {
|
|
10
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
11
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
12
|
+
res.setHeader('Connection', 'keep-alive');
|
|
13
|
+
res.setHeader('X-Accel-Buffering', 'no'); // For Nginx
|
|
14
|
+
// Send initial ping/comment to flush headers and establish connection
|
|
15
|
+
res.write(': connected\n\n');
|
|
16
|
+
}
|
|
17
|
+
const send = (data, event, id) => {
|
|
18
|
+
if (id)
|
|
19
|
+
res.write(`id: ${id}\n`);
|
|
20
|
+
if (event)
|
|
21
|
+
res.write(`event: ${event}\n`);
|
|
22
|
+
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
23
|
+
res.write(`data: ${payload}\n\n`);
|
|
24
|
+
};
|
|
25
|
+
const close = () => {
|
|
26
|
+
res.end();
|
|
27
|
+
};
|
|
28
|
+
ctx.req.on('close', () => {
|
|
29
|
+
// console.log('SSE client disconnected');
|
|
30
|
+
});
|
|
31
|
+
return { send, close };
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./simple"), exports);
|
|
19
|
+
__exportStar(require("./zod"), exports);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SimpleValidator = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
class SimpleValidator {
|
|
6
|
+
validate(schema, data) {
|
|
7
|
+
try {
|
|
8
|
+
// If schema is not a SimpleSchema object, we can't validate it with this validator
|
|
9
|
+
// But for simplicity, we assume the user passes a valid SimpleSchema if they use this validator.
|
|
10
|
+
const validData = this.check(schema, data);
|
|
11
|
+
return { success: true, data: validData };
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
return {
|
|
15
|
+
success: false,
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
error: new types_1.ValidationError(err.message || 'Validation failed')
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
check(schema, data, path = '') {
|
|
23
|
+
if (schema.required !== false && (data === undefined || data === null)) {
|
|
24
|
+
throw new Error(`Field '${path}' is required`);
|
|
25
|
+
}
|
|
26
|
+
if (data === undefined || data === null) {
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
switch (schema.type) {
|
|
30
|
+
case 'string':
|
|
31
|
+
if (typeof data !== 'string')
|
|
32
|
+
throw new Error(`Field '${path}' must be a string`);
|
|
33
|
+
if (schema.min !== undefined && data.length < schema.min)
|
|
34
|
+
throw new Error(`Field '${path}' too short (min ${schema.min})`);
|
|
35
|
+
if (schema.max !== undefined && data.length > schema.max)
|
|
36
|
+
throw new Error(`Field '${path}' too long (max ${schema.max})`);
|
|
37
|
+
if (schema.pattern && !new RegExp(schema.pattern).test(data))
|
|
38
|
+
throw new Error(`Field '${path}' format invalid`);
|
|
39
|
+
if (schema.enum && !schema.enum.includes(data))
|
|
40
|
+
throw new Error(`Field '${path}' must be one of [${schema.enum.join(', ')}]`);
|
|
41
|
+
return data;
|
|
42
|
+
case 'number':
|
|
43
|
+
// Try to coerce if it's a string looking like a number (useful for query params)
|
|
44
|
+
let num = data;
|
|
45
|
+
if (typeof data === 'string' && !isNaN(Number(data))) {
|
|
46
|
+
num = Number(data);
|
|
47
|
+
}
|
|
48
|
+
if (typeof num !== 'number' || isNaN(num))
|
|
49
|
+
throw new Error(`Field '${path}' must be a number`);
|
|
50
|
+
if (schema.min !== undefined && num < schema.min)
|
|
51
|
+
throw new Error(`Field '${path}' too small (min ${schema.min})`);
|
|
52
|
+
if (schema.max !== undefined && num > schema.max)
|
|
53
|
+
throw new Error(`Field '${path}' too large (max ${schema.max})`);
|
|
54
|
+
if (schema.enum && !schema.enum.includes(num))
|
|
55
|
+
throw new Error(`Field '${path}' must be one of [${schema.enum.join(', ')}]`);
|
|
56
|
+
return num;
|
|
57
|
+
case 'boolean':
|
|
58
|
+
if (typeof data === 'boolean')
|
|
59
|
+
return data;
|
|
60
|
+
if (data === 'true')
|
|
61
|
+
return true;
|
|
62
|
+
if (data === 'false')
|
|
63
|
+
return false;
|
|
64
|
+
throw new Error(`Field '${path}' must be a boolean`);
|
|
65
|
+
case 'array':
|
|
66
|
+
if (!Array.isArray(data))
|
|
67
|
+
throw new Error(`Field '${path}' must be an array`);
|
|
68
|
+
if (schema.min !== undefined && data.length < schema.min)
|
|
69
|
+
throw new Error(`Field '${path}' too few items (min ${schema.min})`);
|
|
70
|
+
if (schema.max !== undefined && data.length > schema.max)
|
|
71
|
+
throw new Error(`Field '${path}' too many items (max ${schema.max})`);
|
|
72
|
+
if (schema.items) {
|
|
73
|
+
return data.map((item, i) => this.check(schema.items, item, `${path}[${i}]`));
|
|
74
|
+
}
|
|
75
|
+
return data;
|
|
76
|
+
case 'object':
|
|
77
|
+
if (typeof data !== 'object' || Array.isArray(data))
|
|
78
|
+
throw new Error(`Field '${path}' must be an object`);
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
+
const result = {};
|
|
81
|
+
const props = schema.properties || {};
|
|
82
|
+
// Check known properties
|
|
83
|
+
for (const key in props) {
|
|
84
|
+
const propSchema = props[key];
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
const propValue = data[key];
|
|
87
|
+
result[key] = this.check(propSchema, propValue, path ? `${path}.${key}` : key);
|
|
88
|
+
}
|
|
89
|
+
// Pass through unknown properties?
|
|
90
|
+
// For strictness, maybe we should strip them?
|
|
91
|
+
// Let's keep unknown properties for now to be safe, or make it configurable.
|
|
92
|
+
// For now: Only keep validated properties if properties are defined.
|
|
93
|
+
if (Object.keys(props).length > 0) {
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
return data;
|
|
97
|
+
default:
|
|
98
|
+
return data;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.SimpleValidator = SimpleValidator;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ValidationError = void 0;
|
|
4
|
+
class ValidationError extends Error {
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
constructor(details) {
|
|
7
|
+
super('Validation Error');
|
|
8
|
+
this.details = details;
|
|
9
|
+
this.name = 'ValidationError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.ValidationError = ValidationError;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZodValidator = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
class ZodValidator {
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
async validate(schema, data) {
|
|
8
|
+
try {
|
|
9
|
+
const zodSchema = schema;
|
|
10
|
+
const result = await zodSchema.parseAsync(data);
|
|
11
|
+
return { success: true, data: result };
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return { success: false, error: new types_1.ValidationError(error) };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.ZodValidator = ZodValidator;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const postgres_1 = require("../src/database/adapters/postgres");
|
|
5
|
+
const mongo_1 = require("../src/database/adapters/mongo");
|
|
6
|
+
// Define mocks
|
|
7
|
+
const pgPoolQuery = vitest_1.vi.fn(() => Promise.resolve({ rows: [], command: 'SELECT' }));
|
|
8
|
+
const pgConnect = vitest_1.vi.fn(() => Promise.resolve({ release: vitest_1.vi.fn() }));
|
|
9
|
+
const pgEnd = vitest_1.vi.fn();
|
|
10
|
+
vitest_1.vi.mock('pg', () => {
|
|
11
|
+
const Pool = function () {
|
|
12
|
+
return {
|
|
13
|
+
connect: pgConnect,
|
|
14
|
+
query: pgPoolQuery,
|
|
15
|
+
end: pgEnd,
|
|
16
|
+
on: vitest_1.vi.fn(),
|
|
17
|
+
ended: false
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
default: { Pool },
|
|
22
|
+
Pool
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
const mongoFind = vitest_1.vi.fn(() => ({
|
|
26
|
+
toArray: vitest_1.vi.fn(() => Promise.resolve([])),
|
|
27
|
+
limit: vitest_1.vi.fn(),
|
|
28
|
+
skip: vitest_1.vi.fn()
|
|
29
|
+
}));
|
|
30
|
+
const mongoConnect = vitest_1.vi.fn(() => Promise.resolve());
|
|
31
|
+
const mongoClose = vitest_1.vi.fn();
|
|
32
|
+
vitest_1.vi.mock('mongodb', () => {
|
|
33
|
+
const MongoClient = function () {
|
|
34
|
+
return {
|
|
35
|
+
connect: mongoConnect,
|
|
36
|
+
db: vitest_1.vi.fn(() => ({
|
|
37
|
+
collection: vitest_1.vi.fn(() => ({
|
|
38
|
+
find: mongoFind,
|
|
39
|
+
findOne: vitest_1.vi.fn(),
|
|
40
|
+
insertOne: vitest_1.vi.fn(),
|
|
41
|
+
// add other methods as needed
|
|
42
|
+
updateOne: vitest_1.vi.fn(),
|
|
43
|
+
updateMany: vitest_1.vi.fn(),
|
|
44
|
+
deleteOne: vitest_1.vi.fn(),
|
|
45
|
+
deleteMany: vitest_1.vi.fn(),
|
|
46
|
+
aggregate: vitest_1.vi.fn(() => ({ toArray: vitest_1.vi.fn(() => Promise.resolve([])) }))
|
|
47
|
+
}))
|
|
48
|
+
})),
|
|
49
|
+
close: mongoClose
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
default: { MongoClient },
|
|
54
|
+
MongoClient
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
(0, vitest_1.describe)('Database Adapters', () => {
|
|
58
|
+
(0, vitest_1.describe)('PostgresAdapter', () => {
|
|
59
|
+
let adapter;
|
|
60
|
+
(0, vitest_1.beforeEach)(() => {
|
|
61
|
+
adapter = new postgres_1.PostgresAdapter({ type: 'postgres', database: 'test' });
|
|
62
|
+
vitest_1.vi.clearAllMocks();
|
|
63
|
+
});
|
|
64
|
+
(0, vitest_1.it)('should connect and disconnect', async () => {
|
|
65
|
+
await adapter.connect();
|
|
66
|
+
(0, vitest_1.expect)(pgConnect).toHaveBeenCalled();
|
|
67
|
+
(0, vitest_1.expect)(adapter.isConnected()).toBe(true);
|
|
68
|
+
await adapter.disconnect();
|
|
69
|
+
(0, vitest_1.expect)(pgEnd).toHaveBeenCalled();
|
|
70
|
+
// Since our mock object is static in the factory, we can't easily check 'pool' null state
|
|
71
|
+
// via the mock, but we can check the adapter state if it relies on the pool property.
|
|
72
|
+
// The adapter sets this.pool = null.
|
|
73
|
+
(0, vitest_1.expect)(adapter.isConnected()).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)('should execute query', async () => {
|
|
76
|
+
await adapter.connect();
|
|
77
|
+
await adapter.query('SELECT * FROM users');
|
|
78
|
+
(0, vitest_1.expect)(pgPoolQuery).toHaveBeenCalledWith('SELECT * FROM users', undefined);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.describe)('MongoAdapter', () => {
|
|
82
|
+
let adapter;
|
|
83
|
+
(0, vitest_1.beforeEach)(() => {
|
|
84
|
+
adapter = new mongo_1.MongoAdapter({ type: 'mongo', url: 'mongodb://localhost' });
|
|
85
|
+
vitest_1.vi.clearAllMocks();
|
|
86
|
+
});
|
|
87
|
+
(0, vitest_1.it)('should connect and disconnect', async () => {
|
|
88
|
+
await adapter.connect();
|
|
89
|
+
(0, vitest_1.expect)(mongoConnect).toHaveBeenCalled();
|
|
90
|
+
(0, vitest_1.expect)(adapter.isConnected()).toBe(true);
|
|
91
|
+
await adapter.disconnect();
|
|
92
|
+
(0, vitest_1.expect)(mongoClose).toHaveBeenCalled();
|
|
93
|
+
(0, vitest_1.expect)(adapter.isConnected()).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
(0, vitest_1.it)('should execute find query via object syntax', async () => {
|
|
96
|
+
await adapter.connect();
|
|
97
|
+
await adapter.query({ collection: 'users', action: 'find', filter: { id: 1 } });
|
|
98
|
+
(0, vitest_1.expect)(mongoFind).toHaveBeenCalledWith({ id: 1 });
|
|
99
|
+
});
|
|
100
|
+
(0, vitest_1.it)('should execute find query via string syntax', async () => {
|
|
101
|
+
await adapter.connect();
|
|
102
|
+
await adapter.query('users.find', [{ id: 1 }]);
|
|
103
|
+
(0, vitest_1.expect)(mongoFind).toHaveBeenCalledWith({ id: 1 });
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|