mastercontroller 1.3.13 → 1.3.15
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/MasterAction.js +302 -62
- package/MasterActionFilters.js +556 -82
- package/MasterControl.js +77 -44
- package/MasterCors.js +61 -19
- package/MasterPipeline.js +29 -6
- package/MasterRequest.js +579 -102
- package/MasterRouter.js +446 -75
- package/MasterSocket.js +380 -15
- package/MasterTemp.js +292 -10
- package/MasterTimeout.js +420 -64
- package/MasterTools.js +478 -77
- package/README.md +505 -0
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -29
- package/.github/workflows/ci.yml +0 -317
- package/PERFORMANCE_SECURITY_AUDIT.md +0 -677
- package/SENIOR_ENGINEER_AUDIT.md +0 -2477
- package/VERIFICATION_CHECKLIST.md +0 -726
- package/log/mastercontroller.log +0 -2
- package/test-json-empty-body.js +0 -76
- package/test-raw-body-preservation.js +0 -128
- package/test-v1.3.4-fixes.js +0 -129
package/MasterRouter.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// version 0.0.250
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const toolClass = require('./MasterTools');
|
|
4
4
|
const EventEmitter = require("events");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const path = require('path');
|
|
6
|
+
// REMOVED: Global currentRoute (race condition bug) - now stored in requestObject
|
|
7
|
+
const tools = new toolClass();
|
|
8
8
|
|
|
9
9
|
// Enhanced error handling
|
|
10
10
|
const { handleRoutingError, handleControllerError, sendErrorResponse } = require('./error/MasterBackendErrorHandler');
|
|
@@ -17,18 +17,69 @@ const { escapeHTML } = require('./security/MasterSanitizer');
|
|
|
17
17
|
|
|
18
18
|
const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.master === 'development';
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// HTTP Status Code Constants
|
|
21
|
+
const HTTP_STATUS = {
|
|
22
|
+
OK: 200,
|
|
23
|
+
NO_CONTENT: 204,
|
|
24
|
+
BAD_REQUEST: 400,
|
|
25
|
+
NOT_FOUND: 404,
|
|
26
|
+
INTERNAL_ERROR: 500
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Event Names Constants
|
|
30
|
+
const EVENT_NAMES = {
|
|
31
|
+
ROUTE_CONSTRAINT_GOOD: 'routeConstraintGood',
|
|
32
|
+
CONTROLLER: 'controller'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// HTTP Methods Constants
|
|
36
|
+
const HTTP_METHODS = {
|
|
37
|
+
GET: 'get',
|
|
38
|
+
POST: 'post',
|
|
39
|
+
PUT: 'put',
|
|
40
|
+
DELETE: 'delete',
|
|
41
|
+
OPTIONS: 'options'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Router Configuration Constants
|
|
45
|
+
const ROUTER_CONFIG = {
|
|
46
|
+
ROUTE_ID_LENGTH: 4,
|
|
47
|
+
DEFAULT_TIMEOUT: 30000, // 30 seconds
|
|
48
|
+
MAX_ROUTE_LENGTH: 2048
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Normalize and match request path against route path, extracting parameters
|
|
53
|
+
*
|
|
54
|
+
* Compares request path segments with route path segments. When a route segment
|
|
55
|
+
* starts with ":", treats it as a parameter and extracts the corresponding value
|
|
56
|
+
* from the request path.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} requestPath - The incoming request path (e.g., "/users/123")
|
|
59
|
+
* @param {string} routePath - The route pattern (e.g., "/users/:id")
|
|
60
|
+
* @param {Object} requestParams - Object to populate with extracted parameters
|
|
61
|
+
* @returns {Object} Object with normalized requestPath and routePath
|
|
62
|
+
* @returns {string} result.requestPath - Original request path
|
|
63
|
+
* @returns {string} result.routePath - Route path with parameters replaced by actual values
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* const params = {};
|
|
67
|
+
* const result = normalizePaths("/users/123", "/users/:id", params);
|
|
68
|
+
* // params = { id: "123" }
|
|
69
|
+
* // result = { requestPath: "/users/123", routePath: "/users/123" }
|
|
70
|
+
*/
|
|
71
|
+
const normalizePaths = function(requestPath, routePath, requestParams){
|
|
72
|
+
const obj = {
|
|
22
73
|
requestPath : "",
|
|
23
74
|
routePath : ""
|
|
24
75
|
}
|
|
25
76
|
|
|
26
|
-
|
|
27
|
-
|
|
77
|
+
const requestPathList = requestPath.split("/");
|
|
78
|
+
const routePathList = routePath.split("/");
|
|
28
79
|
|
|
29
|
-
for(i = 0; i < requestPathList.length; i++){
|
|
30
|
-
requestItem = requestPathList[i];
|
|
31
|
-
routeItem = routePathList[i];
|
|
80
|
+
for(let i = 0; i < requestPathList.length; i++){
|
|
81
|
+
const requestItem = requestPathList[i];
|
|
82
|
+
const routeItem = routePathList[i];
|
|
32
83
|
if(routeItem){
|
|
33
84
|
if(routeItem.indexOf(":") > -1){
|
|
34
85
|
const paramName = routeItem.replace(":", "");
|
|
@@ -50,8 +101,23 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
|
|
|
50
101
|
|
|
51
102
|
/**
|
|
52
103
|
* Sanitize route parameter to prevent injection attacks
|
|
104
|
+
*
|
|
105
|
+
* Checks for and mitigates:
|
|
106
|
+
* - Path traversal attempts (../, ./)
|
|
107
|
+
* - SQL injection patterns
|
|
108
|
+
* - Command injection characters (; | & ` $ ( ))
|
|
109
|
+
*
|
|
110
|
+
* Logs security warnings when attacks are detected.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} paramName - Name of the route parameter
|
|
113
|
+
* @param {string} paramValue - Value to sanitize
|
|
114
|
+
* @returns {string} Sanitized parameter value
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* sanitizeRouteParam("id", "123") // Returns: "123"
|
|
118
|
+
* sanitizeRouteParam("path", "../etc/passwd") // Returns: "etcpasswd" (dangerous parts removed)
|
|
53
119
|
*/
|
|
54
|
-
|
|
120
|
+
const sanitizeRouteParam = function(paramName, paramValue) {
|
|
55
121
|
if (!paramValue || typeof paramValue !== 'string') {
|
|
56
122
|
return paramValue;
|
|
57
123
|
}
|
|
@@ -102,11 +168,38 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
|
|
|
102
168
|
return escapeHTML(paramValue);
|
|
103
169
|
}
|
|
104
170
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Process routes and match against request
|
|
173
|
+
*
|
|
174
|
+
* Iterates through registered routes, attempting to match the request path and HTTP method.
|
|
175
|
+
* When a match is found:
|
|
176
|
+
* 1. Extracts route parameters
|
|
177
|
+
* 2. Executes route constraints (if defined)
|
|
178
|
+
* 3. Emits EVENT_NAMES.ROUTE_CONSTRAINT_GOOD to trigger controller execution
|
|
179
|
+
*
|
|
180
|
+
* Handles OPTIONS requests for CORS preflight.
|
|
181
|
+
*
|
|
182
|
+
* @param {Object} requestObject - Request context with path, method, params
|
|
183
|
+
* @param {EventEmitter} emitter - Event emitter for route match notification
|
|
184
|
+
* @param {Object} routeObject - Route configuration
|
|
185
|
+
* @param {Array} routeObject.routes - Array of route definitions
|
|
186
|
+
* @param {string} routeObject.root - Application root path
|
|
187
|
+
* @param {boolean} routeObject.isComponent - Whether this is a component route
|
|
188
|
+
* @returns {boolean|number} true if route matched, -1 if no match
|
|
189
|
+
* @throws {Error} If route processing fails
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* const result = processRoutes(requestObj, emitter, {
|
|
193
|
+
* routes: [{ path: "/users/:id", type: "get", toController: "users", toAction: "show" }],
|
|
194
|
+
* root: "/app",
|
|
195
|
+
* isComponent: false
|
|
196
|
+
* });
|
|
197
|
+
*/
|
|
198
|
+
const processRoutes = function(requestObject, emitter, routeObject){
|
|
199
|
+
const routeList = routeObject.routes;
|
|
200
|
+
const root = routeObject.root;
|
|
201
|
+
const isComponent = routeObject.isComponent;
|
|
202
|
+
let currentRouteBeingProcessed = null; // Track current route for better error messages
|
|
110
203
|
|
|
111
204
|
try{
|
|
112
205
|
// Ensure routes is an array
|
|
@@ -138,8 +231,8 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
|
|
|
138
231
|
|
|
139
232
|
// FIX: Create a clean copy of params for each route test to prevent parameter pollution
|
|
140
233
|
// This prevents parameters from non-matching routes from accumulating in requestObject.params
|
|
141
|
-
|
|
142
|
-
|
|
234
|
+
const testParams = Object.assign({}, requestObject.params);
|
|
235
|
+
const pathObj = normalizePaths(requestObject.pathName, route.path, testParams);
|
|
143
236
|
|
|
144
237
|
// if we find the route that matches the request
|
|
145
238
|
if(pathObj.requestPath === pathObj.routePath && route.type === requestObject.type){
|
|
@@ -149,17 +242,20 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
|
|
|
149
242
|
// call Constraint
|
|
150
243
|
if(typeof route.constraint === "function"){
|
|
151
244
|
|
|
152
|
-
|
|
245
|
+
const newObj = {};
|
|
153
246
|
//tools.combineObjects(newObj, this._master.controllerList);
|
|
154
247
|
newObj.next = function(){
|
|
155
|
-
|
|
156
|
-
currentRoute
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
248
|
+
// CRITICAL FIX: Store route info in requestObject instead of global
|
|
249
|
+
requestObject.currentRoute = {
|
|
250
|
+
root,
|
|
251
|
+
pathName: requestObject.pathName,
|
|
252
|
+
toAction: requestObject.toAction,
|
|
253
|
+
toController: requestObject.toController,
|
|
254
|
+
response: requestObject.response,
|
|
255
|
+
isComponent,
|
|
256
|
+
routeDef: currentRouteBeingProcessed
|
|
257
|
+
};
|
|
258
|
+
emitter.emit(EVENT_NAMES.ROUTE_CONSTRAINT_GOOD, requestObject);
|
|
163
259
|
};
|
|
164
260
|
|
|
165
261
|
// Wrap constraint execution with error handling
|
|
@@ -188,24 +284,27 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
|
|
|
188
284
|
return true;
|
|
189
285
|
}else{
|
|
190
286
|
|
|
191
|
-
|
|
192
|
-
currentRoute
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
287
|
+
// CRITICAL FIX: Store route info in requestObject instead of global
|
|
288
|
+
requestObject.currentRoute = {
|
|
289
|
+
root,
|
|
290
|
+
pathName: requestObject.pathName,
|
|
291
|
+
toAction: requestObject.toAction,
|
|
292
|
+
toController: requestObject.toController,
|
|
293
|
+
response: requestObject.response,
|
|
294
|
+
isComponent,
|
|
295
|
+
routeDef: currentRouteBeingProcessed
|
|
296
|
+
};
|
|
297
|
+
emitter.emit(EVENT_NAMES.ROUTE_CONSTRAINT_GOOD, requestObject);
|
|
199
298
|
return true;
|
|
200
299
|
}
|
|
201
300
|
|
|
202
301
|
}
|
|
203
302
|
|
|
204
|
-
if(pathObj.requestPath === pathObj.routePath &&
|
|
303
|
+
if(pathObj.requestPath === pathObj.routePath && HTTP_METHODS.OPTIONS === requestObject.type.toLowerCase()){
|
|
205
304
|
// this means that the request is correct but its an options request means its the browser checking to see if the request is allowed
|
|
206
305
|
// Commit the params for OPTIONS requests too
|
|
207
306
|
requestObject.params = testParams;
|
|
208
|
-
requestObject.response.writeHead(
|
|
307
|
+
requestObject.response.writeHead(HTTP_STATUS.OK, {'Content-Type': 'application/json'});
|
|
209
308
|
requestObject.response.end(JSON.stringify({"done": "true"}));
|
|
210
309
|
return true;
|
|
211
310
|
}
|
|
@@ -238,10 +337,23 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
|
|
|
238
337
|
}
|
|
239
338
|
};
|
|
240
339
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Load scoped service instances into request context
|
|
342
|
+
*
|
|
343
|
+
* CRITICAL FIX: Stores scoped services in the request-specific context instead of
|
|
344
|
+
* the shared requestList object. This prevents race conditions where concurrent
|
|
345
|
+
* requests would overwrite each other's services, causing unpredictable behavior
|
|
346
|
+
* and data corruption in production environments.
|
|
347
|
+
*
|
|
348
|
+
* @param {Object} context - Request-specific context object
|
|
349
|
+
* @returns {void}
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* const requestContext = {};
|
|
353
|
+
* loadScopedListClasses.call(masterRouter, requestContext);
|
|
354
|
+
* // requestContext now has scoped service instances
|
|
355
|
+
*/
|
|
356
|
+
const loadScopedListClasses = function(context){
|
|
245
357
|
// FIXED: Use Object.entries() for safe iteration (prevents prototype pollution)
|
|
246
358
|
for (const [key, className] of Object.entries(this._master._scopedList)) {
|
|
247
359
|
// Store scoped services in the context object (request-specific) instead of shared requestList
|
|
@@ -250,11 +362,72 @@ var loadScopedListClasses = function(context){
|
|
|
250
362
|
};
|
|
251
363
|
|
|
252
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Validate route path format
|
|
367
|
+
*
|
|
368
|
+
* @param {string} path - Route path to validate
|
|
369
|
+
* @throws {Error} If path is invalid
|
|
370
|
+
*/
|
|
371
|
+
function validateRoutePath(path) {
|
|
372
|
+
if (!path || typeof path !== 'string') {
|
|
373
|
+
throw new TypeError('Route path must be a non-empty string');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (path.length > ROUTER_CONFIG.MAX_ROUTE_LENGTH) {
|
|
377
|
+
throw new Error(`Route path exceeds maximum length (${ROUTER_CONFIG.MAX_ROUTE_LENGTH} characters)`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check for invalid characters
|
|
381
|
+
if (/[<>{}[\]\\^`|]/.test(path)) {
|
|
382
|
+
throw new Error(`Route path contains invalid characters: ${path}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Validate HTTP method
|
|
388
|
+
*
|
|
389
|
+
* @param {string} method - HTTP method to validate
|
|
390
|
+
* @throws {Error} If method is invalid
|
|
391
|
+
*/
|
|
392
|
+
function validateHttpMethod(method) {
|
|
393
|
+
const validMethods = Object.values(HTTP_METHODS);
|
|
394
|
+
if (!validMethods.includes(method.toLowerCase())) {
|
|
395
|
+
throw new Error(`Invalid HTTP method: ${method}. Must be one of: ${validMethods.join(', ')}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Validate controller/action name
|
|
401
|
+
*
|
|
402
|
+
* @param {string} name - Name to validate
|
|
403
|
+
* @param {string} type - Type (controller or action)
|
|
404
|
+
* @throws {Error} If name is invalid
|
|
405
|
+
*/
|
|
406
|
+
function validateIdentifier(name, type) {
|
|
407
|
+
if (!name || typeof name !== 'string') {
|
|
408
|
+
throw new TypeError(`${type} name must be a non-empty string`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Must be valid JavaScript identifier
|
|
412
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
|
|
413
|
+
throw new Error(`Invalid ${type} name: ${name}. Must be a valid identifier.`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
253
417
|
/**
|
|
254
418
|
* Normalize route path: lowercase segments but preserve param names
|
|
255
419
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
420
|
+
* Ensures consistent route matching by:
|
|
421
|
+
* - Converting path segments to lowercase
|
|
422
|
+
* - Preserving parameter names (segments starting with :)
|
|
423
|
+
* - Removing leading/trailing slashes
|
|
424
|
+
*
|
|
425
|
+
* @param {string} path - Route path like "/Period/:periodId/Items/:itemId"
|
|
426
|
+
* @returns {string} Normalized path: "period/:periodId/items/:itemId"
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* normalizeRoutePath("/Users/:userId/Posts/:postId")
|
|
430
|
+
* // Returns: "users/:userId/posts/:postId"
|
|
258
431
|
*/
|
|
259
432
|
function normalizeRoutePath(path) {
|
|
260
433
|
const trimmed = path.replace(/^\/|\/$/g, '');
|
|
@@ -272,9 +445,22 @@ function normalizeRoutePath(path) {
|
|
|
272
445
|
return normalized.join('/');
|
|
273
446
|
}
|
|
274
447
|
|
|
448
|
+
/**
|
|
449
|
+
* MasterRouter - Route management and request routing
|
|
450
|
+
*
|
|
451
|
+
* Handles:
|
|
452
|
+
* - Route registration (manual and RESTful resources)
|
|
453
|
+
* - Path normalization and parameter extraction
|
|
454
|
+
* - Controller/action resolution and execution
|
|
455
|
+
* - Route constraints and middleware
|
|
456
|
+
* - Request-specific context isolation (no shared state)
|
|
457
|
+
*
|
|
458
|
+
* @class MasterRouter
|
|
459
|
+
*/
|
|
275
460
|
class MasterRouter {
|
|
276
461
|
currentRouteName = null
|
|
277
462
|
_routes = {}
|
|
463
|
+
_currentRoute = null // Instance property instead of global
|
|
278
464
|
|
|
279
465
|
// Lazy-load master to avoid circular dependency (Google-style lazy initialization)
|
|
280
466
|
get _master() {
|
|
@@ -284,18 +470,52 @@ class MasterRouter {
|
|
|
284
470
|
return this.__masterCache;
|
|
285
471
|
}
|
|
286
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Start route definition builder
|
|
475
|
+
*
|
|
476
|
+
* Returns an object with methods for defining routes:
|
|
477
|
+
* - route(path, toPath, type, constraint): Define a single route
|
|
478
|
+
* - resources(routeName): Define RESTful resource routes
|
|
479
|
+
*
|
|
480
|
+
* @returns {Object} Route builder with route() and resources() methods
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* const builder = masterRouter.start();
|
|
484
|
+
* builder.route("/users/:id", "users#show", "get");
|
|
485
|
+
* builder.resources("posts"); // Creates index, new, create, show, edit, update, destroy
|
|
486
|
+
*/
|
|
287
487
|
start(){
|
|
288
|
-
|
|
488
|
+
const $that = this;
|
|
289
489
|
return {
|
|
290
490
|
route : function(path, toPath, type, constraint){ // function to add to list of routes
|
|
491
|
+
// Input validation
|
|
492
|
+
validateRoutePath(path);
|
|
493
|
+
validateHttpMethod(type);
|
|
494
|
+
|
|
495
|
+
if (!toPath || typeof toPath !== 'string') {
|
|
496
|
+
throw new TypeError('Route target (toPath) must be a non-empty string');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!/^[^#]+#[^#]+$/.test(toPath)) {
|
|
500
|
+
throw new Error(`Invalid route target format: ${toPath}. Must be "controller#action"`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const pathList = toPath.replace(/^\/|\/$/g, '').split("#");
|
|
504
|
+
const controller = pathList[0].replace(/^\/|\/$/g, '');
|
|
505
|
+
const action = pathList[1];
|
|
506
|
+
|
|
507
|
+
validateIdentifier(controller, 'controller');
|
|
508
|
+
validateIdentifier(action, 'action');
|
|
291
509
|
|
|
292
|
-
|
|
510
|
+
if (constraint !== undefined && constraint !== null && typeof constraint !== 'function') {
|
|
511
|
+
throw new TypeError('Route constraint must be a function or null/undefined');
|
|
512
|
+
}
|
|
293
513
|
|
|
294
|
-
|
|
514
|
+
const route = {
|
|
295
515
|
type: type.toLowerCase(),
|
|
296
516
|
path: normalizeRoutePath(path),
|
|
297
|
-
toController
|
|
298
|
-
toAction:
|
|
517
|
+
toController: controller,
|
|
518
|
+
toAction: action,
|
|
299
519
|
constraint : constraint
|
|
300
520
|
};
|
|
301
521
|
|
|
@@ -304,10 +524,16 @@ class MasterRouter {
|
|
|
304
524
|
},
|
|
305
525
|
|
|
306
526
|
resources: function(routeName){ // function to add to list of routes using resources bulk
|
|
527
|
+
// Input validation
|
|
528
|
+
if (!routeName || typeof routeName !== 'string') {
|
|
529
|
+
throw new TypeError('Resource name must be a non-empty string');
|
|
530
|
+
}
|
|
307
531
|
|
|
532
|
+
validateIdentifier(routeName, 'resource');
|
|
533
|
+
validateRoutePath(`/${routeName}`);
|
|
308
534
|
|
|
309
535
|
$that._routes[$that.currentRouteName].routes.push({
|
|
310
|
-
type:
|
|
536
|
+
type: HTTP_METHODS.GET,
|
|
311
537
|
path: normalizeRoutePath(routeName),
|
|
312
538
|
toController :routeName,
|
|
313
539
|
toAction: "index",
|
|
@@ -315,7 +541,7 @@ class MasterRouter {
|
|
|
315
541
|
});
|
|
316
542
|
|
|
317
543
|
$that._routes[$that.currentRouteName].routes.push({
|
|
318
|
-
type:
|
|
544
|
+
type: HTTP_METHODS.GET,
|
|
319
545
|
path: normalizeRoutePath(routeName),
|
|
320
546
|
toController :routeName,
|
|
321
547
|
toAction: "new",
|
|
@@ -323,7 +549,7 @@ class MasterRouter {
|
|
|
323
549
|
});
|
|
324
550
|
|
|
325
551
|
$that._routes[$that.currentRouteName].routes.push({
|
|
326
|
-
type:
|
|
552
|
+
type: HTTP_METHODS.POST,
|
|
327
553
|
path: normalizeRoutePath(routeName),
|
|
328
554
|
toController :routeName,
|
|
329
555
|
toAction: "create",
|
|
@@ -332,7 +558,7 @@ class MasterRouter {
|
|
|
332
558
|
|
|
333
559
|
$that._routes[$that.currentRouteName].routes.push({
|
|
334
560
|
// pages/3
|
|
335
|
-
type:
|
|
561
|
+
type: HTTP_METHODS.GET,
|
|
336
562
|
path: normalizeRoutePath(routeName + "/:id"),
|
|
337
563
|
toController :routeName,
|
|
338
564
|
toAction: "show",
|
|
@@ -340,7 +566,7 @@ class MasterRouter {
|
|
|
340
566
|
});
|
|
341
567
|
|
|
342
568
|
$that._routes[$that.currentRouteName].routes.push({
|
|
343
|
-
type:
|
|
569
|
+
type: HTTP_METHODS.GET,
|
|
344
570
|
path: normalizeRoutePath(routeName + "/:id/edit"),
|
|
345
571
|
toController :routeName,
|
|
346
572
|
toAction: "edit",
|
|
@@ -348,7 +574,7 @@ class MasterRouter {
|
|
|
348
574
|
});
|
|
349
575
|
|
|
350
576
|
$that._routes[$that.currentRouteName].routes.push({
|
|
351
|
-
type:
|
|
577
|
+
type: HTTP_METHODS.PUT,
|
|
352
578
|
path: normalizeRoutePath(routeName + "/:id"),
|
|
353
579
|
toController :routeName,
|
|
354
580
|
toAction: "update",
|
|
@@ -356,7 +582,7 @@ class MasterRouter {
|
|
|
356
582
|
});
|
|
357
583
|
|
|
358
584
|
$that._routes[$that.currentRouteName].routes.push({
|
|
359
|
-
type:
|
|
585
|
+
type: HTTP_METHODS.DELETE,
|
|
360
586
|
path: normalizeRoutePath(routeName + "/:id"),
|
|
361
587
|
toController :routeName,
|
|
362
588
|
toAction: "destroy",
|
|
@@ -366,17 +592,61 @@ class MasterRouter {
|
|
|
366
592
|
}
|
|
367
593
|
}
|
|
368
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Initialize router with MIME type list
|
|
597
|
+
*
|
|
598
|
+
* @param {Object} mimeList - Object mapping file extensions to MIME types
|
|
599
|
+
* @returns {void}
|
|
600
|
+
* @deprecated Use addMimeList() instead
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* router.loadRoutes({ json: 'application/json', html: 'text/html' });
|
|
604
|
+
*/
|
|
369
605
|
loadRoutes(mimeList){
|
|
370
606
|
this.init(mimeList);
|
|
371
607
|
}
|
|
372
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Add MIME type mappings
|
|
611
|
+
*
|
|
612
|
+
* @param {Object} mimeList - Object mapping file extensions to MIME types
|
|
613
|
+
* @returns {void}
|
|
614
|
+
*
|
|
615
|
+
* @example
|
|
616
|
+
* router.addMimeList({ json: 'application/json', xml: 'application/xml' });
|
|
617
|
+
*/
|
|
373
618
|
addMimeList(mimeList){
|
|
374
619
|
this._addMimeList(mimeList);
|
|
375
620
|
}
|
|
376
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Setup a new route scope
|
|
624
|
+
*
|
|
625
|
+
* Creates a new route group with a unique ID. All routes defined via start()
|
|
626
|
+
* will be added to this scope until setup() is called again.
|
|
627
|
+
*
|
|
628
|
+
* @param {Object} route - Route scope configuration
|
|
629
|
+
* @param {string} route.root - Application root path
|
|
630
|
+
* @param {boolean} [route.isComponent=false] - Whether this is a component route
|
|
631
|
+
* @returns {void}
|
|
632
|
+
*
|
|
633
|
+
* @example
|
|
634
|
+
* router.setup({ root: '/app', isComponent: false });
|
|
635
|
+
* const builder = router.start();
|
|
636
|
+
* builder.route("/users", "users#index", "get");
|
|
637
|
+
*/
|
|
377
638
|
setup(route){
|
|
378
|
-
|
|
379
|
-
|
|
639
|
+
// Input validation
|
|
640
|
+
if (!route || typeof route !== 'object') {
|
|
641
|
+
throw new TypeError('Route configuration must be an object');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (!route.root || typeof route.root !== 'string') {
|
|
645
|
+
throw new TypeError('Route configuration must have a valid root path');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
this.currentRouteName = tools.makeWordId(ROUTER_CONFIG.ROUTE_ID_LENGTH);
|
|
649
|
+
|
|
380
650
|
if(this._routes[this.currentRouteName] === undefined){
|
|
381
651
|
this._routes[this.currentRouteName] = {
|
|
382
652
|
root : route.root,
|
|
@@ -386,21 +656,44 @@ class MasterRouter {
|
|
|
386
656
|
}
|
|
387
657
|
}
|
|
388
658
|
|
|
659
|
+
/**
|
|
660
|
+
* Get current route (deprecated - use requestObject.currentRoute instead)
|
|
661
|
+
* @deprecated Store route in requestObject.currentRoute for request isolation
|
|
662
|
+
* @returns {Object} Current route information
|
|
663
|
+
*/
|
|
389
664
|
get currentRoute(){
|
|
390
|
-
return
|
|
665
|
+
return this._currentRoute;
|
|
391
666
|
}
|
|
392
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Set current route (deprecated - use requestObject.currentRoute instead)
|
|
670
|
+
* @deprecated Store route in requestObject.currentRoute for request isolation
|
|
671
|
+
* @param {Object} data - Route data
|
|
672
|
+
*/
|
|
393
673
|
set currentRoute(data){
|
|
394
|
-
|
|
674
|
+
this._currentRoute = data;
|
|
395
675
|
}
|
|
396
676
|
|
|
397
677
|
_addMimeList(mimeObject){
|
|
398
|
-
|
|
678
|
+
const that = this;
|
|
399
679
|
if(mimeObject){
|
|
400
680
|
that.mimeTypes = mimeObject;
|
|
401
681
|
}
|
|
402
682
|
}
|
|
403
683
|
|
|
684
|
+
/**
|
|
685
|
+
* Find MIME type for file extension
|
|
686
|
+
*
|
|
687
|
+
* Performs O(1) constant-time lookup in MIME types object.
|
|
688
|
+
*
|
|
689
|
+
* @param {string} fileExt - File extension (with or without leading dot)
|
|
690
|
+
* @returns {string|boolean} MIME type string or false if not found
|
|
691
|
+
*
|
|
692
|
+
* @example
|
|
693
|
+
* router.findMimeType("json") // Returns: "application/json"
|
|
694
|
+
* router.findMimeType(".html") // Returns: "text/html"
|
|
695
|
+
* router.findMimeType("unknown") // Returns: false
|
|
696
|
+
*/
|
|
404
697
|
findMimeType(fileExt){
|
|
405
698
|
if(!fileExt){
|
|
406
699
|
return false;
|
|
@@ -417,18 +710,39 @@ class MasterRouter {
|
|
|
417
710
|
return type || false;
|
|
418
711
|
}
|
|
419
712
|
|
|
713
|
+
/**
|
|
714
|
+
* Execute controller action for matched route
|
|
715
|
+
*
|
|
716
|
+
* Internal method that:
|
|
717
|
+
* 1. Loads the controller file
|
|
718
|
+
* 2. Creates controller instance with request context
|
|
719
|
+
* 3. Executes beforeAction filters
|
|
720
|
+
* 4. Calls the action method
|
|
721
|
+
* 5. Handles errors and sends responses
|
|
722
|
+
*
|
|
723
|
+
* @private
|
|
724
|
+
* @param {Object} requestObject - Request context with route information
|
|
725
|
+
* @returns {void}
|
|
726
|
+
*
|
|
727
|
+
* @example
|
|
728
|
+
* // Called internally when route matches
|
|
729
|
+
* this._call(requestObject);
|
|
730
|
+
*/
|
|
420
731
|
_call(requestObject){
|
|
421
732
|
|
|
422
733
|
// Start performance tracking
|
|
423
734
|
const requestId = `${Date.now()}-${Math.random()}`;
|
|
424
735
|
performanceTracker.start(requestId, requestObject);
|
|
425
736
|
|
|
737
|
+
// CRITICAL FIX: Use currentRoute from requestObject (not global)
|
|
738
|
+
const currentRoute = requestObject.currentRoute;
|
|
739
|
+
|
|
426
740
|
// CRITICAL FIX: Create a request-specific context instead of using shared requestList
|
|
427
741
|
// This prevents race conditions where concurrent requests overwrite each other's services
|
|
428
742
|
const requestContext = Object.create(this._master.requestList);
|
|
429
743
|
tools.combineObjects(requestContext, requestObject);
|
|
430
744
|
requestObject = requestContext;
|
|
431
|
-
|
|
745
|
+
let Control = null;
|
|
432
746
|
|
|
433
747
|
try{
|
|
434
748
|
// Try to load controller
|
|
@@ -459,10 +773,10 @@ class MasterRouter {
|
|
|
459
773
|
Control.prototype.__currentRoute = currentRoute;
|
|
460
774
|
Control.prototype.__response = requestObject.response;
|
|
461
775
|
Control.prototype.__request = requestObject.request;
|
|
462
|
-
|
|
463
|
-
|
|
776
|
+
const control = new Control(requestObject);
|
|
777
|
+
const _callEmit = new EventEmitter();
|
|
464
778
|
|
|
465
|
-
_callEmit.on(
|
|
779
|
+
_callEmit.on(EVENT_NAMES.CONTROLLER, function(){
|
|
466
780
|
try {
|
|
467
781
|
control.next = function(){
|
|
468
782
|
control.__callAfterAction(control, requestObject);
|
|
@@ -484,6 +798,8 @@ class MasterRouter {
|
|
|
484
798
|
Promise.resolve(wrappedAction.call(control, requestObject))
|
|
485
799
|
.then(() => {
|
|
486
800
|
performanceTracker.end(requestId);
|
|
801
|
+
// MEMORY LEAK FIX: Clean up event listeners
|
|
802
|
+
_callEmit.removeAllListeners();
|
|
487
803
|
})
|
|
488
804
|
.catch((error) => {
|
|
489
805
|
const mcError = handleControllerError(
|
|
@@ -495,6 +811,8 @@ class MasterRouter {
|
|
|
495
811
|
);
|
|
496
812
|
sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
|
|
497
813
|
performanceTracker.end(requestId);
|
|
814
|
+
// MEMORY LEAK FIX: Clean up event listeners
|
|
815
|
+
_callEmit.removeAllListeners();
|
|
498
816
|
});
|
|
499
817
|
|
|
500
818
|
} catch (error) {
|
|
@@ -508,6 +826,8 @@ class MasterRouter {
|
|
|
508
826
|
);
|
|
509
827
|
sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
|
|
510
828
|
performanceTracker.end(requestId);
|
|
829
|
+
// MEMORY LEAK FIX: Clean up event listeners
|
|
830
|
+
_callEmit.removeAllListeners();
|
|
511
831
|
}
|
|
512
832
|
});
|
|
513
833
|
|
|
@@ -533,26 +853,75 @@ class MasterRouter {
|
|
|
533
853
|
|
|
534
854
|
}
|
|
535
855
|
|
|
856
|
+
/**
|
|
857
|
+
* Load and route incoming request
|
|
858
|
+
*
|
|
859
|
+
* Main entry point for request routing:
|
|
860
|
+
* 1. Creates request-specific context
|
|
861
|
+
* 2. Loads scoped services
|
|
862
|
+
* 3. Normalizes request path
|
|
863
|
+
* 4. Searches for matching route
|
|
864
|
+
* 5. Triggers controller execution or sends 404
|
|
865
|
+
*
|
|
866
|
+
* @param {Object} rr - Raw request object
|
|
867
|
+
* @param {Object} rr.request - HTTP request
|
|
868
|
+
* @param {Object} rr.response - HTTP response
|
|
869
|
+
* @param {string} rr.pathName - Request path
|
|
870
|
+
* @param {string} rr.type - HTTP method (get, post, etc.)
|
|
871
|
+
* @param {Object} [rr.params={}] - Query parameters
|
|
872
|
+
* @returns {void}
|
|
873
|
+
*
|
|
874
|
+
* @example
|
|
875
|
+
* router.load({
|
|
876
|
+
* request: req,
|
|
877
|
+
* response: res,
|
|
878
|
+
* pathName: "/users/123",
|
|
879
|
+
* type: "get",
|
|
880
|
+
* params: {}
|
|
881
|
+
* });
|
|
882
|
+
*/
|
|
536
883
|
load(rr){ // load the the router
|
|
884
|
+
// Input validation
|
|
885
|
+
if (!rr || typeof rr !== 'object') {
|
|
886
|
+
throw new TypeError('Request object must be a valid object');
|
|
887
|
+
}
|
|
537
888
|
|
|
538
|
-
|
|
539
|
-
|
|
889
|
+
if (!rr.request || typeof rr.request !== 'object') {
|
|
890
|
+
throw new TypeError('Request object must have a valid request property');
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (!rr.response || typeof rr.response !== 'object') {
|
|
894
|
+
throw new TypeError('Request object must have a valid response property');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (!rr.pathName || typeof rr.pathName !== 'string') {
|
|
898
|
+
throw new TypeError('Request object must have a valid pathName');
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (!rr.type || typeof rr.type !== 'string') {
|
|
902
|
+
throw new TypeError('Request object must have a valid type (HTTP method)');
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const $that = this;
|
|
906
|
+
const requestObject = Object.create(rr);
|
|
540
907
|
|
|
541
908
|
// CRITICAL FIX: Load scoped services into request-specific context
|
|
542
909
|
// Pass requestObject so scoped services are stored per-request, not globally
|
|
543
910
|
loadScopedListClasses.call(this, requestObject);
|
|
544
911
|
requestObject.pathName = requestObject.pathName.replace(/^\/|\/$/g, '').toLowerCase();
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
_loadEmit.on(
|
|
912
|
+
|
|
913
|
+
const _loadEmit = new EventEmitter();
|
|
914
|
+
|
|
915
|
+
_loadEmit.on(EVENT_NAMES.ROUTE_CONSTRAINT_GOOD, function(requestObj){
|
|
549
916
|
$that._call(requestObj);
|
|
917
|
+
// MEMORY LEAK FIX: Clean up event listeners after handling route
|
|
918
|
+
_loadEmit.removeAllListeners();
|
|
550
919
|
});
|
|
551
|
-
|
|
552
|
-
|
|
920
|
+
|
|
921
|
+
let routeFound = false;
|
|
553
922
|
const routes = Object.keys(this._routes);
|
|
554
923
|
for (const route of routes) {
|
|
555
|
-
|
|
924
|
+
const result = processRoutes(requestObject, _loadEmit, this._routes[route] );
|
|
556
925
|
if(result === true){
|
|
557
926
|
routeFound = true;
|
|
558
927
|
break;
|
|
@@ -570,7 +939,9 @@ class MasterRouter {
|
|
|
570
939
|
|
|
571
940
|
const mcError = handleRoutingError(requestObject.pathName, allRoutes);
|
|
572
941
|
sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
|
|
573
|
-
|
|
942
|
+
// MEMORY LEAK FIX: Clean up event listeners if route not found
|
|
943
|
+
_loadEmit.removeAllListeners();
|
|
944
|
+
}
|
|
574
945
|
|
|
575
946
|
}
|
|
576
947
|
|