qhttpx 2.1.0 → 2.3.1
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/LICENSE +201 -21
- package/README.md +117 -221
- package/dist/chunk-QW72SEAS.mjs +98 -0
- package/dist/{src/cli/index.d.ts → cli.d.mts} +0 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +287 -0
- package/dist/cli.mjs +209 -0
- package/dist/index.d.mts +433 -0
- package/dist/index.d.ts +433 -0
- package/dist/index.js +1955 -0
- package/dist/index.mjs +1863 -0
- package/dist/qhttpx-core-new.linux-x64-gnu.node +0 -0
- package/dist/qhttpx-core-new.node +0 -0
- package/dist/qhttpx-core-new.win32-x64-msvc.node +0 -0
- package/examples/benchmark.ts +67 -0
- package/examples/body_demo.ts +32 -0
- package/examples/chat_client.ts +36 -0
- package/examples/chat_demo.ts +41 -0
- package/examples/cluster_demo.ts +26 -0
- package/examples/cors_demo.ts +25 -0
- package/examples/db_auth_demo.ts +66 -0
- package/examples/demo.ts +52 -0
- package/examples/headers_demo.ts +24 -0
- package/examples/hello.ts +7 -0
- package/examples/http2_demo.ts +52 -0
- package/examples/magic-dev.ts +15 -0
- package/examples/middleware_demo.ts +34 -0
- package/examples/mongo_demo.ts +40 -0
- package/examples/native_policy_demo.ts +33 -0
- package/examples/observability_demo.ts +24 -0
- package/examples/public/1mb.dat +1 -0
- package/examples/query_demo.ts +29 -0
- package/examples/response_demo.ts +21 -0
- package/examples/routing_demo.ts +24 -0
- package/examples/server.ts +51 -0
- package/examples/static_demo.ts +29 -0
- package/examples/test_middleware_client.ts +33 -0
- package/examples/test_query_client.ts +34 -0
- package/examples/test_response_client.ts +30 -0
- package/examples/tls_demo.ts +32 -0
- package/examples/upload_demo.ts +43 -0
- package/examples/uploads/test.txt +1 -0
- package/examples/verify_upload.ts +69 -0
- package/examples/ws_client.ts +26 -0
- package/examples/ws_demo.ts +21 -0
- package/package.json +65 -81
- package/CHANGELOG.md +0 -285
- package/dist/examples/api-server.d.ts +0 -1
- package/dist/examples/api-server.js +0 -80
- package/dist/examples/basic.d.ts +0 -1
- package/dist/examples/basic.js +0 -9
- package/dist/examples/compression.d.ts +0 -1
- package/dist/examples/compression.js +0 -15
- package/dist/examples/cors.d.ts +0 -1
- package/dist/examples/cors.js +0 -18
- package/dist/examples/errors.d.ts +0 -1
- package/dist/examples/errors.js +0 -26
- package/dist/examples/file-upload.d.ts +0 -1
- package/dist/examples/file-upload.js +0 -22
- package/dist/examples/fusion.d.ts +0 -1
- package/dist/examples/fusion.js +0 -21
- package/dist/examples/rate-limiting.d.ts +0 -1
- package/dist/examples/rate-limiting.js +0 -17
- package/dist/examples/validation.d.ts +0 -1
- package/dist/examples/validation.js +0 -22
- package/dist/examples/websockets.d.ts +0 -1
- package/dist/examples/websockets.js +0 -19
- package/dist/package.json +0 -107
- package/dist/src/benchmarks/quantam-users.d.ts +0 -1
- package/dist/src/benchmarks/quantam-users.js +0 -56
- package/dist/src/benchmarks/quick-bench.d.ts +0 -1
- package/dist/src/benchmarks/quick-bench.js +0 -57
- package/dist/src/benchmarks/simple-json.d.ts +0 -1
- package/dist/src/benchmarks/simple-json.js +0 -171
- package/dist/src/benchmarks/ultra-mode.d.ts +0 -1
- package/dist/src/benchmarks/ultra-mode.js +0 -64
- package/dist/src/cli/index.js +0 -222
- package/dist/src/client/index.d.ts +0 -17
- package/dist/src/client/index.js +0 -72
- package/dist/src/core/batch.d.ts +0 -24
- package/dist/src/core/batch.js +0 -97
- package/dist/src/core/body-parser.d.ts +0 -15
- package/dist/src/core/body-parser.js +0 -121
- package/dist/src/core/buffer-pool.d.ts +0 -41
- package/dist/src/core/buffer-pool.js +0 -70
- package/dist/src/core/config.d.ts +0 -7
- package/dist/src/core/config.js +0 -50
- package/dist/src/core/context-pool.d.ts +0 -12
- package/dist/src/core/context-pool.js +0 -34
- package/dist/src/core/errors.d.ts +0 -34
- package/dist/src/core/errors.js +0 -70
- package/dist/src/core/fusion.d.ts +0 -20
- package/dist/src/core/fusion.js +0 -191
- package/dist/src/core/logger.d.ts +0 -22
- package/dist/src/core/logger.js +0 -49
- package/dist/src/core/metrics.d.ts +0 -50
- package/dist/src/core/metrics.js +0 -123
- package/dist/src/core/resources.d.ts +0 -9
- package/dist/src/core/resources.js +0 -25
- package/dist/src/core/scheduler.d.ts +0 -38
- package/dist/src/core/scheduler.js +0 -126
- package/dist/src/core/scope.d.ts +0 -41
- package/dist/src/core/scope.js +0 -107
- package/dist/src/core/serializer.d.ts +0 -10
- package/dist/src/core/serializer.js +0 -82
- package/dist/src/core/server.d.ts +0 -179
- package/dist/src/core/server.js +0 -1511
- package/dist/src/core/stream.d.ts +0 -15
- package/dist/src/core/stream.js +0 -71
- package/dist/src/core/tasks.d.ts +0 -29
- package/dist/src/core/tasks.js +0 -87
- package/dist/src/core/timer.d.ts +0 -11
- package/dist/src/core/timer.js +0 -29
- package/dist/src/core/types.d.ts +0 -225
- package/dist/src/core/types.js +0 -19
- package/dist/src/core/websocket.d.ts +0 -25
- package/dist/src/core/websocket.js +0 -86
- package/dist/src/core/worker-queue.d.ts +0 -41
- package/dist/src/core/worker-queue.js +0 -73
- package/dist/src/database/adapters/memory.d.ts +0 -21
- package/dist/src/database/adapters/memory.js +0 -90
- package/dist/src/database/adapters/mongo.d.ts +0 -11
- package/dist/src/database/adapters/mongo.js +0 -141
- package/dist/src/database/adapters/postgres.d.ts +0 -10
- package/dist/src/database/adapters/postgres.js +0 -111
- package/dist/src/database/adapters/sqlite.d.ts +0 -10
- package/dist/src/database/adapters/sqlite.js +0 -42
- package/dist/src/database/coalescer.d.ts +0 -14
- package/dist/src/database/coalescer.js +0 -134
- package/dist/src/database/manager.d.ts +0 -35
- package/dist/src/database/manager.js +0 -87
- package/dist/src/database/types.d.ts +0 -20
- package/dist/src/database/types.js +0 -2
- package/dist/src/index.d.ts +0 -52
- package/dist/src/index.js +0 -92
- package/dist/src/middleware/compression.d.ts +0 -2
- package/dist/src/middleware/compression.js +0 -133
- package/dist/src/middleware/cors.d.ts +0 -2
- package/dist/src/middleware/cors.js +0 -66
- package/dist/src/middleware/presets.d.ts +0 -15
- package/dist/src/middleware/presets.js +0 -52
- package/dist/src/middleware/rate-limit.d.ts +0 -14
- package/dist/src/middleware/rate-limit.js +0 -83
- package/dist/src/middleware/security.d.ts +0 -10
- package/dist/src/middleware/security.js +0 -74
- package/dist/src/middleware/static.d.ts +0 -11
- package/dist/src/middleware/static.js +0 -191
- package/dist/src/openapi/generator.d.ts +0 -19
- package/dist/src/openapi/generator.js +0 -149
- package/dist/src/router/radix-router.d.ts +0 -18
- package/dist/src/router/radix-router.js +0 -89
- package/dist/src/router/radix-tree.d.ts +0 -21
- package/dist/src/router/radix-tree.js +0 -175
- package/dist/src/router/router.d.ts +0 -37
- package/dist/src/router/router.js +0 -203
- package/dist/src/testing/index.d.ts +0 -25
- package/dist/src/testing/index.js +0 -84
- package/dist/src/utils/cookies.d.ts +0 -3
- package/dist/src/utils/cookies.js +0 -59
- package/dist/src/utils/logger.d.ts +0 -2
- package/dist/src/utils/logger.js +0 -45
- package/dist/src/utils/signals.d.ts +0 -6
- package/dist/src/utils/signals.js +0 -31
- package/dist/src/utils/sse.d.ts +0 -6
- package/dist/src/utils/sse.js +0 -32
- package/dist/src/validation/index.d.ts +0 -3
- package/dist/src/validation/index.js +0 -19
- package/dist/src/validation/simple.d.ts +0 -5
- package/dist/src/validation/simple.js +0 -102
- package/dist/src/validation/types.d.ts +0 -32
- package/dist/src/validation/types.js +0 -12
- package/dist/src/validation/zod.d.ts +0 -4
- package/dist/src/validation/zod.js +0 -18
- package/dist/src/views/index.d.ts +0 -1
- package/dist/src/views/index.js +0 -17
- package/dist/src/views/types.d.ts +0 -3
- package/dist/src/views/types.js +0 -2
- package/dist/tests/adapters.test.d.ts +0 -1
- package/dist/tests/adapters.test.js +0 -106
- package/dist/tests/batch.test.d.ts +0 -1
- package/dist/tests/batch.test.js +0 -117
- package/dist/tests/body-parser.test.d.ts +0 -1
- package/dist/tests/body-parser.test.js +0 -52
- package/dist/tests/compression-sse.test.d.ts +0 -1
- package/dist/tests/compression-sse.test.js +0 -87
- package/dist/tests/cookies.test.d.ts +0 -1
- package/dist/tests/cookies.test.js +0 -63
- package/dist/tests/cors.test.d.ts +0 -1
- package/dist/tests/cors.test.js +0 -55
- package/dist/tests/database.test.d.ts +0 -1
- package/dist/tests/database.test.js +0 -80
- package/dist/tests/dx.test.d.ts +0 -1
- package/dist/tests/dx.test.js +0 -114
- package/dist/tests/ecosystem.test.d.ts +0 -1
- package/dist/tests/ecosystem.test.js +0 -133
- package/dist/tests/features.test.d.ts +0 -1
- package/dist/tests/features.test.js +0 -47
- package/dist/tests/fusion.test.d.ts +0 -1
- package/dist/tests/fusion.test.js +0 -92
- package/dist/tests/http-basic.test.d.ts +0 -1
- package/dist/tests/http-basic.test.js +0 -124
- package/dist/tests/logger.test.d.ts +0 -1
- package/dist/tests/logger.test.js +0 -33
- package/dist/tests/middleware.test.d.ts +0 -1
- package/dist/tests/middleware.test.js +0 -109
- package/dist/tests/observability.test.d.ts +0 -1
- package/dist/tests/observability.test.js +0 -59
- package/dist/tests/openapi.test.d.ts +0 -1
- package/dist/tests/openapi.test.js +0 -64
- package/dist/tests/plugin.test.d.ts +0 -1
- package/dist/tests/plugin.test.js +0 -65
- package/dist/tests/plugins.test.d.ts +0 -1
- package/dist/tests/plugins.test.js +0 -71
- package/dist/tests/rate-limit.test.d.ts +0 -1
- package/dist/tests/rate-limit.test.js +0 -77
- package/dist/tests/resources.test.d.ts +0 -1
- package/dist/tests/resources.test.js +0 -47
- package/dist/tests/scheduler.test.d.ts +0 -1
- package/dist/tests/scheduler.test.js +0 -46
- package/dist/tests/schema-routes.test.d.ts +0 -1
- package/dist/tests/schema-routes.test.js +0 -79
- package/dist/tests/security.test.d.ts +0 -1
- package/dist/tests/security.test.js +0 -83
- package/dist/tests/server-db.test.d.ts +0 -1
- package/dist/tests/server-db.test.js +0 -72
- package/dist/tests/smoke.test.d.ts +0 -1
- package/dist/tests/smoke.test.js +0 -10
- package/dist/tests/sqlite-fusion.test.d.ts +0 -1
- package/dist/tests/sqlite-fusion.test.js +0 -92
- package/dist/tests/static.test.d.ts +0 -1
- package/dist/tests/static.test.js +0 -102
- package/dist/tests/stream.test.d.ts +0 -1
- package/dist/tests/stream.test.js +0 -44
- package/dist/tests/task-metrics.test.d.ts +0 -1
- package/dist/tests/task-metrics.test.js +0 -53
- package/dist/tests/tasks.test.d.ts +0 -1
- package/dist/tests/tasks.test.js +0 -62
- package/dist/tests/testing.test.d.ts +0 -1
- package/dist/tests/testing.test.js +0 -47
- package/dist/tests/validation.test.d.ts +0 -1
- package/dist/tests/validation.test.js +0 -107
- package/dist/tests/websocket.test.d.ts +0 -1
- package/dist/tests/websocket.test.js +0 -146
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -9
- package/docs/AEGIS.md +0 -91
- package/docs/API_REFERENCE.md +0 -749
- package/docs/BENCHMARKS.md +0 -39
- package/docs/CAPABILITIES.md +0 -70
- package/docs/CLI.md +0 -43
- package/docs/DATABASE.md +0 -142
- package/docs/ECOSYSTEM.md +0 -146
- package/docs/ERRORS.md +0 -112
- package/docs/FUSION.md +0 -87
- package/docs/MIDDLEWARE.md +0 -65
- package/docs/MIGRATION_1.9_TO_2.0.md +0 -495
- package/docs/NEXT_STEPS.md +0 -99
- package/docs/OPENAPI.md +0 -99
- package/docs/PLUGINS.md +0 -59
- package/docs/PRODUCTION_DEPLOYMENT.md +0 -798
- package/docs/REAL_WORLD_EXAMPLES.md +0 -109
- package/docs/ROADMAP.md +0 -366
- package/docs/ROUTING.md +0 -78
- package/docs/SECURITY.md +0 -876
- package/docs/STATIC.md +0 -61
- package/docs/VALIDATION.md +0 -114
- package/docs/WEBSOCKETS.md +0 -76
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1863 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require,
|
|
3
|
+
__toESM,
|
|
4
|
+
env,
|
|
5
|
+
loadEnv,
|
|
6
|
+
require_core
|
|
7
|
+
} from "./chunk-QW72SEAS.mjs";
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
var import_core = __toESM(require_core());
|
|
11
|
+
|
|
12
|
+
// src/context.ts
|
|
13
|
+
var Context = class {
|
|
14
|
+
constructor(engine, id, rawParams, queryString, rawBody, rawHeaders, rawUrl, responseHandle, method, serializer) {
|
|
15
|
+
this.engine = engine;
|
|
16
|
+
this.id = id;
|
|
17
|
+
this.rawParams = rawParams;
|
|
18
|
+
this.queryString = queryString;
|
|
19
|
+
this.rawBody = rawBody;
|
|
20
|
+
this.rawHeaders = rawHeaders;
|
|
21
|
+
this.responseHandle = responseHandle;
|
|
22
|
+
this.serializer = serializer;
|
|
23
|
+
this.method = method;
|
|
24
|
+
this.env = Object.entries(process.env).filter(([, value]) => value !== void 0).reduce((acc, [key, value]) => {
|
|
25
|
+
acc[key] = value;
|
|
26
|
+
return acc;
|
|
27
|
+
}, {});
|
|
28
|
+
this.perf = {
|
|
29
|
+
startTime: Date.now(),
|
|
30
|
+
dbDuration: 0,
|
|
31
|
+
parseDuration: 0,
|
|
32
|
+
allocations: 0
|
|
33
|
+
};
|
|
34
|
+
try {
|
|
35
|
+
this.url = new URL(rawUrl, "http://localhost");
|
|
36
|
+
} catch {
|
|
37
|
+
this.url = new URL("http://localhost");
|
|
38
|
+
}
|
|
39
|
+
this.path = this.url.pathname;
|
|
40
|
+
this.db = {
|
|
41
|
+
query: async (sql, ttl) => {
|
|
42
|
+
return this.engine.queryDb(sql, ttl);
|
|
43
|
+
},
|
|
44
|
+
queryWithParams: async (sql, params, ttl) => {
|
|
45
|
+
return this.engine.queryDbWithParams(sql, params, ttl);
|
|
46
|
+
},
|
|
47
|
+
mongo: (dbName, collName) => ({
|
|
48
|
+
find: async (query) => {
|
|
49
|
+
const json = await this.engine.queryMongo(dbName, collName, JSON.stringify(query));
|
|
50
|
+
return JSON.parse(json);
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
_status = 200;
|
|
56
|
+
_params = null;
|
|
57
|
+
_headers = null;
|
|
58
|
+
// Placeholder implementations for interface compliance
|
|
59
|
+
url;
|
|
60
|
+
method;
|
|
61
|
+
path;
|
|
62
|
+
body;
|
|
63
|
+
env;
|
|
64
|
+
db;
|
|
65
|
+
perf;
|
|
66
|
+
get params() {
|
|
67
|
+
if (!this._params) {
|
|
68
|
+
this._params = {};
|
|
69
|
+
for (let i = 0; i < this.rawParams.length; i += 2) {
|
|
70
|
+
this._params[this.rawParams[i]] = this.rawParams[i + 1];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return this._params;
|
|
74
|
+
}
|
|
75
|
+
_query = null;
|
|
76
|
+
get query() {
|
|
77
|
+
if (!this._query) {
|
|
78
|
+
this._query = {};
|
|
79
|
+
if (this.queryString) {
|
|
80
|
+
const searchParams = new URLSearchParams(this.queryString);
|
|
81
|
+
searchParams.forEach((value, key) => {
|
|
82
|
+
if (this._query[key]) {
|
|
83
|
+
if (Array.isArray(this._query[key])) {
|
|
84
|
+
this._query[key].push(value);
|
|
85
|
+
} else {
|
|
86
|
+
this._query[key] = [this._query[key], value];
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
this._query[key] = value;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return this._query;
|
|
95
|
+
}
|
|
96
|
+
get headers() {
|
|
97
|
+
if (!this._headers) {
|
|
98
|
+
this._headers = /* @__PURE__ */ new Map();
|
|
99
|
+
for (let i = 0; i < this.rawHeaders.length; i += 2) {
|
|
100
|
+
this._headers.set(this.rawHeaders[i].toLowerCase(), this.rawHeaders[i + 1]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return this._headers;
|
|
104
|
+
}
|
|
105
|
+
// Body Methods (Getters)
|
|
106
|
+
text() {
|
|
107
|
+
return this.rawBody.toString("utf-8");
|
|
108
|
+
}
|
|
109
|
+
get req() {
|
|
110
|
+
return {
|
|
111
|
+
json: () => {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(this.text());
|
|
114
|
+
} catch (e) {
|
|
115
|
+
throw new Error(`Invalid JSON body: ${e}`);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
text: () => this.text(),
|
|
119
|
+
param: (key) => this.params[key],
|
|
120
|
+
query: (key) => {
|
|
121
|
+
const val = this.query[key];
|
|
122
|
+
return Array.isArray(val) ? val[0] : val;
|
|
123
|
+
},
|
|
124
|
+
queries: (key) => {
|
|
125
|
+
const val = this.query[key];
|
|
126
|
+
return Array.isArray(val) ? val : [val];
|
|
127
|
+
},
|
|
128
|
+
header: (key) => this.headers.get(key.toLowerCase())
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// Legacy support (Deprecated)
|
|
132
|
+
json(data, status) {
|
|
133
|
+
if (data !== void 0) {
|
|
134
|
+
if (status) this._status = status;
|
|
135
|
+
return this.send(data);
|
|
136
|
+
}
|
|
137
|
+
return this.req.json();
|
|
138
|
+
}
|
|
139
|
+
send(data) {
|
|
140
|
+
if (typeof data === "string") {
|
|
141
|
+
this.engine.sendResponse(this.responseHandle, this._status, data);
|
|
142
|
+
} else {
|
|
143
|
+
if (this.serializer) {
|
|
144
|
+
const body = this.serializer(data);
|
|
145
|
+
this.engine.sendResponse(this.responseHandle, this._status, body);
|
|
146
|
+
} else {
|
|
147
|
+
this.engine.sendResponse(this.responseHandle, this._status, JSON.stringify(data));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
html(content) {
|
|
152
|
+
this.engine.sendHtml(this.responseHandle, this._status, content);
|
|
153
|
+
}
|
|
154
|
+
status(code) {
|
|
155
|
+
this._status = code;
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
get statusCode() {
|
|
159
|
+
return this._status;
|
|
160
|
+
}
|
|
161
|
+
snapshot() {
|
|
162
|
+
const normalizeValue = (value) => {
|
|
163
|
+
if (Array.isArray(value)) {
|
|
164
|
+
return [...value].map(normalizeValue).sort();
|
|
165
|
+
}
|
|
166
|
+
if (value && typeof value === "object") {
|
|
167
|
+
const keys = Object.keys(value).sort();
|
|
168
|
+
const sorted = {};
|
|
169
|
+
for (const key of keys) {
|
|
170
|
+
sorted[key] = normalizeValue(value[key]);
|
|
171
|
+
}
|
|
172
|
+
return sorted;
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
};
|
|
176
|
+
const headers = {};
|
|
177
|
+
for (const [key, value] of this.headers.entries()) {
|
|
178
|
+
headers[key] = value;
|
|
179
|
+
}
|
|
180
|
+
let body;
|
|
181
|
+
try {
|
|
182
|
+
body = this.req.json();
|
|
183
|
+
} catch {
|
|
184
|
+
body = this.text();
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
id: this.id,
|
|
188
|
+
method: this.method,
|
|
189
|
+
url: this.url.toString(),
|
|
190
|
+
path: this.path,
|
|
191
|
+
params: normalizeValue(this.params),
|
|
192
|
+
query: normalizeValue(this.query),
|
|
193
|
+
headers: normalizeValue(headers),
|
|
194
|
+
body: normalizeValue(body),
|
|
195
|
+
env: normalizeValue(this.env),
|
|
196
|
+
perf: normalizeValue(this.perf)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// src/compose.ts
|
|
202
|
+
function compose(middleware) {
|
|
203
|
+
if (!Array.isArray(middleware)) throw new TypeError("Middleware stack must be an array!");
|
|
204
|
+
for (const fn of middleware) {
|
|
205
|
+
if (typeof fn !== "function") throw new TypeError("Middleware must be composed of functions!");
|
|
206
|
+
}
|
|
207
|
+
return function(context, next) {
|
|
208
|
+
let index = -1;
|
|
209
|
+
return dispatch(0);
|
|
210
|
+
function dispatch(i) {
|
|
211
|
+
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
212
|
+
index = i;
|
|
213
|
+
let fn = middleware[i];
|
|
214
|
+
if (i === middleware.length) fn = next;
|
|
215
|
+
if (!fn) return Promise.resolve();
|
|
216
|
+
try {
|
|
217
|
+
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return Promise.reject(err);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/index.ts
|
|
226
|
+
import fastJson from "fast-json-stringify";
|
|
227
|
+
|
|
228
|
+
// src/generator.ts
|
|
229
|
+
function generateClient(routes) {
|
|
230
|
+
const interfaces = [];
|
|
231
|
+
const paths = [];
|
|
232
|
+
let interfaceCounter = 0;
|
|
233
|
+
function schemaToTs(schema, name) {
|
|
234
|
+
if (!schema) return "any";
|
|
235
|
+
if (schema.type === "object") {
|
|
236
|
+
const props = [];
|
|
237
|
+
const required = new Set(schema.required || []);
|
|
238
|
+
for (const key in schema.properties) {
|
|
239
|
+
const propSchema = schema.properties[key];
|
|
240
|
+
const isReq = required.has(key);
|
|
241
|
+
const tsType = schemaToTs(propSchema, `${name}_${key}`);
|
|
242
|
+
props.push(` ${key}${isReq ? "" : "?"}: ${tsType};`);
|
|
243
|
+
}
|
|
244
|
+
const interfaceName = `I${name}`;
|
|
245
|
+
interfaces.push(`export interface ${interfaceName} {
|
|
246
|
+
${props.join("\n")}
|
|
247
|
+
}`);
|
|
248
|
+
return interfaceName;
|
|
249
|
+
}
|
|
250
|
+
if (schema.type === "array") {
|
|
251
|
+
const itemType = schemaToTs(schema.items, `${name}_item`);
|
|
252
|
+
return `${itemType}[]`;
|
|
253
|
+
}
|
|
254
|
+
if (schema.enum) {
|
|
255
|
+
return schema.enum.map((v) => `'${v}'`).join(" | ");
|
|
256
|
+
}
|
|
257
|
+
if (schema.type === "string") return "string";
|
|
258
|
+
if (schema.type === "integer" || schema.type === "number") return "number";
|
|
259
|
+
if (schema.type === "boolean") return "boolean";
|
|
260
|
+
return "any";
|
|
261
|
+
}
|
|
262
|
+
const routeMap = {};
|
|
263
|
+
for (const route of routes) {
|
|
264
|
+
if (route.path.includes("/docs")) continue;
|
|
265
|
+
if (!routeMap[route.path]) routeMap[route.path] = {};
|
|
266
|
+
let reqType = "any";
|
|
267
|
+
if (route.options && route.options.schema) {
|
|
268
|
+
try {
|
|
269
|
+
const schema = JSON.parse(route.options.schema);
|
|
270
|
+
const name = `${route.method}_${route.path.replace(/[^a-zA-Z0-9]/g, "_")}_Body`;
|
|
271
|
+
reqType = schemaToTs(schema, name);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
console.warn(`Failed to parse schema for client gen: ${route.path}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const resType = "any";
|
|
277
|
+
routeMap[route.path][route.method] = { req: reqType, res: resType };
|
|
278
|
+
}
|
|
279
|
+
const lines = [
|
|
280
|
+
`// Auto-generated QHTTPX Client`,
|
|
281
|
+
`// Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
282
|
+
``,
|
|
283
|
+
`export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';`,
|
|
284
|
+
``,
|
|
285
|
+
`// --- Interfaces ---`,
|
|
286
|
+
...interfaces,
|
|
287
|
+
``,
|
|
288
|
+
`// --- Route Definitions ---`,
|
|
289
|
+
`export interface AppRoutes {`
|
|
290
|
+
];
|
|
291
|
+
for (const [path, methods] of Object.entries(routeMap)) {
|
|
292
|
+
lines.push(` '${path}': {`);
|
|
293
|
+
for (const [method, types] of Object.entries(methods)) {
|
|
294
|
+
lines.push(` '${method}': {`);
|
|
295
|
+
lines.push(` request: ${types.req};`);
|
|
296
|
+
lines.push(` response: ${types.res};`);
|
|
297
|
+
lines.push(` };`);
|
|
298
|
+
}
|
|
299
|
+
lines.push(` };`);
|
|
300
|
+
}
|
|
301
|
+
lines.push(`}`);
|
|
302
|
+
lines.push(``);
|
|
303
|
+
lines.push(`
|
|
304
|
+
export class Client {
|
|
305
|
+
constructor(private baseUrl: string, private token?: string) {}
|
|
306
|
+
|
|
307
|
+
setToken(token: string) {
|
|
308
|
+
this.token = token;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private async request<P extends keyof AppRoutes, M extends keyof AppRoutes[P]>(
|
|
312
|
+
method: M,
|
|
313
|
+
path: P,
|
|
314
|
+
body?: any
|
|
315
|
+
): Promise<any> {
|
|
316
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
317
|
+
if (this.token) headers['Authorization'] = \`Bearer \${this.token}\`;
|
|
318
|
+
|
|
319
|
+
// Handle path params if any (naive replacement)
|
|
320
|
+
// Note: The typed path must match the definition (e.g. /users/:id).
|
|
321
|
+
// Real usage would need a path builder, but for now we assume exact match or user string manipulation.
|
|
322
|
+
|
|
323
|
+
const res = await fetch(\`\${this.baseUrl}\${path as string}\`, {
|
|
324
|
+
method: method as string,
|
|
325
|
+
headers,
|
|
326
|
+
body: body ? JSON.stringify(body) : undefined
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (!res.ok) {
|
|
330
|
+
throw new Error(\`Request failed: \${res.status} \${res.statusText}\`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const contentType = res.headers.get('content-type');
|
|
334
|
+
if (contentType && contentType.includes('application/json')) {
|
|
335
|
+
return res.json();
|
|
336
|
+
}
|
|
337
|
+
return res.text();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
get<P extends keyof AppRoutes>(path: P): Promise<AppRoutes[P]['GET' & keyof AppRoutes[P]]['response']> {
|
|
341
|
+
return this.request('GET', path);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
post<P extends keyof AppRoutes>(
|
|
345
|
+
path: P,
|
|
346
|
+
body: AppRoutes[P]['POST' & keyof AppRoutes[P]]['request']
|
|
347
|
+
): Promise<AppRoutes[P]['POST' & keyof AppRoutes[P]]['response']> {
|
|
348
|
+
return this.request('POST', path, body);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
put<P extends keyof AppRoutes>(
|
|
352
|
+
path: P,
|
|
353
|
+
body: AppRoutes[P]['PUT' & keyof AppRoutes[P]]['request']
|
|
354
|
+
): Promise<AppRoutes[P]['PUT' & keyof AppRoutes[P]]['response']> {
|
|
355
|
+
return this.request('PUT', path, body);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
delete<P extends keyof AppRoutes>(path: P): Promise<AppRoutes[P]['DELETE' & keyof AppRoutes[P]]['response']> {
|
|
359
|
+
return this.request('DELETE', path);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
`);
|
|
363
|
+
return lines.join("\n");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/index.ts
|
|
367
|
+
import * as fs from "fs";
|
|
368
|
+
|
|
369
|
+
// src/test.ts
|
|
370
|
+
var TestClient = class {
|
|
371
|
+
app;
|
|
372
|
+
port;
|
|
373
|
+
baseUrl;
|
|
374
|
+
running = false;
|
|
375
|
+
constructor(app) {
|
|
376
|
+
this.app = app;
|
|
377
|
+
this.port = 0;
|
|
378
|
+
this.baseUrl = "";
|
|
379
|
+
}
|
|
380
|
+
async start() {
|
|
381
|
+
if (this.running) return;
|
|
382
|
+
this.port = Math.floor(Math.random() * 1e4) + 2e4;
|
|
383
|
+
this.baseUrl = `http://localhost:${this.port}`;
|
|
384
|
+
return new Promise((resolve, reject) => {
|
|
385
|
+
try {
|
|
386
|
+
this.app.listen(this.port, () => {
|
|
387
|
+
this.running = true;
|
|
388
|
+
resolve();
|
|
389
|
+
});
|
|
390
|
+
} catch (e) {
|
|
391
|
+
reject(e);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
async stop() {
|
|
396
|
+
if (this.running) {
|
|
397
|
+
this.app.stop();
|
|
398
|
+
this.running = false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async get(path, headers) {
|
|
402
|
+
const h = { "Connection": "close", ...headers };
|
|
403
|
+
return fetch(`${this.baseUrl}${path}`, { method: "GET", headers: h });
|
|
404
|
+
}
|
|
405
|
+
async post(path, body, headers) {
|
|
406
|
+
const isJson = typeof body === "object";
|
|
407
|
+
const h = { "Connection": "close", ...headers };
|
|
408
|
+
if (isJson && !h["Content-Type"]) h["Content-Type"] = "application/json";
|
|
409
|
+
return fetch(`${this.baseUrl}${path}`, {
|
|
410
|
+
method: "POST",
|
|
411
|
+
headers: h,
|
|
412
|
+
body: isJson ? JSON.stringify(body) : body
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
async put(path, body, headers) {
|
|
416
|
+
const isJson = typeof body === "object";
|
|
417
|
+
const h = { "Connection": "close", ...headers };
|
|
418
|
+
if (isJson && !h["Content-Type"]) h["Content-Type"] = "application/json";
|
|
419
|
+
return fetch(`${this.baseUrl}${path}`, {
|
|
420
|
+
method: "PUT",
|
|
421
|
+
headers: h,
|
|
422
|
+
body: isJson ? JSON.stringify(body) : body
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
async delete(path, headers) {
|
|
426
|
+
const h = { "Connection": "close", ...headers };
|
|
427
|
+
return fetch(`${this.baseUrl}${path}`, { method: "DELETE", headers: h });
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
function createTestClient(app) {
|
|
431
|
+
return new TestClient(app);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/fluent.ts
|
|
435
|
+
var requireOptional = (pkg) => {
|
|
436
|
+
try {
|
|
437
|
+
return __require(pkg);
|
|
438
|
+
} catch (e) {
|
|
439
|
+
throw new Error(`Dependency '${pkg}' is required for this feature. Please install it: npm install ${pkg}`);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
var schemaFromValidation = (schema) => {
|
|
443
|
+
const properties = {};
|
|
444
|
+
const required = [];
|
|
445
|
+
for (const [key, type] of Object.entries(schema)) {
|
|
446
|
+
required.push(key);
|
|
447
|
+
if (type === "email") {
|
|
448
|
+
properties[key] = { type: "string", format: "email" };
|
|
449
|
+
} else if (type === "int" || type === "integer") {
|
|
450
|
+
properties[key] = { type: "integer" };
|
|
451
|
+
} else if (type === "bool" || type === "boolean") {
|
|
452
|
+
properties[key] = { type: "boolean" };
|
|
453
|
+
} else {
|
|
454
|
+
properties[key] = { type: "string" };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return { type: "object", properties, required };
|
|
458
|
+
};
|
|
459
|
+
var FluentBuilder = class {
|
|
460
|
+
constructor(app, method, path) {
|
|
461
|
+
this.app = app;
|
|
462
|
+
this.method = method;
|
|
463
|
+
this.path = path;
|
|
464
|
+
}
|
|
465
|
+
steps = [];
|
|
466
|
+
requestSchema;
|
|
467
|
+
responseSchemaDef;
|
|
468
|
+
queryBuilder;
|
|
469
|
+
// RouteBuilder compatibility methods
|
|
470
|
+
desc(description) {
|
|
471
|
+
this.steps.push({
|
|
472
|
+
name: "desc",
|
|
473
|
+
fn: (ctx, state) => {
|
|
474
|
+
return state;
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
return this;
|
|
478
|
+
}
|
|
479
|
+
auth(strategy) {
|
|
480
|
+
this.steps.push({
|
|
481
|
+
name: "auth",
|
|
482
|
+
fn: (ctx, state) => {
|
|
483
|
+
return state;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
return this;
|
|
487
|
+
}
|
|
488
|
+
cache(options) {
|
|
489
|
+
this.steps.push({
|
|
490
|
+
name: "cache",
|
|
491
|
+
fn: (ctx, state) => {
|
|
492
|
+
return state;
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
return this;
|
|
496
|
+
}
|
|
497
|
+
rateLimit(options) {
|
|
498
|
+
this.steps.push({
|
|
499
|
+
name: "rateLimit",
|
|
500
|
+
fn: (ctx, state) => {
|
|
501
|
+
return state;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
status(statusCode) {
|
|
507
|
+
this.steps.push({
|
|
508
|
+
name: "status",
|
|
509
|
+
fn: (ctx, state) => {
|
|
510
|
+
return { ...state, _status: statusCode };
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
return this;
|
|
514
|
+
}
|
|
515
|
+
priority(level) {
|
|
516
|
+
this.steps.push({
|
|
517
|
+
name: "priority",
|
|
518
|
+
fn: (ctx, state) => {
|
|
519
|
+
return state;
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
return this;
|
|
523
|
+
}
|
|
524
|
+
slo(targetMs) {
|
|
525
|
+
this.steps.push({
|
|
526
|
+
name: "slo",
|
|
527
|
+
fn: (ctx, state) => {
|
|
528
|
+
return state;
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
return this;
|
|
532
|
+
}
|
|
533
|
+
// 1. Validation Step
|
|
534
|
+
validate(schema) {
|
|
535
|
+
if (!this.requestSchema) {
|
|
536
|
+
this.requestSchema = schemaFromValidation(schema);
|
|
537
|
+
}
|
|
538
|
+
this.steps.push({
|
|
539
|
+
name: "validate",
|
|
540
|
+
fn: (ctx, state) => {
|
|
541
|
+
const body = ctx.req.json();
|
|
542
|
+
const errors = [];
|
|
543
|
+
const cleanData = {};
|
|
544
|
+
for (const [key, type] of Object.entries(schema)) {
|
|
545
|
+
if (!body[key]) {
|
|
546
|
+
errors.push(`Missing ${key}`);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (type === "email" && !body[key].includes("@")) {
|
|
550
|
+
errors.push(`Invalid email for ${key}`);
|
|
551
|
+
}
|
|
552
|
+
cleanData[key] = body[key];
|
|
553
|
+
}
|
|
554
|
+
if (errors.length > 0) {
|
|
555
|
+
throw { status: 400, message: errors.join(", ") };
|
|
556
|
+
}
|
|
557
|
+
return { ...state, ...cleanData };
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
return this;
|
|
561
|
+
}
|
|
562
|
+
schema(def) {
|
|
563
|
+
this.requestSchema = def;
|
|
564
|
+
return this;
|
|
565
|
+
}
|
|
566
|
+
responseSchema(def) {
|
|
567
|
+
this.responseSchemaDef = def;
|
|
568
|
+
return this;
|
|
569
|
+
}
|
|
570
|
+
query(schemaBuilder) {
|
|
571
|
+
this.queryBuilder = schemaBuilder;
|
|
572
|
+
return this;
|
|
573
|
+
}
|
|
574
|
+
queryState(fields) {
|
|
575
|
+
this.steps.push({
|
|
576
|
+
name: "queryState",
|
|
577
|
+
fn: (ctx, state) => {
|
|
578
|
+
const query = ctx.query;
|
|
579
|
+
if (!fields || fields.length === 0) {
|
|
580
|
+
return { ...state, ...query };
|
|
581
|
+
}
|
|
582
|
+
const next = { ...state };
|
|
583
|
+
for (const field of fields) {
|
|
584
|
+
if (query[field] !== void 0) {
|
|
585
|
+
next[field] = query[field];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return next;
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
return this;
|
|
592
|
+
}
|
|
593
|
+
use(fn) {
|
|
594
|
+
this.steps.push({
|
|
595
|
+
name: "use",
|
|
596
|
+
fn
|
|
597
|
+
});
|
|
598
|
+
return this;
|
|
599
|
+
}
|
|
600
|
+
sql(query, params) {
|
|
601
|
+
this.steps.push({
|
|
602
|
+
name: "sql",
|
|
603
|
+
fn: async (ctx, state) => {
|
|
604
|
+
if (!ctx.db) throw { status: 500, message: "Database not connected" };
|
|
605
|
+
const resolvedParams = typeof params === "function" ? params(state) : params;
|
|
606
|
+
const res = resolvedParams ? await ctx.db.queryWithParams(query, resolvedParams) : await ctx.db.query(query);
|
|
607
|
+
return JSON.parse(res);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
return this;
|
|
611
|
+
}
|
|
612
|
+
// 2. Transformation Step (e.g., Hashing)
|
|
613
|
+
transform(fn) {
|
|
614
|
+
this.steps.push({
|
|
615
|
+
name: "transform",
|
|
616
|
+
fn: async (ctx, state) => {
|
|
617
|
+
const result = await fn(state);
|
|
618
|
+
return { ...state, ...result };
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
return this;
|
|
622
|
+
}
|
|
623
|
+
// Pre-built Transform: Hash Password
|
|
624
|
+
hash(field = "password") {
|
|
625
|
+
return this.transform(async (data) => {
|
|
626
|
+
const bcrypt = requireOptional("bcryptjs");
|
|
627
|
+
if (data[field]) {
|
|
628
|
+
const hash = await bcrypt.hash(data[field], 10);
|
|
629
|
+
return { [field]: hash };
|
|
630
|
+
}
|
|
631
|
+
return {};
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
// 3. Database Step (Insert)
|
|
635
|
+
insert(table) {
|
|
636
|
+
this.steps.push({
|
|
637
|
+
name: `insert:${table}`,
|
|
638
|
+
fn: async (ctx, state) => {
|
|
639
|
+
if (!ctx.db) throw { status: 500, message: "Database not connected" };
|
|
640
|
+
const keys = Object.keys(state);
|
|
641
|
+
const values = Object.values(state);
|
|
642
|
+
const placeholders = values.map((_, i) => `$${i + 1}`);
|
|
643
|
+
const sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders.join(", ")}) RETURNING *`;
|
|
644
|
+
try {
|
|
645
|
+
const res = await ctx.db.queryWithParams(sql, values);
|
|
646
|
+
const rows = JSON.parse(res);
|
|
647
|
+
return rows[0];
|
|
648
|
+
} catch (e) {
|
|
649
|
+
if (e.message?.includes("duplicate")) {
|
|
650
|
+
throw { status: 409, message: "Resource already exists" };
|
|
651
|
+
}
|
|
652
|
+
throw e;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
return this;
|
|
657
|
+
}
|
|
658
|
+
// Database Select
|
|
659
|
+
find(table, by) {
|
|
660
|
+
this.steps.push({
|
|
661
|
+
name: `find:${table}`,
|
|
662
|
+
fn: async (ctx, state) => {
|
|
663
|
+
if (!ctx.db) throw { status: 500, message: "Database not connected" };
|
|
664
|
+
const val = state[by] || ctx.req.json()[by];
|
|
665
|
+
if (!val) throw { status: 400, message: `Missing lookup value for ${by}` };
|
|
666
|
+
const fields = state._query_select ? state._query_select.join(", ") : "*";
|
|
667
|
+
const sql = `SELECT ${fields} FROM ${table} WHERE ${by} = $1`;
|
|
668
|
+
const res = await ctx.db.queryWithParams(sql, [val]);
|
|
669
|
+
const rows = JSON.parse(res);
|
|
670
|
+
if (rows.length === 0) return null;
|
|
671
|
+
return rows[0];
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
return this;
|
|
675
|
+
}
|
|
676
|
+
// 4. Logic/Guard Step
|
|
677
|
+
ensure(condition, errorMsg = "Condition failed", status = 400) {
|
|
678
|
+
this.steps.push({
|
|
679
|
+
name: "ensure",
|
|
680
|
+
fn: (ctx, state) => {
|
|
681
|
+
if (!condition(state)) {
|
|
682
|
+
throw { status, message: errorMsg };
|
|
683
|
+
}
|
|
684
|
+
return state;
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
return this;
|
|
688
|
+
}
|
|
689
|
+
verifyPassword(field = "password") {
|
|
690
|
+
this.steps.push({
|
|
691
|
+
name: "verifyPassword",
|
|
692
|
+
fn: async (ctx, state) => {
|
|
693
|
+
const bcrypt = requireOptional("bcryptjs");
|
|
694
|
+
const body = ctx.req.json();
|
|
695
|
+
const inputPass = body[field];
|
|
696
|
+
const storedHash = state[field] || state[`${field}_hash`];
|
|
697
|
+
if (!inputPass || !storedHash) {
|
|
698
|
+
throw { status: 400, message: "Missing credentials" };
|
|
699
|
+
}
|
|
700
|
+
const valid = await bcrypt.compare(inputPass, storedHash);
|
|
701
|
+
if (!valid) throw { status: 401, message: "Invalid credentials" };
|
|
702
|
+
return state;
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
return this;
|
|
706
|
+
}
|
|
707
|
+
// 5. Auth Step (JWT)
|
|
708
|
+
jwt(options = {}) {
|
|
709
|
+
this.steps.push({
|
|
710
|
+
name: "jwt",
|
|
711
|
+
fn: (ctx, state) => {
|
|
712
|
+
const jwt = requireOptional("jsonwebtoken");
|
|
713
|
+
const secret = options.secret || process.env.JWT_SECRET || "secret";
|
|
714
|
+
const payload = { ...state };
|
|
715
|
+
delete payload.password;
|
|
716
|
+
delete payload.password_hash;
|
|
717
|
+
if (payload.id) payload.sub = payload.id;
|
|
718
|
+
const token = jwt.sign(payload, secret, { expiresIn: options.expiresIn || "24h" });
|
|
719
|
+
return { token, user: payload };
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
return this;
|
|
723
|
+
}
|
|
724
|
+
// 6. Response Step (Finalizer)
|
|
725
|
+
respond(handlerOrStatus = 200) {
|
|
726
|
+
if (typeof handlerOrStatus === "function") {
|
|
727
|
+
const fn = handlerOrStatus;
|
|
728
|
+
this.use(async (ctx, state) => await fn(ctx));
|
|
729
|
+
this.respond(200);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (typeof handlerOrStatus === "object" && handlerOrStatus !== null) {
|
|
733
|
+
const route2 = this.app[this.method.toLowerCase()](this.path, handlerOrStatus);
|
|
734
|
+
this.applyOptionsToRoute(route2);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const status = handlerOrStatus;
|
|
738
|
+
const handler = async (ctx) => {
|
|
739
|
+
let state = {};
|
|
740
|
+
let finalStatus = status;
|
|
741
|
+
try {
|
|
742
|
+
for (const step of this.steps) {
|
|
743
|
+
const result = await step.fn(ctx, state);
|
|
744
|
+
if (result !== void 0) {
|
|
745
|
+
state = result;
|
|
746
|
+
}
|
|
747
|
+
if (state._status !== void 0) {
|
|
748
|
+
finalStatus = state._status;
|
|
749
|
+
delete state._status;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
ctx.json(state, finalStatus);
|
|
753
|
+
} catch (e) {
|
|
754
|
+
const status2 = e.status || 500;
|
|
755
|
+
const message = e.message || "Internal Server Error";
|
|
756
|
+
console.error(`[Fluent] Error in ${this.method} ${this.path}:`, e);
|
|
757
|
+
ctx.json({ error: message }, status2);
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
const route = this.app[this.method.toLowerCase()](this.path, handler);
|
|
761
|
+
this.applyOptionsToRoute(route);
|
|
762
|
+
}
|
|
763
|
+
applyOptionsToRoute(route) {
|
|
764
|
+
if (this.requestSchema) {
|
|
765
|
+
route.schema(this.requestSchema);
|
|
766
|
+
}
|
|
767
|
+
if (this.responseSchemaDef) {
|
|
768
|
+
route.responseSchema(this.responseSchemaDef);
|
|
769
|
+
}
|
|
770
|
+
if (this.queryBuilder) {
|
|
771
|
+
route.query(this.queryBuilder);
|
|
772
|
+
}
|
|
773
|
+
if (this.useJwt) {
|
|
774
|
+
route.jwt();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
useJwt = false;
|
|
778
|
+
// Route Options
|
|
779
|
+
secure() {
|
|
780
|
+
this.useJwt = true;
|
|
781
|
+
this.steps.unshift({
|
|
782
|
+
name: "auth:extract",
|
|
783
|
+
fn: (ctx, state) => {
|
|
784
|
+
const authHeader = ctx.req.header("authorization");
|
|
785
|
+
if (authHeader) {
|
|
786
|
+
const token = authHeader.split(" ")[1];
|
|
787
|
+
try {
|
|
788
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
|
789
|
+
return { user: payload };
|
|
790
|
+
} catch (e) {
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return state;
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
return this;
|
|
797
|
+
}
|
|
798
|
+
// Query Configuration Steps
|
|
799
|
+
select(fields) {
|
|
800
|
+
this.steps.push({
|
|
801
|
+
name: "select",
|
|
802
|
+
fn: (ctx, state) => ({ ...state, _query_select: fields })
|
|
803
|
+
});
|
|
804
|
+
return this;
|
|
805
|
+
}
|
|
806
|
+
sort(field, direction = "ASC") {
|
|
807
|
+
this.steps.push({
|
|
808
|
+
name: "sort",
|
|
809
|
+
fn: (ctx, state) => ({ ...state, _query_sort: { field, direction } })
|
|
810
|
+
});
|
|
811
|
+
return this;
|
|
812
|
+
}
|
|
813
|
+
paginate(options) {
|
|
814
|
+
this.steps.push({
|
|
815
|
+
name: "paginate",
|
|
816
|
+
fn: (ctx, state) => ({ ...state, _query_paginate: options })
|
|
817
|
+
});
|
|
818
|
+
return this;
|
|
819
|
+
}
|
|
820
|
+
autoFilter(table, allow, options = {}) {
|
|
821
|
+
const sortAllow = options.sort || allow;
|
|
822
|
+
const selectAllow = options.select || allow;
|
|
823
|
+
const defaultSort = options.defaultSort || "id";
|
|
824
|
+
const defaultSortSafe = sortAllow.length > 0 ? sortAllow.includes(defaultSort) ? defaultSort : sortAllow[0] : defaultSort;
|
|
825
|
+
const defaultDirection = options.defaultDirection || "DESC";
|
|
826
|
+
const maxLimit = options.maxLimit || 100;
|
|
827
|
+
const pageParam = options.pageParam || "page";
|
|
828
|
+
const limitParam = options.limitParam || "limit";
|
|
829
|
+
const sortParam = options.sortParam || "sort";
|
|
830
|
+
const fieldsParam = options.fieldsParam || "fields";
|
|
831
|
+
const existingQueryBuilder = this.queryBuilder;
|
|
832
|
+
this.queryBuilder = (q) => {
|
|
833
|
+
if (existingQueryBuilder) existingQueryBuilder(q);
|
|
834
|
+
for (const field of allow) {
|
|
835
|
+
q.string(field).optional();
|
|
836
|
+
}
|
|
837
|
+
q.int(pageParam).optional().min(1);
|
|
838
|
+
q.int(limitParam).optional().min(1).max(maxLimit);
|
|
839
|
+
q.string(sortParam).optional();
|
|
840
|
+
q.string(fieldsParam).optional();
|
|
841
|
+
};
|
|
842
|
+
this.steps.push({
|
|
843
|
+
name: `autoFilter:${table}`,
|
|
844
|
+
fn: async (ctx, state) => {
|
|
845
|
+
if (!ctx.db) throw { status: 500, message: "Database not connected" };
|
|
846
|
+
const query = ctx.query;
|
|
847
|
+
const filters = {};
|
|
848
|
+
for (const field of allow) {
|
|
849
|
+
if (query[field] !== void 0) {
|
|
850
|
+
filters[field] = query[field];
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
let selectFields = "*";
|
|
854
|
+
const fieldsRaw = query[fieldsParam];
|
|
855
|
+
if (typeof fieldsRaw === "string") {
|
|
856
|
+
const parts = fieldsRaw.split(",").map((item) => item.trim()).filter((item) => selectAllow.includes(item));
|
|
857
|
+
if (parts.length > 0) {
|
|
858
|
+
selectFields = parts.join(", ");
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
let sortField = defaultSortSafe;
|
|
862
|
+
let sortDirection = defaultDirection;
|
|
863
|
+
const sortRaw = typeof query[sortParam] === "string" ? query[sortParam] : void 0;
|
|
864
|
+
if (sortRaw) {
|
|
865
|
+
let field = sortRaw;
|
|
866
|
+
let direction = defaultDirection;
|
|
867
|
+
if (sortRaw.includes(":")) {
|
|
868
|
+
const [f, d] = sortRaw.split(":");
|
|
869
|
+
field = f;
|
|
870
|
+
direction = d?.toLowerCase() === "desc" ? "DESC" : "ASC";
|
|
871
|
+
} else if (sortRaw.endsWith("_desc")) {
|
|
872
|
+
field = sortRaw.replace("_desc", "");
|
|
873
|
+
direction = "DESC";
|
|
874
|
+
} else if (sortRaw.endsWith("_asc")) {
|
|
875
|
+
field = sortRaw.replace("_asc", "");
|
|
876
|
+
direction = "ASC";
|
|
877
|
+
}
|
|
878
|
+
if (sortAllow.includes(field)) {
|
|
879
|
+
sortField = field;
|
|
880
|
+
sortDirection = direction;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
let limit;
|
|
884
|
+
if (typeof query[limitParam] === "number") {
|
|
885
|
+
limit = query[limitParam];
|
|
886
|
+
} else if (typeof query[limitParam] === "string") {
|
|
887
|
+
const parsed = Number.parseInt(query[limitParam], 10);
|
|
888
|
+
if (!Number.isNaN(parsed)) limit = parsed;
|
|
889
|
+
}
|
|
890
|
+
if (limit !== void 0) {
|
|
891
|
+
limit = Math.min(Math.max(limit, 1), maxLimit);
|
|
892
|
+
}
|
|
893
|
+
let page = typeof query[pageParam] === "number" ? query[pageParam] : 1;
|
|
894
|
+
if (typeof query[pageParam] === "string") {
|
|
895
|
+
const parsed = Number.parseInt(query[pageParam], 10);
|
|
896
|
+
if (!Number.isNaN(parsed)) page = parsed;
|
|
897
|
+
}
|
|
898
|
+
if (!page || page < 1) page = 1;
|
|
899
|
+
let sql = `SELECT ${selectFields} FROM ${table}`;
|
|
900
|
+
const conditions = [];
|
|
901
|
+
const params = [];
|
|
902
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
903
|
+
if (Array.isArray(value)) {
|
|
904
|
+
const offset = params.length;
|
|
905
|
+
for (const item of value) {
|
|
906
|
+
params.push(item);
|
|
907
|
+
}
|
|
908
|
+
const placeholdersFixed = value.map((_, idx) => `$${offset + idx + 1}`).join(", ");
|
|
909
|
+
conditions.push(`${key} IN (${placeholdersFixed})`);
|
|
910
|
+
} else {
|
|
911
|
+
params.push(value);
|
|
912
|
+
conditions.push(`${key} = $${params.length}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (conditions.length > 0) {
|
|
916
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
917
|
+
}
|
|
918
|
+
if (sortField) {
|
|
919
|
+
sql += ` ORDER BY ${sortField} ${sortDirection}`;
|
|
920
|
+
}
|
|
921
|
+
if (limit !== void 0) {
|
|
922
|
+
params.push(limit);
|
|
923
|
+
sql += ` LIMIT $${params.length}`;
|
|
924
|
+
}
|
|
925
|
+
if (limit !== void 0) {
|
|
926
|
+
const offset = (page - 1) * limit;
|
|
927
|
+
params.push(offset);
|
|
928
|
+
sql += ` OFFSET $${params.length}`;
|
|
929
|
+
}
|
|
930
|
+
const res = await ctx.db.queryWithParams(sql, params);
|
|
931
|
+
const rows = JSON.parse(res);
|
|
932
|
+
return {
|
|
933
|
+
data: rows,
|
|
934
|
+
meta: {
|
|
935
|
+
page,
|
|
936
|
+
limit,
|
|
937
|
+
sort: { field: sortField, direction: sortDirection },
|
|
938
|
+
filters
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
return this;
|
|
944
|
+
}
|
|
945
|
+
// Database List (Select with filter)
|
|
946
|
+
list(table, options = {}) {
|
|
947
|
+
this.steps.push({
|
|
948
|
+
name: `list:${table}`,
|
|
949
|
+
fn: async (ctx, state) => {
|
|
950
|
+
if (!ctx.db) throw { status: 500, message: "Database not connected" };
|
|
951
|
+
const fields = state._query_select ? state._query_select.join(", ") : "*";
|
|
952
|
+
let sql = `SELECT ${fields} FROM ${table}`;
|
|
953
|
+
const conditions = [];
|
|
954
|
+
const params = [];
|
|
955
|
+
if (options.where) {
|
|
956
|
+
for (const [key, valRef] of Object.entries(options.where)) {
|
|
957
|
+
let val;
|
|
958
|
+
if (valRef.startsWith("@")) {
|
|
959
|
+
const path = valRef.substring(1).split(".");
|
|
960
|
+
val = path.reduce((acc, part) => acc && acc[part], state);
|
|
961
|
+
} else {
|
|
962
|
+
val = valRef;
|
|
963
|
+
}
|
|
964
|
+
if (val === void 0) continue;
|
|
965
|
+
conditions.push(`${key} = $${params.length + 1}`);
|
|
966
|
+
params.push(val);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (conditions.length > 0) {
|
|
970
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
971
|
+
}
|
|
972
|
+
if (state._query_sort) {
|
|
973
|
+
sql += ` ORDER BY ${state._query_sort.field} ${state._query_sort.direction}`;
|
|
974
|
+
} else {
|
|
975
|
+
sql += " ORDER BY id DESC";
|
|
976
|
+
}
|
|
977
|
+
const limit = options.limit || state._query_paginate?.limit;
|
|
978
|
+
if (limit) {
|
|
979
|
+
sql += ` LIMIT ${limit}`;
|
|
980
|
+
}
|
|
981
|
+
if (state._query_paginate?.page && limit) {
|
|
982
|
+
const offset = (state._query_paginate.page - 1) * limit;
|
|
983
|
+
sql += ` OFFSET ${offset}`;
|
|
984
|
+
}
|
|
985
|
+
const res = await ctx.db.queryWithParams(sql, params);
|
|
986
|
+
return JSON.parse(res);
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
return this;
|
|
990
|
+
}
|
|
991
|
+
// Database Update
|
|
992
|
+
update(table, options) {
|
|
993
|
+
this.steps.push({
|
|
994
|
+
name: `update:${table}`,
|
|
995
|
+
fn: async (ctx, state) => {
|
|
996
|
+
if (!ctx.db) throw { status: 500, message: "Database not connected" };
|
|
997
|
+
const body = ctx.req.json();
|
|
998
|
+
const params = [];
|
|
999
|
+
const updates = [];
|
|
1000
|
+
for (const [key, val] of Object.entries(body)) {
|
|
1001
|
+
if (key === "id") continue;
|
|
1002
|
+
if (options.fields && !options.fields.includes(key)) continue;
|
|
1003
|
+
updates.push(`${key} = $${params.length + 1}`);
|
|
1004
|
+
params.push(val);
|
|
1005
|
+
}
|
|
1006
|
+
if (updates.length === 0) return state;
|
|
1007
|
+
const conditions = [];
|
|
1008
|
+
for (const [key, valRef] of Object.entries(options.where)) {
|
|
1009
|
+
let val;
|
|
1010
|
+
if (valRef.startsWith("@")) {
|
|
1011
|
+
const path = valRef.substring(1).split(".");
|
|
1012
|
+
val = path.reduce((acc, part) => acc && acc[part], state);
|
|
1013
|
+
} else if (valRef.startsWith(":")) {
|
|
1014
|
+
val = ctx.req.param(valRef.substring(1));
|
|
1015
|
+
} else {
|
|
1016
|
+
val = valRef;
|
|
1017
|
+
}
|
|
1018
|
+
if (val === void 0) throw { status: 400, message: `Missing value for ${key}` };
|
|
1019
|
+
conditions.push(`${key} = $${params.length + 1}`);
|
|
1020
|
+
params.push(val);
|
|
1021
|
+
}
|
|
1022
|
+
const sql = `UPDATE ${table} SET ${updates.join(", ")} WHERE ${conditions.join(" AND ")} RETURNING *`;
|
|
1023
|
+
const res = await ctx.db.queryWithParams(sql, params);
|
|
1024
|
+
const rows = JSON.parse(res);
|
|
1025
|
+
if (rows.length === 0) throw { status: 404, message: "Resource not found" };
|
|
1026
|
+
return rows[0];
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
return this;
|
|
1030
|
+
}
|
|
1031
|
+
// Database Soft Delete
|
|
1032
|
+
softDelete(table, options) {
|
|
1033
|
+
this.steps.push({
|
|
1034
|
+
name: `softDelete:${table}`,
|
|
1035
|
+
fn: async (ctx, state) => {
|
|
1036
|
+
if (!ctx.db) throw { status: 500, message: "Database not connected" };
|
|
1037
|
+
const conditions = [];
|
|
1038
|
+
const params = [];
|
|
1039
|
+
for (const [key, valRef] of Object.entries(options.where)) {
|
|
1040
|
+
let val;
|
|
1041
|
+
if (valRef.startsWith("@")) {
|
|
1042
|
+
const path = valRef.substring(1).split(".");
|
|
1043
|
+
val = path.reduce((acc, part) => acc && acc[part], state);
|
|
1044
|
+
} else if (valRef.startsWith(":")) {
|
|
1045
|
+
val = ctx.req.param(valRef.substring(1));
|
|
1046
|
+
} else {
|
|
1047
|
+
val = valRef;
|
|
1048
|
+
}
|
|
1049
|
+
if (val === void 0) throw { status: 400, message: `Missing value for ${key}` };
|
|
1050
|
+
conditions.push(`${key} = $${params.length + 1}`);
|
|
1051
|
+
params.push(val);
|
|
1052
|
+
}
|
|
1053
|
+
const sql = `UPDATE ${table} SET deleted_at = NOW() WHERE ${conditions.join(" AND ")} RETURNING *`;
|
|
1054
|
+
const res = await ctx.db.queryWithParams(sql, params);
|
|
1055
|
+
const rows = JSON.parse(res);
|
|
1056
|
+
if (rows.length === 0) throw { status: 404, message: "Resource not found" };
|
|
1057
|
+
return { success: true, deleted: rows[0] };
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
return this;
|
|
1061
|
+
}
|
|
1062
|
+
// Database Delete
|
|
1063
|
+
delete(table, options) {
|
|
1064
|
+
this.steps.push({
|
|
1065
|
+
name: `delete:${table}`,
|
|
1066
|
+
fn: async (ctx, state) => {
|
|
1067
|
+
if (!ctx.db) throw { status: 500, message: "Database not connected" };
|
|
1068
|
+
const conditions = [];
|
|
1069
|
+
const params = [];
|
|
1070
|
+
for (const [key, valRef] of Object.entries(options.where)) {
|
|
1071
|
+
let val;
|
|
1072
|
+
if (valRef.startsWith("@")) {
|
|
1073
|
+
const path = valRef.substring(1).split(".");
|
|
1074
|
+
val = path.reduce((acc, part) => acc && acc[part], state);
|
|
1075
|
+
} else if (valRef.startsWith(":")) {
|
|
1076
|
+
val = ctx.req.param(valRef.substring(1));
|
|
1077
|
+
} else {
|
|
1078
|
+
val = valRef;
|
|
1079
|
+
}
|
|
1080
|
+
if (val === void 0) throw { status: 400, message: `Missing value for ${key}` };
|
|
1081
|
+
conditions.push(`${key} = $${params.length + 1}`);
|
|
1082
|
+
params.push(val);
|
|
1083
|
+
}
|
|
1084
|
+
const sql = `DELETE FROM ${table} WHERE ${conditions.join(" AND ")} RETURNING *`;
|
|
1085
|
+
const res = await ctx.db.queryWithParams(sql, params);
|
|
1086
|
+
const rows = JSON.parse(res);
|
|
1087
|
+
if (rows.length === 0) throw { status: 404, message: "Resource not found" };
|
|
1088
|
+
return { success: true, deleted: rows[0] };
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
return this;
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
// src/index.ts
|
|
1096
|
+
loadEnv();
|
|
1097
|
+
var Q = class {
|
|
1098
|
+
static env = env;
|
|
1099
|
+
static loadEnv = loadEnv;
|
|
1100
|
+
static app(config) {
|
|
1101
|
+
return new App(config);
|
|
1102
|
+
}
|
|
1103
|
+
static schema(def) {
|
|
1104
|
+
const properties = {};
|
|
1105
|
+
for (const key in def) {
|
|
1106
|
+
if (def[key] && typeof def[key].toJSON === "function") {
|
|
1107
|
+
properties[key] = def[key].toJSON();
|
|
1108
|
+
} else {
|
|
1109
|
+
properties[key] = def[key];
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
type: "object",
|
|
1114
|
+
properties,
|
|
1115
|
+
required: Object.keys(properties)
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
static string() {
|
|
1119
|
+
return new SchemaBuilder("string");
|
|
1120
|
+
}
|
|
1121
|
+
static int() {
|
|
1122
|
+
return new SchemaBuilder("integer");
|
|
1123
|
+
}
|
|
1124
|
+
static email() {
|
|
1125
|
+
return new SchemaBuilder("string").format("email");
|
|
1126
|
+
}
|
|
1127
|
+
static enum(...values) {
|
|
1128
|
+
return new SchemaBuilder("string").enum(values);
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
var SchemaBuilder = class {
|
|
1132
|
+
def = {};
|
|
1133
|
+
constructor(type) {
|
|
1134
|
+
this.def.type = type;
|
|
1135
|
+
}
|
|
1136
|
+
min(val) {
|
|
1137
|
+
if (this.def.type === "string") this.def.minLength = val;
|
|
1138
|
+
else this.def.minimum = val;
|
|
1139
|
+
return this;
|
|
1140
|
+
}
|
|
1141
|
+
max(val) {
|
|
1142
|
+
if (this.def.type === "string") this.def.maxLength = val;
|
|
1143
|
+
else this.def.maximum = val;
|
|
1144
|
+
return this;
|
|
1145
|
+
}
|
|
1146
|
+
format(fmt) {
|
|
1147
|
+
this.def.format = fmt;
|
|
1148
|
+
return this;
|
|
1149
|
+
}
|
|
1150
|
+
enum(values) {
|
|
1151
|
+
this.def.enum = values;
|
|
1152
|
+
return this;
|
|
1153
|
+
}
|
|
1154
|
+
toJSON() {
|
|
1155
|
+
return this.def;
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
var QuerySchemaBuilder = class {
|
|
1159
|
+
schema = {};
|
|
1160
|
+
string(name) {
|
|
1161
|
+
return new QueryFieldBuilder(this.schema, name, "string");
|
|
1162
|
+
}
|
|
1163
|
+
int(name) {
|
|
1164
|
+
return new QueryFieldBuilder(this.schema, name, "integer");
|
|
1165
|
+
}
|
|
1166
|
+
bool(name) {
|
|
1167
|
+
return new QueryFieldBuilder(this.schema, name, "boolean");
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
var QueryFieldBuilder = class {
|
|
1171
|
+
constructor(schema, name, type) {
|
|
1172
|
+
this.schema = schema;
|
|
1173
|
+
this.name = name;
|
|
1174
|
+
this.type = type;
|
|
1175
|
+
if (!this.schema[this.name]) {
|
|
1176
|
+
this.schema[this.name] = { type: this.type };
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
default(val) {
|
|
1180
|
+
this.schema[this.name].default = val;
|
|
1181
|
+
return this;
|
|
1182
|
+
}
|
|
1183
|
+
optional() {
|
|
1184
|
+
this.schema[this.name].optional = true;
|
|
1185
|
+
return this;
|
|
1186
|
+
}
|
|
1187
|
+
max(val) {
|
|
1188
|
+
this.schema[this.name].max = val;
|
|
1189
|
+
return this;
|
|
1190
|
+
}
|
|
1191
|
+
min(val) {
|
|
1192
|
+
this.schema[this.name].min = val;
|
|
1193
|
+
return this;
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
var normalizeQuery = (schema, ctx) => {
|
|
1197
|
+
const errors = [];
|
|
1198
|
+
const normalized = { ...ctx.query };
|
|
1199
|
+
const parseValue = (value, field) => {
|
|
1200
|
+
if (field.type === "integer") {
|
|
1201
|
+
const parsed = Number.parseInt(value, 10);
|
|
1202
|
+
if (Number.isNaN(parsed)) {
|
|
1203
|
+
return { ok: false, value };
|
|
1204
|
+
}
|
|
1205
|
+
return { ok: true, value: parsed };
|
|
1206
|
+
}
|
|
1207
|
+
if (field.type === "boolean") {
|
|
1208
|
+
if (value === "true" || value === "1") return { ok: true, value: true };
|
|
1209
|
+
if (value === "false" || value === "0") return { ok: true, value: false };
|
|
1210
|
+
return { ok: false, value };
|
|
1211
|
+
}
|
|
1212
|
+
return { ok: true, value };
|
|
1213
|
+
};
|
|
1214
|
+
const enforceBounds = (value, field) => {
|
|
1215
|
+
if (typeof value === "string") {
|
|
1216
|
+
if (field.min !== void 0 && value.length < field.min) return false;
|
|
1217
|
+
if (field.max !== void 0 && value.length > field.max) return false;
|
|
1218
|
+
}
|
|
1219
|
+
if (typeof value === "number") {
|
|
1220
|
+
if (field.min !== void 0 && value < field.min) return false;
|
|
1221
|
+
if (field.max !== void 0 && value > field.max) return false;
|
|
1222
|
+
}
|
|
1223
|
+
return true;
|
|
1224
|
+
};
|
|
1225
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
1226
|
+
const raw = ctx.query[key];
|
|
1227
|
+
if (raw === void 0) {
|
|
1228
|
+
if (field.default !== void 0) {
|
|
1229
|
+
normalized[key] = field.default;
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (field.optional) {
|
|
1233
|
+
delete normalized[key];
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
errors.push(`Missing query param: ${key}`);
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
if (Array.isArray(raw)) {
|
|
1240
|
+
const parsedValues = [];
|
|
1241
|
+
let ok = true;
|
|
1242
|
+
for (const item of raw) {
|
|
1243
|
+
const parsed2 = parseValue(item, field);
|
|
1244
|
+
if (!parsed2.ok) {
|
|
1245
|
+
ok = false;
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
if (!enforceBounds(parsed2.value, field)) {
|
|
1249
|
+
ok = false;
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
parsedValues.push(parsed2.value);
|
|
1253
|
+
}
|
|
1254
|
+
if (!ok) {
|
|
1255
|
+
errors.push(`Invalid query param: ${key}`);
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
normalized[key] = parsedValues;
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
const parsed = parseValue(raw, field);
|
|
1262
|
+
if (!parsed.ok || !enforceBounds(parsed.value, field)) {
|
|
1263
|
+
errors.push(`Invalid query param: ${key}`);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
normalized[key] = parsed.value;
|
|
1267
|
+
}
|
|
1268
|
+
if (errors.length > 0) {
|
|
1269
|
+
return { ok: false, errors };
|
|
1270
|
+
}
|
|
1271
|
+
return { ok: true, query: normalized };
|
|
1272
|
+
};
|
|
1273
|
+
var normalizeRoutePath = (path) => {
|
|
1274
|
+
return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
|
|
1275
|
+
};
|
|
1276
|
+
var App = class {
|
|
1277
|
+
constructor(config) {
|
|
1278
|
+
this.config = config;
|
|
1279
|
+
this.db = {
|
|
1280
|
+
connectPostgres: async (url) => {
|
|
1281
|
+
if (!this.engine) throw new Error("Server not started");
|
|
1282
|
+
await this.engine.connectPostgres(url);
|
|
1283
|
+
},
|
|
1284
|
+
connectSqlite: async (url) => {
|
|
1285
|
+
if (!this.engine) throw new Error("Server not started");
|
|
1286
|
+
await this.engine.connectSqlite(url);
|
|
1287
|
+
},
|
|
1288
|
+
connectRedis: (url) => {
|
|
1289
|
+
if (!this.engine) throw new Error("Server not started");
|
|
1290
|
+
this.engine.connectRedis(url);
|
|
1291
|
+
},
|
|
1292
|
+
connectMongo: async (url) => {
|
|
1293
|
+
if (!this.engine) throw new Error("Server not started");
|
|
1294
|
+
await this.engine.connectMongo(url);
|
|
1295
|
+
},
|
|
1296
|
+
query: async (sql, ttl) => {
|
|
1297
|
+
if (!this.engine) throw new Error("Server not started");
|
|
1298
|
+
return this.engine.queryDb(sql, ttl);
|
|
1299
|
+
},
|
|
1300
|
+
redis: {
|
|
1301
|
+
set: async (key, value, ttl) => {
|
|
1302
|
+
if (!this.engine) throw new Error("Server not started");
|
|
1303
|
+
await this.engine.redisSet(key, value, ttl);
|
|
1304
|
+
},
|
|
1305
|
+
get: async (key) => {
|
|
1306
|
+
if (!this.engine) throw new Error("Server not started");
|
|
1307
|
+
return this.engine.redisGet(key);
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
mongo: (dbName, collName) => ({
|
|
1311
|
+
find: async (query) => {
|
|
1312
|
+
if (!this.engine) throw new Error("Server not started");
|
|
1313
|
+
const json = await this.engine.queryMongo(dbName, collName, JSON.stringify(query));
|
|
1314
|
+
return JSON.parse(json);
|
|
1315
|
+
}
|
|
1316
|
+
})
|
|
1317
|
+
};
|
|
1318
|
+
this.auth = {
|
|
1319
|
+
setJwtSecret: (secret) => {
|
|
1320
|
+
if (!this.engine) throw new Error("Server not started");
|
|
1321
|
+
this.engine.setJwtSecret(secret);
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
engine = null;
|
|
1326
|
+
routes = [];
|
|
1327
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1328
|
+
handlerCounter = 0;
|
|
1329
|
+
middlewares = [];
|
|
1330
|
+
staticRoutes = /* @__PURE__ */ new Map();
|
|
1331
|
+
// prefix -> dir
|
|
1332
|
+
wsHandlers = /* @__PURE__ */ new Map();
|
|
1333
|
+
activeSockets = /* @__PURE__ */ new Map();
|
|
1334
|
+
corsConfig = null;
|
|
1335
|
+
loggingEnabled = false;
|
|
1336
|
+
errorHandler = null;
|
|
1337
|
+
errorHooksRegistered = false;
|
|
1338
|
+
shutdownHooksRegistered = false;
|
|
1339
|
+
// Fluent API Entry Point
|
|
1340
|
+
flow(method, path) {
|
|
1341
|
+
return new FluentBuilder(this, method, path);
|
|
1342
|
+
}
|
|
1343
|
+
doc(path) {
|
|
1344
|
+
this.get(`${path}/json`, (req) => {
|
|
1345
|
+
const spec = this.generateOpenApiSpec();
|
|
1346
|
+
return req.send(spec);
|
|
1347
|
+
});
|
|
1348
|
+
this.get(path, (req) => {
|
|
1349
|
+
const html = `
|
|
1350
|
+
<!DOCTYPE html>
|
|
1351
|
+
<html lang="en">
|
|
1352
|
+
<head>
|
|
1353
|
+
<meta charset="utf-8" />
|
|
1354
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1355
|
+
<title>API Documentation</title>
|
|
1356
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
|
1357
|
+
</head>
|
|
1358
|
+
<body>
|
|
1359
|
+
<div id="swagger-ui"></div>
|
|
1360
|
+
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" crossorigin></script>
|
|
1361
|
+
<script>
|
|
1362
|
+
window.onload = () => {
|
|
1363
|
+
window.ui = SwaggerUIBundle({
|
|
1364
|
+
url: '${path}/json',
|
|
1365
|
+
dom_id: '#swagger-ui',
|
|
1366
|
+
});
|
|
1367
|
+
};
|
|
1368
|
+
</script>
|
|
1369
|
+
</body>
|
|
1370
|
+
</html>`;
|
|
1371
|
+
return req.html(html);
|
|
1372
|
+
});
|
|
1373
|
+
return this;
|
|
1374
|
+
}
|
|
1375
|
+
exportClient(outputPath) {
|
|
1376
|
+
const code = generateClient(this.routes);
|
|
1377
|
+
fs.writeFileSync(outputPath, code);
|
|
1378
|
+
console.log(`\u2705 Generated Type-Safe Client at ${outputPath}`);
|
|
1379
|
+
}
|
|
1380
|
+
generateOpenApiSpec() {
|
|
1381
|
+
const paths = {};
|
|
1382
|
+
for (const route of this.routes) {
|
|
1383
|
+
if (route.path.includes("/docs")) continue;
|
|
1384
|
+
const specPath = normalizeRoutePath(route.path);
|
|
1385
|
+
if (!paths[specPath]) paths[specPath] = {};
|
|
1386
|
+
const method = route.method.toLowerCase();
|
|
1387
|
+
const operation = {
|
|
1388
|
+
responses: {
|
|
1389
|
+
"200": { description: "Successful response" }
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
if (route.description) {
|
|
1393
|
+
operation.summary = route.description;
|
|
1394
|
+
}
|
|
1395
|
+
if (route.options.schema) {
|
|
1396
|
+
try {
|
|
1397
|
+
const schema = typeof route.options.schema === "string" ? JSON.parse(route.options.schema) : route.options.schema;
|
|
1398
|
+
operation.requestBody = {
|
|
1399
|
+
content: {
|
|
1400
|
+
"application/json": {
|
|
1401
|
+
schema
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
} catch (e) {
|
|
1406
|
+
console.warn(`Failed to parse schema for ${method} ${route.path}`);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (route.options.query_schema) {
|
|
1410
|
+
const params = [];
|
|
1411
|
+
const querySchema = route.options.query_schema;
|
|
1412
|
+
for (const [name, def] of Object.entries(querySchema)) {
|
|
1413
|
+
params.push({
|
|
1414
|
+
name,
|
|
1415
|
+
in: "query",
|
|
1416
|
+
required: !def.optional && def.default === void 0,
|
|
1417
|
+
schema: {
|
|
1418
|
+
type: def.type === "integer" ? "integer" : def.type === "boolean" ? "boolean" : "string",
|
|
1419
|
+
minimum: def.min,
|
|
1420
|
+
maximum: def.max
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
if (params.length > 0) {
|
|
1425
|
+
operation.parameters = params;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (route.options.response_schema) {
|
|
1429
|
+
try {
|
|
1430
|
+
const schema = typeof route.options.response_schema === "string" ? JSON.parse(route.options.response_schema) : route.options.response_schema;
|
|
1431
|
+
operation.responses["200"] = {
|
|
1432
|
+
description: "Successful response",
|
|
1433
|
+
content: {
|
|
1434
|
+
"application/json": { schema }
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
} catch (e) {
|
|
1438
|
+
console.warn(`Failed to parse response schema for ${method} ${route.path}`);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (route.options.jwt_auth) {
|
|
1442
|
+
operation.security = [{ bearerAuth: [] }];
|
|
1443
|
+
}
|
|
1444
|
+
paths[specPath][method] = operation;
|
|
1445
|
+
}
|
|
1446
|
+
return {
|
|
1447
|
+
openapi: "3.0.0",
|
|
1448
|
+
info: {
|
|
1449
|
+
title: "QHTTPX API",
|
|
1450
|
+
version: "1.0.0"
|
|
1451
|
+
},
|
|
1452
|
+
components: {
|
|
1453
|
+
securitySchemes: {
|
|
1454
|
+
bearerAuth: {
|
|
1455
|
+
type: "http",
|
|
1456
|
+
scheme: "bearer",
|
|
1457
|
+
bearerFormat: "JWT"
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
},
|
|
1461
|
+
paths
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
// DB & Auth namespaces
|
|
1465
|
+
enableLogging() {
|
|
1466
|
+
this.engine?.initLogger();
|
|
1467
|
+
this.loggingEnabled = true;
|
|
1468
|
+
if (!process.env.RUST_LOG) {
|
|
1469
|
+
process.env.RUST_LOG = "info";
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
getMetrics() {
|
|
1473
|
+
if (!this.engine) throw new Error("Server not started");
|
|
1474
|
+
return this.engine.getMetrics();
|
|
1475
|
+
}
|
|
1476
|
+
onError(handler) {
|
|
1477
|
+
this.errorHandler = handler;
|
|
1478
|
+
return this;
|
|
1479
|
+
}
|
|
1480
|
+
gracefulShutdown(signals = ["SIGINT", "SIGTERM"]) {
|
|
1481
|
+
if (this.shutdownHooksRegistered) return this;
|
|
1482
|
+
this.shutdownHooksRegistered = true;
|
|
1483
|
+
for (const signal of signals) {
|
|
1484
|
+
process.on(signal, () => {
|
|
1485
|
+
this.stop();
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
return this;
|
|
1489
|
+
}
|
|
1490
|
+
security() {
|
|
1491
|
+
if (this.engine) {
|
|
1492
|
+
this.engine.setSecurityHeaders(true);
|
|
1493
|
+
} else {
|
|
1494
|
+
this.pendingSecurity = true;
|
|
1495
|
+
}
|
|
1496
|
+
return this;
|
|
1497
|
+
}
|
|
1498
|
+
pendingSecurity = false;
|
|
1499
|
+
registerErrorHooks() {
|
|
1500
|
+
if (this.errorHooksRegistered) return;
|
|
1501
|
+
this.errorHooksRegistered = true;
|
|
1502
|
+
process.on("unhandledRejection", (reason) => {
|
|
1503
|
+
this.handleError(reason);
|
|
1504
|
+
});
|
|
1505
|
+
process.on("uncaughtException", (err) => {
|
|
1506
|
+
this.handleError(err);
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
handleError(err, ctx) {
|
|
1510
|
+
if (this.errorHandler) {
|
|
1511
|
+
try {
|
|
1512
|
+
this.errorHandler(err, ctx);
|
|
1513
|
+
return;
|
|
1514
|
+
} catch (handlerError) {
|
|
1515
|
+
console.error("Error handler failed:", handlerError);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
console.error("Unhandled error:", err);
|
|
1519
|
+
}
|
|
1520
|
+
db;
|
|
1521
|
+
auth;
|
|
1522
|
+
static(prefix, root) {
|
|
1523
|
+
if (!prefix.startsWith("/")) prefix = "/" + prefix;
|
|
1524
|
+
const absRoot = __require("path").resolve(root);
|
|
1525
|
+
this.staticRoutes.set(prefix, absRoot);
|
|
1526
|
+
return this;
|
|
1527
|
+
}
|
|
1528
|
+
use(fn) {
|
|
1529
|
+
this.middlewares.push(fn);
|
|
1530
|
+
return this;
|
|
1531
|
+
}
|
|
1532
|
+
ws(path, handler) {
|
|
1533
|
+
if (!path.startsWith("/")) path = "/" + path;
|
|
1534
|
+
this.wsHandlers.set(path, handler);
|
|
1535
|
+
}
|
|
1536
|
+
cors(config = {}) {
|
|
1537
|
+
this.corsConfig = {
|
|
1538
|
+
origin: config.origin || "*",
|
|
1539
|
+
methods: (config.methods || ["GET", "POST", "PUT", "DELETE", "OPTIONS"]).join(", "),
|
|
1540
|
+
headers: (config.headers || ["Content-Type", "Authorization"]).join(", "),
|
|
1541
|
+
credentials: config.credentials || false
|
|
1542
|
+
};
|
|
1543
|
+
return this;
|
|
1544
|
+
}
|
|
1545
|
+
get(path, arg2, arg3) {
|
|
1546
|
+
const fluentBuilder = new FluentBuilder(this, "GET", path);
|
|
1547
|
+
if (arg3) {
|
|
1548
|
+
const route = this.addRoute("GET", path);
|
|
1549
|
+
route.options = arg2;
|
|
1550
|
+
route.handler = arg3;
|
|
1551
|
+
} else if (arg2) {
|
|
1552
|
+
const route = this.addRoute("GET", path);
|
|
1553
|
+
if (typeof arg2 === "function") {
|
|
1554
|
+
route.handler = arg2;
|
|
1555
|
+
} else if ("json" in arg2 || "text" in arg2 || "upload" in arg2) {
|
|
1556
|
+
route.routeConfig = arg2;
|
|
1557
|
+
} else {
|
|
1558
|
+
route.options = arg2;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return fluentBuilder;
|
|
1562
|
+
}
|
|
1563
|
+
post(path, arg2, arg3) {
|
|
1564
|
+
const fluentBuilder = new FluentBuilder(this, "POST", path);
|
|
1565
|
+
if (arg3) {
|
|
1566
|
+
const route = this.addRoute("POST", path);
|
|
1567
|
+
route.options = arg2;
|
|
1568
|
+
route.handler = arg3;
|
|
1569
|
+
} else if (arg2) {
|
|
1570
|
+
const route = this.addRoute("POST", path);
|
|
1571
|
+
if (typeof arg2 === "function") {
|
|
1572
|
+
route.handler = arg2;
|
|
1573
|
+
} else if ("json" in arg2 || "text" in arg2 || "upload" in arg2) {
|
|
1574
|
+
route.routeConfig = arg2;
|
|
1575
|
+
} else {
|
|
1576
|
+
route.options = arg2;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return fluentBuilder;
|
|
1580
|
+
}
|
|
1581
|
+
put(path, arg2, arg3) {
|
|
1582
|
+
const fluentBuilder = new FluentBuilder(this, "PUT", path);
|
|
1583
|
+
if (arg3) {
|
|
1584
|
+
const route = this.addRoute("PUT", path);
|
|
1585
|
+
route.options = arg2;
|
|
1586
|
+
route.handler = arg3;
|
|
1587
|
+
} else if (arg2) {
|
|
1588
|
+
const route = this.addRoute("PUT", path);
|
|
1589
|
+
if (typeof arg2 === "function") {
|
|
1590
|
+
route.handler = arg2;
|
|
1591
|
+
} else if ("json" in arg2 || "text" in arg2 || "upload" in arg2) {
|
|
1592
|
+
route.routeConfig = arg2;
|
|
1593
|
+
} else {
|
|
1594
|
+
route.options = arg2;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return fluentBuilder;
|
|
1598
|
+
}
|
|
1599
|
+
delete(path, arg2, arg3) {
|
|
1600
|
+
const fluentBuilder = new FluentBuilder(this, "DELETE", path);
|
|
1601
|
+
if (arg3) {
|
|
1602
|
+
const route = this.addRoute("DELETE", path);
|
|
1603
|
+
route.options = arg2;
|
|
1604
|
+
route.handler = arg3;
|
|
1605
|
+
} else if (arg2) {
|
|
1606
|
+
const route = this.addRoute("DELETE", path);
|
|
1607
|
+
if (typeof arg2 === "function") {
|
|
1608
|
+
route.handler = arg2;
|
|
1609
|
+
} else if ("json" in arg2 || "text" in arg2 || "upload" in arg2) {
|
|
1610
|
+
route.routeConfig = arg2;
|
|
1611
|
+
} else {
|
|
1612
|
+
route.options = arg2;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return fluentBuilder;
|
|
1616
|
+
}
|
|
1617
|
+
addRoute(method, path) {
|
|
1618
|
+
const route = new Route(path, method, this);
|
|
1619
|
+
this.routes.push(route);
|
|
1620
|
+
return route;
|
|
1621
|
+
}
|
|
1622
|
+
registerHandler(handler, serializer) {
|
|
1623
|
+
const id = ++this.handlerCounter;
|
|
1624
|
+
this.handlers.set(id, { handler, serializer });
|
|
1625
|
+
return id;
|
|
1626
|
+
}
|
|
1627
|
+
listen(portOrOptions, cb) {
|
|
1628
|
+
let port;
|
|
1629
|
+
let callback = cb;
|
|
1630
|
+
let tls;
|
|
1631
|
+
if (typeof portOrOptions === "object") {
|
|
1632
|
+
port = portOrOptions.port;
|
|
1633
|
+
callback = portOrOptions.callback || cb;
|
|
1634
|
+
tls = portOrOptions.tls;
|
|
1635
|
+
} else {
|
|
1636
|
+
port = portOrOptions;
|
|
1637
|
+
}
|
|
1638
|
+
if (!this.engine) {
|
|
1639
|
+
this.engine = new import_core.NativeEngine(port);
|
|
1640
|
+
this.registerErrorHooks();
|
|
1641
|
+
if (this.loggingEnabled || process.env.RUST_LOG) {
|
|
1642
|
+
if (!process.env.RUST_LOG) process.env.RUST_LOG = "info";
|
|
1643
|
+
this.engine.initLogger();
|
|
1644
|
+
}
|
|
1645
|
+
if (tls) {
|
|
1646
|
+
this.engine.setTls(tls.cert, tls.key);
|
|
1647
|
+
}
|
|
1648
|
+
for (const [prefix, dir] of this.staticRoutes) {
|
|
1649
|
+
this.engine.addStaticRoute(prefix, dir);
|
|
1650
|
+
}
|
|
1651
|
+
if (this.corsConfig) {
|
|
1652
|
+
this.engine.setCors(
|
|
1653
|
+
this.corsConfig.origin,
|
|
1654
|
+
this.corsConfig.methods,
|
|
1655
|
+
this.corsConfig.headers,
|
|
1656
|
+
this.corsConfig.credentials
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
if (this.pendingSecurity) {
|
|
1660
|
+
this.engine.setSecurityHeaders(true);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
this.engine.setHandler((event) => {
|
|
1664
|
+
const { handlerId, reqId, params, query, body, headers, url, responseHandle, method } = event;
|
|
1665
|
+
const routeConfig = this.handlers.get(handlerId);
|
|
1666
|
+
if (routeConfig) {
|
|
1667
|
+
const { handler, serializer } = routeConfig;
|
|
1668
|
+
const ctx = new Context(this.engine, reqId, params, query, body, headers, url, responseHandle, method, serializer);
|
|
1669
|
+
const routeMiddleware = async (c, next) => {
|
|
1670
|
+
const res = await handler(c);
|
|
1671
|
+
if (res !== void 0) {
|
|
1672
|
+
c.send(res);
|
|
1673
|
+
}
|
|
1674
|
+
await next();
|
|
1675
|
+
};
|
|
1676
|
+
const fn = compose([...this.middlewares, routeMiddleware]);
|
|
1677
|
+
fn(ctx).catch((err) => {
|
|
1678
|
+
this.handleError(err, ctx);
|
|
1679
|
+
try {
|
|
1680
|
+
ctx.status(500).send({ error: "Internal Server Error" });
|
|
1681
|
+
} catch {
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
} else {
|
|
1685
|
+
console.warn(`No handler found for ID ${handlerId}`);
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
this.engine.setWsHandler((event) => {
|
|
1689
|
+
const { socketId, eventType, payload, path } = event;
|
|
1690
|
+
if (eventType === "open") {
|
|
1691
|
+
const handler = this.wsHandlers.get(path);
|
|
1692
|
+
if (handler) {
|
|
1693
|
+
this.activeSockets.set(socketId, { handler, path });
|
|
1694
|
+
if (handler.open) {
|
|
1695
|
+
const ws = {
|
|
1696
|
+
send: (msg) => this.engine.wsSend(socketId, msg),
|
|
1697
|
+
subscribe: (room) => this.engine.wsSubscribe(socketId, room),
|
|
1698
|
+
unsubscribe: (room) => this.engine.wsUnsubscribe(socketId, room),
|
|
1699
|
+
publish: (room, msg) => this.engine.wsPublish(room, msg)
|
|
1700
|
+
};
|
|
1701
|
+
handler.open(ws);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
} else {
|
|
1705
|
+
const ctx = this.activeSockets.get(socketId);
|
|
1706
|
+
if (ctx) {
|
|
1707
|
+
const { handler } = ctx;
|
|
1708
|
+
const ws = {
|
|
1709
|
+
send: (msg) => this.engine.wsSend(socketId, msg),
|
|
1710
|
+
subscribe: (room) => this.engine.wsSubscribe(socketId, room),
|
|
1711
|
+
unsubscribe: (room) => this.engine.wsUnsubscribe(socketId, room),
|
|
1712
|
+
publish: (room, msg) => this.engine.wsPublish(room, msg)
|
|
1713
|
+
};
|
|
1714
|
+
if (eventType === "message") {
|
|
1715
|
+
if (handler.message && payload) {
|
|
1716
|
+
handler.message(ws, payload);
|
|
1717
|
+
}
|
|
1718
|
+
} else if (eventType === "close") {
|
|
1719
|
+
if (handler.close) {
|
|
1720
|
+
handler.close(ws);
|
|
1721
|
+
}
|
|
1722
|
+
this.activeSockets.delete(socketId);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
for (const route of this.routes) {
|
|
1728
|
+
const enginePath = normalizeRoutePath(route.path);
|
|
1729
|
+
const options = route.options || {};
|
|
1730
|
+
if (options.schema && typeof options.schema !== "string") {
|
|
1731
|
+
options.schema = JSON.stringify(options.schema);
|
|
1732
|
+
}
|
|
1733
|
+
if (route.routeConfig) {
|
|
1734
|
+
if (route.routeConfig.text) {
|
|
1735
|
+
this.engine.registerStaticRoute(route.method, enginePath, route.routeConfig.text, "text/plain", options);
|
|
1736
|
+
} else if (route.routeConfig.json) {
|
|
1737
|
+
const content = typeof route.routeConfig.json === "string" ? route.routeConfig.json : JSON.stringify(route.routeConfig.json);
|
|
1738
|
+
this.engine.registerJsonRoute(route.method, enginePath, content, options);
|
|
1739
|
+
} else if (route.routeConfig.upload) {
|
|
1740
|
+
let handlerId;
|
|
1741
|
+
if (route.routeConfig.upload.handler) {
|
|
1742
|
+
handlerId = this.registerHandler(route.routeConfig.upload.handler);
|
|
1743
|
+
}
|
|
1744
|
+
this.engine.registerUploadRoute(
|
|
1745
|
+
route.method,
|
|
1746
|
+
enginePath,
|
|
1747
|
+
route.routeConfig.upload.dir,
|
|
1748
|
+
handlerId,
|
|
1749
|
+
options
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
} else if (route.handlerId) {
|
|
1753
|
+
try {
|
|
1754
|
+
this.engine.registerRoute(route.method, enginePath, route.handlerId, options);
|
|
1755
|
+
} catch (e) {
|
|
1756
|
+
console.error(`Failed to register route ${route.method} ${route.path}:`, e);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
this.engine.start().then(() => {
|
|
1761
|
+
if (cb) cb();
|
|
1762
|
+
}).catch((err) => {
|
|
1763
|
+
console.error("Native Engine crashed:", err);
|
|
1764
|
+
process.exit(1);
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
stop() {
|
|
1768
|
+
if (this.engine) {
|
|
1769
|
+
try {
|
|
1770
|
+
this.engine.stop();
|
|
1771
|
+
} catch (e) {
|
|
1772
|
+
console.error("Failed to stop engine:", e);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
var Route = class {
|
|
1778
|
+
constructor(path, method, app) {
|
|
1779
|
+
this.path = path;
|
|
1780
|
+
this.method = method;
|
|
1781
|
+
this.app = app;
|
|
1782
|
+
}
|
|
1783
|
+
handlerId = null;
|
|
1784
|
+
routeConfig;
|
|
1785
|
+
options = {};
|
|
1786
|
+
description;
|
|
1787
|
+
middlewares = [];
|
|
1788
|
+
handler;
|
|
1789
|
+
serializer;
|
|
1790
|
+
desc(description) {
|
|
1791
|
+
this.description = description;
|
|
1792
|
+
return this;
|
|
1793
|
+
}
|
|
1794
|
+
auth(strategy) {
|
|
1795
|
+
return this;
|
|
1796
|
+
}
|
|
1797
|
+
jwt() {
|
|
1798
|
+
this.options.jwt_auth = true;
|
|
1799
|
+
return this;
|
|
1800
|
+
}
|
|
1801
|
+
cache(options) {
|
|
1802
|
+
this.options.cache_ttl = options.ttl;
|
|
1803
|
+
return this;
|
|
1804
|
+
}
|
|
1805
|
+
rateLimit(options) {
|
|
1806
|
+
this.options.rate_limit_limit = options.limit;
|
|
1807
|
+
this.options.rate_limit_window = options.window;
|
|
1808
|
+
return this;
|
|
1809
|
+
}
|
|
1810
|
+
query(schemaBuilder) {
|
|
1811
|
+
const builder = new QuerySchemaBuilder();
|
|
1812
|
+
schemaBuilder(builder);
|
|
1813
|
+
this.options.query_schema = builder.schema;
|
|
1814
|
+
return this;
|
|
1815
|
+
}
|
|
1816
|
+
schema(def) {
|
|
1817
|
+
this.options.schema = def;
|
|
1818
|
+
return this;
|
|
1819
|
+
}
|
|
1820
|
+
priority(level) {
|
|
1821
|
+
this.options.priority = level;
|
|
1822
|
+
return this;
|
|
1823
|
+
}
|
|
1824
|
+
slo(targetMs) {
|
|
1825
|
+
this.options.slo_target = targetMs;
|
|
1826
|
+
return this;
|
|
1827
|
+
}
|
|
1828
|
+
responseSchema(def) {
|
|
1829
|
+
try {
|
|
1830
|
+
this.serializer = fastJson(def);
|
|
1831
|
+
this.options.response_schema = def;
|
|
1832
|
+
} catch (e) {
|
|
1833
|
+
console.error(`Failed to compile response schema for ${this.method} ${this.path}:`, e);
|
|
1834
|
+
}
|
|
1835
|
+
return this;
|
|
1836
|
+
}
|
|
1837
|
+
respond(handler) {
|
|
1838
|
+
if (typeof handler === "object" && handler !== null && ("text" in handler || "json" in handler || "upload" in handler)) {
|
|
1839
|
+
this.routeConfig = handler;
|
|
1840
|
+
} else {
|
|
1841
|
+
const original = handler;
|
|
1842
|
+
const wrapped = async (ctx) => {
|
|
1843
|
+
if (this.options.query_schema) {
|
|
1844
|
+
const result = normalizeQuery(this.options.query_schema, ctx);
|
|
1845
|
+
if (!result.ok) {
|
|
1846
|
+
const errors = "errors" in result && result.errors ? result.errors : [];
|
|
1847
|
+
ctx.status(400).send({ error: errors.join(", ") });
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
ctx._query = result.query;
|
|
1851
|
+
}
|
|
1852
|
+
return original(ctx);
|
|
1853
|
+
};
|
|
1854
|
+
this.handlerId = this.app.registerHandler(wrapped, this.serializer);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
export {
|
|
1859
|
+
FluentBuilder,
|
|
1860
|
+
Q,
|
|
1861
|
+
TestClient,
|
|
1862
|
+
createTestClient
|
|
1863
|
+
};
|