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
package/package.json CHANGED
@@ -1,25 +1,12 @@
1
1
  {
2
2
  "name": "millas",
3
- "version": "0.2.12-beta",
3
+ "version": "0.2.12-beta-2",
4
4
  "description": "A modern batteries-included backend framework for Node.js — built on Express, inspired by Laravel, Django, and FastAPI",
5
5
  "main": "src/index.js",
6
6
  "exports": {
7
7
  ".": "./src/index.js",
8
- "./src": "./src/index.js",
9
- "./src/*": "./src/*.js",
10
- "./bin/*": "./bin/*.js",
11
- "./validation": "./src/validation/Validator.js",
12
- "./facades/Http": "./src/facades/Http.js",
13
- "./facades/Validation": "./src/facades/Validation.js",
14
- "./facades/Database": "./src/facades/Database.js",
15
- "./facades/Auth": "./src/facades/Auth.js",
16
- "./facades/Log": "./src/facades/Log.js",
17
- "./facades/Cache": "./src/facades/Cache.js",
18
- "./facades/Mail": "./src/facades/Mail.js",
19
- "./facades/Queue": "./src/facades/Queue.js",
20
- "./facades/Events": "./src/facades/Events.js",
21
- "./facades/Storage": "./src/facades/Storage.js",
22
- "./facades/Admin": "./src/facades/Admin.js"
8
+ "./core/*": "./src/core/*.js",
9
+ "./facades/*": "./src/facades/*.js"
23
10
  },
24
11
  "bin": {
25
12
  "millas": "./bin/millas.js"
@@ -1,95 +1,196 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * AdminActivityLog
4
+ * ActivityLog
5
5
  *
6
- * Lightweight in-process activity log for the admin panel.
7
- * Stores the last N actions in a ring buffer — no database required.
6
+ * Persistent admin activity log equivalent to Django's django_admin_log.
8
7
  *
9
- * Each entry:
10
- * { id, action, resource, recordId, label, user, at, meta }
8
+ * ── Storage strategy ─────────────────────────────────────────────────────────
11
9
  *
12
- * Usage (automatic Admin.js calls this internally):
13
- * ActivityLog.record('create', 'users', 5, 'Alice Smith');
14
- * ActivityLog.record('update', 'posts', 12, 'Hello World');
15
- * ActivityLog.record('delete', 'comments', 7, '#7');
10
+ * Writes go to the `millas_admin_log` DB table (created by system migration
11
+ * 0002_admin_log). If the DB is unavailable (table not yet created, connection
12
+ * not configured, etc.) it falls back silently to an in-memory ring buffer
13
+ * so the admin panel never crashes because of a log write.
16
14
  *
17
- * Read:
18
- * ActivityLog.recent(20) // last 20 entries, newest first
19
- * ActivityLog.forResource('users', 10)
15
+ * Reads come from the DB when available, memory otherwise.
16
+ *
17
+ * All DB writes are fire-and-forget — they never block or throw into
18
+ * the calling request handler.
19
+ *
20
+ * ── Usage ─────────────────────────────────────────────────────────────────────
21
+ *
22
+ * // Admin.js calls this automatically — you don't call it directly.
23
+ * ActivityLog.record('create', 'posts', 5, 'Hello World', req.adminUser);
24
+ * ActivityLog.record('update', 'users', 12, 'alice@example.com', req.adminUser);
25
+ * ActivityLog.record('delete', 'comments', 7, '#7', req.adminUser);
26
+ *
27
+ * // Dashboard reads
28
+ * const entries = await ActivityLog.recent(25);
29
+ * const totals = await ActivityLog.totals();
30
+ *
31
+ * ── Entry shape ───────────────────────────────────────────────────────────────
32
+ *
33
+ * {
34
+ * id: number,
35
+ * user_id: number|null,
36
+ * user_email: string,
37
+ * resource: string, // resource slug
38
+ * record_id: string, // PK of affected record (null for bulk)
39
+ * action: 'create'|'update'|'delete',
40
+ * label: string, // human-readable record name
41
+ * change_msg: string, // optional extra detail
42
+ * created_at: string, // ISO timestamp
43
+ * }
20
44
  */
21
45
  class AdminActivityLog {
22
- constructor(maxSize = 200) {
23
- this._log = [];
24
- this._maxSize = maxSize;
46
+ constructor(maxMemory = 200) {
47
+ this._mem = []; // in-memory fallback ring buffer
48
+ this._maxMem = maxMemory;
25
49
  this._seq = 0;
50
+ this._dbReady = null; // null=unknown, true=available, false=unavailable
26
51
  }
27
52
 
53
+ // ─── Public API ────────────────────────────────────────────────────────────
54
+
28
55
  /**
29
56
  * Record an admin action.
57
+ *
30
58
  * @param {'create'|'update'|'delete'} action
31
- * @param {string} resource — resource slug
32
- * @param {*} recordId
33
- * @param {string} label — human-readable name of the record
34
- * @param {string} [user] who performed the action (optional)
59
+ * @param {string} resource — resource slug
60
+ * @param {*} recordId — PK of the affected record (null for bulk)
61
+ * @param {string} label — human-readable description
62
+ * @param {object|null} user req.adminUser (live User model instance)
63
+ * @param {string} [changeMsg] — optional detail (changed fields, etc.)
35
64
  */
36
- record(action, resource, recordId, label, user = null) {
37
- this._seq++;
65
+ record(action, resource, recordId, label, user = null, changeMsg = null) {
66
+ const userId = user?.id || null;
67
+ const userEmail = user?.email || 'System';
38
68
  const entry = {
39
- id: this._seq,
40
- action,
69
+ user_id: userId,
70
+ user_email: userEmail,
41
71
  resource,
42
- recordId,
43
- label: label || `#${recordId}`,
44
- user: user || 'Admin',
45
- at: new Date().toISOString(),
46
- _ts: Date.now(),
72
+ record_id: recordId !== null && recordId !== undefined ? String(recordId) : null,
73
+ action,
74
+ label: label || (recordId ? `#${recordId}` : resource),
75
+ change_msg: changeMsg,
76
+ created_at: new Date().toISOString(),
47
77
  };
48
78
 
49
- this._log.unshift(entry);
79
+ // ── Write to DB fire-and-forget ──────────────────────────────────────
80
+ this._writeDb(entry).catch(() => {});
50
81
 
51
- // Trim to max size
52
- if (this._log.length > this._maxSize) {
53
- this._log.length = this._maxSize;
54
- }
55
- }
56
-
57
- /**
58
- * Return the most recent N entries, newest first.
59
- */
60
- recent(n = 20) {
61
- return this._log.slice(0, n);
82
+ // ── Always write to in-memory buffer too ─────────────────────────────
83
+ // This ensures recent() works immediately even before the DB round-trip
84
+ // completes, and provides a fallback if DB is unavailable.
85
+ this._seq++;
86
+ this._mem.unshift({ id: this._seq, ...entry });
87
+ if (this._mem.length > this._maxMem) this._mem.length = this._maxMem;
62
88
  }
63
89
 
64
90
  /**
65
- * Return recent entries for a specific resource slug.
91
+ * Return the most recent N log entries, newest first.
92
+ * Reads from DB if available, memory otherwise.
66
93
  */
67
- forResource(slug, n = 10) {
68
- return this._log.filter(e => e.resource === slug).slice(0, n);
94
+ async recent(n = 25) {
95
+ try {
96
+ const db = this._getDb();
97
+ if (db) {
98
+ const rows = await db('millas_admin_log')
99
+ .orderBy('id', 'desc')
100
+ .limit(n);
101
+ return rows;
102
+ }
103
+ } catch { /* DB unavailable */ }
104
+ return this._mem.slice(0, n);
69
105
  }
70
106
 
71
107
  /**
72
- * Return totals by action type.
108
+ * Return action totals for the current calendar day.
73
109
  * { create: N, update: N, delete: N }
110
+ * Reads from DB if available, memory otherwise.
74
111
  */
75
- totals() {
112
+ async totals() {
113
+ try {
114
+ const db = this._getDb();
115
+ if (db) {
116
+ const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
117
+ const rows = await db('millas_admin_log')
118
+ .whereRaw(`date(created_at) = ?`, [today])
119
+ .select('action')
120
+ .count('* as count')
121
+ .groupBy('action');
122
+
123
+ const t = { create: 0, update: 0, delete: 0 };
124
+ for (const r of rows) {
125
+ if (t[r.action] !== undefined) t[r.action] = Number(r.count);
126
+ }
127
+ return t;
128
+ }
129
+ } catch { /* DB unavailable */ }
130
+
131
+ // In-memory fallback — count today's entries
132
+ const today = new Date().toISOString().slice(0, 10);
76
133
  const t = { create: 0, update: 0, delete: 0 };
77
- for (const e of this._log) {
78
- if (t[e.action] !== undefined) t[e.action]++;
134
+ for (const e of this._mem) {
135
+ if (e.created_at?.slice(0, 10) === today && t[e.action] !== undefined) {
136
+ t[e.action]++;
137
+ }
79
138
  }
80
139
  return t;
81
140
  }
82
141
 
83
142
  /**
84
- * Clear all log entries.
143
+ * Return recent entries for a specific resource slug.
85
144
  */
86
- clear() {
87
- this._log = [];
88
- this._seq = 0;
145
+ async forResource(slug, n = 10) {
146
+ try {
147
+ const db = this._getDb();
148
+ if (db) {
149
+ return db('millas_admin_log')
150
+ .where('resource', slug)
151
+ .orderBy('id', 'desc')
152
+ .limit(n);
153
+ }
154
+ } catch {}
155
+ return this._mem.filter(e => e.resource === slug).slice(0, n);
156
+ }
157
+
158
+ /**
159
+ * Clear all log entries (DB + memory).
160
+ * Primarily for testing.
161
+ */
162
+ async clear() {
163
+ this._mem = [];
164
+ this._seq = 0;
165
+ try {
166
+ const db = this._getDb();
167
+ if (db) await db('millas_admin_log').delete();
168
+ } catch {}
169
+ }
170
+
171
+ // ─── Internals ─────────────────────────────────────────────────────────────
172
+
173
+ async _writeDb(entry) {
174
+ const db = this._getDb();
175
+ if (!db) return;
176
+ await db('millas_admin_log').insert(entry);
177
+ this._dbReady = true;
178
+ }
179
+
180
+ _getDb() {
181
+ // Don't retry after a confirmed failure in this process lifetime
182
+ if (this._dbReady === false) return null;
183
+ try {
184
+ const DatabaseManager = require('../orm/drivers/DatabaseManager');
185
+ if (!DatabaseManager._config) return null;
186
+ return DatabaseManager.connection();
187
+ } catch {
188
+ return null;
189
+ }
89
190
  }
90
191
  }
91
192
 
92
193
  // Singleton
93
194
  const activityLog = new AdminActivityLog();
94
195
  module.exports = activityLog;
95
- module.exports.AdminActivityLog = AdminActivityLog;
196
+ module.exports.AdminActivityLog = AdminActivityLog;