millas 0.1.5 → 0.1.7
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 +135 -754
package/package.json
CHANGED
package/src/commands/migrate.js
CHANGED
|
@@ -156,7 +156,7 @@ async function getDbConnection() {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
async function getRunner() {
|
|
159
|
-
const
|
|
159
|
+
const MigrationRunner = require('../orm/migration/MigrationRunner');
|
|
160
160
|
const ctx = getProjectContext();
|
|
161
161
|
const db = await getDbConnection();
|
|
162
162
|
return new MigrationRunner(db, ctx.migrationsPath);
|
package/src/router/Router.js
CHANGED
|
@@ -6,722 +6,150 @@ 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
|
-
--warn-lt: #fffbeb;
|
|
32
|
-
--text: #1c1917;
|
|
33
|
-
--text2: #44403c;
|
|
34
|
-
--muted: #78716c;
|
|
35
|
-
--faint: #a8a29e;
|
|
36
|
-
}
|
|
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
|
+
}
|
|
37
31
|
|
|
38
|
-
|
|
32
|
+
.container{
|
|
33
|
+
max-width:720px;
|
|
34
|
+
padding:40px;
|
|
35
|
+
}
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
color: var(--text);
|
|
44
|
-
min-height: 100vh;
|
|
45
|
-
line-height: 1.6;
|
|
46
|
-
}
|
|
37
|
+
.icon{
|
|
38
|
+
margin-bottom:20px;
|
|
39
|
+
}
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
padding: 0 32px;
|
|
53
|
-
display: flex;
|
|
54
|
-
align-items: center;
|
|
55
|
-
justify-content: space-between;
|
|
56
|
-
height: 52px;
|
|
57
|
-
position: sticky;
|
|
58
|
-
top: 0;
|
|
59
|
-
z-index: 100;
|
|
60
|
-
}
|
|
61
|
-
.topbar-brand {
|
|
62
|
-
display: flex;
|
|
63
|
-
align-items: center;
|
|
64
|
-
gap: 10px;
|
|
65
|
-
font-family: 'Libre Baskerville', Georgia, serif;
|
|
66
|
-
font-size: 17px;
|
|
67
|
-
font-weight: 700;
|
|
68
|
-
color: var(--text);
|
|
69
|
-
text-decoration: none;
|
|
70
|
-
letter-spacing: -0.2px;
|
|
71
|
-
}
|
|
72
|
-
.topbar-brand .brand-icon {
|
|
73
|
-
width: 28px;
|
|
74
|
-
height: 28px;
|
|
75
|
-
background: var(--primary);
|
|
76
|
-
border-radius: 6px;
|
|
77
|
-
display: flex;
|
|
78
|
-
align-items: center;
|
|
79
|
-
justify-content: center;
|
|
80
|
-
color: white;
|
|
81
|
-
font-size: 13px;
|
|
82
|
-
}
|
|
83
|
-
.topbar-links {
|
|
84
|
-
display: flex;
|
|
85
|
-
align-items: center;
|
|
86
|
-
gap: 4px;
|
|
87
|
-
}
|
|
88
|
-
.topbar-links a {
|
|
89
|
-
display: flex;
|
|
90
|
-
align-items: center;
|
|
91
|
-
gap: 6px;
|
|
92
|
-
padding: 6px 12px;
|
|
93
|
-
border-radius: 6px;
|
|
94
|
-
text-decoration: none;
|
|
95
|
-
font-size: 13px;
|
|
96
|
-
font-weight: 500;
|
|
97
|
-
color: var(--muted);
|
|
98
|
-
transition: background .15s, color .15s;
|
|
99
|
-
}
|
|
100
|
-
.topbar-links a:hover { background: var(--bg); color: var(--text); }
|
|
101
|
-
.topbar-links a i { font-size: 12px; }
|
|
102
|
-
|
|
103
|
-
/* ── Layout ── */
|
|
104
|
-
.layout {
|
|
105
|
-
display: grid;
|
|
106
|
-
grid-template-columns: 220px 1fr;
|
|
107
|
-
min-height: calc(100vh - 52px);
|
|
108
|
-
}
|
|
41
|
+
h1{
|
|
42
|
+
font-size:32px;
|
|
43
|
+
margin-bottom:12px;
|
|
44
|
+
}
|
|
109
45
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
top: 52px;
|
|
117
|
-
height: calc(100vh - 52px);
|
|
118
|
-
overflow-y: auto;
|
|
119
|
-
}
|
|
120
|
-
.sidebar-section {
|
|
121
|
-
padding: 0 16px 20px;
|
|
122
|
-
}
|
|
123
|
-
.sidebar-label {
|
|
124
|
-
font-size: 10px;
|
|
125
|
-
font-weight: 600;
|
|
126
|
-
letter-spacing: 0.8px;
|
|
127
|
-
text-transform: uppercase;
|
|
128
|
-
color: var(--faint);
|
|
129
|
-
padding: 0 8px;
|
|
130
|
-
margin-bottom: 6px;
|
|
131
|
-
}
|
|
132
|
-
.sidebar-link {
|
|
133
|
-
display: flex;
|
|
134
|
-
align-items: center;
|
|
135
|
-
gap: 9px;
|
|
136
|
-
padding: 7px 10px;
|
|
137
|
-
border-radius: 7px;
|
|
138
|
-
text-decoration: none;
|
|
139
|
-
font-size: 13.5px;
|
|
140
|
-
font-weight: 500;
|
|
141
|
-
color: var(--text2);
|
|
142
|
-
transition: background .15s, color .15s;
|
|
143
|
-
margin-bottom: 1px;
|
|
144
|
-
}
|
|
145
|
-
.sidebar-link i {
|
|
146
|
-
width: 16px;
|
|
147
|
-
text-align: center;
|
|
148
|
-
font-size: 13px;
|
|
149
|
-
color: var(--faint);
|
|
150
|
-
flex-shrink: 0;
|
|
151
|
-
}
|
|
152
|
-
.sidebar-link:hover { background: var(--bg); color: var(--text); }
|
|
153
|
-
.sidebar-link:hover i { color: var(--primary); }
|
|
154
|
-
.sidebar-link.active { background: var(--primary-lt); color: var(--primary); }
|
|
155
|
-
.sidebar-link.active i { color: var(--primary); }
|
|
156
|
-
.sidebar-divider {
|
|
157
|
-
height: 1px;
|
|
158
|
-
background: var(--border);
|
|
159
|
-
margin: 4px 16px 20px;
|
|
160
|
-
}
|
|
46
|
+
.subtitle{
|
|
47
|
+
color:#555;
|
|
48
|
+
font-size:16px;
|
|
49
|
+
line-height:1.7;
|
|
50
|
+
margin-bottom:25px;
|
|
51
|
+
}
|
|
161
52
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
53
|
+
code{
|
|
54
|
+
background:#fff4ed;
|
|
55
|
+
color:#ea580c;
|
|
56
|
+
padding:4px 8px;
|
|
57
|
+
border-radius:6px;
|
|
58
|
+
font-size:13px;
|
|
59
|
+
}
|
|
167
60
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
border-radius: 8px;
|
|
174
|
-
padding: 14px 18px;
|
|
175
|
-
display: flex;
|
|
176
|
-
align-items: flex-start;
|
|
177
|
-
gap: 12px;
|
|
178
|
-
margin-bottom: 36px;
|
|
179
|
-
}
|
|
180
|
-
.success-banner i {
|
|
181
|
-
color: var(--accent);
|
|
182
|
-
font-size: 16px;
|
|
183
|
-
margin-top: 1px;
|
|
184
|
-
flex-shrink: 0;
|
|
185
|
-
}
|
|
186
|
-
.success-banner-text { font-size: 14px; color: #065f46; line-height: 1.5; }
|
|
187
|
-
.success-banner-text strong { font-weight: 600; display: block; margin-bottom: 2px; }
|
|
188
|
-
|
|
189
|
-
/* ── Page heading ── */
|
|
190
|
-
.page-heading {
|
|
191
|
-
margin-bottom: 36px;
|
|
192
|
-
padding-bottom: 28px;
|
|
193
|
-
border-bottom: 1px solid var(--border);
|
|
194
|
-
}
|
|
195
|
-
.page-heading h1 {
|
|
196
|
-
font-family: 'Libre Baskerville', Georgia, serif;
|
|
197
|
-
font-size: 30px;
|
|
198
|
-
font-weight: 700;
|
|
199
|
-
letter-spacing: -0.5px;
|
|
200
|
-
line-height: 1.2;
|
|
201
|
-
color: var(--text);
|
|
202
|
-
margin-bottom: 8px;
|
|
203
|
-
}
|
|
204
|
-
.page-heading p {
|
|
205
|
-
font-size: 15px;
|
|
206
|
-
color: var(--muted);
|
|
207
|
-
max-width: 560px;
|
|
208
|
-
}
|
|
209
|
-
.page-heading p code {
|
|
210
|
-
font-family: 'DM Mono', monospace;
|
|
211
|
-
font-size: 12.5px;
|
|
212
|
-
background: var(--bg2);
|
|
213
|
-
color: var(--primary);
|
|
214
|
-
padding: 1px 6px;
|
|
215
|
-
border-radius: 4px;
|
|
216
|
-
border: 1px solid var(--border);
|
|
217
|
-
}
|
|
61
|
+
.steps{
|
|
62
|
+
margin-top:30px;
|
|
63
|
+
text-align:left;
|
|
64
|
+
display:inline-block;
|
|
65
|
+
}
|
|
218
66
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
gap: 14px;
|
|
224
|
-
margin-bottom: 36px;
|
|
225
|
-
}
|
|
226
|
-
.info-card {
|
|
227
|
-
background: var(--white);
|
|
228
|
-
border: 1px solid var(--border);
|
|
229
|
-
border-radius: 9px;
|
|
230
|
-
padding: 16px 18px;
|
|
231
|
-
}
|
|
232
|
-
.info-card-label {
|
|
233
|
-
font-size: 11px;
|
|
234
|
-
font-weight: 600;
|
|
235
|
-
letter-spacing: 0.5px;
|
|
236
|
-
text-transform: uppercase;
|
|
237
|
-
color: var(--faint);
|
|
238
|
-
margin-bottom: 6px;
|
|
239
|
-
display: flex;
|
|
240
|
-
align-items: center;
|
|
241
|
-
gap: 6px;
|
|
242
|
-
}
|
|
243
|
-
.info-card-label i { font-size: 10px; }
|
|
244
|
-
.info-card-value {
|
|
245
|
-
font-size: 14px;
|
|
246
|
-
font-weight: 600;
|
|
247
|
-
color: var(--text);
|
|
248
|
-
display: flex;
|
|
249
|
-
align-items: center;
|
|
250
|
-
gap: 7px;
|
|
251
|
-
}
|
|
252
|
-
.dot-green {
|
|
253
|
-
width: 7px; height: 7px; border-radius: 50%;
|
|
254
|
-
background: var(--accent);
|
|
255
|
-
box-shadow: 0 0 6px rgba(5,150,105,0.4);
|
|
256
|
-
flex-shrink: 0;
|
|
257
|
-
}
|
|
258
|
-
.info-card-value a {
|
|
259
|
-
color: var(--primary);
|
|
260
|
-
text-decoration: none;
|
|
261
|
-
font-weight: 600;
|
|
262
|
-
}
|
|
263
|
-
.info-card-value a:hover { text-decoration: underline; }
|
|
264
|
-
|
|
265
|
-
/* ── Section titles ── */
|
|
266
|
-
.section-title {
|
|
267
|
-
font-size: 12px;
|
|
268
|
-
font-weight: 600;
|
|
269
|
-
letter-spacing: 0.6px;
|
|
270
|
-
text-transform: uppercase;
|
|
271
|
-
color: var(--faint);
|
|
272
|
-
margin-bottom: 14px;
|
|
273
|
-
display: flex;
|
|
274
|
-
align-items: center;
|
|
275
|
-
gap: 8px;
|
|
276
|
-
}
|
|
277
|
-
.section-title::after {
|
|
278
|
-
content: '';
|
|
279
|
-
flex: 1;
|
|
280
|
-
height: 1px;
|
|
281
|
-
background: var(--border);
|
|
282
|
-
}
|
|
67
|
+
.steps h3{
|
|
68
|
+
margin-bottom:10px;
|
|
69
|
+
font-size:16px;
|
|
70
|
+
}
|
|
283
71
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
overflow: hidden;
|
|
290
|
-
margin-bottom: 36px;
|
|
291
|
-
}
|
|
292
|
-
.feature-table-row {
|
|
293
|
-
display: grid;
|
|
294
|
-
grid-template-columns: 40px 1fr;
|
|
295
|
-
align-items: start;
|
|
296
|
-
padding: 14px 18px;
|
|
297
|
-
gap: 14px;
|
|
298
|
-
border-bottom: 1px solid var(--border);
|
|
299
|
-
transition: background .12s;
|
|
300
|
-
}
|
|
301
|
-
.feature-table-row:last-child { border-bottom: none; }
|
|
302
|
-
.feature-table-row:hover { background: var(--bg); }
|
|
303
|
-
.feature-icon-wrap {
|
|
304
|
-
width: 34px; height: 34px;
|
|
305
|
-
background: var(--bg2);
|
|
306
|
-
border: 1px solid var(--border);
|
|
307
|
-
border-radius: 8px;
|
|
308
|
-
display: flex;
|
|
309
|
-
align-items: center;
|
|
310
|
-
justify-content: center;
|
|
311
|
-
flex-shrink: 0;
|
|
312
|
-
margin-top: 1px;
|
|
313
|
-
}
|
|
314
|
-
.feature-icon-wrap i {
|
|
315
|
-
font-size: 14px;
|
|
316
|
-
color: var(--primary);
|
|
317
|
-
}
|
|
318
|
-
.feature-body {}
|
|
319
|
-
.feature-name {
|
|
320
|
-
font-size: 14px;
|
|
321
|
-
font-weight: 600;
|
|
322
|
-
color: var(--text);
|
|
323
|
-
margin-bottom: 2px;
|
|
324
|
-
}
|
|
325
|
-
.feature-desc {
|
|
326
|
-
font-size: 13px;
|
|
327
|
-
color: var(--muted);
|
|
328
|
-
line-height: 1.55;
|
|
329
|
-
}
|
|
330
|
-
.feature-desc code {
|
|
331
|
-
font-family: 'DM Mono', monospace;
|
|
332
|
-
font-size: 11.5px;
|
|
333
|
-
background: var(--bg2);
|
|
334
|
-
color: var(--primary);
|
|
335
|
-
padding: 1px 5px;
|
|
336
|
-
border-radius: 4px;
|
|
337
|
-
border: 1px solid var(--border);
|
|
338
|
-
}
|
|
72
|
+
.steps ul{
|
|
73
|
+
margin:0;
|
|
74
|
+
padding-left:20px;
|
|
75
|
+
color:#555;
|
|
76
|
+
}
|
|
339
77
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
.code-header {
|
|
349
|
-
background: var(--bg2);
|
|
350
|
-
border-bottom: 1px solid var(--border);
|
|
351
|
-
padding: 9px 16px;
|
|
352
|
-
display: flex;
|
|
353
|
-
align-items: center;
|
|
354
|
-
justify-content: space-between;
|
|
355
|
-
gap: 10px;
|
|
356
|
-
}
|
|
357
|
-
.code-header-left {
|
|
358
|
-
display: flex;
|
|
359
|
-
align-items: center;
|
|
360
|
-
gap: 8px;
|
|
361
|
-
}
|
|
362
|
-
.code-header-left i {
|
|
363
|
-
color: var(--faint);
|
|
364
|
-
font-size: 12px;
|
|
365
|
-
}
|
|
366
|
-
.code-filename {
|
|
367
|
-
font-family: 'DM Mono', monospace;
|
|
368
|
-
font-size: 12px;
|
|
369
|
-
color: var(--text2);
|
|
370
|
-
font-weight: 500;
|
|
371
|
-
}
|
|
372
|
-
.code-lang {
|
|
373
|
-
font-size: 11px;
|
|
374
|
-
font-weight: 600;
|
|
375
|
-
letter-spacing: 0.4px;
|
|
376
|
-
text-transform: uppercase;
|
|
377
|
-
color: var(--faint);
|
|
378
|
-
background: var(--border);
|
|
379
|
-
padding: 2px 7px;
|
|
380
|
-
border-radius: 4px;
|
|
381
|
-
}
|
|
382
|
-
pre {
|
|
383
|
-
padding: 20px 22px;
|
|
384
|
-
font-family: 'DM Mono', monospace;
|
|
385
|
-
font-size: 12.5px;
|
|
386
|
-
line-height: 1.75;
|
|
387
|
-
overflow-x: auto;
|
|
388
|
-
color: var(--text2);
|
|
389
|
-
}
|
|
390
|
-
.kw { color: #7c3aed; font-weight: 500; }
|
|
391
|
-
.fn { color: #2563eb; }
|
|
392
|
-
.str { color: #059669; }
|
|
393
|
-
.cm { color: var(--faint); font-style: italic; }
|
|
394
|
-
.cl { color: #b45309; }
|
|
395
|
-
.pm { color: var(--text); }
|
|
396
|
-
.num { color: #dc2626; }
|
|
397
|
-
|
|
398
|
-
/* ── Warning notice ── */
|
|
399
|
-
.notice {
|
|
400
|
-
background: var(--warn-lt);
|
|
401
|
-
border: 1px solid #fde68a;
|
|
402
|
-
border-left: 4px solid var(--warn);
|
|
403
|
-
border-radius: 8px;
|
|
404
|
-
padding: 13px 16px;
|
|
405
|
-
display: flex;
|
|
406
|
-
align-items: flex-start;
|
|
407
|
-
gap: 10px;
|
|
408
|
-
margin-bottom: 36px;
|
|
409
|
-
font-size: 13.5px;
|
|
410
|
-
color: #78350f;
|
|
411
|
-
line-height: 1.55;
|
|
412
|
-
}
|
|
413
|
-
.notice i { color: var(--warn); font-size: 14px; margin-top: 2px; flex-shrink: 0; }
|
|
414
|
-
.notice code {
|
|
415
|
-
font-family: 'DM Mono', monospace;
|
|
416
|
-
font-size: 11.5px;
|
|
417
|
-
background: #fef3c7;
|
|
418
|
-
padding: 1px 5px;
|
|
419
|
-
border-radius: 4px;
|
|
420
|
-
border: 1px solid #fde68a;
|
|
421
|
-
color: #92400e;
|
|
422
|
-
}
|
|
78
|
+
.links{
|
|
79
|
+
margin-top:35px;
|
|
80
|
+
display:flex;
|
|
81
|
+
justify-content:center;
|
|
82
|
+
gap:18px;
|
|
83
|
+
flex-wrap:wrap;
|
|
84
|
+
}
|
|
423
85
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
color: var(--muted);
|
|
445
|
-
text-decoration: none;
|
|
446
|
-
display: flex;
|
|
447
|
-
align-items: center;
|
|
448
|
-
gap: 5px;
|
|
449
|
-
transition: color .15s;
|
|
450
|
-
}
|
|
451
|
-
.page-footer-links a i { font-size: 11px; }
|
|
452
|
-
.page-footer-links a:hover { color: var(--primary); }
|
|
453
|
-
|
|
454
|
-
@media(max-width: 768px) {
|
|
455
|
-
.layout { grid-template-columns: 1fr; }
|
|
456
|
-
.sidebar { display: none; }
|
|
457
|
-
.main { padding: 28px 20px 48px; }
|
|
458
|
-
.info-strip { grid-template-columns: 1fr; }
|
|
459
|
-
}
|
|
460
|
-
</style>
|
|
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>
|
|
461
106
|
</head>
|
|
107
|
+
|
|
462
108
|
<body>
|
|
463
109
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
<a href="#queue" class="sidebar-link">
|
|
508
|
-
<i class="fa-solid fa-layer-group"></i> Queue
|
|
509
|
-
</a>
|
|
510
|
-
<a href="#cache" class="sidebar-link">
|
|
511
|
-
<i class="fa-solid fa-bolt"></i> Cache
|
|
512
|
-
</a>
|
|
513
|
-
<a href="#storage" class="sidebar-link">
|
|
514
|
-
<i class="fa-solid fa-folder-open"></i> Storage
|
|
515
|
-
</a>
|
|
516
|
-
</div>
|
|
517
|
-
|
|
518
|
-
<div class="sidebar-divider"></div>
|
|
519
|
-
|
|
520
|
-
<div class="sidebar-section">
|
|
521
|
-
<div class="sidebar-label">External</div>
|
|
522
|
-
<a href="https://www.npmjs.com/package/millas" target="_blank" class="sidebar-link">
|
|
523
|
-
<i class="fa-brands fa-npm"></i> npm package
|
|
524
|
-
</a>
|
|
525
|
-
<a href="https://github.com/millas-framework/millas" target="_blank" class="sidebar-link">
|
|
526
|
-
<i class="fa-brands fa-github"></i> GitHub
|
|
527
|
-
</a>
|
|
528
|
-
</div>
|
|
529
|
-
</aside>
|
|
530
|
-
|
|
531
|
-
<!-- Main -->
|
|
532
|
-
<main class="main">
|
|
533
|
-
|
|
534
|
-
<!-- Success banner -->
|
|
535
|
-
<div class="success-banner">
|
|
536
|
-
<i class="fa-solid fa-circle-check"></i>
|
|
537
|
-
<div class="success-banner-text">
|
|
538
|
-
<strong>Installation successful!</strong>
|
|
539
|
-
Millas v0.1.2 is running. Define a route at <code style="font-family:'DM Mono',monospace;font-size:12px;background:rgba(5,150,105,0.08);color:#065f46;padding:1px 6px;border-radius:4px;border:1px solid #a7f3d0">GET /</code> in <code style="font-family:'DM Mono',monospace;font-size:12px;background:rgba(5,150,105,0.08);color:#065f46;padding:1px 6px;border-radius:4px;border:1px solid #a7f3d0">routes/web.js</code> to replace this page.
|
|
540
|
-
</div>
|
|
541
|
-
</div>
|
|
542
|
-
|
|
543
|
-
<!-- Heading -->
|
|
544
|
-
<div class="page-heading">
|
|
545
|
-
<h1>Millas Framework</h1>
|
|
546
|
-
<p>
|
|
547
|
-
A Node.js web framework built on Express. This welcome page is shown
|
|
548
|
-
because no route is registered at <code>/</code>. Add one to get started.
|
|
549
|
-
</p>
|
|
550
|
-
</div>
|
|
551
|
-
|
|
552
|
-
<!-- Status strip -->
|
|
553
|
-
<div class="info-strip">
|
|
554
|
-
<div class="info-card">
|
|
555
|
-
<div class="info-card-label"><i class="fa-solid fa-server"></i> Server</div>
|
|
556
|
-
<div class="info-card-value"><span class="dot-green"></span> Running</div>
|
|
557
|
-
</div>
|
|
558
|
-
<div class="info-card">
|
|
559
|
-
<div class="info-card-label"><i class="fa-solid fa-gauge-high"></i> Admin panel</div>
|
|
560
|
-
<div class="info-card-value"><a href="/admin">/admin →</a></div>
|
|
561
|
-
</div>
|
|
562
|
-
<div class="info-card">
|
|
563
|
-
<div class="info-card-label"><i class="fa-solid fa-heart-pulse"></i> API health</div>
|
|
564
|
-
<div class="info-card-value"><a href="/api/health">/api/health →</a></div>
|
|
565
|
-
</div>
|
|
566
|
-
</div>
|
|
567
|
-
|
|
568
|
-
<!-- Notice -->
|
|
569
|
-
<div class="notice">
|
|
570
|
-
<i class="fa-solid fa-triangle-exclamation"></i>
|
|
571
|
-
<span>
|
|
572
|
-
This welcome page is only visible while no <code>GET /</code> route is defined.
|
|
573
|
-
As soon as you add one to <code>routes/web.js</code>, it disappears automatically.
|
|
574
|
-
</span>
|
|
575
|
-
</div>
|
|
576
|
-
|
|
577
|
-
<!-- Included features -->
|
|
578
|
-
<div class="section-title">What’s included</div>
|
|
579
|
-
<div class="feature-table">
|
|
580
|
-
|
|
581
|
-
<div class="feature-table-row" id="routing">
|
|
582
|
-
<div class="feature-icon-wrap"><i class="fa-solid fa-route"></i></div>
|
|
583
|
-
<div class="feature-body">
|
|
584
|
-
<div class="feature-name">Expressive Router</div>
|
|
585
|
-
<div class="feature-desc">Route groups, prefixes, middleware chains, resource routes, and the <code>Route.auth()</code> shortcut for all auth endpoints in one line.</div>
|
|
586
|
-
</div>
|
|
587
|
-
</div>
|
|
588
|
-
|
|
589
|
-
<div class="feature-table-row" id="database">
|
|
590
|
-
<div class="feature-icon-wrap"><i class="fa-solid fa-database"></i></div>
|
|
591
|
-
<div class="feature-body">
|
|
592
|
-
<div class="feature-name">ORM & Migrations</div>
|
|
593
|
-
<div class="feature-desc">Model-driven schema management. Run <code>millas makemigrations</code> to detect changes and <code>millas migrate</code> to apply them.</div>
|
|
594
|
-
</div>
|
|
595
|
-
</div>
|
|
596
|
-
|
|
597
|
-
<div class="feature-table-row" id="auth">
|
|
598
|
-
<div class="feature-icon-wrap"><i class="fa-solid fa-lock"></i></div>
|
|
599
|
-
<div class="feature-body">
|
|
600
|
-
<div class="feature-name">Authentication</div>
|
|
601
|
-
<div class="feature-desc">JWT out of the box. Register, login, token refresh, and password reset — all accessible via <code>Auth.login()</code>.</div>
|
|
602
|
-
</div>
|
|
603
|
-
</div>
|
|
604
|
-
|
|
605
|
-
<div class="feature-table-row" id="mail">
|
|
606
|
-
<div class="feature-icon-wrap"><i class="fa-solid fa-envelope"></i></div>
|
|
607
|
-
<div class="feature-body">
|
|
608
|
-
<div class="feature-name">Mail</div>
|
|
609
|
-
<div class="feature-desc">SMTP, SendGrid, and Mailgun drivers. Template engine with <code>{{ variable }}</code> interpolation, loops, and conditionals.</div>
|
|
610
|
-
</div>
|
|
611
|
-
</div>
|
|
612
|
-
|
|
613
|
-
<div class="feature-table-row" id="queue">
|
|
614
|
-
<div class="feature-icon-wrap"><i class="fa-solid fa-layer-group"></i></div>
|
|
615
|
-
<div class="feature-body">
|
|
616
|
-
<div class="feature-name">Queue System</div>
|
|
617
|
-
<div class="feature-desc">Background job processing with <code>dispatch(new Job())</code>. Database and synchronous drivers included.</div>
|
|
618
|
-
</div>
|
|
619
|
-
</div>
|
|
620
|
-
|
|
621
|
-
<div class="feature-table-row">
|
|
622
|
-
<div class="feature-icon-wrap"><i class="fa-solid fa-satellite-dish"></i></div>
|
|
623
|
-
<div class="feature-body">
|
|
624
|
-
<div class="feature-name">Event System</div>
|
|
625
|
-
<div class="feature-desc">Fire and listen to events across your application. Listeners can run inline or be pushed through the queue.</div>
|
|
626
|
-
</div>
|
|
627
|
-
</div>
|
|
628
|
-
|
|
629
|
-
<div class="feature-table-row" id="cache">
|
|
630
|
-
<div class="feature-icon-wrap"><i class="fa-solid fa-bolt"></i></div>
|
|
631
|
-
<div class="feature-body">
|
|
632
|
-
<div class="feature-name">Cache</div>
|
|
633
|
-
<div class="feature-desc">Memory, file, and null drivers. Tag-based invalidation with <code>Cache.tags('users').flush()</code>.</div>
|
|
634
|
-
</div>
|
|
635
|
-
</div>
|
|
636
|
-
|
|
637
|
-
<div class="feature-table-row" id="storage">
|
|
638
|
-
<div class="feature-icon-wrap"><i class="fa-solid fa-folder-open"></i></div>
|
|
639
|
-
<div class="feature-body">
|
|
640
|
-
<div class="feature-name">File Storage</div>
|
|
641
|
-
<div class="feature-desc">Local disk with multiple named disks. Upload, copy, move, list, and stream files.</div>
|
|
642
|
-
</div>
|
|
643
|
-
</div>
|
|
644
|
-
|
|
645
|
-
<div class="feature-table-row">
|
|
646
|
-
<div class="feature-icon-wrap"><i class="fa-solid fa-shield-halved"></i></div>
|
|
647
|
-
<div class="feature-body">
|
|
648
|
-
<div class="feature-name">Admin Panel</div>
|
|
649
|
-
<div class="feature-desc">Register any model and get a full CRUD dashboard at <a href="/admin" style="color:var(--primary)">/admin</a> automatically — no extra configuration required.</div>
|
|
650
|
-
</div>
|
|
651
|
-
</div>
|
|
652
|
-
|
|
653
|
-
</div>
|
|
654
|
-
|
|
655
|
-
<!-- Quick start code -->
|
|
656
|
-
<div class="section-title">Quick start</div>
|
|
657
|
-
<div class="code-wrap">
|
|
658
|
-
<div class="code-header">
|
|
659
|
-
<div class="code-header-left">
|
|
660
|
-
<i class="fa-regular fa-file-code"></i>
|
|
661
|
-
<span class="code-filename">routes/web.js</span>
|
|
662
|
-
</div>
|
|
663
|
-
<span class="code-lang">JavaScript</span>
|
|
664
|
-
</div>
|
|
665
|
-
<pre><span class="cm">// Replace this welcome page by defining GET /</span>
|
|
666
|
-
<span class="kw">module</span>.<span class="fn">exports</span> = <span class="kw">function</span> (<span class="pm">Route</span>) {
|
|
667
|
-
|
|
668
|
-
<span class="pm">Route</span>.<span class="fn">get</span>(<span class="str">'/'</span>, (<span class="pm">req</span>, <span class="pm">res</span>) => {
|
|
669
|
-
<span class="pm">res</span>.<span class="fn">json</span>({ message: <span class="str">'Hello from Millas!'</span> });
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
<span class="cm">// Register a full resource (index, show, store, update, destroy)</span>
|
|
673
|
-
<span class="pm">Route</span>.<span class="fn">resource</span>(<span class="str">'/posts'</span>, <span class="cl">PostController</span>);
|
|
674
|
-
|
|
675
|
-
<span class="cm">// Protect routes with auth middleware</span>
|
|
676
|
-
<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>(() => {
|
|
677
|
-
<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>);
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
<span class="cm">// All auth routes in one line</span>
|
|
681
|
-
<span class="pm">Route</span>.<span class="fn">auth</span>(<span class="str">'/auth'</span>);
|
|
682
|
-
|
|
683
|
-
};</pre>
|
|
684
|
-
</div>
|
|
685
|
-
|
|
686
|
-
<!-- CLI reference -->
|
|
687
|
-
<div class="section-title">CLI reference</div>
|
|
688
|
-
<div class="code-wrap">
|
|
689
|
-
<div class="code-header">
|
|
690
|
-
<div class="code-header-left">
|
|
691
|
-
<i class="fa-solid fa-terminal"></i>
|
|
692
|
-
<span class="code-filename">terminal</span>
|
|
693
|
-
</div>
|
|
694
|
-
<span class="code-lang">Shell</span>
|
|
695
|
-
</div>
|
|
696
|
-
<pre><span class="cm"># Generate files</span>
|
|
697
|
-
millas make:controller <span class="cl">PostController</span> --resource
|
|
698
|
-
millas make:model <span class="cl">Post</span> --migration
|
|
699
|
-
millas make:middleware <span class="cl">AdminOnly</span>
|
|
700
|
-
millas make:job <span class="cl">SendEmailJob</span>
|
|
701
|
-
|
|
702
|
-
<span class="cm"># Database</span>
|
|
703
|
-
millas makemigrations <span class="cm"># detect model changes</span>
|
|
704
|
-
millas migrate <span class="cm"># run pending migrations</span>
|
|
705
|
-
millas migrate:rollback <span class="cm"># undo last batch</span>
|
|
706
|
-
|
|
707
|
-
<span class="cm"># Utilities</span>
|
|
708
|
-
millas route:list <span class="cm"># show all registered routes</span>
|
|
709
|
-
millas queue:work <span class="cm"># start background job worker</span></pre>
|
|
710
|
-
</div>
|
|
711
|
-
|
|
712
|
-
<!-- Footer -->
|
|
713
|
-
<div class="page-footer">
|
|
714
|
-
<div class="page-footer-left">Millas v0.1.2 — Built on Node.js & Express</div>
|
|
715
|
-
<div class="page-footer-links">
|
|
716
|
-
<a href="/admin"><i class="fa-solid fa-gauge-high"></i> Admin</a>
|
|
717
|
-
<a href="/api/health"><i class="fa-solid fa-heart-pulse"></i> Health</a>
|
|
718
|
-
<a href="https://www.npmjs.com/package/millas" target="_blank"><i class="fa-brands fa-npm"></i> npm</a>
|
|
719
|
-
<a href="https://github.com/millas-framework/millas" target="_blank"><i class="fa-brands fa-github"></i> GitHub</a>
|
|
720
|
-
</div>
|
|
721
|
-
</div>
|
|
722
|
-
|
|
723
|
-
</main>
|
|
724
|
-
</div>
|
|
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>
|
|
725
153
|
|
|
726
154
|
</body>
|
|
727
155
|
</html>`;
|
|
@@ -736,22 +164,12 @@ millas queue:work <span class="cm"># start background job worker</span
|
|
|
736
164
|
* rejections are forwarded to Express error handlers.
|
|
737
165
|
*/
|
|
738
166
|
class Router {
|
|
739
|
-
/**
|
|
740
|
-
* @param {object} expressApp — the Express application
|
|
741
|
-
* @param {RouteRegistry} registry
|
|
742
|
-
* @param {MiddlewareRegistry} middlewareRegistry
|
|
743
|
-
*/
|
|
744
167
|
constructor(expressApp, registry, middlewareRegistry) {
|
|
745
168
|
this._app = expressApp;
|
|
746
169
|
this._registry = registry;
|
|
747
170
|
this._mw = middlewareRegistry || new MiddlewareRegistry();
|
|
748
171
|
}
|
|
749
172
|
|
|
750
|
-
/**
|
|
751
|
-
* Bind all registered routes onto the Express app.
|
|
752
|
-
* Does NOT add 404/error handlers — call mountFallbacks() after
|
|
753
|
-
* all other middleware (like Admin) has been registered.
|
|
754
|
-
*/
|
|
755
173
|
mountRoutes() {
|
|
756
174
|
const routes = this._registry.all();
|
|
757
175
|
for (const route of routes) {
|
|
@@ -760,13 +178,7 @@ class Router {
|
|
|
760
178
|
return this;
|
|
761
179
|
}
|
|
762
180
|
|
|
763
|
-
/**
|
|
764
|
-
* Add the 404 + global error handlers.
|
|
765
|
-
* Must be called LAST — after all routes and admin panels.
|
|
766
|
-
*/
|
|
767
181
|
mountFallbacks() {
|
|
768
|
-
// If no route defined for GET /, show a Django-style welcome page.
|
|
769
|
-
// Users override this by defining Route.get('/') in routes/web.js
|
|
770
182
|
const hasRootRoute = this._registry.all().some(
|
|
771
183
|
r => r.verb === 'GET' && r.path === '/'
|
|
772
184
|
);
|
|
@@ -778,7 +190,6 @@ class Router {
|
|
|
778
190
|
});
|
|
779
191
|
}
|
|
780
192
|
|
|
781
|
-
// 404 handler
|
|
782
193
|
this._app.use((req, res) => {
|
|
783
194
|
res.status(404).json({
|
|
784
195
|
error: 'Not Found',
|
|
@@ -787,7 +198,6 @@ class Router {
|
|
|
787
198
|
});
|
|
788
199
|
});
|
|
789
200
|
|
|
790
|
-
// Global error handler
|
|
791
201
|
this._app.use((err, req, res, _next) => {
|
|
792
202
|
const status = err.status || err.statusCode || 500;
|
|
793
203
|
const message = err.message || 'Internal Server Error';
|
|
@@ -807,35 +217,20 @@ class Router {
|
|
|
807
217
|
return this;
|
|
808
218
|
}
|
|
809
219
|
|
|
810
|
-
/**
|
|
811
|
-
* Bind all registered routes onto the Express app
|
|
812
|
-
* AND add 404/error handlers (original behaviour).
|
|
813
|
-
*/
|
|
814
220
|
mount() {
|
|
815
221
|
const routes = this._registry.all();
|
|
816
|
-
|
|
817
222
|
for (const route of routes) {
|
|
818
223
|
this._bindRoute(route);
|
|
819
224
|
}
|
|
820
|
-
|
|
821
|
-
// Re-use mountFallbacks for consistency
|
|
822
225
|
this.mountFallbacks();
|
|
823
226
|
return this;
|
|
824
227
|
}
|
|
825
228
|
|
|
826
|
-
// ─── Private ──────────────────────────────────────────────────────────────
|
|
827
|
-
|
|
828
229
|
_bindRoute(route) {
|
|
829
|
-
const verb
|
|
830
|
-
const path
|
|
831
|
-
|
|
832
|
-
// Resolve middleware chain
|
|
230
|
+
const verb = route.verb.toLowerCase();
|
|
231
|
+
const path = route.path;
|
|
833
232
|
const mwHandlers = this._resolveMiddleware(route.middleware || []);
|
|
834
|
-
|
|
835
|
-
// Resolve the terminal handler
|
|
836
|
-
const terminal = this._resolveHandler(route.handler, route.method);
|
|
837
|
-
|
|
838
|
-
// Register on Express: app.get(path, [...mw], handler)
|
|
233
|
+
const terminal = this._resolveHandler(route.handler, route.method);
|
|
839
234
|
this._app[verb](path, ...mwHandlers, terminal);
|
|
840
235
|
}
|
|
841
236
|
|
|
@@ -851,42 +246,28 @@ class Router {
|
|
|
851
246
|
}
|
|
852
247
|
|
|
853
248
|
_resolveHandler(handler, method) {
|
|
854
|
-
// Case 1: raw async/sync function
|
|
855
249
|
if (typeof handler === 'function' && !method) {
|
|
856
250
|
return this._wrapAsync(handler);
|
|
857
251
|
}
|
|
858
|
-
|
|
859
|
-
// Case 2: controller class + method name string
|
|
860
252
|
if (typeof handler === 'function' && typeof method === 'string') {
|
|
861
253
|
const instance = new handler();
|
|
862
254
|
if (typeof instance[method] !== 'function') {
|
|
863
|
-
throw new Error(
|
|
864
|
-
`Method "${method}" not found on controller "${handler.name}".`
|
|
865
|
-
);
|
|
255
|
+
throw new Error(`Method "${method}" not found on controller "${handler.name}".`);
|
|
866
256
|
}
|
|
867
257
|
return this._wrapAsync(instance[method].bind(instance));
|
|
868
258
|
}
|
|
869
|
-
|
|
870
|
-
// Case 3: already-instantiated object + method name
|
|
871
259
|
if (typeof handler === 'object' && handler !== null && typeof method === 'string') {
|
|
872
260
|
if (typeof handler[method] !== 'function') {
|
|
873
261
|
throw new Error(`Method "${method}" not found on handler object.`);
|
|
874
262
|
}
|
|
875
263
|
return this._wrapAsync(handler[method].bind(handler));
|
|
876
264
|
}
|
|
877
|
-
|
|
878
|
-
// Case 4: plain object/function with no method (fallback)
|
|
879
265
|
if (typeof handler === 'function') {
|
|
880
266
|
return this._wrapAsync(handler);
|
|
881
267
|
}
|
|
882
|
-
|
|
883
268
|
throw new Error(`Invalid route handler: ${JSON.stringify(handler)}`);
|
|
884
269
|
}
|
|
885
270
|
|
|
886
|
-
/**
|
|
887
|
-
* Wrap an async function so rejections are forwarded to next(err).
|
|
888
|
-
* Sync functions pass through unchanged.
|
|
889
|
-
*/
|
|
890
271
|
_wrapAsync(fn) {
|
|
891
272
|
if (fn.constructor.name === 'AsyncFunction') {
|
|
892
273
|
return (req, res, next) => fn(req, res, next).catch(next);
|