millas 0.2.10 → 0.2.12-beta

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.
Files changed (48) hide show
  1. package/package.json +17 -3
  2. package/src/auth/AuthController.js +42 -133
  3. package/src/auth/AuthMiddleware.js +12 -23
  4. package/src/auth/RoleMiddleware.js +7 -17
  5. package/src/commands/migrate.js +46 -31
  6. package/src/commands/serve.js +266 -37
  7. package/src/container/Application.js +88 -8
  8. package/src/container/MillasApp.js +6 -14
  9. package/src/controller/Controller.js +79 -300
  10. package/src/errors/ErrorRenderer.js +640 -0
  11. package/src/facades/Admin.js +49 -0
  12. package/src/facades/Auth.js +46 -0
  13. package/src/facades/Cache.js +17 -0
  14. package/src/facades/Database.js +43 -0
  15. package/src/facades/Events.js +24 -0
  16. package/src/facades/Http.js +54 -0
  17. package/src/facades/Log.js +56 -0
  18. package/src/facades/Mail.js +40 -0
  19. package/src/facades/Queue.js +23 -0
  20. package/src/facades/Storage.js +17 -0
  21. package/src/facades/Validation.js +69 -0
  22. package/src/http/MillasRequest.js +253 -0
  23. package/src/http/MillasResponse.js +196 -0
  24. package/src/http/RequestContext.js +176 -0
  25. package/src/http/ResponseDispatcher.js +144 -0
  26. package/src/http/helpers.js +164 -0
  27. package/src/http/index.js +13 -0
  28. package/src/index.js +55 -2
  29. package/src/logger/internal.js +76 -0
  30. package/src/logger/patchConsole.js +135 -0
  31. package/src/middleware/CorsMiddleware.js +22 -30
  32. package/src/middleware/LogMiddleware.js +27 -59
  33. package/src/middleware/Middleware.js +24 -15
  34. package/src/middleware/MiddlewarePipeline.js +30 -67
  35. package/src/middleware/MiddlewareRegistry.js +126 -0
  36. package/src/middleware/ThrottleMiddleware.js +22 -26
  37. package/src/orm/fields/index.js +124 -56
  38. package/src/orm/migration/ModelInspector.js +7 -3
  39. package/src/orm/model/Model.js +96 -6
  40. package/src/orm/query/QueryBuilder.js +141 -3
  41. package/src/providers/LogServiceProvider.js +88 -18
  42. package/src/providers/ProviderRegistry.js +14 -1
  43. package/src/providers/ServiceProvider.js +40 -8
  44. package/src/router/Router.js +155 -223
  45. package/src/scaffold/maker.js +24 -59
  46. package/src/scaffold/templates.js +13 -12
  47. package/src/validation/BaseValidator.js +193 -0
  48. package/src/validation/Validator.js +680 -0
package/src/index.js CHANGED
@@ -1,6 +1,46 @@
1
1
  'use strict';
2
2
 
3
3
  // ── HTTP Layer ────────────────────────────────────────────────────
4
+ const {
5
+ MillasRequest,
6
+ MillasResponse,
7
+ ResponseDispatcher,
8
+ jsonify,
9
+ view,
10
+ redirect,
11
+ text,
12
+ file,
13
+ empty,
14
+ abort,
15
+ notFound,
16
+ unauthorized,
17
+ forbidden,
18
+ } = require('./http/index');
19
+ const RequestContext = require('./http/RequestContext');
20
+
21
+ // ── Validation ────────────────────────────────────────────────────
22
+ const {
23
+ Validator,
24
+ BaseValidator,
25
+ StringValidator,
26
+ EmailValidator,
27
+ NumberValidator,
28
+ BooleanValidator,
29
+ DateValidator,
30
+ ArrayValidator,
31
+ ObjectValidator,
32
+ FileValidator,
33
+ string,
34
+ email,
35
+ number,
36
+ boolean,
37
+ date,
38
+ array,
39
+ object: objectField,
40
+ file: fileField,
41
+ } = require('./validation/Validator');
42
+ const MillasApp = require('./container/MillasApp');
43
+ // ── HTTP Layer (old) ──────────────────────────────────────────────
4
44
  const Controller = require('./controller/Controller');
5
45
  const Middleware = require('./middleware/Middleware');
6
46
  const MiddlewarePipeline = require('./middleware/MiddlewarePipeline');
@@ -12,7 +52,6 @@ const HttpError = require('./errors/HttpError');
12
52
  // ── DI Container ─────────────────────────────────────────────────
13
53
  const Container = require('./container/Container');
14
54
  const Application = require('./container/Application');
15
- const MillasApp = require('./container/MillasApp');
16
55
  const ServiceProvider = require('./providers/ServiceProvider');
17
56
  const ProviderRegistry = require('./providers/ProviderRegistry');
18
57
 
@@ -61,11 +100,25 @@ const Storage = require('./storage/Storage');
61
100
  const LocalDriver = require('./storage/drivers/LocalDriver');
62
101
 
63
102
  module.exports = {
103
+ // Millas App
104
+ MillasApp,
105
+ // ── Millas HTTP layer ──────────────────────────────────────────
106
+ MillasRequest, MillasResponse, ResponseDispatcher, RequestContext,
107
+ jsonify, view, redirect, text, send_file:file, empty,
108
+ abort, notFound, unauthorized, forbidden,
109
+ // ── Validation ────────────────────────────────────────────────
110
+ Validator,
111
+ BaseValidator,
112
+ StringValidator, EmailValidator, NumberValidator, BooleanValidator,
113
+ DateValidator, ArrayValidator, ObjectValidator, FileValidator,
114
+ string, email, number, boolean, date, array,
115
+ object: objectField,
116
+ file: fileField,
64
117
  // HTTP
65
118
  Controller, Middleware, MiddlewarePipeline,
66
119
  CorsMiddleware, ThrottleMiddleware, LogMiddleware, HttpError,
67
120
  // DI
68
- Container, Application, MillasApp, ServiceProvider, ProviderRegistry,
121
+ Container, Application, ServiceProvider, ProviderRegistry,
69
122
  // ORM
70
123
  Model, fields, QueryBuilder, DatabaseManager, SchemaBuilder,
71
124
  MigrationRunner, ModelInspector, DatabaseServiceProvider,
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * src/logger/internal.js
5
+ *
6
+ * MillasLog — the Millas framework's own internal logger.
7
+ *
8
+ * This is separate from the user-facing `Log` singleton. It is used by:
9
+ * - ORM (queries, relation warnings, migration runner)
10
+ * - Admin panel
11
+ * - Queue worker
12
+ * - Any other framework-internal code
13
+ *
14
+ * It always works — no provider, no config, no try/catch needed at call sites.
15
+ * It starts in a sensible default state (WARN+ to console) and is upgraded
16
+ * by LogServiceProvider once the app boots and config/logging.js is read.
17
+ *
18
+ * ── Usage inside framework code ────────────────────────────────────────────
19
+ *
20
+ * const MillasLog = require('../logger/internal');
21
+ *
22
+ * MillasLog.d('ORM', 'Running query', { table: 'users' });
23
+ * MillasLog.w('ORM', 'Relation not defined', { model: 'Post', name: 'tags' });
24
+ * MillasLog.e('Migration', 'Failed to run migration', error);
25
+ *
26
+ * ── Configuring from config/logging.js ─────────────────────────────────────
27
+ *
28
+ * module.exports = {
29
+ * // ... app channels ...
30
+ *
31
+ * internal: {
32
+ * level: 'debug', // show all ORM/framework logs (default: 'warn')
33
+ * format: 'pretty', // pretty | simple | json
34
+ * },
35
+ * };
36
+ *
37
+ * ── Disabling internal logs entirely ───────────────────────────────────────
38
+ *
39
+ * internal: false
40
+ *
41
+ * ── Writing to a separate file ─────────────────────────────────────────────
42
+ *
43
+ * internal: {
44
+ * level: 'debug',
45
+ * channels: [
46
+ * { driver: 'console', format: 'pretty', level: 'warn' },
47
+ * { driver: 'file', format: 'simple', path: 'storage/logs', prefix: 'millas-internal', level: 'debug' },
48
+ * ],
49
+ * },
50
+ */
51
+
52
+ const Logger = require('./Logger');
53
+ const { LEVELS } = require('./levels');
54
+ const ConsoleChannel = require('./channels/ConsoleChannel');
55
+ const PrettyFormatter = require('./formatters/PrettyFormatter');
56
+ const { NullChannel } = require('./channels/index');
57
+
58
+ // ── Create the MillasLog singleton ───────────────────────────────────────────
59
+
60
+ const MillasLog = new Logger();
61
+
62
+ // Default: WARN and above, pretty-formatted to console.
63
+ // This means normal app runs are quiet — you only see warnings and errors
64
+ // from the framework itself unless you opt in to lower levels.
65
+ MillasLog.configure({
66
+ defaultTag: 'Millas',
67
+ minLevel: LEVELS.WARN,
68
+ channel: new ConsoleChannel({
69
+ formatter: new PrettyFormatter({
70
+ colour: process.stdout.isTTY !== false,
71
+ }),
72
+ minLevel: LEVELS.WARN,
73
+ }),
74
+ });
75
+
76
+ module.exports = MillasLog;
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ const { LEVELS } = require('./levels');
4
+
5
+ /**
6
+ * patchConsole(Log, defaultTag)
7
+ *
8
+ * Replaces the global console.* methods so that all console output
9
+ * in the app goes through the configured Log channels — same formatting,
10
+ * same level filtering, same file output.
11
+ *
12
+ * Safe: ConsoleChannel writes via process.stdout.write / process.stderr.write,
13
+ * never via console.*, so there is no recursion risk.
14
+ *
15
+ * Called once from LogServiceProvider.boot() when interceptConsole is enabled.
16
+ * Returns a restore() function that puts the originals back.
17
+ *
18
+ * Level mapping:
19
+ * console.log → Log.i (INFO)
20
+ * console.info → Log.i (INFO)
21
+ * console.warn → Log.w (WARN)
22
+ * console.error → Log.e (ERROR)
23
+ * console.debug → Log.d (DEBUG)
24
+ * console.trace → Log.v (VERBOSE)
25
+ * console.dir → Log.d (DEBUG)
26
+ */
27
+ function patchConsole(Log, defaultTag = 'App') {
28
+ // Save originals — restore() puts these back
29
+ const originals = {
30
+ log: console.log.bind(console),
31
+ info: console.info.bind(console),
32
+ warn: console.warn.bind(console),
33
+ error: console.error.bind(console),
34
+ debug: console.debug.bind(console),
35
+ trace: console.trace.bind(console),
36
+ dir: console.dir.bind(console),
37
+ };
38
+
39
+ // Build a dispatcher for a given level
40
+ function make(level) {
41
+ return function (...args) {
42
+ const { message, context, error } = parse(args);
43
+ Log._emit({
44
+ level,
45
+ tag: defaultTag,
46
+ message: message || '',
47
+ context,
48
+ error,
49
+ timestamp: new Date().toISOString(),
50
+ pid: process.pid,
51
+ });
52
+ };
53
+ }
54
+
55
+ console.log = make(LEVELS.INFO);
56
+ console.info = make(LEVELS.INFO);
57
+ console.warn = make(LEVELS.WARN);
58
+ console.error = make(LEVELS.ERROR);
59
+ console.debug = make(LEVELS.DEBUG);
60
+ console.trace = make(LEVELS.VERBOSE);
61
+ console.dir = (obj) => Log._emit({ level: LEVELS.DEBUG, tag: defaultTag, message: '', context: obj, error: undefined, timestamp: new Date().toISOString(), pid: process.pid });
62
+
63
+ return function restore() {
64
+ Object.assign(console, originals);
65
+ };
66
+ }
67
+
68
+ // ── Argument parser ───────────────────────────────────────────────────────────
69
+ //
70
+ // Handles the main shapes people pass to console.*:
71
+ //
72
+ // console.log('message')
73
+ // console.log('message', { ctx })
74
+ // console.log('message', error)
75
+ // console.error(error)
76
+ // console.log({ obj })
77
+ // console.log('x:', 42) → message: 'x: 42'
78
+ // console.log('a', 'b', 'c') → message: 'a b c'
79
+
80
+ function parse(args) {
81
+ if (args.length === 0) {
82
+ return { message: '', context: undefined, error: undefined };
83
+ }
84
+
85
+ if (args.length === 1) {
86
+ const a = args[0];
87
+ if (a instanceof Error) return { message: a.message, context: undefined, error: a };
88
+ if (typeof a === 'object' && a !== null) return { message: '', context: a, error: undefined };
89
+ return { message: String(a), context: undefined, error: undefined };
90
+ }
91
+
92
+ const [first, ...rest] = args;
93
+
94
+ // First arg is an Error
95
+ if (first instanceof Error) {
96
+ return { message: first.message, context: rest.length ? rest : undefined, error: first };
97
+ }
98
+
99
+ // First arg is a string message
100
+ if (typeof first === 'string') {
101
+ // Single extra arg
102
+ if (rest.length === 1) {
103
+ const r = rest[0];
104
+ if (r instanceof Error) return { message: first, context: undefined, error: r };
105
+ if (typeof r === 'object' && r !== null) return { message: first, context: r, error: undefined };
106
+ // Scalar extra: append to message (console.log('count:', 42))
107
+ return { message: first + ' ' + String(r), context: undefined, error: undefined };
108
+ }
109
+
110
+ // Multiple extra args — find a trailing Error, collect the rest as context
111
+ const lastArg = rest[rest.length - 1];
112
+ if (lastArg instanceof Error) {
113
+ const ctx = rest.slice(0, -1);
114
+ return { message: first, context: ctx.length ? ctx : undefined, error: lastArg };
115
+ }
116
+
117
+ // All strings/scalars — join into message
118
+ if (rest.every(r => typeof r !== 'object' || r === null)) {
119
+ return { message: [first, ...rest].map(String).join(' '), context: undefined, error: undefined };
120
+ }
121
+
122
+ // Mixed — put extras in context
123
+ return { message: first, context: rest, error: undefined };
124
+ }
125
+
126
+ // First arg is an object
127
+ if (typeof first === 'object' && first !== null) {
128
+ return { message: '', context: first, error: undefined };
129
+ }
130
+
131
+ // Fallback — join everything as a string
132
+ return { message: args.map(String).join(' '), context: undefined, error: undefined };
133
+ }
134
+
135
+ module.exports = patchConsole;
@@ -1,58 +1,50 @@
1
1
  'use strict';
2
2
 
3
- const Middleware = require('./Middleware');
3
+ const Middleware = require('./Middleware');
4
+ const MillasResponse = require('../http/MillasResponse');
4
5
 
5
6
  /**
6
7
  * CorsMiddleware
7
8
  *
8
- * Adds Cross-Origin Resource Sharing headers to every response.
9
- *
10
- * Register:
11
- * middlewareRegistry.register('cors', CorsMiddleware);
12
- *
13
- * Configure in bootstrap/app.js:
14
- * new CorsMiddleware({
15
- * origins: ['https://myapp.com'],
16
- * methods: ['GET', 'POST'],
17
- * })
18
- *
19
- * Or register the class directly for default permissive settings.
9
+ * Adds CORS headers. Uses the new ctx signature.
20
10
  */
21
11
  class CorsMiddleware extends Middleware {
22
12
  constructor(options = {}) {
23
13
  super();
24
14
  this.origins = options.origins || ['*'];
25
- this.methods = options.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
26
- this.headers = options.headers || ['Content-Type', 'Authorization', 'X-Requested-With'];
15
+ this.methods = options.methods || ['GET','POST','PUT','PATCH','DELETE','OPTIONS'];
16
+ this.headers = options.headers || ['Content-Type','Authorization','X-Requested-With'];
27
17
  this.credentials = options.credentials ?? false;
28
18
  this.maxAge = options.maxAge || 86400;
29
19
  }
30
20
 
31
- async handle(req, res, next) {
32
- const origin = req.headers.origin;
21
+ async handle({ req }, next) {
22
+ const origin = req.header('origin');
33
23
 
34
- // Origin matching
24
+ // Build headers map
25
+ const h = {};
35
26
  if (this.origins.includes('*')) {
36
- res.setHeader('Access-Control-Allow-Origin', '*');
27
+ h['Access-Control-Allow-Origin'] = '*';
37
28
  } else if (origin && this.origins.includes(origin)) {
38
- res.setHeader('Access-Control-Allow-Origin', origin);
39
- res.setHeader('Vary', 'Origin');
29
+ h['Access-Control-Allow-Origin'] = origin;
30
+ h['Vary'] = 'Origin';
40
31
  }
41
-
42
- res.setHeader('Access-Control-Allow-Methods', this.methods.join(', '));
43
- res.setHeader('Access-Control-Allow-Headers', this.headers.join(', '));
44
- res.setHeader('Access-Control-Max-Age', String(this.maxAge));
45
-
32
+ h['Access-Control-Allow-Methods'] = this.methods.join(', ');
33
+ h['Access-Control-Allow-Headers'] = this.headers.join(', ');
34
+ h['Access-Control-Max-Age'] = String(this.maxAge);
46
35
  if (this.credentials) {
47
- res.setHeader('Access-Control-Allow-Credentials', 'true');
36
+ h['Access-Control-Allow-Credentials'] = 'true';
48
37
  }
49
38
 
50
- // Handle preflight
39
+ // Preflight — short-circuit with 204
51
40
  if (req.method === 'OPTIONS') {
52
- return res.status(204).send();
41
+ return MillasResponse.empty(204).withHeaders(h);
53
42
  }
54
43
 
55
- next();
44
+ // Proceed — but we still need headers on the eventual response.
45
+ // Store on raw req so ResponseDispatcher can pick them up.
46
+ req.raw._corsHeaders = h;
47
+ return next();
56
48
  }
57
49
  }
58
50
 
@@ -5,31 +5,8 @@ const Middleware = require('./Middleware');
5
5
  /**
6
6
  * LogMiddleware
7
7
  *
8
- * Django-style HTTP request logger.
9
- * Uses the Millas Log singleton output goes through your configured channels.
10
- *
11
- * Log levels per status:
12
- * 2xx, 3xx → INFO (green)
13
- * 4xx → WARN (yellow)
14
- * 5xx → ERROR (red)
15
- * slow req → WARN (with slow=true in context)
16
- *
17
- * Output:
18
- * I HTTP GET /api/users 200 4ms
19
- * W HTTP POST /api/login 401 12ms
20
- * E HTTP GET /api/crash 500 3ms
21
- *
22
- * Usage (register as middleware):
23
- * app.use(new LogMiddleware().handle.bind(new LogMiddleware()));
24
- *
25
- * Or use Log.requestMiddleware() directly for more options.
26
- *
27
- * Options:
28
- * silent — suppress all output (default: false)
29
- * includeQuery — include query string in URL (default: false)
30
- * includeIp — include client IP (default: true)
31
- * slowThreshold — warn if response > Nms (default: 1000)
32
- * skip — function(req, res) => bool — skip matching routes
8
+ * Django-style HTTP request logging via MillasLog.
9
+ * Uses the new ctx signature: handle({ req }, next)
33
10
  */
34
11
  class LogMiddleware extends Middleware {
35
12
  constructor(options = {}) {
@@ -41,46 +18,37 @@ class LogMiddleware extends Middleware {
41
18
  this.skip = options.skip ?? null;
42
19
  }
43
20
 
44
- async handle(req, res, next) {
21
+ async handle({ req }, next) {
45
22
  if (this.silent) return next();
46
- if (typeof this.skip === 'function' && this.skip(req, res)) return next();
47
-
48
- // Lazy-require Log so this file loads even before LogServiceProvider runs
49
- let Log;
50
- try {
51
- Log = require('../logger/index').Log;
52
- } catch {
53
- return next();
54
- }
23
+ if (typeof this.skip === 'function' && this.skip(req)) return next();
55
24
 
56
25
  const start = Date.now();
57
- const { LEVELS } = require('../logger/levels');
58
-
59
- res.on('finish', () => {
60
- const ms = Date.now() - start;
61
- const status = res.statusCode;
62
- const method = req.method;
63
- let url = req.path || req.url || '/';
64
-
65
- if (this.includeQuery && req.url && req.url.includes('?')) {
66
- url = req.url;
67
- }
68
-
69
- // Level based on status + response time
70
- let level;
71
- if (status >= 500) level = LEVELS.ERROR;
72
- else if (status >= 400) level = LEVELS.WARN;
73
- else if (ms > this.slowThreshold) level = LEVELS.WARN;
74
- else level = LEVELS.INFO;
75
-
76
- const ctx = { status, ms };
77
- if (this.includeIp) ctx.ip = req.ip || req.connection?.remoteAddress;
78
- if (ms > this.slowThreshold) ctx.slow = true;
79
26
 
80
- Log._log(level, 'HTTP', `${method} ${url} ${status} ${ms}ms`, ctx);
27
+ // We can't hook into the response here since next() returns undefined.
28
+ // Instead attach a finish listener to the raw Express res.
29
+ req.raw.res.on('finish', () => {
30
+ try {
31
+ const MillasLog = require('../logger/internal');
32
+ const { LEVELS } = require('../logger/levels');
33
+ const ms = Date.now() - start;
34
+ const status = req.raw.res.statusCode;
35
+ let url = req.path;
36
+ if (this.includeQuery && req.raw.url?.includes('?')) url = req.raw.url;
37
+
38
+ let level = status >= 500 ? LEVELS.ERROR
39
+ : status >= 400 ? LEVELS.WARN
40
+ : ms > this.slowThreshold ? LEVELS.WARN
41
+ : LEVELS.INFO;
42
+
43
+ const ctx = { status, ms };
44
+ if (this.includeIp) ctx.ip = req.ip;
45
+ if (ms > this.slowThreshold) ctx.slow = true;
46
+
47
+ MillasLog._log(level, 'HTTP', `${req.method} ${url} ${status} ${ms}ms`, ctx);
48
+ } catch {}
81
49
  });
82
50
 
83
- next();
51
+ return next();
84
52
  }
85
53
  }
86
54
 
@@ -4,33 +4,42 @@
4
4
  * Middleware
5
5
  *
6
6
  * Base class for all Millas middleware.
7
- * Subclasses must implement handle(req, res, next).
8
7
  *
9
- * Usage:
8
+ * Middleware receives a RequestContext and a next() function.
9
+ * Destructure exactly what you need from the context — same as route handlers.
10
+ *
10
11
  * class AuthMiddleware extends Middleware {
11
- * async handle(req, res, next) {
12
- * // ...
13
- * next();
12
+ * async handle({ headers, user }, next) {
13
+ * if (!headers.authorization) {
14
+ * return jsonify({ error: 'Unauthorized' }, { status: 401 });
15
+ * }
16
+ * return next();
17
+ * }
18
+ * }
19
+ *
20
+ * class LogMiddleware extends Middleware {
21
+ * async handle({ req }, next) {
22
+ * Log.i('HTTP', `${req.method} ${req.path}`);
23
+ * return next();
14
24
  * }
15
25
  * }
16
26
  */
17
27
  class Middleware {
18
28
  /**
19
- * Handle the incoming request.
20
- *
21
- * @param {import('express').Request} req
22
- * @param {import('express').Response} res
23
- * @param {Function} next
29
+ * @param {import('../http/RequestContext')} ctx
30
+ * @param {Function} next
31
+ * @returns {import('../http/MillasResponse')|Promise<import('../http/MillasResponse')>}
24
32
  */
25
- async handle(req, res, next) {
26
- throw new Error(`${this.constructor.name} must implement handle(req, res, next)`);
33
+ async handle(ctx, next) {
34
+ throw new Error(`${this.constructor.name} must implement handle(ctx, next).`);
27
35
  }
28
36
 
29
37
  /**
30
- * Called after the response is sent.
31
- * Override for cleanup, logging, etc.
38
+ * Called after the response is dispatched.
39
+ * @param {import('../http/RequestContext')} ctx
40
+ * @param {import('../http/MillasResponse')} response
32
41
  */
33
- async terminate(req, res) {}
42
+ async terminate(ctx, response) {}
34
43
  }
35
44
 
36
45
  module.exports = Middleware;
@@ -1,93 +1,56 @@
1
1
  'use strict';
2
2
 
3
+ const MillasRequest = require('../http/MillasRequest');
4
+ const RequestContext = require('../http/RequestContext');
5
+ const MillasResponse = require('../http/MillasResponse');
6
+ const ResponseDispatcher = require('../http/ResponseDispatcher');
7
+
3
8
  /**
4
9
  * MiddlewarePipeline
5
10
  *
6
- * Composes an ordered list of middleware handlers into a single
7
- * Express-compatible function. Used internally by the Router.
8
- *
9
- * Each handler can be:
10
- * - A Middleware subclass (instantiated automatically)
11
- * - An already-instantiated Middleware object
12
- * - A raw Express function (req, res, next) => {}
11
+ * Runs an ordered list of middleware instances against a request.
12
+ * Used for programmatic pipelines outside of the router (e.g. queue webhooks).
13
13
  *
14
- * Usage (internal):
15
- * const pipeline = new MiddlewarePipeline([AuthMiddleware, LogMiddleware]);
16
- * app.use(pipeline.compose());
14
+ * Each middleware receives a RequestContext and a next() function.
17
15
  */
18
16
  class MiddlewarePipeline {
19
- constructor(handlers = []) {
20
- this._handlers = handlers;
21
- }
22
-
23
- /**
24
- * Add a handler to the end of the pipeline.
25
- */
26
- pipe(handler) {
27
- this._handlers.push(handler);
28
- return this;
17
+ constructor(middlewares = []) {
18
+ this._middlewares = middlewares;
29
19
  }
30
20
 
31
- /**
32
- * Add a handler to the beginning of the pipeline.
33
- */
34
- prepend(handler) {
35
- this._handlers.unshift(handler);
21
+ add(middleware) {
22
+ this._middlewares.push(middleware);
36
23
  return this;
37
24
  }
38
25
 
39
26
  /**
40
- * Compose all handlers into a single (req, res, next) function.
27
+ * Run the pipeline against an Express req/res.
28
+ * @param {object} expressReq
29
+ * @param {object} expressRes
30
+ * @param {object|null} container
41
31
  */
42
- compose() {
43
- const fns = this._handlers.map(h => this._resolve(h));
32
+ async run(expressReq, expressRes, container = null) {
33
+ const millaReq = new MillasRequest(expressReq);
34
+ const ctx = new RequestContext(millaReq, container);
44
35
 
45
- return function pipeline(req, res, next) {
46
- let index = 0;
36
+ const dispatch = async (index) => {
37
+ if (index >= this._middlewares.length) return null;
47
38
 
48
- function dispatch(i) {
49
- if (i >= fns.length) return next();
50
- const fn = fns[i];
39
+ const mw = this._middlewares[index];
40
+ const next = () => dispatch(index + 1);
51
41
 
52
- try {
53
- const result = fn(req, res, (err) => {
54
- if (err) return next(err);
55
- dispatch(i + 1);
56
- });
57
- // Handle async middleware that returns a Promise
58
- if (result && typeof result.catch === 'function') {
59
- result.catch(next);
60
- }
61
- } catch (err) {
62
- next(err);
63
- }
64
- }
42
+ const instance = typeof mw === 'function' && mw.prototype?.handle
43
+ ? new mw()
44
+ : mw;
65
45
 
66
- dispatch(0);
46
+ return instance.handle(ctx, next);
67
47
  };
68
- }
69
-
70
- /**
71
- * Resolve a handler to a plain (req, res, next) => {} function.
72
- */
73
- _resolve(handler) {
74
- // Raw Express function
75
- if (typeof handler === 'function' && !(handler.prototype instanceof require('./Middleware'))) {
76
- return handler;
77
- }
78
48
 
79
- // Middleware class (not yet instantiated)
80
- if (typeof handler === 'function' && handler.prototype instanceof require('./Middleware')) {
81
- const instance = new handler();
82
- return (req, res, next) => instance.handle(req, res, next);
83
- }
49
+ const response = await dispatch(0);
84
50
 
85
- // Instantiated Middleware object
86
- if (handler && typeof handler.handle === 'function') {
87
- return (req, res, next) => handler.handle(req, res, next);
51
+ if (response && MillasResponse.isResponse(response) && !expressRes.headersSent) {
52
+ ResponseDispatcher.dispatch(response, expressRes);
88
53
  }
89
-
90
- throw new Error(`Invalid middleware: ${handler}. Must be a Middleware class, instance, or function.`);
91
54
  }
92
55
  }
93
56