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/package.json
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "millas",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12-beta",
|
|
4
4
|
"description": "A modern batteries-included backend framework for Node.js — built on Express, inspired by Laravel, Django, and FastAPI",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js",
|
|
8
8
|
"./src": "./src/index.js",
|
|
9
9
|
"./src/*": "./src/*.js",
|
|
10
|
-
"./bin/*": "./bin/*.js"
|
|
10
|
+
"./bin/*": "./bin/*.js",
|
|
11
|
+
"./validation": "./src/validation/Validator.js",
|
|
12
|
+
"./facades/Http": "./src/facades/Http.js",
|
|
13
|
+
"./facades/Validation": "./src/facades/Validation.js",
|
|
14
|
+
"./facades/Database": "./src/facades/Database.js",
|
|
15
|
+
"./facades/Auth": "./src/facades/Auth.js",
|
|
16
|
+
"./facades/Log": "./src/facades/Log.js",
|
|
17
|
+
"./facades/Cache": "./src/facades/Cache.js",
|
|
18
|
+
"./facades/Mail": "./src/facades/Mail.js",
|
|
19
|
+
"./facades/Queue": "./src/facades/Queue.js",
|
|
20
|
+
"./facades/Events": "./src/facades/Events.js",
|
|
21
|
+
"./facades/Storage": "./src/facades/Storage.js",
|
|
22
|
+
"./facades/Admin": "./src/facades/Admin.js"
|
|
11
23
|
},
|
|
12
24
|
"bin": {
|
|
13
25
|
"millas": "./bin/millas.js"
|
|
@@ -39,6 +51,7 @@
|
|
|
39
51
|
"dependencies": {
|
|
40
52
|
"bcryptjs": "3.0.2",
|
|
41
53
|
"chalk": "4.1.2",
|
|
54
|
+
"chokidar": "^3.6.0",
|
|
42
55
|
"commander": "^11.0.0",
|
|
43
56
|
"fs-extra": "^11.0.0",
|
|
44
57
|
"inquirer": "8.2.6",
|
|
@@ -46,7 +59,8 @@
|
|
|
46
59
|
"knex": "^3.1.0",
|
|
47
60
|
"nodemailer": "^6.9.0",
|
|
48
61
|
"nunjucks": "^3.2.4",
|
|
49
|
-
"ora": "5.4.1"
|
|
62
|
+
"ora": "5.4.1",
|
|
63
|
+
"sqlite3": "^5.1.7"
|
|
50
64
|
},
|
|
51
65
|
"peerDependencies": {
|
|
52
66
|
"express": "^4.18.0"
|
|
@@ -2,62 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
const Controller = require('../controller/Controller');
|
|
4
4
|
const Auth = require('./Auth');
|
|
5
|
+
const { string, email } = require('../validation/Validator');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* AuthController
|
|
8
9
|
*
|
|
9
|
-
* Drop-in authentication controller.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* const AuthController = require('millas/src/auth/AuthController');
|
|
13
|
-
*
|
|
14
|
-
* Route.post('/auth/register', AuthController, 'register');
|
|
15
|
-
* Route.post('/auth/login', AuthController, 'login');
|
|
16
|
-
* Route.post('/auth/logout', AuthController, 'logout');
|
|
17
|
-
* Route.get('/auth/me', AuthController, 'me');
|
|
18
|
-
* Route.post('/auth/refresh', AuthController, 'refresh');
|
|
19
|
-
* Route.post('/auth/forgot-password', AuthController, 'forgotPassword');
|
|
20
|
-
* Route.post('/auth/reset-password', AuthController, 'resetPassword');
|
|
21
|
-
*
|
|
22
|
-
* Or use the convenience helper:
|
|
23
|
-
* Route.auth() — registers all routes above under /auth
|
|
10
|
+
* Drop-in authentication controller using the new ctx signature.
|
|
11
|
+
* All methods receive RequestContext and return MillasResponse.
|
|
24
12
|
*/
|
|
25
13
|
class AuthController extends Controller {
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const data = await this.validate(req, {
|
|
33
|
-
name: 'required|string|min:2|max:100',
|
|
34
|
-
email: 'required|email',
|
|
35
|
-
password: 'required|string|min:8',
|
|
15
|
+
async register({ body }) {
|
|
16
|
+
const data = await body.validate({
|
|
17
|
+
name: string().required().min(2).max(100),
|
|
18
|
+
email: email().required(),
|
|
19
|
+
password: string().required().min(8),
|
|
36
20
|
});
|
|
37
21
|
|
|
38
22
|
const user = await Auth.register(data);
|
|
39
23
|
const token = Auth.issueToken(user);
|
|
40
24
|
|
|
41
|
-
return this.created(
|
|
25
|
+
return this.created({
|
|
42
26
|
message: 'Registration successful',
|
|
43
27
|
user: this._safeUser(user),
|
|
44
28
|
token,
|
|
45
29
|
});
|
|
46
30
|
}
|
|
47
31
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
async login(req, res) {
|
|
53
|
-
const { email, password } = await this.validate(req, {
|
|
54
|
-
email: 'required|email',
|
|
55
|
-
password: 'required|string',
|
|
32
|
+
async login({ body }) {
|
|
33
|
+
const { email: emailVal, password } = await body.validate({
|
|
34
|
+
email: email().required(),
|
|
35
|
+
password: string().required(),
|
|
56
36
|
});
|
|
57
37
|
|
|
58
|
-
const { user, token, refreshToken } = await Auth.login(
|
|
38
|
+
const { user, token, refreshToken } = await Auth.login(emailVal, password);
|
|
59
39
|
|
|
60
|
-
return this.ok(
|
|
40
|
+
return this.ok({
|
|
61
41
|
message: 'Login successful',
|
|
62
42
|
user: this._safeUser(user),
|
|
63
43
|
token,
|
|
@@ -65,123 +45,52 @@ class AuthController extends Controller {
|
|
|
65
45
|
});
|
|
66
46
|
}
|
|
67
47
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
* Header: Authorization: Bearer <token>
|
|
71
|
-
*
|
|
72
|
-
* JWT is stateless — logout just instructs the client to discard the token.
|
|
73
|
-
* Phase 11 (cache) will add token blocklisting.
|
|
74
|
-
*/
|
|
75
|
-
async logout(req, res) {
|
|
76
|
-
return this.ok(res, { message: 'Logged out successfully' });
|
|
48
|
+
async logout() {
|
|
49
|
+
return this.ok({ message: 'Logged out successfully' });
|
|
77
50
|
}
|
|
78
51
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
async me(req, res) {
|
|
84
|
-
try {
|
|
85
|
-
const user = await Auth.userOrFail(req);
|
|
86
|
-
return this.ok(res, { user: this._safeUser(user) });
|
|
87
|
-
} catch (err) {
|
|
88
|
-
if (err.status === 401) return this.unauthorized(res, err.message);
|
|
89
|
-
return this.unauthorized(res, 'Unauthenticated');
|
|
52
|
+
async me({ user }) {
|
|
53
|
+
if (!user) {
|
|
54
|
+
const HttpError = require('../errors/HttpError');
|
|
55
|
+
throw new HttpError(401, 'Unauthenticated');
|
|
90
56
|
}
|
|
57
|
+
return this.ok({ user: this._safeUser(user) });
|
|
91
58
|
}
|
|
92
59
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
*/
|
|
97
|
-
async refresh(req, res) {
|
|
98
|
-
const { refresh_token } = await this.validate(req, {
|
|
99
|
-
refresh_token: 'required|string',
|
|
60
|
+
async refresh({ body }) {
|
|
61
|
+
const { refresh_token } = await body.validate({
|
|
62
|
+
refresh_token: string().required(),
|
|
100
63
|
});
|
|
101
64
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
payload = Auth.verify(refresh_token);
|
|
106
|
-
} catch {
|
|
107
|
-
return this.unauthorized(res, 'Invalid or expired refresh token');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const user = await Auth.user({ headers: { authorization: `Bearer ${refresh_token}` } });
|
|
111
|
-
if (!user) return this.unauthorized(res, 'User not found');
|
|
112
|
-
|
|
113
|
-
const newToken = Auth.issueToken(user);
|
|
114
|
-
const newRefreshToken = Auth.issueToken(user, { expiresIn: '30d' });
|
|
115
|
-
|
|
116
|
-
return this.ok(res, {
|
|
117
|
-
token: newToken,
|
|
118
|
-
refresh_token: newRefreshToken,
|
|
119
|
-
});
|
|
65
|
+
const tokens = await Auth.refresh(refresh_token);
|
|
66
|
+
return this.ok(tokens);
|
|
120
67
|
}
|
|
121
68
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
*/
|
|
126
|
-
async forgotPassword(req, res) {
|
|
127
|
-
const { email } = await this.validate(req, {
|
|
128
|
-
email: 'required|email',
|
|
69
|
+
async forgotPassword({ body }) {
|
|
70
|
+
const { email: emailVal } = await body.validate({
|
|
71
|
+
email: email().required(),
|
|
129
72
|
});
|
|
130
73
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const user = await Auth.user({ headers: {} });
|
|
134
|
-
if (user) {
|
|
135
|
-
const resetToken = Auth.generateResetToken(user);
|
|
136
|
-
// Phase 8: Mail.send({ to: email, template: 'password-reset', data: { resetToken } })
|
|
137
|
-
// For now, return token in dev mode only
|
|
138
|
-
if (process.env.APP_ENV !== 'production') {
|
|
139
|
-
return this.ok(res, {
|
|
140
|
-
message: 'Reset link sent (dev mode — token exposed)',
|
|
141
|
-
reset_token: resetToken,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
} catch { /* silent */ }
|
|
146
|
-
|
|
147
|
-
return this.ok(res, {
|
|
148
|
-
message: 'If that email exists, a reset link has been sent.',
|
|
149
|
-
});
|
|
74
|
+
await Auth.sendPasswordResetEmail(emailVal);
|
|
75
|
+
return this.ok({ message: 'Password reset email sent' });
|
|
150
76
|
}
|
|
151
77
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
async resetPassword(req, res) {
|
|
157
|
-
const { token, password } = await this.validate(req, {
|
|
158
|
-
token: 'required|string',
|
|
159
|
-
password: 'required|string|min:8',
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
const payload = Auth.verifyResetToken(token);
|
|
163
|
-
const user = await Auth.user({
|
|
164
|
-
headers: { authorization: `Bearer ${token}` },
|
|
78
|
+
async resetPassword({ body }) {
|
|
79
|
+
const { token, password } = await body.validate({
|
|
80
|
+
token: string().required(),
|
|
81
|
+
password: string().required().min(8).confirmed(),
|
|
165
82
|
});
|
|
166
83
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const hashed = await Auth.hashPassword(password);
|
|
172
|
-
await user.update({ password: hashed });
|
|
173
|
-
|
|
174
|
-
return this.ok(res, { message: 'Password reset successfully' });
|
|
84
|
+
await Auth.resetPassword(token, password);
|
|
85
|
+
return this.ok({ message: 'Password reset successfully' });
|
|
175
86
|
}
|
|
176
87
|
|
|
177
|
-
// ─── Internal ─────────────────────────────────────────────────────────────
|
|
178
|
-
|
|
179
88
|
_safeUser(user) {
|
|
180
89
|
if (!user) return null;
|
|
181
|
-
const
|
|
182
|
-
delete
|
|
183
|
-
delete
|
|
184
|
-
return
|
|
90
|
+
const data = user.toJSON ? user.toJSON() : { ...user };
|
|
91
|
+
delete data.password;
|
|
92
|
+
delete data.remember_token;
|
|
93
|
+
return data;
|
|
185
94
|
}
|
|
186
95
|
}
|
|
187
96
|
|
|
@@ -8,25 +8,15 @@ const Auth = require('./Auth');
|
|
|
8
8
|
* AuthMiddleware
|
|
9
9
|
*
|
|
10
10
|
* Guards routes from unauthenticated access using JWT.
|
|
11
|
-
*
|
|
12
11
|
* Reads the Bearer token from the Authorization header,
|
|
13
|
-
* verifies it, loads the user
|
|
14
|
-
* and attaches them to req.user.
|
|
15
|
-
*
|
|
16
|
-
* Throws 401 if:
|
|
17
|
-
* - No Authorization header
|
|
18
|
-
* - Token is malformed / expired
|
|
19
|
-
* - User no longer exists in DB
|
|
12
|
+
* verifies it, loads the user, and attaches them to req.user.
|
|
20
13
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* Apply to routes:
|
|
25
|
-
* Route.prefix('/api').middleware(['auth']).group(() => { ... })
|
|
14
|
+
* Uses the Millas middleware signature: handle(req, next)
|
|
15
|
+
* No Express res — returns a MillasResponse or calls next().
|
|
26
16
|
*/
|
|
27
17
|
class AuthMiddleware extends Middleware {
|
|
28
|
-
async handle(
|
|
29
|
-
const header =
|
|
18
|
+
async handle({ headers, req }, next) {
|
|
19
|
+
const header = headers.authorization || headers.Authorization;
|
|
30
20
|
|
|
31
21
|
if (!header) {
|
|
32
22
|
throw new HttpError(401, 'Authorization header missing');
|
|
@@ -41,26 +31,25 @@ class AuthMiddleware extends Middleware {
|
|
|
41
31
|
throw new HttpError(401, 'Token is empty');
|
|
42
32
|
}
|
|
43
33
|
|
|
44
|
-
// Verify token — throws 401 if expired or invalid
|
|
45
34
|
const payload = Auth.verify(token);
|
|
46
35
|
|
|
47
|
-
// Load user from DB
|
|
48
36
|
let user;
|
|
49
37
|
try {
|
|
50
|
-
user = await Auth.user(req);
|
|
38
|
+
user = await Auth.user(req.raw);
|
|
51
39
|
} catch {
|
|
52
40
|
throw new HttpError(401, 'Authentication service not configured');
|
|
53
41
|
}
|
|
42
|
+
|
|
54
43
|
if (!user) {
|
|
55
44
|
throw new HttpError(401, 'User not found or has been deleted');
|
|
56
45
|
}
|
|
57
46
|
|
|
58
|
-
// Attach to request
|
|
59
|
-
req.user
|
|
60
|
-
req.token
|
|
61
|
-
req.tokenPayload = payload;
|
|
47
|
+
// Attach to the underlying request so downstream handlers see req.user
|
|
48
|
+
req.raw.user = user;
|
|
49
|
+
req.raw.token = token;
|
|
50
|
+
req.raw.tokenPayload = payload;
|
|
62
51
|
|
|
63
|
-
next();
|
|
52
|
+
return next();
|
|
64
53
|
}
|
|
65
54
|
}
|
|
66
55
|
|
|
@@ -6,38 +6,28 @@ const HttpError = require('../errors/HttpError');
|
|
|
6
6
|
/**
|
|
7
7
|
* RoleMiddleware
|
|
8
8
|
*
|
|
9
|
-
* Restricts
|
|
10
|
-
* Must
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* middlewareRegistry.register('admin', new RoleMiddleware(['admin']));
|
|
14
|
-
* middlewareRegistry.register('staff', new RoleMiddleware(['admin', 'staff']));
|
|
15
|
-
*
|
|
16
|
-
* Route.prefix('/admin').middleware(['auth', 'admin']).group(() => { ... });
|
|
9
|
+
* Restricts access to users with specific roles.
|
|
10
|
+
* Must run after AuthMiddleware (requires ctx.user).
|
|
17
11
|
*/
|
|
18
12
|
class RoleMiddleware extends Middleware {
|
|
19
|
-
/**
|
|
20
|
-
* @param {string[]} roles — list of allowed role values
|
|
21
|
-
*/
|
|
22
13
|
constructor(roles = []) {
|
|
23
14
|
super();
|
|
24
15
|
this.roles = Array.isArray(roles) ? roles : [roles];
|
|
25
16
|
}
|
|
26
17
|
|
|
27
|
-
async handle(
|
|
28
|
-
if (!
|
|
29
|
-
throw new HttpError(401, 'Unauthenticated —
|
|
18
|
+
async handle({ user }, next) {
|
|
19
|
+
if (!user) {
|
|
20
|
+
throw new HttpError(401, 'Unauthenticated — AuthMiddleware must run first');
|
|
30
21
|
}
|
|
31
22
|
|
|
32
|
-
const userRole =
|
|
33
|
-
|
|
23
|
+
const userRole = user.role || null;
|
|
34
24
|
if (!userRole || !this.roles.includes(userRole)) {
|
|
35
25
|
throw new HttpError(403,
|
|
36
26
|
`Access denied. Required role: ${this.roles.join(' or ')}`
|
|
37
27
|
);
|
|
38
28
|
}
|
|
39
29
|
|
|
40
|
-
next();
|
|
30
|
+
return next();
|
|
41
31
|
}
|
|
42
32
|
}
|
|
43
33
|
|
package/src/commands/migrate.js
CHANGED
|
@@ -6,17 +6,15 @@ const fs = require('fs-extra');
|
|
|
6
6
|
|
|
7
7
|
module.exports = function (program) {
|
|
8
8
|
|
|
9
|
-
// ── makemigrations
|
|
10
|
-
|
|
9
|
+
// ── makemigrations ──────────────────────────────────────────────────────────
|
|
11
10
|
program
|
|
12
11
|
.command('makemigrations')
|
|
13
12
|
.description('Scan model files, detect schema changes, generate migration files')
|
|
14
13
|
.action(async () => {
|
|
15
14
|
try {
|
|
16
|
-
const ctx
|
|
17
|
-
// Fixed: was incorrectly destructured as { ModelInspector }
|
|
15
|
+
const ctx = getProjectContext();
|
|
18
16
|
const ModelInspector = require('../orm/migration/ModelInspector');
|
|
19
|
-
const inspector
|
|
17
|
+
const inspector = new ModelInspector(
|
|
20
18
|
ctx.modelsPath,
|
|
21
19
|
ctx.migrationsPath,
|
|
22
20
|
ctx.snapshotPath,
|
|
@@ -31,14 +29,12 @@ module.exports = function (program) {
|
|
|
31
29
|
console.log(chalk.gray('\n Run: millas migrate to apply these migrations.\n'));
|
|
32
30
|
}
|
|
33
31
|
} catch (err) {
|
|
34
|
-
|
|
35
|
-
if (process.env.DEBUG) console.error(err.stack);
|
|
36
|
-
process.exit(1);
|
|
32
|
+
bail('makemigrations', err);
|
|
37
33
|
}
|
|
34
|
+
// makemigrations doesn't open a DB connection so no closeDb() needed
|
|
38
35
|
});
|
|
39
36
|
|
|
40
|
-
// ── migrate
|
|
41
|
-
|
|
37
|
+
// ── migrate ─────────────────────────────────────────────────────────────────
|
|
42
38
|
program
|
|
43
39
|
.command('migrate')
|
|
44
40
|
.description('Run all pending migrations')
|
|
@@ -46,14 +42,15 @@ module.exports = function (program) {
|
|
|
46
42
|
try {
|
|
47
43
|
const runner = await getRunner();
|
|
48
44
|
const result = await runner.migrate();
|
|
49
|
-
|
|
45
|
+
printMigrationResult(result, 'Ran');
|
|
50
46
|
} catch (err) {
|
|
51
47
|
bail('migrate', err);
|
|
48
|
+
} finally {
|
|
49
|
+
await closeDb();
|
|
52
50
|
}
|
|
53
51
|
});
|
|
54
52
|
|
|
55
|
-
// ── migrate:fresh
|
|
56
|
-
|
|
53
|
+
// ── migrate:fresh ────────────────────────────────────────────────────────────
|
|
57
54
|
program
|
|
58
55
|
.command('migrate:fresh')
|
|
59
56
|
.description('Drop ALL tables then re-run every migration from scratch')
|
|
@@ -62,14 +59,15 @@ module.exports = function (program) {
|
|
|
62
59
|
console.log(chalk.yellow('\n ⚠ Dropping all tables…\n'));
|
|
63
60
|
const runner = await getRunner();
|
|
64
61
|
const result = await runner.fresh();
|
|
65
|
-
|
|
62
|
+
printMigrationResult(result, 'Ran');
|
|
66
63
|
} catch (err) {
|
|
67
64
|
bail('migrate:fresh', err);
|
|
65
|
+
} finally {
|
|
66
|
+
await closeDb();
|
|
68
67
|
}
|
|
69
68
|
});
|
|
70
69
|
|
|
71
|
-
// ── migrate:rollback
|
|
72
|
-
|
|
70
|
+
// ── migrate:rollback ─────────────────────────────────────────────────────────
|
|
73
71
|
program
|
|
74
72
|
.command('migrate:rollback')
|
|
75
73
|
.description('Rollback the last batch of migrations')
|
|
@@ -78,14 +76,15 @@ module.exports = function (program) {
|
|
|
78
76
|
try {
|
|
79
77
|
const runner = await getRunner();
|
|
80
78
|
const result = await runner.rollback(Number(options.steps));
|
|
81
|
-
|
|
79
|
+
printMigrationResult(result, 'Rolled back');
|
|
82
80
|
} catch (err) {
|
|
83
81
|
bail('migrate:rollback', err);
|
|
82
|
+
} finally {
|
|
83
|
+
await closeDb();
|
|
84
84
|
}
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
// ── migrate:reset
|
|
88
|
-
|
|
87
|
+
// ── migrate:reset ────────────────────────────────────────────────────────────
|
|
89
88
|
program
|
|
90
89
|
.command('migrate:reset')
|
|
91
90
|
.description('Rollback ALL migrations')
|
|
@@ -93,14 +92,15 @@ module.exports = function (program) {
|
|
|
93
92
|
try {
|
|
94
93
|
const runner = await getRunner();
|
|
95
94
|
const result = await runner.reset();
|
|
96
|
-
|
|
95
|
+
printMigrationResult(result, 'Rolled back');
|
|
97
96
|
} catch (err) {
|
|
98
97
|
bail('migrate:reset', err);
|
|
98
|
+
} finally {
|
|
99
|
+
await closeDb();
|
|
99
100
|
}
|
|
100
101
|
});
|
|
101
102
|
|
|
102
|
-
// ── migrate:refresh
|
|
103
|
-
|
|
103
|
+
// ── migrate:refresh ──────────────────────────────────────────────────────────
|
|
104
104
|
program
|
|
105
105
|
.command('migrate:refresh')
|
|
106
106
|
.description('Rollback all then re-run all migrations')
|
|
@@ -108,14 +108,15 @@ module.exports = function (program) {
|
|
|
108
108
|
try {
|
|
109
109
|
const runner = await getRunner();
|
|
110
110
|
const result = await runner.refresh();
|
|
111
|
-
|
|
111
|
+
printMigrationResult(result, 'Ran');
|
|
112
112
|
} catch (err) {
|
|
113
113
|
bail('migrate:refresh', err);
|
|
114
|
+
} finally {
|
|
115
|
+
await closeDb();
|
|
114
116
|
}
|
|
115
117
|
});
|
|
116
118
|
|
|
117
|
-
// ── migrate:status
|
|
118
|
-
|
|
119
|
+
// ── migrate:status ───────────────────────────────────────────────────────────
|
|
119
120
|
program
|
|
120
121
|
.command('migrate:status')
|
|
121
122
|
.description('Show the status of all migration files')
|
|
@@ -143,11 +144,12 @@ module.exports = function (program) {
|
|
|
143
144
|
console.log();
|
|
144
145
|
} catch (err) {
|
|
145
146
|
bail('migrate:status', err);
|
|
147
|
+
} finally {
|
|
148
|
+
await closeDb();
|
|
146
149
|
}
|
|
147
150
|
});
|
|
148
151
|
|
|
149
|
-
// ── db:seed
|
|
150
|
-
|
|
152
|
+
// ── db:seed ──────────────────────────────────────────────────────────────────
|
|
151
153
|
program
|
|
152
154
|
.command('db:seed')
|
|
153
155
|
.description('Run all database seeders')
|
|
@@ -180,6 +182,8 @@ module.exports = function (program) {
|
|
|
180
182
|
console.log();
|
|
181
183
|
} catch (err) {
|
|
182
184
|
bail('db:seed', err);
|
|
185
|
+
} finally {
|
|
186
|
+
await closeDb();
|
|
183
187
|
}
|
|
184
188
|
});
|
|
185
189
|
};
|
|
@@ -208,14 +212,25 @@ async function getDbConnection() {
|
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
async function getRunner() {
|
|
211
|
-
// Fixed: was incorrectly destructured as { MigrationRunner }
|
|
212
215
|
const MigrationRunner = require('../orm/migration/MigrationRunner');
|
|
213
216
|
const ctx = getProjectContext();
|
|
214
217
|
const db = await getDbConnection();
|
|
215
218
|
return new MigrationRunner(db, ctx.migrationsPath);
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Destroy all open knex connection pools so the CLI process exits cleanly.
|
|
223
|
+
* Without this, knex keeps the event loop alive indefinitely after the
|
|
224
|
+
* command finishes, causing the terminal to appear to hang.
|
|
225
|
+
*/
|
|
226
|
+
async function closeDb() {
|
|
227
|
+
try {
|
|
228
|
+
const DatabaseManager = require('../orm/drivers/DatabaseManager');
|
|
229
|
+
await DatabaseManager.closeAll();
|
|
230
|
+
} catch { /* already closed or never opened — safe to ignore */ }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function printMigrationResult(result, verb) {
|
|
219
234
|
const list = result.ran || result.rolledBack || [];
|
|
220
235
|
if (list.length === 0) {
|
|
221
236
|
console.log(chalk.yellow(`\n ${result.message}\n`));
|
|
@@ -223,7 +238,7 @@ function printResult(result, verb) {
|
|
|
223
238
|
}
|
|
224
239
|
console.log(chalk.green(`\n ✔ ${result.message}`));
|
|
225
240
|
list.forEach(f =>
|
|
226
|
-
console.log(chalk.cyan(` ${verb === 'Ran' ? '+' : '-'} ${f}`))
|
|
241
|
+
console.log(chalk.cyan(` ${verb === 'Ran' ? '+' : '-'} ${f}`))
|
|
227
242
|
);
|
|
228
243
|
console.log();
|
|
229
244
|
}
|
|
@@ -231,5 +246,5 @@ function printResult(result, verb) {
|
|
|
231
246
|
function bail(cmd, err) {
|
|
232
247
|
console.error(chalk.red(`\n ✖ ${cmd} failed: ${err.message}\n`));
|
|
233
248
|
if (process.env.DEBUG) console.error(err.stack);
|
|
234
|
-
process.exit(1);
|
|
249
|
+
closeDb().finally(() => process.exit(1));
|
|
235
250
|
}
|