millas 0.2.10 → 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.
Files changed (48) hide show
  1. package/package.json +17 -3
  2. package/src/auth/AuthController.js +42 -133
  3. package/src/auth/AuthMiddleware.js +12 -23
  4. package/src/auth/RoleMiddleware.js +7 -17
  5. package/src/commands/migrate.js +46 -31
  6. package/src/commands/serve.js +266 -37
  7. package/src/container/Application.js +88 -8
  8. package/src/container/MillasApp.js +6 -14
  9. package/src/controller/Controller.js +79 -300
  10. package/src/errors/ErrorRenderer.js +640 -0
  11. package/src/facades/Admin.js +49 -0
  12. package/src/facades/Auth.js +46 -0
  13. package/src/facades/Cache.js +17 -0
  14. package/src/facades/Database.js +43 -0
  15. package/src/facades/Events.js +24 -0
  16. package/src/facades/Http.js +54 -0
  17. package/src/facades/Log.js +56 -0
  18. package/src/facades/Mail.js +40 -0
  19. package/src/facades/Queue.js +23 -0
  20. package/src/facades/Storage.js +17 -0
  21. package/src/facades/Validation.js +69 -0
  22. package/src/http/MillasRequest.js +253 -0
  23. package/src/http/MillasResponse.js +196 -0
  24. package/src/http/RequestContext.js +176 -0
  25. package/src/http/ResponseDispatcher.js +144 -0
  26. package/src/http/helpers.js +164 -0
  27. package/src/http/index.js +13 -0
  28. package/src/index.js +55 -2
  29. package/src/logger/internal.js +76 -0
  30. package/src/logger/patchConsole.js +135 -0
  31. package/src/middleware/CorsMiddleware.js +22 -30
  32. package/src/middleware/LogMiddleware.js +27 -59
  33. package/src/middleware/Middleware.js +24 -15
  34. package/src/middleware/MiddlewarePipeline.js +30 -67
  35. package/src/middleware/MiddlewareRegistry.js +126 -0
  36. package/src/middleware/ThrottleMiddleware.js +22 -26
  37. package/src/orm/fields/index.js +124 -56
  38. package/src/orm/migration/ModelInspector.js +7 -3
  39. package/src/orm/model/Model.js +96 -6
  40. package/src/orm/query/QueryBuilder.js +141 -3
  41. package/src/providers/LogServiceProvider.js +88 -18
  42. package/src/providers/ProviderRegistry.js +14 -1
  43. package/src/providers/ServiceProvider.js +40 -8
  44. package/src/router/Router.js +155 -223
  45. package/src/scaffold/maker.js +24 -59
  46. package/src/scaffold/templates.js +13 -12
  47. package/src/validation/BaseValidator.js +193 -0
  48. 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.10",
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
- * Register its routes in routes/api.js:
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
- * POST /auth/register
29
- * Body: { name, email, password, password_confirmation? }
30
- */
31
- async register(req, res) {
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(res, {
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
- * POST /auth/login
50
- * Body: { email, password }
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(email, password);
38
+ const { user, token, refreshToken } = await Auth.login(emailVal, password);
59
39
 
60
- return this.ok(res, {
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
- * POST /auth/logout
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
- * GET /auth/me
81
- * Header: Authorization: Bearer <token>
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
- * POST /auth/refresh
95
- * Body: { refresh_token }
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
- // Verify the refresh token
103
- let payload;
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
- * POST /auth/forgot-password
124
- * Body: { email }
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
- // Always return 200 to prevent email enumeration
132
- try {
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
- * POST /auth/reset-password
154
- * Body: { token, password }
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
- if (!user || user.email !== payload.email) {
168
- return this.badRequest(res, 'Invalid reset token');
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 obj = user.toJSON ? user.toJSON() : { ...user };
182
- delete obj.password;
183
- delete obj.remember_token;
184
- return obj;
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 from the database,
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
- * Register in bootstrap/app.js:
22
- * app.middleware('auth', AuthMiddleware)
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(req, res, next) {
29
- const header = req.headers['authorization'];
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 = user;
60
- req.token = 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 route access to users with specific roles.
10
- * Must be used AFTER AuthMiddleware (requires req.user).
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(req, res, next) {
28
- if (!req.user) {
29
- throw new HttpError(401, 'Unauthenticated — run AuthMiddleware before RoleMiddleware');
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 = req.user.role || null;
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
 
@@ -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 = getProjectContext();
17
- // Fixed: was incorrectly destructured as { ModelInspector }
15
+ const ctx = getProjectContext();
18
16
  const ModelInspector = require('../orm/migration/ModelInspector');
19
- const inspector = new ModelInspector(
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
- console.error(chalk.red(`\n ✖ makemigrations failed: ${err.message}\n`));
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
- printResult(result, 'Ran');
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
- printResult(result, 'Ran');
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
- printResult(result, 'Rolled back');
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
- printResult(result, 'Rolled back');
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
- printResult(result, 'Ran');
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
- function printResult(result, verb) {
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
  }