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
package/src/router/Router.js
CHANGED
|
@@ -1,175 +1,31 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const MiddlewareRegistry = require('./MiddlewareRegistry');
|
|
4
|
+
const ErrorRenderer = require('../errors/ErrorRenderer');
|
|
5
|
+
const MillasRequest = require('../http/MillasRequest');
|
|
6
|
+
const MillasResponse = require('../http/MillasResponse');
|
|
7
|
+
const ResponseDispatcher = require('../http/ResponseDispatcher');
|
|
8
|
+
const RequestContext = require('../http/RequestContext');
|
|
4
9
|
|
|
5
|
-
// ── Welcome page — shown at / when no route is defined ────────────────────────
|
|
6
|
-
const WELCOME_PAGE = `<!DOCTYPE html>
|
|
7
|
-
<html lang="en">
|
|
8
|
-
<head>
|
|
9
|
-
<meta charset="UTF-8">
|
|
10
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
11
|
-
|
|
12
|
-
<title>Welcome to Millas</title>
|
|
13
|
-
|
|
14
|
-
<link rel="stylesheet"
|
|
15
|
-
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
|
|
16
|
-
crossorigin="anonymous" referrerpolicy="no-referrer"/>
|
|
17
|
-
|
|
18
|
-
<style>
|
|
19
|
-
|
|
20
|
-
body{
|
|
21
|
-
margin:0;
|
|
22
|
-
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
|
|
23
|
-
background:#fff;
|
|
24
|
-
color:#333;
|
|
25
|
-
display:flex;
|
|
26
|
-
align-items:center;
|
|
27
|
-
justify-content:center;
|
|
28
|
-
height:100vh;
|
|
29
|
-
text-align:center;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.container{
|
|
33
|
-
max-width:720px;
|
|
34
|
-
padding:40px;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.icon{
|
|
38
|
-
margin-bottom:20px;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
h1{
|
|
42
|
-
font-size:32px;
|
|
43
|
-
margin-bottom:12px;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.subtitle{
|
|
47
|
-
color:#555;
|
|
48
|
-
font-size:16px;
|
|
49
|
-
line-height:1.7;
|
|
50
|
-
margin-bottom:25px;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
code{
|
|
54
|
-
background:#fff4ed;
|
|
55
|
-
color:#ea580c;
|
|
56
|
-
padding:4px 8px;
|
|
57
|
-
border-radius:6px;
|
|
58
|
-
font-size:13px;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.steps{
|
|
62
|
-
margin-top:30px;
|
|
63
|
-
text-align:left;
|
|
64
|
-
display:inline-block;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.steps h3{
|
|
68
|
-
margin-bottom:10px;
|
|
69
|
-
font-size:16px;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.steps ul{
|
|
73
|
-
margin:0;
|
|
74
|
-
padding-left:20px;
|
|
75
|
-
color:#555;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.links{
|
|
79
|
-
margin-top:35px;
|
|
80
|
-
display:flex;
|
|
81
|
-
justify-content:center;
|
|
82
|
-
gap:18px;
|
|
83
|
-
flex-wrap:wrap;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.links a{
|
|
87
|
-
color:#f97316;
|
|
88
|
-
text-decoration:none;
|
|
89
|
-
font-size:14px;
|
|
90
|
-
display:flex;
|
|
91
|
-
align-items:center;
|
|
92
|
-
gap:6px;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.links a:hover{
|
|
96
|
-
text-decoration:underline;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.footer{
|
|
100
|
-
margin-top:35px;
|
|
101
|
-
font-size:13px;
|
|
102
|
-
color:#888;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
</style>
|
|
106
|
-
</head>
|
|
107
|
-
|
|
108
|
-
<body>
|
|
109
|
-
|
|
110
|
-
<div class="container">
|
|
111
|
-
|
|
112
|
-
<div class="icon">
|
|
113
|
-
<img src="https://www.saaspegasus.com/static/images/web/landing-page/rocket-laptop.39d6be7451e6.svg" alt="Millas" style="width:140px;height:auto;">
|
|
114
|
-
</div>
|
|
115
|
-
|
|
116
|
-
<h1>It worked!</h1>
|
|
117
|
-
|
|
118
|
-
<p class="subtitle">
|
|
119
|
-
Millas is installed and running correctly.
|
|
120
|
-
</p>
|
|
121
|
-
|
|
122
|
-
<div class="steps">
|
|
123
|
-
<h3>Next steps</h3>
|
|
124
|
-
<ul>
|
|
125
|
-
<li>Create your first route in <code>routes/web.js</code></li>
|
|
126
|
-
</ul>
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
<div class="links">
|
|
130
|
-
|
|
131
|
-
<a href="/admin">
|
|
132
|
-
<i class="fa-solid fa-gauge"></i>
|
|
133
|
-
Admin panel
|
|
134
|
-
</a>
|
|
135
|
-
|
|
136
|
-
<a href="/api/health">
|
|
137
|
-
<i class="fa-solid fa-heart-pulse"></i>
|
|
138
|
-
Health check
|
|
139
|
-
</a>
|
|
140
|
-
|
|
141
|
-
<a href="https://github.com/millas-framework/millas" target="_blank">
|
|
142
|
-
<i class="fa-brands fa-github"></i>
|
|
143
|
-
GitHub
|
|
144
|
-
</a>
|
|
145
|
-
|
|
146
|
-
</div>
|
|
147
|
-
|
|
148
|
-
<div class="footer">
|
|
149
|
-
Millas v0.1.2
|
|
150
|
-
</div>
|
|
151
|
-
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
</body>
|
|
155
|
-
</html>`;
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Router
|
|
159
|
-
*
|
|
160
|
-
* Takes a populated RouteRegistry and binds every route
|
|
161
|
-
* onto a live Express app instance.
|
|
162
|
-
*
|
|
163
|
-
* Also wraps async controller methods so unhandled promise
|
|
164
|
-
* rejections are forwarded to Express error handlers.
|
|
165
|
-
*/
|
|
166
10
|
class Router {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} expressApp
|
|
13
|
+
* @param {RouteRegistry} registry
|
|
14
|
+
* @param {MiddlewareRegistry} middlewareRegistry
|
|
15
|
+
* @param {Container|null} container — DI container, injected into RequestContext
|
|
16
|
+
*/
|
|
17
|
+
constructor(expressApp, registry, middlewareRegistry, container = null) {
|
|
18
|
+
this._app = expressApp;
|
|
19
|
+
this._registry = registry;
|
|
20
|
+
this._mw = middlewareRegistry || new MiddlewareRegistry();
|
|
21
|
+
this._container = container;
|
|
171
22
|
}
|
|
172
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Bind all registered routes onto the Express app.
|
|
26
|
+
* Does NOT add 404/error handlers — call mountFallbacks() after
|
|
27
|
+
* all other middleware (like Admin) has been registered.
|
|
28
|
+
*/
|
|
173
29
|
mountRoutes() {
|
|
174
30
|
const routes = this._registry.all();
|
|
175
31
|
for (const route of routes) {
|
|
@@ -178,59 +34,38 @@ class Router {
|
|
|
178
34
|
return this;
|
|
179
35
|
}
|
|
180
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Add the 404 + global error handlers.
|
|
39
|
+
* Must be called LAST — after all routes and admin panels.
|
|
40
|
+
*/
|
|
181
41
|
mountFallbacks() {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
if (!hasRootRoute) {
|
|
187
|
-
this._app.get('/', (req, res) => {
|
|
188
|
-
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
189
|
-
res.send(WELCOME_PAGE);
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
this._app.use((req, res) => {
|
|
194
|
-
res.status(404).json({
|
|
195
|
-
error: 'Not Found',
|
|
196
|
-
message: `Cannot ${req.method} ${req.path}`,
|
|
197
|
-
status: 404,
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
this._app.use((err, req, res, _next) => {
|
|
202
|
-
const status = err.status || err.statusCode || 500;
|
|
203
|
-
const message = err.message || 'Internal Server Error';
|
|
204
|
-
|
|
205
|
-
if (status >= 500 && process.env.NODE_ENV !== 'production') {
|
|
206
|
-
console.error(err.stack);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
res.status(status).json({
|
|
210
|
-
error: status >= 500 ? 'Internal Server Error' : message,
|
|
211
|
-
message,
|
|
212
|
-
status,
|
|
213
|
-
...(err.errors && { errors: err.errors }),
|
|
214
|
-
...(status >= 500 && process.env.NODE_ENV !== 'production' && { stack: err.stack }),
|
|
215
|
-
});
|
|
216
|
-
});
|
|
42
|
+
this._app.use(ErrorRenderer.notFound());
|
|
43
|
+
this._app.use(ErrorRenderer.handler());
|
|
217
44
|
return this;
|
|
218
45
|
}
|
|
219
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Bind all registered routes onto the Express app
|
|
49
|
+
* AND add 404/error handlers (original behaviour).
|
|
50
|
+
*/
|
|
220
51
|
mount() {
|
|
221
52
|
const routes = this._registry.all();
|
|
222
53
|
for (const route of routes) {
|
|
223
54
|
this._bindRoute(route);
|
|
224
55
|
}
|
|
225
|
-
this.
|
|
226
|
-
|
|
56
|
+
this._app.use(ErrorRenderer.notFound());
|
|
57
|
+
this._app.use(ErrorRenderer.handler());
|
|
227
58
|
}
|
|
228
59
|
|
|
60
|
+
// ─── Private ──────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
229
62
|
_bindRoute(route) {
|
|
230
|
-
const verb
|
|
231
|
-
const path
|
|
63
|
+
const verb = route.verb.toLowerCase();
|
|
64
|
+
const path = route.path;
|
|
65
|
+
|
|
232
66
|
const mwHandlers = this._resolveMiddleware(route.middleware || []);
|
|
233
|
-
const terminal = this._resolveHandler(route.handler, route.method);
|
|
67
|
+
const terminal = this._resolveHandler(route.handler, route.method, route.verb, path, route.name);
|
|
68
|
+
|
|
234
69
|
this._app[verb](path, ...mwHandlers, terminal);
|
|
235
70
|
}
|
|
236
71
|
|
|
@@ -245,35 +80,132 @@ class Router {
|
|
|
245
80
|
});
|
|
246
81
|
}
|
|
247
82
|
|
|
248
|
-
_resolveHandler(handler, method) {
|
|
83
|
+
_resolveHandler(handler, method, verb, path, routeName) {
|
|
84
|
+
let fn;
|
|
85
|
+
let inferredName;
|
|
86
|
+
|
|
249
87
|
if (typeof handler === 'function' && !method) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
88
|
+
fn = handler;
|
|
89
|
+
// Use function name if it has one, otherwise build from route
|
|
90
|
+
inferredName = handler.name && handler.name !== 'anonymous' && handler.name !== ''
|
|
91
|
+
? handler.name
|
|
92
|
+
: null;
|
|
93
|
+
} else if (typeof handler === 'function' && typeof method === 'string') {
|
|
253
94
|
const instance = new handler();
|
|
254
95
|
if (typeof instance[method] !== 'function') {
|
|
255
96
|
throw new Error(`Method "${method}" not found on controller "${handler.name}".`);
|
|
256
97
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (typeof handler === 'object' && handler !== null && typeof method === 'string') {
|
|
98
|
+
fn = instance[method].bind(instance);
|
|
99
|
+
inferredName = `${handler.name}.${method}`;
|
|
100
|
+
} else if (typeof handler === 'object' && handler !== null && typeof method === 'string') {
|
|
260
101
|
if (typeof handler[method] !== 'function') {
|
|
261
102
|
throw new Error(`Method "${method}" not found on handler object.`);
|
|
262
103
|
}
|
|
263
|
-
|
|
104
|
+
fn = handler[method].bind(handler);
|
|
105
|
+
inferredName = `${handler.constructor?.name || 'Controller'}.${method}`;
|
|
106
|
+
} else if (typeof handler === 'function') {
|
|
107
|
+
fn = handler;
|
|
108
|
+
inferredName = handler.name && handler.name !== 'anonymous' ? handler.name : null;
|
|
109
|
+
} else {
|
|
110
|
+
throw new Error(`Invalid route handler: ${JSON.stringify(handler)}`);
|
|
264
111
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
112
|
+
|
|
113
|
+
// Build the display name shown in error messages, priority:
|
|
114
|
+
// 1. Explicit route name (Route.get(...).name('users.index'))
|
|
115
|
+
// 2. Controller.method (UserController.index)
|
|
116
|
+
// 3. Named function (async function getUsers)
|
|
117
|
+
// 4. Route signature (GET /users/:id)
|
|
118
|
+
const displayName = routeName
|
|
119
|
+
|| inferredName
|
|
120
|
+
|| (verb && path ? `${verb.toUpperCase()} ${path}` : 'anonymous');
|
|
121
|
+
|
|
122
|
+
return this._buildKernelHandler(fn, displayName);
|
|
269
123
|
}
|
|
270
124
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The kernel handler — the Promise wrapper that:
|
|
127
|
+
* 1. Wraps the raw Express req into a MillasRequest
|
|
128
|
+
* 2. Calls the route handler with only the MillasRequest
|
|
129
|
+
* 3. Receives a MillasResponse (or plain value, auto-wrapped)
|
|
130
|
+
* 4. Dispatches through ResponseDispatcher to Express res
|
|
131
|
+
* 5. Raises a clear error if the handler returned nothing
|
|
132
|
+
*/
|
|
133
|
+
_buildKernelHandler(fn, displayName) {
|
|
134
|
+
const fnName = displayName || fn.name || 'anonymous';
|
|
135
|
+
const container = this._container;
|
|
136
|
+
|
|
137
|
+
return (expressReq, expressRes, expressNext) => {
|
|
138
|
+
const millaReq = new MillasRequest(expressReq);
|
|
139
|
+
const ctx = new RequestContext(millaReq, container);
|
|
140
|
+
|
|
141
|
+
let nextCalled = false;
|
|
142
|
+
const trackedNext = (...args) => {
|
|
143
|
+
nextCalled = true;
|
|
144
|
+
expressNext(...args);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
new Promise((resolve, reject) => {
|
|
148
|
+
try {
|
|
149
|
+
resolve(fn(ctx, trackedNext));
|
|
150
|
+
} catch (err) {
|
|
151
|
+
reject(err);
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
.then(value => {
|
|
155
|
+
if (nextCalled) return;
|
|
156
|
+
if (expressRes.headersSent) return;
|
|
157
|
+
|
|
158
|
+
if (value === undefined || value === null) {
|
|
159
|
+
const err = Object.assign(
|
|
160
|
+
new Error(
|
|
161
|
+
`The route handler "${fnName}" did not return a response.\n` +
|
|
162
|
+
`Return a MillasResponse or a plain value:\n\n` +
|
|
163
|
+
` return jsonify({ ok: true })\n` +
|
|
164
|
+
` return { ok: true } // auto-wrapped\n` +
|
|
165
|
+
` return 'Hello world' // auto-wrapped\n` +
|
|
166
|
+
` return redirect('/login')\n` +
|
|
167
|
+
` return view('home', { data })`
|
|
168
|
+
),
|
|
169
|
+
{ status: 500, statusCode: 500 }
|
|
170
|
+
);
|
|
171
|
+
return expressNext(err);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (value instanceof Error) return expressNext(value);
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const response = MillasResponse.isResponse(value)
|
|
178
|
+
? value
|
|
179
|
+
: ResponseDispatcher.autoWrap(value);
|
|
180
|
+
ResponseDispatcher.dispatch(response, expressRes);
|
|
181
|
+
} catch (dispatchErr) {
|
|
182
|
+
expressNext(dispatchErr);
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
.catch(expressNext);
|
|
186
|
+
};
|
|
276
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Wrap a route handler so that:
|
|
191
|
+
*
|
|
192
|
+
* 1. Async functions have their rejections forwarded to next(err)
|
|
193
|
+
* 2. Return values are automatically sent as a response — so developers
|
|
194
|
+
* can write handlers without touching req/res at all:
|
|
195
|
+
*
|
|
196
|
+
* Route.get('/', () => ({ status: 'ok' }))
|
|
197
|
+
* Route.get('/hello', () => 'Hello world')
|
|
198
|
+
* Route.get('/user', async () => await User.find(1))
|
|
199
|
+
* Route.get('/ping', (req, res) => res.json({ pong: true })) // still works
|
|
200
|
+
*
|
|
201
|
+
* Return value rules:
|
|
202
|
+
* string → res.send(value)
|
|
203
|
+
* number → res.json(value) (rarely useful but safe)
|
|
204
|
+
* object/array → res.json(value)
|
|
205
|
+
* null/undefined → do nothing (handler called res itself, or next())
|
|
206
|
+
* Error → next(value)
|
|
207
|
+
*/
|
|
208
|
+
|
|
277
209
|
}
|
|
278
210
|
|
|
279
|
-
module.exports = Router;
|
|
211
|
+
module.exports = Router;
|
package/src/scaffold/maker.js
CHANGED
|
@@ -38,6 +38,7 @@ async function makeController(name, options = {}) {
|
|
|
38
38
|
? `'use strict';
|
|
39
39
|
|
|
40
40
|
const { Controller } = require('millas');
|
|
41
|
+
const { string, email, number } = require('millas/validation');
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* ${className}
|
|
@@ -46,34 +47,32 @@ const { Controller } = require('millas');
|
|
|
46
47
|
*/
|
|
47
48
|
class ${className} extends Controller {
|
|
48
49
|
/** GET /${name.toLowerCase()}s */
|
|
49
|
-
async index(
|
|
50
|
-
return this.ok(
|
|
50
|
+
async index({ query }) {
|
|
51
|
+
return this.ok({ data: [] });
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/** GET /${name.toLowerCase()}s/:id */
|
|
54
|
-
async show(
|
|
55
|
-
|
|
56
|
-
return this.ok(res, { data: { id } });
|
|
55
|
+
async show({ params }) {
|
|
56
|
+
return this.ok({ data: { id: params.id } });
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/** POST /${name.toLowerCase()}s */
|
|
60
|
-
async store(
|
|
61
|
-
const data = await
|
|
62
|
-
//
|
|
60
|
+
async store({ body }) {
|
|
61
|
+
const data = await body.validate({
|
|
62
|
+
// name: string().required().max(255),
|
|
63
63
|
});
|
|
64
|
-
return this.created(
|
|
64
|
+
return this.created({ data });
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/** PUT /${name.toLowerCase()}s/:id */
|
|
68
|
-
async update(
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
return this.ok(res, { data: { id, ...data } });
|
|
68
|
+
async update({ params, body }) {
|
|
69
|
+
const data = body.except(['id']);
|
|
70
|
+
return this.ok({ data: { id: params.id, ...data } });
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
/** DELETE /${name.toLowerCase()}s/:id */
|
|
75
|
-
async destroy(
|
|
76
|
-
return this.noContent(
|
|
74
|
+
async destroy({ params }) {
|
|
75
|
+
return this.noContent();
|
|
77
76
|
}
|
|
78
77
|
}
|
|
79
78
|
|
|
@@ -87,8 +86,8 @@ const { Controller } = require('millas');
|
|
|
87
86
|
* ${className}
|
|
88
87
|
*/
|
|
89
88
|
class ${className} extends Controller {
|
|
90
|
-
async index(
|
|
91
|
-
return this.ok(
|
|
89
|
+
async index({ query }) {
|
|
90
|
+
return this.ok({ message: 'Hello from ${className}' });
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
93
|
|
|
@@ -105,58 +104,23 @@ async function makeModel(name, options = {}) {
|
|
|
105
104
|
|
|
106
105
|
const content = `'use strict';
|
|
107
106
|
|
|
108
|
-
const { Model, fields
|
|
107
|
+
const { Model, fields } = require('millas');
|
|
109
108
|
|
|
110
109
|
/**
|
|
111
110
|
* ${className} Model
|
|
112
111
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* millas makemigrations — generates migration files from your changes
|
|
117
|
-
* millas migrate — applies them to the database
|
|
112
|
+
* Represents the "${tableName}" table.
|
|
113
|
+
* Run: millas makemigrations — to generate the migration.
|
|
114
|
+
* Run: millas migrate — to apply it.
|
|
118
115
|
*/
|
|
119
116
|
class ${className} extends Model {
|
|
120
117
|
static table = '${tableName}';
|
|
121
118
|
|
|
122
|
-
// ── Fields ──────────────────────────────────────────────────────
|
|
123
119
|
static fields = {
|
|
124
120
|
id: fields.id(),
|
|
125
121
|
created_at: fields.timestamp(),
|
|
126
122
|
updated_at: fields.timestamp(),
|
|
127
|
-
// deleted_at: fields.timestamp({ nullable: true }), // uncomment + set softDeletes = true
|
|
128
123
|
};
|
|
129
|
-
|
|
130
|
-
// ── Soft deletes ─────────────────────────────────────────────────
|
|
131
|
-
// static softDeletes = true;
|
|
132
|
-
|
|
133
|
-
// ── Relations ─────────────────────────────────────────────────────
|
|
134
|
-
// static relations = {
|
|
135
|
-
// posts: new HasMany(() => require('./Post'), 'user_id'),
|
|
136
|
-
// profile: new HasOne(() => require('./Profile'), 'user_id'),
|
|
137
|
-
// role: new BelongsTo(() => require('./Role'), 'role_id'),
|
|
138
|
-
// tags: new BelongsToMany(() => require('./Tag'), '${tableName.replace(/s$/, '')}_tag', '${tableName.replace(/s$/, '')}_id', 'tag_id'),
|
|
139
|
-
// };
|
|
140
|
-
|
|
141
|
-
// ── Scopes ───────────────────────────────────────────────────────
|
|
142
|
-
// static scopes = {
|
|
143
|
-
// active: qb => qb.where('active', true),
|
|
144
|
-
// recent: qb => qb.latest().limit(10),
|
|
145
|
-
// byUser: (qb, userId) => qb.where('user_id', userId),
|
|
146
|
-
// };
|
|
147
|
-
|
|
148
|
-
// ── Validation ───────────────────────────────────────────────────
|
|
149
|
-
// static validate(data) {
|
|
150
|
-
// if (!data.name) throw new Error('name is required');
|
|
151
|
-
// }
|
|
152
|
-
|
|
153
|
-
// ── Lifecycle hooks ──────────────────────────────────────────────
|
|
154
|
-
// static async beforeCreate(data) { return data; }
|
|
155
|
-
// static async afterCreate(instance) {}
|
|
156
|
-
// static async beforeUpdate(data) { return data; }
|
|
157
|
-
// static async afterUpdate(instance) {}
|
|
158
|
-
// static async beforeDelete(instance){}
|
|
159
|
-
// static async afterDelete(instance) {}
|
|
160
124
|
}
|
|
161
125
|
|
|
162
126
|
module.exports = ${className};
|
|
@@ -194,10 +158,11 @@ class ${className} extends Middleware {
|
|
|
194
158
|
* Handle the incoming request.
|
|
195
159
|
* Call next() to continue, or return a response to halt the chain.
|
|
196
160
|
*/
|
|
197
|
-
async handle(req
|
|
198
|
-
//
|
|
161
|
+
async handle({ req }, next) {
|
|
162
|
+
// Destructure what you need: { params, body, query, user, headers, req }
|
|
163
|
+
// Return a MillasResponse to short-circuit, or call next() to continue.
|
|
199
164
|
|
|
200
|
-
next();
|
|
165
|
+
return next();
|
|
201
166
|
}
|
|
202
167
|
}
|
|
203
168
|
|
|
@@ -130,12 +130,13 @@ module.exports = function (Route) {
|
|
|
130
130
|
module.exports = function (Route) {
|
|
131
131
|
Route.prefix('/api').group(() => {
|
|
132
132
|
|
|
133
|
-
Route.get('/health', (
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
Route.get('/health', () =>
|
|
134
|
+
jsonify({ status: 'ok', timestamp: new Date().toISOString() })
|
|
135
|
+
);
|
|
136
136
|
|
|
137
|
-
// Your API routes here
|
|
137
|
+
// Your API routes here:
|
|
138
138
|
// Route.resource('/users', UserController);
|
|
139
|
+
// Route.auth('/auth');
|
|
139
140
|
|
|
140
141
|
});
|
|
141
142
|
};
|
|
@@ -248,22 +249,22 @@ const { ServiceProvider } = require('millas');
|
|
|
248
249
|
*
|
|
249
250
|
* Register and bootstrap your application services here.
|
|
250
251
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
252
|
+
* beforeBoot(container) — runs first, before any register(). Synchronous.
|
|
253
|
+
* Good for patching globals, loading config.
|
|
254
|
+
* register(container) — bind singletons, factories, instances.
|
|
255
|
+
* boot(container, app) — all providers registered. Async OK.
|
|
256
|
+
* Good for routes, event listeners, admin resources.
|
|
253
257
|
*/
|
|
254
258
|
class AppServiceProvider extends ServiceProvider {
|
|
255
259
|
register(container) {
|
|
256
260
|
// container.bind('UserService', UserService);
|
|
257
|
-
// container.singleton('
|
|
261
|
+
// container.singleton('Cache', CacheService);
|
|
258
262
|
// container.instance('Config', require('../config/app'));
|
|
259
263
|
}
|
|
260
264
|
|
|
261
265
|
async boot(container, app) {
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
// Register resources in the Admin panel:
|
|
266
|
-
// const { Admin, AdminResource } = require('millas');
|
|
266
|
+
// Register Admin resources:
|
|
267
|
+
// const { Admin } = require('millas');
|
|
267
268
|
// const Post = require('../app/models/Post');
|
|
268
269
|
// Admin.register(Post);
|
|
269
270
|
}
|