millas 0.2.11 → 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.
- package/package.json +17 -3
- package/src/auth/AuthController.js +42 -133
- package/src/auth/AuthMiddleware.js +12 -23
- package/src/auth/RoleMiddleware.js +7 -17
- package/src/commands/migrate.js +46 -31
- package/src/commands/serve.js +266 -37
- package/src/container/Application.js +88 -8
- package/src/controller/Controller.js +79 -300
- package/src/errors/ErrorRenderer.js +640 -0
- package/src/facades/Admin.js +49 -0
- package/src/facades/Auth.js +46 -0
- package/src/facades/Cache.js +17 -0
- package/src/facades/Database.js +43 -0
- package/src/facades/Events.js +24 -0
- package/src/facades/Http.js +54 -0
- package/src/facades/Log.js +56 -0
- package/src/facades/Mail.js +40 -0
- package/src/facades/Queue.js +23 -0
- package/src/facades/Storage.js +17 -0
- package/src/facades/Validation.js +69 -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 +144 -0
- package/src/http/helpers.js +164 -0
- package/src/http/index.js +13 -0
- package/src/index.js +55 -2
- package/src/logger/internal.js +76 -0
- package/src/logger/patchConsole.js +135 -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 +126 -0
- package/src/middleware/ThrottleMiddleware.js +22 -26
- package/src/orm/fields/index.js +124 -56
- package/src/orm/migration/ModelInspector.js +7 -3
- package/src/orm/model/Model.js +96 -6
- package/src/orm/query/QueryBuilder.js +141 -3
- package/src/providers/LogServiceProvider.js +88 -18
- package/src/providers/ProviderRegistry.js +14 -1
- package/src/providers/ServiceProvider.js +40 -8
- package/src/router/Router.js +155 -223
- package/src/scaffold/maker.js +24 -59
- package/src/scaffold/templates.js +13 -12
- package/src/validation/BaseValidator.js +193 -0
- package/src/validation/Validator.js +680 -0
|
@@ -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,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MillasResponse = require('./MillasResponse');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ResponseDispatcher
|
|
7
|
+
*
|
|
8
|
+
* The only place in the entire framework where Express res methods are called.
|
|
9
|
+
* Takes a MillasResponse value object and drives the Express response.
|
|
10
|
+
*
|
|
11
|
+
* This is an internal kernel component — developers never call this directly.
|
|
12
|
+
* The Router calls it after the handler (and middleware pipeline) resolves.
|
|
13
|
+
*/
|
|
14
|
+
class ResponseDispatcher {
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Dispatch a MillasResponse to the Express res.
|
|
18
|
+
*
|
|
19
|
+
* @param {MillasResponse} response
|
|
20
|
+
* @param {import('express').Response} expressRes
|
|
21
|
+
*/
|
|
22
|
+
static dispatch(response, expressRes) {
|
|
23
|
+
if (!response || !MillasResponse.isResponse(response)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'[ResponseDispatcher] Expected a MillasResponse instance. ' +
|
|
26
|
+
'Got: ' + typeof response
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Status ──────────────────────────────────────────────────────────────
|
|
31
|
+
expressRes.status(response.statusCode);
|
|
32
|
+
|
|
33
|
+
// ── CORS passthrough headers ─────────────────────────────────────────────
|
|
34
|
+
// CorsMiddleware stores headers on req._corsHeaders when it calls next()
|
|
35
|
+
// (i.e. non-preflight requests). Apply them here so every response carries
|
|
36
|
+
// the CORS headers regardless of what the route handler returned.
|
|
37
|
+
const corsHeaders = expressRes.req?._corsHeaders;
|
|
38
|
+
if (corsHeaders) {
|
|
39
|
+
for (const [name, value] of Object.entries(corsHeaders)) {
|
|
40
|
+
expressRes.setHeader(name, value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Response headers ─────────────────────────────────────────────────────
|
|
45
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
46
|
+
expressRes.setHeader(name, value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Cookies ─────────────────────────────────────────────────────────────
|
|
50
|
+
for (const [name, { value, options }] of Object.entries(response.cookies)) {
|
|
51
|
+
if (options.maxAge === 0 || options.expires?.getTime() === 0) {
|
|
52
|
+
expressRes.clearCookie(name, options);
|
|
53
|
+
} else {
|
|
54
|
+
expressRes.cookie(name, value, options);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Body ─────────────────────────────────────────────────────────────────
|
|
59
|
+
const { type, body } = response;
|
|
60
|
+
|
|
61
|
+
switch (type) {
|
|
62
|
+
|
|
63
|
+
case 'json':
|
|
64
|
+
// Let Express handle JSON serialisation and Content-Type
|
|
65
|
+
return expressRes.json(body);
|
|
66
|
+
|
|
67
|
+
case 'html':
|
|
68
|
+
return expressRes.send(body);
|
|
69
|
+
|
|
70
|
+
case 'text':
|
|
71
|
+
return expressRes.send(body);
|
|
72
|
+
|
|
73
|
+
case 'redirect':
|
|
74
|
+
return expressRes.redirect(response.statusCode, body);
|
|
75
|
+
|
|
76
|
+
case 'empty':
|
|
77
|
+
return expressRes.end();
|
|
78
|
+
|
|
79
|
+
case 'file': {
|
|
80
|
+
const { path: filePath, download, name: fileName } = body;
|
|
81
|
+
if (download) {
|
|
82
|
+
return expressRes.download(filePath, fileName || require('path').basename(filePath));
|
|
83
|
+
}
|
|
84
|
+
return expressRes.sendFile(require('path').resolve(filePath));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case 'view': {
|
|
88
|
+
// Nunjucks (or whatever template engine is wired) renders the template.
|
|
89
|
+
// expressRes.render() is configured by the framework's view engine setup.
|
|
90
|
+
const { template, data } = body;
|
|
91
|
+
return expressRes.render(template, data);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case 'stream': {
|
|
95
|
+
// body is a readable stream
|
|
96
|
+
if (body && typeof body.pipe === 'function') {
|
|
97
|
+
body.pipe(expressRes);
|
|
98
|
+
} else {
|
|
99
|
+
expressRes.end();
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
default:
|
|
105
|
+
// Unknown type — try to send as-is
|
|
106
|
+
return expressRes.send(body);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Auto-wrap a plain return value into a MillasResponse.
|
|
112
|
+
*
|
|
113
|
+
* Called when a handler returns something that is NOT a MillasResponse —
|
|
114
|
+
* e.g. a plain object, string, number, or array.
|
|
115
|
+
*
|
|
116
|
+
* @param {*} value
|
|
117
|
+
* @returns {MillasResponse}
|
|
118
|
+
*/
|
|
119
|
+
static autoWrap(value) {
|
|
120
|
+
if (MillasResponse.isResponse(value)) return value;
|
|
121
|
+
|
|
122
|
+
if (value instanceof Error) {
|
|
123
|
+
// Let the caller handle errors — don't wrap them into responses
|
|
124
|
+
throw value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof value === 'string') {
|
|
128
|
+
// Detect HTML (starts with < tag) vs plain text
|
|
129
|
+
const isHtml = value.trimStart().startsWith('<');
|
|
130
|
+
return isHtml
|
|
131
|
+
? MillasResponse.html(value)
|
|
132
|
+
: MillasResponse.text(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (typeof value === 'object' || typeof value === 'number' || typeof value === 'boolean') {
|
|
136
|
+
return MillasResponse.json(value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Fallback
|
|
140
|
+
return MillasResponse.text(String(value));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = ResponseDispatcher;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MillasResponse = require('./MillasResponse');
|
|
4
|
+
const HttpError = require('../errors/HttpError');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Millas HTTP Helper Functions
|
|
8
|
+
*
|
|
9
|
+
* These are the only response-building tools developers need.
|
|
10
|
+
* Import them at the top of any route/controller file.
|
|
11
|
+
*
|
|
12
|
+
* const { jsonify, view, redirect, text, abort } = require('millas');
|
|
13
|
+
*
|
|
14
|
+
* Every helper returns a MillasResponse instance. Nothing is written
|
|
15
|
+
* to the socket until the kernel's ResponseDispatcher processes it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return a JSON response.
|
|
20
|
+
*
|
|
21
|
+
* return jsonify(users)
|
|
22
|
+
* return jsonify(user, { status: 201 })
|
|
23
|
+
* return jsonify({ error: 'Not found' }, { status: 404 })
|
|
24
|
+
* return jsonify(data).header('X-Total', String(total))
|
|
25
|
+
* return jsonify(data).cookie('token', jwt, { httpOnly: true })
|
|
26
|
+
*
|
|
27
|
+
* @param {*} data
|
|
28
|
+
* @param {object} [options]
|
|
29
|
+
* @param {number} [options.status=200]
|
|
30
|
+
* @param {object} [options.headers={}]
|
|
31
|
+
* @returns {MillasResponse}
|
|
32
|
+
*/
|
|
33
|
+
function jsonify(data, options = {}) {
|
|
34
|
+
return MillasResponse.json(data, options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Return an HTML view (template) response.
|
|
39
|
+
*
|
|
40
|
+
* return view('users/index', { users })
|
|
41
|
+
* return view('emails/welcome', { user }, { status: 200 })
|
|
42
|
+
*
|
|
43
|
+
* @param {string} template — template path relative to views directory
|
|
44
|
+
* @param {object} [data={}] — data passed to the template
|
|
45
|
+
* @param {object} [options]
|
|
46
|
+
* @param {number} [options.status=200]
|
|
47
|
+
* @returns {MillasResponse}
|
|
48
|
+
*/
|
|
49
|
+
function view(template, data = {}, options = {}) {
|
|
50
|
+
return MillasResponse.view(template, data, options);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Return a redirect response.
|
|
55
|
+
*
|
|
56
|
+
* return redirect('/login')
|
|
57
|
+
* return redirect('/dashboard', { status: 301 })
|
|
58
|
+
* return redirect('back') // redirects to Referer header or '/'
|
|
59
|
+
*
|
|
60
|
+
* @param {string} url
|
|
61
|
+
* @param {object} [options]
|
|
62
|
+
* @param {number} [options.status=302]
|
|
63
|
+
* @returns {MillasResponse}
|
|
64
|
+
*/
|
|
65
|
+
function redirect(url, options = {}) {
|
|
66
|
+
return MillasResponse.redirect(url, options);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Return a plain text response.
|
|
71
|
+
*
|
|
72
|
+
* return text('Hello, world')
|
|
73
|
+
* return text('Created', { status: 201 })
|
|
74
|
+
*
|
|
75
|
+
* @param {string} content
|
|
76
|
+
* @param {object} [options]
|
|
77
|
+
* @param {number} [options.status=200]
|
|
78
|
+
* @returns {MillasResponse}
|
|
79
|
+
*/
|
|
80
|
+
function text(content, options = {}) {
|
|
81
|
+
return MillasResponse.text(content, options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Return a file response (send / download).
|
|
86
|
+
*
|
|
87
|
+
* return file('/storage/uploads/report.pdf')
|
|
88
|
+
* return file('/storage/uploads/report.pdf', { download: true })
|
|
89
|
+
* return file('/storage/uploads/report.pdf', { download: true, name: 'report.pdf' })
|
|
90
|
+
*
|
|
91
|
+
* @param {string} filePath — absolute or relative path
|
|
92
|
+
* @param {object} [options]
|
|
93
|
+
* @param {boolean} [options.download=false] — force download (Content-Disposition: attachment)
|
|
94
|
+
* @param {string} [options.name] — filename shown to the user on download
|
|
95
|
+
* @returns {MillasResponse}
|
|
96
|
+
*/
|
|
97
|
+
function file(filePath, options = {}) {
|
|
98
|
+
return MillasResponse.file(filePath, options);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Return an empty response.
|
|
103
|
+
*
|
|
104
|
+
* return empty() // 204 No Content
|
|
105
|
+
* return empty(200) // 200 with no body
|
|
106
|
+
*
|
|
107
|
+
* @param {number} [status=204]
|
|
108
|
+
* @returns {MillasResponse}
|
|
109
|
+
*/
|
|
110
|
+
function empty(status = 204) {
|
|
111
|
+
return MillasResponse.empty(status);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Throw an HTTP error — caught by the kernel and rendered by ErrorRenderer.
|
|
116
|
+
*
|
|
117
|
+
* abort(404)
|
|
118
|
+
* abort(403, 'You are not allowed to do that')
|
|
119
|
+
* abort(422, 'Validation failed', { email: ['Email is required'] })
|
|
120
|
+
*
|
|
121
|
+
* @param {number} status
|
|
122
|
+
* @param {string} [message]
|
|
123
|
+
* @param {object} [errors]
|
|
124
|
+
* @throws {HttpError}
|
|
125
|
+
*/
|
|
126
|
+
function abort(status, message, errors = null) {
|
|
127
|
+
throw new HttpError(status, message, errors);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Throw a 404 Not Found error.
|
|
132
|
+
* notFound()
|
|
133
|
+
* notFound('User not found')
|
|
134
|
+
*/
|
|
135
|
+
function notFound(message = 'Not Found') {
|
|
136
|
+
abort(404, message);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Throw a 401 Unauthorized error.
|
|
141
|
+
*/
|
|
142
|
+
function unauthorized(message = 'Unauthorized') {
|
|
143
|
+
abort(401, message);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Throw a 403 Forbidden error.
|
|
148
|
+
*/
|
|
149
|
+
function forbidden(message = 'Forbidden') {
|
|
150
|
+
abort(403, message);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
jsonify,
|
|
155
|
+
view,
|
|
156
|
+
redirect,
|
|
157
|
+
text,
|
|
158
|
+
file,
|
|
159
|
+
empty,
|
|
160
|
+
abort,
|
|
161
|
+
notFound,
|
|
162
|
+
unauthorized,
|
|
163
|
+
forbidden,
|
|
164
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MillasRequest = require('./MillasRequest');
|
|
4
|
+
const MillasResponse = require('./MillasResponse');
|
|
5
|
+
const ResponseDispatcher = require('./ResponseDispatcher');
|
|
6
|
+
const helpers = require('./helpers');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
MillasRequest,
|
|
10
|
+
MillasResponse,
|
|
11
|
+
ResponseDispatcher,
|
|
12
|
+
...helpers,
|
|
13
|
+
};
|