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,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
|
+
}
|