mastercontroller 1.2.14 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MasterControl.js +148 -101
- package/MasterCors.js +29 -0
- package/MasterPipeline.js +344 -0
- package/MasterRouter.js +44 -22
- package/MasterSession.js +19 -0
- package/MasterTimeout.js +332 -0
- package/README.md +1496 -36
- package/docs/timeout-and-error-handling.md +712 -0
- package/error/MasterErrorRenderer.js +529 -0
- package/package.json +5 -5
- package/security/SecurityMiddleware.js +73 -1
package/MasterControl.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// MasterControl - by Alexander rich
|
|
2
|
-
// version 1.0.
|
|
2
|
+
// version 1.0.252
|
|
3
3
|
|
|
4
4
|
var url = require('url');
|
|
5
5
|
var fileserver = require('fs');
|
|
@@ -192,7 +192,16 @@ class MasterControl {
|
|
|
192
192
|
|
|
193
193
|
component(folderLocation, innerFolder){
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
// Enhanced: Support both relative (to master.root) and absolute paths
|
|
196
|
+
// If folderLocation is absolute, use it directly; otherwise join with master.root
|
|
197
|
+
var rootFolderLocation;
|
|
198
|
+
if (path.isAbsolute(folderLocation)) {
|
|
199
|
+
// Absolute path provided - use it directly
|
|
200
|
+
rootFolderLocation = path.join(folderLocation, innerFolder);
|
|
201
|
+
} else {
|
|
202
|
+
// Relative path - join with master.root (original behavior)
|
|
203
|
+
rootFolderLocation = path.join(this.root, folderLocation, innerFolder);
|
|
204
|
+
}
|
|
196
205
|
|
|
197
206
|
// Structure is always: {rootFolderLocation}/config/initializers/config.js
|
|
198
207
|
var configPath = path.join(rootFolderLocation, 'config', 'initializers', 'config.js');
|
|
@@ -251,6 +260,9 @@ class MasterControl {
|
|
|
251
260
|
// before user config initializes them.
|
|
252
261
|
try {
|
|
253
262
|
$that.addInternalTools([
|
|
263
|
+
'MasterPipeline',
|
|
264
|
+
'MasterTimeout',
|
|
265
|
+
'MasterErrorRenderer',
|
|
254
266
|
'MasterAction',
|
|
255
267
|
'MasterActionFilters',
|
|
256
268
|
'MasterRouter',
|
|
@@ -267,6 +279,10 @@ class MasterControl {
|
|
|
267
279
|
} catch (e) {
|
|
268
280
|
console.error('[MasterControl] Failed to load internal tools:', e && e.message);
|
|
269
281
|
}
|
|
282
|
+
|
|
283
|
+
// Register core middleware that must run for framework to function
|
|
284
|
+
$that._registerCoreMiddleware();
|
|
285
|
+
|
|
270
286
|
if(type === "http"){
|
|
271
287
|
$that.serverProtocol = "http";
|
|
272
288
|
return http.createServer(async function(req, res) {
|
|
@@ -423,120 +439,148 @@ class MasterControl {
|
|
|
423
439
|
});
|
|
424
440
|
}
|
|
425
441
|
|
|
426
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Register core middleware that must run for the framework to function
|
|
444
|
+
* This includes: static files, body parsing, scoped services, routing, error handling
|
|
445
|
+
*/
|
|
446
|
+
_registerCoreMiddleware(){
|
|
427
447
|
var $that = this;
|
|
428
|
-
console.log("path", `${req.method} ${req.url}`);
|
|
429
448
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
});
|
|
449
|
+
// 1. Static File Serving
|
|
450
|
+
$that.pipeline.use(async (ctx, next) => {
|
|
451
|
+
if (ctx.isStatic) {
|
|
452
|
+
// Serve static files
|
|
453
|
+
let pathname = `.${ctx.request.url}`;
|
|
454
|
+
|
|
455
|
+
fs.exists(pathname, function (exist) {
|
|
456
|
+
if (!exist) {
|
|
457
|
+
ctx.response.statusCode = 404;
|
|
458
|
+
ctx.response.end(`File ${pathname} not found!`);
|
|
459
|
+
return;
|
|
442
460
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
res.setHeader('access-control-allow-methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
447
|
-
if (req.headers['access-control-request-headers']) {
|
|
448
|
-
res.setHeader('access-control-allow-headers', req.headers['access-control-request-headers']);
|
|
461
|
+
|
|
462
|
+
if (fs.statSync(pathname).isDirectory()) {
|
|
463
|
+
pathname += '/index' + path.parse(pathname).ext;
|
|
449
464
|
}
|
|
450
|
-
res.setHeader('access-control-max-age', '86400');
|
|
451
|
-
}
|
|
452
|
-
} catch (e) {
|
|
453
|
-
res.setHeader('access-control-allow-origin', '*');
|
|
454
|
-
res.setHeader('access-control-allow-methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
455
|
-
if (req.headers['access-control-request-headers']) {
|
|
456
|
-
res.setHeader('access-control-allow-headers', req.headers['access-control-request-headers']);
|
|
457
|
-
}
|
|
458
|
-
res.setHeader('access-control-max-age', '86400');
|
|
459
|
-
}
|
|
460
|
-
res.statusCode = 204;
|
|
461
|
-
res.setHeader('content-length', '0');
|
|
462
|
-
res.end();
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
465
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
466
|
+
fs.readFile(pathname, function(err, data) {
|
|
467
|
+
if (err) {
|
|
468
|
+
ctx.response.statusCode = 500;
|
|
469
|
+
ctx.response.end(`Error getting the file: ${err}.`);
|
|
470
|
+
} else {
|
|
471
|
+
const mimeType = $that.router.findMimeType(path.parse(pathname).ext);
|
|
472
|
+
ctx.response.setHeader('Content-type', mimeType || 'text/plain');
|
|
473
|
+
ctx.response.end(data);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
});
|
|
474
477
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// extract URL path
|
|
478
|
-
let pathname = `.${parsedUrl.pathname}`;
|
|
478
|
+
return; // Terminal - don't call next()
|
|
479
|
+
}
|
|
479
480
|
|
|
480
|
-
|
|
481
|
-
|
|
481
|
+
await next(); // Not static, continue pipeline
|
|
482
|
+
});
|
|
482
483
|
|
|
483
|
-
//
|
|
484
|
+
// 2. Timeout Tracking (optional - disabled by default until init)
|
|
485
|
+
// Will be configured by user in config.js with master.timeout.init()
|
|
486
|
+
// This is just a placeholder registration - actual timeout is set in user config
|
|
484
487
|
|
|
488
|
+
// 3. Request Body Parsing (always needed)
|
|
489
|
+
$that.pipeline.use(async (ctx, next) => {
|
|
490
|
+
// Parse body using MasterRequest
|
|
491
|
+
const params = await $that.request.getRequestParam(ctx.request, ctx.response);
|
|
485
492
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if(requestObject !== -1){
|
|
490
|
-
// HSTS header if enabled
|
|
491
|
-
if(this.serverProtocol === 'https' && this._hstsEnabled){
|
|
492
|
-
res.setHeader('strict-transport-security', `max-age=${this._hstsMaxAge}; includeSubDomains`);
|
|
493
|
+
// Merge parsed params into context
|
|
494
|
+
if (params && params.query) {
|
|
495
|
+
ctx.params.query = params.query;
|
|
493
496
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
loadedDone = $that._loadedFunc(requestObject);
|
|
497
|
-
if (loadedDone){
|
|
498
|
-
require(`${this.root}/config/load`)(requestObject);
|
|
499
|
-
}
|
|
497
|
+
if (params && params.formData) {
|
|
498
|
+
ctx.params.formData = params.formData;
|
|
500
499
|
}
|
|
501
|
-
|
|
502
|
-
|
|
500
|
+
|
|
501
|
+
await next();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// 4. Load Scoped Services (per request - always needed)
|
|
505
|
+
$that.pipeline.use(async (ctx, next) => {
|
|
506
|
+
for (var key in $that._scopedList) {
|
|
507
|
+
var className = $that._scopedList[key];
|
|
508
|
+
$that.requestList[key] = new className();
|
|
503
509
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
// if the file is found, set Content-type and send data
|
|
531
|
-
res.setHeader('Content-type', mimeType || 'text/plain' );
|
|
532
|
-
res.end(data);
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
|
|
510
|
+
await next();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// 4. HSTS Header (if enabled for HTTPS)
|
|
514
|
+
$that.pipeline.use(async (ctx, next) => {
|
|
515
|
+
if ($that.serverProtocol === 'https' && $that._hstsEnabled) {
|
|
516
|
+
ctx.response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
517
|
+
}
|
|
518
|
+
await next();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// 5. Routing (TERMINAL - always needed)
|
|
522
|
+
$that.pipeline.run(async (ctx) => {
|
|
523
|
+
// Load config/load which triggers routing
|
|
524
|
+
require(`${$that.root}/config/load`)(ctx);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// 6. Global Error Handler
|
|
528
|
+
$that.pipeline.useError(async (error, ctx, next) => {
|
|
529
|
+
logger.error({
|
|
530
|
+
code: 'MC_ERR_PIPELINE',
|
|
531
|
+
message: 'Error in middleware pipeline',
|
|
532
|
+
error: error.message,
|
|
533
|
+
stack: error.stack,
|
|
534
|
+
path: ctx.request.url,
|
|
535
|
+
method: ctx.type
|
|
536
536
|
});
|
|
537
|
+
|
|
538
|
+
if (!ctx.response.headersSent) {
|
|
539
|
+
ctx.response.statusCode = 500;
|
|
540
|
+
ctx.response.setHeader('Content-Type', 'application/json');
|
|
541
|
+
ctx.response.end(JSON.stringify({
|
|
542
|
+
error: 'Internal Server Error',
|
|
543
|
+
message: process.env.NODE_ENV === 'production'
|
|
544
|
+
? 'An error occurred'
|
|
545
|
+
: error.message
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async serverRun(req, res){
|
|
552
|
+
var $that = this;
|
|
553
|
+
console.log("path", `${req.method} ${req.url}`);
|
|
554
|
+
|
|
555
|
+
// Create request context for middleware pipeline
|
|
556
|
+
const parsedUrl = url.parse(req.url);
|
|
557
|
+
const pathname = parsedUrl.pathname;
|
|
558
|
+
const ext = path.parse(pathname).ext;
|
|
559
|
+
|
|
560
|
+
const context = {
|
|
561
|
+
request: req,
|
|
562
|
+
response: res,
|
|
563
|
+
requrl: url.parse(req.url, true),
|
|
564
|
+
pathName: pathname.replace(/^\/|\/$/g, '').toLowerCase(),
|
|
565
|
+
type: req.method.toLowerCase(),
|
|
566
|
+
params: {},
|
|
567
|
+
state: {}, // User-defined state shared across middleware
|
|
568
|
+
master: $that, // Access to framework instance
|
|
569
|
+
isStatic: ext !== '' // Is this a static file request?
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// Execute middleware pipeline
|
|
573
|
+
try {
|
|
574
|
+
await $that.pipeline.execute(context);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
console.error('Pipeline execution failed:', error);
|
|
577
|
+
if (!res.headersSent) {
|
|
578
|
+
res.statusCode = 500;
|
|
579
|
+
res.end('Internal Server Error');
|
|
580
|
+
}
|
|
537
581
|
}
|
|
538
|
-
|
|
539
|
-
} // end
|
|
582
|
+
|
|
583
|
+
} // end serverRun()
|
|
540
584
|
|
|
541
585
|
start(server){
|
|
542
586
|
this.server = server;
|
|
@@ -565,6 +609,9 @@ class MasterControl {
|
|
|
565
609
|
if(requiredList.constructor === Array){
|
|
566
610
|
// Map module names to their new organized paths
|
|
567
611
|
const modulePathMap = {
|
|
612
|
+
'MasterPipeline': './MasterPipeline',
|
|
613
|
+
'MasterTimeout': './MasterTimeout',
|
|
614
|
+
'MasterErrorRenderer': './error/MasterErrorRenderer',
|
|
568
615
|
'MasterError': './error/MasterError',
|
|
569
616
|
'MasterAction': './MasterAction',
|
|
570
617
|
'MasterActionFilters': './MasterActionFilters',
|
package/MasterCors.js
CHANGED
|
@@ -11,6 +11,13 @@ class MasterCors{
|
|
|
11
11
|
else{
|
|
12
12
|
master.error.log("cors options missing", "warn");
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
// Auto-register with pipeline if available
|
|
16
|
+
if (master.pipeline) {
|
|
17
|
+
master.pipeline.use(this.middleware());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return this; // Chainable
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
load(params){
|
|
@@ -167,6 +174,28 @@ class MasterCors{
|
|
|
167
174
|
}
|
|
168
175
|
}
|
|
169
176
|
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get CORS middleware for the pipeline
|
|
180
|
+
* Handles both preflight OPTIONS requests and regular requests
|
|
181
|
+
*/
|
|
182
|
+
middleware() {
|
|
183
|
+
var $that = this;
|
|
184
|
+
|
|
185
|
+
return async (ctx, next) => {
|
|
186
|
+
// Handle preflight OPTIONS request
|
|
187
|
+
if (ctx.type === 'options') {
|
|
188
|
+
$that.load({ request: ctx.request, response: ctx.response });
|
|
189
|
+
ctx.response.statusCode = 204;
|
|
190
|
+
ctx.response.end();
|
|
191
|
+
return; // Terminal - don't call next()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Regular request - apply CORS headers
|
|
195
|
+
$that.load({ request: ctx.request, response: ctx.response });
|
|
196
|
+
await next();
|
|
197
|
+
};
|
|
198
|
+
}
|
|
170
199
|
}
|
|
171
200
|
|
|
172
201
|
master.extend("cors", MasterCors);
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// MasterPipeline - Middleware Pipeline System
|
|
2
|
+
// version 1.0
|
|
3
|
+
|
|
4
|
+
var master = require('./MasterControl');
|
|
5
|
+
const { logger } = require('./error/MasterErrorLogger');
|
|
6
|
+
|
|
7
|
+
class MasterPipeline {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.middleware = [];
|
|
10
|
+
this.errorHandlers = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Use: Add middleware that processes request/response
|
|
15
|
+
*
|
|
16
|
+
* Middleware signature: async (ctx, next) => { await next(); }
|
|
17
|
+
* - ctx: Request context { request, response, params, state, ... }
|
|
18
|
+
* - next: Function to call next middleware in chain
|
|
19
|
+
*
|
|
20
|
+
* Example:
|
|
21
|
+
* master.use(async (ctx, next) => {
|
|
22
|
+
* console.log('Before');
|
|
23
|
+
* await next();
|
|
24
|
+
* console.log('After');
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* @param {Function} middleware - Middleware function
|
|
28
|
+
* @returns {MasterPipeline} - For chaining
|
|
29
|
+
*/
|
|
30
|
+
use(middleware) {
|
|
31
|
+
if (typeof middleware !== 'function') {
|
|
32
|
+
throw new Error('Middleware must be a function');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.middleware.push({
|
|
36
|
+
type: 'use',
|
|
37
|
+
handler: middleware,
|
|
38
|
+
path: null
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return this; // Chainable
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run: Add terminal middleware that ends the pipeline
|
|
46
|
+
*
|
|
47
|
+
* Terminal middleware signature: async (ctx) => { /* send response */ }
|
|
48
|
+
* - Does NOT call next()
|
|
49
|
+
* - Must send response
|
|
50
|
+
*
|
|
51
|
+
* Example:
|
|
52
|
+
* master.run(async (ctx) => {
|
|
53
|
+
* ctx.response.end('Hello World');
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* @param {Function} middleware - Terminal middleware function
|
|
57
|
+
* @returns {MasterPipeline} - For chaining
|
|
58
|
+
*/
|
|
59
|
+
run(middleware) {
|
|
60
|
+
if (typeof middleware !== 'function') {
|
|
61
|
+
throw new Error('Terminal middleware must be a function');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.middleware.push({
|
|
65
|
+
type: 'run',
|
|
66
|
+
handler: middleware,
|
|
67
|
+
path: null
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return this; // Chainable
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Map: Conditionally execute middleware based on path
|
|
75
|
+
*
|
|
76
|
+
* Map signature: (path, configure)
|
|
77
|
+
* - path: String or RegExp to match request path
|
|
78
|
+
* - configure: Function that receives a branch pipeline
|
|
79
|
+
*
|
|
80
|
+
* Example:
|
|
81
|
+
* master.map('/api/*', (api) => {
|
|
82
|
+
* api.use(authMiddleware);
|
|
83
|
+
* api.use(jsonMiddleware);
|
|
84
|
+
* });
|
|
85
|
+
*
|
|
86
|
+
* @param {String|RegExp} path - Path pattern to match
|
|
87
|
+
* @param {Function} configure - Function to configure branch pipeline
|
|
88
|
+
* @returns {MasterPipeline} - For chaining
|
|
89
|
+
*/
|
|
90
|
+
map(path, configure) {
|
|
91
|
+
if (typeof configure !== 'function') {
|
|
92
|
+
throw new Error('Map configuration must be a function');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create sub-pipeline for this branch
|
|
96
|
+
const branch = new MasterPipeline();
|
|
97
|
+
configure(branch);
|
|
98
|
+
|
|
99
|
+
// Wrap branch in conditional middleware
|
|
100
|
+
const conditionalMiddleware = async (ctx, next) => {
|
|
101
|
+
const requestPath = ctx.pathName || ctx.request.url;
|
|
102
|
+
|
|
103
|
+
if (this._pathMatches(requestPath, path)) {
|
|
104
|
+
// Execute branch pipeline
|
|
105
|
+
await branch.execute(ctx);
|
|
106
|
+
// After branch completes, continue main pipeline
|
|
107
|
+
await next();
|
|
108
|
+
} else {
|
|
109
|
+
// Skip branch, continue main pipeline
|
|
110
|
+
await next();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
this.middleware.push({
|
|
115
|
+
type: 'map',
|
|
116
|
+
handler: conditionalMiddleware,
|
|
117
|
+
path: path
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return this; // Chainable
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* UseError: Add error handling middleware
|
|
125
|
+
*
|
|
126
|
+
* Error middleware signature: async (error, ctx, next) => { }
|
|
127
|
+
* - error: The caught error
|
|
128
|
+
* - ctx: Request context
|
|
129
|
+
* - next: Pass to next error handler or rethrow
|
|
130
|
+
*
|
|
131
|
+
* Example:
|
|
132
|
+
* master.useError(async (err, ctx, next) => {
|
|
133
|
+
* if (err.statusCode === 404) {
|
|
134
|
+
* ctx.response.statusCode = 404;
|
|
135
|
+
* ctx.response.end('Not Found');
|
|
136
|
+
* } else {
|
|
137
|
+
* await next(); // Pass to next error handler
|
|
138
|
+
* }
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* @param {Function} handler - Error handler function
|
|
142
|
+
* @returns {MasterPipeline} - For chaining
|
|
143
|
+
*/
|
|
144
|
+
useError(handler) {
|
|
145
|
+
if (typeof handler !== 'function') {
|
|
146
|
+
throw new Error('Error handler must be a function');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.errorHandlers.push(handler);
|
|
150
|
+
return this; // Chainable
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Execute: Run the middleware pipeline for a request
|
|
155
|
+
*
|
|
156
|
+
* Called internally by the framework for each request
|
|
157
|
+
*
|
|
158
|
+
* @param {Object} context - Request context
|
|
159
|
+
*/
|
|
160
|
+
async execute(context) {
|
|
161
|
+
let index = 0;
|
|
162
|
+
|
|
163
|
+
// Create the next function for middleware chain
|
|
164
|
+
const next = async () => {
|
|
165
|
+
// If we've run all middleware, we're done
|
|
166
|
+
if (index >= this.middleware.length) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const current = this.middleware[index++];
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
if (current.type === 'run') {
|
|
174
|
+
// Terminal middleware - don't pass next
|
|
175
|
+
await current.handler(context);
|
|
176
|
+
} else {
|
|
177
|
+
// Regular middleware - pass next
|
|
178
|
+
await current.handler(context, next);
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// Error occurred, run error handlers
|
|
182
|
+
await this._handleError(error, context);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Start the pipeline
|
|
187
|
+
await next();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handle errors through error handler chain
|
|
192
|
+
*
|
|
193
|
+
* @param {Error} error - The error that occurred
|
|
194
|
+
* @param {Object} context - Request context
|
|
195
|
+
*/
|
|
196
|
+
async _handleError(error, context) {
|
|
197
|
+
let errorIndex = 0;
|
|
198
|
+
|
|
199
|
+
const nextError = async () => {
|
|
200
|
+
if (errorIndex >= this.errorHandlers.length) {
|
|
201
|
+
// No more error handlers, log and send generic error
|
|
202
|
+
logger.error({
|
|
203
|
+
code: 'MC_ERR_UNHANDLED',
|
|
204
|
+
message: 'Unhandled error in middleware pipeline',
|
|
205
|
+
error: error.message,
|
|
206
|
+
stack: error.stack
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (!context.response.headersSent) {
|
|
210
|
+
context.response.statusCode = 500;
|
|
211
|
+
context.response.end('Internal Server Error');
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const handler = this.errorHandlers[errorIndex++];
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await handler(error, context, nextError);
|
|
220
|
+
} catch (handlerError) {
|
|
221
|
+
// Error in error handler
|
|
222
|
+
logger.error({
|
|
223
|
+
code: 'MC_ERR_ERROR_HANDLER_FAILED',
|
|
224
|
+
message: 'Error handler threw an error',
|
|
225
|
+
error: handlerError.message
|
|
226
|
+
});
|
|
227
|
+
await nextError();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
await nextError();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if request path matches the map path pattern
|
|
236
|
+
*
|
|
237
|
+
* @param {String} requestPath - The request path
|
|
238
|
+
* @param {String|RegExp} pattern - The pattern to match
|
|
239
|
+
* @returns {Boolean} - True if matches
|
|
240
|
+
*/
|
|
241
|
+
_pathMatches(requestPath, pattern) {
|
|
242
|
+
// Normalize paths (ensure leading slash)
|
|
243
|
+
requestPath = '/' + requestPath.replace(/^\/|\/$/g, '');
|
|
244
|
+
|
|
245
|
+
if (typeof pattern === 'string') {
|
|
246
|
+
pattern = '/' + pattern.replace(/^\/|\/$/g, '');
|
|
247
|
+
|
|
248
|
+
// Wildcard support: /api/* matches /api/users, /api/posts, etc.
|
|
249
|
+
if (pattern.endsWith('/*')) {
|
|
250
|
+
const prefix = pattern.slice(0, -2);
|
|
251
|
+
return requestPath === prefix || requestPath.startsWith(prefix + '/');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Exact or prefix match
|
|
255
|
+
return requestPath === pattern || requestPath.startsWith(pattern + '/');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (pattern instanceof RegExp) {
|
|
259
|
+
return pattern.test(requestPath);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Discover and load middleware from folders
|
|
267
|
+
*
|
|
268
|
+
* @param {String|Object} options - Folder path or options object
|
|
269
|
+
*/
|
|
270
|
+
discoverMiddleware(options) {
|
|
271
|
+
const fs = require('fs');
|
|
272
|
+
const path = require('path');
|
|
273
|
+
|
|
274
|
+
const folders = typeof options === 'string'
|
|
275
|
+
? [options]
|
|
276
|
+
: (options.folders || ['middleware']);
|
|
277
|
+
|
|
278
|
+
folders.forEach(folder => {
|
|
279
|
+
const dir = path.join(master.root, folder);
|
|
280
|
+
if (!fs.existsSync(dir)) {
|
|
281
|
+
console.warn(`[Middleware] Folder not found: ${folder}`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const files = fs.readdirSync(dir)
|
|
286
|
+
.filter(file => file.endsWith('.js'))
|
|
287
|
+
.sort(); // Alphabetical order
|
|
288
|
+
|
|
289
|
+
files.forEach(file => {
|
|
290
|
+
try {
|
|
291
|
+
const middlewarePath = path.join(dir, file);
|
|
292
|
+
const middleware = require(middlewarePath);
|
|
293
|
+
|
|
294
|
+
// Support two patterns:
|
|
295
|
+
// Pattern 1: module.exports = async (ctx, next) => {}
|
|
296
|
+
if (typeof middleware === 'function') {
|
|
297
|
+
this.use(middleware);
|
|
298
|
+
}
|
|
299
|
+
// Pattern 2: module.exports = { register: (master) => {} }
|
|
300
|
+
else if (middleware.register && typeof middleware.register === 'function') {
|
|
301
|
+
middleware.register(master);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
console.warn(`[Middleware] Invalid export in ${folder}/${file}`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log(`[Middleware] Loaded: ${folder}/${file}`);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
console.error(`[Middleware] Failed to load ${folder}/${file}:`, err.message);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Clear all middleware (useful for testing)
|
|
318
|
+
*/
|
|
319
|
+
clear() {
|
|
320
|
+
this.middleware = [];
|
|
321
|
+
this.errorHandlers = [];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Inspect pipeline (for debugging)
|
|
326
|
+
*
|
|
327
|
+
* @returns {Object} - Pipeline information
|
|
328
|
+
*/
|
|
329
|
+
inspect() {
|
|
330
|
+
return {
|
|
331
|
+
middlewareCount: this.middleware.length,
|
|
332
|
+
errorHandlerCount: this.errorHandlers.length,
|
|
333
|
+
middleware: this.middleware.map((m, i) => ({
|
|
334
|
+
index: i,
|
|
335
|
+
type: m.type,
|
|
336
|
+
path: m.path,
|
|
337
|
+
name: m.handler.name || 'anonymous'
|
|
338
|
+
}))
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Register with master
|
|
344
|
+
master.extend("pipeline", MasterPipeline);
|