exisjs 0.4.2 → 0.4.4
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/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +43 -55
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/manifest.d.ts +1 -0
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +76 -17
- package/dist/cli/manifest.js.map +1 -1
- package/dist/exports/sanitize.d.ts +2 -0
- package/dist/exports/sanitize.d.ts.map +1 -0
- package/dist/exports/sanitize.js +6 -0
- package/dist/exports/sanitize.js.map +1 -0
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/middleware.js +5 -0
- package/dist/middleware/middleware.js.map +1 -1
- package/dist/server/app.d.ts +51 -55
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +123 -2047
- package/dist/server/app.js.map +1 -1
- package/dist/server/bootstrapper.d.ts +24 -0
- package/dist/server/bootstrapper.d.ts.map +1 -0
- package/dist/server/bootstrapper.js +408 -0
- package/dist/server/bootstrapper.js.map +1 -0
- package/dist/server/controller-registrar.d.ts +8 -0
- package/dist/server/controller-registrar.d.ts.map +1 -0
- package/dist/server/controller-registrar.js +351 -0
- package/dist/server/controller-registrar.js.map +1 -0
- package/dist/server/plugin-manager.d.ts +26 -0
- package/dist/server/plugin-manager.d.ts.map +1 -0
- package/dist/server/plugin-manager.js +150 -0
- package/dist/server/plugin-manager.js.map +1 -0
- package/dist/server/queue-manager.d.ts +18 -0
- package/dist/server/queue-manager.d.ts.map +1 -0
- package/dist/server/queue-manager.js +54 -0
- package/dist/server/queue-manager.js.map +1 -0
- package/dist/server/request-handler.d.ts +25 -0
- package/dist/server/request-handler.d.ts.map +1 -0
- package/dist/server/request-handler.js +230 -0
- package/dist/server/request-handler.js.map +1 -0
- package/dist/server/route-scanner.d.ts +20 -0
- package/dist/server/route-scanner.d.ts.map +1 -0
- package/dist/server/route-scanner.js +892 -0
- package/dist/server/route-scanner.js.map +1 -0
- package/dist/server/ws-orchestrator.d.ts +15 -0
- package/dist/server/ws-orchestrator.d.ts.map +1 -0
- package/dist/server/ws-orchestrator.js +156 -0
- package/dist/server/ws-orchestrator.js.map +1 -0
- package/dist/utils/port.d.ts +2 -0
- package/dist/utils/port.d.ts.map +1 -0
- package/dist/utils/port.js +30 -0
- package/dist/utils/port.js.map +1 -0
- package/dist/utils/sanitize.d.ts +33 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +95 -0
- package/dist/utils/sanitize.js.map +1 -0
- package/dist/utils/validator.d.ts +2 -0
- package/dist/utils/validator.d.ts.map +1 -1
- package/dist/utils/validator.js +55 -17
- package/dist/utils/validator.js.map +1 -1
- package/package.json +6 -1
package/dist/server/app.js
CHANGED
|
@@ -35,31 +35,68 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.activeAppInstance = exports.app = exports.App = void 0;
|
|
37
37
|
exports.getActiveApp = getActiveApp;
|
|
38
|
-
const node_http_1 = require("node:http");
|
|
39
|
-
const node_https_1 = require("node:https");
|
|
40
|
-
const node_http2_1 = require("node:http2");
|
|
41
38
|
const ws_1 = require("ws");
|
|
42
|
-
const node_url_1 = require("node:url");
|
|
43
|
-
const validator_1 = require("../utils/validator");
|
|
44
|
-
const request_1 = require("./request");
|
|
45
|
-
const response_1 = require("./response");
|
|
46
|
-
const context_1 = require("./context");
|
|
47
39
|
const router_1 = require("../router/router");
|
|
48
40
|
const middleware_1 = require("../middleware/middleware");
|
|
49
41
|
const errors_1 = require("../utils/errors");
|
|
50
42
|
const server_1 = require("../websocket/server");
|
|
51
|
-
const socket_1 = require("../websocket/socket");
|
|
52
43
|
const config_1 = require("../utils/config");
|
|
53
44
|
const logger_1 = require("../utils/logger");
|
|
54
45
|
const hot_reload_1 = require("./hot-reload");
|
|
55
|
-
const dev_error_overlay_1 = require("./dev-error-overlay");
|
|
56
|
-
const uws_adapter_1 = require("./uws-adapter");
|
|
57
|
-
const client_1 = require("../queue/client");
|
|
58
|
-
const worker_1 = require("../queue/worker");
|
|
59
|
-
const scheduler_1 = require("../cron/scheduler");
|
|
60
46
|
const container_1 = require("../di/container");
|
|
47
|
+
const bootstrapper_1 = require("./bootstrapper");
|
|
48
|
+
const ws_orchestrator_1 = require("./ws-orchestrator");
|
|
49
|
+
const plugin_manager_1 = require("./plugin-manager");
|
|
50
|
+
const route_scanner_1 = require("./route-scanner");
|
|
51
|
+
const queue_manager_1 = require("./queue-manager");
|
|
52
|
+
const request_handler_1 = require("./request-handler");
|
|
53
|
+
const controller_registrar_1 = require("./controller-registrar");
|
|
61
54
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
62
55
|
class App {
|
|
56
|
+
get server() {
|
|
57
|
+
return this.bootstrapper.getServer();
|
|
58
|
+
}
|
|
59
|
+
get redirectServer() {
|
|
60
|
+
return this.bootstrapper.redirectServer;
|
|
61
|
+
}
|
|
62
|
+
get onStartHook() {
|
|
63
|
+
return this.pluginManager.onStartHook;
|
|
64
|
+
}
|
|
65
|
+
set onStartHook(cb) {
|
|
66
|
+
this.pluginManager.onStartHook = cb;
|
|
67
|
+
}
|
|
68
|
+
get onCloseHook() {
|
|
69
|
+
return this.pluginManager.onCloseHook;
|
|
70
|
+
}
|
|
71
|
+
set onCloseHook(cb) {
|
|
72
|
+
this.pluginManager.onCloseHook = cb;
|
|
73
|
+
}
|
|
74
|
+
// ─── Lifecycle Hooks Registry ───────────────────────────────────────────────
|
|
75
|
+
get hooks() {
|
|
76
|
+
return this.pluginManager.hooks;
|
|
77
|
+
}
|
|
78
|
+
// ─── Queue ──────────────────────────────────────────────────────────────────
|
|
79
|
+
get apiDir() {
|
|
80
|
+
return this.routeScanner.apiDir;
|
|
81
|
+
}
|
|
82
|
+
set apiDir(val) {
|
|
83
|
+
this.routeScanner.apiDir = val;
|
|
84
|
+
}
|
|
85
|
+
get _queueClient() {
|
|
86
|
+
return this.queueManager._queueClient;
|
|
87
|
+
}
|
|
88
|
+
get _queueWorker() {
|
|
89
|
+
return this.queueManager._queueWorker;
|
|
90
|
+
}
|
|
91
|
+
get _cronScheduler() {
|
|
92
|
+
return this.queueManager._cronScheduler;
|
|
93
|
+
}
|
|
94
|
+
get _pendingQueueJobs() {
|
|
95
|
+
return this.queueManager._pendingQueueJobs;
|
|
96
|
+
}
|
|
97
|
+
set _pendingQueueJobs(v) {
|
|
98
|
+
this.queueManager._pendingQueueJobs = v;
|
|
99
|
+
}
|
|
63
100
|
get queueClient() {
|
|
64
101
|
return this._queueClient;
|
|
65
102
|
}
|
|
@@ -67,64 +104,40 @@ class App {
|
|
|
67
104
|
return this._queueWorker;
|
|
68
105
|
}
|
|
69
106
|
constructor(options = {}) {
|
|
70
|
-
this.redirectServer = null;
|
|
71
|
-
this.apiDir = null;
|
|
72
107
|
this.globalMiddleware = [];
|
|
73
108
|
this.errorHandlers = [];
|
|
74
109
|
this._configured = false;
|
|
75
110
|
this._routesMounted = false;
|
|
76
111
|
this._loggerCreated = false;
|
|
77
|
-
this.registeredPlugins = new Map();
|
|
78
|
-
this.routeMap = new Map();
|
|
79
112
|
this.hotReloader = null;
|
|
80
|
-
this.lazyRoutes = new Map();
|
|
81
113
|
this.wsServer = new server_1.ExisWebSocketServer();
|
|
82
114
|
this.rawWsServer = new ws_1.WebSocketServer({ noServer: true });
|
|
83
115
|
// ─── uWebSockets.js Backend ──────────────────────────────────────────────────
|
|
84
116
|
this._useUws = false;
|
|
85
117
|
this._uwsApp = null;
|
|
86
118
|
this._uwsListenToken = null;
|
|
87
|
-
// ─── Lifecycle Hooks Registry ───────────────────────────────────────────────
|
|
88
|
-
this.hooks = {
|
|
89
|
-
ready: [],
|
|
90
|
-
close: [],
|
|
91
|
-
request: [],
|
|
92
|
-
response: [],
|
|
93
|
-
error: [],
|
|
94
|
-
route: [],
|
|
95
|
-
};
|
|
96
119
|
// ─── Dataloaders Registry ───────────────────────────────────────────────────
|
|
97
120
|
this._dataloaders = new Map();
|
|
98
121
|
this.explicitOptions = {};
|
|
99
122
|
// ─── Dependency Injection ───────────────────────────────────────────────────
|
|
100
123
|
this.container = new container_1.Container();
|
|
101
|
-
//
|
|
102
|
-
this._queueClient = null;
|
|
103
|
-
this._queueWorker = null;
|
|
104
|
-
this._cronScheduler = null;
|
|
105
|
-
this._pendingQueueJobs = [];
|
|
124
|
+
// WebSocket Upgrade logic has been moved to WsOrchestrator
|
|
106
125
|
this.shutdownHooks = [];
|
|
107
|
-
// ─── App Initialization
|
|
126
|
+
// ─── App Initialization ───────────────────────────────────────────────────────
|
|
108
127
|
this._configLoaded = false;
|
|
109
128
|
this.explicitOptions = options;
|
|
110
129
|
this.options = (0, config_1.mergeConfig)(config_1.defaultConfig, options);
|
|
111
130
|
this.router = new router_1.Router();
|
|
112
131
|
this.ensureLogger();
|
|
113
|
-
|
|
132
|
+
this.pluginManager = new plugin_manager_1.PluginManager(this);
|
|
133
|
+
this.wsOrchestrator = new ws_orchestrator_1.WsOrchestrator(this);
|
|
134
|
+
this.bootstrapper = new bootstrapper_1.ServerBootstrapper(this);
|
|
135
|
+
this.routeScanner = new route_scanner_1.RouteScanner(this);
|
|
136
|
+
this.queueManager = new queue_manager_1.QueueManager(this);
|
|
137
|
+
this.requestHandler = new request_handler_1.RequestHandler(this);
|
|
138
|
+
this.controllerRegistrar = new controller_registrar_1.ControllerRegistrar(this);
|
|
114
139
|
if (this.options.queue) {
|
|
115
|
-
this._initQueue(this.options.queue);
|
|
116
|
-
}
|
|
117
|
-
// Auto-detect uWebSockets.js backend
|
|
118
|
-
if (options.server === 'uws' ||
|
|
119
|
-
(options.server !== 'node' &&
|
|
120
|
-
(0, uws_adapter_1.isUwsAvailable)() &&
|
|
121
|
-
this.options.env !== 'test')) {
|
|
122
|
-
this._useUws = true;
|
|
123
|
-
// Don't init Node server — we'll create the uWS app at listen() time
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
this._useUws = false;
|
|
127
|
-
this.server = this._initServer();
|
|
140
|
+
this.queueManager._initQueue(this.options.queue);
|
|
128
141
|
}
|
|
129
142
|
}
|
|
130
143
|
ensureLogger() {
|
|
@@ -137,58 +150,30 @@ class App {
|
|
|
137
150
|
}
|
|
138
151
|
this.log = (0, logger_1.createLogger)(loggerOptions);
|
|
139
152
|
}
|
|
140
|
-
_initServer
|
|
141
|
-
let server;
|
|
142
|
-
if (this.options.ssl) {
|
|
143
|
-
if (this.options.http2 !== false) {
|
|
144
|
-
server = (0, node_http2_1.createSecureServer)({ allowHTTP1: true, ...this.options.ssl }, this.handle.bind(this));
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
server = (0, node_https_1.createServer)(this.options.ssl, this.handle.bind(this));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
server = (0, node_http_1.createServer)(this.handle.bind(this));
|
|
152
|
-
}
|
|
153
|
-
server.on('upgrade', this._handleUpgrade.bind(this));
|
|
154
|
-
const keepAlive = this.options.keepAlive;
|
|
155
|
-
if (keepAlive) {
|
|
156
|
-
const kaConfig = keepAlive === true ? {} : keepAlive;
|
|
157
|
-
if ('keepAliveTimeout' in server)
|
|
158
|
-
server.keepAliveTimeout = kaConfig.timeoutMs ?? 5000;
|
|
159
|
-
if ('headersTimeout' in server)
|
|
160
|
-
server.headersTimeout = kaConfig.headersTimeoutMs ?? 60000;
|
|
161
|
-
if (kaConfig.maxRequests !== undefined &&
|
|
162
|
-
'maxRequestsPerSocket' in server) {
|
|
163
|
-
;
|
|
164
|
-
server.maxRequestsPerSocket = kaConfig.maxRequests;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return server;
|
|
168
|
-
}
|
|
153
|
+
// _initServer logic has been moved to ServerBootstrapper
|
|
169
154
|
// ─── Lifecycle Hooks Registration ───────────────────────────────────────────
|
|
170
155
|
onReady(cb) {
|
|
171
|
-
this.
|
|
156
|
+
this.pluginManager.onReady(cb);
|
|
172
157
|
return this;
|
|
173
158
|
}
|
|
174
159
|
onClose(cb) {
|
|
175
|
-
this.
|
|
160
|
+
this.pluginManager.onClose(cb);
|
|
176
161
|
return this;
|
|
177
162
|
}
|
|
178
163
|
onRequest(cb) {
|
|
179
|
-
this.
|
|
164
|
+
this.pluginManager.onRequest(cb);
|
|
180
165
|
return this;
|
|
181
166
|
}
|
|
182
167
|
onResponse(cb) {
|
|
183
|
-
this.
|
|
168
|
+
this.pluginManager.onResponse(cb);
|
|
184
169
|
return this;
|
|
185
170
|
}
|
|
186
171
|
onError(cb) {
|
|
187
|
-
this.
|
|
172
|
+
this.pluginManager.onError(cb);
|
|
188
173
|
return this;
|
|
189
174
|
}
|
|
190
175
|
onRoute(cb) {
|
|
191
|
-
this.
|
|
176
|
+
this.pluginManager.onRoute(cb);
|
|
192
177
|
return this;
|
|
193
178
|
}
|
|
194
179
|
// ─── Dataloaders ────────────────────────────────────────────────────────────
|
|
@@ -205,108 +190,10 @@ class App {
|
|
|
205
190
|
}
|
|
206
191
|
// ─── Plugin System ──────────────────────────────────────────────────────────
|
|
207
192
|
hasPlugin(name) {
|
|
208
|
-
return this.
|
|
193
|
+
return this.pluginManager.hasPlugin(name);
|
|
209
194
|
}
|
|
210
195
|
async register(pluginOrInstance, legacyOptions) {
|
|
211
|
-
|
|
212
|
-
let options;
|
|
213
|
-
if ('plugin' in pluginOrInstance && !('register' in pluginOrInstance)) {
|
|
214
|
-
plugin = pluginOrInstance.plugin;
|
|
215
|
-
options = pluginOrInstance.options;
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
plugin = pluginOrInstance;
|
|
219
|
-
options = legacyOptions;
|
|
220
|
-
}
|
|
221
|
-
if (this.registeredPlugins.has(plugin.name)) {
|
|
222
|
-
throw new Error(`Plugin '${plugin.name}' is already registered`);
|
|
223
|
-
}
|
|
224
|
-
if (plugin.dependencies) {
|
|
225
|
-
for (const dep of plugin.dependencies) {
|
|
226
|
-
if (!this.registeredPlugins.has(dep)) {
|
|
227
|
-
throw new Error(`Plugin '${plugin.name}' requires dependency '${dep}' which is not registered`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
232
|
-
let targetApp = this;
|
|
233
|
-
let pluginRouter;
|
|
234
|
-
if (plugin.encapsulate !== false) {
|
|
235
|
-
pluginRouter = new router_1.Router();
|
|
236
|
-
// Create a proxy that intercepts route and middleware registrations
|
|
237
|
-
targetApp = new Proxy(this, {
|
|
238
|
-
get(target, prop, receiver) {
|
|
239
|
-
if ([
|
|
240
|
-
'get',
|
|
241
|
-
'post',
|
|
242
|
-
'put',
|
|
243
|
-
'patch',
|
|
244
|
-
'delete',
|
|
245
|
-
'httpOptions',
|
|
246
|
-
'head',
|
|
247
|
-
'all',
|
|
248
|
-
'ws',
|
|
249
|
-
'query',
|
|
250
|
-
'connect',
|
|
251
|
-
'trace',
|
|
252
|
-
].includes(prop)) {
|
|
253
|
-
return (path, ...handlers) => {
|
|
254
|
-
const routerMethod = prop === 'httpOptions' ? 'options' : prop;
|
|
255
|
-
pluginRouter[routerMethod](path, ...handlers);
|
|
256
|
-
return receiver;
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
if (prop === 'use') {
|
|
260
|
-
return (...handlers) => {
|
|
261
|
-
for (const h of handlers) {
|
|
262
|
-
if (h.length === 4) {
|
|
263
|
-
// error handlers stay global for now, or we could scope them
|
|
264
|
-
target.use(h);
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
pluginRouter.use(h);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return receiver;
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
// Encapsulate lifecycle hooks by converting them to scoped middleware!
|
|
274
|
-
if (['onRequest', 'onResponse'].includes(prop)) {
|
|
275
|
-
return (cb) => {
|
|
276
|
-
if (prop === 'onRequest') {
|
|
277
|
-
pluginRouter.use(async (req, res, next) => {
|
|
278
|
-
try {
|
|
279
|
-
await cb(req, res);
|
|
280
|
-
next();
|
|
281
|
-
}
|
|
282
|
-
catch (err) {
|
|
283
|
-
next(err);
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
else if (prop === 'onResponse') {
|
|
288
|
-
pluginRouter.use((req, res, next) => {
|
|
289
|
-
res.raw.on('finish', () => {
|
|
290
|
-
cb(req, res).catch(() => {
|
|
291
|
-
/* ignore */
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
next();
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
return receiver;
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
const value = Reflect.get(target, prop, receiver);
|
|
301
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
await plugin.register(targetApp, options);
|
|
306
|
-
if (pluginRouter) {
|
|
307
|
-
this.mount('/', pluginRouter);
|
|
308
|
-
}
|
|
309
|
-
this.registeredPlugins.set(plugin.name, plugin);
|
|
196
|
+
await this.pluginManager.register(pluginOrInstance, legacyOptions);
|
|
310
197
|
return this;
|
|
311
198
|
}
|
|
312
199
|
// ─── Middleware ─────────────────────────────────────────────────────────────
|
|
@@ -407,340 +294,7 @@ class App {
|
|
|
407
294
|
}
|
|
408
295
|
// ─── Class-Based Controllers Registration ──────────────────────────────────
|
|
409
296
|
registerControllers(controllers, basePath = '', prefixMiddlewares = []) {
|
|
410
|
-
|
|
411
|
-
const CONTROLLER_PREFIX = Symbol.for('exisjs:controller_prefix');
|
|
412
|
-
const CONTROLLER_HOST = Symbol.for('exisjs:controller_host');
|
|
413
|
-
const MIDDLEWARE_REGISTRY = Symbol.for('exisjs:middlewares');
|
|
414
|
-
const ROUTE_METADATA = Symbol.for('exisjs:route_metadata');
|
|
415
|
-
const LIFECYCLE_METADATA = Symbol.for('exisjs:lifecycle_metadata');
|
|
416
|
-
const PARAM_METADATA = Symbol.for('exisjs:param_metadata');
|
|
417
|
-
for (const ControllerClass of controllers) {
|
|
418
|
-
const prefix = ControllerClass.prototype[CONTROLLER_PREFIX] || '';
|
|
419
|
-
const host = ControllerClass.prototype[CONTROLLER_HOST];
|
|
420
|
-
const routes = ControllerClass.prototype[ROUTE_REGISTRY] || [];
|
|
421
|
-
const classMiddlewares = [
|
|
422
|
-
...(Array.isArray(ControllerClass.prototype[MIDDLEWARE_REGISTRY])
|
|
423
|
-
? ControllerClass.prototype[MIDDLEWARE_REGISTRY]
|
|
424
|
-
: ControllerClass.prototype[MIDDLEWARE_REGISTRY]?._classMiddlewares ||
|
|
425
|
-
[]),
|
|
426
|
-
];
|
|
427
|
-
const methodMiddlewares = (!Array.isArray(ControllerClass.prototype[MIDDLEWARE_REGISTRY])
|
|
428
|
-
? ControllerClass.prototype[MIDDLEWARE_REGISTRY]
|
|
429
|
-
: {}) || {};
|
|
430
|
-
const routeMetadataMap = ControllerClass.prototype[ROUTE_METADATA] || {};
|
|
431
|
-
const lifecycleMetadataMap = ControllerClass.prototype[LIFECYCLE_METADATA] || {};
|
|
432
|
-
const paramMetadataMap = ControllerClass.prototype[PARAM_METADATA] || {};
|
|
433
|
-
for (const route of routes) {
|
|
434
|
-
const method = route.method.toLowerCase();
|
|
435
|
-
const executeCore = async (req, res, next, streamOrSocket) => {
|
|
436
|
-
let instance = this.container.resolve(ControllerClass);
|
|
437
|
-
if (!instance) {
|
|
438
|
-
// Auto-instantiate if not provided via DI
|
|
439
|
-
instance = new ControllerClass();
|
|
440
|
-
this.container.provide(ControllerClass, { useValue: instance });
|
|
441
|
-
}
|
|
442
|
-
try {
|
|
443
|
-
// 0. Enforce Route Permissions (Role Authorization)
|
|
444
|
-
const routeMetadata = routeMetadataMap[route.handlerName] || {};
|
|
445
|
-
const permissions = routeMetadata.permissions;
|
|
446
|
-
if (permissions && permissions.length > 0) {
|
|
447
|
-
const userPerms = req.user?.permissions || (req.user?.role ? [req.user.role] : []);
|
|
448
|
-
const hasPermission = permissions.some((p) => userPerms.includes(p));
|
|
449
|
-
if (!hasPermission) {
|
|
450
|
-
if (res) {
|
|
451
|
-
res.status(403).json({
|
|
452
|
-
success: false,
|
|
453
|
-
error: {
|
|
454
|
-
code: 'FORBIDDEN',
|
|
455
|
-
message: 'Insufficient permissions to access this resource',
|
|
456
|
-
},
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
// 1. Run Guards
|
|
463
|
-
const routeLifecycle = lifecycleMetadataMap[route.handlerName] || {};
|
|
464
|
-
const guards = routeLifecycle.guards || [];
|
|
465
|
-
for (const guard of guards) {
|
|
466
|
-
let allowed = false;
|
|
467
|
-
if (typeof guard === 'function') {
|
|
468
|
-
if (guard.prototype &&
|
|
469
|
-
typeof guard.prototype.canActivate === 'function') {
|
|
470
|
-
let guardInstance = this.container.resolve(guard);
|
|
471
|
-
if (!guardInstance) {
|
|
472
|
-
guardInstance = new guard();
|
|
473
|
-
}
|
|
474
|
-
allowed = await guardInstance.canActivate(req);
|
|
475
|
-
}
|
|
476
|
-
else {
|
|
477
|
-
allowed = await guard(req);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (!allowed) {
|
|
481
|
-
if (res) {
|
|
482
|
-
res.status(403).json({
|
|
483
|
-
success: false,
|
|
484
|
-
error: { code: 'FORBIDDEN', message: 'Forbidden by Guard' },
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
// 2. Resolve parameters
|
|
491
|
-
const paramMetadata = paramMetadataMap[route.handlerName] || [];
|
|
492
|
-
const args = [];
|
|
493
|
-
if (paramMetadata.length === 0) {
|
|
494
|
-
if (method === 'ws' || method === 'sse') {
|
|
495
|
-
args.push(req, streamOrSocket);
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
args.push(req, res, next);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
for (const param of paramMetadata) {
|
|
503
|
-
if (!param) {
|
|
504
|
-
args.push(undefined);
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
let rawArg;
|
|
508
|
-
switch (param.type) {
|
|
509
|
-
case 'req':
|
|
510
|
-
rawArg = req;
|
|
511
|
-
break;
|
|
512
|
-
case 'res':
|
|
513
|
-
rawArg = res;
|
|
514
|
-
break;
|
|
515
|
-
case 'body':
|
|
516
|
-
if (req.body === undefined &&
|
|
517
|
-
['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
518
|
-
const contentType = req.headers['content-type'] || '';
|
|
519
|
-
if (contentType.includes('json')) {
|
|
520
|
-
await req.json().catch(() => {
|
|
521
|
-
/* noop */
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
else if (contentType.includes('form')) {
|
|
525
|
-
await req.formData().catch(() => {
|
|
526
|
-
/* noop */
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
rawArg = req.body;
|
|
531
|
-
break;
|
|
532
|
-
case 'param':
|
|
533
|
-
rawArg = param.name ? req.params[param.name] : req.params;
|
|
534
|
-
break;
|
|
535
|
-
case 'query':
|
|
536
|
-
rawArg = param.name ? req.query[param.name] : req.query;
|
|
537
|
-
break;
|
|
538
|
-
case 'header':
|
|
539
|
-
rawArg = param.name
|
|
540
|
-
? req.headers[param.name.toLowerCase()]
|
|
541
|
-
: req.headers;
|
|
542
|
-
break;
|
|
543
|
-
case 'session':
|
|
544
|
-
rawArg = req.session;
|
|
545
|
-
break;
|
|
546
|
-
case 'next':
|
|
547
|
-
rawArg = next;
|
|
548
|
-
break;
|
|
549
|
-
case 'host':
|
|
550
|
-
rawArg = param.name
|
|
551
|
-
? req.params[param.name]
|
|
552
|
-
: req.hostname;
|
|
553
|
-
break;
|
|
554
|
-
case 'socket':
|
|
555
|
-
case 'stream':
|
|
556
|
-
rawArg = streamOrSocket;
|
|
557
|
-
break;
|
|
558
|
-
case 'ip':
|
|
559
|
-
rawArg = req.ip;
|
|
560
|
-
break;
|
|
561
|
-
case 'uploadedFile':
|
|
562
|
-
case 'uploadedFiles':
|
|
563
|
-
if (req.body === undefined &&
|
|
564
|
-
['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
565
|
-
await req.formData().catch(() => {
|
|
566
|
-
/* noop */
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
if (req.files) {
|
|
570
|
-
const isMulti = param.type === 'uploadedFiles';
|
|
571
|
-
let files = [];
|
|
572
|
-
if (param.name) {
|
|
573
|
-
files = req.files.filter((f) => f.fieldname === param.name);
|
|
574
|
-
}
|
|
575
|
-
else {
|
|
576
|
-
files = req.files;
|
|
577
|
-
}
|
|
578
|
-
rawArg = isMulti ? files : files[0];
|
|
579
|
-
}
|
|
580
|
-
break;
|
|
581
|
-
default:
|
|
582
|
-
rawArg = undefined;
|
|
583
|
-
}
|
|
584
|
-
if (param.pipes && param.pipes.length > 0) {
|
|
585
|
-
for (const pipe of param.pipes) {
|
|
586
|
-
if (typeof pipe === 'function' &&
|
|
587
|
-
pipe.prototype?.transform) {
|
|
588
|
-
let pipeInstance = this.container.resolve(pipe);
|
|
589
|
-
if (!pipeInstance)
|
|
590
|
-
pipeInstance = new pipe();
|
|
591
|
-
rawArg = await pipeInstance.transform(rawArg, {
|
|
592
|
-
type: param.type,
|
|
593
|
-
name: param.name,
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
else if (typeof pipe === 'object' &&
|
|
597
|
-
typeof pipe.parse === 'function') {
|
|
598
|
-
rawArg = await pipe.parse(rawArg);
|
|
599
|
-
}
|
|
600
|
-
else if (typeof pipe === 'object' &&
|
|
601
|
-
typeof pipe.transform === 'function') {
|
|
602
|
-
rawArg = await pipe.transform(rawArg, {
|
|
603
|
-
type: param.type,
|
|
604
|
-
name: param.name,
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
else if (typeof pipe === 'function') {
|
|
608
|
-
rawArg = await pipe(rawArg);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
args.push(rawArg);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
// 3. Execute Custom Interceptors
|
|
616
|
-
const interceptors = routeLifecycle.interceptors || [];
|
|
617
|
-
for (const interceptor of interceptors) {
|
|
618
|
-
if (typeof interceptor === 'function') {
|
|
619
|
-
if (interceptor.prototype &&
|
|
620
|
-
typeof interceptor.prototype.intercept === 'function') {
|
|
621
|
-
let interceptorInstance = this.container.resolve(interceptor);
|
|
622
|
-
if (!interceptorInstance) {
|
|
623
|
-
interceptorInstance = new interceptor();
|
|
624
|
-
}
|
|
625
|
-
await interceptorInstance.intercept(req, res);
|
|
626
|
-
}
|
|
627
|
-
else {
|
|
628
|
-
await interceptor(req, res);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
// 4. Invoke the method
|
|
633
|
-
const result = await instance[route.handlerName](...args);
|
|
634
|
-
if (res && !res.headersSent) {
|
|
635
|
-
const routeMeta = routeMetadataMap[route.handlerName] || {};
|
|
636
|
-
if (routeMeta.redirect) {
|
|
637
|
-
res.redirect(routeMeta.redirect.url, routeMeta.redirect.statusCode);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
// Apply custom headers
|
|
641
|
-
if (routeMeta.headers) {
|
|
642
|
-
for (const [name, val] of Object.entries(routeMeta.headers)) {
|
|
643
|
-
res.setHeader(name, val);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
// Apply custom HTTP Code
|
|
647
|
-
if (routeMeta.httpCode) {
|
|
648
|
-
res.status(routeMeta.httpCode);
|
|
649
|
-
}
|
|
650
|
-
if (result !== undefined) {
|
|
651
|
-
res.json(result);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
catch (err) {
|
|
656
|
-
let handled = false;
|
|
657
|
-
const routeLifecycle = lifecycleMetadataMap[route.handlerName] || {};
|
|
658
|
-
const filters = routeLifecycle.filters || [];
|
|
659
|
-
for (const filter of filters) {
|
|
660
|
-
if (typeof filter === 'function' && filter.prototype?.catch) {
|
|
661
|
-
let filterInstance = this.container.resolve(filter);
|
|
662
|
-
if (!filterInstance)
|
|
663
|
-
filterInstance = new filter();
|
|
664
|
-
await filterInstance.catch(err, { req, res });
|
|
665
|
-
handled = true;
|
|
666
|
-
break;
|
|
667
|
-
}
|
|
668
|
-
else if (typeof filter === 'object' &&
|
|
669
|
-
typeof filter.catch === 'function') {
|
|
670
|
-
await filter.catch(err, { req, res });
|
|
671
|
-
handled = true;
|
|
672
|
-
break;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
if (!handled) {
|
|
676
|
-
if (next)
|
|
677
|
-
next(err);
|
|
678
|
-
else
|
|
679
|
-
console.error(`[Exis ${method.toUpperCase()} Error]`, err);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
let finalHandler;
|
|
684
|
-
if (method === 'ws' || method === 'sse') {
|
|
685
|
-
finalHandler = async (streamOrSocket, rawReq) => {
|
|
686
|
-
await executeCore(rawReq, undefined, undefined, streamOrSocket);
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
else {
|
|
690
|
-
finalHandler = async (req, res, next) => {
|
|
691
|
-
await executeCore(req, res, next);
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
const fullPath = (basePath + prefix + route.path).replace(/\/+/g, '/') || '/';
|
|
695
|
-
const routeSpecificMiddlewares = methodMiddlewares[route.handlerName] || [];
|
|
696
|
-
const allMiddlewares = [
|
|
697
|
-
...prefixMiddlewares,
|
|
698
|
-
...classMiddlewares,
|
|
699
|
-
...routeSpecificMiddlewares,
|
|
700
|
-
];
|
|
701
|
-
const routeMeta = routeMetadataMap[route.handlerName] || {};
|
|
702
|
-
const finalHost = routeMeta.hosts || host;
|
|
703
|
-
let schema = route.schema;
|
|
704
|
-
const paramMetadata = paramMetadataMap[route.handlerName] || [];
|
|
705
|
-
let extractedBodySchema = undefined;
|
|
706
|
-
let extractedQuerySchema = undefined;
|
|
707
|
-
for (const param of paramMetadata) {
|
|
708
|
-
if (!param)
|
|
709
|
-
continue;
|
|
710
|
-
if (param.type === 'body') {
|
|
711
|
-
const bodyPipe = param.pipes?.find((p) => p && typeof p.parse === 'function');
|
|
712
|
-
if (bodyPipe)
|
|
713
|
-
extractedBodySchema = bodyPipe;
|
|
714
|
-
}
|
|
715
|
-
if (param.type === 'query') {
|
|
716
|
-
const queryPipe = param.pipes?.find((p) => p && typeof p.parse === 'function');
|
|
717
|
-
if (queryPipe)
|
|
718
|
-
extractedQuerySchema = queryPipe;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
const extractedResponseSchema = routeMeta.responseSchema;
|
|
722
|
-
if (schema ||
|
|
723
|
-
finalHost ||
|
|
724
|
-
extractedBodySchema ||
|
|
725
|
-
extractedQuerySchema ||
|
|
726
|
-
extractedResponseSchema) {
|
|
727
|
-
schema = schema || {};
|
|
728
|
-
if (finalHost)
|
|
729
|
-
schema.host = finalHost;
|
|
730
|
-
if (extractedBodySchema && !schema.body)
|
|
731
|
-
schema.body = extractedBodySchema;
|
|
732
|
-
if (extractedQuerySchema && !schema.query)
|
|
733
|
-
schema.query = extractedQuerySchema;
|
|
734
|
-
if (extractedResponseSchema && !schema.response)
|
|
735
|
-
schema.response = extractedResponseSchema;
|
|
736
|
-
this.router[method](fullPath, ...allMiddlewares, schema, finalHandler);
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
;
|
|
740
|
-
this.router[method](fullPath, ...allMiddlewares, finalHandler);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
297
|
+
this.controllerRegistrar.registerControllers(controllers, basePath, prefixMiddlewares);
|
|
744
298
|
return this;
|
|
745
299
|
}
|
|
746
300
|
// ─── Dependency Injection ───────────────────────────────────────────────────
|
|
@@ -788,681 +342,32 @@ class App {
|
|
|
788
342
|
this.errorHandlers.push((0, errors_1.createErrorHandler)(this.options.env === 'development'));
|
|
789
343
|
}
|
|
790
344
|
}
|
|
791
|
-
getCompiledPipeline() {
|
|
792
|
-
if (this._compiledPipeline)
|
|
793
|
-
return this._compiledPipeline;
|
|
794
|
-
this.applyBuiltins();
|
|
795
|
-
this._compiledPipeline = [
|
|
796
|
-
...this.globalMiddleware,
|
|
797
|
-
(req, res, next) => this.router.handle(req, res, next),
|
|
798
|
-
middleware_1.notFound,
|
|
799
|
-
];
|
|
800
|
-
return this._compiledPipeline;
|
|
801
|
-
}
|
|
802
|
-
// ─── Web Standard Fetch (Edge Runtime) & Inject ──────────────────────────────
|
|
803
345
|
async fetch(request, env, ctx) {
|
|
804
|
-
|
|
805
|
-
if (typeof this.create === 'function')
|
|
806
|
-
await this.create();
|
|
807
|
-
if (typeof this.onStartHook === 'function')
|
|
808
|
-
await this.onStartHook(this);
|
|
809
|
-
}
|
|
810
|
-
const { handleFetch } = await Promise.resolve().then(() => __importStar(require('../adapters/fetch')));
|
|
811
|
-
return handleFetch(this, request, env, ctx);
|
|
346
|
+
return this.requestHandler.fetch(request, env, ctx);
|
|
812
347
|
}
|
|
813
348
|
async inject(options) {
|
|
814
|
-
|
|
815
|
-
const url = options.url.startsWith('http')
|
|
816
|
-
? options.url
|
|
817
|
-
: `http://localhost${options.url}`;
|
|
818
|
-
const headers = new Headers();
|
|
819
|
-
if (options.headers) {
|
|
820
|
-
for (const [k, v] of Object.entries(options.headers)) {
|
|
821
|
-
headers.set(k, v);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
let bodyStr;
|
|
825
|
-
if (options.body) {
|
|
826
|
-
bodyStr =
|
|
827
|
-
typeof options.body === 'string'
|
|
828
|
-
? options.body
|
|
829
|
-
: JSON.stringify(options.body);
|
|
830
|
-
if (!headers.has('content-type') && typeof options.body !== 'string') {
|
|
831
|
-
headers.set('content-type', 'application/json');
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
const req = new Request(url, {
|
|
835
|
-
method,
|
|
836
|
-
headers,
|
|
837
|
-
body: ['GET', 'HEAD', 'OPTIONS'].includes(method) ? undefined : bodyStr,
|
|
838
|
-
});
|
|
839
|
-
const fetchRes = await this.fetch(req);
|
|
840
|
-
const resText = await fetchRes.text();
|
|
841
|
-
let parsedBody = resText;
|
|
842
|
-
try {
|
|
843
|
-
parsedBody = JSON.parse(resText);
|
|
844
|
-
}
|
|
845
|
-
catch {
|
|
846
|
-
// ignore
|
|
847
|
-
}
|
|
848
|
-
const resHeaders = {};
|
|
849
|
-
fetchRes.headers.forEach((v, k) => {
|
|
850
|
-
resHeaders[k] = v;
|
|
851
|
-
});
|
|
852
|
-
return {
|
|
853
|
-
status: fetchRes.status,
|
|
854
|
-
headers: resHeaders,
|
|
855
|
-
body: parsedBody,
|
|
856
|
-
text: resText,
|
|
857
|
-
};
|
|
349
|
+
return this.requestHandler.inject(options);
|
|
858
350
|
}
|
|
859
351
|
// ─── Private Internal Pipeline Handler ─────────────────────────────────────────────────────
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
state: {},
|
|
863
|
-
afterCallbacks: [],
|
|
864
|
-
req: req,
|
|
865
|
-
res: res,
|
|
866
|
-
app: this,
|
|
867
|
-
diCache: new Map(),
|
|
868
|
-
};
|
|
869
|
-
res.raw.on('finish', () => {
|
|
870
|
-
for (const cb of store.afterCallbacks) {
|
|
871
|
-
try {
|
|
872
|
-
const promise = cb();
|
|
873
|
-
if (promise instanceof Promise) {
|
|
874
|
-
promise.catch((err) => {
|
|
875
|
-
this.log.error({ err }, 'Error in after() background task');
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
catch (err) {
|
|
880
|
-
this.log.error({ err }, 'Error in after() background task');
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
});
|
|
884
|
-
context_1.executionContext.run(store, execution);
|
|
352
|
+
getCompiledPipeline() {
|
|
353
|
+
return this.requestHandler.getCompiledPipeline();
|
|
885
354
|
}
|
|
886
355
|
handle(rawReq, rawRes) {
|
|
887
|
-
|
|
888
|
-
const req = new request_1.ExisRequest(rawReq, res, this.options.trustProxy, this.options.bodyLimit);
|
|
889
|
-
res.req = req;
|
|
890
|
-
res.etagEnabled = this.options.etag === true;
|
|
891
|
-
req.log = this.log;
|
|
892
|
-
req._dataloaderFns = this._dataloaders;
|
|
893
|
-
this._executeWithContext(req, res, () => {
|
|
894
|
-
if (this.hooks.request.length === 0 && this.hooks.response.length === 0) {
|
|
895
|
-
(0, router_1.runHandlers)(this.getCompiledPipeline(), req, res, (err) => {
|
|
896
|
-
if (err) {
|
|
897
|
-
this._runErrorHandlers(err, req, res).catch((e) => {
|
|
898
|
-
this.log.error({ err: e }, 'Error in error handler');
|
|
899
|
-
});
|
|
900
|
-
}
|
|
901
|
-
});
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
this._handleWithHooks(req, res, rawRes);
|
|
905
|
-
});
|
|
356
|
+
this.requestHandler.handle(rawReq, rawRes);
|
|
906
357
|
}
|
|
907
358
|
/**
|
|
908
359
|
* Handle a request from the uWebSockets.js backend.
|
|
909
360
|
* Uses the same middleware pipeline as Node HTTP but with uWS shims.
|
|
910
361
|
*/
|
|
911
362
|
handleUws(shimReq, shimRes) {
|
|
912
|
-
|
|
913
|
-
// The shims implement the same interface that ExisRequest/ExisResponse expect
|
|
914
|
-
const res = new response_1.ExisResponse(shimRes);
|
|
915
|
-
const req = new request_1.ExisRequest(shimReq, res, this.options.trustProxy, this.options.bodyLimit);
|
|
916
|
-
res.req = req;
|
|
917
|
-
res.etagEnabled = this.options.etag === true;
|
|
918
|
-
req.log = this.log;
|
|
919
|
-
req._dataloaderFns = this._dataloaders;
|
|
920
|
-
this._executeWithContext(req, res, () => {
|
|
921
|
-
if (this.hooks.request.length === 0 && this.hooks.response.length === 0) {
|
|
922
|
-
(0, router_1.runHandlers)(this.getCompiledPipeline(), req, res, (err) => {
|
|
923
|
-
if (err) {
|
|
924
|
-
this._runErrorHandlers(err, req, res).catch((e) => {
|
|
925
|
-
this.log.error({ err: e }, 'Error in error handler');
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
});
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
this._handleWithHooks(req, res, shimRes);
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
async _handleWithHooks(req, res, rawRes) {
|
|
935
|
-
// ─── onRequest Hook ───
|
|
936
|
-
if (this.hooks.request.length > 0) {
|
|
937
|
-
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
938
|
-
for (let i = 0; i < this.hooks.request.length; i++) {
|
|
939
|
-
await this.hooks.request[i](req, res);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
if (res.headersSent)
|
|
943
|
-
return;
|
|
944
|
-
// ─── onResponse Hook ───
|
|
945
|
-
if (this.hooks.response.length > 0) {
|
|
946
|
-
rawRes.on('finish', () => {
|
|
947
|
-
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
948
|
-
for (let i = 0; i < this.hooks.response.length; i++) {
|
|
949
|
-
Promise.resolve(this.hooks.response[i](req, res)).catch((err) => {
|
|
950
|
-
this.log.error({ err }, 'Error in onResponse hook');
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
}
|
|
955
|
-
(0, router_1.runHandlers)(this.getCompiledPipeline(), req, res, (err) => {
|
|
956
|
-
if (err) {
|
|
957
|
-
this._runErrorHandlers(err, req, res).catch((e) => {
|
|
958
|
-
this.log.error({ err: e }, 'Error in error handler');
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
async _runErrorHandlers(err, req, res) {
|
|
964
|
-
// ─── onError Hook ───
|
|
965
|
-
if (this.hooks.error.length > 0) {
|
|
966
|
-
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
967
|
-
for (let i = 0; i < this.hooks.error.length; i++) {
|
|
968
|
-
await this.hooks.error[i](err, req, res);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
if (res.headersSent)
|
|
972
|
-
return;
|
|
973
|
-
for (const handler of this.errorHandlers) {
|
|
974
|
-
let handled = false;
|
|
975
|
-
await handler(err, req, res, () => {
|
|
976
|
-
handled = true;
|
|
977
|
-
});
|
|
978
|
-
if (!handled || res.headersSent)
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
// fallback if no error handler ran
|
|
982
|
-
const defaultHandler = (0, errors_1.createErrorHandler)(this.options.env === 'development');
|
|
983
|
-
await defaultHandler(err, req, res, () => {
|
|
984
|
-
/* noop */
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
// ─── WebSocket Upgrade Handler ───────────────────────────────────────────────
|
|
988
|
-
async _handleUpgrade(rawReq, socket, head) {
|
|
989
|
-
this.applyBuiltins();
|
|
990
|
-
// Match route
|
|
991
|
-
const reqPath = (rawReq.url || '').split('?')[0];
|
|
992
|
-
const matched = this.router.match('WS', reqPath);
|
|
993
|
-
if (!matched) {
|
|
994
|
-
socket.write('HTTP/1.1 404 Not Found\\r\\n\\r\\n');
|
|
995
|
-
socket.destroy();
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
// Create a dummy ServerResponse so standard HTTP middleware can run and reject if needed
|
|
999
|
-
const rawRes = new node_http_1.ServerResponse(rawReq);
|
|
1000
|
-
rawRes.assignSocket(socket);
|
|
1001
|
-
const res = new response_1.ExisResponse(rawRes);
|
|
1002
|
-
const originalMethod = rawReq.method;
|
|
1003
|
-
const req = new request_1.ExisRequest(rawReq, res, this.options.trustProxy, this.options.bodyLimit);
|
|
1004
|
-
res.req = req;
|
|
1005
|
-
req.method = 'WS';
|
|
1006
|
-
req.params = matched.params;
|
|
1007
|
-
req.log = this.log;
|
|
1008
|
-
// Build the middleware pipeline specifically for this WebSocket route
|
|
1009
|
-
const pipeline = [
|
|
1010
|
-
...this.globalMiddleware,
|
|
1011
|
-
async (req, res, next) => {
|
|
1012
|
-
// Exclude the last handler which is the actual WsHandler wrapper
|
|
1013
|
-
const routeHandlers = matched.route.handlers.slice(0, -1);
|
|
1014
|
-
(0, router_1.runHandlers)(routeHandlers, req, res, (err) => {
|
|
1015
|
-
if (err)
|
|
1016
|
-
return next(err);
|
|
1017
|
-
next();
|
|
1018
|
-
});
|
|
1019
|
-
},
|
|
1020
|
-
];
|
|
1021
|
-
await (0, router_1.runHandlers)(pipeline, req, res, async (err) => {
|
|
1022
|
-
if (err) {
|
|
1023
|
-
this.log.error({ err }, 'Error during WebSocket upgrade middleware');
|
|
1024
|
-
socket.write('HTTP/1.1 500 Internal Server Error\\r\\n\\r\\n');
|
|
1025
|
-
socket.destroy();
|
|
1026
|
-
return;
|
|
1027
|
-
}
|
|
1028
|
-
if (res.headersSent) {
|
|
1029
|
-
// A middleware intercepted and sent an HTTP response (e.g. 401 Unauthorized)
|
|
1030
|
-
socket.destroy();
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
// Restore original HTTP method (usually 'GET') so the `ws` package doesn't reject with 405
|
|
1034
|
-
rawReq.method = originalMethod;
|
|
1035
|
-
// Middleware passed, perform upgrade
|
|
1036
|
-
this.rawWsServer.handleUpgrade(rawReq, socket, head, (ws) => {
|
|
1037
|
-
const exisWs = new socket_1.ExisWebSocket(ws, req, this.wsServer);
|
|
1038
|
-
this.wsServer.track(exisWs);
|
|
1039
|
-
req.ws = exisWs;
|
|
1040
|
-
const finalHandler = matched.route.handlers[matched.route.handlers.length - 1];
|
|
1041
|
-
Promise.resolve(finalHandler(req, res, () => {
|
|
1042
|
-
/* noop */
|
|
1043
|
-
})).catch((err) => {
|
|
1044
|
-
this.log.error({ err }, 'Error in WebSocket handler');
|
|
1045
|
-
ws.close(1011, 'Internal Server Error');
|
|
1046
|
-
});
|
|
1047
|
-
});
|
|
1048
|
-
});
|
|
363
|
+
this.requestHandler.handleUws(shimReq, shimRes);
|
|
1049
364
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
* Runs the middleware pipeline and if successful, upgrades the connection.
|
|
1053
|
-
*/
|
|
1054
|
-
async handleUwsUpgrade(shimReq, uwsRes, context) {
|
|
1055
|
-
this.applyBuiltins();
|
|
1056
|
-
const reqPath = (shimReq.url || '').split('?')[0];
|
|
1057
|
-
const matched = this.router.match('WS', reqPath);
|
|
1058
|
-
if (!matched) {
|
|
1059
|
-
// 404
|
|
1060
|
-
uwsRes.cork(() => {
|
|
1061
|
-
uwsRes.writeStatus('404 Not Found');
|
|
1062
|
-
uwsRes.end();
|
|
1063
|
-
});
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
let isAborted = false;
|
|
1067
|
-
uwsRes.onAborted(() => {
|
|
1068
|
-
isAborted = true;
|
|
1069
|
-
});
|
|
1070
|
-
const shimRes = new uws_adapter_1.UwsServerResponse(uwsRes);
|
|
1071
|
-
const res = new response_1.ExisResponse(shimRes);
|
|
1072
|
-
const req = new request_1.ExisRequest(shimReq, res, this.options.trustProxy, this.options.bodyLimit);
|
|
1073
|
-
res.req = req;
|
|
1074
|
-
req.method = 'WS';
|
|
1075
|
-
req.params = matched.params;
|
|
1076
|
-
req.log = this.log;
|
|
1077
|
-
const pipeline = [
|
|
1078
|
-
...this.globalMiddleware,
|
|
1079
|
-
async (req, res, next) => {
|
|
1080
|
-
const routeHandlers = matched.route.handlers.slice(0, -1);
|
|
1081
|
-
(0, router_1.runHandlers)(routeHandlers, req, res, (err) => {
|
|
1082
|
-
if (err)
|
|
1083
|
-
return next(err);
|
|
1084
|
-
next();
|
|
1085
|
-
});
|
|
1086
|
-
},
|
|
1087
|
-
];
|
|
1088
|
-
this._executeWithContext(req, res, () => {
|
|
1089
|
-
(0, router_1.runHandlers)(pipeline, req, res, async (err) => {
|
|
1090
|
-
if (isAborted)
|
|
1091
|
-
return;
|
|
1092
|
-
if (err) {
|
|
1093
|
-
this.log.error({ err }, 'Error during uWS WebSocket upgrade middleware');
|
|
1094
|
-
uwsRes.cork(() => {
|
|
1095
|
-
uwsRes.writeStatus('500 Internal Server Error');
|
|
1096
|
-
uwsRes.end();
|
|
1097
|
-
});
|
|
1098
|
-
return;
|
|
1099
|
-
}
|
|
1100
|
-
if (res.headersSent) {
|
|
1101
|
-
return; // Middleware sent an HTTP response
|
|
1102
|
-
}
|
|
1103
|
-
// We need to fetch headers directly from the shimReq.headers
|
|
1104
|
-
const secWebSocketKey = shimReq.headers['sec-websocket-key'] || '';
|
|
1105
|
-
const secWebSocketProtocol = shimReq.headers['sec-websocket-protocol'] || '';
|
|
1106
|
-
const secWebSocketExtensions = shimReq.headers['sec-websocket-extensions'] || '';
|
|
1107
|
-
// Prepare user data for the upgraded websocket
|
|
1108
|
-
const userData = {
|
|
1109
|
-
req,
|
|
1110
|
-
res,
|
|
1111
|
-
exisWs: new socket_1.ExisWebSocket(null, req, this.wsServer), // the raw will be set to shim in open()
|
|
1112
|
-
finalHandler: matched.route.handlers[matched.route.handlers.length - 1],
|
|
1113
|
-
};
|
|
1114
|
-
this.wsServer.track(userData.exisWs);
|
|
1115
|
-
req.ws = userData.exisWs;
|
|
1116
|
-
uwsRes.cork(() => {
|
|
1117
|
-
uwsRes.upgrade(userData, secWebSocketKey, secWebSocketProtocol, secWebSocketExtensions, context);
|
|
1118
|
-
});
|
|
1119
|
-
Promise.resolve(userData.finalHandler(req, res, () => {
|
|
1120
|
-
/* noop */
|
|
1121
|
-
})).catch((e) => {
|
|
1122
|
-
this.log.error({ err: e }, 'Error in uWS WebSocket handler');
|
|
1123
|
-
});
|
|
1124
|
-
});
|
|
1125
|
-
});
|
|
365
|
+
getErrorHandlers() {
|
|
366
|
+
return this.errorHandlers;
|
|
1126
367
|
}
|
|
1127
368
|
// ─── Listen ───────────────────────────────────────────────────────────────────
|
|
1128
|
-
listen(portOrOptions, callback) {
|
|
1129
|
-
if (process.env.EXIS_CLI_MODE === '1') {
|
|
1130
|
-
return undefined;
|
|
1131
|
-
}
|
|
1132
|
-
this.applyBuiltins();
|
|
1133
|
-
let port;
|
|
1134
|
-
let host;
|
|
1135
|
-
let onListen;
|
|
1136
|
-
if (typeof portOrOptions === 'number') {
|
|
1137
|
-
port = portOrOptions;
|
|
1138
|
-
host = this.options.host;
|
|
1139
|
-
onListen = callback ? () => callback() : undefined;
|
|
1140
|
-
}
|
|
1141
|
-
else if (typeof portOrOptions === 'object') {
|
|
1142
|
-
if (portOrOptions.ssl) {
|
|
1143
|
-
this.options.ssl = portOrOptions.ssl;
|
|
1144
|
-
// Re-initialize server if ssl is passed during listen
|
|
1145
|
-
this.server = this._initServer();
|
|
1146
|
-
}
|
|
1147
|
-
if (portOrOptions.redirectHttp !== undefined) {
|
|
1148
|
-
this.options.redirectHttp = portOrOptions.redirectHttp;
|
|
1149
|
-
}
|
|
1150
|
-
port = portOrOptions.port ?? this.options.port;
|
|
1151
|
-
host = portOrOptions.host ?? this.options.host;
|
|
1152
|
-
onListen = portOrOptions.onListen;
|
|
1153
|
-
}
|
|
1154
|
-
else {
|
|
1155
|
-
port = this.options.port;
|
|
1156
|
-
host = this.options.host;
|
|
1157
|
-
}
|
|
1158
|
-
// Auto HTTP -> HTTPS redirect server (only for Node HTTP backend)
|
|
1159
|
-
// ─── uWebSockets.js listen path ───
|
|
1160
|
-
if (this._useUws) {
|
|
1161
|
-
this._uwsApp = (0, uws_adapter_1.createUwsApp)(this.handleUws.bind(this), this.handleUwsUpgrade.bind(this), this.options.ssl);
|
|
1162
|
-
this._uwsApp.listen(port, host, async (token) => {
|
|
1163
|
-
if (!token) {
|
|
1164
|
-
this.log.error(`Failed to listen on ${host}:${port} (uWS)`);
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
this._uwsListenToken = token;
|
|
1168
|
-
const address = { port, host };
|
|
1169
|
-
if (this.hotReloader) {
|
|
1170
|
-
this.hotReloader.stop();
|
|
1171
|
-
}
|
|
1172
|
-
if (onListen) {
|
|
1173
|
-
onListen(address);
|
|
1174
|
-
}
|
|
1175
|
-
else {
|
|
1176
|
-
if (!process.env.__EXIS_DEV_SERVER && !process.env.__EXIS_CLI) {
|
|
1177
|
-
const url = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
|
|
1178
|
-
this.log.info({ url, env: this.options.env, backend: 'uws' }, `Server running at ${url} (uWebSockets.js)`);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
if (this.options.env === 'production') {
|
|
1182
|
-
const handleShutdown = async () => {
|
|
1183
|
-
this.log.info('Received shutdown signal (SIGTERM/SIGINT)');
|
|
1184
|
-
try {
|
|
1185
|
-
await this.close();
|
|
1186
|
-
process.exit(0);
|
|
1187
|
-
}
|
|
1188
|
-
catch (err) {
|
|
1189
|
-
this.log.error({ err }, 'Error during graceful shutdown');
|
|
1190
|
-
process.exit(1);
|
|
1191
|
-
}
|
|
1192
|
-
};
|
|
1193
|
-
process.once('SIGTERM', handleShutdown);
|
|
1194
|
-
process.once('SIGINT', handleShutdown);
|
|
1195
|
-
}
|
|
1196
|
-
// ─── onReady Hook ───
|
|
1197
|
-
for (const hook of this.hooks.ready) {
|
|
1198
|
-
await hook();
|
|
1199
|
-
}
|
|
1200
|
-
if (callback)
|
|
1201
|
-
callback();
|
|
1202
|
-
});
|
|
1203
|
-
// Return a minimal server-like object for compatibility
|
|
1204
|
-
return this.server;
|
|
1205
|
-
}
|
|
1206
|
-
// ─── Node HTTP listen path ───
|
|
1207
|
-
// Auto HTTP -> HTTPS redirect server
|
|
1208
|
-
if (this.options.ssl &&
|
|
1209
|
-
this.options.redirectHttp !== undefined &&
|
|
1210
|
-
this.options.redirectHttp !== false) {
|
|
1211
|
-
const redirectPort = typeof this.options.redirectHttp === 'number'
|
|
1212
|
-
? this.options.redirectHttp
|
|
1213
|
-
: 80;
|
|
1214
|
-
this.redirectServer = (0, node_http_1.createServer)((req, res) => {
|
|
1215
|
-
const targetHost = req.headers.host?.split(':')[0] || host;
|
|
1216
|
-
const actualPort = port === 0
|
|
1217
|
-
? this.server.address()?.port ||
|
|
1218
|
-
port
|
|
1219
|
-
: port;
|
|
1220
|
-
const targetPort = actualPort === 443 ? '' : `:${actualPort}`;
|
|
1221
|
-
res.writeHead(301, {
|
|
1222
|
-
Location: `https://${targetHost}${targetPort}${req.url || '/'}`,
|
|
1223
|
-
});
|
|
1224
|
-
res.end();
|
|
1225
|
-
});
|
|
1226
|
-
this.redirectServer.listen(redirectPort, host, () => {
|
|
1227
|
-
this.log.info(`Redirect server listening on port ${redirectPort} -> HTTPS port ${port}`);
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
this.server.on('error', (err) => {
|
|
1231
|
-
if (err.code === 'EADDRINUSE') {
|
|
1232
|
-
console.error(`\n\x1b[31m✗ Port ${port} is already in use.\x1b[0m`);
|
|
1233
|
-
console.error(` Try killing the process or use a different port in exis.config.ts\n`);
|
|
1234
|
-
process.exit(1);
|
|
1235
|
-
}
|
|
1236
|
-
else {
|
|
1237
|
-
this.log.error({ err }, 'Failed to start server');
|
|
1238
|
-
process.exit(1);
|
|
1239
|
-
}
|
|
1240
|
-
});
|
|
1241
|
-
this.server.listen(port, host, async () => {
|
|
1242
|
-
const address = { port, host };
|
|
1243
|
-
if (this.options.env === 'production') {
|
|
1244
|
-
const handleShutdown = async () => {
|
|
1245
|
-
this.log.info('Received shutdown signal (SIGTERM/SIGINT)');
|
|
1246
|
-
try {
|
|
1247
|
-
await this.close();
|
|
1248
|
-
process.exit(0);
|
|
1249
|
-
}
|
|
1250
|
-
catch (err) {
|
|
1251
|
-
this.log.error({ err }, 'Error during graceful shutdown');
|
|
1252
|
-
process.exit(1);
|
|
1253
|
-
}
|
|
1254
|
-
};
|
|
1255
|
-
process.once('SIGTERM', handleShutdown);
|
|
1256
|
-
process.once('SIGINT', handleShutdown);
|
|
1257
|
-
}
|
|
1258
|
-
// ─── onReady Hook ───
|
|
1259
|
-
for (const hook of this.hooks.ready) {
|
|
1260
|
-
await hook();
|
|
1261
|
-
}
|
|
1262
|
-
// Start queue worker if initialized
|
|
1263
|
-
if (this._queueWorker) {
|
|
1264
|
-
await this._queueWorker.start();
|
|
1265
|
-
}
|
|
1266
|
-
if (onListen) {
|
|
1267
|
-
onListen(address);
|
|
1268
|
-
}
|
|
1269
|
-
else {
|
|
1270
|
-
if (!process.env.__EXIS_DEV_SERVER && !process.env.__EXIS_CLI) {
|
|
1271
|
-
const url = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
|
|
1272
|
-
this.log.info({ url, env: this.options.env }, `Server running at ${url}`);
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
if (callback)
|
|
1276
|
-
callback();
|
|
1277
|
-
});
|
|
1278
|
-
return this.server;
|
|
1279
|
-
}
|
|
1280
|
-
// ─── Banner ───────────────────────────────────────────────────────────────────
|
|
1281
|
-
async _printStartupBanner() {
|
|
1282
|
-
const port = this.options.port ?? 3000;
|
|
1283
|
-
const host = this.options.host ?? 'localhost';
|
|
1284
|
-
const c = {
|
|
1285
|
-
cyan: '\x1b[36m',
|
|
1286
|
-
green: '\x1b[32m',
|
|
1287
|
-
gray: '\x1b[90m',
|
|
1288
|
-
dim: '\x1b[2m',
|
|
1289
|
-
bold: '\x1b[1m',
|
|
1290
|
-
reset: '\x1b[0m',
|
|
1291
|
-
magenta: '\x1b[35m',
|
|
1292
|
-
bgMagenta: '\x1b[45m',
|
|
1293
|
-
white: '\x1b[37m',
|
|
1294
|
-
primary: '\x1b[38;2;160;70;255m',
|
|
1295
|
-
blue: '\x1b[38;2;41;169;206m',
|
|
1296
|
-
};
|
|
1297
|
-
let fwVersion = '0.0.0';
|
|
1298
|
-
try {
|
|
1299
|
-
const fs = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
1300
|
-
const path = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
1301
|
-
const pkgPath = path.join(__dirname, '../../package.json');
|
|
1302
|
-
fwVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
1303
|
-
}
|
|
1304
|
-
catch {
|
|
1305
|
-
/* ignore */
|
|
1306
|
-
}
|
|
1307
|
-
const displayHost = host === '0.0.0.0' || host === '127.0.0.1' ? 'localhost' : host;
|
|
1308
|
-
let localIp = '<YOUR_IP>';
|
|
1309
|
-
try {
|
|
1310
|
-
const os = await Promise.resolve().then(() => __importStar(require('node:os')));
|
|
1311
|
-
const nets = os.networkInterfaces();
|
|
1312
|
-
for (const name of Object.keys(nets)) {
|
|
1313
|
-
for (const net of nets[name] || []) {
|
|
1314
|
-
if (net.family === 'IPv4' && !net.internal) {
|
|
1315
|
-
localIp = net.address;
|
|
1316
|
-
break;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
if (localIp !== '<YOUR_IP>')
|
|
1320
|
-
break;
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
catch {
|
|
1324
|
-
/* ignore */
|
|
1325
|
-
}
|
|
1326
|
-
const networkHost = host === '0.0.0.0'
|
|
1327
|
-
? `http://${localIp}:${port}`
|
|
1328
|
-
: 'use --host 0.0.0.0 to expose';
|
|
1329
|
-
const readyMs = 125;
|
|
1330
|
-
const displayEnv = this.options.env || process.env.NODE_ENV || 'development';
|
|
1331
|
-
const workerCount = process.env.__EXIS_CLUSTER_WORKERS;
|
|
1332
|
-
const cluster = await Promise.resolve().then(() => __importStar(require('node:cluster')));
|
|
1333
|
-
// In cluster mode, ONLY let Worker #1 print the banner so we don't spam the console 12 times
|
|
1334
|
-
if (cluster.default.isWorker && cluster.default.worker?.id !== 1) {
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
console.log(`\n ${c.primary}${c.bold}EXIS v${fwVersion}${c.reset} ready in ${c.bold}${readyMs} ms${c.reset}\n`);
|
|
1338
|
-
console.log(` ${c.white}➜${c.reset} ${c.bold}Local:${c.reset} ${c.blue}http://${displayHost}:${port}/${c.reset}`);
|
|
1339
|
-
console.log(` ${c.white}➜${c.reset} ${c.dim}Network:${c.reset} ${c.blue}${networkHost}/${c.reset}`);
|
|
1340
|
-
console.log(` ${c.white}➜${c.reset} ${c.dim}Environ:${c.reset} ${c.green}${displayEnv}${c.reset}`);
|
|
1341
|
-
if (workerCount && Number(workerCount) > 1) {
|
|
1342
|
-
console.log(` ${c.white}➜${c.reset} ${c.dim}Workers:${c.reset} ${c.magenta}${workerCount}${c.reset}`);
|
|
1343
|
-
}
|
|
1344
|
-
console.log(` ${c.white}➜${c.reset} press ${c.bold}h + enter${c.reset} ${c.dim}to show help${c.reset}\n`);
|
|
1345
|
-
}
|
|
1346
|
-
// ─── Graceful Shutdown ────────────────────────────────────────────────────────
|
|
1347
|
-
onShutdown(hook) {
|
|
1348
|
-
this.shutdownHooks.push(hook);
|
|
1349
|
-
return this;
|
|
1350
|
-
}
|
|
1351
|
-
close(timeout = 5000) {
|
|
1352
|
-
return new Promise((resolve, reject) => {
|
|
1353
|
-
this.log.info('Initiating graceful shutdown');
|
|
1354
|
-
let finishCalled = false;
|
|
1355
|
-
const finish = async () => {
|
|
1356
|
-
if (finishCalled)
|
|
1357
|
-
return;
|
|
1358
|
-
finishCalled = true;
|
|
1359
|
-
try {
|
|
1360
|
-
for (const hook of this.shutdownHooks) {
|
|
1361
|
-
await hook();
|
|
1362
|
-
}
|
|
1363
|
-
// Also execute onClose hooks
|
|
1364
|
-
for (const hook of this.hooks.close) {
|
|
1365
|
-
await hook();
|
|
1366
|
-
}
|
|
1367
|
-
if (this.onCloseHook) {
|
|
1368
|
-
await this.onCloseHook(this);
|
|
1369
|
-
}
|
|
1370
|
-
if (this._cronScheduler) {
|
|
1371
|
-
this._cronScheduler.stop();
|
|
1372
|
-
}
|
|
1373
|
-
if (this._queueWorker) {
|
|
1374
|
-
await this._queueWorker.stop();
|
|
1375
|
-
}
|
|
1376
|
-
if (this._queueClient) {
|
|
1377
|
-
await this._queueClient.close();
|
|
1378
|
-
}
|
|
1379
|
-
if (this.hotReloader) {
|
|
1380
|
-
await this.hotReloader.stop();
|
|
1381
|
-
}
|
|
1382
|
-
this.log.info('Graceful shutdown completed');
|
|
1383
|
-
resolve();
|
|
1384
|
-
}
|
|
1385
|
-
catch (err) {
|
|
1386
|
-
this.log.error({ err }, 'Error executing shutdown hooks');
|
|
1387
|
-
reject(err);
|
|
1388
|
-
}
|
|
1389
|
-
};
|
|
1390
|
-
if (!this._useUws && (!this.server || !this.server.listening)) {
|
|
1391
|
-
finish();
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
if (this._useUws && !this._uwsListenToken) {
|
|
1395
|
-
finish();
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
// Close idle keep-alive connections immediately
|
|
1399
|
-
if (!this._useUws &&
|
|
1400
|
-
this.server &&
|
|
1401
|
-
'closeIdleConnections' in this.server) {
|
|
1402
|
-
;
|
|
1403
|
-
this.server.closeIdleConnections?.();
|
|
1404
|
-
}
|
|
1405
|
-
// Set timeout to force close active connections
|
|
1406
|
-
const timer = setTimeout(() => {
|
|
1407
|
-
this.log.warn(`Shutdown timeout of ${timeout}ms exceeded, forcefully terminating active connections`);
|
|
1408
|
-
if (!this._useUws &&
|
|
1409
|
-
this.server &&
|
|
1410
|
-
'closeAllConnections' in this.server) {
|
|
1411
|
-
;
|
|
1412
|
-
this.server.closeAllConnections?.();
|
|
1413
|
-
}
|
|
1414
|
-
finish(); // Ensure finish is called on timeout
|
|
1415
|
-
}, timeout);
|
|
1416
|
-
const cleanupAndFinish = async () => {
|
|
1417
|
-
clearTimeout(timer);
|
|
1418
|
-
await finish();
|
|
1419
|
-
};
|
|
1420
|
-
this.wsServer.close();
|
|
1421
|
-
for (const client of this.rawWsServer.clients) {
|
|
1422
|
-
client.terminate();
|
|
1423
|
-
}
|
|
1424
|
-
this.rawWsServer.close();
|
|
1425
|
-
if (this.redirectServer) {
|
|
1426
|
-
this.redirectServer.close();
|
|
1427
|
-
}
|
|
1428
|
-
if (this._useUws && this._uwsApp) {
|
|
1429
|
-
this._uwsApp.close(this._uwsListenToken);
|
|
1430
|
-
this._uwsListenToken = null;
|
|
1431
|
-
cleanupAndFinish();
|
|
1432
|
-
}
|
|
1433
|
-
else {
|
|
1434
|
-
this.server.close((err) => {
|
|
1435
|
-
if (err)
|
|
1436
|
-
reject(err);
|
|
1437
|
-
else
|
|
1438
|
-
cleanupAndFinish();
|
|
1439
|
-
});
|
|
1440
|
-
}
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1443
|
-
// ─── Expose internals ─────────────────────────────────────────────────────────
|
|
1444
|
-
getRouter() {
|
|
1445
|
-
return this.router;
|
|
1446
|
-
}
|
|
1447
|
-
async loadAllRoutes() {
|
|
1448
|
-
for (const [filePath, entry] of this.lazyRoutes.entries()) {
|
|
1449
|
-
if (!entry.loaded) {
|
|
1450
|
-
entry.loaded = true;
|
|
1451
|
-
this.router.removeRoutesBySource('lazy:' + filePath);
|
|
1452
|
-
const routePath = this.routeMap.get(filePath);
|
|
1453
|
-
if (routePath) {
|
|
1454
|
-
try {
|
|
1455
|
-
await this.mountRouteFile(filePath, routePath);
|
|
1456
|
-
}
|
|
1457
|
-
catch (e) {
|
|
1458
|
-
this.log.error({ err: e, file: filePath }, 'Failed to eager-load route');
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
369
|
getServer() {
|
|
1465
|
-
return this.
|
|
370
|
+
return this.bootstrapper.getServer();
|
|
1466
371
|
}
|
|
1467
372
|
getConfig() {
|
|
1468
373
|
return this.options;
|
|
@@ -1481,6 +386,16 @@ class App {
|
|
|
1481
386
|
this._loggerCreated = false;
|
|
1482
387
|
this.ensureLogger();
|
|
1483
388
|
this._configLoaded = true;
|
|
389
|
+
if (this.options.env === 'development') {
|
|
390
|
+
const { getAvailablePort } = await Promise.resolve().then(() => __importStar(require('../utils/port')));
|
|
391
|
+
const requestedPort = this.options.port || 3000;
|
|
392
|
+
const host = this.options.host || '0.0.0.0';
|
|
393
|
+
const availablePort = await getAvailablePort(requestedPort, host);
|
|
394
|
+
if (availablePort !== requestedPort) {
|
|
395
|
+
this.log.info(`Port ${requestedPort} is in use, using ${availablePort} instead.`);
|
|
396
|
+
}
|
|
397
|
+
this.options.port = availablePort;
|
|
398
|
+
}
|
|
1484
399
|
if ((process.env.__EXIS_DEV_SERVER || process.env.__EXIS_CLI) &&
|
|
1485
400
|
!process.env.__EXIS_REPL &&
|
|
1486
401
|
!process.env.__EXIS_TEST) {
|
|
@@ -1488,910 +403,71 @@ class App {
|
|
|
1488
403
|
}
|
|
1489
404
|
}
|
|
1490
405
|
// Initialize Queue after config is loaded
|
|
1491
|
-
if (this.options.queue && !this._queueClient) {
|
|
1492
|
-
this._initQueue(this.options.queue);
|
|
406
|
+
if (this.options.queue && !this.queueManager._queueClient) {
|
|
407
|
+
this.queueManager._initQueue(this.options.queue);
|
|
1493
408
|
}
|
|
1494
409
|
// Flush any pending jobs that were registered before config was loaded
|
|
1495
|
-
if (this._pendingQueueJobs.length > 0) {
|
|
1496
|
-
if (!this._queueWorker) {
|
|
410
|
+
if (this.queueManager._pendingQueueJobs.length > 0) {
|
|
411
|
+
if (!this.queueManager._queueWorker) {
|
|
1497
412
|
if (this.options.queue?.enableWorkers !== false) {
|
|
1498
413
|
throw new Error('Queue worker is not initialized. Please configure ExisConfig.queue first.');
|
|
1499
414
|
}
|
|
1500
415
|
}
|
|
1501
416
|
else {
|
|
1502
|
-
for (const job of this._pendingQueueJobs) {
|
|
1503
|
-
this._queueWorker.registerJob(job);
|
|
1504
|
-
if (this._cronScheduler && job.cron) {
|
|
1505
|
-
this._cronScheduler.registerJob(job);
|
|
417
|
+
for (const job of this.queueManager._pendingQueueJobs) {
|
|
418
|
+
this.queueManager._queueWorker.registerJob(job);
|
|
419
|
+
if (this.queueManager._cronScheduler && job.cron) {
|
|
420
|
+
this.queueManager._cronScheduler.registerJob(job);
|
|
1506
421
|
}
|
|
1507
422
|
}
|
|
1508
423
|
}
|
|
1509
|
-
this._pendingQueueJobs = [];
|
|
424
|
+
this.queueManager._pendingQueueJobs = [];
|
|
1510
425
|
}
|
|
1511
426
|
if (this._routesMounted)
|
|
1512
427
|
return this;
|
|
1513
428
|
// Automatically scan and mount file-based routes and jobs
|
|
1514
|
-
await this.autoMountRoutes(root);
|
|
1515
|
-
await this.autoMountJobs(root);
|
|
429
|
+
await this.routeScanner.autoMountRoutes(root);
|
|
430
|
+
await this.routeScanner.autoMountJobs(root);
|
|
1516
431
|
this._routesMounted = true;
|
|
1517
432
|
// Start Hot Route Reloading in dev mode
|
|
1518
|
-
if (this.options.env === 'development' &&
|
|
433
|
+
if (this.options.env === 'development' &&
|
|
434
|
+
this.routeScanner._allApiDirs) {
|
|
1519
435
|
this.hotReloader = new hot_reload_1.HotReloader({
|
|
1520
|
-
apiDirs: this._allApiDirs,
|
|
436
|
+
apiDirs: this.routeScanner._allApiDirs,
|
|
1521
437
|
router: this.router,
|
|
1522
|
-
routeMap: this.routeMap,
|
|
1523
|
-
mountRoute: (filePath, routePath) => this.mountRouteFile(filePath, routePath),
|
|
438
|
+
routeMap: this.routeScanner.routeMap,
|
|
439
|
+
mountRoute: (filePath, routePath) => this.routeScanner.mountRouteFile(filePath, routePath),
|
|
1524
440
|
});
|
|
1525
441
|
await this.hotReloader.start();
|
|
1526
442
|
}
|
|
1527
443
|
return this;
|
|
1528
444
|
}
|
|
1529
|
-
// ───
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
const path = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
1533
|
-
const isProd = process.env.NODE_ENV === 'production';
|
|
1534
|
-
const isDev = this.options.env === 'development';
|
|
1535
|
-
// Try to load the pre-built manifest first (O(1) boot)
|
|
1536
|
-
const manifestPath = path.join(root, '.exis', 'routes-manifest.js');
|
|
1537
|
-
try {
|
|
1538
|
-
const stat = await fs.stat(manifestPath);
|
|
1539
|
-
if (stat.isFile()) {
|
|
1540
|
-
let url = (0, node_url_1.pathToFileURL)(manifestPath).href;
|
|
1541
|
-
if (!isProd)
|
|
1542
|
-
url += '?t=' + Date.now(); // cache bust for dev
|
|
1543
|
-
let mod;
|
|
1544
|
-
if (process.env.VITEST || process.env.NODE_ENV === 'test') {
|
|
1545
|
-
mod = await Promise.resolve(`${url}`).then(s => __importStar(require(s)));
|
|
1546
|
-
}
|
|
1547
|
-
else {
|
|
1548
|
-
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
1549
|
-
mod = await dynamicImport(url);
|
|
1550
|
-
}
|
|
1551
|
-
const manifest = mod.manifest;
|
|
1552
|
-
if (Array.isArray(manifest)) {
|
|
1553
|
-
for (const entry of manifest) {
|
|
1554
|
-
const { routePath, module: routeMod, filePath } = entry;
|
|
1555
|
-
const normalizedPath = path.resolve(root, filePath);
|
|
1556
|
-
this.routeMap.set(normalizedPath, routePath);
|
|
1557
|
-
const CONTROLLER_PREFIX = Symbol.for('exisjs:controller_prefix');
|
|
1558
|
-
const isClassController = (obj) => obj &&
|
|
1559
|
-
obj.prototype &&
|
|
1560
|
-
obj.prototype[CONTROLLER_PREFIX] !== undefined;
|
|
1561
|
-
const unwrappedMod = routeMod.default && routeMod.default.default
|
|
1562
|
-
? routeMod.default.default
|
|
1563
|
-
: routeMod.default
|
|
1564
|
-
? routeMod.default
|
|
1565
|
-
: routeMod;
|
|
1566
|
-
const functionalControllerObj = unwrappedMod && unwrappedMod.__isController ? unwrappedMod : null;
|
|
1567
|
-
const routerInstance = routeMod.router || routeMod.default || routeMod;
|
|
1568
|
-
if (functionalControllerObj) {
|
|
1569
|
-
const router = this.compileFunctionalController(functionalControllerObj);
|
|
1570
|
-
this.mountRouteWithSource(routePath, router, normalizedPath);
|
|
1571
|
-
}
|
|
1572
|
-
else if (isClassController(routerInstance)) {
|
|
1573
|
-
this.registerControllers([routerInstance], routePath);
|
|
1574
|
-
}
|
|
1575
|
-
else {
|
|
1576
|
-
this.mountRouteWithSource(routePath, routerInstance, normalizedPath);
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
// Skip the filesystem scan completely!
|
|
1580
|
-
return;
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
catch (err) {
|
|
1585
|
-
if (err.code !== 'ENOENT') {
|
|
1586
|
-
console.warn('exisjs: Failed to load routes manifest:', err.message);
|
|
1587
|
-
}
|
|
1588
|
-
// Fall back to scanning the filesystem
|
|
1589
|
-
}
|
|
1590
|
-
const searchDirs = isProd
|
|
1591
|
-
? [
|
|
1592
|
-
path.join(root, '.exis', 'server', 'src', 'http'),
|
|
1593
|
-
path.join(root, '.exis', 'server', 'http'),
|
|
1594
|
-
path.join(root, 'dist', 'src', 'http'),
|
|
1595
|
-
path.join(root, 'dist', 'http'),
|
|
1596
|
-
path.join(root, 'src', 'http'),
|
|
1597
|
-
path.join(root, 'http'),
|
|
1598
|
-
]
|
|
1599
|
-
: [path.join(root, 'src', 'http'), path.join(root, 'http')];
|
|
1600
|
-
const appDirs = [];
|
|
1601
|
-
for (const dir of searchDirs) {
|
|
1602
|
-
try {
|
|
1603
|
-
const stat = await fs.stat(dir);
|
|
1604
|
-
if (stat.isDirectory()) {
|
|
1605
|
-
const baseName = path.basename(path.dirname(dir)) === 'src' ? 'src/http' : 'http';
|
|
1606
|
-
const alreadyHasCompiledOrRawForThisBase = appDirs.some((existing) => {
|
|
1607
|
-
const existingBase = path.basename(path.dirname(existing)) === 'src'
|
|
1608
|
-
? 'src/http'
|
|
1609
|
-
: 'http';
|
|
1610
|
-
return existingBase === baseName;
|
|
1611
|
-
});
|
|
1612
|
-
if (!alreadyHasCompiledOrRawForThisBase) {
|
|
1613
|
-
appDirs.push(dir);
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
catch {
|
|
1618
|
-
/* ignore */
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
if (appDirs.length === 0)
|
|
1622
|
-
return;
|
|
1623
|
-
this.apiDir = appDirs[0] // keep property name for backwards compatibility
|
|
1624
|
-
;
|
|
1625
|
-
this._allApiDirs = appDirs;
|
|
1626
|
-
const cc = {
|
|
1627
|
-
green: '\x1b[32m',
|
|
1628
|
-
cyan: '\x1b[36m',
|
|
1629
|
-
gray: '\x1b[90m',
|
|
1630
|
-
reset: '\x1b[0m',
|
|
1631
|
-
};
|
|
1632
|
-
for (const appDir of appDirs) {
|
|
1633
|
-
const routes = await this.scanDirectory(appDir);
|
|
1634
|
-
for (const { filePath, routePath } of routes) {
|
|
1635
|
-
if (filePath.endsWith('route.ts') || filePath.endsWith('route.js')) {
|
|
1636
|
-
const normalized = path.resolve(filePath);
|
|
1637
|
-
this.routeMap.set(normalized, routePath);
|
|
1638
|
-
if (isDev) {
|
|
1639
|
-
const lazyKey = normalized;
|
|
1640
|
-
this.lazyRoutes.set(lazyKey, {
|
|
1641
|
-
filePath: normalized,
|
|
1642
|
-
loaded: false,
|
|
1643
|
-
});
|
|
1644
|
-
const lazyHandler = async (req, res, next) => {
|
|
1645
|
-
const entry = this.lazyRoutes.get(lazyKey);
|
|
1646
|
-
if (entry && !entry.loaded) {
|
|
1647
|
-
entry.loaded = true;
|
|
1648
|
-
this.router.removeRoutesBySource('lazy:' + lazyKey);
|
|
1649
|
-
try {
|
|
1650
|
-
await this.mountRouteFile(normalized, routePath);
|
|
1651
|
-
const relative = path
|
|
1652
|
-
.relative(process.cwd(), normalized)
|
|
1653
|
-
.replace(/\\/g, '/');
|
|
1654
|
-
console.log(` ${cc.green}✓${cc.reset} Lazy loaded ${cc.cyan}${relative}${cc.reset}`);
|
|
1655
|
-
}
|
|
1656
|
-
catch (err) {
|
|
1657
|
-
(0, dev_error_overlay_1.formatDevError)(err, normalized);
|
|
1658
|
-
return;
|
|
1659
|
-
}
|
|
1660
|
-
await this.router.handle(req, res, next);
|
|
1661
|
-
}
|
|
1662
|
-
else {
|
|
1663
|
-
next?.();
|
|
1664
|
-
}
|
|
1665
|
-
};
|
|
1666
|
-
const lazyRouter = new router_1.Router();
|
|
1667
|
-
lazyRouter.all(routePath, lazyHandler);
|
|
1668
|
-
lazyRouter.all(routePath + '/*path', lazyHandler);
|
|
1669
|
-
for (const r of lazyRouter.getRoutes()) {
|
|
1670
|
-
r.sourceFile = 'lazy:' + lazyKey;
|
|
1671
|
-
this.router.addRawRoute(r);
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
else {
|
|
1675
|
-
try {
|
|
1676
|
-
await this.mountRouteFile(normalized, routePath);
|
|
1677
|
-
}
|
|
1678
|
-
catch (err) {
|
|
1679
|
-
this.log.error({ err, file: filePath }, `Failed to load route file: ${filePath}`);
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
// ─── Route File Mounting (shared by autoMount and HotReloader) ──────────────
|
|
1687
|
-
// ─── Auto-Mount File-Based Jobs ──────────────────────────────────────────────
|
|
1688
|
-
async autoMountJobs(root) {
|
|
1689
|
-
if (!this._queueWorker)
|
|
1690
|
-
return;
|
|
1691
|
-
const fs = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
1692
|
-
const path = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
1693
|
-
const isProd = process.env.NODE_ENV === 'production';
|
|
1694
|
-
const searchDirs = isProd
|
|
1695
|
-
? [
|
|
1696
|
-
path.join(root, '.exis', 'server', 'src', 'jobs'),
|
|
1697
|
-
path.join(root, '.exis', 'server', 'jobs'),
|
|
1698
|
-
path.join(root, 'dist', 'src', 'jobs'),
|
|
1699
|
-
path.join(root, 'dist', 'jobs'),
|
|
1700
|
-
path.join(root, 'src', 'jobs'),
|
|
1701
|
-
path.join(root, 'jobs'),
|
|
1702
|
-
]
|
|
1703
|
-
: [path.join(root, 'src', 'jobs'), path.join(root, 'jobs')];
|
|
1704
|
-
let jobsDir = '';
|
|
1705
|
-
for (const dir of searchDirs) {
|
|
1706
|
-
try {
|
|
1707
|
-
const stat = await fs.stat(dir);
|
|
1708
|
-
if (stat.isDirectory()) {
|
|
1709
|
-
jobsDir = dir;
|
|
1710
|
-
break;
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
catch {
|
|
1714
|
-
/* ignore */
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
if (!jobsDir)
|
|
1718
|
-
return;
|
|
1719
|
-
try {
|
|
1720
|
-
const entries = await fs.readdir(jobsDir, { withFileTypes: true });
|
|
1721
|
-
for (const entry of entries) {
|
|
1722
|
-
if (entry.isFile() &&
|
|
1723
|
-
(entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
|
|
1724
|
-
const filePath = path.join(jobsDir, entry.name);
|
|
1725
|
-
const name = entry.name.replace(/\.(ts|js)$/, '');
|
|
1726
|
-
try {
|
|
1727
|
-
// Load the definition directly (no compilation needed here just to get the schema)
|
|
1728
|
-
const mod = await Promise.resolve(`${filePath}`).then(s => __importStar(require(s)));
|
|
1729
|
-
const jobDef = mod.default || mod;
|
|
1730
|
-
if (jobDef) {
|
|
1731
|
-
const def = {
|
|
1732
|
-
name: jobDef.name || name,
|
|
1733
|
-
filePath, // Attach filePath for Thread Pool execution
|
|
1734
|
-
cron: jobDef.cron,
|
|
1735
|
-
schema: jobDef.schema,
|
|
1736
|
-
defaultOptions: jobDef.defaultOptions,
|
|
1737
|
-
};
|
|
1738
|
-
this._queueWorker.registerJob(def);
|
|
1739
|
-
if (this._cronScheduler && jobDef.cron) {
|
|
1740
|
-
this._cronScheduler.registerJob(def);
|
|
1741
|
-
}
|
|
1742
|
-
this.log.info(`Registered background job: ${name}`);
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
catch (err) {
|
|
1746
|
-
this.log.error(`Failed to load job file ${filePath}: ${err}`);
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
catch {
|
|
1752
|
-
// Ignore if jobs directory does not exist
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
async mountRouteFile(filePath, routePath) {
|
|
1756
|
-
let mod;
|
|
1757
|
-
try {
|
|
1758
|
-
const url = process.env.VITEST || process.env.NODE_ENV === 'test'
|
|
1759
|
-
? (0, node_url_1.pathToFileURL)(filePath).href
|
|
1760
|
-
: (0, node_url_1.pathToFileURL)(filePath).href + '?t=' + Date.now();
|
|
1761
|
-
if (process.env.VITEST || process.env.NODE_ENV === 'test') {
|
|
1762
|
-
mod = await Promise.resolve(`${url}`).then(s => __importStar(require(s)));
|
|
1763
|
-
}
|
|
1764
|
-
else {
|
|
1765
|
-
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
1766
|
-
mod = await dynamicImport(url);
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
catch {
|
|
1770
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1771
|
-
mod = require(filePath);
|
|
1772
|
-
}
|
|
1773
|
-
const path = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
1774
|
-
const fs = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
|
|
1775
|
-
const dirname = path.dirname(filePath);
|
|
1776
|
-
const apiDir = this.apiDir || dirname;
|
|
1777
|
-
const normDirname = path.resolve(dirname).replace(/\\/g, '/').toLowerCase();
|
|
1778
|
-
const normApiDir = path.resolve(apiDir).replace(/\\/g, '/').toLowerCase();
|
|
1779
|
-
const segments = normDirname.startsWith(normApiDir)
|
|
1780
|
-
? normDirname.slice(normApiDir.length).split('/').filter(Boolean)
|
|
1781
|
-
: [];
|
|
1782
|
-
const allMiddlewares = [];
|
|
1783
|
-
const allFilters = [];
|
|
1784
|
-
const allGuards = [];
|
|
1785
|
-
const allInterceptors = [];
|
|
1786
|
-
let allMetadata = {};
|
|
1787
|
-
let allCors = undefined;
|
|
1788
|
-
let allHeaders = {};
|
|
1789
|
-
const activeGateways = [];
|
|
1790
|
-
const dirsToCheck = [apiDir];
|
|
1791
|
-
let tempDir = apiDir;
|
|
1792
|
-
for (const segment of segments) {
|
|
1793
|
-
tempDir = path.join(tempDir, segment);
|
|
1794
|
-
dirsToCheck.push(tempDir);
|
|
1795
|
-
}
|
|
1796
|
-
for (const dir of dirsToCheck) {
|
|
1797
|
-
try {
|
|
1798
|
-
const gwPathJs = path.join(dir, 'gateway.js');
|
|
1799
|
-
const gwPathTs = path.join(dir, 'gateway.ts');
|
|
1800
|
-
let targetGw = '';
|
|
1801
|
-
if (await fs.stat(gwPathTs).catch(() => null))
|
|
1802
|
-
targetGw = gwPathTs;
|
|
1803
|
-
else if (await fs.stat(gwPathJs).catch(() => null))
|
|
1804
|
-
targetGw = gwPathJs;
|
|
1805
|
-
if (targetGw) {
|
|
1806
|
-
activeGateways.push(targetGw);
|
|
1807
|
-
const gwUrl = process.env.VITEST || process.env.NODE_ENV === 'test'
|
|
1808
|
-
? (0, node_url_1.pathToFileURL)(targetGw).href
|
|
1809
|
-
: (0, node_url_1.pathToFileURL)(targetGw).href + '?t=' + Date.now();
|
|
1810
|
-
let gwMod;
|
|
1811
|
-
try {
|
|
1812
|
-
if (process.env.VITEST || process.env.NODE_ENV === 'test') {
|
|
1813
|
-
gwMod = await Promise.resolve(`${gwUrl}`).then(s => __importStar(require(s)));
|
|
1814
|
-
}
|
|
1815
|
-
else {
|
|
1816
|
-
const dynamicImportGw = new Function('specifier', 'return import(specifier)');
|
|
1817
|
-
gwMod = await dynamicImportGw(gwUrl);
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
catch {
|
|
1821
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1822
|
-
gwMod = require(targetGw);
|
|
1823
|
-
}
|
|
1824
|
-
const gwConfig = gwMod && gwMod.default && gwMod.default.default
|
|
1825
|
-
? gwMod.default.default
|
|
1826
|
-
: gwMod && gwMod.default
|
|
1827
|
-
? gwMod.default
|
|
1828
|
-
: gwMod;
|
|
1829
|
-
if (gwConfig) {
|
|
1830
|
-
const isExcluded = (reqPath, reqMethod) => {
|
|
1831
|
-
if (!gwConfig.exclude)
|
|
1832
|
-
return false;
|
|
1833
|
-
for (const rule of gwConfig.exclude) {
|
|
1834
|
-
if (typeof rule === 'string') {
|
|
1835
|
-
if (rule.endsWith('/*')) {
|
|
1836
|
-
if (reqPath.startsWith(rule.slice(0, -2)))
|
|
1837
|
-
return true;
|
|
1838
|
-
}
|
|
1839
|
-
else if (reqPath === rule)
|
|
1840
|
-
return true;
|
|
1841
|
-
}
|
|
1842
|
-
else {
|
|
1843
|
-
const pathMatch = rule.path.endsWith('/*')
|
|
1844
|
-
? reqPath.startsWith(rule.path.slice(0, -2))
|
|
1845
|
-
: reqPath === rule.path;
|
|
1846
|
-
if (pathMatch) {
|
|
1847
|
-
if (!rule.methods ||
|
|
1848
|
-
rule.methods.includes(reqMethod))
|
|
1849
|
-
return true;
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
return false;
|
|
1854
|
-
};
|
|
1855
|
-
if (gwConfig.middleware) {
|
|
1856
|
-
const wrapped = gwConfig.middleware.map((m) => (req, res, next) => isExcluded(req.path, req.method) ? next() : m(req, res, next));
|
|
1857
|
-
allMiddlewares.push(...wrapped);
|
|
1858
|
-
}
|
|
1859
|
-
if (gwConfig.filters) {
|
|
1860
|
-
const wrapped = gwConfig.filters.map((f) => (err, req, res, next) => isExcluded(req.path, req.method)
|
|
1861
|
-
? next(err)
|
|
1862
|
-
: f.prototype && f.prototype.catch
|
|
1863
|
-
? new f().catch(err, req, res, next)
|
|
1864
|
-
: f(err, req, res, next));
|
|
1865
|
-
allFilters.push(...wrapped);
|
|
1866
|
-
}
|
|
1867
|
-
if (gwConfig.guards) {
|
|
1868
|
-
const wrapped = gwConfig.guards.map((g) => {
|
|
1869
|
-
return async (req) => {
|
|
1870
|
-
if (isExcluded(req.path, req.method))
|
|
1871
|
-
return true;
|
|
1872
|
-
return typeof g === 'function'
|
|
1873
|
-
? g.prototype?.canActivate
|
|
1874
|
-
? new g().canActivate(req)
|
|
1875
|
-
: g(req)
|
|
1876
|
-
: g.canActivate(req);
|
|
1877
|
-
};
|
|
1878
|
-
});
|
|
1879
|
-
allGuards.push(...wrapped);
|
|
1880
|
-
}
|
|
1881
|
-
if (gwConfig.interceptors) {
|
|
1882
|
-
const wrapped = gwConfig.interceptors.map((i) => {
|
|
1883
|
-
return async (req, res) => {
|
|
1884
|
-
if (isExcluded(req.path, req.method))
|
|
1885
|
-
return;
|
|
1886
|
-
return typeof i === 'function'
|
|
1887
|
-
? i.prototype?.intercept
|
|
1888
|
-
? new i().intercept(req, res)
|
|
1889
|
-
: i(req, res)
|
|
1890
|
-
: i.intercept(req, res);
|
|
1891
|
-
};
|
|
1892
|
-
});
|
|
1893
|
-
allInterceptors.push(...wrapped);
|
|
1894
|
-
}
|
|
1895
|
-
if (gwConfig.timeout !== undefined) {
|
|
1896
|
-
const baseTimeout = gwConfig.timeout;
|
|
1897
|
-
const timeoutMiddleware = (req, res, next) => {
|
|
1898
|
-
if (isExcluded(req.path, req.method))
|
|
1899
|
-
return next();
|
|
1900
|
-
const ms = typeof baseTimeout === 'function'
|
|
1901
|
-
? baseTimeout(req)
|
|
1902
|
-
: baseTimeout;
|
|
1903
|
-
if (ms) {
|
|
1904
|
-
req.timeoutTimer = setTimeout(() => {
|
|
1905
|
-
if (!res.headersSent) {
|
|
1906
|
-
res.status(408).json({
|
|
1907
|
-
success: false,
|
|
1908
|
-
error: {
|
|
1909
|
-
code: 'REQUEST_TIMEOUT',
|
|
1910
|
-
message: 'Request Timeout',
|
|
1911
|
-
},
|
|
1912
|
-
});
|
|
1913
|
-
}
|
|
1914
|
-
}, ms);
|
|
1915
|
-
res.raw.on('finish', () => clearTimeout(req.timeoutTimer));
|
|
1916
|
-
}
|
|
1917
|
-
next();
|
|
1918
|
-
};
|
|
1919
|
-
allMiddlewares.push(timeoutMiddleware);
|
|
1920
|
-
}
|
|
1921
|
-
if (gwConfig.metadata) {
|
|
1922
|
-
allMetadata = { ...allMetadata, ...gwConfig.metadata };
|
|
1923
|
-
}
|
|
1924
|
-
if (gwConfig.cors !== undefined)
|
|
1925
|
-
allCors = gwConfig.cors;
|
|
1926
|
-
if (gwConfig.headers)
|
|
1927
|
-
allHeaders = { ...allHeaders, ...gwConfig.headers };
|
|
1928
|
-
if (gwConfig.plugins) {
|
|
1929
|
-
for (const plugin of gwConfig.plugins) {
|
|
1930
|
-
await this.register(plugin);
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
if (gwConfig.imports) {
|
|
1934
|
-
for (const mod of gwConfig.imports) {
|
|
1935
|
-
const name = 'plugin' in mod ? mod.plugin.name : mod.name;
|
|
1936
|
-
if (!this.hasPlugin(name)) {
|
|
1937
|
-
await this.register(mod);
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
if (gwConfig.providers) {
|
|
1942
|
-
for (const [token, providerConfig] of gwConfig.providers) {
|
|
1943
|
-
this.provide(token, providerConfig);
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
catch {
|
|
1950
|
-
/* ignore */
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
const currentMod = mod && mod.default && mod.default.default
|
|
1954
|
-
? mod.default.default
|
|
1955
|
-
: mod && mod.default
|
|
1956
|
-
? mod.default
|
|
1957
|
-
: mod;
|
|
1958
|
-
const isControllerObj = currentMod && currentMod.__isController === true;
|
|
1959
|
-
const routeConfig = {
|
|
1960
|
-
...(isControllerObj ? currentMod : {}),
|
|
1961
|
-
...(mod.config || {}),
|
|
1962
|
-
};
|
|
1963
|
-
// Fallback to global app.options.cors if neither gateway nor route specify it
|
|
1964
|
-
if (routeConfig.cors !== undefined)
|
|
1965
|
-
allCors = routeConfig.cors;
|
|
1966
|
-
else if (allCors === undefined && this.options.cors !== undefined)
|
|
1967
|
-
allCors = this.options.cors;
|
|
1968
|
-
if (routeConfig.middleware)
|
|
1969
|
-
allMiddlewares.push(...(Array.isArray(routeConfig.middleware)
|
|
1970
|
-
? routeConfig.middleware
|
|
1971
|
-
: [routeConfig.middleware]));
|
|
1972
|
-
if (routeConfig.headers)
|
|
1973
|
-
allHeaders = { ...allHeaders, ...routeConfig.headers };
|
|
1974
|
-
if (routeConfig.plugins) {
|
|
1975
|
-
for (const plugin of routeConfig.plugins) {
|
|
1976
|
-
await this.register(plugin);
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
// Pass gateway features to controllers via metadata if necessary
|
|
1980
|
-
const combinedFilters = [
|
|
1981
|
-
...allFilters,
|
|
1982
|
-
...(routeConfig.filters
|
|
1983
|
-
? Array.isArray(routeConfig.filters)
|
|
1984
|
-
? routeConfig.filters
|
|
1985
|
-
: [routeConfig.filters]
|
|
1986
|
-
: []),
|
|
1987
|
-
];
|
|
1988
|
-
const combinedGuards = [
|
|
1989
|
-
...allGuards,
|
|
1990
|
-
...(routeConfig.guards
|
|
1991
|
-
? Array.isArray(routeConfig.guards)
|
|
1992
|
-
? routeConfig.guards
|
|
1993
|
-
: [routeConfig.guards]
|
|
1994
|
-
: []),
|
|
1995
|
-
];
|
|
1996
|
-
const combinedInterceptors = [
|
|
1997
|
-
...allInterceptors,
|
|
1998
|
-
...(routeConfig.interceptors
|
|
1999
|
-
? Array.isArray(routeConfig.interceptors)
|
|
2000
|
-
? routeConfig.interceptors
|
|
2001
|
-
: [routeConfig.interceptors]
|
|
2002
|
-
: []),
|
|
2003
|
-
];
|
|
2004
|
-
// Inject into the default export if it's a functional controller
|
|
2005
|
-
if (mod.default && typeof mod.default === 'object') {
|
|
2006
|
-
try {
|
|
2007
|
-
if (!mod.default.filters)
|
|
2008
|
-
mod.default.filters = [];
|
|
2009
|
-
mod.default.filters.push(...combinedFilters);
|
|
2010
|
-
if (!mod.default.guards)
|
|
2011
|
-
mod.default.guards = [];
|
|
2012
|
-
mod.default.guards.push(...combinedGuards);
|
|
2013
|
-
if (!mod.default.interceptors)
|
|
2014
|
-
mod.default.interceptors = [];
|
|
2015
|
-
mod.default.interceptors.push(...combinedInterceptors);
|
|
2016
|
-
if (!mod.default.metadata)
|
|
2017
|
-
mod.default.metadata = {};
|
|
2018
|
-
Object.assign(mod.default.metadata, {
|
|
2019
|
-
...allMetadata,
|
|
2020
|
-
...(routeConfig.metadata || {}),
|
|
2021
|
-
});
|
|
2022
|
-
}
|
|
2023
|
-
catch {
|
|
2024
|
-
// If mod.default is frozen, ignore
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
const prefixMiddlewares = [];
|
|
2028
|
-
if (this.options.debugRouting) {
|
|
2029
|
-
const pathLib = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
2030
|
-
const relFile = pathLib
|
|
2031
|
-
.relative(process.cwd(), filePath)
|
|
2032
|
-
.replace(/\\/g, '/');
|
|
2033
|
-
const gwStr = activeGateways.length > 0
|
|
2034
|
-
? activeGateways
|
|
2035
|
-
.map((g) => pathLib.relative(process.cwd(), g).replace(/\\/g, '/'))
|
|
2036
|
-
.join(' -> ') + ' -> '
|
|
2037
|
-
: '';
|
|
2038
|
-
prefixMiddlewares.push((req, res, next) => {
|
|
2039
|
-
this.log.debug(`[Request] ${req.method} ${req.path} -> ${gwStr}${relFile}`);
|
|
2040
|
-
next();
|
|
2041
|
-
});
|
|
2042
|
-
}
|
|
2043
|
-
if (allCors !== undefined) {
|
|
2044
|
-
if (allCors === true)
|
|
2045
|
-
prefixMiddlewares.push((0, middleware_1.cors)({}));
|
|
2046
|
-
else if (allCors !== false)
|
|
2047
|
-
prefixMiddlewares.push((0, middleware_1.cors)(allCors));
|
|
2048
|
-
}
|
|
2049
|
-
if (Object.keys(allHeaders).length > 0) {
|
|
2050
|
-
prefixMiddlewares.push((req, res, next) => {
|
|
2051
|
-
for (const [k, v] of Object.entries(allHeaders))
|
|
2052
|
-
res.setHeader(k, v);
|
|
2053
|
-
next();
|
|
2054
|
-
});
|
|
2055
|
-
}
|
|
2056
|
-
prefixMiddlewares.push(...allMiddlewares);
|
|
2057
|
-
const isRouter = (obj) => obj && 'handle' in obj && 'get' in obj;
|
|
2058
|
-
const CONTROLLER_PREFIX = Symbol.for('exisjs:controller_prefix');
|
|
2059
|
-
const isController = (obj) => obj && obj.prototype && obj.prototype[CONTROLLER_PREFIX] !== undefined;
|
|
2060
|
-
const unwrappedMod = mod && mod.default && mod.default.default
|
|
2061
|
-
? mod.default.default
|
|
2062
|
-
: mod && mod.default
|
|
2063
|
-
? mod.default
|
|
2064
|
-
: mod;
|
|
2065
|
-
const functionalControllerObj = unwrappedMod && unwrappedMod.__isController ? unwrappedMod : null;
|
|
2066
|
-
if (functionalControllerObj) {
|
|
2067
|
-
// ─── THE NEW PERFECT FUNCTIONAL CONTROLLER ───
|
|
2068
|
-
const compiledRouter = this.compileFunctionalController(functionalControllerObj);
|
|
2069
|
-
this.mountRouteWithSource(routePath, compiledRouter, filePath, prefixMiddlewares);
|
|
2070
|
-
}
|
|
2071
|
-
else if (mod.router && isRouter(mod.router)) {
|
|
2072
|
-
this.mountRouteWithSource(routePath, mod.router, filePath, prefixMiddlewares);
|
|
2073
|
-
}
|
|
2074
|
-
else if (unwrappedMod && isController(unwrappedMod)) {
|
|
2075
|
-
this.registerControllers([unwrappedMod], routePath, prefixMiddlewares);
|
|
2076
|
-
}
|
|
2077
|
-
else if (unwrappedMod && isRouter(unwrappedMod)) {
|
|
2078
|
-
this.mountRouteWithSource(routePath, unwrappedMod, filePath, prefixMiddlewares);
|
|
2079
|
-
}
|
|
2080
|
-
else if (isRouter(mod)) {
|
|
2081
|
-
this.mountRouteWithSource(routePath, mod, filePath, prefixMiddlewares);
|
|
2082
|
-
}
|
|
2083
|
-
else {
|
|
2084
|
-
this.log.warn(`\n[WARN] Route file \x1b[36m${filePath}\x1b[0m does not export a recognized router or controller.\n`);
|
|
2085
|
-
}
|
|
445
|
+
// ─── Listen ───────────────────────────────────────────────────────────────────
|
|
446
|
+
listen(portOrOptions, callback) {
|
|
447
|
+
return this.bootstrapper.listen(portOrOptions, callback);
|
|
2086
448
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
if (config.cors) {
|
|
2091
|
-
fileMiddleware.push(config.cors === true ? (0, middleware_1.cors)({}) : (0, middleware_1.cors)(config.cors));
|
|
2092
|
-
}
|
|
2093
|
-
if (config.middleware) {
|
|
2094
|
-
fileMiddleware.push(...(Array.isArray(config.middleware)
|
|
2095
|
-
? config.middleware
|
|
2096
|
-
: [config.middleware]));
|
|
2097
|
-
}
|
|
2098
|
-
const { onError, onResponse } = config;
|
|
2099
|
-
for (const [key, routeConfig] of Object.entries(config)) {
|
|
2100
|
-
if ([
|
|
2101
|
-
'cors',
|
|
2102
|
-
'middleware',
|
|
2103
|
-
'onError',
|
|
2104
|
-
'onResponse',
|
|
2105
|
-
'__isController',
|
|
2106
|
-
].includes(key))
|
|
2107
|
-
continue;
|
|
2108
|
-
const rc = routeConfig;
|
|
2109
|
-
if (!rc || !rc.method || !rc.path || !rc.handle)
|
|
2110
|
-
continue;
|
|
2111
|
-
const routeMiddlewares = [...fileMiddleware];
|
|
2112
|
-
if (rc.cors) {
|
|
2113
|
-
routeMiddlewares.push(rc.cors === true ? (0, middleware_1.cors)({}) : (0, middleware_1.cors)(rc.cors));
|
|
2114
|
-
}
|
|
2115
|
-
if (rc.middleware) {
|
|
2116
|
-
routeMiddlewares.push(...(Array.isArray(rc.middleware) ? rc.middleware : [rc.middleware]));
|
|
2117
|
-
}
|
|
2118
|
-
// ─── SUPER HANDLER WRAPPER ───
|
|
2119
|
-
const superHandler = async (req, res, next) => {
|
|
2120
|
-
if (onResponse) {
|
|
2121
|
-
res.raw.on('finish', () => onResponse(req, res));
|
|
2122
|
-
}
|
|
2123
|
-
try {
|
|
2124
|
-
// 0. Enforce Route Permissions (Role Authorization)
|
|
2125
|
-
if (rc.permissions && rc.permissions.length > 0) {
|
|
2126
|
-
if (!req.user) {
|
|
2127
|
-
res.status(401).json({
|
|
2128
|
-
success: false,
|
|
2129
|
-
error: {
|
|
2130
|
-
code: 'UNAUTHORIZED',
|
|
2131
|
-
message: 'Unauthorized: User not found on request',
|
|
2132
|
-
},
|
|
2133
|
-
});
|
|
2134
|
-
return;
|
|
2135
|
-
}
|
|
2136
|
-
const userPerms = req.user.permissions || req.user.roles || req.user.role || [];
|
|
2137
|
-
const permsArray = Array.isArray(userPerms)
|
|
2138
|
-
? userPerms
|
|
2139
|
-
: [userPerms];
|
|
2140
|
-
const hasPerm = rc.permissions.every((p) => permsArray.includes(p));
|
|
2141
|
-
if (!hasPerm) {
|
|
2142
|
-
res.status(403).json({
|
|
2143
|
-
success: false,
|
|
2144
|
-
error: {
|
|
2145
|
-
code: 'FORBIDDEN',
|
|
2146
|
-
message: 'Forbidden: Insufficient permissions',
|
|
2147
|
-
},
|
|
2148
|
-
});
|
|
2149
|
-
return;
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
// 1. Run Guards
|
|
2153
|
-
const routeGuards = [...(config.guards || []), ...(rc.guards || [])];
|
|
2154
|
-
for (const guard of routeGuards) {
|
|
2155
|
-
const allowed = await (typeof guard === 'function'
|
|
2156
|
-
? guard.prototype?.canActivate
|
|
2157
|
-
? new guard().canActivate(req)
|
|
2158
|
-
: guard(req)
|
|
2159
|
-
: guard.canActivate(req));
|
|
2160
|
-
if (!allowed) {
|
|
2161
|
-
if (res && !res.headersSent) {
|
|
2162
|
-
res.status(403).json({
|
|
2163
|
-
success: false,
|
|
2164
|
-
error: { code: 'FORBIDDEN', message: 'Forbidden by Guard' },
|
|
2165
|
-
});
|
|
2166
|
-
}
|
|
2167
|
-
return;
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
// Build Context
|
|
2171
|
-
const ctx = {
|
|
2172
|
-
body: req.body,
|
|
2173
|
-
query: req.query,
|
|
2174
|
-
params: req.params,
|
|
2175
|
-
headers: req.headers,
|
|
2176
|
-
req,
|
|
2177
|
-
res,
|
|
2178
|
-
app: this,
|
|
2179
|
-
socket: req.ws,
|
|
2180
|
-
state: context_1.executionContext.getStore()?.state || {},
|
|
2181
|
-
...req, // Spread req to allow access to user, session, etc.
|
|
2182
|
-
};
|
|
2183
|
-
const result = await rc.handle(ctx);
|
|
2184
|
-
// 2. Run Interceptors
|
|
2185
|
-
const routeInterceptors = [
|
|
2186
|
-
...(config.interceptors || []),
|
|
2187
|
-
...(rc.interceptors || []),
|
|
2188
|
-
];
|
|
2189
|
-
for (const interceptor of routeInterceptors) {
|
|
2190
|
-
await (typeof interceptor === 'function'
|
|
2191
|
-
? interceptor.prototype?.intercept
|
|
2192
|
-
? new interceptor().intercept(req, res)
|
|
2193
|
-
: interceptor(req, res)
|
|
2194
|
-
: interceptor.intercept(req, res));
|
|
2195
|
-
}
|
|
2196
|
-
if (result !== undefined && !res.headersSent) {
|
|
2197
|
-
if (typeof result === 'object' && result !== null) {
|
|
2198
|
-
res.json(result);
|
|
2199
|
-
}
|
|
2200
|
-
else {
|
|
2201
|
-
res.send(String(result));
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
catch (err) {
|
|
2206
|
-
if (onError) {
|
|
2207
|
-
try {
|
|
2208
|
-
await onError(err, req, res);
|
|
2209
|
-
}
|
|
2210
|
-
catch (hookErr) {
|
|
2211
|
-
next(hookErr);
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
else {
|
|
2215
|
-
next(err); // Hands off to ExisJS global error handler (which handles HttpErrors automatically!)
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
};
|
|
2219
|
-
// Mount to router
|
|
2220
|
-
const method = rc.method.toLowerCase();
|
|
2221
|
-
const schema = {};
|
|
2222
|
-
const isValidatorOrPipe = (v) => v.parse ||
|
|
2223
|
-
v.transform ||
|
|
2224
|
-
(typeof v === 'function' && v.prototype?.transform);
|
|
2225
|
-
if (rc.body) {
|
|
2226
|
-
schema.body = isValidatorOrPipe(rc.body) ? rc.body : validator_1.v.object(rc.body);
|
|
2227
|
-
}
|
|
2228
|
-
if (rc.query) {
|
|
2229
|
-
schema.query = isValidatorOrPipe(rc.query)
|
|
2230
|
-
? rc.query
|
|
2231
|
-
: validator_1.v.object(rc.query);
|
|
2232
|
-
}
|
|
2233
|
-
if (rc.params) {
|
|
2234
|
-
schema.params = isValidatorOrPipe(rc.params)
|
|
2235
|
-
? rc.params
|
|
2236
|
-
: validator_1.v.object(rc.params);
|
|
2237
|
-
}
|
|
2238
|
-
if (rc.host) {
|
|
2239
|
-
schema.host = rc.host;
|
|
2240
|
-
}
|
|
2241
|
-
const combinedFilters = [
|
|
2242
|
-
...(config.filters
|
|
2243
|
-
? Array.isArray(config.filters)
|
|
2244
|
-
? config.filters
|
|
2245
|
-
: [config.filters]
|
|
2246
|
-
: []),
|
|
2247
|
-
...(rc.filters
|
|
2248
|
-
? Array.isArray(rc.filters)
|
|
2249
|
-
? rc.filters
|
|
2250
|
-
: [rc.filters]
|
|
2251
|
-
: []),
|
|
2252
|
-
];
|
|
2253
|
-
if (combinedFilters.length > 0) {
|
|
2254
|
-
schema.filters = combinedFilters;
|
|
2255
|
-
}
|
|
2256
|
-
const combinedMetadata = {
|
|
2257
|
-
...(config.metadata || {}),
|
|
2258
|
-
...(rc.metadata || {}),
|
|
2259
|
-
};
|
|
2260
|
-
if (Object.keys(combinedMetadata).length > 0) {
|
|
2261
|
-
schema.metadata = combinedMetadata;
|
|
2262
|
-
}
|
|
2263
|
-
if (method === 'sse') {
|
|
2264
|
-
const sseHandler = async (stream, rawReq) => {
|
|
2265
|
-
try {
|
|
2266
|
-
const ctx = {
|
|
2267
|
-
body: rawReq.body,
|
|
2268
|
-
query: rawReq.query,
|
|
2269
|
-
params: rawReq.params,
|
|
2270
|
-
headers: rawReq.headers,
|
|
2271
|
-
req: rawReq,
|
|
2272
|
-
res: undefined, // Res is not available in standard SSE after handshake
|
|
2273
|
-
app: this,
|
|
2274
|
-
stream,
|
|
2275
|
-
...rawReq,
|
|
2276
|
-
};
|
|
2277
|
-
await rc.handle(ctx);
|
|
2278
|
-
}
|
|
2279
|
-
catch (err) {
|
|
2280
|
-
if (onError) {
|
|
2281
|
-
await onError(err, rawReq, {});
|
|
2282
|
-
}
|
|
2283
|
-
else {
|
|
2284
|
-
console.error('[Exis SSE Error]', err);
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
};
|
|
2288
|
-
router.sse(rc.path, ...routeMiddlewares, sseHandler);
|
|
2289
|
-
}
|
|
2290
|
-
else {
|
|
2291
|
-
if (Object.keys(schema).length > 0) {
|
|
2292
|
-
;
|
|
2293
|
-
router[method](rc.path, ...routeMiddlewares, schema, superHandler);
|
|
2294
|
-
}
|
|
2295
|
-
else {
|
|
2296
|
-
;
|
|
2297
|
-
router[method](rc.path, ...routeMiddlewares, superHandler);
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
return router;
|
|
449
|
+
// ─── Banner ───────────────────────────────────────────────────────────────────
|
|
450
|
+
async _printStartupBanner() {
|
|
451
|
+
return this.bootstrapper.printStartupBanner();
|
|
2302
452
|
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
const r = { ...route, path: fullPath, sourceFile };
|
|
2307
|
-
if (prefixMiddlewares.length > 0) {
|
|
2308
|
-
r.handlers = [...prefixMiddlewares, ...r.handlers];
|
|
2309
|
-
}
|
|
2310
|
-
this.router.addRawRoute(r);
|
|
2311
|
-
if (this.hooks.route.length > 0) {
|
|
2312
|
-
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
2313
|
-
for (let i = 0; i < this.hooks.route.length; i++) {
|
|
2314
|
-
this.hooks.route[i]({ method: route.method, path: fullPath });
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
453
|
+
// ─── Graceful Shutdown ────────────────────────────────────────────────────────
|
|
454
|
+
onShutdown(hook) {
|
|
455
|
+
this.bootstrapper.onShutdown(hook);
|
|
2318
456
|
return this;
|
|
2319
457
|
}
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
const fullPath = path.join(dir, entry.name);
|
|
2327
|
-
if (entry.isDirectory()) {
|
|
2328
|
-
let segment = entry.name;
|
|
2329
|
-
if (segment.startsWith('(') && segment.endsWith(')')) {
|
|
2330
|
-
const subResults = await this.scanDirectory(fullPath, baseRoute);
|
|
2331
|
-
results.push(...subResults);
|
|
2332
|
-
continue;
|
|
2333
|
-
}
|
|
2334
|
-
if (segment.startsWith('[...') && segment.endsWith(']')) {
|
|
2335
|
-
segment = '*' + segment.slice(4, -1);
|
|
2336
|
-
}
|
|
2337
|
-
else {
|
|
2338
|
-
segment = segment.replace(/\[(.*?)\]/g, ':$1');
|
|
2339
|
-
}
|
|
2340
|
-
const nextBase = baseRoute === '/' ? `/${segment}` : `${baseRoute}/${segment}`;
|
|
2341
|
-
const subResults = await this.scanDirectory(fullPath, nextBase);
|
|
2342
|
-
results.push(...subResults);
|
|
2343
|
-
}
|
|
2344
|
-
else {
|
|
2345
|
-
results.push({ filePath: fullPath, routePath: baseRoute });
|
|
2346
|
-
}
|
|
2347
|
-
}
|
|
2348
|
-
return results;
|
|
458
|
+
close(timeout = 5000) {
|
|
459
|
+
return this.bootstrapper.close(timeout);
|
|
460
|
+
}
|
|
461
|
+
// ─── Expose internals ─────────────────────────────────────────────────────────
|
|
462
|
+
getRouter() {
|
|
463
|
+
return this.router;
|
|
2349
464
|
}
|
|
2350
465
|
// ─── Queues ─────────────────────────────────────────────────────────────────
|
|
2351
|
-
/**
|
|
2352
|
-
* Registers a background job worker handler.
|
|
2353
|
-
*/
|
|
2354
466
|
queue(name, handler, options) {
|
|
2355
|
-
|
|
2356
|
-
if (this.options.queue?.enableWorkers === false) {
|
|
2357
|
-
// workers disabled explicitly, gracefully skip registering
|
|
2358
|
-
return;
|
|
2359
|
-
}
|
|
2360
|
-
// Worker not initialized yet (e.g. config loading is pending), buffer the job
|
|
2361
|
-
this._pendingQueueJobs.push({
|
|
2362
|
-
name,
|
|
2363
|
-
handler: handler,
|
|
2364
|
-
...options,
|
|
2365
|
-
});
|
|
2366
|
-
return;
|
|
2367
|
-
}
|
|
2368
|
-
this._queueWorker.registerJob({
|
|
2369
|
-
name,
|
|
2370
|
-
handler: handler,
|
|
2371
|
-
...options,
|
|
2372
|
-
});
|
|
467
|
+
return this.queueManager.queue(name, handler, options);
|
|
2373
468
|
}
|
|
2374
|
-
/**
|
|
2375
|
-
* Enqueues a payload for background processing.
|
|
2376
|
-
*/
|
|
2377
469
|
async enqueue(name, payload, opts) {
|
|
2378
|
-
|
|
2379
|
-
throw new Error(`Queue is not initialized. Please configure ExisConfig.queue first.`);
|
|
2380
|
-
}
|
|
2381
|
-
return this._queueClient.enqueue(name, payload, opts);
|
|
2382
|
-
}
|
|
2383
|
-
_initQueue(qConfig) {
|
|
2384
|
-
if (!this._queueClient) {
|
|
2385
|
-
this._queueClient = new client_1.ExisQueue(qConfig);
|
|
2386
|
-
}
|
|
2387
|
-
const enableWorkers = qConfig.enableWorkers ?? true;
|
|
2388
|
-
if (enableWorkers && !this._queueWorker) {
|
|
2389
|
-
this._queueWorker = new worker_1.ExisWorker(qConfig, this.log, this._queueClient.getDriver());
|
|
2390
|
-
this._queueWorker.start();
|
|
2391
|
-
const redisClient = this._queueWorker.redis || null;
|
|
2392
|
-
this._cronScheduler = new scheduler_1.CronScheduler(this._queueWorker, redisClient, this.log);
|
|
2393
|
-
this._cronScheduler.start();
|
|
2394
|
-
}
|
|
470
|
+
return this.queueManager.enqueue(name, payload, opts);
|
|
2395
471
|
}
|
|
2396
472
|
}
|
|
2397
473
|
exports.App = App;
|