millas 0.2.12-beta → 0.2.12-beta-2
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 +3 -16
- package/src/admin/ActivityLog.js +153 -52
- package/src/admin/Admin.js +400 -167
- package/src/admin/AdminAuth.js +213 -98
- package/src/admin/FormGenerator.js +372 -0
- package/src/admin/HookRegistry.js +256 -0
- package/src/admin/QueryEngine.js +263 -0
- package/src/admin/ViewContext.js +309 -0
- package/src/admin/WidgetRegistry.js +406 -0
- package/src/admin/index.js +17 -0
- package/src/admin/resources/AdminResource.js +383 -97
- package/src/admin/static/admin.css +1341 -0
- package/src/admin/static/date-picker.css +157 -0
- package/src/admin/static/date-picker.js +316 -0
- package/src/admin/static/json-editor.css +649 -0
- package/src/admin/static/json-editor.js +1429 -0
- package/src/admin/static/ui.js +1044 -0
- package/src/admin/views/layouts/base.njk +65 -1013
- package/src/admin/views/pages/detail.njk +40 -16
- package/src/admin/views/pages/form.njk +47 -599
- package/src/admin/views/pages/list.njk +145 -62
- package/src/admin/views/partials/form-field.njk +53 -0
- package/src/admin/views/partials/form-footer.njk +28 -0
- package/src/admin/views/partials/form-readonly.njk +114 -0
- package/src/admin/views/partials/form-scripts.njk +476 -0
- package/src/admin/views/partials/form-widget.njk +296 -0
- package/src/admin/views/partials/json-dialog.njk +80 -0
- package/src/admin/views/partials/json-editor.njk +37 -0
- package/src/admin.zip +0 -0
- package/src/auth/Auth.js +31 -10
- package/src/auth/AuthController.js +3 -1
- package/src/auth/AuthUser.js +119 -0
- package/src/cli.js +4 -2
- package/src/commands/createsuperuser.js +254 -0
- package/src/commands/lang.js +589 -0
- package/src/commands/migrate.js +154 -81
- package/src/commands/serve.js +82 -110
- package/src/container/AppInitializer.js +215 -0
- package/src/container/Application.js +278 -253
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +29 -279
- package/src/container/MillasConfig.js +192 -0
- package/src/core/admin.js +5 -0
- package/src/core/auth.js +9 -0
- package/src/core/db.js +9 -0
- package/src/core/foundation.js +59 -0
- package/src/core/http.js +11 -0
- package/src/core/lang.js +1 -0
- package/src/core/mail.js +6 -0
- package/src/core/queue.js +7 -0
- package/src/core/validation.js +29 -0
- package/src/facades/Admin.js +1 -1
- package/src/facades/Auth.js +22 -39
- package/src/facades/Cache.js +21 -10
- package/src/facades/Database.js +1 -1
- package/src/facades/Events.js +18 -17
- package/src/facades/Facade.js +197 -0
- package/src/facades/Http.js +42 -45
- package/src/facades/Log.js +25 -49
- package/src/facades/Mail.js +27 -32
- package/src/facades/Queue.js +22 -15
- package/src/facades/Storage.js +18 -10
- package/src/facades/Url.js +53 -0
- package/src/http/HttpClient.js +673 -0
- package/src/http/ResponseDispatcher.js +18 -111
- package/src/http/UrlGenerator.js +375 -0
- package/src/http/WelcomePage.js +273 -0
- package/src/http/adapters/ExpressAdapter.js +315 -0
- package/src/http/adapters/HttpAdapter.js +168 -0
- package/src/http/adapters/index.js +9 -0
- package/src/i18n/I18nServiceProvider.js +91 -0
- package/src/i18n/Translator.js +635 -0
- package/src/i18n/defaults.js +122 -0
- package/src/i18n/index.js +164 -0
- package/src/i18n/locales/en.js +55 -0
- package/src/i18n/locales/sw.js +48 -0
- package/src/index.js +5 -144
- package/src/logger/formatters/PrettyFormatter.js +103 -57
- package/src/logger/internal.js +2 -2
- package/src/logger/patchConsole.js +91 -81
- package/src/middleware/MiddlewareRegistry.js +62 -82
- package/src/migrations/system/0001_users.js +21 -0
- package/src/migrations/system/0002_admin_log.js +25 -0
- package/src/migrations/system/0003_sessions.js +23 -0
- package/src/orm/fields/index.js +210 -188
- package/src/orm/migration/DefaultValueParser.js +325 -0
- package/src/orm/migration/InteractiveResolver.js +191 -0
- package/src/orm/migration/Makemigrations.js +312 -0
- package/src/orm/migration/MigrationGraph.js +227 -0
- package/src/orm/migration/MigrationRunner.js +202 -108
- package/src/orm/migration/MigrationWriter.js +463 -0
- package/src/orm/migration/ModelInspector.js +412 -344
- package/src/orm/migration/ModelScanner.js +225 -0
- package/src/orm/migration/ProjectState.js +213 -0
- package/src/orm/migration/RenameDetector.js +175 -0
- package/src/orm/migration/SchemaBuilder.js +8 -81
- package/src/orm/migration/operations/base.js +57 -0
- package/src/orm/migration/operations/column.js +191 -0
- package/src/orm/migration/operations/fields.js +252 -0
- package/src/orm/migration/operations/index.js +55 -0
- package/src/orm/migration/operations/models.js +152 -0
- package/src/orm/migration/operations/registry.js +131 -0
- package/src/orm/migration/operations/special.js +51 -0
- package/src/orm/migration/utils.js +208 -0
- package/src/orm/model/Model.js +81 -13
- package/src/providers/AdminServiceProvider.js +66 -9
- package/src/providers/AuthServiceProvider.js +46 -7
- package/src/providers/CacheStorageServiceProvider.js +5 -3
- package/src/providers/DatabaseServiceProvider.js +3 -2
- package/src/providers/EventServiceProvider.js +2 -1
- package/src/providers/LogServiceProvider.js +7 -3
- package/src/providers/MailServiceProvider.js +4 -3
- package/src/providers/QueueServiceProvider.js +4 -3
- package/src/router/Router.js +119 -152
- package/src/scaffold/templates.js +83 -26
- package/src/facades/Validation.js +0 -69
|
@@ -2,39 +2,96 @@
|
|
|
2
2
|
|
|
3
3
|
const ServiceProvider = require('./ServiceProvider');
|
|
4
4
|
const Admin = require('../admin/Admin');
|
|
5
|
+
const AdminAuth = require('../admin/AdminAuth');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* AdminServiceProvider
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* Boots the admin panel and wires it to the app's User model.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
* app.providers([..., AdminServiceProvider])
|
|
12
|
+
* ── Authentication flow ───────────────────────────────────────────────────
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* AdminAuth resolves the User model in this priority order:
|
|
15
|
+
* 1. Explicit model in config/admin.js auth.model
|
|
16
|
+
* 2. The same model AuthServiceProvider resolved (app/models/User)
|
|
17
|
+
* 3. Built-in AuthUser (framework fallback)
|
|
18
|
+
*
|
|
19
|
+
* It then enforces:
|
|
20
|
+
* - user.is_active === true (account not disabled)
|
|
21
|
+
* - user.is_staff === true (has admin panel access)
|
|
22
|
+
*
|
|
23
|
+
* Run `millas createsuperuser` to create your first admin user.
|
|
24
|
+
* Run `millas migrate` first if you haven't — the users table must exist.
|
|
25
|
+
*
|
|
26
|
+
* ── Usage (bootstrap/app.js) ─────────────────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* module.exports = Millas.config()
|
|
29
|
+
* .providers([AppServiceProvider])
|
|
30
|
+
* .withAdmin()
|
|
31
|
+
* .create();
|
|
32
|
+
*
|
|
33
|
+
* ── Optional config/admin.js ─────────────────────────────────────────────
|
|
34
|
+
*
|
|
35
|
+
* module.exports = {
|
|
36
|
+
* prefix: '/admin',
|
|
37
|
+
* title: 'My App Admin',
|
|
38
|
+
* auth: {
|
|
39
|
+
* cookieMaxAge: 60 * 60 * 8,
|
|
40
|
+
* rememberAge: 60 * 60 * 24 * 30,
|
|
41
|
+
* maxAttempts: 5,
|
|
42
|
+
* lockoutMinutes: 15,
|
|
43
|
+
* // model: require('../app/models/AdminUser'), // explicit override
|
|
44
|
+
* },
|
|
45
|
+
* // auth: false — disable auth entirely (not recommended)
|
|
46
|
+
* };
|
|
17
47
|
*/
|
|
18
48
|
class AdminServiceProvider extends ServiceProvider {
|
|
19
49
|
register(container) {
|
|
20
50
|
container.instance('Admin', Admin);
|
|
51
|
+
container.instance('AdminAuth', AdminAuth);
|
|
21
52
|
container.instance('AdminResource', require('../admin/resources/AdminResource').AdminResource);
|
|
22
53
|
container.instance('AdminField', require('../admin/resources/AdminResource').AdminField);
|
|
23
54
|
container.instance('AdminFilter', require('../admin/resources/AdminResource').AdminFilter);
|
|
24
55
|
}
|
|
25
56
|
|
|
26
57
|
async boot(container) {
|
|
58
|
+
const basePath = container.make('basePath') || process.cwd();
|
|
27
59
|
let adminConfig = {};
|
|
28
60
|
try {
|
|
29
|
-
adminConfig = require(
|
|
30
|
-
} catch { /*
|
|
61
|
+
adminConfig = require(basePath + '/config/admin');
|
|
62
|
+
} catch { /* optional */ }
|
|
63
|
+
|
|
64
|
+
// auth: {} means "use the User model with is_staff gate" — the Django default.
|
|
65
|
+
// auth: false disables auth entirely.
|
|
66
|
+
// Anything else is passed through as-is (model override, cookie settings, etc.)
|
|
67
|
+
const authConfig = adminConfig.auth !== undefined ? adminConfig.auth : {};
|
|
31
68
|
|
|
32
69
|
Admin.configure({
|
|
33
70
|
prefix: adminConfig.prefix || '/admin',
|
|
34
71
|
title: adminConfig.title || process.env.APP_NAME || 'Millas Admin',
|
|
35
72
|
...adminConfig,
|
|
73
|
+
auth: authConfig,
|
|
36
74
|
});
|
|
75
|
+
|
|
76
|
+
// ── Wire basePath + User model into AdminAuth ──────────────────────────
|
|
77
|
+
// Pass basePath so AdminAuth._resolveUserModel() can find app/models/User
|
|
78
|
+
// without calling process.cwd() at request time.
|
|
79
|
+
AdminAuth.setBasePath(basePath);
|
|
80
|
+
|
|
81
|
+
// AuthServiceProvider runs before us (Database → Auth → Admin order).
|
|
82
|
+
// It already resolved app/models/User → AuthUser fallback and gave it
|
|
83
|
+
// to the Auth singleton. We grab the same model so AdminAuth and the
|
|
84
|
+
// API auth system always use the same table.
|
|
85
|
+
if (authConfig !== false) {
|
|
86
|
+
try {
|
|
87
|
+
const Auth = require('../auth/Auth');
|
|
88
|
+
// Auth._UserModel is the resolved model — reuse it directly
|
|
89
|
+
if (Auth._UserModel) {
|
|
90
|
+
AdminAuth.setUserModel(Auth._UserModel);
|
|
91
|
+
}
|
|
92
|
+
} catch { /* Auth not booted yet — AdminAuth will lazy-resolve */ }
|
|
93
|
+
}
|
|
37
94
|
}
|
|
38
95
|
}
|
|
39
96
|
|
|
40
|
-
module.exports = AdminServiceProvider;
|
|
97
|
+
module.exports = AdminServiceProvider;
|
|
@@ -16,15 +16,17 @@ const RoleMiddleware = require('../auth/RoleMiddleware');
|
|
|
16
16
|
*/
|
|
17
17
|
class AuthServiceProvider extends ServiceProvider {
|
|
18
18
|
register(container) {
|
|
19
|
-
container.instance('Auth',
|
|
19
|
+
container.instance('Auth', Auth);
|
|
20
|
+
container.alias('auth', 'Auth');
|
|
20
21
|
container.instance('AuthMiddleware', AuthMiddleware);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
async boot(container, app) {
|
|
25
|
+
const basePath = container.make('basePath') || process.cwd();
|
|
24
26
|
// Load auth config
|
|
25
27
|
let authConfig;
|
|
26
28
|
try {
|
|
27
|
-
authConfig = require(
|
|
29
|
+
authConfig = require(basePath + '/config/auth');
|
|
28
30
|
} catch {
|
|
29
31
|
authConfig = {
|
|
30
32
|
default: 'jwt',
|
|
@@ -32,11 +34,48 @@ class AuthServiceProvider extends ServiceProvider {
|
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
+
// ── Resolve the User model ──────────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// Priority order (mirrors Django's AUTH_USER_MODEL pattern):
|
|
40
|
+
//
|
|
41
|
+
// 1. config/app.js → auth_user: 'User'
|
|
42
|
+
// The model name is looked up in app/models/index.js exports.
|
|
43
|
+
// This is the recommended approach — explicit and refactor-safe.
|
|
44
|
+
//
|
|
45
|
+
// 2. app/models/User.js (default export or named User export)
|
|
46
|
+
// Conventional fallback — works if auth_user is not set and
|
|
47
|
+
// the file exists at the default path.
|
|
48
|
+
//
|
|
49
|
+
// 3. Built-in AuthUser
|
|
50
|
+
// Abstract base class — no table. Used only as a last resort
|
|
51
|
+
// so Auth always has a model to work with during early dev.
|
|
52
|
+
//
|
|
53
|
+
let UserModel;
|
|
37
54
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
// Step 1: read auth_user from config/app.js
|
|
56
|
+
let authUserName = null;
|
|
57
|
+
try {
|
|
58
|
+
const appConfig = require(basePath + '/config/app');
|
|
59
|
+
authUserName = appConfig.auth_user || null;
|
|
60
|
+
} catch { /* config/app.js missing or has no auth_user key */ }
|
|
61
|
+
|
|
62
|
+
if (authUserName) {
|
|
63
|
+
// Resolve by name from app/models/index.js
|
|
64
|
+
const modelsIndex = require(basePath + '/app/models/index');
|
|
65
|
+
const resolved = modelsIndex[authUserName];
|
|
66
|
+
if (!resolved) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`[AuthServiceProvider] auth_user: '${authUserName}' not found in app/models/index.js.\n` +
|
|
69
|
+
` Available exports: ${Object.keys(modelsIndex).join(', ')}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
UserModel = resolved;
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err.message.includes('[AuthServiceProvider]')) throw err; // re-throw config errors
|
|
76
|
+
// Step 3: fall back to built-in AuthUser (abstract — no table)
|
|
77
|
+
UserModel = require('../auth/AuthUser');
|
|
78
|
+
}
|
|
40
79
|
|
|
41
80
|
// Configure the Auth singleton
|
|
42
81
|
Auth.configure(authConfig, UserModel);
|
|
@@ -50,4 +89,4 @@ class AuthServiceProvider extends ServiceProvider {
|
|
|
50
89
|
}
|
|
51
90
|
}
|
|
52
91
|
|
|
53
|
-
module.exports = AuthServiceProvider;
|
|
92
|
+
module.exports = AuthServiceProvider;
|
|
@@ -12,12 +12,13 @@ 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() {
|
|
18
19
|
let cacheConfig;
|
|
19
20
|
try {
|
|
20
|
-
cacheConfig = require(process.cwd() + '/config/cache');
|
|
21
|
+
cacheConfig = require((container.make('basePath') || process.cwd()) + '/config/cache');
|
|
21
22
|
} catch {
|
|
22
23
|
cacheConfig = {
|
|
23
24
|
default: process.env.CACHE_DRIVER || 'memory',
|
|
@@ -41,12 +42,13 @@ 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() {
|
|
47
49
|
let storageConfig;
|
|
48
50
|
try {
|
|
49
|
-
storageConfig = require(process.cwd() + '/config/storage');
|
|
51
|
+
storageConfig = require((container.make('basePath') || process.cwd()) + '/config/storage');
|
|
50
52
|
} catch {
|
|
51
53
|
storageConfig = {
|
|
52
54
|
default: process.env.STORAGE_DRIVER || 'local',
|
|
@@ -68,4 +70,4 @@ class StorageServiceProvider extends ServiceProvider {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
module.exports = { CacheServiceProvider, StorageServiceProvider };
|
|
73
|
+
module.exports = { CacheServiceProvider, StorageServiceProvider };
|
|
@@ -21,10 +21,11 @@ class DatabaseServiceProvider extends ServiceProvider {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
async boot(container) {
|
|
24
|
+
const basePath = container.make('basePath') || process.cwd();
|
|
24
25
|
// Load the database config
|
|
25
26
|
let dbConfig;
|
|
26
27
|
try {
|
|
27
|
-
dbConfig = require(
|
|
28
|
+
dbConfig = require(basePath + '/config/database');
|
|
28
29
|
} catch {
|
|
29
30
|
// Fallback for tests / programmatic use
|
|
30
31
|
dbConfig = {
|
|
@@ -42,4 +43,4 @@ class DatabaseServiceProvider extends ServiceProvider {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
module.exports = DatabaseServiceProvider;
|
|
46
|
+
module.exports = DatabaseServiceProvider;
|
|
@@ -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',
|
|
43
|
-
container.instance('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
|
|
|
@@ -58,6 +59,9 @@ class LogServiceProvider extends ServiceProvider {
|
|
|
58
59
|
beforeBoot(container) {
|
|
59
60
|
let config = {};
|
|
60
61
|
try {
|
|
62
|
+
// Note: beforeBoot runs before any container bindings exist.
|
|
63
|
+
// basePath is not yet available — process.cwd() is the correct fallback here.
|
|
64
|
+
// The path will be correct as long as `millas serve` is run from project root.
|
|
61
65
|
config = require(process.cwd() + '/config/logging');
|
|
62
66
|
} catch {
|
|
63
67
|
// No config file — defaults already applied in logger/index.js
|
|
@@ -205,4 +209,4 @@ class LogServiceProvider extends ServiceProvider {
|
|
|
205
209
|
}
|
|
206
210
|
}
|
|
207
211
|
|
|
208
|
-
module.exports = LogServiceProvider;
|
|
212
|
+
module.exports = LogServiceProvider;
|
|
@@ -19,14 +19,15 @@ const MailMessage = require('../mail/MailMessage');
|
|
|
19
19
|
*/
|
|
20
20
|
class MailServiceProvider extends ServiceProvider {
|
|
21
21
|
register(container) {
|
|
22
|
-
container.instance('Mail',
|
|
22
|
+
container.instance('Mail', Mail);
|
|
23
|
+
container.alias('mail', 'Mail');
|
|
23
24
|
container.instance('MailMessage', MailMessage);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
async boot(container) {
|
|
27
28
|
let mailConfig;
|
|
28
29
|
try {
|
|
29
|
-
mailConfig = require(process.cwd() + '/config/mail');
|
|
30
|
+
mailConfig = require((container.make('basePath') || process.cwd()) + '/config/mail');
|
|
30
31
|
} catch {
|
|
31
32
|
mailConfig = {
|
|
32
33
|
default: process.env.MAIL_DRIVER || 'log',
|
|
@@ -48,4 +49,4 @@ class MailServiceProvider extends ServiceProvider {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
module.exports = MailServiceProvider;
|
|
52
|
+
module.exports = MailServiceProvider;
|
|
@@ -21,14 +21,15 @@ const { dispatch } = require('../queue/Queue');
|
|
|
21
21
|
*/
|
|
22
22
|
class QueueServiceProvider extends ServiceProvider {
|
|
23
23
|
register(container) {
|
|
24
|
-
container.instance('Queue',
|
|
24
|
+
container.instance('Queue', Queue);
|
|
25
|
+
container.alias('queue', 'Queue');
|
|
25
26
|
container.instance('dispatch', dispatch);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
async boot(container) {
|
|
29
30
|
let queueConfig;
|
|
30
31
|
try {
|
|
31
|
-
queueConfig = require(process.cwd() + '/config/queue');
|
|
32
|
+
queueConfig = require((container.make('basePath') || process.cwd()) + '/config/queue');
|
|
32
33
|
} catch {
|
|
33
34
|
queueConfig = {
|
|
34
35
|
default: process.env.QUEUE_DRIVER || 'sync',
|
|
@@ -49,4 +50,4 @@ class QueueServiceProvider extends ServiceProvider {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
module.exports = QueueServiceProvider;
|
|
53
|
+
module.exports = QueueServiceProvider;
|
package/src/router/Router.js
CHANGED
|
@@ -1,211 +1,178 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 {
|
|
13
|
-
* @param {RouteRegistry}
|
|
14
|
-
* @param {MiddlewareRegistry}
|
|
15
|
-
* @param {Container|null}
|
|
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(
|
|
18
|
-
this.
|
|
23
|
+
constructor(adapter, registry, middlewareRegistry, container = null) {
|
|
24
|
+
this._adapter = adapter;
|
|
19
25
|
this._registry = registry;
|
|
20
|
-
this._mw = middlewareRegistry
|
|
26
|
+
this._mw = middlewareRegistry;
|
|
21
27
|
this._container = container;
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
24
32
|
/**
|
|
25
|
-
*
|
|
26
|
-
* Does NOT add
|
|
27
|
-
*
|
|
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
|
|
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
|
-
*
|
|
39
|
-
* Must be called LAST — after all routes and admin
|
|
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.
|
|
43
|
-
this.
|
|
49
|
+
this._maybeInjectWelcome();
|
|
50
|
+
this._adapter.mountNotFound();
|
|
51
|
+
this._adapter.mountErrorHandler();
|
|
44
52
|
return this;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/**
|
|
48
|
-
*
|
|
49
|
-
* AND add 404/error handlers (original behaviour).
|
|
56
|
+
* Mount routes + fallbacks in one call.
|
|
50
57
|
*/
|
|
51
58
|
mount() {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this
|
|
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
|
-
//
|
|
66
|
+
// ── Private ────────────────────────────────────────────────────────────────
|
|
61
67
|
|
|
62
68
|
_bindRoute(route) {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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]
|
|
78
|
-
return (
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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(
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Method "${method}" not found on controller "${handler.name}".`
|
|
118
|
+
);
|
|
97
119
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
134
|
+
throw new Error(`Invalid route handler: ${JSON.stringify(handler)}`);
|
|
123
135
|
}
|
|
124
136
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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;
|