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,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SQLiteAdapter = void 0;
|
|
7
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
8
|
+
class SQLiteAdapter {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.db = null;
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
async connect() {
|
|
14
|
+
const filename = this.config.database || ':memory:';
|
|
15
|
+
this.db = new better_sqlite3_1.default(filename, this.config.options);
|
|
16
|
+
}
|
|
17
|
+
async disconnect() {
|
|
18
|
+
if (this.db) {
|
|
19
|
+
this.db.close();
|
|
20
|
+
this.db = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
async query(sql, params) {
|
|
25
|
+
if (!this.db) {
|
|
26
|
+
throw new Error('Database not connected');
|
|
27
|
+
}
|
|
28
|
+
const stmt = this.db.prepare(sql);
|
|
29
|
+
// better-sqlite3 handles '?' params automatically
|
|
30
|
+
// Determine if it's a read or write operation
|
|
31
|
+
if (sql.trim().toLowerCase().startsWith('select')) {
|
|
32
|
+
return stmt.all(params || []);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
return stmt.run(params || []);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
isConnected() {
|
|
39
|
+
return this.db !== null && this.db.open;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.SQLiteAdapter = SQLiteAdapter;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QueryCoalescer = void 0;
|
|
4
|
+
// eslint-enable @typescript-eslint/no-explicit-any
|
|
5
|
+
class QueryCoalescer {
|
|
6
|
+
constructor(adapter) {
|
|
7
|
+
this.pending = new Map();
|
|
8
|
+
this.timeout = null;
|
|
9
|
+
this.adapter = adapter;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Intercepts a query and attempts to coalesce it with others.
|
|
13
|
+
* Only supports simple queries of the form "SELECT ... WHERE col = ?" for now.
|
|
14
|
+
*/
|
|
15
|
+
async query(query, params) {
|
|
16
|
+
// Basic check for coalescing eligibility:
|
|
17
|
+
// 1. Must have exactly one parameter (for simplicity in this v1)
|
|
18
|
+
// 2. Must contain " = ?" pattern
|
|
19
|
+
if (!params || params.length !== 1 || !query.includes(' = ?')) {
|
|
20
|
+
return this.adapter.query(query, params);
|
|
21
|
+
}
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
if (!this.pending.has(query)) {
|
|
24
|
+
this.pending.set(query, []);
|
|
25
|
+
}
|
|
26
|
+
this.pending.get(query).push({
|
|
27
|
+
query,
|
|
28
|
+
paramValue: params[0],
|
|
29
|
+
resolve: resolve,
|
|
30
|
+
reject
|
|
31
|
+
});
|
|
32
|
+
this.scheduleFlush();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
scheduleFlush() {
|
|
36
|
+
if (this.timeout)
|
|
37
|
+
return;
|
|
38
|
+
// Use microtask or short timeout to gather queries from the current event loop tick
|
|
39
|
+
this.timeout = setTimeout(() => this.flush(), 0);
|
|
40
|
+
}
|
|
41
|
+
async flush() {
|
|
42
|
+
this.timeout = null;
|
|
43
|
+
const currentBatch = this.pending;
|
|
44
|
+
this.pending = new Map(); // Clear for next batch
|
|
45
|
+
for (const [originalQuery, items] of currentBatch.entries()) {
|
|
46
|
+
if (items.length === 1) {
|
|
47
|
+
// No fusion needed
|
|
48
|
+
const item = items[0];
|
|
49
|
+
try {
|
|
50
|
+
const result = await this.adapter.query(item.query, [item.paramValue]);
|
|
51
|
+
item.resolve(result);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
item.reject(err);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// Fuse queries
|
|
59
|
+
// Transform "SELECT * FROM table WHERE id = ?"
|
|
60
|
+
// into "SELECT * FROM table WHERE id IN (?, ?, ...)"
|
|
61
|
+
// Basic string manipulation (safe enough for this controlled feature)
|
|
62
|
+
// We assume the query ends with " = ?" or contains it clearly.
|
|
63
|
+
const fusedQuery = originalQuery.replace(' = ?', ` IN (${items.map(() => '?').join(', ')})`);
|
|
64
|
+
const allParams = items.map(i => i.paramValue);
|
|
65
|
+
try {
|
|
66
|
+
const results = await this.adapter.query(fusedQuery, allParams);
|
|
67
|
+
// Distribute results back to callers
|
|
68
|
+
// We need to map results back to params.
|
|
69
|
+
// This is the tricky part: standard SQL doesn't guarantee order matching input IN clause.
|
|
70
|
+
// We must assume the result objects contain the key used in WHERE.
|
|
71
|
+
// Extract the column name from the query
|
|
72
|
+
// "WHERE id = ?" -> "id"
|
|
73
|
+
const match = originalQuery.match(/WHERE\s+(\w+)\s*=\s*\?/i);
|
|
74
|
+
if (!match) {
|
|
75
|
+
// Fallback if we can't parse: execute individually (should not happen given check above)
|
|
76
|
+
await Promise.all(items.map(async (item) => {
|
|
77
|
+
try {
|
|
78
|
+
const r = await this.adapter.query(item.query, [item.paramValue]);
|
|
79
|
+
item.resolve(r);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
item.reject(e);
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const keyColumn = match[1];
|
|
88
|
+
// Map results by key
|
|
89
|
+
// Note: This assumes the result is an array of objects
|
|
90
|
+
// If the adapter returns something else, this might fail.
|
|
91
|
+
if (!Array.isArray(results)) {
|
|
92
|
+
// Fallback for non-array results
|
|
93
|
+
// Just give everyone the full result? No, that's wrong.
|
|
94
|
+
// If it's not an array, we probably can't split it.
|
|
95
|
+
// Reject? Or execute individually?
|
|
96
|
+
// Let's execute individually as fallback.
|
|
97
|
+
await Promise.all(items.map(async (item) => {
|
|
98
|
+
try {
|
|
99
|
+
const r = await this.adapter.query(item.query, [item.paramValue]);
|
|
100
|
+
item.resolve(r);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
item.reject(e);
|
|
104
|
+
}
|
|
105
|
+
}));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const resultMap = new Map();
|
|
109
|
+
results.forEach(row => {
|
|
110
|
+
if (typeof row === 'object' && row !== null && keyColumn in row) {
|
|
111
|
+
const typedRow = row;
|
|
112
|
+
const key = typedRow[keyColumn];
|
|
113
|
+
if (typeof key === 'string' || typeof key === 'number') {
|
|
114
|
+
if (!resultMap.has(key)) {
|
|
115
|
+
resultMap.set(key, []);
|
|
116
|
+
}
|
|
117
|
+
resultMap.get(key).push(row);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
items.forEach(item => {
|
|
122
|
+
const key = typeof item.paramValue === 'string' || typeof item.paramValue === 'number' ? item.paramValue : undefined;
|
|
123
|
+
const res = key !== undefined ? resultMap.get(key) || [] : [];
|
|
124
|
+
item.resolve(res);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
// Fail all
|
|
129
|
+
items.forEach(item => item.reject(err));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
exports.QueryCoalescer = QueryCoalescer;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DatabaseManager = void 0;
|
|
4
|
+
class DatabaseManager {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.connections = new Map();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Manually register an already initialized adapter instance
|
|
11
|
+
* @param name The connection name
|
|
12
|
+
* @param adapter The initialized adapter instance
|
|
13
|
+
*/
|
|
14
|
+
registerConnection(name, adapter) {
|
|
15
|
+
this.connections.set(name, adapter);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Register a new database adapter type
|
|
19
|
+
* @param type The type identifier (e.g., 'postgres', 'mysql', 'mongo')
|
|
20
|
+
* @param adapter The adapter class
|
|
21
|
+
*/
|
|
22
|
+
static registerAdapter(type, adapter) {
|
|
23
|
+
DatabaseManager.adapterRegistry.set(type, adapter);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Connect to a specific database or the default one
|
|
27
|
+
* @param name Connection name from config
|
|
28
|
+
*/
|
|
29
|
+
async connect(name) {
|
|
30
|
+
const connectionName = name || this.config.default;
|
|
31
|
+
if (!connectionName) {
|
|
32
|
+
throw new Error('No connection name provided and no default connection configured.');
|
|
33
|
+
}
|
|
34
|
+
// Return existing connection if available and connected
|
|
35
|
+
const existingConnection = this.connections.get(connectionName);
|
|
36
|
+
if (existingConnection && existingConnection.isConnected()) {
|
|
37
|
+
return existingConnection;
|
|
38
|
+
}
|
|
39
|
+
const dbConfig = this.config.connections[connectionName];
|
|
40
|
+
if (!dbConfig) {
|
|
41
|
+
throw new Error(`Connection configuration for '${connectionName}' not found.`);
|
|
42
|
+
}
|
|
43
|
+
const AdapterClass = DatabaseManager.adapterRegistry.get(dbConfig.type);
|
|
44
|
+
if (!AdapterClass) {
|
|
45
|
+
throw new Error(`No adapter registered for database type '${dbConfig.type}'.`);
|
|
46
|
+
}
|
|
47
|
+
const adapter = new AdapterClass(dbConfig);
|
|
48
|
+
await adapter.connect();
|
|
49
|
+
this.connections.set(connectionName, adapter);
|
|
50
|
+
return adapter;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Disconnect a specific connection or all connections
|
|
54
|
+
* @param name Connection name (optional). If not provided, disconnects all.
|
|
55
|
+
*/
|
|
56
|
+
async disconnect(name) {
|
|
57
|
+
if (name) {
|
|
58
|
+
const adapter = this.connections.get(name);
|
|
59
|
+
if (adapter) {
|
|
60
|
+
await adapter.disconnect();
|
|
61
|
+
this.connections.delete(name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const promises = Array.from(this.connections.values()).map(adapter => adapter.disconnect());
|
|
66
|
+
await Promise.all(promises);
|
|
67
|
+
this.connections.clear();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get an active connection
|
|
72
|
+
* @param name Connection name
|
|
73
|
+
*/
|
|
74
|
+
get(name) {
|
|
75
|
+
const connectionName = name || this.config.default;
|
|
76
|
+
if (!connectionName) {
|
|
77
|
+
throw new Error('No connection name provided and no default connection configured.');
|
|
78
|
+
}
|
|
79
|
+
const adapter = this.connections.get(connectionName);
|
|
80
|
+
if (!adapter) {
|
|
81
|
+
throw new Error(`Connection '${connectionName}' is not active. Call connect() first.`);
|
|
82
|
+
}
|
|
83
|
+
return adapter;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.DatabaseManager = DatabaseManager;
|
|
87
|
+
DatabaseManager.adapterRegistry = new Map();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.getStringifier = exports.fastJsonStringify = exports.BufferPool = exports.QHTTPX = void 0;
|
|
18
|
+
exports.createHttpApp = createHttpApp;
|
|
19
|
+
const server_1 = require("./core/server");
|
|
20
|
+
const presets_1 = require("./middleware/presets");
|
|
21
|
+
var server_2 = require("./core/server");
|
|
22
|
+
Object.defineProperty(exports, "QHTTPX", { enumerable: true, get: function () { return server_2.QHTTPX; } });
|
|
23
|
+
__exportStar(require("./core/types"), exports);
|
|
24
|
+
__exportStar(require("./middleware/cors"), exports);
|
|
25
|
+
__exportStar(require("./middleware/security"), exports);
|
|
26
|
+
__exportStar(require("./middleware/static"), exports);
|
|
27
|
+
__exportStar(require("./middleware/compression"), exports);
|
|
28
|
+
__exportStar(require("./core/stream"), exports);
|
|
29
|
+
__exportStar(require("./utils/logger"), exports);
|
|
30
|
+
var buffer_pool_1 = require("./core/buffer-pool");
|
|
31
|
+
Object.defineProperty(exports, "BufferPool", { enumerable: true, get: function () { return buffer_pool_1.BufferPool; } });
|
|
32
|
+
__exportStar(require("./testing"), exports);
|
|
33
|
+
__exportStar(require("./utils/signals"), exports);
|
|
34
|
+
__exportStar(require("./middleware/presets"), exports);
|
|
35
|
+
__exportStar(require("./utils/cookies"), exports);
|
|
36
|
+
__exportStar(require("./utils/sse"), exports);
|
|
37
|
+
var serializer_1 = require("./core/serializer");
|
|
38
|
+
Object.defineProperty(exports, "fastJsonStringify", { enumerable: true, get: function () { return serializer_1.fastJsonStringify; } });
|
|
39
|
+
Object.defineProperty(exports, "getStringifier", { enumerable: true, get: function () { return serializer_1.getStringifier; } });
|
|
40
|
+
__exportStar(require("./database/types"), exports);
|
|
41
|
+
__exportStar(require("./database/manager"), exports);
|
|
42
|
+
__exportStar(require("./database/adapters/memory"), exports);
|
|
43
|
+
__exportStar(require("./views"), exports);
|
|
44
|
+
__exportStar(require("./validation"), exports);
|
|
45
|
+
__exportStar(require("./database/adapters/sqlite"), exports);
|
|
46
|
+
__exportStar(require("./database/adapters/postgres"), exports);
|
|
47
|
+
__exportStar(require("./database/adapters/mongo"), exports);
|
|
48
|
+
__exportStar(require("./core/fusion"), exports);
|
|
49
|
+
__exportStar(require("./validation/types"), exports);
|
|
50
|
+
__exportStar(require("./validation/simple"), exports);
|
|
51
|
+
__exportStar(require("./openapi/generator"), exports);
|
|
52
|
+
__exportStar(require("./client"), exports);
|
|
53
|
+
function createHttpApp(options = {}) {
|
|
54
|
+
const app = new server_1.QHTTPX(options);
|
|
55
|
+
// Skip middleware in ultra mode for maximum performance
|
|
56
|
+
if (options.performanceMode !== 'ultra') {
|
|
57
|
+
const middlewares = (0, presets_1.createApiPreset)();
|
|
58
|
+
middlewares.forEach((mw) => app.use(mw));
|
|
59
|
+
}
|
|
60
|
+
return app;
|
|
61
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createCompressionMiddleware = createCompressionMiddleware;
|
|
7
|
+
const zlib_1 = __importDefault(require("zlib"));
|
|
8
|
+
function createCompressionMiddleware(options = {}) {
|
|
9
|
+
const threshold = options.threshold ?? 1024;
|
|
10
|
+
const level = options.level ?? zlib_1.default.constants.Z_DEFAULT_COMPRESSION;
|
|
11
|
+
return async (ctx, next) => {
|
|
12
|
+
const req = ctx.req;
|
|
13
|
+
const res = ctx.res;
|
|
14
|
+
const acceptEncoding = req.headers['accept-encoding'] || '';
|
|
15
|
+
let stream;
|
|
16
|
+
let encoding = '';
|
|
17
|
+
if (/\bbr\b/.test(acceptEncoding)) {
|
|
18
|
+
stream = zlib_1.default.createBrotliCompress({
|
|
19
|
+
params: {
|
|
20
|
+
[zlib_1.default.constants.BROTLI_PARAM_QUALITY]: level,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
encoding = 'br';
|
|
24
|
+
}
|
|
25
|
+
else if (/\bgzip\b/.test(acceptEncoding)) {
|
|
26
|
+
stream = zlib_1.default.createGzip({ level });
|
|
27
|
+
encoding = 'gzip';
|
|
28
|
+
}
|
|
29
|
+
else if (/\bdeflate\b/.test(acceptEncoding)) {
|
|
30
|
+
stream = zlib_1.default.createDeflate({ level });
|
|
31
|
+
encoding = 'deflate';
|
|
32
|
+
}
|
|
33
|
+
if (!stream) {
|
|
34
|
+
await next();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const originalWrite = res.write;
|
|
38
|
+
const originalEnd = res.end;
|
|
39
|
+
// const originalSetHeader = res.setHeader;
|
|
40
|
+
// We need to defer compression decision until we know the content type/length
|
|
41
|
+
// But since we are streaming, we might just start compressing if headers are sent?
|
|
42
|
+
// Actually, we can hook into write/end.
|
|
43
|
+
let headersSent = false;
|
|
44
|
+
let compress = false;
|
|
45
|
+
// Helper to check if we should compress based on content-type
|
|
46
|
+
const shouldCompress = () => {
|
|
47
|
+
const contentType = res.getHeader('content-type');
|
|
48
|
+
if (!contentType)
|
|
49
|
+
return true; // Assume yes if unknown? Or no? Usually text/json is compressed.
|
|
50
|
+
const type = String(contentType).toLowerCase();
|
|
51
|
+
if (type.includes('text/event-stream')) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return (type.includes('text') ||
|
|
55
|
+
type.includes('json') ||
|
|
56
|
+
type.includes('xml') ||
|
|
57
|
+
type.includes('javascript') ||
|
|
58
|
+
type.includes('svg'));
|
|
59
|
+
};
|
|
60
|
+
// Override write
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
res.write = function (chunk, ...args) {
|
|
63
|
+
if (!headersSent) {
|
|
64
|
+
if (shouldCompress()) {
|
|
65
|
+
compress = true;
|
|
66
|
+
// Disable auto-end because we handle the stream asynchronously
|
|
67
|
+
ctx.disableAutoEnd = true;
|
|
68
|
+
res.setHeader('Content-Encoding', encoding);
|
|
69
|
+
res.removeHeader('Content-Length');
|
|
70
|
+
res.setHeader('Vary', 'Accept-Encoding');
|
|
71
|
+
stream.on('data', (data) => {
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
originalWrite.call(res, data);
|
|
74
|
+
});
|
|
75
|
+
stream.on('end', () => {
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
originalEnd.call(res);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
compress = false;
|
|
82
|
+
}
|
|
83
|
+
headersSent = true;
|
|
84
|
+
}
|
|
85
|
+
if (compress && stream) {
|
|
86
|
+
return stream.write(chunk, ...args);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
+
return originalWrite.apply(res, [chunk, ...args]);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
// Override end
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
+
res.end = function (chunk, ...args) {
|
|
96
|
+
if (!headersSent) {
|
|
97
|
+
const len = chunk ? Buffer.byteLength(chunk) : 0;
|
|
98
|
+
if (shouldCompress() && len >= threshold) {
|
|
99
|
+
compress = true;
|
|
100
|
+
// Disable auto-end because we handle the stream asynchronously
|
|
101
|
+
ctx.disableAutoEnd = true;
|
|
102
|
+
res.setHeader('Content-Encoding', encoding);
|
|
103
|
+
res.removeHeader('Content-Length');
|
|
104
|
+
res.setHeader('Vary', 'Accept-Encoding');
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
stream.on('data', (data) => originalWrite.call(res, data));
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
stream.on('end', () => originalEnd.call(res));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
compress = false;
|
|
112
|
+
}
|
|
113
|
+
headersSent = true;
|
|
114
|
+
}
|
|
115
|
+
if (compress && stream) {
|
|
116
|
+
if (chunk)
|
|
117
|
+
stream.write(chunk);
|
|
118
|
+
stream.end();
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
return originalEnd.apply(res, [chunk, ...args]);
|
|
123
|
+
}
|
|
124
|
+
return res;
|
|
125
|
+
};
|
|
126
|
+
// Fix for the pipe issue:
|
|
127
|
+
// If we set compress=true, we should pipe stream to res ONCE.
|
|
128
|
+
// If we use { end: false }, we must manually end res.
|
|
129
|
+
// If we use { end: true }, stream.end() will end res.
|
|
130
|
+
// Let's refine the logic.
|
|
131
|
+
await next();
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCorsMiddleware = createCorsMiddleware;
|
|
4
|
+
function resolveOrigin(origin, requestOrigin) {
|
|
5
|
+
if (!origin) {
|
|
6
|
+
return '*';
|
|
7
|
+
}
|
|
8
|
+
if (typeof origin === 'string') {
|
|
9
|
+
return origin;
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(origin)) {
|
|
12
|
+
if (!requestOrigin) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (origin.includes(requestOrigin)) {
|
|
16
|
+
return requestOrigin;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return origin(requestOrigin);
|
|
21
|
+
}
|
|
22
|
+
function createCorsMiddleware(options = {}) {
|
|
23
|
+
const methodsHeader = options.methods && options.methods.length > 0
|
|
24
|
+
? options.methods.join(', ')
|
|
25
|
+
: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS';
|
|
26
|
+
const allowedHeadersHeader = options.allowedHeaders && options.allowedHeaders.length > 0
|
|
27
|
+
? options.allowedHeaders.join(', ')
|
|
28
|
+
: undefined;
|
|
29
|
+
const exposedHeadersHeader = options.exposedHeaders && options.exposedHeaders.length > 0
|
|
30
|
+
? options.exposedHeaders.join(', ')
|
|
31
|
+
: undefined;
|
|
32
|
+
const maxAgeHeader = typeof options.maxAgeSeconds === 'number'
|
|
33
|
+
? String(options.maxAgeSeconds)
|
|
34
|
+
: undefined;
|
|
35
|
+
const allowCredentials = options.credentials ?? false;
|
|
36
|
+
return async (ctx, next) => {
|
|
37
|
+
const requestOriginHeader = ctx.req.headers.origin;
|
|
38
|
+
const resolvedOrigin = resolveOrigin(options.origin, requestOriginHeader);
|
|
39
|
+
if (resolvedOrigin) {
|
|
40
|
+
ctx.res.setHeader('access-control-allow-origin', resolvedOrigin);
|
|
41
|
+
if (allowCredentials) {
|
|
42
|
+
ctx.res.setHeader('access-control-allow-credentials', 'true');
|
|
43
|
+
}
|
|
44
|
+
if (exposedHeadersHeader) {
|
|
45
|
+
ctx.res.setHeader('access-control-expose-headers', exposedHeadersHeader);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (ctx.req.method === 'OPTIONS') {
|
|
49
|
+
ctx.res.statusCode = 204;
|
|
50
|
+
ctx.res.setHeader('access-control-allow-methods', methodsHeader);
|
|
51
|
+
const requestHeaders = typeof ctx.req.headers['access-control-request-headers'] === 'string'
|
|
52
|
+
? ctx.req.headers['access-control-request-headers']
|
|
53
|
+
: undefined;
|
|
54
|
+
const headersValue = allowedHeadersHeader || requestHeaders;
|
|
55
|
+
if (headersValue) {
|
|
56
|
+
ctx.res.setHeader('access-control-allow-headers', headersValue);
|
|
57
|
+
}
|
|
58
|
+
if (maxAgeHeader) {
|
|
59
|
+
ctx.res.setHeader('access-control-max-age', maxAgeHeader);
|
|
60
|
+
}
|
|
61
|
+
ctx.res.end();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await next();
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createApiPreset = createApiPreset;
|
|
4
|
+
exports.createStaticAppPreset = createStaticAppPreset;
|
|
5
|
+
const security_1 = require("./security");
|
|
6
|
+
const logger_1 = require("../utils/logger");
|
|
7
|
+
const static_1 = require("./static");
|
|
8
|
+
function createApiPreset(options = {}) {
|
|
9
|
+
const middlewares = [];
|
|
10
|
+
// 1. Security (CORS, Headers)
|
|
11
|
+
middlewares.push(...(0, security_1.createSecureDefaults)(options.security));
|
|
12
|
+
// 2. Logging
|
|
13
|
+
if (options.logging !== false) {
|
|
14
|
+
const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
|
|
15
|
+
middlewares.push((0, logger_1.createLoggerMiddleware)(loggerOptions));
|
|
16
|
+
}
|
|
17
|
+
return middlewares;
|
|
18
|
+
}
|
|
19
|
+
function createStaticAppPreset(options) {
|
|
20
|
+
const middlewares = [];
|
|
21
|
+
// 1. Security
|
|
22
|
+
middlewares.push(...(0, security_1.createSecureDefaults)(options.security));
|
|
23
|
+
// 2. Logging
|
|
24
|
+
if (options.logging !== false) {
|
|
25
|
+
const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
|
|
26
|
+
middlewares.push((0, logger_1.createLoggerMiddleware)(loggerOptions));
|
|
27
|
+
}
|
|
28
|
+
// 3. Static Files
|
|
29
|
+
// We force fallthrough to true so API routes can handle non-static requests
|
|
30
|
+
const staticOptions = { ...options.static, fallthrough: true };
|
|
31
|
+
middlewares.push((0, static_1.createStaticMiddleware)(staticOptions));
|
|
32
|
+
return middlewares;
|
|
33
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rateLimit = exports.MemoryStore = void 0;
|
|
4
|
+
class MemoryStore {
|
|
5
|
+
constructor(clearPeriodMs = 60000) {
|
|
6
|
+
this.hits = new Map();
|
|
7
|
+
// Cleanup expired entries periodically
|
|
8
|
+
if (clearPeriodMs > 0) {
|
|
9
|
+
this.interval = setInterval(() => this.cleanup(), clearPeriodMs);
|
|
10
|
+
this.interval.unref(); // Don't hold process open
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async increment(key, windowMs) {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
let record = this.hits.get(key);
|
|
16
|
+
if (!record || now > record.resetTime) {
|
|
17
|
+
record = { count: 1, resetTime: now + windowMs };
|
|
18
|
+
this.hits.set(key, record);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
record.count++;
|
|
22
|
+
}
|
|
23
|
+
return { total: record.count, resetTime: record.resetTime };
|
|
24
|
+
}
|
|
25
|
+
async decrement(key) {
|
|
26
|
+
const record = this.hits.get(key);
|
|
27
|
+
if (record && record.count > 0) {
|
|
28
|
+
record.count--;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async reset(key) {
|
|
32
|
+
this.hits.delete(key);
|
|
33
|
+
}
|
|
34
|
+
cleanup() {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const [key, record] of this.hits.entries()) {
|
|
37
|
+
if (now > record.resetTime) {
|
|
38
|
+
this.hits.delete(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.MemoryStore = MemoryStore;
|
|
44
|
+
const rateLimit = (options = {}) => {
|
|
45
|
+
const windowMs = options.windowMs ?? 60000; // 1 minute default
|
|
46
|
+
const max = options.max ?? 100; // 100 requests default
|
|
47
|
+
const message = options.message ?? 'Too many requests, please try again later.';
|
|
48
|
+
const statusCode = options.statusCode ?? 429;
|
|
49
|
+
const headers = options.headers ?? true;
|
|
50
|
+
const store = options.store ?? new MemoryStore();
|
|
51
|
+
const keyGenerator = options.keyGenerator ?? ((ctx) => {
|
|
52
|
+
return ctx.req.socket.remoteAddress || 'unknown';
|
|
53
|
+
});
|
|
54
|
+
return async (ctx, next) => {
|
|
55
|
+
if (options.skip?.(ctx)) {
|
|
56
|
+
return next();
|
|
57
|
+
}
|
|
58
|
+
const key = keyGenerator(ctx);
|
|
59
|
+
const { total, resetTime } = await store.increment(key, windowMs);
|
|
60
|
+
const remaining = Math.max(0, max - total);
|
|
61
|
+
const resetSeconds = Math.ceil((resetTime - Date.now()) / 1000);
|
|
62
|
+
if (headers) {
|
|
63
|
+
ctx.res.setHeader('X-RateLimit-Limit', max);
|
|
64
|
+
ctx.res.setHeader('X-RateLimit-Remaining', remaining);
|
|
65
|
+
ctx.res.setHeader('X-RateLimit-Reset', resetSeconds);
|
|
66
|
+
}
|
|
67
|
+
if (total > max) {
|
|
68
|
+
if (headers) {
|
|
69
|
+
ctx.res.setHeader('Retry-After', resetSeconds);
|
|
70
|
+
}
|
|
71
|
+
ctx.json(typeof message === 'string' ? { error: message } : message, statusCode);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await next();
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
exports.rateLimit = rateLimit;
|