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
@@ -0,0 +1,253 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * MillasRequest
5
+ *
6
+ * Wraps an Express request and exposes a clean, framework-level API.
7
+ * Developers never touch the raw Express req — they use this instead.
8
+ *
9
+ * The raw Express req is accessible via req.raw for escape hatches,
10
+ * but should never be needed in normal application code.
11
+ *
12
+ * Usage in route handlers:
13
+ * Route.get('/users/:id', async (req) => {
14
+ * const id = req.param('id');
15
+ * const page = req.input('page', 1);
16
+ * const user = await User.findOrFail(id);
17
+ * return jsonify(user);
18
+ * });
19
+ */
20
+ class MillasRequest {
21
+ /**
22
+ * @param {import('express').Request} expressReq
23
+ */
24
+ constructor(expressReq) {
25
+ /** @private — access via req.raw if absolutely necessary */
26
+ this._req = expressReq;
27
+
28
+ // Proxy commonly-used scalar properties for convenience
29
+ this.method = expressReq.method;
30
+ this.path = expressReq.path;
31
+ this.url = expressReq.url;
32
+ this.baseUrl = expressReq.baseUrl;
33
+ this.originalUrl = expressReq.originalUrl;
34
+ }
35
+
36
+ // ─── Input ─────────────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Read a value from body, query, or route params — in that priority order.
40
+ * Returns defaultValue if not present.
41
+ *
42
+ * req.input('email')
43
+ * req.input('page', 1)
44
+ */
45
+ input(key, defaultValue = null) {
46
+ if (key === undefined) return this.all();
47
+ const r = this._req;
48
+ const v =
49
+ (r.body && r.body[key] !== undefined ? r.body[key] : undefined) ??
50
+ (r.query && r.query[key] !== undefined ? r.query[key] : undefined) ??
51
+ (r.params && r.params[key] !== undefined ? r.params[key] : undefined);
52
+ return v !== undefined ? v : defaultValue;
53
+ }
54
+
55
+ /**
56
+ * Read a route parameter.
57
+ * req.param('id')
58
+ */
59
+ param(key, defaultValue = null) {
60
+ return this._req.params?.[key] ?? defaultValue;
61
+ }
62
+
63
+ /**
64
+ * Read a query string value.
65
+ * req.query('page', 1)
66
+ */
67
+ query(key, defaultValue = null) {
68
+ if (key === undefined) return this._req.query || {};
69
+ return this._req.query?.[key] ?? defaultValue;
70
+ }
71
+
72
+ /**
73
+ * Read from the request body only.
74
+ * req.body('email')
75
+ */
76
+ body(key, defaultValue = null) {
77
+ if (key === undefined) return this._req.body || {};
78
+ return this._req.body?.[key] ?? defaultValue;
79
+ }
80
+
81
+ /**
82
+ * Merge body + query + params into one flat object.
83
+ */
84
+ all() {
85
+ return {
86
+ ...this._req.params,
87
+ ...this._req.query,
88
+ ...this._req.body,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Return only the specified keys from the merged input.
94
+ * req.only(['name', 'email'])
95
+ */
96
+ only(keys = []) {
97
+ const all = this.all();
98
+ return keys.reduce((acc, k) => {
99
+ if (k in all) acc[k] = all[k];
100
+ return acc;
101
+ }, {});
102
+ }
103
+
104
+ /**
105
+ * Return all input except the specified keys.
106
+ * req.except(['password', 'token'])
107
+ */
108
+ except(keys = []) {
109
+ const all = this.all();
110
+ return Object.fromEntries(
111
+ Object.entries(all).filter(([k]) => !keys.includes(k))
112
+ );
113
+ }
114
+
115
+ // ─── Headers ────────────────────────────────────────────────────────────────
116
+
117
+ /**
118
+ * Read a request header (case-insensitive).
119
+ * req.header('Authorization')
120
+ * req.header('content-type')
121
+ */
122
+ header(name, defaultValue = null) {
123
+ if (name === undefined) return this._req.headers || {};
124
+ return this._req.headers?.[name.toLowerCase()] ?? defaultValue;
125
+ }
126
+
127
+ /**
128
+ * All request headers as a plain object.
129
+ */
130
+ get headers() {
131
+ return this._req.headers || {};
132
+ }
133
+
134
+ // ─── Cookies ────────────────────────────────────────────────────────────────
135
+
136
+ /**
137
+ * Read a cookie value.
138
+ * req.cookie('session_id')
139
+ */
140
+ cookie(name, defaultValue = null) {
141
+ return this._req.cookies?.[name] ?? defaultValue;
142
+ }
143
+
144
+ // ─── Files ──────────────────────────────────────────────────────────────────
145
+
146
+ /**
147
+ * Get an uploaded file (requires multer or similar middleware upstream).
148
+ * req.file('avatar')
149
+ */
150
+ file(name) {
151
+ if (this._req.files && this._req.files[name]) return this._req.files[name];
152
+ if (this._req.file && this._req.file.fieldname === name) return this._req.file;
153
+ return null;
154
+ }
155
+
156
+ /**
157
+ * All uploaded files.
158
+ */
159
+ get files() {
160
+ return this._req.files || {};
161
+ }
162
+
163
+ // ─── User ───────────────────────────────────────────────────────────────────
164
+
165
+ /**
166
+ * The authenticated user (set by AuthMiddleware).
167
+ */
168
+ get user() {
169
+ return this._req.user ?? null;
170
+ }
171
+
172
+ /** Set the authenticated user (used by AuthMiddleware). */
173
+ set user(value) {
174
+ this._req.user = value;
175
+ }
176
+
177
+ // ─── Content negotiation ────────────────────────────────────────────────────
178
+
179
+ /**
180
+ * Returns true if the request expects a JSON response.
181
+ */
182
+ wantsJson() {
183
+ const accept = this.header('accept', '');
184
+ return accept.includes('application/json') || accept.includes('*/*');
185
+ }
186
+
187
+ /**
188
+ * Returns true if the request body is JSON.
189
+ */
190
+ isJson() {
191
+ const ct = this.header('content-type', '');
192
+ return ct.includes('application/json');
193
+ }
194
+
195
+ /**
196
+ * Returns true if this was an XMLHttpRequest / fetch call.
197
+ */
198
+ isAjax() {
199
+ return this.header('x-requested-with', '').toLowerCase() === 'xmlhttprequest';
200
+ }
201
+
202
+ // ─── Network ────────────────────────────────────────────────────────────────
203
+
204
+ /**
205
+ * Client IP address.
206
+ */
207
+ get ip() {
208
+ return this._req.ip || this._req.connection?.remoteAddress || null;
209
+ }
210
+
211
+ /**
212
+ * Request hostname (from Host header).
213
+ */
214
+ get hostname() {
215
+ return this._req.hostname || '';
216
+ }
217
+
218
+ /**
219
+ * Whether the request is HTTPS.
220
+ */
221
+ get secure() {
222
+ return this._req.secure || false;
223
+ }
224
+
225
+ // ─── Validation ─────────────────────────────────────────────────────────────
226
+
227
+ /**
228
+ * Validate request input against rules. Throws 422 HttpError on failure.
229
+ * Returns the validated data on success.
230
+ *
231
+ * const data = await req.validate({
232
+ * name: 'required|string|min:2|max:100',
233
+ * email: 'required|email',
234
+ * age: 'optional|number|min:0',
235
+ * });
236
+ */
237
+ async validate(rules) {
238
+ const { Validator } = require('../validation/Validator');
239
+ return Validator.validate(this.all(), rules);
240
+ }
241
+
242
+ // ─── Escape hatch ────────────────────────────────────────────────────────────
243
+
244
+ /**
245
+ * The raw underlying Express request.
246
+ * Only use this when you genuinely need something MillasRequest doesn't expose.
247
+ */
248
+ get raw() {
249
+ return this._req;
250
+ }
251
+ }
252
+
253
+ module.exports = MillasRequest;
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * MillasResponse
5
+ *
6
+ * An immutable value object representing an HTTP response.
7
+ * Nothing is written to the socket until ResponseDispatcher.dispatch() is called.
8
+ *
9
+ * Developers never instantiate this directly — use the helper functions:
10
+ * jsonify(data, options)
11
+ * view('template', data, options)
12
+ * redirect('/path', options)
13
+ * text('Hello', options)
14
+ * file('/path/to/file')
15
+ * empty(204)
16
+ *
17
+ * Route handlers return a MillasResponse (or a plain value that gets
18
+ * auto-wrapped). Middleware can inspect or modify the response before
19
+ * it reaches the dispatcher.
20
+ *
21
+ * Fluent mutation — each method returns a NEW MillasResponse:
22
+ * return jsonify(user).status(201).header('X-Custom', 'value');
23
+ */
24
+ class MillasResponse {
25
+ /**
26
+ * @param {object} options
27
+ * @param {string} options.type — 'json' | 'html' | 'text' | 'redirect' | 'file' | 'empty' | 'stream'
28
+ * @param {*} options.body — response body
29
+ * @param {number} [options.status] — HTTP status code (default: 200)
30
+ * @param {object} [options.headers]— additional headers
31
+ * @param {object} [options.cookies]— cookies to set: { name: { value, options } }
32
+ */
33
+ constructor({ type, body, status = 200, headers = {}, cookies = {} } = {}) {
34
+ this._type = type;
35
+ this._body = body;
36
+ this._status = status;
37
+ this._headers = { ...headers };
38
+ this._cookies = { ...cookies };
39
+
40
+ // Make immutable after construction
41
+ Object.freeze(this._headers);
42
+ Object.freeze(this._cookies);
43
+ }
44
+
45
+ // ─── Accessors ──────────────────────────────────────────────────────────────
46
+
47
+ get type() { return this._type; }
48
+ get body() { return this._body; }
49
+ get statusCode() { return this._status; }
50
+ get headers() { return this._headers; }
51
+ get cookies() { return this._cookies; }
52
+
53
+ // ─── Fluent builders (return new instance each time) ─────────────────────
54
+
55
+ /**
56
+ * Set the HTTP status code.
57
+ * return jsonify(data).status(201)
58
+ */
59
+ status(code) {
60
+ return new MillasResponse({
61
+ type: this._type,
62
+ body: this._body,
63
+ status: code,
64
+ headers: this._headers,
65
+ cookies: this._cookies,
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Add or override a response header.
71
+ * return jsonify(data).header('X-Custom-Id', '123')
72
+ */
73
+ header(name, value) {
74
+ return new MillasResponse({
75
+ type: this._type,
76
+ body: this._body,
77
+ status: this._status,
78
+ headers: { ...this._headers, [name]: value },
79
+ cookies: this._cookies,
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Add multiple headers at once.
85
+ * return jsonify(data).withHeaders({ 'X-A': '1', 'X-B': '2' })
86
+ */
87
+ withHeaders(map = {}) {
88
+ return new MillasResponse({
89
+ type: this._type,
90
+ body: this._body,
91
+ status: this._status,
92
+ headers: { ...this._headers, ...map },
93
+ cookies: this._cookies,
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Set a cookie on the response.
99
+ *
100
+ * return jsonify(data).cookie('token', value, { httpOnly: true, maxAge: 3600 })
101
+ */
102
+ cookie(name, value, options = {}) {
103
+ return new MillasResponse({
104
+ type: this._type,
105
+ body: this._body,
106
+ status: this._status,
107
+ headers: this._headers,
108
+ cookies: { ...this._cookies, [name]: { value, options } },
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Clear a cookie.
114
+ * return jsonify(data).clearCookie('session')
115
+ */
116
+ clearCookie(name, options = {}) {
117
+ return this.cookie(name, '', { ...options, maxAge: 0, expires: new Date(0) });
118
+ }
119
+
120
+ // ─── Static factories ─────────────────────────────────────────────────────
121
+
122
+ /** JSON response */
123
+ static json(data, { status = 200, headers = {} } = {}) {
124
+ return new MillasResponse({
125
+ type: 'json',
126
+ body: data,
127
+ status,
128
+ headers: { 'Content-Type': 'application/json', ...headers },
129
+ });
130
+ }
131
+
132
+ /** HTML response */
133
+ static html(html, { status = 200, headers = {} } = {}) {
134
+ return new MillasResponse({
135
+ type: 'html',
136
+ body: html,
137
+ status,
138
+ headers: { 'Content-Type': 'text/html; charset=utf-8', ...headers },
139
+ });
140
+ }
141
+
142
+ /** Plain text response */
143
+ static text(text, { status = 200, headers = {} } = {}) {
144
+ return new MillasResponse({
145
+ type: 'text',
146
+ body: String(text),
147
+ status,
148
+ headers: { 'Content-Type': 'text/plain; charset=utf-8', ...headers },
149
+ });
150
+ }
151
+
152
+ /** Redirect response */
153
+ static redirect(url, { status = 302 } = {}) {
154
+ return new MillasResponse({
155
+ type: 'redirect',
156
+ body: url,
157
+ status,
158
+ headers: { Location: url },
159
+ });
160
+ }
161
+
162
+ /** File download / serve response */
163
+ static file(filePath, { download = false, name = null, headers = {} } = {}) {
164
+ return new MillasResponse({
165
+ type: 'file',
166
+ body: { path: filePath, download, name },
167
+ status: 200,
168
+ headers,
169
+ });
170
+ }
171
+
172
+ /** Empty response (204 No Content by default) */
173
+ static empty(status = 204) {
174
+ return new MillasResponse({ type: 'empty', body: null, status });
175
+ }
176
+
177
+ /** Rendered view/template response */
178
+ static view(template, data = {}, { status = 200, headers = {} } = {}) {
179
+ return new MillasResponse({
180
+ type: 'view',
181
+ body: { template, data },
182
+ status,
183
+ headers: { 'Content-Type': 'text/html; charset=utf-8', ...headers },
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Check if something is a MillasResponse instance.
189
+ * Used by the router to distinguish from plain return values.
190
+ */
191
+ static isResponse(value) {
192
+ return value instanceof MillasResponse;
193
+ }
194
+ }
195
+
196
+ module.exports = MillasResponse;
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * RequestContext
5
+ *
6
+ * The single argument passed to every Millas route handler and middleware.
7
+ * Developers destructure exactly what they need — nothing else is in scope.
8
+ *
9
+ * Inspired by FastAPI's parameter injection. Each key maps to a specific
10
+ * part of the request — no more digging through a monolithic req object.
11
+ *
12
+ * ── Usage ───────────────────────────────────────────────────────────────────
13
+ *
14
+ * // Route params → params
15
+ * Route.get('/users/:id', ({ params }) => User.findOrFail(params.id))
16
+ *
17
+ * // Query string → query
18
+ * Route.get('/users', ({ query }) => User.paginate(query.page, query.per_page))
19
+ *
20
+ * // Request body → body (alias: json)
21
+ * Route.post('/users', async ({ body }) => {
22
+ * const user = await User.create(body);
23
+ * return jsonify(user, { status: 201 });
24
+ * })
25
+ *
26
+ * // Uploaded files → files
27
+ * Route.post('/upload', ({ files }) => {
28
+ * const avatar = files.avatar;
29
+ * return jsonify({ size: avatar.size });
30
+ * })
31
+ *
32
+ * // Authenticated user → user
33
+ * Route.get('/me', ({ user }) => jsonify(user))
34
+ *
35
+ * // Multiple at once — destructure only what you need
36
+ * Route.put('/users/:id', async ({ params, body, user }) => {
37
+ * if (user.id !== params.id) abort(403);
38
+ * return jsonify(await User.update(params.id, body));
39
+ * })
40
+ *
41
+ * // Inline validation on body
42
+ * Route.post('/posts', async ({ body }) => {
43
+ * const data = await body.validate({
44
+ * title: 'required|string|max:255',
45
+ * content: 'required|string',
46
+ * });
47
+ * return jsonify(await Post.create(data));
48
+ * })
49
+ *
50
+ * // DI container — for resolving services at request time
51
+ * Route.get('/stats', ({ container }) => {
52
+ * const cache = container.make('Cache');
53
+ * return cache.remember('stats', 60, () => Stats.compute());
54
+ * })
55
+ *
56
+ * // Full MillasRequest escape hatch — when you need something not covered
57
+ * Route.get('/raw', ({ req }) => {
58
+ * const ip = req.ip;
59
+ * return jsonify({ ip });
60
+ * })
61
+ *
62
+ * ── Context shape ────────────────────────────────────────────────────────────
63
+ *
64
+ * {
65
+ * params, // route parameters { id: '5' }
66
+ * query, // query string { page: '2', search: 'alice' }
67
+ * body, // request body { name: 'Alice', email: '...' } + .validate()
68
+ * json, // alias for body (same object)
69
+ * files, // uploaded files { avatar: File, resume: File }
70
+ * headers, // request headers { authorization: 'Bearer ...' }
71
+ * cookies, // cookies { session: 'abc123' }
72
+ * user, // authenticated user (set by AuthMiddleware)
73
+ * req, // full MillasRequest (escape hatch)
74
+ * container, // DI container container.make('Cache')
75
+ * }
76
+ */
77
+ class RequestContext {
78
+ /**
79
+ * @param {import('./MillasRequest')} millaReq
80
+ * @param {import('../container/Container')|null} container
81
+ */
82
+ constructor(millaReq, container = null) {
83
+ this._req = millaReq;
84
+ this._container = container;
85
+
86
+ // ── params ────────────────────────────────────────────────────────────────
87
+ // Route parameters — /users/:id → params.id
88
+ this.params = millaReq.raw.params || {};
89
+
90
+ // ── query ─────────────────────────────────────────────────────────────────
91
+ // Query string — ?page=2&search=alice → query.page, query.search
92
+ this.query = millaReq.raw.query || {};
93
+
94
+ // ── body / json ───────────────────────────────────────────────────────────
95
+ // Parsed request body (JSON, form data, etc.)
96
+ // body and json are the same object — use whichever reads better.
97
+ const rawBody = millaReq.raw.body || {};
98
+ this.body = this._buildBody(rawBody, millaReq);
99
+ this.json = this.body; // alias
100
+
101
+ // ── files ─────────────────────────────────────────────────────────────────
102
+ // Uploaded files (populated by multer or similar middleware)
103
+ this.files = millaReq.raw.files || {};
104
+
105
+ // ── headers ───────────────────────────────────────────────────────────────
106
+ this.headers = millaReq.raw.headers || {};
107
+
108
+ // ── cookies ───────────────────────────────────────────────────────────────
109
+ this.cookies = millaReq.raw.cookies || {};
110
+
111
+ // ── user ──────────────────────────────────────────────────────────────────
112
+ // Authenticated user — set by AuthMiddleware via req.user
113
+ Object.defineProperty(this, 'user', {
114
+ get: () => millaReq.raw.user ?? null,
115
+ set: (v) => { millaReq.raw.user = v; },
116
+ enumerable: true,
117
+ });
118
+
119
+ // ── req ───────────────────────────────────────────────────────────────────
120
+ // Full MillasRequest — escape hatch for anything not covered above
121
+ this.req = millaReq;
122
+
123
+ // ── container ─────────────────────────────────────────────────────────────
124
+ // DI container — resolve services at request time
125
+ this.container = container;
126
+ }
127
+
128
+ // ─── Body with validation ──────────────────────────────────────────────────
129
+
130
+ /**
131
+ * Build the body object with an attached .validate() method.
132
+ * This keeps validation ergonomic and co-located with the body itself:
133
+ *
134
+ * const data = await body.validate({
135
+ * name: 'required|string|max:100',
136
+ * email: 'required|email',
137
+ * });
138
+ */
139
+ _buildBody(rawBody, millaReq) {
140
+ // Start with the raw body data
141
+ const body = Object.assign(Object.create(null), rawBody);
142
+
143
+ // Attach validate() directly on the body object
144
+ Object.defineProperty(body, 'validate', {
145
+ enumerable: false, // doesn't show up in Object.keys / JSON.stringify
146
+ value: async function validate(rules) {
147
+ const { Validator } = require('../validation/Validator');
148
+ return Validator.validate(rawBody, rules);
149
+ },
150
+ });
151
+
152
+ // Attach only() and except() helpers too
153
+ Object.defineProperty(body, 'only', {
154
+ enumerable: false,
155
+ value: function only(keys) {
156
+ return keys.reduce((acc, k) => {
157
+ if (k in rawBody) acc[k] = rawBody[k];
158
+ return acc;
159
+ }, {});
160
+ },
161
+ });
162
+
163
+ Object.defineProperty(body, 'except', {
164
+ enumerable: false,
165
+ value: function except(keys) {
166
+ return Object.fromEntries(
167
+ Object.entries(rawBody).filter(([k]) => !keys.includes(k))
168
+ );
169
+ },
170
+ });
171
+
172
+ return body;
173
+ }
174
+ }
175
+
176
+ module.exports = RequestContext;
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const MillasResponse = require('./MillasResponse');
4
+
5
+ /**
6
+ * ResponseDispatcher
7
+ *
8
+ * Kernel-side utility — handles auto-wrapping plain return values into
9
+ * MillasResponse objects.
10
+ *
11
+ * Actual dispatch to the HTTP engine (setting headers, writing the body)
12
+ * lives in HttpAdapter.dispatch() — that is the only place HTTP-engine
13
+ * APIs are called.
14
+ *
15
+ * This file has zero imports of Express or any HTTP engine.
16
+ */
17
+ class ResponseDispatcher {
18
+
19
+ /**
20
+ * Auto-wrap a plain JS return value into a MillasResponse.
21
+ *
22
+ * Called when a route handler returns something that is NOT already
23
+ * a MillasResponse — e.g. a plain object, string, number, or array.
24
+ *
25
+ * @param {*} value
26
+ * @returns {MillasResponse}
27
+ */
28
+ static autoWrap(value) {
29
+ if (MillasResponse.isResponse(value)) return value;
30
+
31
+ if (value instanceof Error) throw value;
32
+
33
+ if (typeof value === 'string') {
34
+ return value.trimStart().startsWith('<')
35
+ ? MillasResponse.html(value)
36
+ : MillasResponse.text(value);
37
+ }
38
+
39
+ if (
40
+ typeof value === 'object' ||
41
+ typeof value === 'number' ||
42
+ typeof value === 'boolean'
43
+ ) {
44
+ return MillasResponse.json(value);
45
+ }
46
+
47
+ return MillasResponse.text(String(value));
48
+ }
49
+ }
50
+
51
+ module.exports = ResponseDispatcher;