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.
Files changed (47) 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/controller/Controller.js +79 -300
  9. package/src/errors/ErrorRenderer.js +640 -0
  10. package/src/facades/Admin.js +49 -0
  11. package/src/facades/Auth.js +46 -0
  12. package/src/facades/Cache.js +17 -0
  13. package/src/facades/Database.js +43 -0
  14. package/src/facades/Events.js +24 -0
  15. package/src/facades/Http.js +54 -0
  16. package/src/facades/Log.js +56 -0
  17. package/src/facades/Mail.js +40 -0
  18. package/src/facades/Queue.js +23 -0
  19. package/src/facades/Storage.js +17 -0
  20. package/src/facades/Validation.js +69 -0
  21. package/src/http/MillasRequest.js +253 -0
  22. package/src/http/MillasResponse.js +196 -0
  23. package/src/http/RequestContext.js +176 -0
  24. package/src/http/ResponseDispatcher.js +144 -0
  25. package/src/http/helpers.js +164 -0
  26. package/src/http/index.js +13 -0
  27. package/src/index.js +55 -2
  28. package/src/logger/internal.js +76 -0
  29. package/src/logger/patchConsole.js +135 -0
  30. package/src/middleware/CorsMiddleware.js +22 -30
  31. package/src/middleware/LogMiddleware.js +27 -59
  32. package/src/middleware/Middleware.js +24 -15
  33. package/src/middleware/MiddlewarePipeline.js +30 -67
  34. package/src/middleware/MiddlewareRegistry.js +126 -0
  35. package/src/middleware/ThrottleMiddleware.js +22 -26
  36. package/src/orm/fields/index.js +124 -56
  37. package/src/orm/migration/ModelInspector.js +7 -3
  38. package/src/orm/model/Model.js +96 -6
  39. package/src/orm/query/QueryBuilder.js +141 -3
  40. package/src/providers/LogServiceProvider.js +88 -18
  41. package/src/providers/ProviderRegistry.js +14 -1
  42. package/src/providers/ServiceProvider.js +40 -8
  43. package/src/router/Router.js +155 -223
  44. package/src/scaffold/maker.js +24 -59
  45. package/src/scaffold/templates.js +13 -12
  46. package/src/validation/BaseValidator.js +193 -0
  47. package/src/validation/Validator.js +680 -0
@@ -3,48 +3,277 @@
3
3
  const chalk = require('chalk');
4
4
  const path = require('path');
5
5
  const fs = require('fs-extra');
6
+ const fsnative = require('fs');
7
+ const {fork} = require('child_process');
8
+ const chokidar = require('chokidar');
9
+
10
+ // ── ASCII banner ──────────────────────────────────────────────────────────────
11
+
12
+ const BANNER_LINES = [
13
+ ' ███╗ ███╗██╗██╗ ██╗ █████╗ ███████╗',
14
+ ' ████╗ ████║██║██║ ██║ ██╔══██╗██╔════╝',
15
+ ' ██╔████╔██║██║██║ ██║ ███████║███████╗',
16
+ ' ██║╚██╔╝██║██║██║ ██║ ██╔══██║╚════██║',
17
+ ' ██║ ╚═╝ ██║██║███████╗███████╗██║ ██║███████║',
18
+ ' ╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝',
19
+ ];
20
+
21
+ function printBanner(host, port) {
22
+ const env = process.env.NODE_ENV || 'development';
23
+ const ver = 'v' + (require('../../package.json').version || '0.1.2');
24
+ const url = `http://${host}:${port}`;
25
+ const hr = chalk.dim(' ' + '─'.repeat(54));
26
+
27
+ const envColour = env === 'production' ? chalk.red
28
+ : env === 'staging' ? chalk.yellow
29
+ : chalk.green;
30
+
31
+ process.stdout.write('\n');
32
+ for (const line of BANNER_LINES) {
33
+ process.stdout.write(chalk.bold.cyan(line) + '\n');
34
+ }
35
+ process.stdout.write('\n');
36
+ process.stdout.write(hr + '\n');
37
+ process.stdout.write(
38
+ ' ' +
39
+ chalk.dim(ver.padEnd(8)) +
40
+ chalk.dim('│') + ' ' +
41
+ envColour('⬤ ' + env) + ' ' +
42
+ chalk.dim('│') + ' ' +
43
+ chalk.bold.white(url) +
44
+ '\n'
45
+ );
46
+ process.stdout.write(hr + '\n\n');
47
+ }
48
+
49
+ // ── Hot reload watcher ────────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Directories watched for changes (relative to project root).
53
+ * Watching parent dirs is enough — fs.watch recursive covers all descendants.
54
+ */
55
+ const WATCH_DIRS = [
56
+ 'app',
57
+ 'routes',
58
+ 'config',
59
+ 'bootstrap',
60
+ 'providers',
61
+ 'middleware',
62
+ ];
63
+
64
+ /** File extensions that trigger a reload when changed. */
65
+ const WATCH_EXTS = new Set(['.js', '.mjs', '.cjs', '.json', '.njk', '.env']);
66
+
67
+ /** How long to wait after the last change before restarting (ms). */
68
+ const DEBOUNCE_MS = 300;
69
+
70
+ class HotReloader {
71
+ constructor(bootstrapPath, env) {
72
+ this._bootstrap = bootstrapPath;
73
+ this._cwd = path.dirname(bootstrapPath.replace(/bootstrap.+$/, '')) ||
74
+ process.cwd();
75
+ this._env = env;
76
+ this._child = null;
77
+ this._watchers = [];
78
+ this._timer = null;
79
+ this._starting = false;
80
+ this._restarts = 0;
81
+ }
82
+
83
+ start() {
84
+ this._spawnChild();
85
+ this._watch();
86
+ this._handleSignals();
87
+ }
88
+
89
+ // ── Child process management ──────────────────────────────────────────────
90
+
91
+ _spawnChild() {
92
+ if (this._starting) return;
93
+ this._starting = true;
94
+
95
+ this._child = fork(this._bootstrap, [], {
96
+ env: {...process.env, ...this._env},
97
+ stdio: 'inherit', // child shares parent's stdout/stderr — output appears inline
98
+ // detached: false — child dies with parent
99
+ });
100
+
101
+ this._child.on('exit', (code, signal) => {
102
+ this._starting = false;
103
+ // Abnormal exit (crash) — don't auto-restart, let the developer fix it
104
+ if (code !== 0 && signal !== 'SIGTERM' && signal !== 'SIGKILL') {
105
+ process.stdout.write(
106
+ '\n' + chalk.red(' ✖ App crashed') +
107
+ chalk.dim(` (exit ${code ?? signal})`) +
108
+ chalk.dim(' — waiting for changes…\n\n')
109
+ );
110
+ }
111
+ });
112
+
113
+ this._child.on('error', (err) => {
114
+ this._starting = false;
115
+ process.stderr.write(chalk.red(`\n ✖ Child error: ${err.message}\n\n`));
116
+ });
117
+ }
118
+
119
+ _killChild(cb) {
120
+ if (!this._child || this._child.exitCode !== null) {
121
+ cb();
122
+ return;
123
+ }
124
+ this._child.once('exit', cb);
125
+ this._child.kill('SIGTERM');
126
+
127
+ // Force kill if graceful shutdown takes too long
128
+ setTimeout(() => {
129
+ if (this._child && this._child.exitCode === null) {
130
+ this._child.kill('SIGKILL');
131
+ }
132
+ }, 3000).unref();
133
+ }
134
+
135
+ _restart(changedFile) {
136
+ const rel = changedFile ? path.relative(process.cwd(), changedFile) : '';
137
+ const link = changedFile
138
+ ? `\x1b]8;;file://${changedFile}\x07${changedFile}\x1b]8;;\x07`
139
+ : '';
140
+
141
+ process.stdout.write(
142
+ '\n ' + chalk.yellow('↺') + ' ' +
143
+ chalk.white('Reloading') +
144
+ (link ? ' ' + chalk.cyan(link) : '') +
145
+ '\n'
146
+ );
147
+
148
+ this._restarts++;
149
+
150
+ this._killChild(() => {
151
+ this._spawnChild();
152
+ });
153
+ }
154
+
155
+ // ── File watching ─────────────────────────────────────────────────────────
156
+
157
+ _watch() {
158
+ const cwd = process.cwd();
159
+ const watchPaths = [
160
+ ...WATCH_DIRS.map(d => path.join(cwd, d)),
161
+ path.join(cwd, '.env'),
162
+ path.join(cwd, '.env.local'),
163
+ ];
164
+
165
+ const watcher = chokidar.watch(watchPaths, {
166
+ persistent: true,
167
+ ignoreInitial: true,
168
+ ignored: /(^|[\/\\])\..(?!env)/,
169
+ });
170
+
171
+ watcher.on('all', (event, filePath) => {
172
+ if (WATCH_EXTS.has(path.extname(filePath))) {
173
+ this._scheduleRestart(filePath);
174
+ }
175
+ });
176
+
177
+ this._watchers.push(watcher);
178
+ }
179
+
180
+ _scheduleRestart(changedFile) {
181
+ clearTimeout(this._timer);
182
+ this._timer = setTimeout(() => {
183
+ this._restart(changedFile);
184
+ }, DEBOUNCE_MS);
185
+ }
186
+
187
+ _stopWatching() {
188
+ for (const w of this._watchers) {
189
+ try {
190
+ w.close();
191
+ } catch {
192
+ }
193
+ }
194
+ this._watchers = [];
195
+ }
196
+
197
+
198
+ // ── Signal handling ───────────────────────────────────────────────────────
199
+
200
+ _handleSignals() {
201
+ const cleanup = () => {
202
+ clearTimeout(this._timer);
203
+ this._stopWatching();
204
+ if (this._child) this._child.kill('SIGTERM');
205
+ process.exit(0);
206
+ };
207
+
208
+ process.on('SIGINT', cleanup);
209
+ process.on('SIGTERM', cleanup);
210
+ }
211
+ }
212
+
213
+ // ── Command definition ────────────────────────────────────────────────────────
6
214
 
7
215
  module.exports = function (program) {
8
- program
9
- .command('serve')
10
- .description('Start the development server')
11
- .option('-p, --port <port>', 'Port to listen on', '3000')
12
- .option('-h, --host <host>', 'Host to bind to', 'localhost')
13
- .action(async (options) => {
14
- const appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
15
-
16
- if (!fs.existsSync(appBootstrap)) {
17
- console.error(chalk.red('\n ✖ No Millas project found in current directory.\n'));
18
- console.log(` Make sure you are inside a Millas project and bootstrap/app.js exists.\n`);
19
- process.exit(1);
20
- }
21
216
 
22
- console.log();
23
- console.log(chalk.cyan(' ⚡ Millas Dev Server'));
24
- console.log(chalk.gray(` Starting on http://${options.host}:${options.port}\n`));
217
+ program
218
+ .command('serve')
219
+ .description('Start the development server with hot reload')
220
+ .option('-p, --port <port>', 'Port to listen on', '3000')
221
+ .option('-h, --host <host>', 'Host to bind to', 'localhost')
222
+ .option('--no-reload', 'Disable hot reload (run once, like production)')
223
+ .action((options) => {
224
+ const appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
25
225
 
26
- process.env.MILLAS_PORT = options.port;
27
- process.env.MILLAS_HOST = options.host;
28
- process.env.NODE_ENV = process.env.NODE_ENV || 'development';
226
+ if (!fs.existsSync(appBootstrap)) {
227
+ process.stderr.write(chalk.red('\n ✖ No Millas project found here.\n'));
228
+ process.stderr.write(chalk.dim(' Make sure bootstrap/app.js exists.\n\n'));
229
+ process.exit(1);
230
+ }
231
+
232
+ const env = {
233
+ MILLAS_PORT: options.port,
234
+ MILLAS_HOST: options.host,
235
+ NODE_ENV: process.env.NODE_ENV || 'development',
236
+ MILLAS_RELOAD: options.reload ? '1' : '0',
237
+ };
238
+
239
+ Object.assign(process.env, env);
240
+
241
+ printBanner(options.host, options.port);
242
+
243
+ const hotReload = options.reload !== false;
244
+
245
+ if (hotReload) {
246
+ // ── Hot reload mode ──────────────────────────────────────────────────
247
+ // Parent process watches files. App runs in a child process.
248
+ // On change: kill child, clear require cache, refork.
249
+ process.stdout.write(
250
+ ' ' + chalk.green('✔') + ' ' +
251
+ chalk.dim('Watching for file changes…') +
252
+ '\n'
253
+ );
254
+
255
+ const reloader = new HotReloader(appBootstrap, env);
256
+ reloader.start();
257
+
258
+ } else {
259
+ // ── Single-run mode (--no-reload) ────────────────────────────────────
260
+ try {
261
+ require(appBootstrap);
262
+ } catch (err) {
263
+ process.stderr.write(chalk.red(`\n ✖ Failed to start: ${err.message}\n\n`));
264
+ if (process.env.DEBUG) process.stderr.write(chalk.dim(err.stack) + '\n');
265
+ process.exit(1);
266
+ }
267
+ }
268
+ });
29
269
 
30
- try {
31
- require(appBootstrap);
32
- } catch (err) {
33
- console.error(chalk.red(`\n ✖ Failed to start server: ${err.message}\n`));
34
- console.error(err.stack);
35
- process.exit(1);
36
- }
37
- });
38
270
  };
39
271
 
40
- // Exported so queue:work can import the pattern
41
- module.exports.requireProject = function(command) {
42
- const path = require('path');
43
- const fs = require('fs-extra');
44
- const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
45
- if (!fs.existsSync(bootstrapPath)) {
46
- const chalk = require('chalk');
47
- console.error(chalk.red(`\n ✖ Not inside a Millas project (${command}).\n`));
48
- process.exit(1);
49
- }
272
+ // Exported so other commands (queue:work) can validate the project exists
273
+ module.exports.requireProject = function (command) {
274
+ const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
275
+ if (!fsnative.existsSync(bootstrapPath)) {
276
+ process.stderr.write(chalk.red(`\n ✖ Not inside a Millas project (${command}).\n\n`));
277
+ process.exit(1);
278
+ }
50
279
  };
@@ -82,12 +82,23 @@ class Application {
82
82
  // ─── Lifecycle ───────────────────────────────────────────────────────────────
83
83
 
84
84
  /**
85
- * Run the full provider register → boot lifecycle.
85
+ * Run the full provider lifecycle:
86
+ * Phase 0 — beforeBoot (synchronous, global setup)
87
+ * Phase 1 — register (synchronous, container bindings)
88
+ * Phase 2 — boot (async, all bindings available)
89
+ *
90
+ * Emits: platform.booting → platform.booted
86
91
  */
87
92
  async boot() {
88
93
  if (this._booted) return this;
94
+
95
+ this._emitSync('platform.booting', { providers: this._providers.list() });
96
+
89
97
  await this._providers.boot();
90
98
  this._booted = true;
99
+
100
+ this._emitSync('platform.booted', { providers: this._providers.list() });
101
+
91
102
  return this;
92
103
  }
93
104
 
@@ -104,7 +115,7 @@ class Application {
104
115
  * Or use app.mount() which does all three in one call.
105
116
  */
106
117
  mountRoutes() {
107
- this._router = new Router(this._express, this._route.getRegistry(), this._mwRegistry);
118
+ this._router = new Router(this._express, this._route.getRegistry(), this._mwRegistry, this._container);
108
119
  this._router.mountRoutes();
109
120
  return this;
110
121
  }
@@ -114,7 +125,7 @@ class Application {
114
125
  */
115
126
  mountFallbacks() {
116
127
  if (!this._router) {
117
- this._router = new Router(this._express, this._route.getRegistry(), this._mwRegistry);
128
+ this._router = new Router(this._express, this._route.getRegistry(), this._mwRegistry, this._container);
118
129
  }
119
130
  this._router.mountFallbacks();
120
131
  return this;
@@ -126,30 +137,86 @@ class Application {
126
137
  * is mounted before this call.
127
138
  */
128
139
  mount() {
129
- const router = new Router(this._express, this._route.getRegistry(), this._mwRegistry);
140
+ const router = new Router(this._express, this._route.getRegistry(), this._mwRegistry, this._container);
130
141
  router.mount();
131
142
  return this;
132
143
  }
133
144
 
134
145
  /**
135
146
  * Start the HTTP server.
147
+ * Emits: platform.listening
136
148
  */
137
149
  listen(port, host, callback) {
138
- const _port = port || parseInt(process.env.APP_PORT, 10) || 3000;
150
+ const _port = port || parseInt(process.env.APP_PORT, 10) || 3000;
139
151
  const _host = host || process.env.MILLAS_HOST || 'localhost';
140
152
 
141
- this._express.listen(_port, _host, () => {
153
+ const server = this._express.listen(_port, _host, () => {
142
154
  if (!process.env.MILLAS_ROUTE_LIST) {
155
+ const chalk = _tryChalk();
143
156
  const routeCount = this._route.list().length;
144
- console.log(`\n ⚡ Millas running at http://${_host}:${_port}`);
145
- console.log(` ${routeCount} route(s) registered\n`);
157
+
158
+ // Use process.stdout.write directly so these lines always appear
159
+ // regardless of log level — even if console is patched and debug is off.
160
+ process.stdout.write(
161
+ ' ' + chalk.dim('›') + ' ' +
162
+ chalk.white(routeCount + ' route' + (routeCount !== 1 ? 's' : '') + ' registered') +
163
+ '\n'
164
+ );
165
+ process.stdout.write(
166
+ ' ' + chalk.dim('›') + ' ' +
167
+ chalk.dim('Press ') + chalk.bold('Ctrl+C') + chalk.dim(' to stop') +
168
+ '\n\n'
169
+ );
146
170
  }
171
+
172
+ this._emitSync('platform.listening', { port: _port, host: _host });
173
+
147
174
  if (typeof callback === 'function') callback(_port, _host);
148
175
  });
149
176
 
177
+ this._server = server;
178
+ return this;
179
+ }
180
+
181
+ /**
182
+ * Graceful shutdown — emits platform.shutting_down, then closes the server.
183
+ * Call this instead of process.exit() for clean teardown.
184
+ *
185
+ * process.on('SIGTERM', () => app.shutdown());
186
+ */
187
+ async shutdown(code = 0) {
188
+ this._emitSync('platform.shutting_down', {});
189
+ if (this._server) {
190
+ await new Promise(resolve => this._server.close(resolve));
191
+ }
192
+ process.exit(code);
193
+ }
194
+
195
+ // ─── Platform event bus ───────────────────────────────────────────────────
196
+
197
+ /**
198
+ * Listen to a platform lifecycle event.
199
+ *
200
+ * app.on('platform.booted', ({ providers }) => { ... });
201
+ * app.on('platform.listening', ({ port, host }) => { ... });
202
+ * app.on('platform.shutting_down', () => process.cleanup());
203
+ */
204
+ on(event, fn) {
205
+ if (!this._platformListeners) this._platformListeners = new Map();
206
+ if (!this._platformListeners.has(event)) this._platformListeners.set(event, []);
207
+ this._platformListeners.get(event).push(fn);
150
208
  return this;
151
209
  }
152
210
 
211
+ /** Synchronous platform event dispatch (fire-and-forget, no await). */
212
+ _emitSync(event, data) {
213
+ if (!this._platformListeners) return;
214
+ const listeners = this._platformListeners.get(event) || [];
215
+ for (const fn of listeners) {
216
+ try { fn(data); } catch {}
217
+ }
218
+ }
219
+
153
220
  // ─── Container Proxy ─────────────────────────────────────────────────────────
154
221
 
155
222
  /**
@@ -206,3 +273,16 @@ class Application {
206
273
  }
207
274
 
208
275
  module.exports = Application;
276
+
277
+ // ── Helpers ───────────────────────────────────────────────────────────────────
278
+
279
+ // Safely require chalk — it ships with Millas but we guard in case of oddities
280
+ function _tryChalk() {
281
+ try { return require('chalk'); } catch {
282
+ // Fallback: no-op chalk that returns strings unchanged
283
+ const id = s => s;
284
+ const p = new Proxy({}, { get: () => p, apply: (_, __, [s]) => String(s || '') });
285
+ p.dim = id; p.bold = p; p.white = id;
286
+ return p;
287
+ }
288
+ }