millas 0.2.13 → 0.2.14
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 +6 -3
- package/src/admin/Admin.js +107 -1027
- package/src/admin/AdminAuth.js +1 -1
- package/src/admin/ViewContext.js +1 -1
- package/src/admin/handlers/ActionHandler.js +103 -0
- package/src/admin/handlers/ApiHandler.js +113 -0
- package/src/admin/handlers/AuthHandler.js +76 -0
- package/src/admin/handlers/ExportHandler.js +70 -0
- package/src/admin/handlers/InlineHandler.js +71 -0
- package/src/admin/handlers/PageHandler.js +351 -0
- package/src/admin/resources/AdminResource.js +22 -1
- package/src/admin/static/SelectFilter2.js +34 -0
- package/src/admin/static/actions.js +201 -0
- package/src/admin/static/admin.css +7 -0
- package/src/admin/static/change_form.js +585 -0
- package/src/admin/static/core.js +128 -0
- package/src/admin/static/login.js +76 -0
- package/src/admin/static/vendor/bi/bootstrap-icons.min.css +5 -0
- package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff +0 -0
- package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff2 +0 -0
- package/src/admin/static/vendor/jquery.min.js +2 -0
- package/src/admin/views/layouts/base.njk +30 -113
- package/src/admin/views/pages/detail.njk +10 -9
- package/src/admin/views/pages/form.njk +4 -4
- package/src/admin/views/pages/list.njk +11 -193
- package/src/admin/views/pages/login.njk +19 -64
- package/src/admin/views/partials/form-field.njk +1 -1
- package/src/admin/views/partials/form-scripts.njk +4 -478
- package/src/admin/views/partials/form-widget.njk +10 -10
- package/src/ai/AITokenBudget.js +1 -1
- package/src/auth/Auth.js +112 -3
- package/src/auth/AuthMiddleware.js +18 -15
- package/src/auth/Hasher.js +15 -43
- package/src/cli.js +3 -0
- package/src/commands/call.js +190 -0
- package/src/commands/createsuperuser.js +3 -4
- package/src/commands/key.js +97 -0
- package/src/commands/make.js +16 -2
- package/src/commands/new.js +16 -1
- package/src/commands/serve.js +5 -5
- package/src/console/Command.js +337 -0
- package/src/console/CommandLoader.js +165 -0
- package/src/console/index.js +6 -0
- package/src/container/AppInitializer.js +48 -1
- package/src/container/Application.js +3 -1
- package/src/container/HttpServer.js +0 -1
- package/src/container/MillasConfig.js +48 -0
- package/src/controller/Controller.js +13 -11
- package/src/core/docs.js +6 -0
- package/src/core/foundation.js +8 -0
- package/src/core/http.js +20 -10
- package/src/core/validation.js +58 -27
- package/src/docs/Docs.js +268 -0
- package/src/docs/DocsServiceProvider.js +80 -0
- package/src/docs/SchemaInferrer.js +131 -0
- package/src/docs/handlers/ApiHandler.js +305 -0
- package/src/docs/handlers/PageHandler.js +47 -0
- package/src/docs/index.js +13 -0
- package/src/docs/resources/ApiResource.js +402 -0
- package/src/docs/static/docs.css +723 -0
- package/src/docs/static/docs.js +1181 -0
- package/src/encryption/Encrypter.js +381 -0
- package/src/facades/Auth.js +5 -2
- package/src/facades/Crypt.js +166 -0
- package/src/facades/Docs.js +43 -0
- package/src/facades/Mail.js +1 -1
- package/src/http/MillasRequest.js +7 -31
- package/src/http/RequestContext.js +11 -7
- package/src/http/SecurityBootstrap.js +24 -2
- package/src/http/Shape.js +168 -0
- package/src/http/adapters/ExpressAdapter.js +9 -5
- package/src/middleware/CorsMiddleware.js +3 -0
- package/src/middleware/ThrottleMiddleware.js +10 -7
- package/src/orm/model/Model.js +14 -1
- package/src/providers/EncryptionServiceProvider.js +66 -0
- package/src/router/MiddlewareRegistry.js +79 -54
- package/src/router/Route.js +9 -4
- package/src/router/RouteEntry.js +91 -0
- package/src/router/Router.js +71 -1
- package/src/scaffold/maker.js +138 -1
- package/src/scaffold/templates.js +12 -0
- package/src/serializer/Serializer.js +239 -0
- package/src/support/Str.js +1080 -0
- package/src/validation/BaseValidator.js +45 -5
- package/src/validation/Validator.js +67 -61
- package/src/validation/types.js +490 -0
- package/src/middleware/AuthMiddleware.js +0 -46
- package/src/middleware/MiddlewareRegistry.js +0 -106
|
@@ -22,6 +22,8 @@ class BaseValidator {
|
|
|
22
22
|
this._customFns = []; // [{ fn: async (value, data) => string|null }]
|
|
23
23
|
this._rules = []; // [{ check: fn, message: string }] — added by subclasses
|
|
24
24
|
this._label = null; // field name, set by Validator before running
|
|
25
|
+
this._example = undefined; // docs only — ignored at runtime
|
|
26
|
+
this._describe = null; // docs only — ignored at runtime
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
// ─── Common modifiers ──────────────────────────────────────────────────────
|
|
@@ -88,6 +90,30 @@ class BaseValidator {
|
|
|
88
90
|
return this;
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Set an example value for this field.
|
|
95
|
+
* Used by the docs panel to pre-fill the "Try it" form.
|
|
96
|
+
* Completely ignored at runtime — zero cost.
|
|
97
|
+
*
|
|
98
|
+
* string().required().example('Jane Doe')
|
|
99
|
+
* number().required().example(25000)
|
|
100
|
+
*/
|
|
101
|
+
example(value) {
|
|
102
|
+
this._example = value;
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Set a human-readable description shown in the docs panel.
|
|
108
|
+
* Ignored at runtime.
|
|
109
|
+
*
|
|
110
|
+
* string().nullable().describe('Leave blank to use the default.')
|
|
111
|
+
*/
|
|
112
|
+
description(text) {
|
|
113
|
+
this._describe = text;
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
91
117
|
// ─── Internal ──────────────────────────────────────────────────────────────
|
|
92
118
|
|
|
93
119
|
/**
|
|
@@ -157,8 +183,8 @@ class BaseValidator {
|
|
|
157
183
|
value,
|
|
158
184
|
};
|
|
159
185
|
}
|
|
160
|
-
// Optional and empty — skip all other rules
|
|
161
|
-
return { error: null, value };
|
|
186
|
+
// Optional and empty — skip all other rules, return default or undefined
|
|
187
|
+
return { error: null, value: this._defaultValue !== undefined ? this._defaultValue : value };
|
|
162
188
|
}
|
|
163
189
|
|
|
164
190
|
// ── Type check ──────────────────────────────────────────────────────────
|
|
@@ -167,14 +193,28 @@ class BaseValidator {
|
|
|
167
193
|
return { error: this._typeError || typeErr, value };
|
|
168
194
|
}
|
|
169
195
|
|
|
196
|
+
// ── Coerce before rules (so rules run on coerced value) ─────────────────
|
|
197
|
+
if (this._coerce) {
|
|
198
|
+
value = this._coerce(value);
|
|
199
|
+
}
|
|
200
|
+
|
|
170
201
|
// ── Field-specific rules ─────────────────────────────────────────────────
|
|
171
|
-
for (const
|
|
202
|
+
for (const ruleEntry of this._rules) {
|
|
203
|
+
// .confirmed() — cross-field check
|
|
204
|
+
if (ruleEntry._isConfirmed) {
|
|
205
|
+
const confirmVal = allData[key + '_confirmation'];
|
|
206
|
+
if (value !== confirmVal) {
|
|
207
|
+
return { error: ruleEntry.message || `${label} confirmation does not match`, value };
|
|
208
|
+
}
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const { check, message } = ruleEntry;
|
|
172
212
|
if (!check(value, allData)) {
|
|
173
213
|
return { error: message, value };
|
|
174
214
|
}
|
|
175
215
|
}
|
|
176
216
|
|
|
177
|
-
// ── Custom functions
|
|
217
|
+
// ── Custom async functions ───────────────────────────────────────────────
|
|
178
218
|
for (const fn of this._customFns) {
|
|
179
219
|
const result = await fn(value, allData);
|
|
180
220
|
if (result) return { error: result, value };
|
|
@@ -190,4 +230,4 @@ function _titleCase(str) {
|
|
|
190
230
|
.replace(/\b\w/g, c => c.toUpperCase());
|
|
191
231
|
}
|
|
192
232
|
|
|
193
|
-
module.exports = { BaseValidator, _titleCase };
|
|
233
|
+
module.exports = { BaseValidator, _titleCase };
|
|
@@ -3,60 +3,47 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Validator
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* making validation impossible to forget).
|
|
6
|
+
* Core validation engine for Millas.
|
|
7
|
+
* Works with fluent typed validators from millas/core/validation.
|
|
9
8
|
*
|
|
10
|
-
* ──
|
|
9
|
+
* ── Usage (via body.validate in a handler) ────────────────────────────────────
|
|
11
10
|
*
|
|
12
|
-
*
|
|
11
|
+
* const { string, email, number, boolean, array, date, object, file } =
|
|
12
|
+
* require('millas/core/validation');
|
|
13
13
|
*
|
|
14
|
-
* '
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* 'required|uuid'
|
|
22
|
-
* 'required|url'
|
|
23
|
-
* 'required|date'
|
|
24
|
-
* 'optional|string' — field may be absent; validated if present
|
|
25
|
-
* 'nullable|string' — field may be null or absent
|
|
26
|
-
*
|
|
27
|
-
* ── Inline usage ──────────────────────────────────────────────────────────────
|
|
28
|
-
*
|
|
29
|
-
* Route.post('/register', async (req) => {
|
|
30
|
-
* const data = await req.validate({
|
|
31
|
-
* name: 'required|string|min:2|max:100',
|
|
32
|
-
* email: 'required|email',
|
|
33
|
-
* password: 'required|string|min:8',
|
|
34
|
-
* age: 'optional|number|min:13',
|
|
14
|
+
* Route.post('/register', async ({ body }) => {
|
|
15
|
+
* const data = await body.validate({
|
|
16
|
+
* name: string().required().min(2).max(100),
|
|
17
|
+
* email: email().required(),
|
|
18
|
+
* password: string().required().min(8).confirmed(),
|
|
19
|
+
* age: number().optional().min(13),
|
|
20
|
+
* role: string().oneOf(['admin', 'user']).default('user'),
|
|
35
21
|
* });
|
|
36
|
-
*
|
|
22
|
+
* return jsonify(await User.create(data), { status: 201 });
|
|
37
23
|
* });
|
|
38
24
|
*
|
|
39
|
-
* ──
|
|
25
|
+
* ── Usage (via .shape() on a route — preferred) ───────────────────────────────
|
|
40
26
|
*
|
|
41
|
-
* Route.post('/register',
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
27
|
+
* Route.post('/register', AuthController, 'register')
|
|
28
|
+
* .shape({
|
|
29
|
+
* label: 'Register',
|
|
30
|
+
* group: 'Auth',
|
|
31
|
+
* in: {
|
|
32
|
+
* name: string().required().min(2).max(100).example('Jane Doe'),
|
|
33
|
+
* email: email().required().example('jane@example.com'),
|
|
34
|
+
* password: string().required().min(8).confirmed(),
|
|
35
|
+
* },
|
|
36
|
+
* out: { 201: { token: 'eyJ...' } },
|
|
37
|
+
* });
|
|
51
38
|
*
|
|
52
39
|
* ── Error format ──────────────────────────────────────────────────────────────
|
|
53
40
|
*
|
|
54
|
-
* Throws a 422 ValidationError on failure
|
|
41
|
+
* Throws a 422 ValidationError on failure:
|
|
55
42
|
* {
|
|
56
43
|
* status: 422,
|
|
57
44
|
* message: 'Validation failed',
|
|
58
45
|
* errors: {
|
|
59
|
-
* email: ['Email is required'
|
|
46
|
+
* email: ['Email is required'],
|
|
60
47
|
* password: ['Must be at least 8 characters'],
|
|
61
48
|
* }
|
|
62
49
|
* }
|
|
@@ -285,12 +272,33 @@ class Validator {
|
|
|
285
272
|
* @returns {object} — validated + coerced subset of data
|
|
286
273
|
* @throws {ValidationError}
|
|
287
274
|
*/
|
|
288
|
-
static validate(data, rules) {
|
|
289
|
-
const errors
|
|
290
|
-
const output
|
|
275
|
+
static async validate(data, rules) {
|
|
276
|
+
const errors = {};
|
|
277
|
+
const output = {};
|
|
291
278
|
|
|
292
|
-
|
|
293
|
-
|
|
279
|
+
const { BaseValidator } = require('./BaseValidator');
|
|
280
|
+
|
|
281
|
+
for (const [field, rule] of Object.entries(rules)) {
|
|
282
|
+
const value = data[field];
|
|
283
|
+
|
|
284
|
+
// ── Typed validator instance (string(), email(), number(), etc.) ────────
|
|
285
|
+
// Delegate entirely to BaseValidator.run() which handles:
|
|
286
|
+
// defaults, required, nullable, type check, rules, custom async fns, coercion
|
|
287
|
+
if (rule instanceof BaseValidator) {
|
|
288
|
+
// Set the field label so error messages use the field name
|
|
289
|
+
if (!rule._label) rule._label = _humanise(field);
|
|
290
|
+
|
|
291
|
+
const { error, value: cleaned } = await rule.run(value, field, data);
|
|
292
|
+
if (error) {
|
|
293
|
+
errors[field] = [error];
|
|
294
|
+
} else if (cleaned !== undefined) {
|
|
295
|
+
output[field] = cleaned;
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Pipe-string rule (legacy / shorthand) ──────────────────────────────
|
|
301
|
+
const ruleParts = (Array.isArray(rule) ? rule : rule.split('|'))
|
|
294
302
|
.map(r => r.trim())
|
|
295
303
|
.filter(Boolean);
|
|
296
304
|
|
|
@@ -298,8 +306,6 @@ class Validator {
|
|
|
298
306
|
const isOptional = ruleNames.includes('optional');
|
|
299
307
|
const isNullable = ruleNames.includes('nullable');
|
|
300
308
|
|
|
301
|
-
const value = data[field];
|
|
302
|
-
|
|
303
309
|
// Skip optional fields that are absent
|
|
304
310
|
if (isOptional && (value === undefined || value === null || value === '')) {
|
|
305
311
|
continue;
|
|
@@ -318,7 +324,6 @@ class Validator {
|
|
|
318
324
|
const handler = RULES[name];
|
|
319
325
|
|
|
320
326
|
if (!handler) {
|
|
321
|
-
// Unknown rule — fail loudly in development, skip silently in production
|
|
322
327
|
if (process.env.NODE_ENV !== 'production') {
|
|
323
328
|
throw new Error(`[Millas Validator] Unknown rule: "${name}". Check your validation rules for field "${field}".`);
|
|
324
329
|
}
|
|
@@ -350,9 +355,9 @@ class Validator {
|
|
|
350
355
|
* const { data, errors } = Validator.check(input, rules);
|
|
351
356
|
* if (errors) { ... }
|
|
352
357
|
*/
|
|
353
|
-
static check(data, rules) {
|
|
358
|
+
static async check(data, rules) {
|
|
354
359
|
try {
|
|
355
|
-
const result = Validator.validate(data, rules);
|
|
360
|
+
const result = await Validator.validate(data, rules);
|
|
356
361
|
return { data: result, errors: null };
|
|
357
362
|
} catch (err) {
|
|
358
363
|
if (err instanceof ValidationError) {
|
|
@@ -370,9 +375,6 @@ class Validator {
|
|
|
370
375
|
* return `${field} must be a valid phone number`;
|
|
371
376
|
* }
|
|
372
377
|
* });
|
|
373
|
-
*
|
|
374
|
-
* // Then use it:
|
|
375
|
-
* await req.validate({ phone: 'required|phone' });
|
|
376
378
|
*/
|
|
377
379
|
static extend(name, handler) {
|
|
378
380
|
if (RULES[name]) {
|
|
@@ -393,15 +395,14 @@ class Validator {
|
|
|
393
395
|
}
|
|
394
396
|
|
|
395
397
|
/**
|
|
396
|
-
* Returns
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
* app.post('/register', Validator.middleware({ email: 'required|email' }), handler);
|
|
398
|
+
* Returns an Express middleware function that validates the request.
|
|
399
|
+
* Used internally by Router when a route has .shape({ in: {...} }).
|
|
400
|
+
* Prefer using .shape() on routes rather than calling this directly.
|
|
400
401
|
*
|
|
401
|
-
* @param {object} rules
|
|
402
|
+
* @param {object} rules — { field: BaseValidator | pipe-string }
|
|
402
403
|
*/
|
|
403
404
|
static middleware(rules) {
|
|
404
|
-
return (req, res, next) => {
|
|
405
|
+
return async (req, res, next) => {
|
|
405
406
|
const data = {
|
|
406
407
|
...req.params,
|
|
407
408
|
...req.query,
|
|
@@ -409,7 +410,7 @@ class Validator {
|
|
|
409
410
|
};
|
|
410
411
|
|
|
411
412
|
try {
|
|
412
|
-
req.validated = Validator.validate(data, rules);
|
|
413
|
+
req.validated = await Validator.validate(data, rules);
|
|
413
414
|
next();
|
|
414
415
|
} catch (err) {
|
|
415
416
|
next(err); // passes ValidationError to Express error handler
|
|
@@ -418,4 +419,9 @@ class Validator {
|
|
|
418
419
|
}
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
module.exports = {
|
|
422
|
+
module.exports = {
|
|
423
|
+
Validator,
|
|
424
|
+
ValidationError,
|
|
425
|
+
// Re-export typed builders from types.js for convenience
|
|
426
|
+
...require('./types'),
|
|
427
|
+
};
|