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
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WidgetRegistry
|
|
5
|
+
*
|
|
6
|
+
* Maps field types to widget descriptors that control how a field is
|
|
7
|
+
* rendered in the admin form and how its raw submitted value is coerced.
|
|
8
|
+
*
|
|
9
|
+
* ── What a widget is ─────────────────────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* A widget is a plain object with three responsibilities:
|
|
12
|
+
*
|
|
13
|
+
* type {string} — the HTML input type sent to the template
|
|
14
|
+
* (text | email | number | checkbox | select | textarea
|
|
15
|
+
* | password | date | datetime-local | color | url |
|
|
16
|
+
* phone | json | richtext | image | hidden)
|
|
17
|
+
*
|
|
18
|
+
* coerce {Function} — (rawValue, fieldDef) => typedValue
|
|
19
|
+
* converts the raw string from req.body into the
|
|
20
|
+
* correct JS type before the ORM sees it.
|
|
21
|
+
* This is the single source of truth for type coercion
|
|
22
|
+
* (used by _sanitise() in AdminResource).
|
|
23
|
+
*
|
|
24
|
+
* validate {Function} — (value, fieldDef) => string | null
|
|
25
|
+
* returns an error message string, or null if valid.
|
|
26
|
+
* Called by FormValidator before the ORM write.
|
|
27
|
+
*
|
|
28
|
+
* ── Built-in type → widget mapping ───────────────────────────────────────────
|
|
29
|
+
*
|
|
30
|
+
* ORM type Widget type Notes
|
|
31
|
+
* ────────── ─────────── ─────────────────────────────────────────────
|
|
32
|
+
* id hidden never shown in forms
|
|
33
|
+
* string text max length validated from fieldDef.max
|
|
34
|
+
* text textarea 4-row textarea
|
|
35
|
+
* integer number parseInt, step=1
|
|
36
|
+
* bigInteger number parseInt, step=1
|
|
37
|
+
* float number parseFloat
|
|
38
|
+
* decimal number parseFloat, step derived from fieldDef.scale
|
|
39
|
+
* boolean checkbox 'on'/missing → true/false
|
|
40
|
+
* enum select options from fieldDef.enumValues
|
|
41
|
+
* timestamp datetime-local datetime-local input
|
|
42
|
+
* date date date input
|
|
43
|
+
* json json textarea with JSON validation
|
|
44
|
+
* uuid text plain text
|
|
45
|
+
*
|
|
46
|
+
* ── Custom widgets ────────────────────────────────────────────────────────────
|
|
47
|
+
*
|
|
48
|
+
* Register a custom widget globally:
|
|
49
|
+
*
|
|
50
|
+
* const { WidgetRegistry } = require('millas/src/admin');
|
|
51
|
+
*
|
|
52
|
+
* WidgetRegistry.register('color', {
|
|
53
|
+
* type: 'color',
|
|
54
|
+
* coerce: (raw) => raw || null,
|
|
55
|
+
* validate: (val, def) => null,
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* Override per-field in AdminConfig:
|
|
59
|
+
*
|
|
60
|
+
* class PostAdmin extends AdminResource {
|
|
61
|
+
* static widgets = {
|
|
62
|
+
* body: { type: 'richtext' },
|
|
63
|
+
* cover: { type: 'image' },
|
|
64
|
+
* status: { type: 'select' },
|
|
65
|
+
* };
|
|
66
|
+
* }
|
|
67
|
+
*
|
|
68
|
+
* ── Using the registry ────────────────────────────────────────────────────────
|
|
69
|
+
*
|
|
70
|
+
* const widget = WidgetRegistry.resolve('integer', fieldDef);
|
|
71
|
+
* const typed = widget.coerce('42', fieldDef); // → 42
|
|
72
|
+
* const err = widget.validate(typed, fieldDef); // → null
|
|
73
|
+
*/
|
|
74
|
+
class WidgetRegistry {
|
|
75
|
+
constructor() {
|
|
76
|
+
/** @type {Map<string, object>} ORM type → widget descriptor */
|
|
77
|
+
this._widgets = new Map();
|
|
78
|
+
this._registerDefaults();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Registration ──────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Register or override a widget for an ORM field type.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} ormType — the field.type value (e.g. 'string', 'integer')
|
|
87
|
+
* @param {object} widget — { type, coerce, validate }
|
|
88
|
+
*/
|
|
89
|
+
register(ormType, widget) {
|
|
90
|
+
if (!widget.type) throw new Error(`[WidgetRegistry] widget for "${ormType}" must have a type`);
|
|
91
|
+
if (!widget.coerce) widget.coerce = (v) => v;
|
|
92
|
+
if (!widget.validate) widget.validate = () => null;
|
|
93
|
+
this._widgets.set(ormType, widget);
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the widget for a given ORM type.
|
|
99
|
+
* Falls back to a plain text widget if the type is unknown.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} ormType
|
|
102
|
+
* @param {object} [fieldDef] — field definition (used by some built-ins)
|
|
103
|
+
* @returns {object} widget descriptor
|
|
104
|
+
*/
|
|
105
|
+
resolve(ormType, fieldDef = {}) {
|
|
106
|
+
return this._widgets.get(ormType) || this._widgets.get('string');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if a custom widget is registered for an ORM type.
|
|
111
|
+
*/
|
|
112
|
+
has(ormType) {
|
|
113
|
+
return this._widgets.has(ormType);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Return all registered ORM types.
|
|
118
|
+
*/
|
|
119
|
+
types() {
|
|
120
|
+
return [...this._widgets.keys()];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Default widgets ───────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
_registerDefaults() {
|
|
126
|
+
|
|
127
|
+
// ── id ──────────────────────────────────────────────────────────────────
|
|
128
|
+
this.register('id', {
|
|
129
|
+
type: 'hidden',
|
|
130
|
+
coerce: () => undefined, // id is never written
|
|
131
|
+
validate: () => null,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── string ──────────────────────────────────────────────────────────────
|
|
135
|
+
this.register('string', {
|
|
136
|
+
type: 'text',
|
|
137
|
+
coerce(raw, def) {
|
|
138
|
+
if (raw === undefined || raw === null) return def?.nullable ? null : '';
|
|
139
|
+
const s = String(raw);
|
|
140
|
+
return s === '' && def?.nullable ? null : s;
|
|
141
|
+
},
|
|
142
|
+
validate(val, def) {
|
|
143
|
+
if ((val === null || val === '' || val === undefined) && !def?.nullable) {
|
|
144
|
+
return 'This field is required.';
|
|
145
|
+
}
|
|
146
|
+
if (def?.max && val && String(val).length > def.max) {
|
|
147
|
+
return `Maximum ${def.max} characters allowed.`;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── text ─────────────────────────────────────────────────────────────────
|
|
154
|
+
this.register('text', {
|
|
155
|
+
type: 'textarea',
|
|
156
|
+
coerce(raw, def) {
|
|
157
|
+
if (raw === undefined || raw === null) return def?.nullable ? null : '';
|
|
158
|
+
const s = String(raw);
|
|
159
|
+
return s === '' && def?.nullable ? null : s;
|
|
160
|
+
},
|
|
161
|
+
validate(val, def) {
|
|
162
|
+
if ((val === null || val === '' || val === undefined) && !def?.nullable) {
|
|
163
|
+
return 'This field is required.';
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── integer ──────────────────────────────────────────────────────────────
|
|
170
|
+
this.register('integer', {
|
|
171
|
+
type: 'number',
|
|
172
|
+
coerce(raw, def) {
|
|
173
|
+
if (raw === '' || raw === null || raw === undefined) {
|
|
174
|
+
return def?.nullable ? null : 0;
|
|
175
|
+
}
|
|
176
|
+
const n = parseInt(raw, 10);
|
|
177
|
+
return isNaN(n) ? (def?.nullable ? null : 0) : n;
|
|
178
|
+
},
|
|
179
|
+
validate(val, def) {
|
|
180
|
+
if ((val === null || val === undefined) && !def?.nullable) {
|
|
181
|
+
return 'This field is required.';
|
|
182
|
+
}
|
|
183
|
+
if (val !== null && val !== undefined && !Number.isInteger(Number(val))) {
|
|
184
|
+
return 'Must be a whole number.';
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── bigInteger ───────────────────────────────────────────────────────────
|
|
191
|
+
this.register('bigInteger', {
|
|
192
|
+
type: 'number',
|
|
193
|
+
coerce(raw, def) {
|
|
194
|
+
if (raw === '' || raw === null || raw === undefined) return def?.nullable ? null : 0;
|
|
195
|
+
const n = parseInt(raw, 10);
|
|
196
|
+
return isNaN(n) ? (def?.nullable ? null : 0) : n;
|
|
197
|
+
},
|
|
198
|
+
validate: this._widgets.get?.('integer')?.validate || (() => null),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ── float ────────────────────────────────────────────────────────────────
|
|
202
|
+
this.register('float', {
|
|
203
|
+
type: 'number',
|
|
204
|
+
coerce(raw, def) {
|
|
205
|
+
if (raw === '' || raw === null || raw === undefined) return def?.nullable ? null : 0;
|
|
206
|
+
const n = parseFloat(raw);
|
|
207
|
+
return isNaN(n) ? (def?.nullable ? null : 0) : n;
|
|
208
|
+
},
|
|
209
|
+
validate(val, def) {
|
|
210
|
+
if ((val === null || val === undefined) && !def?.nullable) return 'This field is required.';
|
|
211
|
+
if (val !== null && val !== undefined && isNaN(Number(val))) return 'Must be a number.';
|
|
212
|
+
return null;
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ── decimal ──────────────────────────────────────────────────────────────
|
|
217
|
+
this.register('decimal', {
|
|
218
|
+
type: 'number',
|
|
219
|
+
coerce(raw, def) {
|
|
220
|
+
if (raw === '' || raw === null || raw === undefined) return def?.nullable ? null : 0;
|
|
221
|
+
const n = parseFloat(raw);
|
|
222
|
+
return isNaN(n) ? (def?.nullable ? null : 0) : n;
|
|
223
|
+
},
|
|
224
|
+
validate(val, def) {
|
|
225
|
+
if ((val === null || val === undefined) && !def?.nullable) return 'This field is required.';
|
|
226
|
+
if (val !== null && val !== undefined && isNaN(Number(val))) return 'Must be a number.';
|
|
227
|
+
return null;
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── boolean ──────────────────────────────────────────────────────────────
|
|
232
|
+
this.register('boolean', {
|
|
233
|
+
type: 'checkbox',
|
|
234
|
+
coerce(raw) {
|
|
235
|
+
if (raw === undefined || raw === null || raw === '' || raw === false || raw === '0' || raw === 'false') return false;
|
|
236
|
+
if (raw === true || raw === 'on' || raw === '1' || raw === 'true') return true;
|
|
237
|
+
return Boolean(raw);
|
|
238
|
+
},
|
|
239
|
+
validate: () => null, // booleans are always valid (true or false)
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ── enum ─────────────────────────────────────────────────────────────────
|
|
243
|
+
this.register('enum', {
|
|
244
|
+
type: 'select',
|
|
245
|
+
coerce(raw, def) {
|
|
246
|
+
if (raw === '' || raw === null || raw === undefined) return def?.nullable ? null : (def?.enumValues?.[0] ?? null);
|
|
247
|
+
return raw;
|
|
248
|
+
},
|
|
249
|
+
validate(val, def) {
|
|
250
|
+
if ((val === null || val === undefined) && !def?.nullable) return 'Please select a value.';
|
|
251
|
+
if (val && def?.enumValues && !def.enumValues.includes(val)) {
|
|
252
|
+
return `Invalid value. Must be one of: ${def.enumValues.join(', ')}`;
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── timestamp ────────────────────────────────────────────────────────────
|
|
259
|
+
this.register('timestamp', {
|
|
260
|
+
type: 'datetime-local',
|
|
261
|
+
coerce(raw, def) {
|
|
262
|
+
if (raw === '' || raw === null || raw === undefined) return def?.nullable ? null : undefined;
|
|
263
|
+
// datetime-local gives 'YYYY-MM-DDTHH:mm' — convert to ISO string
|
|
264
|
+
try {
|
|
265
|
+
const d = new Date(raw);
|
|
266
|
+
return isNaN(d.getTime()) ? (def?.nullable ? null : undefined) : d.toISOString();
|
|
267
|
+
} catch {
|
|
268
|
+
return def?.nullable ? null : undefined;
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
validate(val, def) {
|
|
272
|
+
if ((val === null || val === undefined) && !def?.nullable) return 'This field is required.';
|
|
273
|
+
return null;
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ── date ─────────────────────────────────────────────────────────────────
|
|
278
|
+
this.register('date', {
|
|
279
|
+
type: 'date',
|
|
280
|
+
coerce(raw, def) {
|
|
281
|
+
if (raw === '' || raw === null || raw === undefined) return def?.nullable ? null : undefined;
|
|
282
|
+
return raw; // 'YYYY-MM-DD' — pass through as-is, DB handles it
|
|
283
|
+
},
|
|
284
|
+
validate(val, def) {
|
|
285
|
+
if ((val === null || val === undefined) && !def?.nullable) return 'This field is required.';
|
|
286
|
+
if (val && !/^\d{4}-\d{2}-\d{2}$/.test(val)) return 'Must be a valid date (YYYY-MM-DD).';
|
|
287
|
+
return null;
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ── json ─────────────────────────────────────────────────────────────────
|
|
292
|
+
this.register('json', {
|
|
293
|
+
type: 'json',
|
|
294
|
+
coerce(raw, def) {
|
|
295
|
+
if (raw === '' || raw === null || raw === undefined) return def?.nullable ? null : {};
|
|
296
|
+
if (typeof raw === 'object') return raw;
|
|
297
|
+
try { return JSON.parse(raw); } catch { return raw; } // leave as-is, validate will catch
|
|
298
|
+
},
|
|
299
|
+
validate(val, def) {
|
|
300
|
+
if ((val === null || val === undefined) && !def?.nullable) return 'This field is required.';
|
|
301
|
+
if (typeof val === 'string') {
|
|
302
|
+
try { JSON.parse(val); } catch { return 'Must be valid JSON.'; }
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ── uuid ─────────────────────────────────────────────────────────────────
|
|
309
|
+
this.register('uuid', {
|
|
310
|
+
type: 'text',
|
|
311
|
+
coerce(raw, def) {
|
|
312
|
+
if (raw === '' || raw === null || raw === undefined) return def?.nullable ? null : '';
|
|
313
|
+
return String(raw).trim();
|
|
314
|
+
},
|
|
315
|
+
validate(val, def) {
|
|
316
|
+
if ((val === null || val === '' || val === undefined) && !def?.nullable) return 'This field is required.';
|
|
317
|
+
if (val && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val)) {
|
|
318
|
+
return 'Must be a valid UUID.';
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ── email (AdminField type, not ORM type — registered for completeness) ──
|
|
325
|
+
this.register('email', {
|
|
326
|
+
type: 'email',
|
|
327
|
+
coerce(raw, def) {
|
|
328
|
+
if (raw === '' || raw === null || raw === undefined) return def?.nullable ? null : '';
|
|
329
|
+
return String(raw).trim().toLowerCase();
|
|
330
|
+
},
|
|
331
|
+
validate(val, def) {
|
|
332
|
+
if ((val === null || val === '' || val === undefined) && !def?.nullable) return 'This field is required.';
|
|
333
|
+
if (val && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) return 'Must be a valid email address.';
|
|
334
|
+
return null;
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ── url ───────────────────────────────────────────────────────────────────
|
|
339
|
+
this.register('url', {
|
|
340
|
+
type: 'url',
|
|
341
|
+
coerce(raw, def) {
|
|
342
|
+
if (raw === '' || raw === null || raw === undefined) return def?.nullable ? null : '';
|
|
343
|
+
return String(raw).trim();
|
|
344
|
+
},
|
|
345
|
+
validate(val, def) {
|
|
346
|
+
if ((val === null || val === '' || val === undefined) && !def?.nullable) return 'This field is required.';
|
|
347
|
+
if (val) {
|
|
348
|
+
try { new URL(val); } catch { return 'Must be a valid URL.'; }
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ── password ──────────────────────────────────────────────────────────────
|
|
355
|
+
this.register('password', {
|
|
356
|
+
type: 'password',
|
|
357
|
+
coerce(raw) {
|
|
358
|
+
// Empty password on edit = keep existing (return undefined so ORM skips it)
|
|
359
|
+
if (raw === '' || raw === null || raw === undefined) return undefined;
|
|
360
|
+
return String(raw);
|
|
361
|
+
},
|
|
362
|
+
validate(val, def) {
|
|
363
|
+
// On edit, empty is allowed (means "don't change password")
|
|
364
|
+
if (val === undefined || val === null || val === '') return null;
|
|
365
|
+
if (val.length < 8) return 'Password must be at least 8 characters.';
|
|
366
|
+
return null;
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
// ── fk (ForeignKey) ──────────────────────────────────────────────────────
|
|
370
|
+
// Rendered as a searchable select backed by the /admin/api/:resource/options
|
|
371
|
+
// autocomplete endpoint. Falls back to a plain number input if no related
|
|
372
|
+
// resource is registered.
|
|
373
|
+
this.register('fk', {
|
|
374
|
+
type: 'fk',
|
|
375
|
+
coerce(raw, def) {
|
|
376
|
+
if (raw === '' || raw === null || raw === undefined) return def?.nullable ? null : undefined;
|
|
377
|
+
const n = parseInt(raw, 10);
|
|
378
|
+
return isNaN(n) ? (def?.nullable ? null : undefined) : n;
|
|
379
|
+
},
|
|
380
|
+
validate(val, def) {
|
|
381
|
+
if ((val === null || val === undefined) && !def?.nullable) {
|
|
382
|
+
return 'Please select a related record.';
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// ── m2m (ManyToMany) ──────────────────────────────────────────────────
|
|
389
|
+
// Rendered as a dual-list widget (available → chosen).
|
|
390
|
+
// Values submitted as multiple values under the same field name.
|
|
391
|
+
this.register('m2m', {
|
|
392
|
+
type: 'm2m',
|
|
393
|
+
coerce(raw) {
|
|
394
|
+
if (!raw) return [];
|
|
395
|
+
if (Array.isArray(raw)) return raw.map(v => parseInt(v, 10)).filter(n => !isNaN(n));
|
|
396
|
+
return [parseInt(raw, 10)].filter(n => !isNaN(n));
|
|
397
|
+
},
|
|
398
|
+
validate: () => null,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── Singleton global registry ─────────────────────────────────────────────────
|
|
404
|
+
const widgetRegistry = new WidgetRegistry();
|
|
405
|
+
|
|
406
|
+
module.exports = { WidgetRegistry, widgetRegistry };
|
package/src/admin/index.js
CHANGED
|
@@ -5,6 +5,11 @@ const AdminAuth = require('./AdminAuth');
|
|
|
5
5
|
const ActivityLog = require('./ActivityLog');
|
|
6
6
|
const AdminServiceProvider = require('../providers/AdminServiceProvider');
|
|
7
7
|
const { AdminResource, AdminField, AdminFilter, AdminInline } = require('./resources/AdminResource');
|
|
8
|
+
const { AdminHooks, HookRegistry, HookPipeline, VALID_EVENTS } = require('./HookRegistry');
|
|
9
|
+
const { FormGenerator } = require('./FormGenerator');
|
|
10
|
+
const { WidgetRegistry, widgetRegistry } = require('./WidgetRegistry');
|
|
11
|
+
const { QueryEngine } = require('./QueryEngine');
|
|
12
|
+
const { ViewContext } = require('./ViewContext');
|
|
8
13
|
|
|
9
14
|
module.exports = {
|
|
10
15
|
Admin,
|
|
@@ -15,4 +20,16 @@ module.exports = {
|
|
|
15
20
|
AdminFilter,
|
|
16
21
|
AdminInline,
|
|
17
22
|
AdminServiceProvider,
|
|
23
|
+
// Hook system
|
|
24
|
+
AdminHooks,
|
|
25
|
+
HookRegistry,
|
|
26
|
+
HookPipeline,
|
|
27
|
+
VALID_EVENTS,
|
|
28
|
+
// Form system
|
|
29
|
+
FormGenerator,
|
|
30
|
+
WidgetRegistry,
|
|
31
|
+
widgetRegistry,
|
|
32
|
+
// Query + View
|
|
33
|
+
QueryEngine,
|
|
34
|
+
ViewContext,
|
|
18
35
|
};
|