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.
Files changed (116) hide show
  1. package/package.json +3 -16
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +400 -167
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +309 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +383 -97
  12. package/src/admin/static/admin.css +1341 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +65 -1013
  19. package/src/admin/views/pages/detail.njk +40 -16
  20. package/src/admin/views/pages/form.njk +47 -599
  21. package/src/admin/views/pages/list.njk +145 -62
  22. package/src/admin/views/partials/form-field.njk +53 -0
  23. package/src/admin/views/partials/form-footer.njk +28 -0
  24. package/src/admin/views/partials/form-readonly.njk +114 -0
  25. package/src/admin/views/partials/form-scripts.njk +476 -0
  26. package/src/admin/views/partials/form-widget.njk +296 -0
  27. package/src/admin/views/partials/json-dialog.njk +80 -0
  28. package/src/admin/views/partials/json-editor.njk +37 -0
  29. package/src/admin.zip +0 -0
  30. package/src/auth/Auth.js +31 -10
  31. package/src/auth/AuthController.js +3 -1
  32. package/src/auth/AuthUser.js +119 -0
  33. package/src/cli.js +4 -2
  34. package/src/commands/createsuperuser.js +254 -0
  35. package/src/commands/lang.js +589 -0
  36. package/src/commands/migrate.js +154 -81
  37. package/src/commands/serve.js +82 -110
  38. package/src/container/AppInitializer.js +215 -0
  39. package/src/container/Application.js +278 -253
  40. package/src/container/HttpServer.js +156 -0
  41. package/src/container/MillasApp.js +29 -279
  42. package/src/container/MillasConfig.js +192 -0
  43. package/src/core/admin.js +5 -0
  44. package/src/core/auth.js +9 -0
  45. package/src/core/db.js +9 -0
  46. package/src/core/foundation.js +59 -0
  47. package/src/core/http.js +11 -0
  48. package/src/core/lang.js +1 -0
  49. package/src/core/mail.js +6 -0
  50. package/src/core/queue.js +7 -0
  51. package/src/core/validation.js +29 -0
  52. package/src/facades/Admin.js +1 -1
  53. package/src/facades/Auth.js +22 -39
  54. package/src/facades/Cache.js +21 -10
  55. package/src/facades/Database.js +1 -1
  56. package/src/facades/Events.js +18 -17
  57. package/src/facades/Facade.js +197 -0
  58. package/src/facades/Http.js +42 -45
  59. package/src/facades/Log.js +25 -49
  60. package/src/facades/Mail.js +27 -32
  61. package/src/facades/Queue.js +22 -15
  62. package/src/facades/Storage.js +18 -10
  63. package/src/facades/Url.js +53 -0
  64. package/src/http/HttpClient.js +673 -0
  65. package/src/http/ResponseDispatcher.js +18 -111
  66. package/src/http/UrlGenerator.js +375 -0
  67. package/src/http/WelcomePage.js +273 -0
  68. package/src/http/adapters/ExpressAdapter.js +315 -0
  69. package/src/http/adapters/HttpAdapter.js +168 -0
  70. package/src/http/adapters/index.js +9 -0
  71. package/src/i18n/I18nServiceProvider.js +91 -0
  72. package/src/i18n/Translator.js +635 -0
  73. package/src/i18n/defaults.js +122 -0
  74. package/src/i18n/index.js +164 -0
  75. package/src/i18n/locales/en.js +55 -0
  76. package/src/i18n/locales/sw.js +48 -0
  77. package/src/index.js +5 -144
  78. package/src/logger/formatters/PrettyFormatter.js +103 -57
  79. package/src/logger/internal.js +2 -2
  80. package/src/logger/patchConsole.js +91 -81
  81. package/src/middleware/MiddlewareRegistry.js +62 -82
  82. package/src/migrations/system/0001_users.js +21 -0
  83. package/src/migrations/system/0002_admin_log.js +25 -0
  84. package/src/migrations/system/0003_sessions.js +23 -0
  85. package/src/orm/fields/index.js +210 -188
  86. package/src/orm/migration/DefaultValueParser.js +325 -0
  87. package/src/orm/migration/InteractiveResolver.js +191 -0
  88. package/src/orm/migration/Makemigrations.js +312 -0
  89. package/src/orm/migration/MigrationGraph.js +227 -0
  90. package/src/orm/migration/MigrationRunner.js +202 -108
  91. package/src/orm/migration/MigrationWriter.js +463 -0
  92. package/src/orm/migration/ModelInspector.js +412 -344
  93. package/src/orm/migration/ModelScanner.js +225 -0
  94. package/src/orm/migration/ProjectState.js +213 -0
  95. package/src/orm/migration/RenameDetector.js +175 -0
  96. package/src/orm/migration/SchemaBuilder.js +8 -81
  97. package/src/orm/migration/operations/base.js +57 -0
  98. package/src/orm/migration/operations/column.js +191 -0
  99. package/src/orm/migration/operations/fields.js +252 -0
  100. package/src/orm/migration/operations/index.js +55 -0
  101. package/src/orm/migration/operations/models.js +152 -0
  102. package/src/orm/migration/operations/registry.js +131 -0
  103. package/src/orm/migration/operations/special.js +51 -0
  104. package/src/orm/migration/utils.js +208 -0
  105. package/src/orm/model/Model.js +81 -13
  106. package/src/providers/AdminServiceProvider.js +66 -9
  107. package/src/providers/AuthServiceProvider.js +46 -7
  108. package/src/providers/CacheStorageServiceProvider.js +5 -3
  109. package/src/providers/DatabaseServiceProvider.js +3 -2
  110. package/src/providers/EventServiceProvider.js +2 -1
  111. package/src/providers/LogServiceProvider.js +7 -3
  112. package/src/providers/MailServiceProvider.js +4 -3
  113. package/src/providers/QueueServiceProvider.js +4 -3
  114. package/src/router/Router.js +119 -152
  115. package/src/scaffold/templates.js +83 -26
  116. package/src/facades/Validation.js +0 -69
@@ -1,88 +1,134 @@
1
1
  'use strict';
2
2
 
3
- const { LEVEL_NAMES, LEVEL_TAGS, LEVEL_COLOURS, RESET, BOLD, DIM } = require('../levels');
4
-
5
- /**
6
- * PrettyFormatter
7
- *
8
- * Colourful, human-readable output. Designed for development.
9
- * Inspired by Timber (Android) and Laravel's log formatting.
10
- *
11
- * Output:
12
- * [2026-03-15 12:00:00] I UserController User #5 logged in
13
- * [2026-03-15 12:00:01] E Database Connection refused { host: 'localhost' }
14
- * [2026-03-15 12:00:02] W Auth Token expiring soon
15
- *
16
- * WTF level also prints the full stack trace.
17
- */
3
+ const { LEVEL_TAGS, LEVEL_COLOURS, RESET, BOLD } = require('../levels');
4
+
5
+ const SEP = ' ';
6
+ const TAG_WIDTH = 18;
7
+
8
+ // Matches /absolute/path/to/file.js or /absolute/path/to/file.js:12:34
9
+ const FILE_PATH_RE = /(\/[^\s)'"]+\.(js|ts|json|mjs|cjs)(?::\d+(?::\d+)?)?)/g;
10
+ // Matches http(s):// URLs
11
+ const URL_RE = /(https?:\/\/[^\s)'"]+)/g;
12
+
13
+ function linkify(text, dim, useColour) {
14
+ if (!useColour) return text;
15
+
16
+ text = text.replace(FILE_PATH_RE, (match) => {
17
+ const filePath = match.replace(/(:\d+)+$/, '');
18
+ const uri = `file://${filePath}`;
19
+ return `${RESET}\x1b]8;;${uri}\x1b\\${match}\x1b]8;;\x1b\\`;
20
+ });
21
+
22
+ text = text.replace(URL_RE, (match) => {
23
+ return `${RESET}\x1b]8;;${match}\x1b\\${match}\x1b]8;;\x1b\\`;
24
+ });
25
+
26
+ return text;
27
+ }
28
+
18
29
  class PrettyFormatter {
19
- /**
20
- * @param {object} options
21
- * @param {boolean} [options.timestamp=true] — show timestamp
22
- * @param {boolean} [options.tag=true] — show tag/component name
23
- * @param {boolean} [options.colour=true] — ANSI colour (disable for pipes/files)
24
- * @param {string} [options.timestampFormat] — 'iso' | 'short' (default: 'short')
25
- */
26
30
  constructor(options = {}) {
27
31
  this.showTimestamp = options.timestamp !== false;
28
32
  this.showTag = options.tag !== false;
29
33
  this.colour = options.colour !== false;
30
34
  this.tsFormat = options.timestampFormat || 'short';
35
+ this.tagWidth = options.tagWidth || TAG_WIDTH;
31
36
  }
32
37
 
33
38
  format(entry) {
34
39
  const { level, tag, message, context, error } = entry;
35
40
 
36
- const c = this.colour ? LEVEL_COLOURS[level] : '';
37
- const r = this.colour ? RESET : '';
38
- const b = this.colour ? BOLD : '';
39
- const d = this.colour ? '\x1b[2m' : '';
40
- const lvl = LEVEL_TAGS[level] || '?';
41
+ const c = this.colour ? (LEVEL_COLOURS[level] || '') : '';
42
+ const r = this.colour ? RESET : '';
43
+ const b = this.colour ? BOLD : '';
44
+ const d = this.colour ? '\x1b[2m' : '';
45
+ const lvl = LEVEL_TAGS[level] || '?';
41
46
 
42
- const parts = [];
47
+ // ── 1. Measure plain prefix once ─────────────────────────────────────────
48
+ const ts = this._timestamp();
49
+ const plainCols = [];
50
+ if (this.showTimestamp) plainCols.push(`[${ts}]`);
51
+ plainCols.push(lvl);
52
+ if (this.showTag && tag) plainCols.push(tag.padEnd(this.tagWidth));
43
53
 
44
- // Timestamp
45
- if (this.showTimestamp) {
46
- const ts = this._timestamp();
47
- parts.push(`${d}[${ts}]${r}`);
48
- }
54
+ const indentWidth = plainCols.join(SEP).length + SEP.length;
55
+ const indent = ' '.repeat(indentWidth);
49
56
 
50
- // Level tag (single letter, coloured)
51
- parts.push(`${c}${b}${lvl}${r}`);
57
+ // ── 2. Coloured prefix ───────────────────────────────────────────────────
58
+ const colCols = [];
59
+ if (this.showTimestamp) colCols.push(`${d}[${ts}]${r}`);
60
+ colCols.push(`${c}${b}${lvl}${r}`);
61
+ if (this.showTag && tag) colCols.push(`${b}${tag.padEnd(this.tagWidth)}${r}`);
52
62
 
53
- // Component/tag
54
- if (this.showTag && tag) {
55
- const tagStr = tag.padEnd(18);
56
- parts.push(`${b}${tagStr}${r}`);
57
- }
63
+ const prefix = colCols.join(SEP) + SEP;
58
64
 
59
- // Message
60
- parts.push(`${c}${message}${r}`);
65
+ // ── 3. Terminal width ────────────────────────────────────────────────────
66
+ const termWidth = (process.stdout.columns || 120);
67
+ const msgWidth = termWidth - indentWidth;
61
68
 
62
- // Context object
63
- if (context !== undefined && context !== null) {
64
- const ctx = typeof context === 'object'
65
- ? JSON.stringify(context, null, 0)
66
- : String(context);
67
- parts.push(`${d}${ctx}${r}`);
68
- }
69
+ // ── 4. Collect all logical lines ─────────────────────────────────────────
70
+ const logicalLines = [];
71
+
72
+ for (const l of String(message).split('\n')) logicalLines.push({ text: l, dim: false });
69
73
 
70
- let output = parts.join(' ');
74
+ if (context != null) {
75
+ const ctx = typeof context === 'object' ? JSON.stringify(context) : String(context);
76
+ logicalLines.push({ text: ctx, dim: true });
77
+ }
71
78
 
72
- // Error stack
73
79
  if (error instanceof Error) {
74
- output += `\n${d}${error.stack || error.message}${r}`;
80
+ for (const l of (error.stack || error.message).split('\n'))
81
+ logicalLines.push({ text: l, dim: true });
82
+ }
83
+
84
+ // ── 5. Hard-wrap (skip stack frames so paths stay intact) ────────────────
85
+ const wrappedLines = [];
86
+
87
+ for (const { text, dim } of logicalLines) {
88
+ const isStackFrame = dim && /^\s*at /.test(text);
89
+
90
+ if (isStackFrame || text.length <= msgWidth) {
91
+ wrappedLines.push({ text, dim });
92
+ continue;
93
+ }
94
+
95
+ const words = text.split(' ');
96
+ let chunk = '';
97
+ for (const word of words) {
98
+ if (chunk.length === 0) {
99
+ if (word.length > msgWidth) {
100
+ for (let i = 0; i < word.length; i += msgWidth)
101
+ wrappedLines.push({ text: word.slice(i, i + msgWidth), dim });
102
+ } else {
103
+ chunk = word;
104
+ }
105
+ } else if (chunk.length + 1 + word.length <= msgWidth) {
106
+ chunk += ' ' + word;
107
+ } else {
108
+ wrappedLines.push({ text: chunk, dim });
109
+ chunk = word;
110
+ }
111
+ }
112
+ if (chunk.length) wrappedLines.push({ text: chunk, dim });
75
113
  }
76
114
 
77
- // WTF: print big warning banner
115
+ // ── 6. Render + linkify ───────────────────────────────────────────────────
116
+ const rendered = wrappedLines.map((line, i) => {
117
+ const col = line.dim ? d : c;
118
+ const text = linkify(line.text, line.dim, this.colour);
119
+ if (i === 0) return `${prefix}${col}${text}${r}`;
120
+ return `${indent}${col}${text}${r}`;
121
+ }).join('\n');
122
+
123
+ // ── 7. WTF banner ─────────────────────────────────────────────────────────
78
124
  if (level === 5) {
79
- const banner = this.colour
125
+ const bar = this.colour
80
126
  ? `\x1b[35m\x1b[1m${'━'.repeat(60)}\x1b[0m`
81
127
  : '━'.repeat(60);
82
- output = `${banner}\n${output}\n${banner}`;
128
+ return `${bar}\n${rendered}\n${bar}`;
83
129
  }
84
130
 
85
- return output;
131
+ return rendered;
86
132
  }
87
133
 
88
134
  _timestamp() {
@@ -92,4 +138,4 @@ class PrettyFormatter {
92
138
  }
93
139
  }
94
140
 
95
- module.exports = PrettyFormatter;
141
+ module.exports = PrettyFormatter;
@@ -64,12 +64,12 @@ const MillasLog = new Logger();
64
64
  // from the framework itself unless you opt in to lower levels.
65
65
  MillasLog.configure({
66
66
  defaultTag: 'Millas',
67
- minLevel: LEVELS.WARN,
67
+ minLevel: LEVELS.VERBOSE,
68
68
  channel: new ConsoleChannel({
69
69
  formatter: new PrettyFormatter({
70
70
  colour: process.stdout.isTTY !== false,
71
71
  }),
72
- minLevel: LEVELS.WARN,
72
+ minLevel: LEVELS.VERBOSE,
73
73
  }),
74
74
  });
75
75
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { LEVELS } = require('./levels');
3
+ const {LEVELS} = require('./levels');
4
4
 
5
5
  /**
6
6
  * patchConsole(Log, defaultTag)
@@ -25,44 +25,54 @@ const { LEVELS } = require('./levels');
25
25
  * console.dir → Log.d (DEBUG)
26
26
  */
27
27
  function patchConsole(Log, defaultTag = 'App') {
28
- // Save originals — restore() puts these back
29
- const originals = {
30
- log: console.log.bind(console),
31
- info: console.info.bind(console),
32
- warn: console.warn.bind(console),
33
- error: console.error.bind(console),
34
- debug: console.debug.bind(console),
35
- trace: console.trace.bind(console),
36
- dir: console.dir.bind(console),
37
- };
38
-
39
- // Build a dispatcher for a given level
40
- function make(level) {
41
- return function (...args) {
42
- const { message, context, error } = parse(args);
43
- Log._emit({
44
- level,
45
- tag: defaultTag,
46
- message: message || '',
47
- context,
48
- error,
28
+
29
+ // Save originals restore() puts these back
30
+ const originals = {
31
+ log: console.log.bind(console),
32
+ info: console.info.bind(console),
33
+ warn: console.warn.bind(console),
34
+ error: console.error.bind(console),
35
+ debug: console.debug.bind(console),
36
+ trace: console.trace.bind(console),
37
+ dir: console.dir.bind(console),
38
+ };
39
+
40
+ // Build a dispatcher for a given level
41
+ function make(level) {
42
+ return function (...args) {
43
+ const {message, context, error} = parse(args);
44
+ Log._emit({
45
+ level,
46
+ tag: defaultTag,
47
+ message: message || '',
48
+ context,
49
+ error,
50
+ timestamp: new Date().toISOString(),
51
+ pid: process.pid,
52
+ });
53
+ return true
54
+ };
55
+ }
56
+
57
+ console.log = make(LEVELS.INFO);
58
+ console.info = make(LEVELS.INFO);
59
+ console.warn = make(LEVELS.WARN);
60
+ console.error = make(LEVELS.ERROR);
61
+ console.debug = make(LEVELS.DEBUG);
62
+ console.trace = make(LEVELS.VERBOSE);
63
+ console.dir = (obj) => Log._emit({
64
+ level: LEVELS.DEBUG,
65
+ tag: defaultTag,
66
+ message: '',
67
+ context: obj,
68
+ error: undefined,
49
69
  timestamp: new Date().toISOString(),
50
- pid: process.pid,
51
- });
70
+ pid: process.pid
71
+ });
72
+
73
+ return function restore() {
74
+ Object.assign(console, originals);
52
75
  };
53
- }
54
-
55
- console.log = make(LEVELS.INFO);
56
- console.info = make(LEVELS.INFO);
57
- console.warn = make(LEVELS.WARN);
58
- console.error = make(LEVELS.ERROR);
59
- console.debug = make(LEVELS.DEBUG);
60
- console.trace = make(LEVELS.VERBOSE);
61
- console.dir = (obj) => Log._emit({ level: LEVELS.DEBUG, tag: defaultTag, message: '', context: obj, error: undefined, timestamp: new Date().toISOString(), pid: process.pid });
62
-
63
- return function restore() {
64
- Object.assign(console, originals);
65
- };
66
76
  }
67
77
 
68
78
  // ── Argument parser ───────────────────────────────────────────────────────────
@@ -78,58 +88,58 @@ function patchConsole(Log, defaultTag = 'App') {
78
88
  // console.log('a', 'b', 'c') → message: 'a b c'
79
89
 
80
90
  function parse(args) {
81
- if (args.length === 0) {
82
- return { message: '', context: undefined, error: undefined };
83
- }
84
-
85
- if (args.length === 1) {
86
- const a = args[0];
87
- if (a instanceof Error) return { message: a.message, context: undefined, error: a };
88
- if (typeof a === 'object' && a !== null) return { message: '', context: a, error: undefined };
89
- return { message: String(a), context: undefined, error: undefined };
90
- }
91
-
92
- const [first, ...rest] = args;
93
-
94
- // First arg is an Error
95
- if (first instanceof Error) {
96
- return { message: first.message, context: rest.length ? rest : undefined, error: first };
97
- }
98
-
99
- // First arg is a string message
100
- if (typeof first === 'string') {
101
- // Single extra arg
102
- if (rest.length === 1) {
103
- const r = rest[0];
104
- if (r instanceof Error) return { message: first, context: undefined, error: r };
105
- if (typeof r === 'object' && r !== null) return { message: first, context: r, error: undefined };
106
- // Scalar extra: append to message (console.log('count:', 42))
107
- return { message: first + ' ' + String(r), context: undefined, error: undefined };
91
+ if (args.length === 0) {
92
+ return {message: '', context: undefined, error: undefined};
108
93
  }
109
94
 
110
- // Multiple extra args find a trailing Error, collect the rest as context
111
- const lastArg = rest[rest.length - 1];
112
- if (lastArg instanceof Error) {
113
- const ctx = rest.slice(0, -1);
114
- return { message: first, context: ctx.length ? ctx : undefined, error: lastArg };
95
+ if (args.length === 1) {
96
+ const a = args[0];
97
+ if (a instanceof Error) return {message: a.message, context: undefined, error: a};
98
+ if (typeof a === 'object' && a !== null) return {message: '', context: a, error: undefined};
99
+ return {message: String(a), context: undefined, error: undefined};
115
100
  }
116
101
 
117
- // All strings/scalars join into message
118
- if (rest.every(r => typeof r !== 'object' || r === null)) {
119
- return { message: [first, ...rest].map(String).join(' '), context: undefined, error: undefined };
102
+ const [first, ...rest] = args;
103
+
104
+ // First arg is an Error
105
+ if (first instanceof Error) {
106
+ return {message: first.message, context: rest.length ? rest : undefined, error: first};
120
107
  }
121
108
 
122
- // Mixed put extras in context
123
- return { message: first, context: rest, error: undefined };
124
- }
109
+ // First arg is a string message
110
+ if (typeof first === 'string') {
111
+ // Single extra arg
112
+ if (rest.length === 1) {
113
+ const r = rest[0];
114
+ if (r instanceof Error) return {message: first, context: undefined, error: r};
115
+ if (typeof r === 'object' && r !== null) return {message: first, context: r, error: undefined};
116
+ // Scalar extra: append to message (console.log('count:', 42))
117
+ return {message: first + ' ' + String(r), context: undefined, error: undefined};
118
+ }
119
+
120
+ // Multiple extra args — find a trailing Error, collect the rest as context
121
+ const lastArg = rest[rest.length - 1];
122
+ if (lastArg instanceof Error) {
123
+ const ctx = rest.slice(0, -1);
124
+ return {message: first, context: ctx.length ? ctx : undefined, error: lastArg};
125
+ }
126
+
127
+ // All strings/scalars — join into message
128
+ if (rest.every(r => typeof r !== 'object' || r === null)) {
129
+ return {message: [first, ...rest].map(String).join(' '), context: undefined, error: undefined};
130
+ }
131
+
132
+ // Mixed — put extras in context
133
+ return {message: first, context: rest, error: undefined};
134
+ }
125
135
 
126
- // First arg is an object
127
- if (typeof first === 'object' && first !== null) {
128
- return { message: '', context: first, error: undefined };
129
- }
136
+ // First arg is an object
137
+ if (typeof first === 'object' && first !== null) {
138
+ return {message: '', context: first, error: undefined};
139
+ }
130
140
 
131
- // Fallback — join everything as a string
132
- return { message: args.map(String).join(' '), context: undefined, error: undefined };
141
+ // Fallback — join everything as a string
142
+ return {message: args.map(String).join(' '), context: undefined, error: undefined};
133
143
  }
134
144
 
135
145
  module.exports = patchConsole;
@@ -1,54 +1,68 @@
1
1
  'use strict';
2
2
 
3
- const MillasRequest = require('../http/MillasRequest');
4
- const MillasResponse = require('../http/MillasResponse');
5
- const ResponseDispatcher = require('../http/ResponseDispatcher');
6
- const RequestContext = require('../http/RequestContext');
7
-
8
3
  /**
9
4
  * MiddlewareRegistry
10
5
  *
11
- * Maps string aliases → middleware handler classes or functions.
12
- * Resolves them into Express-compatible functions that wrap with MillasRequest.
6
+ * Maps string aliases → Millas middleware classes or instances.
7
+ * Resolution produces adapter-native handler functions via the adapter,
8
+ * so this class has zero knowledge of Express (or any HTTP engine).
9
+ *
10
+ * The adapter is injected at resolution time (not construction time)
11
+ * so the registry can be built before the adapter exists.
13
12
  */
14
13
  class MiddlewareRegistry {
15
- constructor(container = null) {
16
- this._map = {};
17
- this._container = container;
14
+ constructor() {
15
+ this._map = {};
18
16
  }
19
17
 
18
+ /**
19
+ * Register a middleware alias.
20
+ *
21
+ * registry.register('auth', AuthMiddleware)
22
+ * registry.register('throttle', new ThrottleMiddleware({ max: 60 }))
23
+ */
20
24
  register(alias, handler) {
21
25
  this._map[alias] = handler;
22
26
  return this;
23
27
  }
24
28
 
25
29
  /**
26
- * Resolve a middleware alias or function into an Express-compatible handler.
27
- *
28
- * Millas middleware (class with handle(req, next)):
29
- * - Receives MillasRequest
30
- * - Returns MillasResponse or calls next()
31
- * - Kernel dispatches the MillasResponse if returned
30
+ * Resolve a middleware alias or class/instance into an adapter-native handler.
32
31
  *
33
- * Raw Express functions (legacy/escape hatch):
34
- * - Passed through as-is
32
+ * @param {string|Function|object} aliasOrFn
33
+ * @param {import('../http/adapters/HttpAdapter')} adapter
34
+ * @param {object|null} container
35
+ * @returns {Function} adapter-native handler
35
36
  */
36
- resolve(aliasOrFn) {
37
- // Raw function check if it's a Millas middleware class or legacy Express fn
38
- if (typeof aliasOrFn === 'function') {
39
- return this._wrapHandler(aliasOrFn);
40
- }
37
+ resolve(aliasOrFn, adapter, container = null) {
38
+ const Handler = typeof aliasOrFn === 'string'
39
+ ? this._map[aliasOrFn]
40
+ : aliasOrFn;
41
41
 
42
- const Handler = this._map[aliasOrFn];
43
42
  if (!Handler) {
44
43
  throw new Error(`Middleware "${aliasOrFn}" is not registered.`);
45
44
  }
46
45
 
47
- return this._wrapHandler(Handler);
46
+ return this._wrap(Handler, adapter, container);
47
+ }
48
+
49
+ /**
50
+ * Resolve all aliases in a list.
51
+ */
52
+ resolveAll(list = [], adapter, container = null) {
53
+ return list.map(m => this.resolve(m, adapter, container));
48
54
  }
49
55
 
50
- resolveAll(list = []) {
51
- return list.map(m => this.resolve(m));
56
+ /**
57
+ * Return a no-op passthrough handler for the given adapter.
58
+ * Used when a middleware alias is missing but should not crash the app.
59
+ */
60
+ resolvePassthrough(adapter) {
61
+ // Adapter-agnostic: return a function matching the native signature
62
+ // by asking the adapter to wrap a no-op middleware instance.
63
+ return adapter.wrapMiddleware({
64
+ handle: (_ctx, next) => next(),
65
+ }, null);
52
66
  }
53
67
 
54
68
  has(alias) {
@@ -56,71 +70,37 @@ class MiddlewareRegistry {
56
70
  }
57
71
 
58
72
  all() {
59
- return { ...this._map }; }
73
+ return { ...this._map };
74
+ }
60
75
 
61
- // ─── Internal ──────────────────────────────────────────────────────────────
76
+ // ── Internal ────────────────────────────────────────────────────────────────
62
77
 
63
- _wrapHandler(Handler) {
64
- // Pre-instantiated Millas middleware object: { handle(req, next) }
65
- if (typeof Handler === 'object' && Handler !== null &&
66
- typeof Handler.handle === 'function') {
67
- return this._buildMillasWrapper(Handler);
78
+ _wrap(Handler, adapter, container) {
79
+ // Pre-instantiated Millas middleware object with handle()
80
+ if (
81
+ typeof Handler === 'object' &&
82
+ Handler !== null &&
83
+ typeof Handler.handle === 'function'
84
+ ) {
85
+ return adapter.wrapMiddleware(Handler, container);
68
86
  }
69
87
 
70
- // Millas middleware class (has handle on prototype, not Express signature)
71
- if (typeof Handler === 'function' &&
72
- Handler.prototype &&
73
- typeof Handler.prototype.handle === 'function') {
74
- const instance = new Handler();
75
- return this._buildMillasWrapper(instance);
88
+ // Millas middleware class (handle on prototype)
89
+ if (
90
+ typeof Handler === 'function' &&
91
+ Handler.prototype &&
92
+ typeof Handler.prototype.handle === 'function'
93
+ ) {
94
+ return adapter.wrapMiddleware(new Handler(), container);
76
95
  }
77
96
 
78
- // Legacy raw Express function: (req, res, next) => void
79
- // Pass through unchanged — developers using old style still work
97
+ // Raw adapter-native function pass through as-is (escape hatch)
80
98
  if (typeof Handler === 'function') {
81
99
  return Handler;
82
100
  }
83
101
 
84
- throw new Error(`Middleware must be a function or a class with handle().`);
85
- }
86
-
87
- /**
88
- * Build an Express-compatible function from a Millas middleware instance.
89
- *
90
- * The middleware's handle(req, next) is called with a MillasRequest.
91
- * If it returns a MillasResponse, that response is dispatched immediately.
92
- * If it calls next(), Express continues down the chain.
93
- */
94
- _buildMillasWrapper(instance) {
95
- const container = this._container;
96
-
97
- return (expressReq, expressRes, expressNext) => {
98
- const millaReq = new MillasRequest(expressReq);
99
- const ctx = new RequestContext(millaReq, container);
100
-
101
- const next = () => {
102
- expressNext();
103
- return undefined;
104
- };
105
-
106
- new Promise((resolve, reject) => {
107
- try {
108
- resolve(instance.handle(ctx, next));
109
- } catch (err) {
110
- reject(err);
111
- }
112
- })
113
- .then(value => {
114
- if (value !== undefined && value !== null && !expressRes.headersSent) {
115
- const response = MillasResponse.isResponse(value)
116
- ? value
117
- : ResponseDispatcher.autoWrap(value);
118
- ResponseDispatcher.dispatch(response, expressRes);
119
- }
120
- })
121
- .catch(expressNext);
122
- };
102
+ throw new Error('Middleware must be a function or a class with handle().');
123
103
  }
124
104
  }
125
105
 
126
- module.exports = MiddlewareRegistry;
106
+ module.exports = MiddlewareRegistry;
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * System migration: 0001_users
5
+ *
6
+ * Previously created the users table directly.
7
+ *
8
+ * As of Millas 0.3+, the users table is owned by the APP — not the framework.
9
+ * AuthUser is now fully abstract (no table). The app defines its own User model
10
+ * that extends AuthUser and owns whatever table it wants (typically 'users').
11
+ *
12
+ * This migration is kept as a no-op so existing projects that depend on
13
+ * ['system', '0001_users'] don't break. It creates nothing.
14
+ *
15
+ * Equivalent to Django's pattern: AbstractUser has no table; your custom
16
+ * User model creates its own table via app migrations.
17
+ */
18
+ module.exports = {
19
+ dependencies: [],
20
+ operations: [],
21
+ };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const { CreateModel } = require('../../orm/migration/operations');
4
+
5
+ /**
6
+ * System migration: millas_admin_log
7
+ * Equivalent to Django's django_admin_log.
8
+ */
9
+ module.exports = {
10
+ dependencies: [['system', '0001_users']],
11
+
12
+ operations: [
13
+ new CreateModel('millas_admin_log', {
14
+ id: { type: 'id', unsigned: true, nullable: false, unique: false, default: null, max: null, enumValues: null, references: null, precision: null, scale: null },
15
+ user_id: { type: 'integer', unsigned: true, nullable: true, unique: false, default: null, max: null, enumValues: null, references: { table: 'users', column: 'id', onDelete: 'SET NULL' }, precision: null, scale: null },
16
+ user_email: { type: 'string', max: 255, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
17
+ resource: { type: 'string', max: 100, nullable: false, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
18
+ record_id: { type: 'string', max: 100, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
19
+ action: { type: 'enum', enumValues: ['create','update','delete'], nullable: false, unique: false, default: null, max: null, unsigned: false, references: null, precision: null, scale: null },
20
+ label: { type: 'string', max: 255, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
21
+ change_msg: { type: 'text', nullable: true, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
22
+ created_at: { type: 'timestamp', nullable: true, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
23
+ }),
24
+ ],
25
+ };