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/MasterActionFilters.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
// version 2.0 - FIXED: Instance-level filters, async support, multiple filters
|
|
2
2
|
const { logger } = require('./error/MasterErrorLogger');
|
|
3
3
|
|
|
4
|
+
// HTTP Status Code Constants
|
|
5
|
+
const HTTP_STATUS = {
|
|
6
|
+
OK: 200,
|
|
7
|
+
BAD_REQUEST: 400,
|
|
8
|
+
FORBIDDEN: 403,
|
|
9
|
+
INTERNAL_ERROR: 500
|
|
10
|
+
};
|
|
11
|
+
|
|
4
12
|
class MasterActionFilters {
|
|
5
13
|
|
|
14
|
+
// Default filter timeout (5 seconds)
|
|
15
|
+
static DEFAULT_FILTER_TIMEOUT = 5000;
|
|
16
|
+
|
|
6
17
|
// Lazy-load master to avoid circular dependency (Google-style Singleton pattern)
|
|
7
18
|
static get _master() {
|
|
8
19
|
if (!MasterActionFilters.__masterCache) {
|
|
@@ -16,83 +27,256 @@ class MasterActionFilters {
|
|
|
16
27
|
// Each controller gets its own filter arrays
|
|
17
28
|
this._beforeActionFilters = [];
|
|
18
29
|
this._afterActionFilters = [];
|
|
30
|
+
|
|
31
|
+
// Configurable timeout per controller instance
|
|
32
|
+
this._filterTimeout = MasterActionFilters.DEFAULT_FILTER_TIMEOUT;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Normalize action name by removing whitespace
|
|
37
|
+
* @private
|
|
38
|
+
* @param {string} action - Action name
|
|
39
|
+
* @returns {string} Normalized action name
|
|
40
|
+
*/
|
|
41
|
+
_normalizeAction(action) {
|
|
42
|
+
return action.replace(/\s/g, '');
|
|
19
43
|
}
|
|
20
44
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Find matching filters for an action
|
|
47
|
+
* @private
|
|
48
|
+
* @param {Array} filters - Filter array to search
|
|
49
|
+
* @param {Object} obj - Controller instance
|
|
50
|
+
* @param {Object} request - Request object
|
|
51
|
+
* @returns {Array} Matching filters
|
|
52
|
+
*/
|
|
53
|
+
_findMatchingFilters(filters, obj, request) {
|
|
54
|
+
if (!filters || filters.length === 0) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const requestAction = this._normalizeAction(request.toAction);
|
|
59
|
+
|
|
60
|
+
return filters.filter(filter => {
|
|
61
|
+
// Skip disabled filters
|
|
62
|
+
if (filter.enabled === false) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check namespace matches
|
|
67
|
+
if (filter.namespace !== obj.__namespace) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if any action in filter's actionList matches
|
|
72
|
+
return filter.actionList.some(actionName => {
|
|
73
|
+
const normalizedAction = this._normalizeAction(actionName);
|
|
74
|
+
return normalizedAction === requestAction;
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Send standardized error response for filter failures
|
|
81
|
+
* @private
|
|
82
|
+
* @param {Object} request - Request object
|
|
83
|
+
* @param {number} statusCode - HTTP status code
|
|
84
|
+
* @param {string} errorCode - Error code for client
|
|
85
|
+
* @returns {void}
|
|
86
|
+
*/
|
|
87
|
+
_sendFilterErrorResponse(request, statusCode, errorCode) {
|
|
88
|
+
const res = request.response;
|
|
89
|
+
if (res && !res._headerSent && !res.headersSent) {
|
|
90
|
+
const errorResponse = {
|
|
91
|
+
error: true,
|
|
92
|
+
statusCode,
|
|
93
|
+
code: errorCode,
|
|
94
|
+
message: 'Filter execution failed',
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
path: request.pathName,
|
|
97
|
+
method: request.request?.method
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
101
|
+
res.end(JSON.stringify(errorResponse));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate filter registration parameters
|
|
107
|
+
* @private
|
|
108
|
+
* @param {string|Array<string>} actionlist - Action names to filter
|
|
109
|
+
* @param {Function} func - Filter callback function
|
|
110
|
+
* @param {string} filterType - 'before' or 'after' for logging
|
|
111
|
+
* @returns {boolean} True if valid, false otherwise
|
|
112
|
+
*/
|
|
113
|
+
_validateFilterParams(actionlist, func, filterType) {
|
|
114
|
+
// Validate callback function
|
|
24
115
|
if (typeof func !== 'function') {
|
|
25
|
-
|
|
116
|
+
logger.warn({
|
|
117
|
+
code: 'MC_FILTER_INVALID_CALLBACK',
|
|
118
|
+
message: `${filterType}Action callback is not a function`,
|
|
119
|
+
namespace: this.__namespace
|
|
120
|
+
});
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate actionlist exists
|
|
125
|
+
if (!actionlist || (Array.isArray(actionlist) && actionlist.length === 0)) {
|
|
126
|
+
logger.warn({
|
|
127
|
+
code: 'MC_FILTER_EMPTY_ACTIONLIST',
|
|
128
|
+
message: `${filterType}Action actionlist is empty or undefined`,
|
|
129
|
+
namespace: this.__namespace
|
|
130
|
+
});
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Validate actionlist contains valid strings
|
|
135
|
+
const actions = Array.isArray(actionlist) ? actionlist : [actionlist];
|
|
136
|
+
for (const action of actions) {
|
|
137
|
+
if (typeof action !== 'string' || action.trim() === '') {
|
|
138
|
+
logger.warn({
|
|
139
|
+
code: 'MC_FILTER_INVALID_ACTION',
|
|
140
|
+
message: `${filterType}Action actionlist contains invalid action name`,
|
|
141
|
+
namespace: this.__namespace,
|
|
142
|
+
action: action
|
|
143
|
+
});
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Validate namespace exists
|
|
149
|
+
if (!this.__namespace) {
|
|
150
|
+
logger.warn({
|
|
151
|
+
code: 'MC_FILTER_MISSING_NAMESPACE',
|
|
152
|
+
message: `${filterType}Action called but controller has no __namespace`,
|
|
153
|
+
});
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Register a before action filter
|
|
162
|
+
* Filters execute before the specified actions in priority order (higher priority first)
|
|
163
|
+
* @param {string|Array<string>} actionlist - Action name(s) to filter
|
|
164
|
+
* @param {Function} func - Filter callback function(request)
|
|
165
|
+
* @param {Object} [options={}] - Filter options
|
|
166
|
+
* @param {number} [options.priority=0] - Filter priority (higher executes first)
|
|
167
|
+
* @param {string} [options.name] - Filter name for debugging
|
|
168
|
+
* @returns {void}
|
|
169
|
+
* @example
|
|
170
|
+
* this.beforeAction('index', function(request) {
|
|
171
|
+
* if (!this.isAuthenticated()) {
|
|
172
|
+
* this.redirectTo('/login');
|
|
173
|
+
* return;
|
|
174
|
+
* }
|
|
175
|
+
* this.next(); // Continue to action
|
|
176
|
+
* });
|
|
177
|
+
* @example
|
|
178
|
+
* // With priority (higher priority runs first)
|
|
179
|
+
* this.beforeAction(['create', 'update'], function(request) {
|
|
180
|
+
* this.loadUser();
|
|
181
|
+
* this.next();
|
|
182
|
+
* }, { priority: 10, name: 'loadUser' });
|
|
183
|
+
*/
|
|
184
|
+
beforeAction(actionlist, func, options = {}){
|
|
185
|
+
if (!this._validateFilterParams(actionlist, func, 'before')) {
|
|
26
186
|
return;
|
|
27
187
|
}
|
|
28
188
|
|
|
29
|
-
// FIXED: Push to array
|
|
189
|
+
// FIXED: Push to array with metadata and priority
|
|
30
190
|
this._beforeActionFilters.push({
|
|
31
191
|
namespace: this.__namespace,
|
|
32
192
|
actionList: Array.isArray(actionlist) ? actionlist : [actionlist],
|
|
33
193
|
callBack: func,
|
|
34
|
-
that: this
|
|
194
|
+
that: this,
|
|
195
|
+
priority: options.priority || 0,
|
|
196
|
+
name: options.name || func.name || 'anonymous',
|
|
197
|
+
enabled: true,
|
|
198
|
+
registeredAt: new Date().toISOString()
|
|
35
199
|
});
|
|
200
|
+
|
|
201
|
+
// Sort filters by priority (higher priority first)
|
|
202
|
+
this._beforeActionFilters.sort((a, b) => b.priority - a.priority);
|
|
36
203
|
}
|
|
37
204
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Register an after action filter
|
|
207
|
+
* Filters execute after the specified actions complete in priority order
|
|
208
|
+
* @param {string|Array<string>} actionlist - Action name(s) to filter
|
|
209
|
+
* @param {Function} func - Filter callback function(request)
|
|
210
|
+
* @param {Object} [options={}] - Filter options
|
|
211
|
+
* @param {number} [options.priority=0] - Filter priority (higher executes first)
|
|
212
|
+
* @param {string} [options.name] - Filter name for debugging
|
|
213
|
+
* @returns {void}
|
|
214
|
+
* @example
|
|
215
|
+
* this.afterAction('index', function(request) {
|
|
216
|
+
* logger.info({ action: 'index', userId: this.currentUser.id });
|
|
217
|
+
* });
|
|
218
|
+
* @example
|
|
219
|
+
* // With priority
|
|
220
|
+
* this.afterAction(['create', 'update'], function(request) {
|
|
221
|
+
* this.clearCache();
|
|
222
|
+
* }, { priority: 5, name: 'clearCache' });
|
|
223
|
+
*/
|
|
224
|
+
afterAction(actionlist, func, options = {}){
|
|
225
|
+
if (!this._validateFilterParams(actionlist, func, 'after')) {
|
|
43
226
|
return;
|
|
44
227
|
}
|
|
45
228
|
|
|
46
|
-
// FIXED: Push to array
|
|
229
|
+
// FIXED: Push to array with metadata and priority
|
|
47
230
|
this._afterActionFilters.push({
|
|
48
231
|
namespace: this.__namespace,
|
|
49
232
|
actionList: Array.isArray(actionlist) ? actionlist : [actionlist],
|
|
50
233
|
callBack: func,
|
|
51
|
-
that: this
|
|
234
|
+
that: this,
|
|
235
|
+
priority: options.priority || 0,
|
|
236
|
+
name: options.name || func.name || 'anonymous',
|
|
237
|
+
enabled: true,
|
|
238
|
+
registeredAt: new Date().toISOString()
|
|
52
239
|
});
|
|
240
|
+
|
|
241
|
+
// Sort filters by priority (higher priority first)
|
|
242
|
+
this._afterActionFilters.sort((a, b) => b.priority - a.priority);
|
|
53
243
|
}
|
|
54
244
|
|
|
55
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Check if controller has before action filters for this action
|
|
247
|
+
* @private
|
|
248
|
+
* @param {Object} obj - Controller instance
|
|
249
|
+
* @param {Object} request - Request object with toAction property
|
|
250
|
+
* @returns {boolean} True if matching filters exist
|
|
251
|
+
*/
|
|
56
252
|
__hasBeforeAction(obj, request){
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return this._beforeActionFilters.some(filter => {
|
|
62
|
-
if (filter.namespace !== obj.__namespace) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const requestAction = request.toAction.replace(/\s/g, '');
|
|
67
|
-
return filter.actionList.some(action => {
|
|
68
|
-
const filterAction = action.replace(/\s/g, '');
|
|
69
|
-
return filterAction === requestAction;
|
|
70
|
-
});
|
|
71
|
-
});
|
|
253
|
+
const matchingFilters = this._findMatchingFilters(this._beforeActionFilters, obj, request);
|
|
254
|
+
return matchingFilters.length > 0;
|
|
72
255
|
}
|
|
73
256
|
|
|
74
|
-
|
|
75
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Execute all matching before action filters
|
|
259
|
+
* @private
|
|
260
|
+
* @async
|
|
261
|
+
* @param {Object} obj - Controller instance
|
|
262
|
+
* @param {Object} request - Request object
|
|
263
|
+
* @param {EventEmitter} emitter - Event emitter for next() calls
|
|
264
|
+
* @returns {Promise<void>}
|
|
265
|
+
* @throws {Error} If any filter fails
|
|
266
|
+
*/
|
|
76
267
|
async __callBeforeAction(obj, request, emitter) {
|
|
77
|
-
|
|
268
|
+
// Find all matching filters using optimized helper
|
|
269
|
+
const matchingFilters = this._findMatchingFilters(this._beforeActionFilters, obj, request);
|
|
270
|
+
|
|
271
|
+
if (matchingFilters.length === 0) {
|
|
78
272
|
return;
|
|
79
273
|
}
|
|
80
274
|
|
|
81
|
-
|
|
82
|
-
const requestAction = request.toAction.replace(/\s/g, '');
|
|
83
|
-
const matchingFilters = this._beforeActionFilters.filter(filter => {
|
|
84
|
-
if (filter.namespace !== obj.__namespace) {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return filter.actionList.some(actionName => {
|
|
89
|
-
const normalizedAction = actionName.replace(/\s/g, '');
|
|
90
|
-
return normalizedAction === requestAction;
|
|
91
|
-
});
|
|
92
|
-
});
|
|
275
|
+
const requestAction = this._normalizeAction(request.toAction);
|
|
93
276
|
|
|
94
277
|
// Execute all matching filters in order
|
|
95
278
|
for (const filter of matchingFilters) {
|
|
279
|
+
const startTime = Date.now();
|
|
96
280
|
try {
|
|
97
281
|
// FIXED: Add timeout protection (5 seconds default)
|
|
98
282
|
await this._executeWithTimeout(
|
|
@@ -100,28 +284,36 @@ class MasterActionFilters {
|
|
|
100
284
|
filter.that,
|
|
101
285
|
[request],
|
|
102
286
|
emitter,
|
|
103
|
-
|
|
287
|
+
this._filterTimeout
|
|
104
288
|
);
|
|
289
|
+
|
|
290
|
+
// Record successful filter execution timing
|
|
291
|
+
const duration = Date.now() - startTime;
|
|
292
|
+
logger.info({
|
|
293
|
+
code: 'MC_FILTER_EXECUTED',
|
|
294
|
+
message: 'Before action filter executed successfully',
|
|
295
|
+
namespace: filter.namespace,
|
|
296
|
+
action: requestAction,
|
|
297
|
+
duration,
|
|
298
|
+
filterType: 'before'
|
|
299
|
+
});
|
|
105
300
|
} catch (error) {
|
|
106
|
-
|
|
301
|
+
const duration = Date.now() - startTime;
|
|
302
|
+
|
|
303
|
+
// FIXED: Proper error handling with metrics
|
|
107
304
|
logger.error({
|
|
108
305
|
code: 'MC_FILTER_ERROR',
|
|
109
306
|
message: 'Error in beforeAction filter',
|
|
110
307
|
namespace: filter.namespace,
|
|
111
308
|
action: requestAction,
|
|
309
|
+
duration,
|
|
310
|
+
filterType: 'before',
|
|
112
311
|
error: error.message,
|
|
113
312
|
stack: error.stack
|
|
114
313
|
});
|
|
115
314
|
|
|
116
|
-
// Send error response
|
|
117
|
-
|
|
118
|
-
if (res && !res._headerSent && !res.headersSent) {
|
|
119
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
120
|
-
res.end(JSON.stringify({
|
|
121
|
-
error: 'Internal Server Error',
|
|
122
|
-
message: MasterActionFilters._master.environmentType === 'development' ? error.message : 'Filter execution failed'
|
|
123
|
-
}));
|
|
124
|
-
}
|
|
315
|
+
// Send standardized error response (no sensitive info leaked)
|
|
316
|
+
this._sendFilterErrorResponse(request, HTTP_STATUS.INTERNAL_ERROR, 'FILTER_ERROR');
|
|
125
317
|
|
|
126
318
|
// Don't continue to other filters if one fails
|
|
127
319
|
throw error;
|
|
@@ -129,28 +321,27 @@ class MasterActionFilters {
|
|
|
129
321
|
}
|
|
130
322
|
}
|
|
131
323
|
|
|
132
|
-
|
|
133
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Execute all matching after action filters
|
|
326
|
+
* @private
|
|
327
|
+
* @async
|
|
328
|
+
* @param {Object} obj - Controller instance
|
|
329
|
+
* @param {Object} request - Request object
|
|
330
|
+
* @returns {Promise<void>}
|
|
331
|
+
*/
|
|
134
332
|
async __callAfterAction(obj, request) {
|
|
135
|
-
|
|
333
|
+
// Find all matching filters using optimized helper
|
|
334
|
+
const matchingFilters = this._findMatchingFilters(this._afterActionFilters, obj, request);
|
|
335
|
+
|
|
336
|
+
if (matchingFilters.length === 0) {
|
|
136
337
|
return;
|
|
137
338
|
}
|
|
138
339
|
|
|
139
|
-
|
|
140
|
-
const requestAction = request.toAction.replace(/\s/g, '');
|
|
141
|
-
const matchingFilters = this._afterActionFilters.filter(filter => {
|
|
142
|
-
if (filter.namespace !== obj.__namespace) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return filter.actionList.some(actionName => {
|
|
147
|
-
const normalizedAction = actionName.replace(/\s/g, '');
|
|
148
|
-
return normalizedAction === requestAction;
|
|
149
|
-
});
|
|
150
|
-
});
|
|
340
|
+
const requestAction = this._normalizeAction(request.toAction);
|
|
151
341
|
|
|
152
342
|
// Execute all matching filters in order
|
|
153
343
|
for (const filter of matchingFilters) {
|
|
344
|
+
const startTime = Date.now();
|
|
154
345
|
try {
|
|
155
346
|
// FIXED: Add timeout protection (5 seconds default)
|
|
156
347
|
await this._executeWithTimeout(
|
|
@@ -158,15 +349,30 @@ class MasterActionFilters {
|
|
|
158
349
|
filter.that,
|
|
159
350
|
[request],
|
|
160
351
|
null,
|
|
161
|
-
|
|
352
|
+
this._filterTimeout
|
|
162
353
|
);
|
|
354
|
+
|
|
355
|
+
// Record successful filter execution timing
|
|
356
|
+
const duration = Date.now() - startTime;
|
|
357
|
+
logger.info({
|
|
358
|
+
code: 'MC_FILTER_EXECUTED',
|
|
359
|
+
message: 'After action filter executed successfully',
|
|
360
|
+
namespace: filter.namespace,
|
|
361
|
+
action: requestAction,
|
|
362
|
+
duration,
|
|
363
|
+
filterType: 'after'
|
|
364
|
+
});
|
|
163
365
|
} catch (error) {
|
|
164
|
-
|
|
366
|
+
const duration = Date.now() - startTime;
|
|
367
|
+
|
|
368
|
+
// FIXED: Proper error handling with metrics
|
|
165
369
|
logger.error({
|
|
166
370
|
code: 'MC_FILTER_ERROR',
|
|
167
371
|
message: 'Error in afterAction filter',
|
|
168
372
|
namespace: filter.namespace,
|
|
169
373
|
action: requestAction,
|
|
374
|
+
duration,
|
|
375
|
+
filterType: 'after',
|
|
170
376
|
error: error.message,
|
|
171
377
|
stack: error.stack
|
|
172
378
|
});
|
|
@@ -176,24 +382,292 @@ class MasterActionFilters {
|
|
|
176
382
|
}
|
|
177
383
|
}
|
|
178
384
|
|
|
179
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Execute filter function with timeout protection
|
|
387
|
+
* @private
|
|
388
|
+
* @async
|
|
389
|
+
* @param {Function} func - Filter callback to execute
|
|
390
|
+
* @param {Object} context - Execution context (controller instance)
|
|
391
|
+
* @param {Array} args - Arguments to pass to filter
|
|
392
|
+
* @param {EventEmitter|null} emitter - Event emitter for next() calls
|
|
393
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
394
|
+
* @returns {Promise<*>} Filter result
|
|
395
|
+
* @throws {Error} If filter times out or throws error
|
|
396
|
+
*/
|
|
180
397
|
async _executeWithTimeout(func, context, args, emitter, timeout) {
|
|
398
|
+
// Validate context exists
|
|
399
|
+
if (!context) {
|
|
400
|
+
throw new Error('Filter execution context is null or undefined');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Validate request in args still has valid response
|
|
404
|
+
if (args[0] && args[0].response) {
|
|
405
|
+
const res = args[0].response;
|
|
406
|
+
if (res._headerSent || res.headersSent) {
|
|
407
|
+
logger.warn({
|
|
408
|
+
code: 'MC_FILTER_WARN_HEADERS_SENT',
|
|
409
|
+
message: 'Filter executing after headers already sent',
|
|
410
|
+
namespace: context.__namespace
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
181
415
|
// Store emitter in context for next() call
|
|
182
416
|
if (emitter) {
|
|
183
417
|
context.__filterEmitter = emitter;
|
|
184
418
|
}
|
|
185
419
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
Promise.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
420
|
+
// Wrap execution in try-catch for synchronous errors
|
|
421
|
+
try {
|
|
422
|
+
return await Promise.race([
|
|
423
|
+
// Execute the filter
|
|
424
|
+
Promise.resolve(func.call(context, ...args)),
|
|
425
|
+
// Timeout promise
|
|
426
|
+
new Promise((_, reject) =>
|
|
427
|
+
setTimeout(() => reject(new Error(`Filter timeout after ${timeout}ms`)), timeout)
|
|
428
|
+
)
|
|
429
|
+
]);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
// Ensure error has stack trace
|
|
432
|
+
if (!error.stack) {
|
|
433
|
+
error.stack = new Error().stack;
|
|
434
|
+
}
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ==================== Filter Management Utilities ====================
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Remove a before action filter
|
|
443
|
+
* @param {string|Array<string>} actionlist - Action name(s) to remove filter from
|
|
444
|
+
* @param {Function} func - Filter callback function to remove
|
|
445
|
+
* @returns {boolean} True if filter was removed
|
|
446
|
+
* @example
|
|
447
|
+
* this.removeBeforeAction('index', myFilterFunction);
|
|
448
|
+
*/
|
|
449
|
+
removeBeforeAction(actionlist, func) {
|
|
450
|
+
const actions = Array.isArray(actionlist) ? actionlist : [actionlist];
|
|
451
|
+
const initialLength = this._beforeActionFilters.length;
|
|
452
|
+
|
|
453
|
+
this._beforeActionFilters = this._beforeActionFilters.filter(filter => {
|
|
454
|
+
// Keep filter if callback doesn't match
|
|
455
|
+
if (filter.callBack !== func) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Keep filter if no actions match
|
|
460
|
+
return !filter.actionList.some(action => actions.includes(action));
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
return this._beforeActionFilters.length < initialLength;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Remove an after action filter
|
|
468
|
+
* @param {string|Array<string>} actionlist - Action name(s) to remove filter from
|
|
469
|
+
* @param {Function} func - Filter callback function to remove
|
|
470
|
+
* @returns {boolean} True if filter was removed
|
|
471
|
+
* @example
|
|
472
|
+
* this.removeAfterAction('index', myFilterFunction);
|
|
473
|
+
*/
|
|
474
|
+
removeAfterAction(actionlist, func) {
|
|
475
|
+
const actions = Array.isArray(actionlist) ? actionlist : [actionlist];
|
|
476
|
+
const initialLength = this._afterActionFilters.length;
|
|
477
|
+
|
|
478
|
+
this._afterActionFilters = this._afterActionFilters.filter(filter => {
|
|
479
|
+
// Keep filter if callback doesn't match
|
|
480
|
+
if (filter.callBack !== func) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Keep filter if no actions match
|
|
485
|
+
return !filter.actionList.some(action => actions.includes(action));
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
return this._afterActionFilters.length < initialLength;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Clear all filters (useful for testing)
|
|
493
|
+
* @param {string} [type] - 'before', 'after', or undefined for all
|
|
494
|
+
* @returns {void}
|
|
495
|
+
* @example
|
|
496
|
+
* this.clearFilters(); // Clear all
|
|
497
|
+
* this.clearFilters('before'); // Clear only before filters
|
|
498
|
+
*/
|
|
499
|
+
clearFilters(type) {
|
|
500
|
+
if (!type || type === 'before') {
|
|
501
|
+
this._beforeActionFilters = [];
|
|
502
|
+
}
|
|
503
|
+
if (!type || type === 'after') {
|
|
504
|
+
this._afterActionFilters = [];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Get all registered filters for debugging
|
|
510
|
+
* @param {string} [type] - 'before', 'after', or undefined for all
|
|
511
|
+
* @returns {Object} Filter information
|
|
512
|
+
* @example
|
|
513
|
+
* const filters = this.getRegisteredFilters();
|
|
514
|
+
* console.log(filters.before); // Array of before filters
|
|
515
|
+
*/
|
|
516
|
+
getRegisteredFilters(type) {
|
|
517
|
+
const result = {};
|
|
518
|
+
|
|
519
|
+
if (!type || type === 'before') {
|
|
520
|
+
result.before = this._beforeActionFilters.map(f => ({
|
|
521
|
+
namespace: f.namespace,
|
|
522
|
+
actions: f.actionList,
|
|
523
|
+
name: f.name,
|
|
524
|
+
priority: f.priority,
|
|
525
|
+
enabled: f.enabled,
|
|
526
|
+
registeredAt: f.registeredAt
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!type || type === 'after') {
|
|
531
|
+
result.after = this._afterActionFilters.map(f => ({
|
|
532
|
+
namespace: f.namespace,
|
|
533
|
+
actions: f.actionList,
|
|
534
|
+
name: f.name,
|
|
535
|
+
priority: f.priority,
|
|
536
|
+
enabled: f.enabled,
|
|
537
|
+
registeredAt: f.registeredAt
|
|
538
|
+
}));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Check if a filter is registered
|
|
546
|
+
* @param {string} type - 'before' or 'after'
|
|
547
|
+
* @param {string} action - Action name
|
|
548
|
+
* @param {string} [filterName] - Optional filter name to check
|
|
549
|
+
* @returns {boolean} True if filter exists
|
|
550
|
+
* @example
|
|
551
|
+
* if (this.hasFilter('before', 'index', 'authCheck')) { ... }
|
|
552
|
+
*/
|
|
553
|
+
hasFilter(type, action, filterName) {
|
|
554
|
+
const filters = type === 'before' ? this._beforeActionFilters : this._afterActionFilters;
|
|
555
|
+
const normalizedAction = this._normalizeAction(action);
|
|
556
|
+
|
|
557
|
+
return filters.some(filter => {
|
|
558
|
+
if (filterName && filter.name !== filterName) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return filter.actionList.some(a =>
|
|
563
|
+
this._normalizeAction(a) === normalizedAction
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Enable or disable a filter by name
|
|
570
|
+
* @param {string} type - 'before' or 'after'
|
|
571
|
+
* @param {string} filterName - Filter name
|
|
572
|
+
* @param {boolean} enabled - True to enable, false to disable
|
|
573
|
+
* @returns {boolean} True if filter was found and updated
|
|
574
|
+
* @example
|
|
575
|
+
* this.setFilterEnabled('before', 'authCheck', false); // Disable filter
|
|
576
|
+
*/
|
|
577
|
+
setFilterEnabled(type, filterName, enabled) {
|
|
578
|
+
const filters = type === 'before' ? this._beforeActionFilters : this._afterActionFilters;
|
|
579
|
+
let found = false;
|
|
580
|
+
|
|
581
|
+
filters.forEach(filter => {
|
|
582
|
+
if (filter.name === filterName) {
|
|
583
|
+
filter.enabled = enabled;
|
|
584
|
+
found = true;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
return found;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ==================== Test Utilities ====================
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Get filter count for testing
|
|
595
|
+
* @private
|
|
596
|
+
* @param {string} [type] - 'before', 'after', or undefined for total
|
|
597
|
+
* @returns {number} Number of registered filters
|
|
598
|
+
*/
|
|
599
|
+
_getFilterCount(type) {
|
|
600
|
+
if (type === 'before') {
|
|
601
|
+
return this._beforeActionFilters.length;
|
|
602
|
+
}
|
|
603
|
+
if (type === 'after') {
|
|
604
|
+
return this._afterActionFilters.length;
|
|
605
|
+
}
|
|
606
|
+
return this._beforeActionFilters.length + this._afterActionFilters.length;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Reset all filters for test isolation
|
|
611
|
+
* @private
|
|
612
|
+
* @returns {void}
|
|
613
|
+
*/
|
|
614
|
+
_resetFilters() {
|
|
615
|
+
this._beforeActionFilters = [];
|
|
616
|
+
this._afterActionFilters = [];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Check if a specific filter is registered by callback reference
|
|
621
|
+
* @private
|
|
622
|
+
* @param {string} type - 'before' or 'after'
|
|
623
|
+
* @param {Function} callback - Filter callback to check
|
|
624
|
+
* @returns {boolean} True if filter is registered
|
|
625
|
+
*/
|
|
626
|
+
_isFilterRegistered(type, callback) {
|
|
627
|
+
const filters = type === 'before' ? this._beforeActionFilters : this._afterActionFilters;
|
|
628
|
+
return filters.some(filter => filter.callBack === callback);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Get filter timeout value for testing
|
|
633
|
+
* @private
|
|
634
|
+
* @returns {number} Timeout in milliseconds
|
|
635
|
+
*/
|
|
636
|
+
_getFilterTimeout() {
|
|
637
|
+
return this._filterTimeout;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Set filter timeout value (useful for testing)
|
|
642
|
+
* @private
|
|
643
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
644
|
+
* @returns {void}
|
|
645
|
+
*/
|
|
646
|
+
_setFilterTimeout(timeout) {
|
|
647
|
+
if (typeof timeout === 'number' && timeout > 0) {
|
|
648
|
+
this._filterTimeout = timeout;
|
|
649
|
+
} else {
|
|
650
|
+
logger.warn({
|
|
651
|
+
code: 'MC_FILTER_INVALID_TIMEOUT',
|
|
652
|
+
message: 'Invalid timeout value, must be positive number',
|
|
653
|
+
timeout
|
|
654
|
+
});
|
|
655
|
+
}
|
|
194
656
|
}
|
|
195
657
|
|
|
196
|
-
|
|
658
|
+
/**
|
|
659
|
+
* Continue to the next filter or action
|
|
660
|
+
* Call this from beforeAction filters to proceed with request execution
|
|
661
|
+
* @returns {void}
|
|
662
|
+
* @example
|
|
663
|
+
* this.beforeAction('index', function() {
|
|
664
|
+
* if (this.isAuthenticated()) {
|
|
665
|
+
* this.next(); // Continue
|
|
666
|
+
* } else {
|
|
667
|
+
* this.redirectTo('/login'); // Stop and redirect
|
|
668
|
+
* }
|
|
669
|
+
* });
|
|
670
|
+
*/
|
|
197
671
|
next(){
|
|
198
672
|
if (this.__filterEmitter) {
|
|
199
673
|
this.__filterEmitter.emit("controller");
|