millas 0.1.4 → 0.1.6
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 +1 -1
- package/src/commands/migrate.js +1 -1
- package/src/router/Router.js +139 -480
package/package.json
CHANGED
package/src/commands/migrate.js
CHANGED
|
@@ -11,7 +11,7 @@ module.exports = function (program) {
|
|
|
11
11
|
.description('Detect model changes and generate migration files')
|
|
12
12
|
.action(async () => {
|
|
13
13
|
const ctx = getProjectContext();
|
|
14
|
-
const
|
|
14
|
+
const ModelInspector = require('../orm/migration/ModelInspector');
|
|
15
15
|
const inspector = new ModelInspector(
|
|
16
16
|
ctx.modelsPath,
|
|
17
17
|
ctx.migrationsPath,
|
package/src/router/Router.js
CHANGED
|
@@ -6,445 +6,151 @@ const MiddlewareRegistry = require('./MiddlewareRegistry');
|
|
|
6
6
|
const WELCOME_PAGE = `<!DOCTYPE html>
|
|
7
7
|
<html lang="en">
|
|
8
8
|
<head>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
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
|
+
}
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
.container{
|
|
33
|
+
max-width:720px;
|
|
34
|
+
padding:40px;
|
|
35
|
+
}
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
color: var(--text);
|
|
39
|
-
min-height: 100vh;
|
|
40
|
-
line-height: 1.6;
|
|
41
|
-
overflow-x: hidden;
|
|
42
|
-
}
|
|
37
|
+
.icon{
|
|
38
|
+
margin-bottom:20px;
|
|
39
|
+
}
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
inset: 0;
|
|
49
|
-
background-image:
|
|
50
|
-
linear-gradient(rgba(99,102,241,0.03) 1px, transparent 1px),
|
|
51
|
-
linear-gradient(90deg, rgba(99,102,241,0.03) 1px, transparent 1px);
|
|
52
|
-
background-size: 40px 40px;
|
|
53
|
-
pointer-events: none;
|
|
54
|
-
z-index: 0;
|
|
55
|
-
}
|
|
41
|
+
h1{
|
|
42
|
+
font-size:32px;
|
|
43
|
+
margin-bottom:12px;
|
|
44
|
+
}
|
|
56
45
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
z-index: 0;
|
|
64
|
-
opacity: 0.5;
|
|
65
|
-
}
|
|
66
|
-
.orb-1 { width: 500px; height: 500px; background: radial-gradient(circle, rgba(99,102,241,0.15), transparent 70%); top: -100px; left: -100px; animation: drift1 18s ease-in-out infinite; }
|
|
67
|
-
.orb-2 { width: 400px; height: 400px; background: radial-gradient(circle, rgba(168,85,247,0.1), transparent 70%); bottom: -80px; right: -80px; animation: drift2 22s ease-in-out infinite; }
|
|
68
|
-
@keyframes drift1 { 0%,100%{transform:translate(0,0)} 50%{transform:translate(60px,40px)} }
|
|
69
|
-
@keyframes drift2 { 0%,100%{transform:translate(0,0)} 50%{transform:translate(-40px,-60px)} }
|
|
70
|
-
|
|
71
|
-
/* ── Layout ── */
|
|
72
|
-
.page { position: relative; z-index: 1; max-width: 900px; margin: 0 auto; padding: 60px 24px 80px; }
|
|
73
|
-
|
|
74
|
-
/* ── Hero ── */
|
|
75
|
-
.hero { text-align: center; margin-bottom: 64px; }
|
|
76
|
-
|
|
77
|
-
.logo-wrap {
|
|
78
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
79
|
-
width: 80px; height: 80px; border-radius: 22px;
|
|
80
|
-
background: linear-gradient(135deg, #6366f1, #a855f7);
|
|
81
|
-
font-size: 36px; margin-bottom: 28px;
|
|
82
|
-
box-shadow: 0 0 40px rgba(99,102,241,0.3), 0 0 80px rgba(99,102,241,0.1);
|
|
83
|
-
animation: pulse 3s ease-in-out infinite;
|
|
84
|
-
}
|
|
85
|
-
@keyframes pulse { 0%,100%{box-shadow:0 0 40px rgba(99,102,241,0.3),0 0 80px rgba(99,102,241,0.1)} 50%{box-shadow:0 0 60px rgba(99,102,241,0.5),0 0 120px rgba(99,102,241,0.2)} }
|
|
86
|
-
|
|
87
|
-
.version-badge {
|
|
88
|
-
display: inline-flex; align-items: center; gap: 6px;
|
|
89
|
-
background: var(--surface2); border: 1px solid var(--border2);
|
|
90
|
-
color: var(--primary-h); border-radius: 99px;
|
|
91
|
-
padding: 4px 14px; font-size: 12px; font-weight: 600;
|
|
92
|
-
margin-bottom: 24px; letter-spacing: 0.3px;
|
|
93
|
-
}
|
|
94
|
-
.version-dot { width: 6px; height: 6px; background: var(--green); border-radius: 50%; animation: blink 2s ease-in-out infinite; }
|
|
95
|
-
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
|
|
96
|
-
|
|
97
|
-
h1 {
|
|
98
|
-
font-size: clamp(36px, 6vw, 56px);
|
|
99
|
-
font-weight: 700;
|
|
100
|
-
letter-spacing: -1.5px;
|
|
101
|
-
line-height: 1.1;
|
|
102
|
-
margin-bottom: 20px;
|
|
103
|
-
background: linear-gradient(135deg, #fff 0%, #94a3b8 100%);
|
|
104
|
-
-webkit-background-clip: text;
|
|
105
|
-
-webkit-text-fill-color: transparent;
|
|
106
|
-
background-clip: text;
|
|
107
|
-
}
|
|
108
|
-
h1 span {
|
|
109
|
-
background: linear-gradient(135deg, #6366f1, #a855f7);
|
|
110
|
-
-webkit-background-clip: text;
|
|
111
|
-
-webkit-text-fill-color: transparent;
|
|
112
|
-
background-clip: text;
|
|
113
|
-
}
|
|
46
|
+
.subtitle{
|
|
47
|
+
color:#555;
|
|
48
|
+
font-size:16px;
|
|
49
|
+
line-height:1.7;
|
|
50
|
+
margin-bottom:25px;
|
|
51
|
+
}
|
|
114
52
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
53
|
+
code{
|
|
54
|
+
background:#fff4ed;
|
|
55
|
+
color:#ea580c;
|
|
56
|
+
padding:4px 8px;
|
|
57
|
+
border-radius:6px;
|
|
58
|
+
font-size:13px;
|
|
59
|
+
}
|
|
122
60
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
padding: 11px 22px; border-radius: 10px;
|
|
129
|
-
font-size: 14px; font-weight: 600;
|
|
130
|
-
text-decoration: none; transition: all .2s;
|
|
131
|
-
font-family: inherit; cursor: pointer; border: none;
|
|
132
|
-
}
|
|
133
|
-
.btn-primary {
|
|
134
|
-
background: linear-gradient(135deg, #6366f1, #7c3aed);
|
|
135
|
-
color: #fff;
|
|
136
|
-
box-shadow: 0 4px 20px rgba(99,102,241,0.3);
|
|
137
|
-
}
|
|
138
|
-
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 30px rgba(99,102,241,0.4); }
|
|
139
|
-
.btn-ghost {
|
|
140
|
-
background: var(--surface); color: var(--soft);
|
|
141
|
-
border: 1px solid var(--border2);
|
|
142
|
-
}
|
|
143
|
-
.btn-ghost:hover { background: var(--surface2); color: var(--text); border-color: var(--primary); }
|
|
61
|
+
.steps{
|
|
62
|
+
margin-top:30px;
|
|
63
|
+
text-align:left;
|
|
64
|
+
display:inline-block;
|
|
65
|
+
}
|
|
144
66
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
letter-spacing: 1px; font-weight: 600;
|
|
150
|
-
}
|
|
151
|
-
.divider::before {
|
|
152
|
-
content: '';
|
|
153
|
-
position: absolute; top: 50%; left: 0; right: 0;
|
|
154
|
-
height: 1px; background: var(--border);
|
|
155
|
-
}
|
|
156
|
-
.divider span { background: var(--bg); padding: 0 16px; position: relative; }
|
|
157
|
-
|
|
158
|
-
/* ── Feature grid ── */
|
|
159
|
-
.features {
|
|
160
|
-
display: grid;
|
|
161
|
-
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
162
|
-
gap: 16px;
|
|
163
|
-
margin-bottom: 48px;
|
|
164
|
-
}
|
|
165
|
-
.feature {
|
|
166
|
-
background: var(--surface);
|
|
167
|
-
border: 1px solid var(--border);
|
|
168
|
-
border-radius: 14px;
|
|
169
|
-
padding: 24px;
|
|
170
|
-
transition: border-color .2s, transform .2s;
|
|
171
|
-
}
|
|
172
|
-
.feature:hover { border-color: var(--primary); transform: translateY(-3px); }
|
|
173
|
-
.feature-icon { font-size: 26px; margin-bottom: 12px; }
|
|
174
|
-
.feature-title { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 6px; }
|
|
175
|
-
.feature-desc { font-size: 13px; color: var(--muted); line-height: 1.6; }
|
|
176
|
-
|
|
177
|
-
/* ── Code block ── */
|
|
178
|
-
.code-section { margin-bottom: 48px; }
|
|
179
|
-
.code-section h2 { font-size: 16px; font-weight: 600; margin-bottom: 16px; color: var(--soft); }
|
|
180
|
-
.code-block {
|
|
181
|
-
background: var(--surface);
|
|
182
|
-
border: 1px solid var(--border);
|
|
183
|
-
border-radius: 12px;
|
|
184
|
-
overflow: hidden;
|
|
185
|
-
}
|
|
186
|
-
.code-header {
|
|
187
|
-
background: var(--surface2);
|
|
188
|
-
padding: 10px 16px;
|
|
189
|
-
border-bottom: 1px solid var(--border);
|
|
190
|
-
display: flex; align-items: center; gap: 8px;
|
|
191
|
-
font-size: 12px; color: var(--muted);
|
|
192
|
-
}
|
|
193
|
-
.code-dots { display: flex; gap: 6px; }
|
|
194
|
-
.code-dot { width: 10px; height: 10px; border-radius: 50%; }
|
|
195
|
-
.code-dot:nth-child(1){background:#ef4444} .code-dot:nth-child(2){background:#f59e0b} .code-dot:nth-child(3){background:#22c55e}
|
|
196
|
-
.code-filename { font-family: 'JetBrains Mono', monospace; margin-left: 4px; }
|
|
197
|
-
pre {
|
|
198
|
-
padding: 20px 24px;
|
|
199
|
-
font-family: 'JetBrains Mono', monospace;
|
|
200
|
-
font-size: 13px;
|
|
201
|
-
line-height: 1.7;
|
|
202
|
-
overflow-x: auto;
|
|
203
|
-
color: #94a3b8;
|
|
204
|
-
}
|
|
205
|
-
.kw { color: #c084fc; } /* keywords */
|
|
206
|
-
.fn { color: #818cf8; } /* functions */
|
|
207
|
-
.str { color: #34d399; } /* strings */
|
|
208
|
-
.cm { color: #475569; font-style: italic; } /* comments */
|
|
209
|
-
.cl { color: #f8fafc; } /* class names */
|
|
210
|
-
.pm { color: #f59e0b; } /* parameters */
|
|
211
|
-
|
|
212
|
-
/* ── Status strip ── */
|
|
213
|
-
.status-strip {
|
|
214
|
-
display: flex; gap: 12px; flex-wrap: wrap;
|
|
215
|
-
margin-bottom: 48px;
|
|
216
|
-
}
|
|
217
|
-
.status-item {
|
|
218
|
-
flex: 1; min-width: 160px;
|
|
219
|
-
background: var(--surface);
|
|
220
|
-
border: 1px solid var(--border);
|
|
221
|
-
border-radius: 12px;
|
|
222
|
-
padding: 16px 20px;
|
|
223
|
-
display: flex; align-items: center; gap: 12px;
|
|
224
|
-
}
|
|
225
|
-
.status-icon { font-size: 20px; }
|
|
226
|
-
.status-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
|
227
|
-
.status-value { font-size: 14px; font-weight: 600; color: var(--text); }
|
|
228
|
-
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px rgba(34,197,94,0.5); flex-shrink: 0; }
|
|
229
|
-
|
|
230
|
-
/* ── Footer ── */
|
|
231
|
-
footer {
|
|
232
|
-
text-align: center;
|
|
233
|
-
padding-top: 40px;
|
|
234
|
-
border-top: 1px solid var(--border);
|
|
235
|
-
color: var(--muted);
|
|
236
|
-
font-size: 13px;
|
|
237
|
-
display: flex; flex-direction: column; gap: 12px; align-items: center;
|
|
238
|
-
}
|
|
239
|
-
.footer-links { display: flex; gap: 24px; }
|
|
240
|
-
.footer-links a { color: var(--muted); text-decoration: none; font-size: 13px; }
|
|
241
|
-
.footer-links a:hover { color: var(--primary-h); }
|
|
242
|
-
|
|
243
|
-
/* ── Notice ── */
|
|
244
|
-
.notice {
|
|
245
|
-
background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(168,85,247,0.05));
|
|
246
|
-
border: 1px solid rgba(99,102,241,0.2);
|
|
247
|
-
border-radius: 12px;
|
|
248
|
-
padding: 16px 20px;
|
|
249
|
-
font-size: 13px;
|
|
250
|
-
color: var(--soft);
|
|
251
|
-
margin-bottom: 40px;
|
|
252
|
-
display: flex; align-items: flex-start; gap: 12px;
|
|
253
|
-
}
|
|
254
|
-
.notice-icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
|
|
255
|
-
code { font-family: 'JetBrains Mono', monospace; background: var(--surface2); color: var(--primary-h); padding: 1px 7px; border-radius: 4px; font-size: 12px; }
|
|
67
|
+
.steps h3{
|
|
68
|
+
margin-bottom:10px;
|
|
69
|
+
font-size:16px;
|
|
70
|
+
}
|
|
256
71
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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>
|
|
263
106
|
</head>
|
|
107
|
+
|
|
264
108
|
<body>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
<div class="status-label">Admin Panel</div>
|
|
311
|
-
<div class="status-value"><a href="/admin" style="color:var(--primary-h);text-decoration:none">/admin →</a></div>
|
|
312
|
-
</div>
|
|
313
|
-
</div>
|
|
314
|
-
<div class="status-item">
|
|
315
|
-
<span class="status-icon">🌐</span>
|
|
316
|
-
<div>
|
|
317
|
-
<div class="status-label">API</div>
|
|
318
|
-
<div class="status-value"><a href="/api/health" style="color:var(--primary-h);text-decoration:none">/api/health →</a></div>
|
|
319
|
-
</div>
|
|
320
|
-
</div>
|
|
321
|
-
<div class="status-item">
|
|
322
|
-
<span class="status-icon">📦</span>
|
|
323
|
-
<div>
|
|
324
|
-
<div class="status-label">Framework</div>
|
|
325
|
-
<div class="status-value">Millas 0.1.2</div>
|
|
326
|
-
</div>
|
|
327
|
-
</div>
|
|
328
|
-
</div>
|
|
329
|
-
|
|
330
|
-
<div class="divider"><span>What's included</span></div>
|
|
331
|
-
|
|
332
|
-
<!-- Features -->
|
|
333
|
-
<div class="features">
|
|
334
|
-
<div class="feature">
|
|
335
|
-
<div class="feature-icon">🛣️</div>
|
|
336
|
-
<div class="feature-title">Expressive Router</div>
|
|
337
|
-
<div class="feature-desc">Route groups, prefixes, middleware, resource routes, and the <code>Route.auth()</code> shortcut.</div>
|
|
338
|
-
</div>
|
|
339
|
-
<div class="feature">
|
|
340
|
-
<div class="feature-icon">🗃️</div>
|
|
341
|
-
<div class="feature-title">ORM + Migrations</div>
|
|
342
|
-
<div class="feature-desc">Model-driven schema with <code>millas makemigrations</code> and <code>millas migrate</code>.</div>
|
|
343
|
-
</div>
|
|
344
|
-
<div class="feature">
|
|
345
|
-
<div class="feature-icon">🔐</div>
|
|
346
|
-
<div class="feature-title">Authentication</div>
|
|
347
|
-
<div class="feature-desc">JWT out of the box. Register, login, refresh, password reset — all via <code>Auth.login()</code>.</div>
|
|
348
|
-
</div>
|
|
349
|
-
<div class="feature">
|
|
350
|
-
<div class="feature-icon">📬</div>
|
|
351
|
-
<div class="feature-title">Mail</div>
|
|
352
|
-
<div class="feature-desc">SMTP, SendGrid, Mailgun. Template engine with <code>{{ variable }}</code>, loops, and conditionals.</div>
|
|
353
|
-
</div>
|
|
354
|
-
<div class="feature">
|
|
355
|
-
<div class="feature-icon">⚙️</div>
|
|
356
|
-
<div class="feature-title">Queue System</div>
|
|
357
|
-
<div class="feature-desc">Background jobs with <code>dispatch(new Job())</code>. Database and sync drivers included.</div>
|
|
358
|
-
</div>
|
|
359
|
-
<div class="feature">
|
|
360
|
-
<div class="feature-icon">📡</div>
|
|
361
|
-
<div class="feature-title">Event System</div>
|
|
362
|
-
<div class="feature-desc">Fire and listen to events. Listeners can run inline or through the queue.</div>
|
|
363
|
-
</div>
|
|
364
|
-
<div class="feature">
|
|
365
|
-
<div class="feature-icon">⚡</div>
|
|
366
|
-
<div class="feature-title">Cache</div>
|
|
367
|
-
<div class="feature-desc">Memory, file, and null drivers. Tag-based invalidation with <code>Cache.tags('users').flush()</code>.</div>
|
|
368
|
-
</div>
|
|
369
|
-
<div class="feature">
|
|
370
|
-
<div class="feature-icon">🗂️</div>
|
|
371
|
-
<div class="feature-title">File Storage</div>
|
|
372
|
-
<div class="feature-desc">Local disk with multiple named disks. Upload, copy, move, list, stream files.</div>
|
|
373
|
-
</div>
|
|
374
|
-
<div class="feature">
|
|
375
|
-
<div class="feature-icon">🛡️</div>
|
|
376
|
-
<div class="feature-title">Admin Panel</div>
|
|
377
|
-
<div class="feature-desc">Register any model and get a full CRUD dashboard at <a href="/admin" style="color:var(--primary-h)">/admin</a> automatically.</div>
|
|
378
|
-
</div>
|
|
379
|
-
</div>
|
|
380
|
-
|
|
381
|
-
<!-- Quick start code -->
|
|
382
|
-
<div class="code-section">
|
|
383
|
-
<h2>Your first route</h2>
|
|
384
|
-
<div class="code-block">
|
|
385
|
-
<div class="code-header">
|
|
386
|
-
<div class="code-dots"><div class="code-dot"></div><div class="code-dot"></div><div class="code-dot"></div></div>
|
|
387
|
-
<span class="code-filename">routes/web.js</span>
|
|
388
|
-
</div>
|
|
389
|
-
<pre><span class="cm">// Replace this welcome page by defining GET /</span>
|
|
390
|
-
<span class="kw">module</span>.<span class="fn">exports</span> = <span class="kw">function</span> (<span class="pm">Route</span>) {
|
|
391
|
-
|
|
392
|
-
<span class="pm">Route</span>.<span class="fn">get</span>(<span class="str">'/'</span>, (<span class="pm">req</span>, <span class="pm">res</span>) => {
|
|
393
|
-
<span class="pm">res</span>.<span class="fn">json</span>({ message: <span class="str">'Hello from Millas!'</span> });
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
<span class="cm">// Register a full resource (index, show, store, update, destroy)</span>
|
|
397
|
-
<span class="pm">Route</span>.<span class="fn">resource</span>(<span class="str">'/posts'</span>, <span class="cl">PostController</span>);
|
|
398
|
-
|
|
399
|
-
<span class="cm">// Protect routes with auth middleware</span>
|
|
400
|
-
<span class="pm">Route</span>.<span class="fn">prefix</span>(<span class="str">'/api'</span>).<span class="fn">middleware</span>([<span class="str">'auth'</span>]).<span class="fn">group</span>(() => {
|
|
401
|
-
<span class="pm">Route</span>.<span class="fn">get</span>(<span class="str">'/me'</span>, <span class="cl">UserController</span>, <span class="str">'me'</span>);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
<span class="cm">// All auth routes in one line</span>
|
|
405
|
-
<span class="pm">Route</span>.<span class="fn">auth</span>(<span class="str">'/auth'</span>);
|
|
406
|
-
|
|
407
|
-
};</pre>
|
|
408
|
-
</div>
|
|
409
|
-
</div>
|
|
410
|
-
|
|
411
|
-
<!-- CLI quick reference -->
|
|
412
|
-
<div class="code-section">
|
|
413
|
-
<h2>Useful CLI commands</h2>
|
|
414
|
-
<div class="code-block">
|
|
415
|
-
<div class="code-header">
|
|
416
|
-
<div class="code-dots"><div class="code-dot"></div><div class="code-dot"></div><div class="code-dot"></div></div>
|
|
417
|
-
<span class="code-filename">terminal</span>
|
|
418
|
-
</div>
|
|
419
|
-
<pre><span class="cm"># Generate files</span>
|
|
420
|
-
millas make:controller <span class="cl">PostController</span> --resource
|
|
421
|
-
millas make:model <span class="cl">Post</span> --migration
|
|
422
|
-
millas make:middleware <span class="cl">AdminOnly</span>
|
|
423
|
-
millas make:job <span class="cl">SendEmailJob</span>
|
|
424
|
-
|
|
425
|
-
<span class="cm"># Database</span>
|
|
426
|
-
millas makemigrations <span class="cm"># detect model changes</span>
|
|
427
|
-
millas migrate <span class="cm"># run pending migrations</span>
|
|
428
|
-
millas migrate:rollback <span class="cm"># undo last batch</span>
|
|
429
|
-
|
|
430
|
-
<span class="cm"># Utilities</span>
|
|
431
|
-
millas route:list <span class="cm"># show all registered routes</span>
|
|
432
|
-
millas queue:work <span class="cm"># start background job worker</span></pre>
|
|
433
|
-
</div>
|
|
434
|
-
</div>
|
|
435
|
-
|
|
436
|
-
<!-- Footer -->
|
|
437
|
-
<footer>
|
|
438
|
-
<div class="footer-links">
|
|
439
|
-
<a href="/admin">Admin Panel</a>
|
|
440
|
-
<a href="/api/health">API Health</a>
|
|
441
|
-
<a href="https://www.npmjs.com/package/millas" target="_blank">npm</a>
|
|
442
|
-
<a href="https://github.com/millas-framework/millas" target="_blank">GitHub</a>
|
|
443
|
-
</div>
|
|
444
|
-
<span>Built with Millas · Node.js · Express</span>
|
|
445
|
-
</footer>
|
|
446
|
-
|
|
447
|
-
</div>
|
|
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
|
+
|
|
448
154
|
</body>
|
|
449
155
|
</html>`;
|
|
450
156
|
|
|
@@ -458,22 +164,12 @@ millas queue:work <span class="cm"># start background job worker</span
|
|
|
458
164
|
* rejections are forwarded to Express error handlers.
|
|
459
165
|
*/
|
|
460
166
|
class Router {
|
|
461
|
-
/**
|
|
462
|
-
* @param {object} expressApp — the Express application
|
|
463
|
-
* @param {RouteRegistry} registry
|
|
464
|
-
* @param {MiddlewareRegistry} middlewareRegistry
|
|
465
|
-
*/
|
|
466
167
|
constructor(expressApp, registry, middlewareRegistry) {
|
|
467
168
|
this._app = expressApp;
|
|
468
169
|
this._registry = registry;
|
|
469
170
|
this._mw = middlewareRegistry || new MiddlewareRegistry();
|
|
470
171
|
}
|
|
471
172
|
|
|
472
|
-
/**
|
|
473
|
-
* Bind all registered routes onto the Express app.
|
|
474
|
-
* Does NOT add 404/error handlers — call mountFallbacks() after
|
|
475
|
-
* all other middleware (like Admin) has been registered.
|
|
476
|
-
*/
|
|
477
173
|
mountRoutes() {
|
|
478
174
|
const routes = this._registry.all();
|
|
479
175
|
for (const route of routes) {
|
|
@@ -482,13 +178,7 @@ class Router {
|
|
|
482
178
|
return this;
|
|
483
179
|
}
|
|
484
180
|
|
|
485
|
-
/**
|
|
486
|
-
* Add the 404 + global error handlers.
|
|
487
|
-
* Must be called LAST — after all routes and admin panels.
|
|
488
|
-
*/
|
|
489
181
|
mountFallbacks() {
|
|
490
|
-
// If no route defined for GET /, show a Django-style welcome page.
|
|
491
|
-
// Users override this by defining Route.get('/') in routes/web.js
|
|
492
182
|
const hasRootRoute = this._registry.all().some(
|
|
493
183
|
r => r.verb === 'GET' && r.path === '/'
|
|
494
184
|
);
|
|
@@ -500,7 +190,6 @@ class Router {
|
|
|
500
190
|
});
|
|
501
191
|
}
|
|
502
192
|
|
|
503
|
-
// 404 handler
|
|
504
193
|
this._app.use((req, res) => {
|
|
505
194
|
res.status(404).json({
|
|
506
195
|
error: 'Not Found',
|
|
@@ -509,7 +198,6 @@ class Router {
|
|
|
509
198
|
});
|
|
510
199
|
});
|
|
511
200
|
|
|
512
|
-
// Global error handler
|
|
513
201
|
this._app.use((err, req, res, _next) => {
|
|
514
202
|
const status = err.status || err.statusCode || 500;
|
|
515
203
|
const message = err.message || 'Internal Server Error';
|
|
@@ -529,35 +217,20 @@ class Router {
|
|
|
529
217
|
return this;
|
|
530
218
|
}
|
|
531
219
|
|
|
532
|
-
/**
|
|
533
|
-
* Bind all registered routes onto the Express app
|
|
534
|
-
* AND add 404/error handlers (original behaviour).
|
|
535
|
-
*/
|
|
536
220
|
mount() {
|
|
537
221
|
const routes = this._registry.all();
|
|
538
|
-
|
|
539
222
|
for (const route of routes) {
|
|
540
223
|
this._bindRoute(route);
|
|
541
224
|
}
|
|
542
|
-
|
|
543
|
-
// Re-use mountFallbacks for consistency
|
|
544
225
|
this.mountFallbacks();
|
|
545
226
|
return this;
|
|
546
227
|
}
|
|
547
228
|
|
|
548
|
-
// ─── Private ──────────────────────────────────────────────────────────────
|
|
549
|
-
|
|
550
229
|
_bindRoute(route) {
|
|
551
|
-
const verb
|
|
552
|
-
const path
|
|
553
|
-
|
|
554
|
-
// Resolve middleware chain
|
|
230
|
+
const verb = route.verb.toLowerCase();
|
|
231
|
+
const path = route.path;
|
|
555
232
|
const mwHandlers = this._resolveMiddleware(route.middleware || []);
|
|
556
|
-
|
|
557
|
-
// Resolve the terminal handler
|
|
558
|
-
const terminal = this._resolveHandler(route.handler, route.method);
|
|
559
|
-
|
|
560
|
-
// Register on Express: app.get(path, [...mw], handler)
|
|
233
|
+
const terminal = this._resolveHandler(route.handler, route.method);
|
|
561
234
|
this._app[verb](path, ...mwHandlers, terminal);
|
|
562
235
|
}
|
|
563
236
|
|
|
@@ -573,42 +246,28 @@ class Router {
|
|
|
573
246
|
}
|
|
574
247
|
|
|
575
248
|
_resolveHandler(handler, method) {
|
|
576
|
-
// Case 1: raw async/sync function
|
|
577
249
|
if (typeof handler === 'function' && !method) {
|
|
578
250
|
return this._wrapAsync(handler);
|
|
579
251
|
}
|
|
580
|
-
|
|
581
|
-
// Case 2: controller class + method name string
|
|
582
252
|
if (typeof handler === 'function' && typeof method === 'string') {
|
|
583
253
|
const instance = new handler();
|
|
584
254
|
if (typeof instance[method] !== 'function') {
|
|
585
|
-
throw new Error(
|
|
586
|
-
`Method "${method}" not found on controller "${handler.name}".`
|
|
587
|
-
);
|
|
255
|
+
throw new Error(`Method "${method}" not found on controller "${handler.name}".`);
|
|
588
256
|
}
|
|
589
257
|
return this._wrapAsync(instance[method].bind(instance));
|
|
590
258
|
}
|
|
591
|
-
|
|
592
|
-
// Case 3: already-instantiated object + method name
|
|
593
259
|
if (typeof handler === 'object' && handler !== null && typeof method === 'string') {
|
|
594
260
|
if (typeof handler[method] !== 'function') {
|
|
595
261
|
throw new Error(`Method "${method}" not found on handler object.`);
|
|
596
262
|
}
|
|
597
263
|
return this._wrapAsync(handler[method].bind(handler));
|
|
598
264
|
}
|
|
599
|
-
|
|
600
|
-
// Case 4: plain object/function with no method (fallback)
|
|
601
265
|
if (typeof handler === 'function') {
|
|
602
266
|
return this._wrapAsync(handler);
|
|
603
267
|
}
|
|
604
|
-
|
|
605
268
|
throw new Error(`Invalid route handler: ${JSON.stringify(handler)}`);
|
|
606
269
|
}
|
|
607
270
|
|
|
608
|
-
/**
|
|
609
|
-
* Wrap an async function so rejections are forwarded to next(err).
|
|
610
|
-
* Sync functions pass through unchanged.
|
|
611
|
-
*/
|
|
612
271
|
_wrapAsync(fn) {
|
|
613
272
|
if (fn.constructor.name === 'AsyncFunction') {
|
|
614
273
|
return (req, res, next) => fn(req, res, next).catch(next);
|