mastercontroller 1.2.14 → 1.3.1

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.
@@ -0,0 +1,344 @@
1
+ // MasterPipeline - Middleware Pipeline System
2
+ // version 1.0
3
+
4
+ var master = require('./MasterControl');
5
+ const { logger } = require('./error/MasterErrorLogger');
6
+
7
+ class MasterPipeline {
8
+ constructor() {
9
+ this.middleware = [];
10
+ this.errorHandlers = [];
11
+ }
12
+
13
+ /**
14
+ * Use: Add middleware that processes request/response
15
+ *
16
+ * Middleware signature: async (ctx, next) => { await next(); }
17
+ * - ctx: Request context { request, response, params, state, ... }
18
+ * - next: Function to call next middleware in chain
19
+ *
20
+ * Example:
21
+ * master.use(async (ctx, next) => {
22
+ * console.log('Before');
23
+ * await next();
24
+ * console.log('After');
25
+ * });
26
+ *
27
+ * @param {Function} middleware - Middleware function
28
+ * @returns {MasterPipeline} - For chaining
29
+ */
30
+ use(middleware) {
31
+ if (typeof middleware !== 'function') {
32
+ throw new Error('Middleware must be a function');
33
+ }
34
+
35
+ this.middleware.push({
36
+ type: 'use',
37
+ handler: middleware,
38
+ path: null
39
+ });
40
+
41
+ return this; // Chainable
42
+ }
43
+
44
+ /**
45
+ * Run: Add terminal middleware that ends the pipeline
46
+ *
47
+ * Terminal middleware signature: async (ctx) => { ... send response ... }
48
+ * - Does NOT call next()
49
+ * - Must send response
50
+ *
51
+ * Example:
52
+ * master.run(async (ctx) => {
53
+ * ctx.response.end('Hello World');
54
+ * });
55
+ *
56
+ * @param {Function} middleware - Terminal middleware function
57
+ * @returns {MasterPipeline} - For chaining
58
+ */
59
+ run(middleware) {
60
+ if (typeof middleware !== 'function') {
61
+ throw new Error('Terminal middleware must be a function');
62
+ }
63
+
64
+ this.middleware.push({
65
+ type: 'run',
66
+ handler: middleware,
67
+ path: null
68
+ });
69
+
70
+ return this; // Chainable
71
+ }
72
+
73
+ /**
74
+ * Map: Conditionally execute middleware based on path
75
+ *
76
+ * Map signature: (path, configure)
77
+ * - path: String or RegExp to match request path
78
+ * - configure: Function that receives a branch pipeline
79
+ *
80
+ * Example:
81
+ * master.map('/api/*', (api) => {
82
+ * api.use(authMiddleware);
83
+ * api.use(jsonMiddleware);
84
+ * });
85
+ *
86
+ * @param {String|RegExp} path - Path pattern to match
87
+ * @param {Function} configure - Function to configure branch pipeline
88
+ * @returns {MasterPipeline} - For chaining
89
+ */
90
+ map(path, configure) {
91
+ if (typeof configure !== 'function') {
92
+ throw new Error('Map configuration must be a function');
93
+ }
94
+
95
+ // Create sub-pipeline for this branch
96
+ const branch = new MasterPipeline();
97
+ configure(branch);
98
+
99
+ // Wrap branch in conditional middleware
100
+ const conditionalMiddleware = async (ctx, next) => {
101
+ const requestPath = ctx.pathName || ctx.request.url;
102
+
103
+ if (this._pathMatches(requestPath, path)) {
104
+ // Execute branch pipeline
105
+ await branch.execute(ctx);
106
+ // After branch completes, continue main pipeline
107
+ await next();
108
+ } else {
109
+ // Skip branch, continue main pipeline
110
+ await next();
111
+ }
112
+ };
113
+
114
+ this.middleware.push({
115
+ type: 'map',
116
+ handler: conditionalMiddleware,
117
+ path: path
118
+ });
119
+
120
+ return this; // Chainable
121
+ }
122
+
123
+ /**
124
+ * UseError: Add error handling middleware
125
+ *
126
+ * Error middleware signature: async (error, ctx, next) => { }
127
+ * - error: The caught error
128
+ * - ctx: Request context
129
+ * - next: Pass to next error handler or rethrow
130
+ *
131
+ * Example:
132
+ * master.useError(async (err, ctx, next) => {
133
+ * if (err.statusCode === 404) {
134
+ * ctx.response.statusCode = 404;
135
+ * ctx.response.end('Not Found');
136
+ * } else {
137
+ * await next(); // Pass to next error handler
138
+ * }
139
+ * });
140
+ *
141
+ * @param {Function} handler - Error handler function
142
+ * @returns {MasterPipeline} - For chaining
143
+ */
144
+ useError(handler) {
145
+ if (typeof handler !== 'function') {
146
+ throw new Error('Error handler must be a function');
147
+ }
148
+
149
+ this.errorHandlers.push(handler);
150
+ return this; // Chainable
151
+ }
152
+
153
+ /**
154
+ * Execute: Run the middleware pipeline for a request
155
+ *
156
+ * Called internally by the framework for each request
157
+ *
158
+ * @param {Object} context - Request context
159
+ */
160
+ async execute(context) {
161
+ let index = 0;
162
+
163
+ // Create the next function for middleware chain
164
+ const next = async () => {
165
+ // If we've run all middleware, we're done
166
+ if (index >= this.middleware.length) {
167
+ return;
168
+ }
169
+
170
+ const current = this.middleware[index++];
171
+
172
+ try {
173
+ if (current.type === 'run') {
174
+ // Terminal middleware - don't pass next
175
+ await current.handler(context);
176
+ } else {
177
+ // Regular middleware - pass next
178
+ await current.handler(context, next);
179
+ }
180
+ } catch (error) {
181
+ // Error occurred, run error handlers
182
+ await this._handleError(error, context);
183
+ }
184
+ };
185
+
186
+ // Start the pipeline
187
+ await next();
188
+ }
189
+
190
+ /**
191
+ * Handle errors through error handler chain
192
+ *
193
+ * @param {Error} error - The error that occurred
194
+ * @param {Object} context - Request context
195
+ */
196
+ async _handleError(error, context) {
197
+ let errorIndex = 0;
198
+
199
+ const nextError = async () => {
200
+ if (errorIndex >= this.errorHandlers.length) {
201
+ // No more error handlers, log and send generic error
202
+ logger.error({
203
+ code: 'MC_ERR_UNHANDLED',
204
+ message: 'Unhandled error in middleware pipeline',
205
+ error: error.message,
206
+ stack: error.stack
207
+ });
208
+
209
+ if (!context.response.headersSent) {
210
+ context.response.statusCode = 500;
211
+ context.response.end('Internal Server Error');
212
+ }
213
+ return;
214
+ }
215
+
216
+ const handler = this.errorHandlers[errorIndex++];
217
+
218
+ try {
219
+ await handler(error, context, nextError);
220
+ } catch (handlerError) {
221
+ // Error in error handler
222
+ logger.error({
223
+ code: 'MC_ERR_ERROR_HANDLER_FAILED',
224
+ message: 'Error handler threw an error',
225
+ error: handlerError.message
226
+ });
227
+ await nextError();
228
+ }
229
+ };
230
+
231
+ await nextError();
232
+ }
233
+
234
+ /**
235
+ * Check if request path matches the map path pattern
236
+ *
237
+ * @param {String} requestPath - The request path
238
+ * @param {String|RegExp} pattern - The pattern to match
239
+ * @returns {Boolean} - True if matches
240
+ */
241
+ _pathMatches(requestPath, pattern) {
242
+ // Normalize paths (ensure leading slash)
243
+ requestPath = '/' + requestPath.replace(/^\/|\/$/g, '');
244
+
245
+ if (typeof pattern === 'string') {
246
+ pattern = '/' + pattern.replace(/^\/|\/$/g, '');
247
+
248
+ // Wildcard support: /api/* matches /api/users, /api/posts, etc.
249
+ if (pattern.endsWith('/*')) {
250
+ const prefix = pattern.slice(0, -2);
251
+ return requestPath === prefix || requestPath.startsWith(prefix + '/');
252
+ }
253
+
254
+ // Exact or prefix match
255
+ return requestPath === pattern || requestPath.startsWith(pattern + '/');
256
+ }
257
+
258
+ if (pattern instanceof RegExp) {
259
+ return pattern.test(requestPath);
260
+ }
261
+
262
+ return false;
263
+ }
264
+
265
+ /**
266
+ * Discover and load middleware from folders
267
+ *
268
+ * @param {String|Object} options - Folder path or options object
269
+ */
270
+ discoverMiddleware(options) {
271
+ const fs = require('fs');
272
+ const path = require('path');
273
+
274
+ const folders = typeof options === 'string'
275
+ ? [options]
276
+ : (options.folders || ['middleware']);
277
+
278
+ folders.forEach(folder => {
279
+ const dir = path.join(master.root, folder);
280
+ if (!fs.existsSync(dir)) {
281
+ console.warn(`[Middleware] Folder not found: ${folder}`);
282
+ return;
283
+ }
284
+
285
+ const files = fs.readdirSync(dir)
286
+ .filter(file => file.endsWith('.js'))
287
+ .sort(); // Alphabetical order
288
+
289
+ files.forEach(file => {
290
+ try {
291
+ const middlewarePath = path.join(dir, file);
292
+ const middleware = require(middlewarePath);
293
+
294
+ // Support two patterns:
295
+ // Pattern 1: module.exports = async (ctx, next) => {}
296
+ if (typeof middleware === 'function') {
297
+ this.use(middleware);
298
+ }
299
+ // Pattern 2: module.exports = { register: (master) => {} }
300
+ else if (middleware.register && typeof middleware.register === 'function') {
301
+ middleware.register(master);
302
+ }
303
+ else {
304
+ console.warn(`[Middleware] Invalid export in ${folder}/${file}`);
305
+ return;
306
+ }
307
+
308
+ console.log(`[Middleware] Loaded: ${folder}/${file}`);
309
+ } catch (err) {
310
+ console.error(`[Middleware] Failed to load ${folder}/${file}:`, err.message);
311
+ }
312
+ });
313
+ });
314
+ }
315
+
316
+ /**
317
+ * Clear all middleware (useful for testing)
318
+ */
319
+ clear() {
320
+ this.middleware = [];
321
+ this.errorHandlers = [];
322
+ }
323
+
324
+ /**
325
+ * Inspect pipeline (for debugging)
326
+ *
327
+ * @returns {Object} - Pipeline information
328
+ */
329
+ inspect() {
330
+ return {
331
+ middlewareCount: this.middleware.length,
332
+ errorHandlerCount: this.errorHandlers.length,
333
+ middleware: this.middleware.map((m, i) => ({
334
+ index: i,
335
+ type: m.type,
336
+ path: m.path,
337
+ name: m.handler.name || 'anonymous'
338
+ }))
339
+ };
340
+ }
341
+ }
342
+
343
+ // Register with master
344
+ master.extend("pipeline", MasterPipeline);
package/MasterRouter.js CHANGED
@@ -246,88 +246,110 @@ var loadScopedListClasses = function(){
246
246
  };
247
247
 
248
248
 
249
+ /**
250
+ * Normalize route path: lowercase segments but preserve param names
251
+ *
252
+ * @param {String} path - Route path like "/Period/:periodId/Items/:itemId"
253
+ * @returns {String} - Normalized: "period/:periodId/items/:itemId"
254
+ */
255
+ function normalizeRoutePath(path) {
256
+ const trimmed = path.replace(/^\/|\/$/g, '');
257
+ const segments = trimmed.split('/');
258
+
259
+ const normalized = segments.map(segment => {
260
+ // Preserve parameter names (start with :)
261
+ if (segment.startsWith(':')) {
262
+ return segment;
263
+ }
264
+ // Lowercase path segments
265
+ return segment.toLowerCase();
266
+ });
267
+
268
+ return normalized.join('/');
269
+ }
270
+
249
271
  class MasterRouter {
250
272
  currentRouteName = null
251
273
  _routes = {}
252
-
274
+
253
275
  start(){
254
276
  var $that = this;
255
277
  return {
256
278
  route : function(path, toPath, type, constraint){ // function to add to list of routes
257
-
279
+
258
280
  var pathList = toPath.replace(/^\/|\/$/g, '').split("#");
259
-
281
+
260
282
  var route = {
261
283
  type: type.toLowerCase(),
262
- path: path.replace(/^\/|\/$/g, '').toLowerCase(),
284
+ path: normalizeRoutePath(path),
263
285
  toController :pathList[0].replace(/^\/|\/$/g, ''),
264
286
  toAction: pathList[1],
265
287
  constraint : constraint
266
288
  };
267
-
289
+
268
290
  $that._routes[$that.currentRouteName].routes.push(route);
269
-
291
+
270
292
  },
271
293
 
272
294
  resources: function(routeName){ // function to add to list of routes using resources bulk
273
-
295
+
274
296
 
275
297
  $that._routes[$that.currentRouteName].routes.push({
276
298
  type: "get",
277
- path: routeName.toLowerCase(),
299
+ path: normalizeRoutePath(routeName),
278
300
  toController :routeName,
279
301
  toAction: "index",
280
302
  constraint : null
281
303
  });
282
-
304
+
283
305
  $that._routes[$that.currentRouteName].routes.push({
284
306
  type: "get",
285
- path: routeName.toLowerCase(),
307
+ path: normalizeRoutePath(routeName),
286
308
  toController :routeName,
287
309
  toAction: "new",
288
310
  constraint : null
289
311
  });
290
-
312
+
291
313
  $that._routes[$that.currentRouteName].routes.push({
292
314
  type: "post",
293
- path: routeName.toLowerCase(),
315
+ path: normalizeRoutePath(routeName),
294
316
  toController :routeName,
295
317
  toAction: "create",
296
318
  constraint : null
297
319
  });
298
-
320
+
299
321
  $that._routes[$that.currentRouteName].routes.push({
300
322
  // pages/3
301
323
  type: "get",
302
- path: routeName.toLowerCase() + "/:id",
324
+ path: normalizeRoutePath(routeName + "/:id"),
303
325
  toController :routeName,
304
326
  toAction: "show",
305
327
  constraint : null
306
328
  });
307
-
329
+
308
330
  $that._routes[$that.currentRouteName].routes.push({
309
331
  type: "get",
310
- path: routeName.toLowerCase() + "/:id/" + "edit",
332
+ path: normalizeRoutePath(routeName + "/:id/edit"),
311
333
  toController :routeName,
312
334
  toAction: "edit",
313
- constraint : null
335
+ constraint : null
314
336
  });
315
-
337
+
316
338
  $that._routes[$that.currentRouteName].routes.push({
317
339
  type: "put",
318
- path: routeName.toLowerCase() + "/:id",
340
+ path: normalizeRoutePath(routeName + "/:id"),
319
341
  toController :routeName,
320
342
  toAction: "update",
321
343
  constraint : null
322
344
  });
323
-
345
+
324
346
  $that._routes[$that.currentRouteName].routes.push({
325
347
  type: "delete",
326
- path: routeName.toLowerCase() + "/:id",
348
+ path: normalizeRoutePath(routeName + "/:id"),
327
349
  toController :routeName,
328
350
  toAction: "destroy",
329
351
  constraint : null
330
- });
352
+ });
331
353
  }
332
354
  }
333
355
  }
package/MasterSession.js CHANGED
@@ -35,6 +35,11 @@ class MasterSession{
35
35
  this.options.secret = TID;
36
36
  }
37
37
 
38
+ // Auto-register with pipeline if available
39
+ if (master.pipeline) {
40
+ master.pipeline.use(this.middleware());
41
+ }
42
+
38
43
  return {
39
44
  setPath : function(path){
40
45
  $that.options.path = path === undefined ? '/' : path;
@@ -184,6 +189,20 @@ class MasterSession{
184
189
  return -1;
185
190
  }
186
191
  }
192
+
193
+ /**
194
+ * Get session middleware for the pipeline
195
+ * Sessions are accessed lazily via master.sessions in controllers
196
+ */
197
+ middleware() {
198
+ var $that = this;
199
+
200
+ return async (ctx, next) => {
201
+ // Sessions are available via master.sessions.get/set in controllers
202
+ // No action needed here - just continue pipeline
203
+ await next();
204
+ };
205
+ }
187
206
  }
188
207
 
189
208
  master.extend("sessions", MasterSession);