millas 0.2.11 → 0.2.12-beta-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.
Files changed (74) hide show
  1. package/package.json +6 -5
  2. package/src/auth/Auth.js +13 -8
  3. package/src/auth/AuthController.js +45 -134
  4. package/src/auth/AuthMiddleware.js +12 -23
  5. package/src/auth/AuthUser.js +98 -0
  6. package/src/auth/RoleMiddleware.js +7 -17
  7. package/src/cli.js +1 -1
  8. package/src/commands/migrate.js +46 -31
  9. package/src/commands/serve.js +238 -38
  10. package/src/container/AppInitializer.js +158 -0
  11. package/src/container/Application.js +288 -183
  12. package/src/container/HttpServer.js +156 -0
  13. package/src/container/MillasApp.js +23 -280
  14. package/src/container/MillasConfig.js +163 -0
  15. package/src/controller/Controller.js +79 -300
  16. package/src/core/auth.js +9 -0
  17. package/src/core/db.js +8 -0
  18. package/src/core/foundation.js +67 -0
  19. package/src/core/http.js +11 -0
  20. package/src/core/mail.js +6 -0
  21. package/src/core/queue.js +7 -0
  22. package/src/core/validation.js +29 -0
  23. package/src/errors/ErrorRenderer.js +640 -0
  24. package/src/facades/Admin.js +49 -0
  25. package/src/facades/Auth.js +29 -0
  26. package/src/facades/Cache.js +28 -0
  27. package/src/facades/Database.js +43 -0
  28. package/src/facades/Events.js +25 -0
  29. package/src/facades/Facade.js +197 -0
  30. package/src/facades/Http.js +51 -0
  31. package/src/facades/Log.js +32 -0
  32. package/src/facades/Mail.js +35 -0
  33. package/src/facades/Queue.js +30 -0
  34. package/src/facades/Storage.js +25 -0
  35. package/src/facades/Url.js +53 -0
  36. package/src/http/HttpClient.js +673 -0
  37. package/src/http/MillasRequest.js +253 -0
  38. package/src/http/MillasResponse.js +196 -0
  39. package/src/http/RequestContext.js +176 -0
  40. package/src/http/ResponseDispatcher.js +51 -0
  41. package/src/http/UrlGenerator.js +375 -0
  42. package/src/http/WelcomePage.js +273 -0
  43. package/src/http/adapters/ExpressAdapter.js +315 -0
  44. package/src/http/adapters/HttpAdapter.js +168 -0
  45. package/src/http/adapters/index.js +9 -0
  46. package/src/http/helpers.js +164 -0
  47. package/src/http/index.js +13 -0
  48. package/src/index.js +5 -91
  49. package/src/logger/formatters/PrettyFormatter.js +15 -5
  50. package/src/logger/internal.js +76 -0
  51. package/src/logger/patchConsole.js +145 -0
  52. package/src/middleware/CorsMiddleware.js +22 -30
  53. package/src/middleware/LogMiddleware.js +27 -59
  54. package/src/middleware/Middleware.js +24 -15
  55. package/src/middleware/MiddlewarePipeline.js +30 -67
  56. package/src/middleware/MiddlewareRegistry.js +106 -0
  57. package/src/middleware/ThrottleMiddleware.js +22 -26
  58. package/src/orm/fields/index.js +124 -56
  59. package/src/orm/migration/ModelInspector.js +339 -336
  60. package/src/orm/model/Model.js +96 -6
  61. package/src/orm/query/QueryBuilder.js +141 -3
  62. package/src/providers/AuthServiceProvider.js +9 -5
  63. package/src/providers/CacheStorageServiceProvider.js +3 -1
  64. package/src/providers/EventServiceProvider.js +2 -1
  65. package/src/providers/LogServiceProvider.js +88 -17
  66. package/src/providers/MailServiceProvider.js +3 -2
  67. package/src/providers/ProviderRegistry.js +14 -1
  68. package/src/providers/QueueServiceProvider.js +3 -2
  69. package/src/providers/ServiceProvider.js +40 -8
  70. package/src/router/Router.js +121 -222
  71. package/src/scaffold/maker.js +24 -59
  72. package/src/scaffold/templates.js +21 -19
  73. package/src/validation/BaseValidator.js +193 -0
  74. package/src/validation/Validator.js +680 -0
package/src/index.js CHANGED
@@ -1,94 +1,8 @@
1
1
  'use strict';
2
2
 
3
- // ── HTTP Layer ────────────────────────────────────────────────────
4
- const Controller = require('./controller/Controller');
5
- const Middleware = require('./middleware/Middleware');
6
- const MiddlewarePipeline = require('./middleware/MiddlewarePipeline');
7
- const CorsMiddleware = require('./middleware/CorsMiddleware');
8
- const ThrottleMiddleware = require('./middleware/ThrottleMiddleware');
9
- const LogMiddleware = require('./middleware/LogMiddleware');
10
- const HttpError = require('./errors/HttpError');
3
+ const Millas = require('./container/MillasApp');
11
4
 
12
- // ── DI Container ─────────────────────────────────────────────────
13
- const Container = require('./container/Container');
14
- const Application = require('./container/Application');
15
- const MillasApp = require('./container/MillasApp');
16
- const ServiceProvider = require('./providers/ServiceProvider');
17
- const ProviderRegistry = require('./providers/ProviderRegistry');
18
-
19
- // ── ORM ───────────────────────────────────────────────────────────
20
- const { Model, fields, QueryBuilder, DatabaseManager,
21
- SchemaBuilder, MigrationRunner, ModelInspector } = require('./orm');
22
- const DatabaseServiceProvider = require('./providers/DatabaseServiceProvider');
23
-
24
- // ── Auth ──────────────────────────────────────────────────────────
25
- const Auth = require('./auth/Auth');
26
- const Hasher = require('./auth/Hasher');
27
- const JwtDriver = require('./auth/JwtDriver');
28
- const AuthMiddleware = require('./auth/AuthMiddleware');
29
- const RoleMiddleware = require('./auth/RoleMiddleware');
30
- const AuthController = require('./auth/AuthController');
31
- const AuthServiceProvider = require('./providers/AuthServiceProvider');
32
-
33
- // ── Mail ──────────────────────────────────────────────────────────
34
- const { Mail, MailMessage, TemplateEngine,
35
- SmtpDriver, SendGridDriver, MailgunDriver, LogDriver } = require('./mail');
36
- const MailServiceProvider = require('./providers/MailServiceProvider');
37
-
38
- // ── Queue ─────────────────────────────────────────────────────────
39
- const Queue = require('./queue/Queue');
40
- const Job = require('./queue/Job');
41
- const QueueWorker = require('./queue/workers/QueueWorker');
42
- const { dispatch } = require('./queue/Queue');
43
- const QueueServiceProvider = require('./providers/QueueServiceProvider');
44
-
45
- // ── Events ────────────────────────────────────────────────────────
46
- const EventEmitter = require('./events/EventEmitter');
47
- const Event = require('./events/Event');
48
- const Listener = require('./events/Listener');
49
- const { emit } = require('./events/EventEmitter');
50
- const EventServiceProvider = require('./providers/EventServiceProvider');
51
-
52
- // ── Cache ─────────────────────────────────────────────────────────
53
- const Cache = require('./cache/Cache');
54
- const MemoryDriver = require('./cache/drivers/MemoryDriver');
55
- const FileDriver = require('./cache/drivers/FileDriver');
56
- const NullDriver = require('./cache/drivers/NullDriver');
57
- const { CacheServiceProvider, StorageServiceProvider } = require('./providers/CacheStorageServiceProvider');
58
-
59
- // ── Storage ───────────────────────────────────────────────────────
60
- const Storage = require('./storage/Storage');
61
- const LocalDriver = require('./storage/drivers/LocalDriver');
62
-
63
- module.exports = {
64
- // HTTP
65
- Controller, Middleware, MiddlewarePipeline,
66
- CorsMiddleware, ThrottleMiddleware, LogMiddleware, HttpError,
67
- // DI
68
- Container, Application, MillasApp, ServiceProvider, ProviderRegistry,
69
- // ORM
70
- Model, fields, QueryBuilder, DatabaseManager, SchemaBuilder,
71
- MigrationRunner, ModelInspector, DatabaseServiceProvider,
72
- // Auth
73
- Auth, Hasher, JwtDriver, AuthMiddleware, RoleMiddleware,
74
- AuthController, AuthServiceProvider,
75
- // Mail
76
- Mail, MailMessage, TemplateEngine,
77
- SmtpDriver, SendGridDriver, MailgunDriver, LogDriver, MailServiceProvider,
78
- // Queue
79
- Queue, Job, QueueWorker, dispatch, QueueServiceProvider,
80
- // Events
81
- EventEmitter, Event, Listener, emit, EventServiceProvider,
82
- // Cache
83
- Cache, MemoryDriver, FileDriver, NullDriver, CacheServiceProvider,
84
- // Storage
85
- Storage, LocalDriver, StorageServiceProvider,
86
- };
87
-
88
- // ── Admin ─────────────────────────────────────────────────────────
89
- const { Admin, AdminResource, AdminField, AdminFilter } = require('./admin');
90
- const AdminServiceProvider = require('./providers/AdminServiceProvider');
91
-
92
- Object.assign(module.exports, {
93
- Admin, AdminResource, AdminField, AdminFilter, AdminServiceProvider,
94
- });
5
+ /**
6
+ * @module millas
7
+ */
8
+ module.exports = { Millas };
@@ -56,19 +56,29 @@ class PrettyFormatter {
56
56
  parts.push(`${b}${tagStr}${r}`);
57
57
  }
58
58
 
59
- // Message
60
- parts.push(`${c}${message}${r}`);
59
+ // Message (handle multi-line)
60
+ const lines = message.split('\n');
61
+ parts.push(`${c}${lines[0]}${r}`);
62
+
63
+ let output = parts.join(' ');
64
+
65
+ // Continuation lines (aligned with first line)
66
+ if (lines.length > 1) {
67
+ const prefix = parts.slice(0, -1).map(p => p.replace(/\x1b\[[0-9;]*m/g, '')).join(' ');
68
+ const indent = ' '.repeat(prefix.length + 2);
69
+ for (let i = 1; i < lines.length; i++) {
70
+ output += `\n${indent}${c}${lines[i]}${r}`;
71
+ }
72
+ }
61
73
 
62
74
  // Context object
63
75
  if (context !== undefined && context !== null) {
64
76
  const ctx = typeof context === 'object'
65
77
  ? JSON.stringify(context, null, 0)
66
78
  : String(context);
67
- parts.push(`${d}${ctx}${r}`);
79
+ output += ` ${d}${ctx}${r}`;
68
80
  }
69
81
 
70
- let output = parts.join(' ');
71
-
72
82
  // Error stack
73
83
  if (error instanceof Error) {
74
84
  output += `\n${d}${error.stack || error.message}${r}`;
@@ -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.VERBOSE,
68
+ channel: new ConsoleChannel({
69
+ formatter: new PrettyFormatter({
70
+ colour: process.stdout.isTTY !== false,
71
+ }),
72
+ minLevel: LEVELS.VERBOSE,
73
+ }),
74
+ });
75
+
76
+ module.exports = MillasLog;
@@ -0,0 +1,145 @@
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
+
29
+ // Save originals — restore() puts these back
30
+ const originals = {
31
+ log: console.log.bind(console),
32
+ info: console.info.bind(console),
33
+ warn: console.warn.bind(console),
34
+ error: console.error.bind(console),
35
+ debug: console.debug.bind(console),
36
+ trace: console.trace.bind(console),
37
+ dir: console.dir.bind(console),
38
+ };
39
+
40
+ // Build a dispatcher for a given level
41
+ function make(level) {
42
+ return function (...args) {
43
+ const {message, context, error} = parse(args);
44
+ Log._emit({
45
+ level,
46
+ tag: defaultTag,
47
+ message: message || '',
48
+ context,
49
+ error,
50
+ timestamp: new Date().toISOString(),
51
+ pid: process.pid,
52
+ });
53
+ return true
54
+ };
55
+ }
56
+
57
+ console.log = make(LEVELS.INFO);
58
+ console.info = make(LEVELS.INFO);
59
+ console.warn = make(LEVELS.WARN);
60
+ console.error = make(LEVELS.ERROR);
61
+ console.debug = make(LEVELS.DEBUG);
62
+ console.trace = make(LEVELS.VERBOSE);
63
+ console.dir = (obj) => Log._emit({
64
+ level: LEVELS.DEBUG,
65
+ tag: defaultTag,
66
+ message: '',
67
+ context: obj,
68
+ error: undefined,
69
+ timestamp: new Date().toISOString(),
70
+ pid: process.pid
71
+ });
72
+
73
+ return function restore() {
74
+ Object.assign(console, originals);
75
+ };
76
+ }
77
+
78
+ // ── Argument parser ───────────────────────────────────────────────────────────
79
+ //
80
+ // Handles the main shapes people pass to console.*:
81
+ //
82
+ // console.log('message')
83
+ // console.log('message', { ctx })
84
+ // console.log('message', error)
85
+ // console.error(error)
86
+ // console.log({ obj })
87
+ // console.log('x:', 42) → message: 'x: 42'
88
+ // console.log('a', 'b', 'c') → message: 'a b c'
89
+
90
+ function parse(args) {
91
+ if (args.length === 0) {
92
+ return {message: '', context: undefined, error: undefined};
93
+ }
94
+
95
+ if (args.length === 1) {
96
+ const a = args[0];
97
+ if (a instanceof Error) return {message: a.message, context: undefined, error: a};
98
+ if (typeof a === 'object' && a !== null) return {message: '', context: a, error: undefined};
99
+ return {message: String(a), context: undefined, error: undefined};
100
+ }
101
+
102
+ const [first, ...rest] = args;
103
+
104
+ // First arg is an Error
105
+ if (first instanceof Error) {
106
+ return {message: first.message, context: rest.length ? rest : undefined, error: first};
107
+ }
108
+
109
+ // First arg is a string message
110
+ if (typeof first === 'string') {
111
+ // Single extra arg
112
+ if (rest.length === 1) {
113
+ const r = rest[0];
114
+ if (r instanceof Error) return {message: first, context: undefined, error: r};
115
+ if (typeof r === 'object' && r !== null) return {message: first, context: r, error: undefined};
116
+ // Scalar extra: append to message (console.log('count:', 42))
117
+ return {message: first + ' ' + String(r), context: undefined, error: undefined};
118
+ }
119
+
120
+ // Multiple extra args — find a trailing Error, collect the rest as context
121
+ const lastArg = rest[rest.length - 1];
122
+ if (lastArg instanceof Error) {
123
+ const ctx = rest.slice(0, -1);
124
+ return {message: first, context: ctx.length ? ctx : undefined, error: lastArg};
125
+ }
126
+
127
+ // All strings/scalars — join into message
128
+ if (rest.every(r => typeof r !== 'object' || r === null)) {
129
+ return {message: [first, ...rest].map(String).join(' '), context: undefined, error: undefined};
130
+ }
131
+
132
+ // Mixed — put extras in context
133
+ return {message: first, context: rest, error: undefined};
134
+ }
135
+
136
+ // First arg is an object
137
+ if (typeof first === 'object' && first !== null) {
138
+ return {message: '', context: first, error: undefined};
139
+ }
140
+
141
+ // Fallback — join everything as a string
142
+ return {message: args.map(String).join(' '), context: undefined, error: undefined};
143
+ }
144
+
145
+ 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;