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.
- package/package.json +6 -5
- package/src/auth/Auth.js +13 -8
- package/src/auth/AuthController.js +45 -134
- package/src/auth/AuthMiddleware.js +12 -23
- package/src/auth/AuthUser.js +98 -0
- package/src/auth/RoleMiddleware.js +7 -17
- package/src/cli.js +1 -1
- package/src/commands/migrate.js +46 -31
- package/src/commands/serve.js +238 -38
- package/src/container/AppInitializer.js +158 -0
- package/src/container/Application.js +288 -183
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +23 -280
- package/src/container/MillasConfig.js +163 -0
- package/src/controller/Controller.js +79 -300
- package/src/core/auth.js +9 -0
- package/src/core/db.js +8 -0
- package/src/core/foundation.js +67 -0
- package/src/core/http.js +11 -0
- package/src/core/mail.js +6 -0
- package/src/core/queue.js +7 -0
- package/src/core/validation.js +29 -0
- package/src/errors/ErrorRenderer.js +640 -0
- package/src/facades/Admin.js +49 -0
- package/src/facades/Auth.js +29 -0
- package/src/facades/Cache.js +28 -0
- package/src/facades/Database.js +43 -0
- package/src/facades/Events.js +25 -0
- package/src/facades/Facade.js +197 -0
- package/src/facades/Http.js +51 -0
- package/src/facades/Log.js +32 -0
- package/src/facades/Mail.js +35 -0
- package/src/facades/Queue.js +30 -0
- package/src/facades/Storage.js +25 -0
- package/src/facades/Url.js +53 -0
- package/src/http/HttpClient.js +673 -0
- package/src/http/MillasRequest.js +253 -0
- package/src/http/MillasResponse.js +196 -0
- package/src/http/RequestContext.js +176 -0
- package/src/http/ResponseDispatcher.js +51 -0
- package/src/http/UrlGenerator.js +375 -0
- package/src/http/WelcomePage.js +273 -0
- package/src/http/adapters/ExpressAdapter.js +315 -0
- package/src/http/adapters/HttpAdapter.js +168 -0
- package/src/http/adapters/index.js +9 -0
- package/src/http/helpers.js +164 -0
- package/src/http/index.js +13 -0
- package/src/index.js +5 -91
- package/src/logger/formatters/PrettyFormatter.js +15 -5
- package/src/logger/internal.js +76 -0
- package/src/logger/patchConsole.js +145 -0
- package/src/middleware/CorsMiddleware.js +22 -30
- package/src/middleware/LogMiddleware.js +27 -59
- package/src/middleware/Middleware.js +24 -15
- package/src/middleware/MiddlewarePipeline.js +30 -67
- package/src/middleware/MiddlewareRegistry.js +106 -0
- package/src/middleware/ThrottleMiddleware.js +22 -26
- package/src/orm/fields/index.js +124 -56
- package/src/orm/migration/ModelInspector.js +339 -336
- package/src/orm/model/Model.js +96 -6
- package/src/orm/query/QueryBuilder.js +141 -3
- package/src/providers/AuthServiceProvider.js +9 -5
- package/src/providers/CacheStorageServiceProvider.js +3 -1
- package/src/providers/EventServiceProvider.js +2 -1
- package/src/providers/LogServiceProvider.js +88 -17
- package/src/providers/MailServiceProvider.js +3 -2
- package/src/providers/ProviderRegistry.js +14 -1
- package/src/providers/QueueServiceProvider.js +3 -2
- package/src/providers/ServiceProvider.js +40 -8
- package/src/router/Router.js +121 -222
- package/src/scaffold/maker.js +24 -59
- package/src/scaffold/templates.js +21 -19
- package/src/validation/BaseValidator.js +193 -0
- 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;
|