bazis 0.120.0 → 0.121.0

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.
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
232
232
  if (method === "account/updated") this.modelCache = undefined;
233
233
  }, () => { this.clearLogin(); this.modelCache = undefined; });
234
234
  const initialized = await client.request("initialize", {
235
- clientInfo: { name: "bazis", title: "Bazis", version: "0.120.0" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.121.0" }, capabilities: { experimentalApi: true },
236
236
  }, 15_000, signal);
237
237
  if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
238
238
  client.notify("initialized");
@@ -42,11 +42,12 @@ More scenarios (complex conditions, async, nested objects) are in
42
42
 
43
43
  | File | Purpose |
44
44
  |---|---|
45
- | `Validator.ts` | public facade: the decorator + `validate`/`validateAsync`/`setDefaultMessages` |
45
+ | `Validator.ts` | public facade: the decorator + `validate`/`validateAsync`/`ruleSet`/`setDefaultMessages` |
46
46
  | `ValidationError.ts` | one error: `property`, `value`, `message`, `code` |
47
47
  | `ValidationResult.ts` | the outcome: `isValid`, `errors`, `getErrorsFor`, `hasErrorsFor` |
48
48
  | `RuleEngine.ts` | the engine: one pass over the plan, sync/async, nested, cycle protection |
49
49
  | `RuleCompiler.ts` | compiles class rules into a plan + WeakMap cache (RegExp, enum Set) |
50
+ | `RuleSet.ts` | rules as data without a class: `Validator.ruleSet({ field: options })` |
50
51
  | `MessageRegistry.ts` | message templates, placeholders, priority local > global > built-in; built-in texts are English, `RU_VALIDATION_MESSAGES` is a Russian set for `setDefaults` and `runApp({ language: "ru" })` |
51
52
  | `metadata.ts` | stores rules through `context.metadata` / `Symbol.metadata` |
52
53
  | `types/ValidatorOptions.ts` | all decorator options |
@@ -1,4 +1,4 @@
1
- import { rulesOf } from "./metadata";
1
+ import { rulesOf, type FieldRule } from "./metadata";
2
2
  import type { ValidatorOptions } from "./types/ValidatorOptions";
3
3
 
4
4
  /**
@@ -69,9 +69,18 @@ export class RuleCompiler {
69
69
  if (!rules) {
70
70
  return undefined;
71
71
  }
72
+ const plan = RuleCompiler.compileRules(rules);
73
+ RuleCompiler.cache.set(ctor, plan);
74
+ return plan;
75
+ }
76
+
77
+ /**
78
+ * Compiles a rule list that is not tied to a class (see `RuleSet`). Not
79
+ * cached: the caller keeps the result for as long as the rules are valid.
80
+ */
81
+ static compileRules(rules: readonly FieldRule[]): CompiledRule[] {
72
82
  const plan = rules.map((rule) => RuleCompiler.compile(rule.property, rule.options));
73
83
  RuleCompiler.assignNestedCarriers(plan);
74
- RuleCompiler.cache.set(ctor, plan);
75
84
  return plan;
76
85
  }
77
86
 
@@ -49,6 +49,24 @@ export class RuleEngine {
49
49
  return new ValidationResult(errors);
50
50
  }
51
51
 
52
+ /** Synchronous validation of any object by a precompiled plan (see `RuleSet`). */
53
+ static validatePlan(plan: readonly CompiledRule[], value: object): ValidationResult {
54
+ const errors: ValidationError[] = [];
55
+ RuleEngine.run(plan, value, "", errors, undefined, undefined);
56
+ return new ValidationResult(errors);
57
+ }
58
+
59
+ /** Asynchronous validation of any object by a precompiled plan (see `RuleSet`). */
60
+ static async validatePlanAsync(plan: readonly CompiledRule[], value: object): Promise<ValidationResult> {
61
+ const errors: ValidationError[] = [];
62
+ const jobs: AsyncJob[] = [];
63
+ RuleEngine.run(plan, value, "", errors, jobs, undefined);
64
+ for (const job of jobs) {
65
+ await job();
66
+ }
67
+ return new ValidationResult(errors);
68
+ }
69
+
52
70
  /**
53
71
  * Asynchronous validation: all synchronous rules run immediately, async
54
72
  * `custom` functions run sequentially after them (deterministic error order).
@@ -75,6 +93,17 @@ export class RuleEngine {
75
93
  if (!plan) {
76
94
  return;
77
95
  }
96
+ RuleEngine.run(plan, instance, prefix, errors, jobs, seen);
97
+ }
98
+
99
+ private static run(
100
+ plan: readonly CompiledRule[],
101
+ instance: object,
102
+ prefix: string,
103
+ errors: ValidationError[],
104
+ jobs: AsyncJob[] | undefined,
105
+ seen: Set<object> | undefined,
106
+ ): void {
78
107
  for (const rule of plan) {
79
108
  RuleEngine.applyRule(rule, instance, prefix, errors, jobs, seen);
80
109
  }
@@ -0,0 +1,59 @@
1
+ import { RuleCompiler, type CompiledRule } from "./RuleCompiler";
2
+ import { RuleEngine } from "./RuleEngine";
3
+ import type { ValidationResult } from "./ValidationResult";
4
+ import type { FieldRule } from "./metadata";
5
+ import type { ValidatorOptions } from "./types/ValidatorOptions";
6
+
7
+ /** Field rules of a {@link RuleSet}: one options object or several, as several decorators on one field. */
8
+ export type FieldRules = Readonly<Record<string, ValidatorOptions | readonly ValidatorOptions[]>>;
9
+
10
+ /**
11
+ * Validation rules that are data, not a decorated class: for objects whose
12
+ * shape is known only at run time (rows of tables created by users, rules
13
+ * stored in a database). The same engine, options, codes and messages as
14
+ * `@Validator`; the rules are compiled once in the constructor.
15
+ *
16
+ * ```ts
17
+ * const rules = Validator.ruleSet({
18
+ * title: { required: true, maxLength: 200 },
19
+ * amount: [{ type: "number", min: 0 }, { integer: true }],
20
+ * });
21
+ * rules.validate({ title: "", amount: -1.5 }).errors;
22
+ * ```
23
+ *
24
+ * Only the named fields are read. A field name that exists on
25
+ * `Object.prototype` (`__proto__`, `constructor`, `toString`…) is rejected,
26
+ * so a plain object never yields an inherited value.
27
+ */
28
+ export class RuleSet {
29
+ private readonly plan: readonly CompiledRule[];
30
+
31
+ constructor(rules: FieldRules) {
32
+ const list: FieldRule[] = [];
33
+ for (const property of Object.keys(rules)) {
34
+ if (property in Object.prototype) {
35
+ throw new TypeError(`RuleSet: field "${property}" is not allowed: it is a property of Object.prototype.`);
36
+ }
37
+ const options = rules[property]!;
38
+ for (const item of Array.isArray(options) ? options as readonly ValidatorOptions[] : [options as ValidatorOptions]) {
39
+ list.push({ property, options: item });
40
+ }
41
+ }
42
+ this.plan = RuleCompiler.compileRules(list);
43
+ }
44
+
45
+ /** Field names that have rules. */
46
+ get fields(): readonly string[] {
47
+ return [...new Set(this.plan.map((rule) => rule.property))];
48
+ }
49
+
50
+ /** Synchronous validation; an async `custom` adds an `asyncCustomInSyncCall` error. */
51
+ validate(value: object): ValidationResult {
52
+ return RuleEngine.validatePlan(this.plan, value);
53
+ }
54
+
55
+ /** Asynchronous validation: awaits async `custom` functions. */
56
+ validateAsync(value: object): Promise<ValidationResult> {
57
+ return RuleEngine.validatePlanAsync(this.plan, value);
58
+ }
59
+ }
@@ -412,6 +412,34 @@ Validator.validate(new ChildDto()); // checks both id and extra
412
412
  Validator.validate(new BaseDto()); // only id
413
413
  ```
414
414
 
415
+ ### 4.13 Rules as data: `Validator.ruleSet`
416
+
417
+ When the fields are known only at run time (a table created by a user, rules
418
+ stored in a database), there is no class to decorate. `Validator.ruleSet`
419
+ takes the same options per field name and compiles them once:
420
+
421
+ ```ts
422
+ const rules = Validator.ruleSet({
423
+ title: { required: true, maxLength: 200 },
424
+ amount: [{ type: "number", min: 0 }, { integer: true }], // like two decorators
425
+ });
426
+
427
+ rules.validate({ title: "", amount: -1.5 }).errors;
428
+ // [amount: min, amount: integer] — "" is a value, so required passes
429
+ await rules.validateAsync(row); // when a rule has an async custom
430
+ rules.fields; // ["title", "amount"]
431
+ ```
432
+
433
+ - Codes, paths, `params` and messages are the same as with `@Validator`.
434
+ - The plan is compiled in the constructor (RegExp, enum Set) and not cached
435
+ globally: keep the `RuleSet` while the rules are valid and build a new one
436
+ when they change.
437
+ - Only the named fields are read. A field name that exists on
438
+ `Object.prototype` (`__proto__`, `constructor`, `toString`…) throws
439
+ `TypeError`, so a plain object never yields an inherited value.
440
+ - `nested: true` validates a nested value only if it is an instance of a
441
+ decorated class, as with decorators.
442
+
415
443
  ---
416
444
 
417
445
  ## 5. Result: `ValidationResult` and `ValidationError`
@@ -1,5 +1,6 @@
1
1
  import { MessageRegistry } from "./MessageRegistry";
2
2
  import { RuleEngine } from "./RuleEngine";
3
+ import { RuleSet, type FieldRules } from "./RuleSet";
3
4
  import type { ValidationResult } from "./ValidationResult";
4
5
  import { registerFieldRule } from "./metadata";
5
6
  import type { ValidatorOptions } from "./types/ValidatorOptions";
@@ -74,6 +75,18 @@ export const Validator = Object.assign(
74
75
  validateAsync(instance: object): Promise<ValidationResult> {
75
76
  return RuleEngine.validateAsync(instance);
76
77
  },
78
+ /**
79
+ * Rules as data, without a class: for objects whose fields are known only
80
+ * at run time. Compiled once; keep the result and reuse it.
81
+ *
82
+ * ```ts
83
+ * const rules = Validator.ruleSet({ title: { required: true, maxLength: 200 } });
84
+ * rules.validate(row).isValid;
85
+ * ```
86
+ */
87
+ ruleSet(rules: FieldRules): RuleSet {
88
+ return new RuleSet(rules);
89
+ },
77
90
 
78
91
  /**
79
92
  * Globally overrides the standard error messages.
@@ -8,6 +8,7 @@
8
8
  * methods `validate` / `validateAsync` / `setDefaultMessages`.
9
9
  */
10
10
  export { Validator } from "./Validator";
11
+ export { RuleSet, type FieldRules } from "./RuleSet";
11
12
  export { modelValidatorAdapter } from "./modelValidatorAdapter";
12
13
  export { ValidationError } from "./ValidationError";
13
14
  export { ValidationResult } from "./ValidationResult";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.120.0",
3
+ "version": "0.121.0",
4
4
  "description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Alexander Alenin",