millas 0.2.12-beta-1 → 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 +1 -1
- 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 +18 -2
- package/src/auth/AuthUser.js +65 -44
- package/src/cli.js +3 -1
- 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 +1 -0
- package/src/container/AppInitializer.js +65 -8
- package/src/container/MillasApp.js +10 -3
- package/src/container/MillasConfig.js +35 -6
- package/src/core/admin.js +5 -0
- package/src/core/db.js +2 -1
- package/src/core/foundation.js +1 -9
- package/src/core/lang.js +1 -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/logger/formatters/PrettyFormatter.js +101 -65
- 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 +143 -74
- 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 +40 -5
- package/src/providers/CacheStorageServiceProvider.js +2 -2
- package/src/providers/DatabaseServiceProvider.js +3 -2
- package/src/providers/LogServiceProvider.js +4 -1
- package/src/providers/MailServiceProvider.js +1 -1
- package/src/providers/QueueServiceProvider.js +1 -1
- package/src/scaffold/templates.js +77 -21
package/package.json
CHANGED
package/src/admin/ActivityLog.js
CHANGED
|
@@ -1,95 +1,196 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* ActivityLog
|
|
5
5
|
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
10
|
-
* { id, action, resource, recordId, label, user, at, meta }
|
|
8
|
+
* ── Storage strategy ─────────────────────────────────────────────────────────
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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(
|
|
23
|
-
this.
|
|
24
|
-
this.
|
|
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}
|
|
32
|
-
* @param {*}
|
|
33
|
-
* @param {string}
|
|
34
|
-
* @param {
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
69
|
+
user_id: userId,
|
|
70
|
+
user_email: userEmail,
|
|
41
71
|
resource,
|
|
42
|
-
recordId,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
79
|
+
// ── Write to DB fire-and-forget ──────────────────────────────────────
|
|
80
|
+
this._writeDb(entry).catch(() => {});
|
|
50
81
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
91
|
+
* Return the most recent N log entries, newest first.
|
|
92
|
+
* Reads from DB if available, memory otherwise.
|
|
66
93
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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.
|
|
78
|
-
if (t[e.action] !== undefined)
|
|
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
|
-
*
|
|
143
|
+
* Return recent entries for a specific resource slug.
|
|
85
144
|
*/
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|