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,99 @@
1
+ import { DatabaseAdapter, DatabaseConfig } from '../types';
2
+
3
+ export class MemoryAdapter implements DatabaseAdapter {
4
+ private connected: boolean = false;
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ private collections: Map<string, any[]> = new Map();
7
+
8
+ constructor(private config: DatabaseConfig) {}
9
+
10
+ async connect(): Promise<void> {
11
+ // Simulate connection delay
12
+ await new Promise(resolve => setTimeout(resolve, 100));
13
+ this.connected = true;
14
+ }
15
+
16
+ async disconnect(): Promise<void> {
17
+ this.connected = false;
18
+ this.collections.clear();
19
+ }
20
+
21
+ isConnected(): boolean {
22
+ return this.connected;
23
+ }
24
+
25
+ /**
26
+ * Execute a query against the in-memory database
27
+ * @param query Object specifying collection, action, and parameters
28
+ * @example
29
+ * // Find
30
+ * query({ collection: 'users', action: 'find', filter: { id: 1 } })
31
+ * // Insert
32
+ * query({ collection: 'users', action: 'insert', data: { name: 'Alice' } })
33
+ */
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ async query<T = any>(query: any): Promise<T> {
36
+ if (!this.connected) {
37
+ throw new Error('Database not connected');
38
+ }
39
+
40
+ const { collection, action, data, filter } = query;
41
+
42
+ if (!collection || !action) {
43
+ throw new Error('Query must specify collection and action');
44
+ }
45
+
46
+ if (!this.collections.has(collection)) {
47
+ this.collections.set(collection, []);
48
+ }
49
+
50
+ const store = this.collections.get(collection)!;
51
+
52
+ switch (action) {
53
+ case 'find':
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ if (!filter) return store as any;
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ return store.filter(item => this.matches(item, filter)) as any;
58
+
59
+ case 'findOne':
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ if (!filter) return store[0] as any;
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ return store.find(item => this.matches(item, filter)) as any;
64
+
65
+ case 'insert':
66
+ const newItem = { id: Date.now(), ...data };
67
+ store.push(newItem);
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ return newItem as any;
70
+
71
+ case 'update':
72
+ let updatedCount = 0;
73
+ store.forEach((item, index) => {
74
+ if (this.matches(item, filter)) {
75
+ store[index] = { ...item, ...data };
76
+ updatedCount++;
77
+ }
78
+ });
79
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
+ return updatedCount as any;
81
+
82
+ case 'delete':
83
+ const initialLength = store.length;
84
+ const newStore = store.filter(item => !this.matches(item, filter));
85
+ this.collections.set(collection, newStore);
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ return (initialLength - newStore.length) as any;
88
+
89
+ default:
90
+ throw new Error(`Unknown action: ${action}`);
91
+ }
92
+ }
93
+
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ private matches(item: any, filter: any): boolean {
96
+ if (!filter) return true;
97
+ return Object.entries(filter).every(([key, value]) => item[key] === value);
98
+ }
99
+ }
@@ -0,0 +1,116 @@
1
+ import { DatabaseAdapter, DatabaseConfig } from '../types';
2
+
3
+ export class MongoAdapter implements DatabaseAdapter {
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ private client: any = null;
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ private db: any = null;
8
+ private config: DatabaseConfig;
9
+
10
+ constructor(config: DatabaseConfig) {
11
+ this.config = config;
12
+ }
13
+
14
+ async connect(): Promise<void> {
15
+ if (this.client) return;
16
+
17
+ try {
18
+ const mongodb = await import('mongodb');
19
+ const { MongoClient } = mongodb.default || mongodb;
20
+
21
+ const url = this.config.url || `mongodb://${this.config.host || 'localhost'}:${this.config.port || 27017}`;
22
+
23
+ this.client = new MongoClient(url, {
24
+ auth: this.config.username ? {
25
+ username: this.config.username,
26
+ password: this.config.password
27
+ } : undefined,
28
+ ...this.config.options
29
+ });
30
+
31
+ await this.client.connect();
32
+ this.db = this.client.db(this.config.database);
33
+ } catch (err) {
34
+ if ((err as Error).message.includes('Cannot find module')) {
35
+ throw new Error('MongoDB driver not found. Please run: npm install mongodb');
36
+ }
37
+ throw err;
38
+ }
39
+ }
40
+
41
+ async disconnect(): Promise<void> {
42
+ if (this.client) {
43
+ await this.client.close();
44
+ this.client = null;
45
+ this.db = null;
46
+ }
47
+ }
48
+
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ async query<T = any>(query: string | object, params?: any[]): Promise<T> {
51
+ if (!this.db) {
52
+ throw new Error('Database not connected');
53
+ }
54
+
55
+ // MongoDB "query" is usually a collection.find() or similar.
56
+ // We need a convention here.
57
+ // Convention: query is a JSON object with { collection: 'name', action: 'find', filter: {}, ... }
58
+ // OR: query is just the collection name, and params[0] is the operation?
59
+ // Let's go with a structured object for maximum flexibility if passed as object.
60
+
61
+ // If query is string, maybe it's just collection name?
62
+ // Let's support a simple object-based API:
63
+ // db.query({ collection: 'users', action: 'find', filter: { id: 1 } })
64
+
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ let op: any = query;
67
+ if (typeof query === 'string') {
68
+ // Fallback or simple format?
69
+ // Maybe "users.find" ?
70
+ const parts = query.split('.');
71
+ if (parts.length >= 2) {
72
+ op = {
73
+ collection: parts[0],
74
+ action: parts[1],
75
+ filter: params?.[0]
76
+ };
77
+ } else {
78
+ throw new Error('Invalid MongoDB query format. Use object or "collection.action" string.');
79
+ }
80
+ }
81
+
82
+ const collection = this.db.collection(op.collection);
83
+
84
+ switch (op.action) {
85
+ case 'find':
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ const cursor: any = collection.find(op.filter || {});
88
+ if (op.limit) cursor.limit(op.limit);
89
+ if (op.skip) cursor.skip(op.skip);
90
+ return (await cursor.toArray()) as unknown as T;
91
+ case 'findOne':
92
+ return (await collection.findOne(op.filter || {})) as unknown as T;
93
+ case 'insertOne':
94
+ return (await collection.insertOne(op.doc || params?.[0])) as unknown as T;
95
+ case 'insertMany':
96
+ return (await collection.insertMany(op.docs || params?.[0])) as unknown as T;
97
+ case 'updateOne':
98
+ return (await collection.updateOne(op.filter, op.update || params?.[1])) as unknown as T;
99
+ case 'updateMany':
100
+ return (await collection.updateMany(op.filter, op.update || params?.[1])) as unknown as T;
101
+ case 'deleteOne':
102
+ return (await collection.deleteOne(op.filter)) as unknown as T;
103
+ case 'deleteMany':
104
+ return (await collection.deleteMany(op.filter)) as unknown as T;
105
+ case 'aggregate':
106
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
+ return (await collection.aggregate((op.pipeline || params) as any).toArray()) as unknown as T;
108
+ default:
109
+ throw new Error(`Unsupported MongoDB action: ${op.action}`);
110
+ }
111
+ }
112
+
113
+ isConnected(): boolean {
114
+ return this.client !== null;
115
+ }
116
+ }
@@ -0,0 +1,86 @@
1
+ import { DatabaseAdapter, DatabaseConfig } from '../types';
2
+
3
+ export class PostgresAdapter implements DatabaseAdapter {
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ private pool: any = null;
6
+ private config: DatabaseConfig;
7
+
8
+ constructor(config: DatabaseConfig) {
9
+ this.config = config;
10
+ }
11
+
12
+ async connect(): Promise<void> {
13
+ if (this.pool) return;
14
+
15
+ try {
16
+ // Dynamic import
17
+ const pg = await import('pg');
18
+ const { Pool } = pg.default || pg;
19
+
20
+ this.pool = new Pool({
21
+ host: this.config.host,
22
+ port: this.config.port || 5432,
23
+ user: this.config.username,
24
+ password: this.config.password,
25
+ database: this.config.database,
26
+ // Postgres-specific options can be passed via config.options
27
+ ...this.config.options
28
+ });
29
+
30
+ // Verify connection
31
+ const client = await this.pool.connect();
32
+ client.release();
33
+ } catch (err) {
34
+ if ((err as Error).message.includes('Cannot find module')) {
35
+ throw new Error('PostgreSQL driver not found. Please run: npm install pg');
36
+ }
37
+ throw err;
38
+ }
39
+ }
40
+
41
+ async disconnect(): Promise<void> {
42
+ if (this.pool) {
43
+ await this.pool.end();
44
+ this.pool = null;
45
+ }
46
+ }
47
+
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ async query<T = any>(sql: string, params?: any[]): Promise<T> {
50
+ if (!this.pool) {
51
+ throw new Error('Database not connected');
52
+ }
53
+
54
+ try {
55
+ // pg supports parameterized queries using $1, $2, etc.
56
+ // But our Coalescer might be sending '?' if it's generic.
57
+ // If the user writes 'SELECT * FROM users WHERE id = $1', that's fine.
58
+ // If they write '?', we might need to convert it?
59
+ // For now, we assume the user provides SQL compatible with the underlying driver.
60
+ // Or we could implement a simple '?' -> '$n' converter if we want to standardize.
61
+ // Standardizing is better for the generic "engine" feel.
62
+
63
+ if (sql.includes('?')) {
64
+ let i = 1;
65
+ sql = sql.replace(/\?/g, () => `$${i++}`);
66
+ }
67
+
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ const result: any = await this.pool.query(sql, params);
70
+
71
+ // Return rows for SELECT, or result meta for others?
72
+ // Our interface implies T.
73
+ // Usually users expect rows array for SELECT.
74
+ if (result.command === 'SELECT' || result.command === 'INSERT' && result.rows.length > 0) {
75
+ return result.rows as unknown as T;
76
+ }
77
+ return result as unknown as T;
78
+ } catch (err) {
79
+ throw err;
80
+ }
81
+ }
82
+
83
+ isConnected(): boolean {
84
+ return this.pool !== null && !this.pool.ended;
85
+ }
86
+ }
@@ -0,0 +1,44 @@
1
+ import Database from 'better-sqlite3';
2
+ import { DatabaseAdapter, DatabaseConfig } from '../types';
3
+
4
+ export class SQLiteAdapter implements DatabaseAdapter {
5
+ private db: Database.Database | null = null;
6
+ private config: DatabaseConfig;
7
+
8
+ constructor(config: DatabaseConfig) {
9
+ this.config = config;
10
+ }
11
+
12
+ async connect(): Promise<void> {
13
+ const filename = this.config.database || ':memory:';
14
+ this.db = new Database(filename, this.config.options);
15
+ }
16
+
17
+ async disconnect(): Promise<void> {
18
+ if (this.db) {
19
+ this.db.close();
20
+ this.db = null;
21
+ }
22
+ }
23
+
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ async query<T = any>(sql: string, params?: any[]): Promise<T> {
26
+ if (!this.db) {
27
+ throw new Error('Database not connected');
28
+ }
29
+
30
+ const stmt = this.db.prepare(sql);
31
+
32
+ // better-sqlite3 handles '?' params automatically
33
+ // Determine if it's a read or write operation
34
+ if (sql.trim().toLowerCase().startsWith('select')) {
35
+ return stmt.all(params || []) as T;
36
+ } else {
37
+ return stmt.run(params || []) as unknown as T;
38
+ }
39
+ }
40
+
41
+ isConnected(): boolean {
42
+ return this.db !== null && this.db.open;
43
+ }
44
+ }
@@ -0,0 +1,153 @@
1
+ import { DatabaseAdapter } from './types';
2
+
3
+ interface PendingQuery<T> {
4
+ query: string;
5
+ paramValue: unknown;
6
+ resolve: (value: T) => void;
7
+ reject: (reason?: unknown) => void;
8
+ }
9
+ // eslint-enable @typescript-eslint/no-explicit-any
10
+
11
+ export class QueryCoalescer<T = unknown> {
12
+ private pending: Map<string, PendingQuery<T>[]> = new Map();
13
+ private timeout: NodeJS.Timeout | null = null;
14
+ private readonly adapter: DatabaseAdapter;
15
+
16
+ constructor(adapter: DatabaseAdapter) {
17
+ this.adapter = adapter;
18
+ }
19
+
20
+ /**
21
+ * Intercepts a query and attempts to coalesce it with others.
22
+ * Only supports simple queries of the form "SELECT ... WHERE col = ?" for now.
23
+ */
24
+ public async query(query: string, params?: unknown[]): Promise<T> {
25
+ // Basic check for coalescing eligibility:
26
+ // 1. Must have exactly one parameter (for simplicity in this v1)
27
+ // 2. Must contain " = ?" pattern
28
+ if (!params || params.length !== 1 || !query.includes(' = ?')) {
29
+ return this.adapter.query(query, params);
30
+ }
31
+
32
+ return new Promise<T>((resolve, reject) => {
33
+ if (!this.pending.has(query)) {
34
+ this.pending.set(query, []);
35
+ }
36
+
37
+ this.pending.get(query)!.push({
38
+ query,
39
+ paramValue: params[0],
40
+ resolve: resolve as (value: T) => void,
41
+ reject
42
+ });
43
+
44
+ this.scheduleFlush();
45
+ });
46
+ }
47
+
48
+ private scheduleFlush() {
49
+ if (this.timeout) return;
50
+ // Use microtask or short timeout to gather queries from the current event loop tick
51
+ this.timeout = setTimeout(() => this.flush(), 0);
52
+ }
53
+
54
+ private async flush() {
55
+ this.timeout = null;
56
+ const currentBatch = this.pending;
57
+ this.pending = new Map(); // Clear for next batch
58
+
59
+ for (const [originalQuery, items] of currentBatch.entries()) {
60
+ if (items.length === 1) {
61
+ // No fusion needed
62
+ const item = items[0];
63
+ try {
64
+ const result = await this.adapter.query(item.query, [item.paramValue]);
65
+ item.resolve(result);
66
+ } catch (err) {
67
+ item.reject(err);
68
+ }
69
+ continue;
70
+ }
71
+
72
+ // Fuse queries
73
+ // Transform "SELECT * FROM table WHERE id = ?"
74
+ // into "SELECT * FROM table WHERE id IN (?, ?, ...)"
75
+
76
+ // Basic string manipulation (safe enough for this controlled feature)
77
+ // We assume the query ends with " = ?" or contains it clearly.
78
+ const fusedQuery = originalQuery.replace(' = ?', ` IN (${items.map(() => '?').join(', ')})`);
79
+ const allParams = items.map(i => i.paramValue);
80
+
81
+ try {
82
+ const results = await this.adapter.query<unknown[]>(fusedQuery, allParams);
83
+
84
+ // Distribute results back to callers
85
+ // We need to map results back to params.
86
+ // This is the tricky part: standard SQL doesn't guarantee order matching input IN clause.
87
+ // We must assume the result objects contain the key used in WHERE.
88
+
89
+ // Extract the column name from the query
90
+ // "WHERE id = ?" -> "id"
91
+ const match = originalQuery.match(/WHERE\s+(\w+)\s*=\s*\?/i);
92
+ if (!match) {
93
+ // Fallback if we can't parse: execute individually (should not happen given check above)
94
+ await Promise.all(items.map(async (item) => {
95
+ try {
96
+ const r = await this.adapter.query(item.query, [item.paramValue]);
97
+ item.resolve(r);
98
+ } catch (e) {
99
+ item.reject(e);
100
+ }
101
+ }));
102
+ continue;
103
+ }
104
+
105
+ const keyColumn = match[1];
106
+
107
+ // Map results by key
108
+ // Note: This assumes the result is an array of objects
109
+ // If the adapter returns something else, this might fail.
110
+ if (!Array.isArray(results)) {
111
+ // Fallback for non-array results
112
+ // Just give everyone the full result? No, that's wrong.
113
+ // If it's not an array, we probably can't split it.
114
+ // Reject? Or execute individually?
115
+ // Let's execute individually as fallback.
116
+ await Promise.all(items.map(async (item) => {
117
+ try {
118
+ const r = await this.adapter.query(item.query, [item.paramValue]);
119
+ item.resolve(r);
120
+ } catch (e) {
121
+ item.reject(e);
122
+ }
123
+ }));
124
+ continue;
125
+ }
126
+
127
+ const resultMap = new Map<string | number, T[]>();
128
+ results.forEach(row => {
129
+ if (typeof row === 'object' && row !== null && keyColumn in row) {
130
+ const typedRow = row as Record<string, unknown>;
131
+ const key = typedRow[keyColumn];
132
+ if (typeof key === 'string' || typeof key === 'number') {
133
+ if (!resultMap.has(key)) {
134
+ resultMap.set(key, []);
135
+ }
136
+ resultMap.get(key)!.push(row as T);
137
+ }
138
+ }
139
+ });
140
+
141
+ items.forEach(item => {
142
+ const key = typeof item.paramValue === 'string' || typeof item.paramValue === 'number' ? item.paramValue : undefined;
143
+ const res = key !== undefined ? resultMap.get(key) || [] : [];
144
+ item.resolve(res as T);
145
+ });
146
+
147
+ } catch (err) {
148
+ // Fail all
149
+ items.forEach(item => item.reject(err));
150
+ }
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,97 @@
1
+ import { DatabaseAdapter, DatabaseConfig, DatabaseEngineOptions } from './types';
2
+
3
+ export type AdapterConstructor = new (config: DatabaseConfig) => DatabaseAdapter;
4
+
5
+ export class DatabaseManager {
6
+ private static adapterRegistry: Map<string, AdapterConstructor> = new Map();
7
+ private connections: Map<string, DatabaseAdapter> = new Map();
8
+
9
+ constructor(private config: DatabaseEngineOptions) {}
10
+
11
+ /**
12
+ * Manually register an already initialized adapter instance
13
+ * @param name The connection name
14
+ * @param adapter The initialized adapter instance
15
+ */
16
+ public registerConnection(name: string, adapter: DatabaseAdapter) {
17
+ this.connections.set(name, adapter);
18
+ }
19
+
20
+ /**
21
+ * Register a new database adapter type
22
+ * @param type The type identifier (e.g., 'postgres', 'mysql', 'mongo')
23
+ * @param adapter The adapter class
24
+ */
25
+ public static registerAdapter(type: string, adapter: AdapterConstructor) {
26
+ DatabaseManager.adapterRegistry.set(type, adapter);
27
+ }
28
+
29
+ /**
30
+ * Connect to a specific database or the default one
31
+ * @param name Connection name from config
32
+ */
33
+ public async connect(name?: string): Promise<DatabaseAdapter> {
34
+ const connectionName = name || this.config.default;
35
+ if (!connectionName) {
36
+ throw new Error('No connection name provided and no default connection configured.');
37
+ }
38
+
39
+ // Return existing connection if available and connected
40
+ const existingConnection = this.connections.get(connectionName);
41
+ if (existingConnection && existingConnection.isConnected()) {
42
+ return existingConnection;
43
+ }
44
+
45
+ const dbConfig = this.config.connections[connectionName];
46
+ if (!dbConfig) {
47
+ throw new Error(`Connection configuration for '${connectionName}' not found.`);
48
+ }
49
+
50
+ const AdapterClass = DatabaseManager.adapterRegistry.get(dbConfig.type);
51
+ if (!AdapterClass) {
52
+ throw new Error(`No adapter registered for database type '${dbConfig.type}'.`);
53
+ }
54
+
55
+ const adapter = new AdapterClass(dbConfig);
56
+ await adapter.connect();
57
+ this.connections.set(connectionName, adapter);
58
+
59
+ return adapter;
60
+ }
61
+
62
+ /**
63
+ * Disconnect a specific connection or all connections
64
+ * @param name Connection name (optional). If not provided, disconnects all.
65
+ */
66
+ public async disconnect(name?: string): Promise<void> {
67
+ if (name) {
68
+ const adapter = this.connections.get(name);
69
+ if (adapter) {
70
+ await adapter.disconnect();
71
+ this.connections.delete(name);
72
+ }
73
+ } else {
74
+ const promises = Array.from(this.connections.values()).map(adapter => adapter.disconnect());
75
+ await Promise.all(promises);
76
+ this.connections.clear();
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get an active connection
82
+ * @param name Connection name
83
+ */
84
+ public get(name?: string): DatabaseAdapter {
85
+ const connectionName = name || this.config.default;
86
+ if (!connectionName) {
87
+ throw new Error('No connection name provided and no default connection configured.');
88
+ }
89
+
90
+ const adapter = this.connections.get(connectionName);
91
+ if (!adapter) {
92
+ throw new Error(`Connection '${connectionName}' is not active. Call connect() first.`);
93
+ }
94
+
95
+ return adapter;
96
+ }
97
+ }
@@ -0,0 +1,24 @@
1
+ export interface DatabaseConfig {
2
+ type: string;
3
+ url?: string;
4
+ host?: string;
5
+ port?: number;
6
+ username?: string;
7
+ password?: string;
8
+ database?: string;
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ options?: Record<string, any>;
11
+ }
12
+
13
+ export interface DatabaseAdapter {
14
+ connect(): Promise<void>;
15
+ disconnect(): Promise<void>;
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ query<T = any>(query: string | object, params?: any[]): Promise<T>;
18
+ isConnected(): boolean;
19
+ }
20
+
21
+ export interface DatabaseEngineOptions {
22
+ default?: string;
23
+ connections: Record<string, DatabaseConfig>;
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { QHTTPX } from './core/server';
2
+ import type { QHTTPXOptions } from './core/types';
3
+ import { createApiPreset } from './middleware/presets';
4
+
5
+ export { QHTTPX } from './core/server';
6
+ export * from './core/types';
7
+ export * from './middleware/cors';
8
+ export * from './middleware/security';
9
+ export * from './middleware/static';
10
+ export * from './middleware/compression';
11
+ export * from './core/stream';
12
+ export * from './utils/logger';
13
+ export { BufferPool, BufferPoolConfig } from './core/buffer-pool';
14
+ export * from './testing';
15
+ export * from './utils/signals';
16
+ export * from './middleware/presets';
17
+ export * from './utils/cookies';
18
+ export * from './utils/sse';
19
+ export { fastJsonStringify, getStringifier } from './core/serializer';
20
+ export * from './database/types';
21
+ export * from './database/manager';
22
+ export * from './database/adapters/memory';
23
+ export * from './views';
24
+ export * from './validation';
25
+ export * from './database/adapters/sqlite';
26
+ export * from './database/adapters/postgres';
27
+ export * from './database/adapters/mongo';
28
+ export * from './core/fusion';
29
+ export * from './validation/types';
30
+ export * from './validation/simple';
31
+ export * from './openapi/generator';
32
+ export * from './client';
33
+
34
+ export function createHttpApp(options: QHTTPXOptions = {}): QHTTPX {
35
+ const app = new QHTTPX(options);
36
+ // Skip middleware in ultra mode for maximum performance
37
+ if (options.performanceMode !== 'ultra') {
38
+ const middlewares = createApiPreset();
39
+ middlewares.forEach((mw) => app.use(mw));
40
+ }
41
+ return app;
42
+ }