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.
Files changed (197) hide show
  1. package/.eslintrc.json +22 -0
  2. package/.github/workflows/ci.yml +32 -0
  3. package/.github/workflows/npm-publish.yml +37 -0
  4. package/.github/workflows/release.yml +21 -0
  5. package/.prettierrc +7 -0
  6. package/CHANGELOG.md +145 -0
  7. package/LICENSE +21 -0
  8. package/README.md +343 -0
  9. package/dist/package.json +61 -0
  10. package/dist/src/benchmarks/compare-frameworks.js +119 -0
  11. package/dist/src/benchmarks/quantam-users.js +56 -0
  12. package/dist/src/benchmarks/simple-json.js +58 -0
  13. package/dist/src/benchmarks/ultra-mode.js +122 -0
  14. package/dist/src/cli/index.js +200 -0
  15. package/dist/src/client/index.js +72 -0
  16. package/dist/src/core/batch.js +97 -0
  17. package/dist/src/core/body-parser.js +121 -0
  18. package/dist/src/core/buffer-pool.js +70 -0
  19. package/dist/src/core/config.js +50 -0
  20. package/dist/src/core/fusion.js +183 -0
  21. package/dist/src/core/logger.js +49 -0
  22. package/dist/src/core/metrics.js +111 -0
  23. package/dist/src/core/resources.js +25 -0
  24. package/dist/src/core/scheduler.js +85 -0
  25. package/dist/src/core/scope.js +68 -0
  26. package/dist/src/core/serializer.js +44 -0
  27. package/dist/src/core/server.js +905 -0
  28. package/dist/src/core/stream.js +71 -0
  29. package/dist/src/core/tasks.js +87 -0
  30. package/dist/src/core/types.js +19 -0
  31. package/dist/src/core/websocket.js +86 -0
  32. package/dist/src/core/worker-queue.js +73 -0
  33. package/dist/src/database/adapters/memory.js +90 -0
  34. package/dist/src/database/adapters/mongo.js +141 -0
  35. package/dist/src/database/adapters/postgres.js +111 -0
  36. package/dist/src/database/adapters/sqlite.js +42 -0
  37. package/dist/src/database/coalescer.js +134 -0
  38. package/dist/src/database/manager.js +87 -0
  39. package/dist/src/database/types.js +2 -0
  40. package/dist/src/index.js +61 -0
  41. package/dist/src/middleware/compression.js +133 -0
  42. package/dist/src/middleware/cors.js +66 -0
  43. package/dist/src/middleware/presets.js +33 -0
  44. package/dist/src/middleware/rate-limit.js +77 -0
  45. package/dist/src/middleware/security.js +69 -0
  46. package/dist/src/middleware/static.js +191 -0
  47. package/dist/src/openapi/generator.js +149 -0
  48. package/dist/src/router/radix-router.js +89 -0
  49. package/dist/src/router/radix-tree.js +81 -0
  50. package/dist/src/router/router.js +146 -0
  51. package/dist/src/testing/index.js +84 -0
  52. package/dist/src/utils/cookies.js +59 -0
  53. package/dist/src/utils/logger.js +45 -0
  54. package/dist/src/utils/signals.js +31 -0
  55. package/dist/src/utils/sse.js +32 -0
  56. package/dist/src/validation/index.js +19 -0
  57. package/dist/src/validation/simple.js +102 -0
  58. package/dist/src/validation/types.js +12 -0
  59. package/dist/src/validation/zod.js +18 -0
  60. package/dist/src/views/index.js +17 -0
  61. package/dist/src/views/types.js +2 -0
  62. package/dist/tests/adapters.test.js +106 -0
  63. package/dist/tests/batch.test.js +117 -0
  64. package/dist/tests/body-parser.test.js +52 -0
  65. package/dist/tests/compression-sse.test.js +87 -0
  66. package/dist/tests/cookies.test.js +63 -0
  67. package/dist/tests/cors.test.js +55 -0
  68. package/dist/tests/database.test.js +80 -0
  69. package/dist/tests/dx.test.js +64 -0
  70. package/dist/tests/ecosystem.test.js +133 -0
  71. package/dist/tests/features.test.js +47 -0
  72. package/dist/tests/fusion.test.js +92 -0
  73. package/dist/tests/http-basic.test.js +124 -0
  74. package/dist/tests/logger.test.js +33 -0
  75. package/dist/tests/middleware.test.js +109 -0
  76. package/dist/tests/observability.test.js +59 -0
  77. package/dist/tests/openapi.test.js +64 -0
  78. package/dist/tests/plugin.test.js +65 -0
  79. package/dist/tests/plugins.test.js +71 -0
  80. package/dist/tests/rate-limit.test.js +77 -0
  81. package/dist/tests/resources.test.js +44 -0
  82. package/dist/tests/scheduler.test.js +46 -0
  83. package/dist/tests/schema-routes.test.js +77 -0
  84. package/dist/tests/security.test.js +83 -0
  85. package/dist/tests/server-db.test.js +72 -0
  86. package/dist/tests/smoke.test.js +10 -0
  87. package/dist/tests/sqlite-fusion.test.js +92 -0
  88. package/dist/tests/static.test.js +102 -0
  89. package/dist/tests/stream.test.js +44 -0
  90. package/dist/tests/task-metrics.test.js +53 -0
  91. package/dist/tests/tasks.test.js +62 -0
  92. package/dist/tests/testing.test.js +47 -0
  93. package/dist/tests/validation.test.js +107 -0
  94. package/dist/tests/websocket.test.js +146 -0
  95. package/dist/vitest.config.js +9 -0
  96. package/docs/AEGIS.md +76 -0
  97. package/docs/BENCHMARKS.md +36 -0
  98. package/docs/CAPABILITIES.md +70 -0
  99. package/docs/CLI.md +43 -0
  100. package/docs/DATABASE.md +142 -0
  101. package/docs/ECOSYSTEM.md +146 -0
  102. package/docs/NEXT_STEPS.md +99 -0
  103. package/docs/OPENAPI.md +99 -0
  104. package/docs/PLUGINS.md +59 -0
  105. package/docs/REAL_WORLD_EXAMPLES.md +109 -0
  106. package/docs/ROADMAP.md +366 -0
  107. package/docs/VALIDATION.md +136 -0
  108. package/eslint.config.cjs +26 -0
  109. package/examples/api-server.ts +254 -0
  110. package/package.json +61 -0
  111. package/src/benchmarks/compare-frameworks.ts +149 -0
  112. package/src/benchmarks/quantam-users.ts +70 -0
  113. package/src/benchmarks/simple-json.ts +71 -0
  114. package/src/benchmarks/ultra-mode.ts +159 -0
  115. package/src/cli/index.ts +214 -0
  116. package/src/client/index.ts +93 -0
  117. package/src/core/batch.ts +110 -0
  118. package/src/core/body-parser.ts +151 -0
  119. package/src/core/buffer-pool.ts +96 -0
  120. package/src/core/config.ts +60 -0
  121. package/src/core/fusion.ts +210 -0
  122. package/src/core/logger.ts +70 -0
  123. package/src/core/metrics.ts +166 -0
  124. package/src/core/resources.ts +38 -0
  125. package/src/core/scheduler.ts +126 -0
  126. package/src/core/scope.ts +87 -0
  127. package/src/core/serializer.ts +41 -0
  128. package/src/core/server.ts +1113 -0
  129. package/src/core/stream.ts +111 -0
  130. package/src/core/tasks.ts +138 -0
  131. package/src/core/types.ts +178 -0
  132. package/src/core/websocket.ts +112 -0
  133. package/src/core/worker-queue.ts +90 -0
  134. package/src/database/adapters/memory.ts +99 -0
  135. package/src/database/adapters/mongo.ts +116 -0
  136. package/src/database/adapters/postgres.ts +86 -0
  137. package/src/database/adapters/sqlite.ts +44 -0
  138. package/src/database/coalescer.ts +153 -0
  139. package/src/database/manager.ts +97 -0
  140. package/src/database/types.ts +24 -0
  141. package/src/index.ts +42 -0
  142. package/src/middleware/compression.ts +147 -0
  143. package/src/middleware/cors.ts +98 -0
  144. package/src/middleware/presets.ts +50 -0
  145. package/src/middleware/rate-limit.ts +106 -0
  146. package/src/middleware/security.ts +109 -0
  147. package/src/middleware/static.ts +216 -0
  148. package/src/openapi/generator.ts +167 -0
  149. package/src/router/radix-router.ts +119 -0
  150. package/src/router/radix-tree.ts +106 -0
  151. package/src/router/router.ts +190 -0
  152. package/src/testing/index.ts +104 -0
  153. package/src/utils/cookies.ts +67 -0
  154. package/src/utils/logger.ts +59 -0
  155. package/src/utils/signals.ts +45 -0
  156. package/src/utils/sse.ts +41 -0
  157. package/src/validation/index.ts +3 -0
  158. package/src/validation/simple.ts +93 -0
  159. package/src/validation/types.ts +38 -0
  160. package/src/validation/zod.ts +14 -0
  161. package/src/views/index.ts +1 -0
  162. package/src/views/types.ts +4 -0
  163. package/tests/adapters.test.ts +120 -0
  164. package/tests/batch.test.ts +139 -0
  165. package/tests/body-parser.test.ts +83 -0
  166. package/tests/compression-sse.test.ts +98 -0
  167. package/tests/cookies.test.ts +74 -0
  168. package/tests/cors.test.ts +79 -0
  169. package/tests/database.test.ts +90 -0
  170. package/tests/dx.test.ts +78 -0
  171. package/tests/ecosystem.test.ts +156 -0
  172. package/tests/features.test.ts +51 -0
  173. package/tests/fusion.test.ts +121 -0
  174. package/tests/http-basic.test.ts +161 -0
  175. package/tests/logger.test.ts +48 -0
  176. package/tests/middleware.test.ts +137 -0
  177. package/tests/observability.test.ts +91 -0
  178. package/tests/openapi.test.ts +74 -0
  179. package/tests/plugin.test.ts +85 -0
  180. package/tests/plugins.test.ts +93 -0
  181. package/tests/rate-limit.test.ts +97 -0
  182. package/tests/resources.test.ts +64 -0
  183. package/tests/scheduler.test.ts +71 -0
  184. package/tests/schema-routes.test.ts +89 -0
  185. package/tests/security.test.ts +128 -0
  186. package/tests/server-db.test.ts +72 -0
  187. package/tests/smoke.test.ts +9 -0
  188. package/tests/sqlite-fusion.test.ts +106 -0
  189. package/tests/static.test.ts +111 -0
  190. package/tests/stream.test.ts +58 -0
  191. package/tests/task-metrics.test.ts +78 -0
  192. package/tests/tasks.test.ts +90 -0
  193. package/tests/testing.test.ts +53 -0
  194. package/tests/validation.test.ts +126 -0
  195. package/tests/websocket.test.ts +132 -0
  196. package/tsconfig.json +16 -0
  197. package/vitest.config.ts +9 -0
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { PostgresAdapter } from '../src/database/adapters/postgres';
3
+ import { MongoAdapter } from '../src/database/adapters/mongo';
4
+
5
+ // Define mocks
6
+ const pgPoolQuery = vi.fn(() => Promise.resolve({ rows: [], command: 'SELECT' }));
7
+ const pgConnect = vi.fn(() => Promise.resolve({ release: vi.fn() }));
8
+ const pgEnd = vi.fn();
9
+
10
+ vi.mock('pg', () => {
11
+ const Pool = function() {
12
+ return {
13
+ connect: pgConnect,
14
+ query: pgPoolQuery,
15
+ end: pgEnd,
16
+ on: vi.fn(),
17
+ ended: false
18
+ };
19
+ };
20
+ return {
21
+ default: { Pool },
22
+ Pool
23
+ };
24
+ });
25
+
26
+ const mongoFind = vi.fn(() => ({
27
+ toArray: vi.fn(() => Promise.resolve([])),
28
+ limit: vi.fn(),
29
+ skip: vi.fn()
30
+ }));
31
+ const mongoConnect = vi.fn(() => Promise.resolve());
32
+ const mongoClose = vi.fn();
33
+
34
+ vi.mock('mongodb', () => {
35
+ const MongoClient = function() {
36
+ return {
37
+ connect: mongoConnect,
38
+ db: vi.fn(() => ({
39
+ collection: vi.fn(() => ({
40
+ find: mongoFind,
41
+ findOne: vi.fn(),
42
+ insertOne: vi.fn(),
43
+ // add other methods as needed
44
+ updateOne: vi.fn(),
45
+ updateMany: vi.fn(),
46
+ deleteOne: vi.fn(),
47
+ deleteMany: vi.fn(),
48
+ aggregate: vi.fn(() => ({ toArray: vi.fn(() => Promise.resolve([])) }))
49
+ }))
50
+ })),
51
+ close: mongoClose
52
+ };
53
+ };
54
+ return {
55
+ default: { MongoClient },
56
+ MongoClient
57
+ };
58
+ });
59
+
60
+ describe('Database Adapters', () => {
61
+
62
+ describe('PostgresAdapter', () => {
63
+ let adapter: PostgresAdapter;
64
+
65
+ beforeEach(() => {
66
+ adapter = new PostgresAdapter({ type: 'postgres', database: 'test' });
67
+ vi.clearAllMocks();
68
+ });
69
+
70
+ it('should connect and disconnect', async () => {
71
+ await adapter.connect();
72
+ expect(pgConnect).toHaveBeenCalled();
73
+ expect(adapter.isConnected()).toBe(true);
74
+
75
+ await adapter.disconnect();
76
+ expect(pgEnd).toHaveBeenCalled();
77
+ // Since our mock object is static in the factory, we can't easily check 'pool' null state
78
+ // via the mock, but we can check the adapter state if it relies on the pool property.
79
+ // The adapter sets this.pool = null.
80
+ expect(adapter.isConnected()).toBe(false);
81
+ });
82
+
83
+ it('should execute query', async () => {
84
+ await adapter.connect();
85
+ await adapter.query('SELECT * FROM users');
86
+ expect(pgPoolQuery).toHaveBeenCalledWith('SELECT * FROM users', undefined);
87
+ });
88
+ });
89
+
90
+ describe('MongoAdapter', () => {
91
+ let adapter: MongoAdapter;
92
+
93
+ beforeEach(() => {
94
+ adapter = new MongoAdapter({ type: 'mongo', url: 'mongodb://localhost' });
95
+ vi.clearAllMocks();
96
+ });
97
+
98
+ it('should connect and disconnect', async () => {
99
+ await adapter.connect();
100
+ expect(mongoConnect).toHaveBeenCalled();
101
+ expect(adapter.isConnected()).toBe(true);
102
+
103
+ await adapter.disconnect();
104
+ expect(mongoClose).toHaveBeenCalled();
105
+ expect(adapter.isConnected()).toBe(false);
106
+ });
107
+
108
+ it('should execute find query via object syntax', async () => {
109
+ await adapter.connect();
110
+ await adapter.query({ collection: 'users', action: 'find', filter: { id: 1 } });
111
+ expect(mongoFind).toHaveBeenCalledWith({ id: 1 });
112
+ });
113
+
114
+ it('should execute find query via string syntax', async () => {
115
+ await adapter.connect();
116
+ await adapter.query('users.find', [{ id: 1 }]);
117
+ expect(mongoFind).toHaveBeenCalledWith({ id: 1 });
118
+ });
119
+ });
120
+ });
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { QHTTPX } from '../src/core/server';
3
+ import { DatabaseManager } from '../src/database/manager';
4
+ import { MemoryAdapter } from '../src/database/adapters/memory';
5
+
6
+ describe('Batch Engine & Query Fusion', () => {
7
+ let app: QHTTPX;
8
+ let dbManager: DatabaseManager;
9
+
10
+ beforeEach(async () => {
11
+ DatabaseManager.registerAdapter('memory', MemoryAdapter);
12
+
13
+ dbManager = new DatabaseManager({
14
+ default: 'main',
15
+ connections: {
16
+ main: { type: 'memory', database: 'test_db' }
17
+ }
18
+ });
19
+
20
+ await dbManager.connect();
21
+
22
+ app = new QHTTPX({
23
+ database: dbManager,
24
+ enableBatching: true
25
+ });
26
+ });
27
+
28
+ afterEach(async () => {
29
+ await app.close();
30
+ await dbManager.disconnect();
31
+ });
32
+
33
+ it('should execute multiple operations in a single batch request', async () => {
34
+ // Define ops
35
+ app.op('math.add', async (params) => {
36
+ return params.a + params.b;
37
+ });
38
+
39
+ app.op('math.multiply', async (params) => {
40
+ return params.a * params.b;
41
+ });
42
+
43
+ const { port } = await app.listen(0);
44
+
45
+ const batch = {
46
+ batch: [
47
+ { op: 'math.add', params: { a: 5, b: 3 }, id: 1 },
48
+ { op: 'math.multiply', params: { a: 4, b: 2 }, id: 2 }
49
+ ]
50
+ };
51
+
52
+ const response = await fetch(`http://localhost:${port}/qhttpx`, {
53
+ method: 'POST',
54
+ headers: { 'Content-Type': 'application/json' },
55
+ body: JSON.stringify(batch)
56
+ });
57
+
58
+ const data = await response.json();
59
+
60
+ expect(data.results).toHaveLength(2);
61
+ expect(data.results[0]).toEqual({ result: 8, id: 1 });
62
+ expect(data.results[1]).toEqual({ result: 8, id: 2 });
63
+ });
64
+
65
+ it('should fuse database queries', async () => {
66
+ // Setup spy on the adapter's query method to verify fusion
67
+ const adapter = dbManager.get('main');
68
+ const querySpy = vi.spyOn(adapter, 'query');
69
+
70
+ // Define an op that uses DB
71
+ app.op('getUser', async (params, ctx) => {
72
+ // Simulate simple query that matches our coalescer pattern
73
+ // "SELECT * FROM users WHERE id = ?"
74
+ if (!ctx.db) throw new Error('No DB');
75
+ const result = await ctx.db.get().query('SELECT * FROM users WHERE id = ?', [params.id]);
76
+ return result;
77
+ });
78
+
79
+ const { port } = await app.listen(0);
80
+
81
+ // Prepare batch with 3 requests
82
+ const batch = {
83
+ batch: [
84
+ { op: 'getUser', params: { id: 1 }, id: 'req1' },
85
+ { op: 'getUser', params: { id: 2 }, id: 'req2' },
86
+ { op: 'getUser', params: { id: 3 }, id: 'req3' }
87
+ ]
88
+ };
89
+
90
+ // Mock DB response for the FUSED query
91
+ // The coalescer will generate: "SELECT * FROM users WHERE id IN (?, ?, ?)"
92
+ // We need to mock the implementation to return data that the coalescer can map back.
93
+ // The memory adapter handles basic queries, but does it handle "IN"?
94
+ // The current MemoryAdapter likely DOES NOT support SQL parsing.
95
+ // So we need to mock the query method to return expected data for the fused query.
96
+
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ querySpy.mockImplementation(async (query: any) => {
99
+ if (typeof query === 'string' && query.includes('IN')) {
100
+ // This is the fused query
101
+ // Return dummy users
102
+ return [
103
+ { id: 1, name: 'User 1' },
104
+ { id: 2, name: 'User 2' },
105
+ { id: 3, name: 'User 3' }
106
+ ];
107
+ }
108
+ return [];
109
+ });
110
+
111
+ const response = await fetch(`http://localhost:${port}/qhttpx`, {
112
+ method: 'POST',
113
+ headers: { 'Content-Type': 'application/json' },
114
+ body: JSON.stringify(batch)
115
+ });
116
+
117
+ const data = await response.json();
118
+
119
+ // Verification
120
+ // 1. Check results
121
+ expect(data.results).toHaveLength(3);
122
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
+ expect(data.results.find((r: any) => r.id === 'req1').result).toEqual([{ id: 1, name: 'User 1' }]);
124
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
125
+ expect(data.results.find((r: any) => r.id === 'req2').result).toEqual([{ id: 2, name: 'User 2' }]);
126
+
127
+ // 2. Check that query was called ONCE with fused SQL
128
+ // The spy calls should show 1 call with "IN" and maybe others if coalescer failed?
129
+ // Wait, the coalescer calls the ADAPTER.
130
+
131
+ // Filter calls that look like the fused one
132
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
+ const fusedCalls = querySpy.mock.calls.filter((args: any) => (args[0] as string).includes('IN'));
134
+ expect(fusedCalls.length).toBe(1);
135
+
136
+ const individualCalls = querySpy.mock.calls.filter(args => (args[0] as string).includes('= ?'));
137
+ expect(individualCalls.length).toBe(0); // Should be 0 if fusion worked
138
+ });
139
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { IncomingMessage } from 'http';
3
+ import { Socket } from 'net';
4
+ import { BodyParser } from '../src/core/body-parser';
5
+
6
+ // Mock IncomingMessage
7
+ class MockRequest extends IncomingMessage {
8
+ constructor(method: string, headers: Record<string, string>, body?: string | Buffer) {
9
+ super(new Socket());
10
+ this.method = method;
11
+ this.headers = headers;
12
+ if (body) {
13
+ this.push(body);
14
+ this.push(null);
15
+ } else {
16
+ this.push(null);
17
+ }
18
+ }
19
+ }
20
+
21
+ describe('BodyParser', () => {
22
+ it('parses JSON body', async () => {
23
+ const req = new MockRequest(
24
+ 'POST',
25
+ { 'content-type': 'application/json' },
26
+ JSON.stringify({ foo: 'bar' })
27
+ );
28
+
29
+ const parsed = await BodyParser.parse(req);
30
+ expect(parsed.body).toEqual({ foo: 'bar' });
31
+ });
32
+
33
+ it('parses urlencoded body', async () => {
34
+ const req = new MockRequest(
35
+ 'POST',
36
+ { 'content-type': 'application/x-www-form-urlencoded' },
37
+ 'foo=bar&baz=123'
38
+ );
39
+
40
+ const parsed = await BodyParser.parse(req);
41
+ expect(parsed.body).toEqual({ foo: 'bar', baz: '123' });
42
+ });
43
+
44
+ it('returns buffer for unknown content type', async () => {
45
+ const req = new MockRequest(
46
+ 'POST',
47
+ { 'content-type': 'application/octet-stream' },
48
+ 'hello world'
49
+ );
50
+
51
+ const parsed = await BodyParser.parse(req);
52
+ expect(parsed.body).toBeInstanceOf(Buffer);
53
+ expect((parsed.body as Buffer).toString()).toBe('hello world');
54
+ });
55
+
56
+ it('throws error when body too large', async () => {
57
+ const req = new MockRequest(
58
+ 'POST',
59
+ { 'content-type': 'text/plain' },
60
+ '1234567890'
61
+ );
62
+
63
+ await expect(
64
+ BodyParser.parse(req, { maxBodyBytes: 5 })
65
+ ).rejects.toThrow('QHTTPX_BODY_TOO_LARGE');
66
+ });
67
+
68
+ it('throws error for invalid JSON', async () => {
69
+ const req = new MockRequest(
70
+ 'POST',
71
+ { 'content-type': 'application/json' },
72
+ '{ invalid json'
73
+ );
74
+
75
+ await expect(BodyParser.parse(req)).rejects.toThrow('QHTTPX_INVALID_JSON');
76
+ });
77
+
78
+ it('ignores GET requests', async () => {
79
+ const req = new MockRequest('GET', {});
80
+ const parsed = await BodyParser.parse(req);
81
+ expect(parsed.body).toBeUndefined();
82
+ });
83
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { QHTTPX, createCompressionMiddleware, createSSE } from '../src';
3
+ import zlib from 'zlib';
4
+ import http from 'http';
5
+
6
+ describe('Compression Middleware', () => {
7
+ it('should compress JSON response with gzip', async () => {
8
+ const app = new QHTTPX();
9
+ app.use(createCompressionMiddleware());
10
+ const bigData = { data: 'a'.repeat(10000) };
11
+ app.get('/gzip', (ctx) => ctx.json(bigData));
12
+ const { port } = await app.listen(0, '127.0.0.1');
13
+
14
+ // Use http.request to avoid automatic decompression by fetch
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ const response = await new Promise<{ headers: any, data: Buffer }>((resolve, reject) => {
17
+ const req = http.request(`http://127.0.0.1:${port}/gzip`, {
18
+ headers: { 'Accept-Encoding': 'gzip' }
19
+ }, (res) => {
20
+ const chunks: Buffer[] = [];
21
+ res.on('data', (chunk) => chunks.push(chunk));
22
+ res.on('end', () => resolve({ headers: res.headers, data: Buffer.concat(chunks) }));
23
+ res.on('error', reject);
24
+ });
25
+ req.end();
26
+ });
27
+
28
+ expect(response.headers['content-encoding']).toBe('gzip');
29
+ expect(response.headers['vary']).toContain('Accept-Encoding');
30
+
31
+ const decompressed = zlib.gunzipSync(response.data);
32
+ const json = JSON.parse(decompressed.toString());
33
+
34
+ expect(json).toEqual(bigData);
35
+ await app.close();
36
+ });
37
+
38
+ it('should not compress small response if threshold is high', async () => {
39
+ const app = new QHTTPX();
40
+ app.use(createCompressionMiddleware({ threshold: 1000 }));
41
+
42
+ app.get('/small', (ctx) => {
43
+ ctx.json({ data: 'small' });
44
+ });
45
+
46
+ const { port } = await app.listen(0, '127.0.0.1');
47
+
48
+ const response = await fetch(`http://127.0.0.1:${port}/small`, {
49
+ headers: { 'Accept-Encoding': 'gzip' },
50
+ });
51
+
52
+ expect(response.status).toBe(200);
53
+ // Should NOT have content-encoding because it's too small
54
+ expect(response.headers.get('content-encoding')).toBeNull();
55
+
56
+ await app.close();
57
+ });
58
+ });
59
+
60
+ describe('Server-Sent Events (SSE)', () => {
61
+ it('should stream events', async () => {
62
+ const app = new QHTTPX();
63
+ // app.use(createCompressionMiddleware()); // Disable compression for SSE test to debug
64
+ app.get('/sse', (ctx) => {
65
+ const stream = createSSE(ctx);
66
+ let count = 0;
67
+ const interval = setInterval(() => {
68
+ count++;
69
+ stream.send({ count }, 'ping');
70
+ if (count >= 3) {
71
+ clearInterval(interval);
72
+ stream.close();
73
+ }
74
+ }, 50);
75
+ });
76
+
77
+ const { port } = await app.listen(0, '127.0.0.1');
78
+
79
+ const response = await new Promise<string>((resolve, reject) => {
80
+ const req = http.request(`http://127.0.0.1:${port}/sse`, (res) => {
81
+ let data = '';
82
+ res.on('data', (chunk) => {
83
+ data += chunk.toString();
84
+ });
85
+ res.on('end', () => resolve(data));
86
+ res.on('error', reject);
87
+ });
88
+ req.end();
89
+ });
90
+
91
+ expect(response).toContain('event: ping');
92
+ expect(response).toContain('data: {"count":1}');
93
+ expect(response).toContain('data: {"count":2}');
94
+ expect(response).toContain('data: {"count":3}');
95
+
96
+ await app.close();
97
+ });
98
+ });
@@ -0,0 +1,74 @@
1
+
2
+ import { describe, it, expect } from 'vitest';
3
+ import { parseCookies, serializeCookie } from '../src/utils/cookies';
4
+
5
+ describe('Cookie Parsing', () => {
6
+ it('should parse simple cookies', () => {
7
+ const header = 'foo=bar; baz=qux';
8
+ const result = parseCookies(header);
9
+ expect(result).toEqual({ foo: 'bar', baz: 'qux' });
10
+ });
11
+
12
+ it('should handle quoted values', () => {
13
+ const header = 'foo="bar"; baz=qux';
14
+ // Basic implementation might not strip quotes if not decoding specifically for that,
15
+ // but let's see standard behavior. My implementation just decodes URI component.
16
+ // 'foo="bar"' split by '=' -> ['foo', '"bar"']. decodeURIComponent('"bar"') -> '"bar"'.
17
+ const result = parseCookies(header);
18
+ expect(result).toEqual({ foo: '"bar"', baz: 'qux' });
19
+ });
20
+
21
+ it('should handle URL encoded values', () => {
22
+ const header = 'foo=Hello%20World';
23
+ const result = parseCookies(header);
24
+ expect(result).toEqual({ foo: 'Hello World' });
25
+ });
26
+
27
+ it('should handle empty header', () => {
28
+ expect(parseCookies(undefined)).toEqual({});
29
+ expect(parseCookies('')).toEqual({});
30
+ });
31
+
32
+ it('should ignore cookies without name', () => {
33
+ const header = '=bar; baz=qux';
34
+ const result = parseCookies(header);
35
+ expect(result).toEqual({ baz: 'qux' });
36
+ });
37
+ });
38
+
39
+ describe('Cookie Serialization', () => {
40
+ it('should serialize name and value', () => {
41
+ const result = serializeCookie('foo', 'bar');
42
+ expect(result).toBe('foo=bar; Path=/');
43
+ });
44
+
45
+ it('should encode value', () => {
46
+ const result = serializeCookie('foo', 'Hello World');
47
+ expect(result).toBe('foo=Hello%20World; Path=/');
48
+ });
49
+
50
+ it('should handle Max-Age', () => {
51
+ const result = serializeCookie('foo', 'bar', { maxAge: 3600 });
52
+ expect(result).toContain('Max-Age=3600');
53
+ });
54
+
55
+ it('should handle HttpOnly', () => {
56
+ const result = serializeCookie('foo', 'bar', { httpOnly: true });
57
+ expect(result).toContain('HttpOnly');
58
+ });
59
+
60
+ it('should handle Secure', () => {
61
+ const result = serializeCookie('foo', 'bar', { secure: true });
62
+ expect(result).toContain('Secure');
63
+ });
64
+
65
+ it('should handle SameSite', () => {
66
+ const result = serializeCookie('foo', 'bar', { sameSite: 'strict' });
67
+ expect(result).toContain('SameSite=Strict');
68
+ });
69
+
70
+ it('should handle Path', () => {
71
+ const result = serializeCookie('foo', 'bar', { path: '/admin' });
72
+ expect(result).toContain('Path=/admin');
73
+ });
74
+ });
@@ -0,0 +1,79 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { QHTTPX, createCorsMiddleware } from '../src';
3
+
4
+ describe('CORS middleware', () => {
5
+ let app: QHTTPX | undefined;
6
+
7
+ afterEach(async () => {
8
+ if (app) {
9
+ await app.close();
10
+ app = undefined;
11
+ }
12
+ });
13
+
14
+ it('sets default CORS headers and passes through GET', async () => {
15
+ app = new QHTTPX();
16
+
17
+ app.use(createCorsMiddleware());
18
+
19
+ app.get('/ping', (ctx) => {
20
+ ctx.send('pong');
21
+ });
22
+
23
+ const { port } = await app.listen(0, '127.0.0.1');
24
+
25
+ const response = await fetch(`http://127.0.0.1:${port}/ping`, {
26
+ headers: {
27
+ origin: 'http://example.com',
28
+ },
29
+ });
30
+
31
+ expect(response.status).toBe(200);
32
+ expect(await response.text()).toBe('pong');
33
+ expect(response.headers.get('access-control-allow-origin')).toBe('*');
34
+ });
35
+
36
+ it('handles preflight OPTIONS requests', async () => {
37
+ app = new QHTTPX();
38
+
39
+ app.use(
40
+ createCorsMiddleware({
41
+ origin: ['http://example.com'],
42
+ methods: ['GET', 'POST'],
43
+ allowedHeaders: ['content-type'],
44
+ maxAgeSeconds: 600,
45
+ }),
46
+ );
47
+
48
+ app.get('/ping', (ctx) => {
49
+ ctx.send('pong');
50
+ });
51
+
52
+ const { port } = await app.listen(0, '127.0.0.1');
53
+
54
+ const response = await fetch(
55
+ `http://127.0.0.1:${port}/ping`,
56
+ {
57
+ method: 'OPTIONS',
58
+ headers: {
59
+ origin: 'http://example.com',
60
+ 'access-control-request-method': 'GET',
61
+ 'access-control-request-headers': 'content-type',
62
+ },
63
+ },
64
+ );
65
+
66
+ expect(response.status).toBe(204);
67
+ expect(response.headers.get('access-control-allow-origin')).toBe(
68
+ 'http://example.com',
69
+ );
70
+ expect(response.headers.get('access-control-allow-methods')).toBe(
71
+ 'GET, POST',
72
+ );
73
+ expect(response.headers.get('access-control-allow-headers')).toBe(
74
+ 'content-type',
75
+ );
76
+ expect(response.headers.get('access-control-max-age')).toBe('600');
77
+ });
78
+ });
79
+
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { DatabaseManager } from '../src/database/manager';
3
+ import { MemoryAdapter } from '../src/database/adapters/memory';
4
+
5
+ describe('Database Engine', () => {
6
+ let dbManager: DatabaseManager;
7
+
8
+ beforeEach(() => {
9
+ // Register the adapter
10
+ DatabaseManager.registerAdapter('memory', MemoryAdapter);
11
+
12
+ // Initialize manager
13
+ dbManager = new DatabaseManager({
14
+ default: 'main',
15
+ connections: {
16
+ main: {
17
+ type: 'memory',
18
+ database: 'test_db'
19
+ }
20
+ }
21
+ });
22
+ });
23
+
24
+ afterEach(async () => {
25
+ await dbManager.disconnect();
26
+ });
27
+
28
+ it('should connect to the default database', async () => {
29
+ const adapter = await dbManager.connect();
30
+ expect(adapter).toBeDefined();
31
+ expect(adapter.isConnected()).toBe(true);
32
+ });
33
+
34
+ it('should perform CRUD operations with MemoryAdapter', async () => {
35
+ const adapter = await dbManager.connect();
36
+
37
+ // Insert
38
+ const user = await adapter.query({
39
+ collection: 'users',
40
+ action: 'insert',
41
+ data: { name: 'John Doe', email: 'john@example.com' }
42
+ });
43
+ expect(user).toHaveProperty('id');
44
+ expect(user.name).toBe('John Doe');
45
+
46
+ // Find
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ const users = await adapter.query<any[]>({
49
+ collection: 'users',
50
+ action: 'find',
51
+ filter: { name: 'John Doe' }
52
+ });
53
+ expect(users).toHaveLength(1);
54
+ expect(users[0].email).toBe('john@example.com');
55
+
56
+ // Update
57
+ const updateCount = await adapter.query({
58
+ collection: 'users',
59
+ action: 'update',
60
+ filter: { name: 'John Doe' },
61
+ data: { name: 'John Updated' }
62
+ });
63
+ expect(updateCount).toBe(1);
64
+
65
+ // Verify Update
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ const updatedUsers = await adapter.query<any[]>({
68
+ collection: 'users',
69
+ action: 'find',
70
+ filter: { name: 'John Updated' }
71
+ });
72
+ expect(updatedUsers).toHaveLength(1);
73
+
74
+ // Delete
75
+ const deleteCount = await adapter.query({
76
+ collection: 'users',
77
+ action: 'delete',
78
+ filter: { name: 'John Updated' }
79
+ });
80
+ expect(deleteCount).toBe(1);
81
+
82
+ // Verify Delete
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ const remainingUsers = await adapter.query<any[]>({
85
+ collection: 'users',
86
+ action: 'find'
87
+ });
88
+ expect(remainingUsers).toHaveLength(0);
89
+ });
90
+ });