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,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const server_1 = require("../src/core/server");
|
|
5
|
+
const manager_1 = require("../src/database/manager");
|
|
6
|
+
const memory_1 = require("../src/database/adapters/memory");
|
|
7
|
+
(0, vitest_1.describe)('Batch Engine & Query Fusion', () => {
|
|
8
|
+
let app;
|
|
9
|
+
let dbManager;
|
|
10
|
+
(0, vitest_1.beforeEach)(async () => {
|
|
11
|
+
manager_1.DatabaseManager.registerAdapter('memory', memory_1.MemoryAdapter);
|
|
12
|
+
dbManager = new manager_1.DatabaseManager({
|
|
13
|
+
default: 'main',
|
|
14
|
+
connections: {
|
|
15
|
+
main: { type: 'memory', database: 'test_db' }
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
await dbManager.connect();
|
|
19
|
+
app = new server_1.QHTTPX({
|
|
20
|
+
database: dbManager,
|
|
21
|
+
enableBatching: true
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
(0, vitest_1.afterEach)(async () => {
|
|
25
|
+
await app.close();
|
|
26
|
+
await dbManager.disconnect();
|
|
27
|
+
});
|
|
28
|
+
(0, vitest_1.it)('should execute multiple operations in a single batch request', async () => {
|
|
29
|
+
// Define ops
|
|
30
|
+
app.op('math.add', async (params) => {
|
|
31
|
+
return params.a + params.b;
|
|
32
|
+
});
|
|
33
|
+
app.op('math.multiply', async (params) => {
|
|
34
|
+
return params.a * params.b;
|
|
35
|
+
});
|
|
36
|
+
const { port } = await app.listen(0);
|
|
37
|
+
const batch = {
|
|
38
|
+
batch: [
|
|
39
|
+
{ op: 'math.add', params: { a: 5, b: 3 }, id: 1 },
|
|
40
|
+
{ op: 'math.multiply', params: { a: 4, b: 2 }, id: 2 }
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
const response = await fetch(`http://localhost:${port}/qhttpx`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
body: JSON.stringify(batch)
|
|
47
|
+
});
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
(0, vitest_1.expect)(data.results).toHaveLength(2);
|
|
50
|
+
(0, vitest_1.expect)(data.results[0]).toEqual({ result: 8, id: 1 });
|
|
51
|
+
(0, vitest_1.expect)(data.results[1]).toEqual({ result: 8, id: 2 });
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.it)('should fuse database queries', async () => {
|
|
54
|
+
// Setup spy on the adapter's query method to verify fusion
|
|
55
|
+
const adapter = dbManager.get('main');
|
|
56
|
+
const querySpy = vitest_1.vi.spyOn(adapter, 'query');
|
|
57
|
+
// Define an op that uses DB
|
|
58
|
+
app.op('getUser', async (params, ctx) => {
|
|
59
|
+
// Simulate simple query that matches our coalescer pattern
|
|
60
|
+
// "SELECT * FROM users WHERE id = ?"
|
|
61
|
+
if (!ctx.db)
|
|
62
|
+
throw new Error('No DB');
|
|
63
|
+
const result = await ctx.db.get().query('SELECT * FROM users WHERE id = ?', [params.id]);
|
|
64
|
+
return result;
|
|
65
|
+
});
|
|
66
|
+
const { port } = await app.listen(0);
|
|
67
|
+
// Prepare batch with 3 requests
|
|
68
|
+
const batch = {
|
|
69
|
+
batch: [
|
|
70
|
+
{ op: 'getUser', params: { id: 1 }, id: 'req1' },
|
|
71
|
+
{ op: 'getUser', params: { id: 2 }, id: 'req2' },
|
|
72
|
+
{ op: 'getUser', params: { id: 3 }, id: 'req3' }
|
|
73
|
+
]
|
|
74
|
+
};
|
|
75
|
+
// Mock DB response for the FUSED query
|
|
76
|
+
// The coalescer will generate: "SELECT * FROM users WHERE id IN (?, ?, ?)"
|
|
77
|
+
// We need to mock the implementation to return data that the coalescer can map back.
|
|
78
|
+
// The memory adapter handles basic queries, but does it handle "IN"?
|
|
79
|
+
// The current MemoryAdapter likely DOES NOT support SQL parsing.
|
|
80
|
+
// So we need to mock the query method to return expected data for the fused query.
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
82
|
+
querySpy.mockImplementation(async (query) => {
|
|
83
|
+
if (typeof query === 'string' && query.includes('IN')) {
|
|
84
|
+
// This is the fused query
|
|
85
|
+
// Return dummy users
|
|
86
|
+
return [
|
|
87
|
+
{ id: 1, name: 'User 1' },
|
|
88
|
+
{ id: 2, name: 'User 2' },
|
|
89
|
+
{ id: 3, name: 'User 3' }
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
return [];
|
|
93
|
+
});
|
|
94
|
+
const response = await fetch(`http://localhost:${port}/qhttpx`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
body: JSON.stringify(batch)
|
|
98
|
+
});
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
// Verification
|
|
101
|
+
// 1. Check results
|
|
102
|
+
(0, vitest_1.expect)(data.results).toHaveLength(3);
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
(0, vitest_1.expect)(data.results.find((r) => r.id === 'req1').result).toEqual([{ id: 1, name: 'User 1' }]);
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
(0, vitest_1.expect)(data.results.find((r) => r.id === 'req2').result).toEqual([{ id: 2, name: 'User 2' }]);
|
|
107
|
+
// 2. Check that query was called ONCE with fused SQL
|
|
108
|
+
// The spy calls should show 1 call with "IN" and maybe others if coalescer failed?
|
|
109
|
+
// Wait, the coalescer calls the ADAPTER.
|
|
110
|
+
// Filter calls that look like the fused one
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
|
+
const fusedCalls = querySpy.mock.calls.filter((args) => args[0].includes('IN'));
|
|
113
|
+
(0, vitest_1.expect)(fusedCalls.length).toBe(1);
|
|
114
|
+
const individualCalls = querySpy.mock.calls.filter(args => args[0].includes('= ?'));
|
|
115
|
+
(0, vitest_1.expect)(individualCalls.length).toBe(0); // Should be 0 if fusion worked
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const http_1 = require("http");
|
|
5
|
+
const net_1 = require("net");
|
|
6
|
+
const body_parser_1 = require("../src/core/body-parser");
|
|
7
|
+
// Mock IncomingMessage
|
|
8
|
+
class MockRequest extends http_1.IncomingMessage {
|
|
9
|
+
constructor(method, headers, body) {
|
|
10
|
+
super(new net_1.Socket());
|
|
11
|
+
this.method = method;
|
|
12
|
+
this.headers = headers;
|
|
13
|
+
if (body) {
|
|
14
|
+
this.push(body);
|
|
15
|
+
this.push(null);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
this.push(null);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
(0, vitest_1.describe)('BodyParser', () => {
|
|
23
|
+
(0, vitest_1.it)('parses JSON body', async () => {
|
|
24
|
+
const req = new MockRequest('POST', { 'content-type': 'application/json' }, JSON.stringify({ foo: 'bar' }));
|
|
25
|
+
const parsed = await body_parser_1.BodyParser.parse(req);
|
|
26
|
+
(0, vitest_1.expect)(parsed.body).toEqual({ foo: 'bar' });
|
|
27
|
+
});
|
|
28
|
+
(0, vitest_1.it)('parses urlencoded body', async () => {
|
|
29
|
+
const req = new MockRequest('POST', { 'content-type': 'application/x-www-form-urlencoded' }, 'foo=bar&baz=123');
|
|
30
|
+
const parsed = await body_parser_1.BodyParser.parse(req);
|
|
31
|
+
(0, vitest_1.expect)(parsed.body).toEqual({ foo: 'bar', baz: '123' });
|
|
32
|
+
});
|
|
33
|
+
(0, vitest_1.it)('returns buffer for unknown content type', async () => {
|
|
34
|
+
const req = new MockRequest('POST', { 'content-type': 'application/octet-stream' }, 'hello world');
|
|
35
|
+
const parsed = await body_parser_1.BodyParser.parse(req);
|
|
36
|
+
(0, vitest_1.expect)(parsed.body).toBeInstanceOf(Buffer);
|
|
37
|
+
(0, vitest_1.expect)(parsed.body.toString()).toBe('hello world');
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)('throws error when body too large', async () => {
|
|
40
|
+
const req = new MockRequest('POST', { 'content-type': 'text/plain' }, '1234567890');
|
|
41
|
+
await (0, vitest_1.expect)(body_parser_1.BodyParser.parse(req, { maxBodyBytes: 5 })).rejects.toThrow('QHTTPX_BODY_TOO_LARGE');
|
|
42
|
+
});
|
|
43
|
+
(0, vitest_1.it)('throws error for invalid JSON', async () => {
|
|
44
|
+
const req = new MockRequest('POST', { 'content-type': 'application/json' }, '{ invalid json');
|
|
45
|
+
await (0, vitest_1.expect)(body_parser_1.BodyParser.parse(req)).rejects.toThrow('QHTTPX_INVALID_JSON');
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.it)('ignores GET requests', async () => {
|
|
48
|
+
const req = new MockRequest('GET', {});
|
|
49
|
+
const parsed = await body_parser_1.BodyParser.parse(req);
|
|
50
|
+
(0, vitest_1.expect)(parsed.body).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const src_1 = require("../src");
|
|
8
|
+
const zlib_1 = __importDefault(require("zlib"));
|
|
9
|
+
const http_1 = __importDefault(require("http"));
|
|
10
|
+
(0, vitest_1.describe)('Compression Middleware', () => {
|
|
11
|
+
(0, vitest_1.it)('should compress JSON response with gzip', async () => {
|
|
12
|
+
const app = new src_1.QHTTPX();
|
|
13
|
+
app.use((0, src_1.createCompressionMiddleware)());
|
|
14
|
+
const bigData = { data: 'a'.repeat(10000) };
|
|
15
|
+
app.get('/gzip', (ctx) => ctx.json(bigData));
|
|
16
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
17
|
+
// Use http.request to avoid automatic decompression by fetch
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
const response = await new Promise((resolve, reject) => {
|
|
20
|
+
const req = http_1.default.request(`http://127.0.0.1:${port}/gzip`, {
|
|
21
|
+
headers: { 'Accept-Encoding': 'gzip' }
|
|
22
|
+
}, (res) => {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
25
|
+
res.on('end', () => resolve({ headers: res.headers, data: Buffer.concat(chunks) }));
|
|
26
|
+
res.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
req.end();
|
|
29
|
+
});
|
|
30
|
+
(0, vitest_1.expect)(response.headers['content-encoding']).toBe('gzip');
|
|
31
|
+
(0, vitest_1.expect)(response.headers['vary']).toContain('Accept-Encoding');
|
|
32
|
+
const decompressed = zlib_1.default.gunzipSync(response.data);
|
|
33
|
+
const json = JSON.parse(decompressed.toString());
|
|
34
|
+
(0, vitest_1.expect)(json).toEqual(bigData);
|
|
35
|
+
await app.close();
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.it)('should not compress small response if threshold is high', async () => {
|
|
38
|
+
const app = new src_1.QHTTPX();
|
|
39
|
+
app.use((0, src_1.createCompressionMiddleware)({ threshold: 1000 }));
|
|
40
|
+
app.get('/small', (ctx) => {
|
|
41
|
+
ctx.json({ data: 'small' });
|
|
42
|
+
});
|
|
43
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
44
|
+
const response = await fetch(`http://127.0.0.1:${port}/small`, {
|
|
45
|
+
headers: { 'Accept-Encoding': 'gzip' },
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
48
|
+
// Should NOT have content-encoding because it's too small
|
|
49
|
+
(0, vitest_1.expect)(response.headers.get('content-encoding')).toBeNull();
|
|
50
|
+
await app.close();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.describe)('Server-Sent Events (SSE)', () => {
|
|
54
|
+
(0, vitest_1.it)('should stream events', async () => {
|
|
55
|
+
const app = new src_1.QHTTPX();
|
|
56
|
+
// app.use(createCompressionMiddleware()); // Disable compression for SSE test to debug
|
|
57
|
+
app.get('/sse', (ctx) => {
|
|
58
|
+
const stream = (0, src_1.createSSE)(ctx);
|
|
59
|
+
let count = 0;
|
|
60
|
+
const interval = setInterval(() => {
|
|
61
|
+
count++;
|
|
62
|
+
stream.send({ count }, 'ping');
|
|
63
|
+
if (count >= 3) {
|
|
64
|
+
clearInterval(interval);
|
|
65
|
+
stream.close();
|
|
66
|
+
}
|
|
67
|
+
}, 50);
|
|
68
|
+
});
|
|
69
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
70
|
+
const response = await new Promise((resolve, reject) => {
|
|
71
|
+
const req = http_1.default.request(`http://127.0.0.1:${port}/sse`, (res) => {
|
|
72
|
+
let data = '';
|
|
73
|
+
res.on('data', (chunk) => {
|
|
74
|
+
data += chunk.toString();
|
|
75
|
+
});
|
|
76
|
+
res.on('end', () => resolve(data));
|
|
77
|
+
res.on('error', reject);
|
|
78
|
+
});
|
|
79
|
+
req.end();
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.expect)(response).toContain('event: ping');
|
|
82
|
+
(0, vitest_1.expect)(response).toContain('data: {"count":1}');
|
|
83
|
+
(0, vitest_1.expect)(response).toContain('data: {"count":2}');
|
|
84
|
+
(0, vitest_1.expect)(response).toContain('data: {"count":3}');
|
|
85
|
+
await app.close();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const cookies_1 = require("../src/utils/cookies");
|
|
5
|
+
(0, vitest_1.describe)('Cookie Parsing', () => {
|
|
6
|
+
(0, vitest_1.it)('should parse simple cookies', () => {
|
|
7
|
+
const header = 'foo=bar; baz=qux';
|
|
8
|
+
const result = (0, cookies_1.parseCookies)(header);
|
|
9
|
+
(0, vitest_1.expect)(result).toEqual({ foo: 'bar', baz: 'qux' });
|
|
10
|
+
});
|
|
11
|
+
(0, vitest_1.it)('should handle quoted values', () => {
|
|
12
|
+
const header = 'foo="bar"; baz=qux';
|
|
13
|
+
// Basic implementation might not strip quotes if not decoding specifically for that,
|
|
14
|
+
// but let's see standard behavior. My implementation just decodes URI component.
|
|
15
|
+
// 'foo="bar"' split by '=' -> ['foo', '"bar"']. decodeURIComponent('"bar"') -> '"bar"'.
|
|
16
|
+
const result = (0, cookies_1.parseCookies)(header);
|
|
17
|
+
(0, vitest_1.expect)(result).toEqual({ foo: '"bar"', baz: 'qux' });
|
|
18
|
+
});
|
|
19
|
+
(0, vitest_1.it)('should handle URL encoded values', () => {
|
|
20
|
+
const header = 'foo=Hello%20World';
|
|
21
|
+
const result = (0, cookies_1.parseCookies)(header);
|
|
22
|
+
(0, vitest_1.expect)(result).toEqual({ foo: 'Hello World' });
|
|
23
|
+
});
|
|
24
|
+
(0, vitest_1.it)('should handle empty header', () => {
|
|
25
|
+
(0, vitest_1.expect)((0, cookies_1.parseCookies)(undefined)).toEqual({});
|
|
26
|
+
(0, vitest_1.expect)((0, cookies_1.parseCookies)('')).toEqual({});
|
|
27
|
+
});
|
|
28
|
+
(0, vitest_1.it)('should ignore cookies without name', () => {
|
|
29
|
+
const header = '=bar; baz=qux';
|
|
30
|
+
const result = (0, cookies_1.parseCookies)(header);
|
|
31
|
+
(0, vitest_1.expect)(result).toEqual({ baz: 'qux' });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
(0, vitest_1.describe)('Cookie Serialization', () => {
|
|
35
|
+
(0, vitest_1.it)('should serialize name and value', () => {
|
|
36
|
+
const result = (0, cookies_1.serializeCookie)('foo', 'bar');
|
|
37
|
+
(0, vitest_1.expect)(result).toBe('foo=bar; Path=/');
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)('should encode value', () => {
|
|
40
|
+
const result = (0, cookies_1.serializeCookie)('foo', 'Hello World');
|
|
41
|
+
(0, vitest_1.expect)(result).toBe('foo=Hello%20World; Path=/');
|
|
42
|
+
});
|
|
43
|
+
(0, vitest_1.it)('should handle Max-Age', () => {
|
|
44
|
+
const result = (0, cookies_1.serializeCookie)('foo', 'bar', { maxAge: 3600 });
|
|
45
|
+
(0, vitest_1.expect)(result).toContain('Max-Age=3600');
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.it)('should handle HttpOnly', () => {
|
|
48
|
+
const result = (0, cookies_1.serializeCookie)('foo', 'bar', { httpOnly: true });
|
|
49
|
+
(0, vitest_1.expect)(result).toContain('HttpOnly');
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.it)('should handle Secure', () => {
|
|
52
|
+
const result = (0, cookies_1.serializeCookie)('foo', 'bar', { secure: true });
|
|
53
|
+
(0, vitest_1.expect)(result).toContain('Secure');
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.it)('should handle SameSite', () => {
|
|
56
|
+
const result = (0, cookies_1.serializeCookie)('foo', 'bar', { sameSite: 'strict' });
|
|
57
|
+
(0, vitest_1.expect)(result).toContain('SameSite=Strict');
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.it)('should handle Path', () => {
|
|
60
|
+
const result = (0, cookies_1.serializeCookie)('foo', 'bar', { path: '/admin' });
|
|
61
|
+
(0, vitest_1.expect)(result).toContain('Path=/admin');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const src_1 = require("../src");
|
|
5
|
+
(0, vitest_1.describe)('CORS middleware', () => {
|
|
6
|
+
let app;
|
|
7
|
+
(0, vitest_1.afterEach)(async () => {
|
|
8
|
+
if (app) {
|
|
9
|
+
await app.close();
|
|
10
|
+
app = undefined;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
(0, vitest_1.it)('sets default CORS headers and passes through GET', async () => {
|
|
14
|
+
app = new src_1.QHTTPX();
|
|
15
|
+
app.use((0, src_1.createCorsMiddleware)());
|
|
16
|
+
app.get('/ping', (ctx) => {
|
|
17
|
+
ctx.send('pong');
|
|
18
|
+
});
|
|
19
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
20
|
+
const response = await fetch(`http://127.0.0.1:${port}/ping`, {
|
|
21
|
+
headers: {
|
|
22
|
+
origin: 'http://example.com',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
26
|
+
(0, vitest_1.expect)(await response.text()).toBe('pong');
|
|
27
|
+
(0, vitest_1.expect)(response.headers.get('access-control-allow-origin')).toBe('*');
|
|
28
|
+
});
|
|
29
|
+
(0, vitest_1.it)('handles preflight OPTIONS requests', async () => {
|
|
30
|
+
app = new src_1.QHTTPX();
|
|
31
|
+
app.use((0, src_1.createCorsMiddleware)({
|
|
32
|
+
origin: ['http://example.com'],
|
|
33
|
+
methods: ['GET', 'POST'],
|
|
34
|
+
allowedHeaders: ['content-type'],
|
|
35
|
+
maxAgeSeconds: 600,
|
|
36
|
+
}));
|
|
37
|
+
app.get('/ping', (ctx) => {
|
|
38
|
+
ctx.send('pong');
|
|
39
|
+
});
|
|
40
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
41
|
+
const response = await fetch(`http://127.0.0.1:${port}/ping`, {
|
|
42
|
+
method: 'OPTIONS',
|
|
43
|
+
headers: {
|
|
44
|
+
origin: 'http://example.com',
|
|
45
|
+
'access-control-request-method': 'GET',
|
|
46
|
+
'access-control-request-headers': 'content-type',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.expect)(response.status).toBe(204);
|
|
50
|
+
(0, vitest_1.expect)(response.headers.get('access-control-allow-origin')).toBe('http://example.com');
|
|
51
|
+
(0, vitest_1.expect)(response.headers.get('access-control-allow-methods')).toBe('GET, POST');
|
|
52
|
+
(0, vitest_1.expect)(response.headers.get('access-control-allow-headers')).toBe('content-type');
|
|
53
|
+
(0, vitest_1.expect)(response.headers.get('access-control-max-age')).toBe('600');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const manager_1 = require("../src/database/manager");
|
|
5
|
+
const memory_1 = require("../src/database/adapters/memory");
|
|
6
|
+
(0, vitest_1.describe)('Database Engine', () => {
|
|
7
|
+
let dbManager;
|
|
8
|
+
(0, vitest_1.beforeEach)(() => {
|
|
9
|
+
// Register the adapter
|
|
10
|
+
manager_1.DatabaseManager.registerAdapter('memory', memory_1.MemoryAdapter);
|
|
11
|
+
// Initialize manager
|
|
12
|
+
dbManager = new manager_1.DatabaseManager({
|
|
13
|
+
default: 'main',
|
|
14
|
+
connections: {
|
|
15
|
+
main: {
|
|
16
|
+
type: 'memory',
|
|
17
|
+
database: 'test_db'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
(0, vitest_1.afterEach)(async () => {
|
|
23
|
+
await dbManager.disconnect();
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.it)('should connect to the default database', async () => {
|
|
26
|
+
const adapter = await dbManager.connect();
|
|
27
|
+
(0, vitest_1.expect)(adapter).toBeDefined();
|
|
28
|
+
(0, vitest_1.expect)(adapter.isConnected()).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
(0, vitest_1.it)('should perform CRUD operations with MemoryAdapter', async () => {
|
|
31
|
+
const adapter = await dbManager.connect();
|
|
32
|
+
// Insert
|
|
33
|
+
const user = await adapter.query({
|
|
34
|
+
collection: 'users',
|
|
35
|
+
action: 'insert',
|
|
36
|
+
data: { name: 'John Doe', email: 'john@example.com' }
|
|
37
|
+
});
|
|
38
|
+
(0, vitest_1.expect)(user).toHaveProperty('id');
|
|
39
|
+
(0, vitest_1.expect)(user.name).toBe('John Doe');
|
|
40
|
+
// Find
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
const users = await adapter.query({
|
|
43
|
+
collection: 'users',
|
|
44
|
+
action: 'find',
|
|
45
|
+
filter: { name: 'John Doe' }
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.expect)(users).toHaveLength(1);
|
|
48
|
+
(0, vitest_1.expect)(users[0].email).toBe('john@example.com');
|
|
49
|
+
// Update
|
|
50
|
+
const updateCount = await adapter.query({
|
|
51
|
+
collection: 'users',
|
|
52
|
+
action: 'update',
|
|
53
|
+
filter: { name: 'John Doe' },
|
|
54
|
+
data: { name: 'John Updated' }
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.expect)(updateCount).toBe(1);
|
|
57
|
+
// Verify Update
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
const updatedUsers = await adapter.query({
|
|
60
|
+
collection: 'users',
|
|
61
|
+
action: 'find',
|
|
62
|
+
filter: { name: 'John Updated' }
|
|
63
|
+
});
|
|
64
|
+
(0, vitest_1.expect)(updatedUsers).toHaveLength(1);
|
|
65
|
+
// Delete
|
|
66
|
+
const deleteCount = await adapter.query({
|
|
67
|
+
collection: 'users',
|
|
68
|
+
action: 'delete',
|
|
69
|
+
filter: { name: 'John Updated' }
|
|
70
|
+
});
|
|
71
|
+
(0, vitest_1.expect)(deleteCount).toBe(1);
|
|
72
|
+
// Verify Delete
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
const remainingUsers = await adapter.query({
|
|
75
|
+
collection: 'users',
|
|
76
|
+
action: 'find'
|
|
77
|
+
});
|
|
78
|
+
(0, vitest_1.expect)(remainingUsers).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const index_1 = require("../src/index");
|
|
5
|
+
const presets_1 = require("../src/middleware/presets");
|
|
6
|
+
(0, vitest_1.describe)('Developer Experience Features', () => {
|
|
7
|
+
(0, vitest_1.describe)('Routing Ergonomics', () => {
|
|
8
|
+
(0, vitest_1.it)('supports chainable route() builder', async () => {
|
|
9
|
+
const app = new index_1.QHTTPX();
|
|
10
|
+
app.route('/users/:id')
|
|
11
|
+
.get((ctx) => ctx.json({ method: 'GET', id: ctx.params.id }))
|
|
12
|
+
.post((ctx) => ctx.json({ method: 'POST', id: ctx.params.id }))
|
|
13
|
+
.put((ctx) => ctx.json({ method: 'PUT', id: ctx.params.id }))
|
|
14
|
+
.delete((ctx) => ctx.json({ method: 'DELETE', id: ctx.params.id }));
|
|
15
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
16
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
17
|
+
// Test GET
|
|
18
|
+
const resGet = await fetch(`${baseUrl}/users/123`);
|
|
19
|
+
(0, vitest_1.expect)(await resGet.json()).toEqual({ method: 'GET', id: '123' });
|
|
20
|
+
// Test POST
|
|
21
|
+
const resPost = await fetch(`${baseUrl}/users/123`, { method: 'POST' });
|
|
22
|
+
(0, vitest_1.expect)(await resPost.json()).toEqual({ method: 'POST', id: '123' });
|
|
23
|
+
// Test PUT
|
|
24
|
+
const resPut = await fetch(`${baseUrl}/users/123`, { method: 'PUT' });
|
|
25
|
+
(0, vitest_1.expect)(await resPut.json()).toEqual({ method: 'PUT', id: '123' });
|
|
26
|
+
// Test DELETE
|
|
27
|
+
const resDelete = await fetch(`${baseUrl}/users/123`, { method: 'DELETE' });
|
|
28
|
+
(0, vitest_1.expect)(await resDelete.json()).toEqual({ method: 'DELETE', id: '123' });
|
|
29
|
+
await app.close();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.describe)('Simple App Helper', () => {
|
|
33
|
+
(0, vitest_1.it)('supports createHttpApp basic usage', async () => {
|
|
34
|
+
const app = (0, index_1.createHttpApp)();
|
|
35
|
+
app.get('/', (ctx) => {
|
|
36
|
+
ctx.send('Hello World!');
|
|
37
|
+
});
|
|
38
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
39
|
+
const response = await fetch(`http://127.0.0.1:${port}/`);
|
|
40
|
+
(0, vitest_1.expect)(response.status).toBe(200);
|
|
41
|
+
(0, vitest_1.expect)(await response.text()).toBe('Hello World!');
|
|
42
|
+
await app.close();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
(0, vitest_1.describe)('Presets', () => {
|
|
46
|
+
(0, vitest_1.it)('createApiPreset returns middlewares', () => {
|
|
47
|
+
const middlewares = (0, presets_1.createApiPreset)();
|
|
48
|
+
// Should have CORS (1) + Security Headers (1) + Logger (1) = 3
|
|
49
|
+
(0, vitest_1.expect)(middlewares.length).toBe(3);
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.it)('createApiPreset allows disabling logger', () => {
|
|
52
|
+
const middlewares = (0, presets_1.createApiPreset)({ logging: false });
|
|
53
|
+
// CORS + Security Headers = 2
|
|
54
|
+
(0, vitest_1.expect)(middlewares.length).toBe(2);
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)('createStaticAppPreset returns middlewares with static', () => {
|
|
57
|
+
const middlewares = (0, presets_1.createStaticAppPreset)({
|
|
58
|
+
static: { root: './public' },
|
|
59
|
+
});
|
|
60
|
+
// CORS + Security Headers + Logger + Static = 4
|
|
61
|
+
(0, vitest_1.expect)(middlewares.length).toBe(4);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const server_1 = require("../src/core/server");
|
|
5
|
+
// Mock External Libraries
|
|
6
|
+
const mockJose = {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
8
|
+
jwtVerify: async (token, _secret) => {
|
|
9
|
+
if (token === 'valid_token')
|
|
10
|
+
return { payload: { sub: 'user_123', role: 'admin' } };
|
|
11
|
+
throw new Error('Invalid Token');
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const mockRedis = {
|
|
15
|
+
get: async (key) => {
|
|
16
|
+
if (key === 'cache:page:home')
|
|
17
|
+
return '<html>Cached Home</html>';
|
|
18
|
+
return null;
|
|
19
|
+
},
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
21
|
+
set: async (key, val) => 'OK'
|
|
22
|
+
};
|
|
23
|
+
(0, vitest_1.describe)('SaaS Ecosystem Compatibility', () => {
|
|
24
|
+
(0, vitest_1.it)('should allow integration of 3rd party JWT libraries (e.g. jose)', async () => {
|
|
25
|
+
const app = new server_1.QHTTPX();
|
|
26
|
+
const secret = 'super-secret';
|
|
27
|
+
// const token = await new SignJWT({ 'urn:example:claim': true })
|
|
28
|
+
// .setProtectedHeader({ alg: 'HS256' })
|
|
29
|
+
// .setIssuedAt()
|
|
30
|
+
// .setExpirationTime('2h')
|
|
31
|
+
// .sign(new TextEncoder().encode(secret));
|
|
32
|
+
// 1. Create a custom Authentication Middleware using "jose"
|
|
33
|
+
const authMiddleware = async (ctx, next) => {
|
|
34
|
+
const authHeader = ctx.req.headers['authorization'];
|
|
35
|
+
if (!authHeader) {
|
|
36
|
+
ctx.res.statusCode = 401;
|
|
37
|
+
ctx.res.end('Unauthorized');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const token = authHeader.split(' ')[1];
|
|
42
|
+
const { payload } = await mockJose.jwtVerify(token, secret);
|
|
43
|
+
// Store user in ctx.state (Standard Pattern)
|
|
44
|
+
ctx.state.user = payload;
|
|
45
|
+
await next();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
ctx.res.statusCode = 403;
|
|
49
|
+
ctx.res.end('Forbidden');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
app.use(authMiddleware);
|
|
53
|
+
// Protected Route
|
|
54
|
+
app.get('/dashboard', async (ctx) => {
|
|
55
|
+
// Access the user injected by middleware
|
|
56
|
+
const user = ctx.state.user;
|
|
57
|
+
ctx.json({ message: `Welcome ${user.sub}`, role: user.role });
|
|
58
|
+
});
|
|
59
|
+
const { port } = await app.listen(0);
|
|
60
|
+
const url = `http://localhost:${port}`;
|
|
61
|
+
// Test Valid Token
|
|
62
|
+
const res1 = await fetch(`${url}/dashboard`, { headers: { Authorization: 'Bearer valid_token' } });
|
|
63
|
+
(0, vitest_1.expect)(res1.status).toBe(200);
|
|
64
|
+
const data1 = await res1.json();
|
|
65
|
+
(0, vitest_1.expect)(data1).toEqual({ message: 'Welcome user_123', role: 'admin' });
|
|
66
|
+
// Test Invalid Token
|
|
67
|
+
const res2 = await fetch(`${url}/dashboard`, { headers: { Authorization: 'Bearer bad_token' } });
|
|
68
|
+
(0, vitest_1.expect)(res2.status).toBe(403);
|
|
69
|
+
await app.close();
|
|
70
|
+
});
|
|
71
|
+
(0, vitest_1.it)('should allow integration of 3rd party Caching (e.g. Redis)', async () => {
|
|
72
|
+
const app = new server_1.QHTTPX();
|
|
73
|
+
// 2. Create a Caching Middleware using "redis"
|
|
74
|
+
const cacheMiddleware = async (ctx, next) => {
|
|
75
|
+
const key = `cache:page:${ctx.url.pathname.replace('/', '')}`;
|
|
76
|
+
const cached = await mockRedis.get(key);
|
|
77
|
+
if (cached) {
|
|
78
|
+
ctx.res.setHeader('X-Cache', 'HIT');
|
|
79
|
+
ctx.res.end(cached);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
ctx.res.setHeader('X-Cache', 'MISS');
|
|
83
|
+
await next();
|
|
84
|
+
};
|
|
85
|
+
app.use(cacheMiddleware);
|
|
86
|
+
app.get('/home', async (ctx) => {
|
|
87
|
+
ctx.html('<html>Fresh Home</html>');
|
|
88
|
+
});
|
|
89
|
+
const { port } = await app.listen(0);
|
|
90
|
+
const url = `http://localhost:${port}`;
|
|
91
|
+
// Test Cache Hit (Mocked)
|
|
92
|
+
const res1 = await fetch(`${url}/home`);
|
|
93
|
+
(0, vitest_1.expect)(res1.status).toBe(200);
|
|
94
|
+
(0, vitest_1.expect)(res1.headers.get('x-cache')).toBe('HIT');
|
|
95
|
+
const text1 = await res1.text();
|
|
96
|
+
(0, vitest_1.expect)(text1).toBe('<html>Cached Home</html>');
|
|
97
|
+
// Test Cache Miss (Mocked for other route)
|
|
98
|
+
// Note: In a real app we'd need to intercept res.end to write to redis,
|
|
99
|
+
// but for this capability check, reading is enough proof.
|
|
100
|
+
await app.close();
|
|
101
|
+
});
|
|
102
|
+
(0, vitest_1.it)('should allow integration of ORM/DB libraries', async () => {
|
|
103
|
+
const app = new server_1.QHTTPX();
|
|
104
|
+
// Mock TypeORM/Prisma usage
|
|
105
|
+
const db = {
|
|
106
|
+
users: {
|
|
107
|
+
findMany: async () => [{ id: 1, name: 'Alice' }],
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
// Middleware to attach DB to context
|
|
111
|
+
app.use(async (ctx, next) => {
|
|
112
|
+
// We can attach to ctx.state or extend ctx type if we want
|
|
113
|
+
ctx.state.db = db;
|
|
114
|
+
await next();
|
|
115
|
+
});
|
|
116
|
+
app.get('/users', async (ctx) => {
|
|
117
|
+
try {
|
|
118
|
+
const users = await ctx.state.db.users.findMany();
|
|
119
|
+
ctx.res.end(JSON.stringify(users));
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
ctx.res.statusCode = 500;
|
|
124
|
+
ctx.res.end('DB Error');
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
const { port } = await app.listen(0);
|
|
128
|
+
const url = `http://localhost:${port}`;
|
|
129
|
+
const res = await fetch(`${url}/users`);
|
|
130
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
131
|
+
await app.close();
|
|
132
|
+
});
|
|
133
|
+
});
|