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 CHANGED
@@ -1,5 +1,5 @@
1
1
  // MasterControl - by Alexander rich
2
- // version 1.0.251
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
- var rootFolderLocation = path.join(this.root, folderLocation, innerFolder);
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
- async serverRun(req, res){
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
- // Handle CORS preflight (OPTIONS) requests early and positively
431
- if (req.method === 'OPTIONS') {
432
- try {
433
- if (this.cors && typeof this.cors.load === 'function') {
434
- if (!this.cors.options) {
435
- this.cors.init({
436
- origin: true,
437
- methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
438
- allowedHeaders: true,
439
- credentials: false,
440
- maxAge: 86400
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
- this.cors.load({ request: req, response: res });
444
- } else {
445
- res.setHeader('access-control-allow-origin', '*');
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
- // Apply CORS headers to ALL non-OPTIONS requests
467
- try {
468
- if (this.cors && typeof this.cors.load === 'function') {
469
- this.cors.load({ request: req, response: res });
470
- }
471
- } catch (e) {
472
- console.warn('CORS load failed for non-OPTIONS request:', e.message);
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
- // parse URL
476
- const parsedUrl = url.parse(req.url);
477
- // extract URL path
478
- let pathname = `.${parsedUrl.pathname}`;
478
+ return; // Terminal - don't call next()
479
+ }
479
480
 
480
- // based on the URL path, extract the file extension. e.g. .js, .doc, ...
481
- const ext = path.parse(pathname).ext;
481
+ await next(); // Not static, continue pipeline
482
+ });
482
483
 
483
- // handle simple preflight configuration - might need a complex approch for all scenarios
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
- // if extension exist then its a file.
487
- if(ext === ""){
488
- var requestObject = await this.middleware(req, res);
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
- var loadedDone = false;
495
- if (typeof $that._loadedFunc === 'function') {
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
- else{
502
- require(`${this.root}/config/load`)(requestObject);
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
- else{
509
-
510
- fs.exists(pathname, function (exist) {
511
-
512
- if(!exist) {
513
- // if the file is not found, return 404
514
- res.statusCode = 404;
515
- res.end(`File ${pathname} not found!`);
516
- return;
517
- }
518
-
519
- // if is a directory search for index file matching the extension
520
- if (fs.statSync(pathname).isDirectory()) pathname += '/index' + ext;
521
-
522
- // read file from file system
523
- fs.readFile(pathname, function(err, data){
524
- if(err){
525
- res.statusCode = 500;
526
- res.end(`Error getting the file: ${err}.`);
527
- } else {
528
- const mimeType = $that.router.findMimeType(ext);
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 server()
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);