millas 0.2.12-beta → 0.2.12-beta-1

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 (52) hide show
  1. package/package.json +3 -16
  2. package/src/auth/Auth.js +13 -8
  3. package/src/auth/AuthController.js +3 -1
  4. package/src/auth/AuthUser.js +98 -0
  5. package/src/cli.js +1 -1
  6. package/src/commands/serve.js +81 -110
  7. package/src/container/AppInitializer.js +158 -0
  8. package/src/container/Application.js +278 -253
  9. package/src/container/HttpServer.js +156 -0
  10. package/src/container/MillasApp.js +23 -280
  11. package/src/container/MillasConfig.js +163 -0
  12. package/src/core/auth.js +9 -0
  13. package/src/core/db.js +8 -0
  14. package/src/core/foundation.js +67 -0
  15. package/src/core/http.js +11 -0
  16. package/src/core/mail.js +6 -0
  17. package/src/core/queue.js +7 -0
  18. package/src/core/validation.js +29 -0
  19. package/src/facades/Admin.js +1 -1
  20. package/src/facades/Auth.js +22 -39
  21. package/src/facades/Cache.js +21 -10
  22. package/src/facades/Database.js +1 -1
  23. package/src/facades/Events.js +18 -17
  24. package/src/facades/Facade.js +197 -0
  25. package/src/facades/Http.js +42 -45
  26. package/src/facades/Log.js +25 -49
  27. package/src/facades/Mail.js +27 -32
  28. package/src/facades/Queue.js +22 -15
  29. package/src/facades/Storage.js +18 -10
  30. package/src/facades/Url.js +53 -0
  31. package/src/http/HttpClient.js +673 -0
  32. package/src/http/ResponseDispatcher.js +18 -111
  33. package/src/http/UrlGenerator.js +375 -0
  34. package/src/http/WelcomePage.js +273 -0
  35. package/src/http/adapters/ExpressAdapter.js +315 -0
  36. package/src/http/adapters/HttpAdapter.js +168 -0
  37. package/src/http/adapters/index.js +9 -0
  38. package/src/index.js +5 -144
  39. package/src/logger/formatters/PrettyFormatter.js +15 -5
  40. package/src/logger/internal.js +2 -2
  41. package/src/logger/patchConsole.js +91 -81
  42. package/src/middleware/MiddlewareRegistry.js +62 -82
  43. package/src/orm/migration/ModelInspector.js +339 -340
  44. package/src/providers/AuthServiceProvider.js +9 -5
  45. package/src/providers/CacheStorageServiceProvider.js +3 -1
  46. package/src/providers/EventServiceProvider.js +2 -1
  47. package/src/providers/LogServiceProvider.js +3 -2
  48. package/src/providers/MailServiceProvider.js +3 -2
  49. package/src/providers/QueueServiceProvider.js +3 -2
  50. package/src/router/Router.js +119 -152
  51. package/src/scaffold/templates.js +8 -7
  52. package/src/facades/Validation.js +0 -69
@@ -16,7 +16,8 @@ const RoleMiddleware = require('../auth/RoleMiddleware');
16
16
  */
17
17
  class AuthServiceProvider extends ServiceProvider {
18
18
  register(container) {
19
- container.instance('Auth', Auth);
19
+ container.instance('Auth', Auth);
20
+ container.alias('auth', 'Auth');
20
21
  container.instance('AuthMiddleware', AuthMiddleware);
21
22
  }
22
23
 
@@ -32,11 +33,14 @@ class AuthServiceProvider extends ServiceProvider {
32
33
  };
33
34
  }
34
35
 
35
- // Load the User model if available
36
- let UserModel = null;
36
+ // Load the app's User model if it exists.
37
+ // Falls back to the built-in AuthUser so Auth always has a model to work with.
38
+ let UserModel;
37
39
  try {
38
40
  UserModel = require(process.cwd() + '/app/models/User');
39
- } catch { /* User model optional at boot */ }
41
+ } catch {
42
+ UserModel = require('../auth/AuthUser');
43
+ }
40
44
 
41
45
  // Configure the Auth singleton
42
46
  Auth.configure(authConfig, UserModel);
@@ -50,4 +54,4 @@ class AuthServiceProvider extends ServiceProvider {
50
54
  }
51
55
  }
52
56
 
53
- module.exports = AuthServiceProvider;
57
+ module.exports = AuthServiceProvider;
@@ -12,6 +12,7 @@ const Storage = require('../storage/Storage');
12
12
  class CacheServiceProvider extends ServiceProvider {
13
13
  register(container) {
14
14
  container.instance('Cache', Cache);
15
+ container.alias('cache', 'Cache');
15
16
  }
16
17
 
17
18
  async boot() {
@@ -41,6 +42,7 @@ class CacheServiceProvider extends ServiceProvider {
41
42
  class StorageServiceProvider extends ServiceProvider {
42
43
  register(container) {
43
44
  container.instance('Storage', Storage);
45
+ container.alias('storage', 'Storage');
44
46
  }
45
47
 
46
48
  async boot() {
@@ -68,4 +70,4 @@ class StorageServiceProvider extends ServiceProvider {
68
70
  }
69
71
  }
70
72
 
71
- module.exports = { CacheServiceProvider, StorageServiceProvider };
73
+ module.exports = { CacheServiceProvider, StorageServiceProvider };
@@ -19,6 +19,7 @@ const { emit } = require('../events/EventEmitter');
19
19
  class EventServiceProvider extends ServiceProvider {
20
20
  register(container) {
21
21
  container.instance('EventEmitter', EventEmitter);
22
+ container.alias('events', 'EventEmitter');
22
23
  container.instance('emit', emit);
23
24
  }
24
25
 
@@ -31,4 +32,4 @@ class EventServiceProvider extends ServiceProvider {
31
32
  }
32
33
  }
33
34
 
34
- module.exports = EventServiceProvider;
35
+ module.exports = EventServiceProvider;
@@ -39,8 +39,9 @@ const patchConsole = require('../logger/patchConsole');
39
39
  */
40
40
  class LogServiceProvider extends ServiceProvider {
41
41
  register(container) {
42
- container.instance('Log', Log);
43
- container.instance('Logger', Logger);
42
+ container.instance('Log', Log);
43
+ container.instance('Logger', Logger);
44
+ container.alias('log', 'Log');
44
45
  container.instance('MillasLog', MillasLog);
45
46
  }
46
47
 
@@ -19,7 +19,8 @@ const MailMessage = require('../mail/MailMessage');
19
19
  */
20
20
  class MailServiceProvider extends ServiceProvider {
21
21
  register(container) {
22
- container.instance('Mail', Mail);
22
+ container.instance('Mail', Mail);
23
+ container.alias('mail', 'Mail');
23
24
  container.instance('MailMessage', MailMessage);
24
25
  }
25
26
 
@@ -48,4 +49,4 @@ class MailServiceProvider extends ServiceProvider {
48
49
  }
49
50
  }
50
51
 
51
- module.exports = MailServiceProvider;
52
+ module.exports = MailServiceProvider;
@@ -21,7 +21,8 @@ const { dispatch } = require('../queue/Queue');
21
21
  */
22
22
  class QueueServiceProvider extends ServiceProvider {
23
23
  register(container) {
24
- container.instance('Queue', Queue);
24
+ container.instance('Queue', Queue);
25
+ container.alias('queue', 'Queue');
25
26
  container.instance('dispatch', dispatch);
26
27
  }
27
28
 
@@ -49,4 +50,4 @@ class QueueServiceProvider extends ServiceProvider {
49
50
  }
50
51
  }
51
52
 
52
- module.exports = QueueServiceProvider;
53
+ module.exports = QueueServiceProvider;
@@ -1,211 +1,178 @@
1
1
  'use strict';
2
2
 
3
- const MiddlewareRegistry = require('./MiddlewareRegistry');
4
- const ErrorRenderer = require('../errors/ErrorRenderer');
5
- const MillasRequest = require('../http/MillasRequest');
6
- const MillasResponse = require('../http/MillasResponse');
7
- const ResponseDispatcher = require('../http/ResponseDispatcher');
8
- const RequestContext = require('../http/RequestContext');
9
-
3
+ /**
4
+ * Router
5
+ *
6
+ * Bridges the Millas RouteRegistry to any HttpAdapter.
7
+ * Zero knowledge of Express (or any HTTP engine) — it only calls
8
+ * the adapter interface defined in HttpAdapter.js.
9
+ *
10
+ * Responsibilities:
11
+ * - Resolve route handlers from the RouteRegistry
12
+ * - Resolve middleware aliases from the MiddlewareRegistry
13
+ * - Ask the adapter to mount each route
14
+ * - Ask the adapter to mount the welcome page, 404, and error handler
15
+ */
10
16
  class Router {
11
17
  /**
12
- * @param {object} expressApp
13
- * @param {RouteRegistry} registry
14
- * @param {MiddlewareRegistry} middlewareRegistry
15
- * @param {Container|null} container — DI container, injected into RequestContext
18
+ * @param {import('../http/adapters/HttpAdapter')} adapter
19
+ * @param {import('./RouteRegistry')} registry
20
+ * @param {import('./MiddlewareRegistry')} middlewareRegistry
21
+ * @param {import('../container/Container')|null} container
16
22
  */
17
- constructor(expressApp, registry, middlewareRegistry, container = null) {
18
- this._app = expressApp;
23
+ constructor(adapter, registry, middlewareRegistry, container = null) {
24
+ this._adapter = adapter;
19
25
  this._registry = registry;
20
- this._mw = middlewareRegistry || new MiddlewareRegistry();
26
+ this._mw = middlewareRegistry;
21
27
  this._container = container;
22
28
  }
23
29
 
30
+ // ── Public API ─────────────────────────────────────────────────────────────
31
+
24
32
  /**
25
- * Bind all registered routes onto the Express app.
26
- * Does NOT add 404/error handlers — call mountFallbacks() after
27
- * all other middleware (like Admin) has been registered.
33
+ * Mount all registered routes onto the adapter.
34
+ * Does NOT add fallbacks — call mountFallbacks() after all routes
35
+ * and any extra middleware (e.g. Admin panel) have been added.
28
36
  */
29
37
  mountRoutes() {
30
- const routes = this._registry.all();
31
- for (const route of routes) {
38
+ for (const route of this._registry.all()) {
32
39
  this._bindRoute(route);
33
40
  }
34
41
  return this;
35
42
  }
36
43
 
37
44
  /**
38
- * Add the 404 + global error handlers.
39
- * Must be called LAST — after all routes and admin panels.
45
+ * Mount the 404 + error handlers.
46
+ * Must be called LAST — after all routes and the admin panel.
40
47
  */
41
48
  mountFallbacks() {
42
- this._app.use(ErrorRenderer.notFound());
43
- this._app.use(ErrorRenderer.handler());
49
+ this._maybeInjectWelcome();
50
+ this._adapter.mountNotFound();
51
+ this._adapter.mountErrorHandler();
44
52
  return this;
45
53
  }
46
54
 
47
55
  /**
48
- * Bind all registered routes onto the Express app
49
- * AND add 404/error handlers (original behaviour).
56
+ * Mount routes + fallbacks in one call.
50
57
  */
51
58
  mount() {
52
- const routes = this._registry.all();
53
- for (const route of routes) {
54
- this._bindRoute(route);
55
- }
56
- this._app.use(ErrorRenderer.notFound());
57
- this._app.use(ErrorRenderer.handler());
59
+ this.mountRoutes();
60
+ this._maybeInjectWelcome();
61
+ this._adapter.mountNotFound();
62
+ this._adapter.mountErrorHandler();
63
+ return this;
58
64
  }
59
65
 
60
- // ─── Private ──────────────────────────────────────────────────────────────
66
+ // ── Private ────────────────────────────────────────────────────────────────
61
67
 
62
68
  _bindRoute(route) {
63
- const verb = route.verb.toLowerCase();
64
- const path = route.path;
65
-
66
- const mwHandlers = this._resolveMiddleware(route.middleware || []);
67
- const terminal = this._resolveHandler(route.handler, route.method, route.verb, path, route.name);
68
-
69
- this._app[verb](path, ...mwHandlers, terminal);
69
+ const middlewareHandlers = this._resolveMiddleware(route.middleware || []);
70
+ const terminalHandler = this._resolveTerminalHandler(
71
+ route.handler,
72
+ route.method,
73
+ route.verb,
74
+ route.path,
75
+ route.name
76
+ );
77
+
78
+ this._adapter.mountRoute(route.verb, route.path, [
79
+ ...middlewareHandlers,
80
+ terminalHandler,
81
+ ]);
70
82
  }
71
83
 
72
84
  _resolveMiddleware(list) {
73
85
  return list.map(alias => {
74
86
  try {
75
- return this._mw.resolve(alias);
87
+ return this._mw.resolve(alias, this._adapter, this._container);
76
88
  } catch (err) {
77
- console.warn(`[Millas] Warning: ${err.message} — skipping.`);
78
- return (_req, _res, next) => next();
89
+ console.warn(`[Millas] Middleware warning: ${err.message} — skipping.`);
90
+ return this._mw.resolvePassthrough(this._adapter);
79
91
  }
80
92
  });
81
93
  }
82
94
 
83
- _resolveHandler(handler, method, verb, path, routeName) {
84
- let fn;
85
- let inferredName;
95
+ _resolveTerminalHandler(handler, method, verb, path, routeName) {
96
+ const kernelFn = this._extractKernelFn(handler, method);
97
+ const displayName = this._buildDisplayName(handler, method, verb, path, routeName);
98
+ return this._adapter.wrapKernelHandler(kernelFn, displayName, this._container);
99
+ }
86
100
 
101
+ /**
102
+ * Pull the actual function out of the handler definition.
103
+ * Three forms:
104
+ * 1. Bare function/arrow: Route.get('/', () => jsonify({}))
105
+ * 2. Controller class + method: Route.get('/', UserController, 'index')
106
+ * 3. Controller instance + method: Route.get('/', controllerInstance, 'index')
107
+ */
108
+ _extractKernelFn(handler, method) {
87
109
  if (typeof handler === 'function' && !method) {
88
- fn = handler;
89
- // Use function name if it has one, otherwise build from route
90
- inferredName = handler.name && handler.name !== 'anonymous' && handler.name !== ''
91
- ? handler.name
92
- : null;
93
- } else if (typeof handler === 'function' && typeof method === 'string') {
110
+ return handler;
111
+ }
112
+
113
+ if (typeof handler === 'function' && typeof method === 'string') {
94
114
  const instance = new handler();
95
115
  if (typeof instance[method] !== 'function') {
96
- throw new Error(`Method "${method}" not found on controller "${handler.name}".`);
116
+ throw new Error(
117
+ `Method "${method}" not found on controller "${handler.name}".`
118
+ );
97
119
  }
98
- fn = instance[method].bind(instance);
99
- inferredName = `${handler.name}.${method}`;
100
- } else if (typeof handler === 'object' && handler !== null && typeof method === 'string') {
120
+ return instance[method].bind(instance);
121
+ }
122
+
123
+ if (typeof handler === 'object' && handler !== null && typeof method === 'string') {
101
124
  if (typeof handler[method] !== 'function') {
102
125
  throw new Error(`Method "${method}" not found on handler object.`);
103
126
  }
104
- fn = handler[method].bind(handler);
105
- inferredName = `${handler.constructor?.name || 'Controller'}.${method}`;
106
- } else if (typeof handler === 'function') {
107
- fn = handler;
108
- inferredName = handler.name && handler.name !== 'anonymous' ? handler.name : null;
109
- } else {
110
- throw new Error(`Invalid route handler: ${JSON.stringify(handler)}`);
127
+ return handler[method].bind(handler);
111
128
  }
112
129
 
113
- // Build the display name shown in error messages, priority:
114
- // 1. Explicit route name (Route.get(...).name('users.index'))
115
- // 2. Controller.method (UserController.index)
116
- // 3. Named function (async function getUsers)
117
- // 4. Route signature (GET /users/:id)
118
- const displayName = routeName
119
- || inferredName
120
- || (verb && path ? `${verb.toUpperCase()} ${path}` : 'anonymous');
130
+ if (typeof handler === 'function') {
131
+ return handler;
132
+ }
121
133
 
122
- return this._buildKernelHandler(fn, displayName);
134
+ throw new Error(`Invalid route handler: ${JSON.stringify(handler)}`);
123
135
  }
124
136
 
125
- /**
126
- * The kernel handler — the Promise wrapper that:
127
- * 1. Wraps the raw Express req into a MillasRequest
128
- * 2. Calls the route handler with only the MillasRequest
129
- * 3. Receives a MillasResponse (or plain value, auto-wrapped)
130
- * 4. Dispatches through ResponseDispatcher to Express res
131
- * 5. Raises a clear error if the handler returned nothing
132
- */
133
- _buildKernelHandler(fn, displayName) {
134
- const fnName = displayName || fn.name || 'anonymous';
135
- const container = this._container;
136
-
137
- return (expressReq, expressRes, expressNext) => {
138
- const millaReq = new MillasRequest(expressReq);
139
- const ctx = new RequestContext(millaReq, container);
140
-
141
- let nextCalled = false;
142
- const trackedNext = (...args) => {
143
- nextCalled = true;
144
- expressNext(...args);
145
- };
146
-
147
- new Promise((resolve, reject) => {
148
- try {
149
- resolve(fn(ctx, trackedNext));
150
- } catch (err) {
151
- reject(err);
152
- }
153
- })
154
- .then(value => {
155
- if (nextCalled) return;
156
- if (expressRes.headersSent) return;
157
-
158
- if (value === undefined || value === null) {
159
- const err = Object.assign(
160
- new Error(
161
- `The route handler "${fnName}" did not return a response.\n` +
162
- `Return a MillasResponse or a plain value:\n\n` +
163
- ` return jsonify({ ok: true })\n` +
164
- ` return { ok: true } // auto-wrapped\n` +
165
- ` return 'Hello world' // auto-wrapped\n` +
166
- ` return redirect('/login')\n` +
167
- ` return view('home', { data })`
168
- ),
169
- { status: 500, statusCode: 500 }
170
- );
171
- return expressNext(err);
172
- }
173
-
174
- if (value instanceof Error) return expressNext(value);
175
-
176
- try {
177
- const response = MillasResponse.isResponse(value)
178
- ? value
179
- : ResponseDispatcher.autoWrap(value);
180
- ResponseDispatcher.dispatch(response, expressRes);
181
- } catch (dispatchErr) {
182
- expressNext(dispatchErr);
183
- }
184
- })
185
- .catch(expressNext);
186
- };
137
+ _buildDisplayName(handler, method, verb, path, routeName) {
138
+ if (routeName) return routeName;
139
+
140
+ if (typeof handler === 'function' && method) {
141
+ return `${handler.name}.${method}`;
142
+ }
143
+
144
+ if (
145
+ typeof handler === 'function' &&
146
+ handler.name &&
147
+ handler.name !== 'anonymous' &&
148
+ handler.name !== ''
149
+ ) {
150
+ return handler.name;
151
+ }
152
+
153
+ return verb && path ? `${verb.toUpperCase()} ${path}` : 'anonymous';
187
154
  }
188
155
 
189
156
  /**
190
- * Wrap a route handler so that:
191
- *
192
- * 1. Async functions have their rejections forwarded to next(err)
193
- * 2. Return values are automatically sent as a response — so developers
194
- * can write handlers without touching req/res at all:
195
- *
196
- * Route.get('/', () => ({ status: 'ok' }))
197
- * Route.get('/hello', () => 'Hello world')
198
- * Route.get('/user', async () => await User.find(1))
199
- * Route.get('/ping', (req, res) => res.json({ pong: true })) // still works
200
- *
201
- * Return value rules:
202
- * string → res.send(value)
203
- * number → res.json(value) (rarely useful but safe)
204
- * object/array → res.json(value)
205
- * null/undefined → do nothing (handler called res itself, or next())
206
- * Error → next(value)
157
+ * If no user-defined GET / route exists, ask the adapter to serve
158
+ * a developer-friendly welcome page.
159
+ * Only active outside production silently skipped in prod.
207
160
  */
208
-
161
+ _maybeInjectWelcome() {
162
+ if (process.env.NODE_ENV === 'production') return;
163
+
164
+ const hasRoot = this._registry.all().some(
165
+ r => r.verb === 'GET' && (r.path === '/' || r.path === '')
166
+ );
167
+
168
+ if (!hasRoot) {
169
+ let version = '';
170
+ try { version = require('../../package.json').version; } catch {}
171
+ this._adapter.mountWelcome(
172
+ this._adapter.makeWelcomeHandler(version)
173
+ );
174
+ }
175
+ }
209
176
  }
210
177
 
211
- module.exports = Router;
178
+ module.exports = Router;
@@ -12,6 +12,8 @@ function getProjectFiles(projectName) {
12
12
  scripts: {
13
13
  start: 'node bootstrap/app.js',
14
14
  dev: 'millas serve',
15
+ makemigrations: 'millas makemigration',
16
+ migrate: 'millas migrate',
15
17
  serve: 'millas serve',
16
18
  },
17
19
  dependencies: {
@@ -88,18 +90,17 @@ module.exports = {
88
90
 
89
91
  require('dotenv').config();
90
92
 
91
- const { MillasApp } = require('millas');
93
+ const { Millas } = require('millas');
92
94
  const AppServiceProvider = require('../providers/AppServiceProvider');
93
95
 
94
- const app = MillasApp.create()
96
+ module.exports = Millas.config()
95
97
  .providers([AppServiceProvider])
96
98
  .routes(Route => {
97
99
  require('../routes/web')(Route);
98
100
  require('../routes/api')(Route);
99
101
  })
100
- .withAdmin();
101
-
102
- module.exports = app.start();
102
+ .withAdmin()
103
+ .create();
103
104
  `,
104
105
 
105
106
  // ─── routes/web.js ────────────────────────────────────────────
@@ -242,7 +243,7 @@ module.exports = {
242
243
  // ─── providers/AppServiceProvider.js ──────────────────────────
243
244
  'providers/AppServiceProvider.js': `'use strict';
244
245
 
245
- const { ServiceProvider } = require('millas');
246
+ const { ServiceProvider } = require('millas/core/foundation');
246
247
 
247
248
  /**
248
249
  * AppServiceProvider
@@ -326,4 +327,4 @@ providers/ # Service providers
326
327
  };
327
328
  }
328
329
 
329
- module.exports = { getProjectFiles };
330
+ module.exports = { getProjectFiles };
@@ -1,69 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * millas/facades/Validation
5
- *
6
- * Field validators and schema runner.
7
- *
8
- * const { string, email, number, boolean, date, array, object, file, Validator }
9
- * = require('millas/facades/Validation');
10
- *
11
- * Route.post('/register', async ({ body }) => {
12
- * const data = await body.validate({
13
- * name: string('Must be text').required('Name is required').min(2).max(100),
14
- * email: email().required(),
15
- * age: number().optional().min(0).max(120),
16
- * password: string().required().min(8).confirmed(),
17
- * role: string().oneOf(['admin','user']).default('user'),
18
- * tags: array().of(string().required()).optional(),
19
- * address: object({
20
- * city: string().required(),
21
- * zip: string().matches(/^\d{5}$/, 'Invalid ZIP'),
22
- * }).optional(),
23
- * avatar: file().optional().image().maxSize('2mb'),
24
- * });
25
- * return jsonify(await User.create(data), { status: 201 });
26
- * });
27
- */
28
-
29
- const {
30
- Validator,
31
- BaseValidator,
32
- StringValidator,
33
- EmailValidator,
34
- NumberValidator,
35
- BooleanValidator,
36
- DateValidator,
37
- ArrayValidator,
38
- ObjectValidator,
39
- FileValidator,
40
- string,
41
- email,
42
- number,
43
- boolean,
44
- date,
45
- array,
46
- } = require('../index');
47
-
48
- const { object, file } = require('../validation/Validator');
49
-
50
- module.exports = {
51
- Validator,
52
- BaseValidator,
53
- StringValidator,
54
- EmailValidator,
55
- NumberValidator,
56
- BooleanValidator,
57
- DateValidator,
58
- ArrayValidator,
59
- ObjectValidator,
60
- FileValidator,
61
- string,
62
- email,
63
- number,
64
- boolean,
65
- date,
66
- array,
67
- object,
68
- file,
69
- };