mastercontroller 1.3.14 → 1.3.16
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 -38
- package/MasterCors.js +61 -19
- package/MasterPipeline.js +29 -6
- package/MasterRequest.js +579 -102
- package/MasterRouter.js +458 -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,84 @@ 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
|
+
// Controllers can have forward slashes for nested structures (e.g., "api/health")
|
|
412
|
+
// Actions must be simple identifiers
|
|
413
|
+
if (type === 'controller') {
|
|
414
|
+
// Split on slash and validate each segment
|
|
415
|
+
const segments = name.split('/');
|
|
416
|
+
for (const segment of segments) {
|
|
417
|
+
if (!segment || !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(segment)) {
|
|
418
|
+
throw new Error(`Invalid ${type} name: ${name}. Each segment must be a valid identifier.`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
// Actions must be simple identifiers (no slashes)
|
|
423
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
|
|
424
|
+
throw new Error(`Invalid ${type} name: ${name}. Must be a valid identifier.`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
253
429
|
/**
|
|
254
430
|
* Normalize route path: lowercase segments but preserve param names
|
|
255
431
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
432
|
+
* Ensures consistent route matching by:
|
|
433
|
+
* - Converting path segments to lowercase
|
|
434
|
+
* - Preserving parameter names (segments starting with :)
|
|
435
|
+
* - Removing leading/trailing slashes
|
|
436
|
+
*
|
|
437
|
+
* @param {string} path - Route path like "/Period/:periodId/Items/:itemId"
|
|
438
|
+
* @returns {string} Normalized path: "period/:periodId/items/:itemId"
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* normalizeRoutePath("/Users/:userId/Posts/:postId")
|
|
442
|
+
* // Returns: "users/:userId/posts/:postId"
|
|
258
443
|
*/
|
|
259
444
|
function normalizeRoutePath(path) {
|
|
260
445
|
const trimmed = path.replace(/^\/|\/$/g, '');
|
|
@@ -272,9 +457,22 @@ function normalizeRoutePath(path) {
|
|
|
272
457
|
return normalized.join('/');
|
|
273
458
|
}
|
|
274
459
|
|
|
460
|
+
/**
|
|
461
|
+
* MasterRouter - Route management and request routing
|
|
462
|
+
*
|
|
463
|
+
* Handles:
|
|
464
|
+
* - Route registration (manual and RESTful resources)
|
|
465
|
+
* - Path normalization and parameter extraction
|
|
466
|
+
* - Controller/action resolution and execution
|
|
467
|
+
* - Route constraints and middleware
|
|
468
|
+
* - Request-specific context isolation (no shared state)
|
|
469
|
+
*
|
|
470
|
+
* @class MasterRouter
|
|
471
|
+
*/
|
|
275
472
|
class MasterRouter {
|
|
276
473
|
currentRouteName = null
|
|
277
474
|
_routes = {}
|
|
475
|
+
_currentRoute = null // Instance property instead of global
|
|
278
476
|
|
|
279
477
|
// Lazy-load master to avoid circular dependency (Google-style lazy initialization)
|
|
280
478
|
get _master() {
|
|
@@ -284,18 +482,52 @@ class MasterRouter {
|
|
|
284
482
|
return this.__masterCache;
|
|
285
483
|
}
|
|
286
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Start route definition builder
|
|
487
|
+
*
|
|
488
|
+
* Returns an object with methods for defining routes:
|
|
489
|
+
* - route(path, toPath, type, constraint): Define a single route
|
|
490
|
+
* - resources(routeName): Define RESTful resource routes
|
|
491
|
+
*
|
|
492
|
+
* @returns {Object} Route builder with route() and resources() methods
|
|
493
|
+
*
|
|
494
|
+
* @example
|
|
495
|
+
* const builder = masterRouter.start();
|
|
496
|
+
* builder.route("/users/:id", "users#show", "get");
|
|
497
|
+
* builder.resources("posts"); // Creates index, new, create, show, edit, update, destroy
|
|
498
|
+
*/
|
|
287
499
|
start(){
|
|
288
|
-
|
|
500
|
+
const $that = this;
|
|
289
501
|
return {
|
|
290
502
|
route : function(path, toPath, type, constraint){ // function to add to list of routes
|
|
503
|
+
// Input validation
|
|
504
|
+
validateRoutePath(path);
|
|
505
|
+
validateHttpMethod(type);
|
|
291
506
|
|
|
292
|
-
|
|
507
|
+
if (!toPath || typeof toPath !== 'string') {
|
|
508
|
+
throw new TypeError('Route target (toPath) must be a non-empty string');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!/^[^#]+#[^#]+$/.test(toPath)) {
|
|
512
|
+
throw new Error(`Invalid route target format: ${toPath}. Must be "controller#action"`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const pathList = toPath.replace(/^\/|\/$/g, '').split("#");
|
|
516
|
+
const controller = pathList[0].replace(/^\/|\/$/g, '');
|
|
517
|
+
const action = pathList[1];
|
|
518
|
+
|
|
519
|
+
validateIdentifier(controller, 'controller');
|
|
520
|
+
validateIdentifier(action, 'action');
|
|
293
521
|
|
|
294
|
-
|
|
522
|
+
if (constraint !== undefined && constraint !== null && typeof constraint !== 'function') {
|
|
523
|
+
throw new TypeError('Route constraint must be a function or null/undefined');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const route = {
|
|
295
527
|
type: type.toLowerCase(),
|
|
296
528
|
path: normalizeRoutePath(path),
|
|
297
|
-
toController
|
|
298
|
-
toAction:
|
|
529
|
+
toController: controller,
|
|
530
|
+
toAction: action,
|
|
299
531
|
constraint : constraint
|
|
300
532
|
};
|
|
301
533
|
|
|
@@ -304,10 +536,16 @@ class MasterRouter {
|
|
|
304
536
|
},
|
|
305
537
|
|
|
306
538
|
resources: function(routeName){ // function to add to list of routes using resources bulk
|
|
539
|
+
// Input validation
|
|
540
|
+
if (!routeName || typeof routeName !== 'string') {
|
|
541
|
+
throw new TypeError('Resource name must be a non-empty string');
|
|
542
|
+
}
|
|
307
543
|
|
|
544
|
+
validateIdentifier(routeName, 'resource');
|
|
545
|
+
validateRoutePath(`/${routeName}`);
|
|
308
546
|
|
|
309
547
|
$that._routes[$that.currentRouteName].routes.push({
|
|
310
|
-
type:
|
|
548
|
+
type: HTTP_METHODS.GET,
|
|
311
549
|
path: normalizeRoutePath(routeName),
|
|
312
550
|
toController :routeName,
|
|
313
551
|
toAction: "index",
|
|
@@ -315,7 +553,7 @@ class MasterRouter {
|
|
|
315
553
|
});
|
|
316
554
|
|
|
317
555
|
$that._routes[$that.currentRouteName].routes.push({
|
|
318
|
-
type:
|
|
556
|
+
type: HTTP_METHODS.GET,
|
|
319
557
|
path: normalizeRoutePath(routeName),
|
|
320
558
|
toController :routeName,
|
|
321
559
|
toAction: "new",
|
|
@@ -323,7 +561,7 @@ class MasterRouter {
|
|
|
323
561
|
});
|
|
324
562
|
|
|
325
563
|
$that._routes[$that.currentRouteName].routes.push({
|
|
326
|
-
type:
|
|
564
|
+
type: HTTP_METHODS.POST,
|
|
327
565
|
path: normalizeRoutePath(routeName),
|
|
328
566
|
toController :routeName,
|
|
329
567
|
toAction: "create",
|
|
@@ -332,7 +570,7 @@ class MasterRouter {
|
|
|
332
570
|
|
|
333
571
|
$that._routes[$that.currentRouteName].routes.push({
|
|
334
572
|
// pages/3
|
|
335
|
-
type:
|
|
573
|
+
type: HTTP_METHODS.GET,
|
|
336
574
|
path: normalizeRoutePath(routeName + "/:id"),
|
|
337
575
|
toController :routeName,
|
|
338
576
|
toAction: "show",
|
|
@@ -340,7 +578,7 @@ class MasterRouter {
|
|
|
340
578
|
});
|
|
341
579
|
|
|
342
580
|
$that._routes[$that.currentRouteName].routes.push({
|
|
343
|
-
type:
|
|
581
|
+
type: HTTP_METHODS.GET,
|
|
344
582
|
path: normalizeRoutePath(routeName + "/:id/edit"),
|
|
345
583
|
toController :routeName,
|
|
346
584
|
toAction: "edit",
|
|
@@ -348,7 +586,7 @@ class MasterRouter {
|
|
|
348
586
|
});
|
|
349
587
|
|
|
350
588
|
$that._routes[$that.currentRouteName].routes.push({
|
|
351
|
-
type:
|
|
589
|
+
type: HTTP_METHODS.PUT,
|
|
352
590
|
path: normalizeRoutePath(routeName + "/:id"),
|
|
353
591
|
toController :routeName,
|
|
354
592
|
toAction: "update",
|
|
@@ -356,7 +594,7 @@ class MasterRouter {
|
|
|
356
594
|
});
|
|
357
595
|
|
|
358
596
|
$that._routes[$that.currentRouteName].routes.push({
|
|
359
|
-
type:
|
|
597
|
+
type: HTTP_METHODS.DELETE,
|
|
360
598
|
path: normalizeRoutePath(routeName + "/:id"),
|
|
361
599
|
toController :routeName,
|
|
362
600
|
toAction: "destroy",
|
|
@@ -366,17 +604,61 @@ class MasterRouter {
|
|
|
366
604
|
}
|
|
367
605
|
}
|
|
368
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Initialize router with MIME type list
|
|
609
|
+
*
|
|
610
|
+
* @param {Object} mimeList - Object mapping file extensions to MIME types
|
|
611
|
+
* @returns {void}
|
|
612
|
+
* @deprecated Use addMimeList() instead
|
|
613
|
+
*
|
|
614
|
+
* @example
|
|
615
|
+
* router.loadRoutes({ json: 'application/json', html: 'text/html' });
|
|
616
|
+
*/
|
|
369
617
|
loadRoutes(mimeList){
|
|
370
618
|
this.init(mimeList);
|
|
371
619
|
}
|
|
372
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Add MIME type mappings
|
|
623
|
+
*
|
|
624
|
+
* @param {Object} mimeList - Object mapping file extensions to MIME types
|
|
625
|
+
* @returns {void}
|
|
626
|
+
*
|
|
627
|
+
* @example
|
|
628
|
+
* router.addMimeList({ json: 'application/json', xml: 'application/xml' });
|
|
629
|
+
*/
|
|
373
630
|
addMimeList(mimeList){
|
|
374
631
|
this._addMimeList(mimeList);
|
|
375
632
|
}
|
|
376
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Setup a new route scope
|
|
636
|
+
*
|
|
637
|
+
* Creates a new route group with a unique ID. All routes defined via start()
|
|
638
|
+
* will be added to this scope until setup() is called again.
|
|
639
|
+
*
|
|
640
|
+
* @param {Object} route - Route scope configuration
|
|
641
|
+
* @param {string} route.root - Application root path
|
|
642
|
+
* @param {boolean} [route.isComponent=false] - Whether this is a component route
|
|
643
|
+
* @returns {void}
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* router.setup({ root: '/app', isComponent: false });
|
|
647
|
+
* const builder = router.start();
|
|
648
|
+
* builder.route("/users", "users#index", "get");
|
|
649
|
+
*/
|
|
377
650
|
setup(route){
|
|
378
|
-
|
|
379
|
-
|
|
651
|
+
// Input validation
|
|
652
|
+
if (!route || typeof route !== 'object') {
|
|
653
|
+
throw new TypeError('Route configuration must be an object');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (!route.root || typeof route.root !== 'string') {
|
|
657
|
+
throw new TypeError('Route configuration must have a valid root path');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
this.currentRouteName = tools.makeWordId(ROUTER_CONFIG.ROUTE_ID_LENGTH);
|
|
661
|
+
|
|
380
662
|
if(this._routes[this.currentRouteName] === undefined){
|
|
381
663
|
this._routes[this.currentRouteName] = {
|
|
382
664
|
root : route.root,
|
|
@@ -386,21 +668,44 @@ class MasterRouter {
|
|
|
386
668
|
}
|
|
387
669
|
}
|
|
388
670
|
|
|
671
|
+
/**
|
|
672
|
+
* Get current route (deprecated - use requestObject.currentRoute instead)
|
|
673
|
+
* @deprecated Store route in requestObject.currentRoute for request isolation
|
|
674
|
+
* @returns {Object} Current route information
|
|
675
|
+
*/
|
|
389
676
|
get currentRoute(){
|
|
390
|
-
return
|
|
677
|
+
return this._currentRoute;
|
|
391
678
|
}
|
|
392
679
|
|
|
680
|
+
/**
|
|
681
|
+
* Set current route (deprecated - use requestObject.currentRoute instead)
|
|
682
|
+
* @deprecated Store route in requestObject.currentRoute for request isolation
|
|
683
|
+
* @param {Object} data - Route data
|
|
684
|
+
*/
|
|
393
685
|
set currentRoute(data){
|
|
394
|
-
|
|
686
|
+
this._currentRoute = data;
|
|
395
687
|
}
|
|
396
688
|
|
|
397
689
|
_addMimeList(mimeObject){
|
|
398
|
-
|
|
690
|
+
const that = this;
|
|
399
691
|
if(mimeObject){
|
|
400
692
|
that.mimeTypes = mimeObject;
|
|
401
693
|
}
|
|
402
694
|
}
|
|
403
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Find MIME type for file extension
|
|
698
|
+
*
|
|
699
|
+
* Performs O(1) constant-time lookup in MIME types object.
|
|
700
|
+
*
|
|
701
|
+
* @param {string} fileExt - File extension (with or without leading dot)
|
|
702
|
+
* @returns {string|boolean} MIME type string or false if not found
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* router.findMimeType("json") // Returns: "application/json"
|
|
706
|
+
* router.findMimeType(".html") // Returns: "text/html"
|
|
707
|
+
* router.findMimeType("unknown") // Returns: false
|
|
708
|
+
*/
|
|
404
709
|
findMimeType(fileExt){
|
|
405
710
|
if(!fileExt){
|
|
406
711
|
return false;
|
|
@@ -417,18 +722,39 @@ class MasterRouter {
|
|
|
417
722
|
return type || false;
|
|
418
723
|
}
|
|
419
724
|
|
|
725
|
+
/**
|
|
726
|
+
* Execute controller action for matched route
|
|
727
|
+
*
|
|
728
|
+
* Internal method that:
|
|
729
|
+
* 1. Loads the controller file
|
|
730
|
+
* 2. Creates controller instance with request context
|
|
731
|
+
* 3. Executes beforeAction filters
|
|
732
|
+
* 4. Calls the action method
|
|
733
|
+
* 5. Handles errors and sends responses
|
|
734
|
+
*
|
|
735
|
+
* @private
|
|
736
|
+
* @param {Object} requestObject - Request context with route information
|
|
737
|
+
* @returns {void}
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* // Called internally when route matches
|
|
741
|
+
* this._call(requestObject);
|
|
742
|
+
*/
|
|
420
743
|
_call(requestObject){
|
|
421
744
|
|
|
422
745
|
// Start performance tracking
|
|
423
746
|
const requestId = `${Date.now()}-${Math.random()}`;
|
|
424
747
|
performanceTracker.start(requestId, requestObject);
|
|
425
748
|
|
|
749
|
+
// CRITICAL FIX: Use currentRoute from requestObject (not global)
|
|
750
|
+
const currentRoute = requestObject.currentRoute;
|
|
751
|
+
|
|
426
752
|
// CRITICAL FIX: Create a request-specific context instead of using shared requestList
|
|
427
753
|
// This prevents race conditions where concurrent requests overwrite each other's services
|
|
428
754
|
const requestContext = Object.create(this._master.requestList);
|
|
429
755
|
tools.combineObjects(requestContext, requestObject);
|
|
430
756
|
requestObject = requestContext;
|
|
431
|
-
|
|
757
|
+
let Control = null;
|
|
432
758
|
|
|
433
759
|
try{
|
|
434
760
|
// Try to load controller
|
|
@@ -459,10 +785,10 @@ class MasterRouter {
|
|
|
459
785
|
Control.prototype.__currentRoute = currentRoute;
|
|
460
786
|
Control.prototype.__response = requestObject.response;
|
|
461
787
|
Control.prototype.__request = requestObject.request;
|
|
462
|
-
|
|
463
|
-
|
|
788
|
+
const control = new Control(requestObject);
|
|
789
|
+
const _callEmit = new EventEmitter();
|
|
464
790
|
|
|
465
|
-
_callEmit.on(
|
|
791
|
+
_callEmit.on(EVENT_NAMES.CONTROLLER, function(){
|
|
466
792
|
try {
|
|
467
793
|
control.next = function(){
|
|
468
794
|
control.__callAfterAction(control, requestObject);
|
|
@@ -484,6 +810,8 @@ class MasterRouter {
|
|
|
484
810
|
Promise.resolve(wrappedAction.call(control, requestObject))
|
|
485
811
|
.then(() => {
|
|
486
812
|
performanceTracker.end(requestId);
|
|
813
|
+
// MEMORY LEAK FIX: Clean up event listeners
|
|
814
|
+
_callEmit.removeAllListeners();
|
|
487
815
|
})
|
|
488
816
|
.catch((error) => {
|
|
489
817
|
const mcError = handleControllerError(
|
|
@@ -495,6 +823,8 @@ class MasterRouter {
|
|
|
495
823
|
);
|
|
496
824
|
sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
|
|
497
825
|
performanceTracker.end(requestId);
|
|
826
|
+
// MEMORY LEAK FIX: Clean up event listeners
|
|
827
|
+
_callEmit.removeAllListeners();
|
|
498
828
|
});
|
|
499
829
|
|
|
500
830
|
} catch (error) {
|
|
@@ -508,6 +838,8 @@ class MasterRouter {
|
|
|
508
838
|
);
|
|
509
839
|
sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
|
|
510
840
|
performanceTracker.end(requestId);
|
|
841
|
+
// MEMORY LEAK FIX: Clean up event listeners
|
|
842
|
+
_callEmit.removeAllListeners();
|
|
511
843
|
}
|
|
512
844
|
});
|
|
513
845
|
|
|
@@ -533,26 +865,75 @@ class MasterRouter {
|
|
|
533
865
|
|
|
534
866
|
}
|
|
535
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Load and route incoming request
|
|
870
|
+
*
|
|
871
|
+
* Main entry point for request routing:
|
|
872
|
+
* 1. Creates request-specific context
|
|
873
|
+
* 2. Loads scoped services
|
|
874
|
+
* 3. Normalizes request path
|
|
875
|
+
* 4. Searches for matching route
|
|
876
|
+
* 5. Triggers controller execution or sends 404
|
|
877
|
+
*
|
|
878
|
+
* @param {Object} rr - Raw request object
|
|
879
|
+
* @param {Object} rr.request - HTTP request
|
|
880
|
+
* @param {Object} rr.response - HTTP response
|
|
881
|
+
* @param {string} rr.pathName - Request path
|
|
882
|
+
* @param {string} rr.type - HTTP method (get, post, etc.)
|
|
883
|
+
* @param {Object} [rr.params={}] - Query parameters
|
|
884
|
+
* @returns {void}
|
|
885
|
+
*
|
|
886
|
+
* @example
|
|
887
|
+
* router.load({
|
|
888
|
+
* request: req,
|
|
889
|
+
* response: res,
|
|
890
|
+
* pathName: "/users/123",
|
|
891
|
+
* type: "get",
|
|
892
|
+
* params: {}
|
|
893
|
+
* });
|
|
894
|
+
*/
|
|
536
895
|
load(rr){ // load the the router
|
|
896
|
+
// Input validation
|
|
897
|
+
if (!rr || typeof rr !== 'object') {
|
|
898
|
+
throw new TypeError('Request object must be a valid object');
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (!rr.request || typeof rr.request !== 'object') {
|
|
902
|
+
throw new TypeError('Request object must have a valid request property');
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (!rr.response || typeof rr.response !== 'object') {
|
|
906
|
+
throw new TypeError('Request object must have a valid response property');
|
|
907
|
+
}
|
|
537
908
|
|
|
538
|
-
|
|
539
|
-
|
|
909
|
+
if (!rr.pathName || typeof rr.pathName !== 'string') {
|
|
910
|
+
throw new TypeError('Request object must have a valid pathName');
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (!rr.type || typeof rr.type !== 'string') {
|
|
914
|
+
throw new TypeError('Request object must have a valid type (HTTP method)');
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const $that = this;
|
|
918
|
+
const requestObject = Object.create(rr);
|
|
540
919
|
|
|
541
920
|
// CRITICAL FIX: Load scoped services into request-specific context
|
|
542
921
|
// Pass requestObject so scoped services are stored per-request, not globally
|
|
543
922
|
loadScopedListClasses.call(this, requestObject);
|
|
544
923
|
requestObject.pathName = requestObject.pathName.replace(/^\/|\/$/g, '').toLowerCase();
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
_loadEmit.on(
|
|
924
|
+
|
|
925
|
+
const _loadEmit = new EventEmitter();
|
|
926
|
+
|
|
927
|
+
_loadEmit.on(EVENT_NAMES.ROUTE_CONSTRAINT_GOOD, function(requestObj){
|
|
549
928
|
$that._call(requestObj);
|
|
929
|
+
// MEMORY LEAK FIX: Clean up event listeners after handling route
|
|
930
|
+
_loadEmit.removeAllListeners();
|
|
550
931
|
});
|
|
551
|
-
|
|
552
|
-
|
|
932
|
+
|
|
933
|
+
let routeFound = false;
|
|
553
934
|
const routes = Object.keys(this._routes);
|
|
554
935
|
for (const route of routes) {
|
|
555
|
-
|
|
936
|
+
const result = processRoutes(requestObject, _loadEmit, this._routes[route] );
|
|
556
937
|
if(result === true){
|
|
557
938
|
routeFound = true;
|
|
558
939
|
break;
|
|
@@ -570,7 +951,9 @@ class MasterRouter {
|
|
|
570
951
|
|
|
571
952
|
const mcError = handleRoutingError(requestObject.pathName, allRoutes);
|
|
572
953
|
sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
|
|
573
|
-
|
|
954
|
+
// MEMORY LEAK FIX: Clean up event listeners if route not found
|
|
955
|
+
_loadEmit.removeAllListeners();
|
|
956
|
+
}
|
|
574
957
|
|
|
575
958
|
}
|
|
576
959
|
|