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,315 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const HttpAdapter = require('./HttpAdapter');
|
|
4
|
+
const MillasRequest = require('../MillasRequest');
|
|
5
|
+
const MillasResponse = require('../MillasResponse');
|
|
6
|
+
const RequestContext = require('../RequestContext');
|
|
7
|
+
const ErrorRenderer = require('../../errors/ErrorRenderer');
|
|
8
|
+
const WelcomePage = require('../WelcomePage');
|
|
9
|
+
const http = require("http");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ExpressAdapter
|
|
13
|
+
*
|
|
14
|
+
* The Express implementation of HttpAdapter.
|
|
15
|
+
* This is the ONLY file in the entire Millas codebase that imports Express
|
|
16
|
+
* or calls Express APIs (req, res, next, app[verb], app.use, etc.).
|
|
17
|
+
*
|
|
18
|
+
* Swapping to a different HTTP engine means writing a new adapter class —
|
|
19
|
+
* zero changes to the kernel, Router, MiddlewareRegistry, or any user code.
|
|
20
|
+
*/
|
|
21
|
+
class ExpressAdapter extends HttpAdapter {
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {import('express').Application} expressApp
|
|
25
|
+
*/
|
|
26
|
+
constructor(expressApp) {
|
|
27
|
+
super();
|
|
28
|
+
this._app = expressApp;
|
|
29
|
+
this._server = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Setup ──────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
applyBodyParsers() {
|
|
35
|
+
const express = require('express');
|
|
36
|
+
this._app.use(express.json());
|
|
37
|
+
this._app.use(express.urlencoded({extended: true}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
applyMiddleware(fn) {
|
|
41
|
+
this._app.use(fn);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Route mounting ─────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
mountRoute(verb, path, handlers) {
|
|
47
|
+
this._app[verb.toLowerCase()](path, ...handlers);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
mountWelcome(handler) {
|
|
51
|
+
this._app.get('/', handler);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
mountNotFound() {
|
|
55
|
+
this._app.use(ErrorRenderer.notFound());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
mountErrorHandler() {
|
|
59
|
+
this._app.use(ErrorRenderer.handler());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Request / Response bridge ──────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Wrap a Millas kernel handler into an Express (req, res, next) function.
|
|
66
|
+
*
|
|
67
|
+
* This is the ONLY place in the codebase where Express req/res/next
|
|
68
|
+
* appear outside of adapter code. The kernel handler never sees them.
|
|
69
|
+
*/
|
|
70
|
+
wrapKernelHandler(kernelFn, displayName, container) {
|
|
71
|
+
const fnName = displayName || kernelFn.name || 'anonymous';
|
|
72
|
+
|
|
73
|
+
return (expressReq, expressRes, expressNext) => {
|
|
74
|
+
const millaReq = new MillasRequest(expressReq);
|
|
75
|
+
const ctx = new RequestContext(millaReq, container);
|
|
76
|
+
|
|
77
|
+
let nextCalled = false;
|
|
78
|
+
const trackedNext = (...args) => {
|
|
79
|
+
nextCalled = true;
|
|
80
|
+
expressNext(...args);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
new Promise((resolve, reject) => {
|
|
84
|
+
try {
|
|
85
|
+
resolve(kernelFn(ctx, trackedNext));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
reject(err);
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.then(value => {
|
|
91
|
+
if (nextCalled) return;
|
|
92
|
+
if (expressRes.headersSent) return;
|
|
93
|
+
|
|
94
|
+
if (value === undefined || value === null) {
|
|
95
|
+
return expressNext(Object.assign(
|
|
96
|
+
new Error(
|
|
97
|
+
`Route handler "${fnName}" did not return a response.\n` +
|
|
98
|
+
`Return a MillasResponse or a plain value:\n\n` +
|
|
99
|
+
` return jsonify({ ok: true })\n` +
|
|
100
|
+
` return { ok: true } // auto-wrapped\n` +
|
|
101
|
+
` return 'Hello world' // auto-wrapped\n` +
|
|
102
|
+
` return redirect('/login')\n` +
|
|
103
|
+
` return view('home', { data })`
|
|
104
|
+
),
|
|
105
|
+
{status: 500, statusCode: 500}
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (value instanceof Error) return expressNext(value);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const response = MillasResponse.isResponse(value)
|
|
113
|
+
? value
|
|
114
|
+
: this._autoWrap(value);
|
|
115
|
+
this.dispatch(response, expressRes);
|
|
116
|
+
} catch (dispatchErr) {
|
|
117
|
+
expressNext(dispatchErr);
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.catch(expressNext);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Wrap a Millas middleware instance into an Express (req, res, next) function.
|
|
126
|
+
*/
|
|
127
|
+
wrapMiddleware(instance, container) {
|
|
128
|
+
return (expressReq, expressRes, expressNext) => {
|
|
129
|
+
const millaReq = new MillasRequest(expressReq);
|
|
130
|
+
const ctx = new RequestContext(millaReq, container);
|
|
131
|
+
|
|
132
|
+
const next = () => {
|
|
133
|
+
expressNext();
|
|
134
|
+
return undefined;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
new Promise((resolve, reject) => {
|
|
138
|
+
try {
|
|
139
|
+
resolve(instance.handle(ctx, next));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
reject(err);
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
.then(value => {
|
|
145
|
+
if (value !== undefined && value !== null && !expressRes.headersSent) {
|
|
146
|
+
const response = MillasResponse.isResponse(value)
|
|
147
|
+
? value
|
|
148
|
+
: this._autoWrap(value);
|
|
149
|
+
this.dispatch(response, expressRes);
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
.catch(expressNext);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Dispatch a MillasResponse to the Express res object.
|
|
158
|
+
* This is the ONLY place in the codebase where Express res methods are called.
|
|
159
|
+
*/
|
|
160
|
+
dispatch(response, expressRes) {
|
|
161
|
+
if (!response || !MillasResponse.isResponse(response)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
'[ExpressAdapter] Expected a MillasResponse. Got: ' + typeof response
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Status
|
|
168
|
+
expressRes.status(response.statusCode);
|
|
169
|
+
|
|
170
|
+
// CORS headers stored by CorsMiddleware on next() calls
|
|
171
|
+
const corsHeaders = expressRes.req?._corsHeaders;
|
|
172
|
+
if (corsHeaders) {
|
|
173
|
+
for (const [name, value] of Object.entries(corsHeaders)) {
|
|
174
|
+
expressRes.setHeader(name, value);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Response headers
|
|
179
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
180
|
+
expressRes.setHeader(name, value);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Cookies
|
|
184
|
+
for (const [name, {value, options}] of Object.entries(response.cookies)) {
|
|
185
|
+
if (options.maxAge === 0 || options.expires?.getTime() === 0) {
|
|
186
|
+
expressRes.clearCookie(name, options);
|
|
187
|
+
} else {
|
|
188
|
+
expressRes.cookie(name, value, options);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Body
|
|
193
|
+
const {type, body} = response;
|
|
194
|
+
|
|
195
|
+
switch (type) {
|
|
196
|
+
case 'json':
|
|
197
|
+
return expressRes.json(body);
|
|
198
|
+
|
|
199
|
+
case 'html':
|
|
200
|
+
case 'text':
|
|
201
|
+
return expressRes.send(body);
|
|
202
|
+
|
|
203
|
+
case 'redirect':
|
|
204
|
+
return expressRes.redirect(response.statusCode, body);
|
|
205
|
+
|
|
206
|
+
case 'empty':
|
|
207
|
+
return expressRes.end();
|
|
208
|
+
|
|
209
|
+
case 'file': {
|
|
210
|
+
const {path: filePath, download, name: fileName} = body;
|
|
211
|
+
if (download) {
|
|
212
|
+
return expressRes.download(
|
|
213
|
+
filePath,
|
|
214
|
+
fileName || require('path').basename(filePath)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return expressRes.sendFile(require('path').resolve(filePath));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
case 'view': {
|
|
221
|
+
const {template, data} = body;
|
|
222
|
+
return expressRes.render(template, data);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case 'stream': {
|
|
226
|
+
if (body && typeof body.pipe === 'function') {
|
|
227
|
+
body.pipe(expressRes);
|
|
228
|
+
} else {
|
|
229
|
+
expressRes.end();
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
default:
|
|
235
|
+
return expressRes.send(body);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Server lifecycle ───────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
listen(port, host) {
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
this.initListener(port, host, resolve, reject);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
initListener(port, host, onListening, onError) {
|
|
248
|
+
const server = http.createServer(this._app);
|
|
249
|
+
server.listen(port, host);
|
|
250
|
+
server.on('error', onError);
|
|
251
|
+
server.on('listening', onListening);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
close() {
|
|
255
|
+
return new Promise((resolve) => {
|
|
256
|
+
if (!this._server) return resolve();
|
|
257
|
+
this._server.close(() => resolve());
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── WelcomePage factory ───────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Build the welcome page native handler.
|
|
265
|
+
* Returned value is passed directly to mountWelcome().
|
|
266
|
+
*/
|
|
267
|
+
makeWelcomeHandler(version) {
|
|
268
|
+
return WelcomePage.handler(version);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Internal ──────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Auto-wrap a plain JS value into a MillasResponse.
|
|
275
|
+
* Kept inside the adapter — not on the kernel — because it ultimately
|
|
276
|
+
* decides what to send over the wire, which is adapter territory.
|
|
277
|
+
*/
|
|
278
|
+
_autoWrap(value) {
|
|
279
|
+
if (MillasResponse.isResponse(value)) return value;
|
|
280
|
+
if (value instanceof Error) throw value;
|
|
281
|
+
|
|
282
|
+
if (typeof value === 'string') {
|
|
283
|
+
return value.trimStart().startsWith('<')
|
|
284
|
+
? MillasResponse.html(value)
|
|
285
|
+
: MillasResponse.text(value);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
typeof value === 'object' ||
|
|
290
|
+
typeof value === 'number' ||
|
|
291
|
+
typeof value === 'boolean'
|
|
292
|
+
) {
|
|
293
|
+
return MillasResponse.json(value);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return MillasResponse.text(String(value));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Expose the raw Express app for escape hatches.
|
|
301
|
+
* Prefer not using this — it couples code to Express.
|
|
302
|
+
*/
|
|
303
|
+
get nativeApp() {
|
|
304
|
+
return this._app;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Expose the raw net.Server once listen() has been called.
|
|
309
|
+
*/
|
|
310
|
+
get server() {
|
|
311
|
+
return this._server;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = ExpressAdapter;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HttpAdapter
|
|
5
|
+
*
|
|
6
|
+
* Abstract interface between the Millas kernel and any underlying HTTP engine.
|
|
7
|
+
* The kernel never imports Express (or Fastify, or Hono) directly — it only
|
|
8
|
+
* calls the methods defined here.
|
|
9
|
+
*
|
|
10
|
+
* To add a new HTTP engine, create a class that extends HttpAdapter and
|
|
11
|
+
* implements every method. The kernel works unchanged.
|
|
12
|
+
*
|
|
13
|
+
* ── Implemented by ───────────────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* ExpressAdapter (default, ships with Millas)
|
|
16
|
+
* FastifyAdapter (future)
|
|
17
|
+
* HonoAdapter (future)
|
|
18
|
+
*
|
|
19
|
+
* ── Lifecycle ────────────────────────────────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* 1. adapter.applyBodyParsers() — JSON + urlencoded
|
|
22
|
+
* 2. adapter.applyMiddleware(fn) — any raw adapter-level middleware
|
|
23
|
+
* 3. adapter.mountRoute(verb, path, handlers) — register app routes
|
|
24
|
+
* 4. adapter.mountWelcome(handler) — optional dev welcome page
|
|
25
|
+
* 5. adapter.mountNotFound() — 404 handler
|
|
26
|
+
* 6. adapter.mountErrorHandler() — global error handler
|
|
27
|
+
* 7. await adapter.listen(port, host) — start accepting connections
|
|
28
|
+
* 8. adapter.close() — graceful shutdown
|
|
29
|
+
*/
|
|
30
|
+
class HttpAdapter {
|
|
31
|
+
|
|
32
|
+
// ── Setup ──────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Apply JSON + urlencoded body parsers.
|
|
36
|
+
* Called once during bootstrap, before any routes are mounted.
|
|
37
|
+
*/
|
|
38
|
+
applyBodyParsers() {
|
|
39
|
+
throw new Error(`${this.constructor.name} must implement applyBodyParsers()`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Apply a single raw middleware function at the adapter level.
|
|
44
|
+
* Used for things like helmet(), compression() that are engine-specific.
|
|
45
|
+
*
|
|
46
|
+
* @param {Function} fn — adapter-native middleware function
|
|
47
|
+
*/
|
|
48
|
+
applyMiddleware(fn) {
|
|
49
|
+
throw new Error(`${this.constructor.name} must implement applyMiddleware(fn)`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Route mounting ─────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Register a route handler.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} verb — 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'
|
|
58
|
+
* @param {string} path — e.g. '/users/:id'
|
|
59
|
+
* @param {Function[]} handlers — [middleware..., terminalHandler]
|
|
60
|
+
* Each handler is a Millas kernel handler
|
|
61
|
+
* (expressReq, expressRes, expressNext) already
|
|
62
|
+
* converted by the adapter's wrapKernelHandler().
|
|
63
|
+
*/
|
|
64
|
+
mountRoute(verb, path, handlers) {
|
|
65
|
+
throw new Error(`${this.constructor.name} must implement mountRoute(verb, path, handlers)`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Mount the dev welcome page for GET /.
|
|
70
|
+
* Only called when no user route covers GET /.
|
|
71
|
+
*
|
|
72
|
+
* @param {Function} handler — adapter-native handler function
|
|
73
|
+
*/
|
|
74
|
+
mountWelcome(handler) {
|
|
75
|
+
throw new Error(`${this.constructor.name} must implement mountWelcome(handler)`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Mount the 404 fallback handler.
|
|
80
|
+
* Must be called AFTER all routes and mountWelcome().
|
|
81
|
+
*/
|
|
82
|
+
mountNotFound() {
|
|
83
|
+
throw new Error(`${this.constructor.name} must implement mountNotFound()`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Mount the global error handler.
|
|
88
|
+
* Must be called LAST — after mountNotFound().
|
|
89
|
+
*/
|
|
90
|
+
mountErrorHandler() {
|
|
91
|
+
throw new Error(`${this.constructor.name} must implement mountErrorHandler()`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Request / Response bridge ──────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Wrap a Millas kernel handler function into an adapter-native handler.
|
|
98
|
+
*
|
|
99
|
+
* The kernel handler signature is:
|
|
100
|
+
* (millaCtx: RequestContext, trackedNext: Function) => Promise<MillasResponse>
|
|
101
|
+
*
|
|
102
|
+
* The adapter wraps this into whatever its native handler signature is,
|
|
103
|
+
* e.g. (req, res, next) for Express.
|
|
104
|
+
*
|
|
105
|
+
* @param {Function} kernelFn
|
|
106
|
+
* @param {string} displayName — for error messages
|
|
107
|
+
* @param {object} container — DI container
|
|
108
|
+
* @returns {Function} adapter-native handler
|
|
109
|
+
*/
|
|
110
|
+
wrapKernelHandler(kernelFn, displayName, container) {
|
|
111
|
+
throw new Error(`${this.constructor.name} must implement wrapKernelHandler()`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Wrap a Millas middleware instance into an adapter-native handler.
|
|
116
|
+
*
|
|
117
|
+
* @param {object} instance — Millas Middleware instance with handle(ctx, next)
|
|
118
|
+
* @param {object} container — DI container
|
|
119
|
+
* @returns {Function} adapter-native handler
|
|
120
|
+
*/
|
|
121
|
+
wrapMiddleware(instance, container) {
|
|
122
|
+
throw new Error(`${this.constructor.name} must implement wrapMiddleware()`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Dispatch a MillasResponse to the underlying engine's response object.
|
|
127
|
+
*
|
|
128
|
+
* @param {MillasResponse} response
|
|
129
|
+
* @param {*} nativeRes — e.g. Express res
|
|
130
|
+
*/
|
|
131
|
+
dispatch(response, nativeRes) {
|
|
132
|
+
throw new Error(`${this.constructor.name} must implement dispatch(response, nativeRes)`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Server lifecycle ───────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Start listening on port/host.
|
|
139
|
+
* Returns a Promise that resolves once the server is bound.
|
|
140
|
+
*
|
|
141
|
+
* @param {number} port
|
|
142
|
+
* @param {string} host
|
|
143
|
+
* @returns {Promise<void>}
|
|
144
|
+
*/
|
|
145
|
+
listen(port, host) {
|
|
146
|
+
throw new Error(`${this.constructor.name} must implement listen(port, host)`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Gracefully close the server.
|
|
151
|
+
* Returns a Promise that resolves once all connections are drained.
|
|
152
|
+
*
|
|
153
|
+
* @returns {Promise<void>}
|
|
154
|
+
*/
|
|
155
|
+
close() {
|
|
156
|
+
throw new Error(`${this.constructor.name} must implement close()`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The name of this adapter, used in logs and error messages.
|
|
161
|
+
* @returns {string}
|
|
162
|
+
*/
|
|
163
|
+
get name() {
|
|
164
|
+
return this.constructor.name;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = HttpAdapter;
|
|
@@ -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
|
+
};
|