millas 0.2.13 → 0.2.15

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 (88) hide show
  1. package/package.json +6 -3
  2. package/src/admin/Admin.js +107 -1027
  3. package/src/admin/AdminAuth.js +1 -1
  4. package/src/admin/ViewContext.js +1 -1
  5. package/src/admin/handlers/ActionHandler.js +103 -0
  6. package/src/admin/handlers/ApiHandler.js +113 -0
  7. package/src/admin/handlers/AuthHandler.js +76 -0
  8. package/src/admin/handlers/ExportHandler.js +70 -0
  9. package/src/admin/handlers/InlineHandler.js +71 -0
  10. package/src/admin/handlers/PageHandler.js +351 -0
  11. package/src/admin/resources/AdminResource.js +22 -1
  12. package/src/admin/static/SelectFilter2.js +34 -0
  13. package/src/admin/static/actions.js +201 -0
  14. package/src/admin/static/admin.css +7 -0
  15. package/src/admin/static/change_form.js +585 -0
  16. package/src/admin/static/core.js +128 -0
  17. package/src/admin/static/login.js +76 -0
  18. package/src/admin/static/vendor/bi/bootstrap-icons.min.css +5 -0
  19. package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff +0 -0
  20. package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff2 +0 -0
  21. package/src/admin/static/vendor/jquery.min.js +2 -0
  22. package/src/admin/views/layouts/base.njk +30 -113
  23. package/src/admin/views/pages/detail.njk +10 -9
  24. package/src/admin/views/pages/form.njk +4 -4
  25. package/src/admin/views/pages/list.njk +11 -193
  26. package/src/admin/views/pages/login.njk +19 -64
  27. package/src/admin/views/partials/form-field.njk +1 -1
  28. package/src/admin/views/partials/form-scripts.njk +4 -478
  29. package/src/admin/views/partials/form-widget.njk +10 -10
  30. package/src/ai/AITokenBudget.js +1 -1
  31. package/src/auth/Auth.js +112 -3
  32. package/src/auth/AuthMiddleware.js +18 -15
  33. package/src/auth/Hasher.js +15 -43
  34. package/src/cli.js +3 -0
  35. package/src/commands/call.js +190 -0
  36. package/src/commands/createsuperuser.js +3 -4
  37. package/src/commands/key.js +97 -0
  38. package/src/commands/make.js +16 -2
  39. package/src/commands/new.js +16 -1
  40. package/src/commands/serve.js +5 -5
  41. package/src/console/Command.js +337 -0
  42. package/src/console/CommandLoader.js +165 -0
  43. package/src/console/index.js +6 -0
  44. package/src/container/AppInitializer.js +48 -1
  45. package/src/container/Application.js +3 -1
  46. package/src/container/HttpServer.js +0 -1
  47. package/src/container/MillasConfig.js +48 -0
  48. package/src/controller/Controller.js +13 -11
  49. package/src/core/docs.js +6 -0
  50. package/src/core/foundation.js +8 -0
  51. package/src/core/http.js +20 -10
  52. package/src/core/validation.js +58 -27
  53. package/src/docs/Docs.js +268 -0
  54. package/src/docs/DocsServiceProvider.js +80 -0
  55. package/src/docs/SchemaInferrer.js +131 -0
  56. package/src/docs/handlers/ApiHandler.js +305 -0
  57. package/src/docs/handlers/PageHandler.js +47 -0
  58. package/src/docs/index.js +13 -0
  59. package/src/docs/resources/ApiResource.js +402 -0
  60. package/src/docs/static/docs.css +723 -0
  61. package/src/docs/static/docs.js +1181 -0
  62. package/src/encryption/Encrypter.js +381 -0
  63. package/src/facades/Auth.js +5 -2
  64. package/src/facades/Crypt.js +166 -0
  65. package/src/facades/Docs.js +43 -0
  66. package/src/facades/Mail.js +1 -1
  67. package/src/http/MillasRequest.js +7 -31
  68. package/src/http/RequestContext.js +11 -7
  69. package/src/http/SecurityBootstrap.js +24 -2
  70. package/src/http/Shape.js +168 -0
  71. package/src/http/adapters/ExpressAdapter.js +9 -5
  72. package/src/middleware/CorsMiddleware.js +3 -0
  73. package/src/middleware/ThrottleMiddleware.js +10 -7
  74. package/src/orm/model/Model.js +20 -2
  75. package/src/providers/EncryptionServiceProvider.js +66 -0
  76. package/src/router/MiddlewareRegistry.js +79 -54
  77. package/src/router/Route.js +9 -4
  78. package/src/router/RouteEntry.js +91 -0
  79. package/src/router/Router.js +71 -1
  80. package/src/scaffold/maker.js +138 -1
  81. package/src/scaffold/templates.js +12 -0
  82. package/src/serializer/Serializer.js +239 -0
  83. package/src/support/Str.js +1080 -0
  84. package/src/validation/BaseValidator.js +45 -5
  85. package/src/validation/Validator.js +67 -61
  86. package/src/validation/types.js +490 -0
  87. package/src/middleware/AuthMiddleware.js +0 -46
  88. 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 { check, message } of this._rules) {
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
- * Input validation for Millas. Supports both inline usage (req.validate())
7
- * and route-level declaration (rules declared at route definition time,
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
- * ── Rule syntax ───────────────────────────────────────────────────────────────
9
+ * ── Usage (via body.validate in a handler) ────────────────────────────────────
11
10
  *
12
- * Rules are pipe-separated strings or arrays of strings:
11
+ * const { string, email, number, boolean, array, date, object, file } =
12
+ * require('millas/core/validation');
13
13
  *
14
- * 'required|string|min:2|max:100'
15
- * 'required|email'
16
- * 'optional|number|min:0|max:150'
17
- * 'required|boolean'
18
- * 'required|array'
19
- * 'required|in:admin,user,guest'
20
- * 'required|regex:/^[a-z]+$/i'
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
- * // data is the validated + type-coerced subset of input
22
+ * return jsonify(await User.create(data), { status: 201 });
37
23
  * });
38
24
  *
39
- * ── Route-level usage (validation before the handler runs) ────────────────────
25
+ * ── Usage (via .shape() on a route — preferred) ───────────────────────────────
40
26
  *
41
- * Route.post('/register', {
42
- * validate: {
43
- * name: 'required|string|min:2|max:100',
44
- * email: 'required|email',
45
- * password: 'required|string|min:8',
46
- * },
47
- * }, async (req) => {
48
- * // req.validated contains the safe, validated subset
49
- * const { name, email, password } = req.validated;
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. Error shape:
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', 'Must be a valid email address'],
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
- for (const [field, ruleString] of Object.entries(rules)) {
293
- const ruleParts = (Array.isArray(ruleString) ? ruleString : ruleString.split('|'))
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 the Express middleware that runs route-level validation.
397
- * Attaches req.validated with the clean, coerced data on success.
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 = { Validator, ValidationError };
422
+ module.exports = {
423
+ Validator,
424
+ ValidationError,
425
+ // Re-export typed builders from types.js for convenience
426
+ ...require('./types'),
427
+ };