bazis 0.96.5 → 0.96.6

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.
@@ -170,6 +170,16 @@ export function validateModuleEncapsulation(
170
170
  }
171
171
  }
172
172
 
173
+ // Why a provided token is not visible: the owner does not export it, or the
174
+ // consumer module does not import the owner. Both used to read "not exported".
175
+ const explainHidden = (token: Token<unknown>, consumer: ModuleGraphRecord): string => {
176
+ const owner = records.find((record) => record !== consumer && record.providedTokens.has(token));
177
+ if (owner === undefined) return "which is provided by another module but not exported.";
178
+ return exportedTokens(owner).has(token)
179
+ ? `which module "${owner.name}" exports, but "${consumer.name}" does not list "${owner.name}" in its imports. Add "${owner.name}" to the imports of "${consumer.name}".`
180
+ : `which module "${owner.name}" provides but does not export. Add it to the exports of "${owner.name}".`;
181
+ };
182
+
173
183
  const contexts = new Map<ModuleGraphRecord, DependencyCheckContext>();
174
184
  for (let recordIndex = 0; recordIndex < records.length; recordIndex += 1) {
175
185
  const record = records[recordIndex] as ModuleGraphRecord;
@@ -183,7 +193,7 @@ export function validateModuleEncapsulation(
183
193
  }
184
194
 
185
195
  const context: DependencyCheckContext = {
186
- visible, familiesVisible, tokensByName, allProvidedTokens, allProvidedFamilies, issues,
196
+ visible, familiesVisible, tokensByName, allProvidedTokens, allProvidedFamilies, issues, explainHidden,
187
197
  };
188
198
  contexts.set(record, context);
189
199
  for (const definition of record.definitions) {
@@ -214,6 +224,7 @@ interface DependencyCheckContext {
214
224
  readonly allProvidedTokens: ReadonlySet<Token<unknown>>;
215
225
  readonly allProvidedFamilies: ReadonlySet<symbol>;
216
226
  readonly issues: string[];
227
+ readonly explainHidden: (token: Token<unknown>, consumer: ModuleGraphRecord) => string;
217
228
  }
218
229
 
219
230
  function checkDefinition(
@@ -265,8 +276,9 @@ function checkDependency(
265
276
  return;
266
277
  }
267
278
  }
279
+ const provided = candidates.find((candidate) => context.allProvidedTokens.has(candidate as Token<unknown>)) as Token<unknown> | undefined;
268
280
  context.issues.push(
269
- `Module "${record.name}": "${consumer}" depends on "${dep.name}", which is provided by another module but not exported.`,
281
+ `Module "${record.name}": "${consumer}" depends on "${dep.name}", ${provided ? context.explainHidden(provided, record) : "which is provided by another module but not exported."}`,
270
282
  );
271
283
  return;
272
284
  }
@@ -293,7 +305,7 @@ function checkDependency(
293
305
  return;
294
306
  }
295
307
  context.issues.push(
296
- `Module "${record.name}": "${consumer}" depends on "${tokenToDebugName(token)}", which is provided by another module but not exported.`,
308
+ `Module "${record.name}": "${consumer}" depends on "${tokenToDebugName(token)}", ${context.explainHidden(token, record)}`,
297
309
  );
298
310
  }
299
311
 
@@ -3,7 +3,7 @@ import { BadRequestError, UnsupportedMediaTypeError } from "../Errors/HttpError"
3
3
  import type { HttpContext } from "../HttpContext/HttpContext";
4
4
  import type { ParameterBinding } from "./bindings";
5
5
  import { convertOr400 } from "./convert";
6
- import { bindModel } from "./modelBinder";
6
+ import { bindModelAsync } from "./modelBinder";
7
7
 
8
8
  /** Per-action constraints affecting binding (from decorators). */
9
9
  export interface BindingDefaults {
@@ -67,7 +67,7 @@ export async function bindArguments(
67
67
  throw new UnsupportedMediaTypeError("application/json");
68
68
  }
69
69
  const data = await ctx.json();
70
- args[index] = binding.model ? bindModel(binding.model, data, ctx.modelValidator, { primitiveTypes: true }) : data;
70
+ args[index] = binding.model ? await bindModelAsync(binding.model, data, ctx.modelValidator, { primitiveTypes: true }) : data;
71
71
  break;
72
72
  }
73
73
  case "request":
@@ -1,6 +1,6 @@
1
1
  import type { Class } from "../../di";
2
2
  import { BadRequestError, ModelValidationError } from "../Errors/HttpError";
3
- import { getModelValidator, type ModelValidator } from "./modelValidator";
3
+ import { getModelValidator, type ModelValidationIssue, type ModelValidationResult, type ModelValidator } from "./modelValidator";
4
4
  import {
5
5
  findRequestModelShape,
6
6
  type RequestModelClass,
@@ -17,6 +17,9 @@ const MAX_JSON_NODES = 100_000;
17
17
  interface BindingTraversal {
18
18
  readonly stack: WeakSet<object>;
19
19
  readonly options: ModelBindingOptions;
20
+ /** Per-field binding problems; a bad field does not stop the other fields. */
21
+ readonly issues: ModelValidationIssue[];
22
+ readonly typeMessage?: (property: string, expected: string, actual: string) => string;
20
23
  nodes: number;
21
24
  }
22
25
 
@@ -49,19 +52,48 @@ export interface ModelBindingOptions {
49
52
  * runtime fallback.
50
53
  */
51
54
  export function bindModel<T extends object>(model: Class<T>, data: unknown, validator?: ModelValidator, options: ModelBindingOptions = {}): T {
55
+ const activeValidator = validator ?? getModelValidator();
56
+ const { instance, traversal } = hydrate(model, data, activeValidator, options);
57
+ finish(traversal, activeValidator, activeValidator?.validate(instance));
58
+ return instance;
59
+ }
60
+
61
+ /**
62
+ * `bindModel` for callers that can wait: runs async `custom` rules through
63
+ * `ModelValidator.validateAsync` when the validator provides it.
64
+ */
65
+ export async function bindModelAsync<T extends object>(model: Class<T>, data: unknown, validator?: ModelValidator, options: ModelBindingOptions = {}): Promise<T> {
66
+ const activeValidator = validator ?? getModelValidator();
67
+ const { instance, traversal } = hydrate(model, data, activeValidator, options);
68
+ const result = activeValidator?.validateAsync !== undefined
69
+ ? await activeValidator.validateAsync(instance)
70
+ : activeValidator?.validate(instance);
71
+ finish(traversal, activeValidator, result);
72
+ return instance;
73
+ }
74
+
75
+ function hydrate<T extends object>(model: Class<T>, data: unknown, validator: ModelValidator | undefined, options: ModelBindingOptions): { instance: T; traversal: BindingTraversal } {
52
76
  if (!isPlainJsonObject(data)) {
53
77
  throw new BadRequestError("Request body must be a JSON object");
54
78
  }
55
- const traversal: BindingTraversal = { stack: new WeakSet(), nodes: 0, options };
79
+ const traversal: BindingTraversal = {
80
+ stack: new WeakSet(), nodes: 0, options, issues: [],
81
+ typeMessage: validator?.typeMismatchMessage?.bind(validator),
82
+ };
56
83
  const instance = hydrateModel(model as RequestModelClass, data, "", traversal, 0) as T;
57
- const activeValidator = validator ?? getModelValidator();
58
- if (activeValidator !== undefined) {
59
- const result = activeValidator.validate(instance);
60
- if (!result.isValid) {
61
- throw new ModelValidationError(result.errors);
62
- }
84
+ return { instance, traversal };
85
+ }
86
+
87
+ /**
88
+ * One 400 with every problem: binding issues first, then `@Validator` errors of
89
+ * the other fields (a field with a wrong JSON type gets only its type error).
90
+ */
91
+ function finish(traversal: BindingTraversal, validator: ModelValidator | undefined, result: ModelValidationResult | undefined): void {
92
+ const bound = new Set(traversal.issues.map((issue) => issue.property));
93
+ const errors = [...traversal.issues, ...(result?.errors ?? []).filter((error) => !bound.has(error.property))];
94
+ if (errors.length > 0) {
95
+ throw new ModelValidationError(errors, validator?.failureTitle?.() ?? "Validation failed");
63
96
  }
64
- return instance;
65
97
  }
66
98
 
67
99
  function hydrateModel(
@@ -84,7 +116,12 @@ function hydrateModel(
84
116
  }
85
117
  const target = instance as Record<string, unknown>;
86
118
  const fieldPath = path === "" ? key : `${path}.${key}`;
87
- target[key] = bindField(model, key, target[key], source[key], fieldPath, traversal, depth + 1);
119
+ try {
120
+ target[key] = bindField(model, key, target[key], source[key], fieldPath, traversal, depth + 1);
121
+ } catch (error) {
122
+ if (!(error instanceof BindingTypeError) && !(error instanceof ModelValidationError && error.errors.every((issue) => issue.code === "unknownField"))) throw error;
123
+ traversal.issues.push(...(error instanceof BindingTypeError ? [error.issue(traversal.typeMessage)] : error.errors));
124
+ }
88
125
  }
89
126
  return instance;
90
127
  } finally {
@@ -117,13 +154,13 @@ function checkPrimitive(shape: RequestModelPrimitiveFieldShape, incoming: unknow
117
154
  return;
118
155
  }
119
156
  if (shape.array !== true) {
120
- if (typeof incoming !== shape.primitive) throw bindingTypeError(path, shape.primitive);
157
+ if (typeof incoming !== shape.primitive) throw bindingTypeError(path, shape.primitive, incoming);
121
158
  return;
122
159
  }
123
- if (!Array.isArray(incoming)) throw bindingTypeError(path, "array");
160
+ if (!Array.isArray(incoming)) throw bindingTypeError(path, "array", incoming);
124
161
  incoming.forEach((item, index) => {
125
162
  if (!(item === null && shape.elementNullable === true) && typeof item !== shape.primitive) {
126
- throw bindingTypeError(`${path}[${index}]`, shape.primitive);
163
+ throw bindingTypeError(`${path}[${index}]`, shape.primitive, item);
127
164
  }
128
165
  });
129
166
  }
@@ -142,11 +179,11 @@ function hydrateField(
142
179
  if (shape.nullable === true) {
143
180
  return null;
144
181
  }
145
- throw bindingTypeError(path, shape.array === true ? "array" : "object");
182
+ throw bindingTypeError(path, shape.array === true ? "array" : "object", incoming);
146
183
  }
147
184
  if (shape.array === true) {
148
185
  if (!Array.isArray(incoming)) {
149
- throw bindingTypeError(path, "array");
186
+ throw bindingTypeError(path, "array", incoming);
150
187
  }
151
188
  enterContainer(incoming, path, traversal, depth);
152
189
  try {
@@ -156,7 +193,7 @@ function hydrateField(
156
193
  return null;
157
194
  }
158
195
  if (!isPlainJsonObject(item)) {
159
- throw bindingTypeError(itemPath, "object");
196
+ throw bindingTypeError(itemPath, "object", item);
160
197
  }
161
198
  return hydrateModel(shape.model, item, itemPath, traversal, depth + 1);
162
199
  });
@@ -165,17 +202,25 @@ function hydrateField(
165
202
  }
166
203
  }
167
204
  if (!isPlainJsonObject(incoming)) {
168
- throw bindingTypeError(path, "object");
205
+ throw bindingTypeError(path, "object", incoming);
169
206
  }
170
207
  return hydrateModel(shape.model, incoming, path, traversal, depth);
171
208
  }
172
209
 
173
- function bindingTypeError(property: string, expected: string): ModelValidationError {
174
- return new ModelValidationError([{
175
- property: property || "$",
176
- message: `Field "${property || "$"}" must be a ${expected}`,
177
- code: "type",
178
- }]);
210
+ /** A JSON value of the wrong type; the field is skipped and the binding goes on. */
211
+ class BindingTypeError extends ModelValidationError {
212
+ constructor(readonly property: string, readonly expected: string, readonly actual: string) {
213
+ super([{ property, message: `Field "${property}" must be a ${expected}`, code: "type" }]);
214
+ }
215
+
216
+ issue(format?: (property: string, expected: string, actual: string) => string): ModelValidationIssue {
217
+ return { property: this.property, message: format?.(this.property, this.expected, this.actual) ?? this.errors[0]!.message, code: "type" };
218
+ }
219
+ }
220
+
221
+ function bindingTypeError(property: string, expected: string, value: unknown): BindingTypeError {
222
+ const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
223
+ return new BindingTypeError(property || "$", expected, actual);
179
224
  }
180
225
 
181
226
  function isPlainJsonObject(value: unknown): value is Record<string, unknown> {
@@ -215,7 +260,7 @@ function sanitizeBoundValue(
215
260
 
216
261
  if (Array.isArray(incoming)) {
217
262
  if (isNestedModel(template) || isPlainJsonObject(template)) {
218
- throw bindingTypeError(path, "object");
263
+ throw bindingTypeError(path, "object", incoming);
219
264
  }
220
265
  enterContainer(incoming, path, traversal, depth);
221
266
  try {
@@ -228,10 +273,10 @@ function sanitizeBoundValue(
228
273
  }
229
274
 
230
275
  if (!isPlainJsonObject(incoming)) {
231
- throw bindingTypeError(path, "JSON object or array");
276
+ throw bindingTypeError(path, "JSON object or array", incoming);
232
277
  }
233
278
  if (Array.isArray(template)) {
234
- throw bindingTypeError(path, "array");
279
+ throw bindingTypeError(path, "array", incoming);
235
280
  }
236
281
 
237
282
  enterContainer(incoming, path, traversal, depth);
@@ -13,8 +13,19 @@ export interface ModelValidationIssue {
13
13
  readonly code?: string;
14
14
  }
15
15
 
16
+ export interface ModelValidationResult {
17
+ readonly isValid: boolean;
18
+ readonly errors: readonly ModelValidationIssue[];
19
+ }
20
+
16
21
  export interface ModelValidator {
17
- validate(instance: object): { readonly isValid: boolean; readonly errors: readonly ModelValidationIssue[] };
22
+ validate(instance: object): ModelValidationResult;
23
+ /** Async rules (custom checks returning a Promise). HTTP binding prefers it when present. */
24
+ validateAsync?(instance: object): Promise<ModelValidationResult>;
25
+ /** Text for a JSON value of the wrong type; without it the binder uses its English default. */
26
+ typeMismatchMessage?(property: string, expected: string, actual: string): string;
27
+ /** Title of a 400 validation response; without it "Validation failed". */
28
+ failureTitle?(): string;
18
29
  }
19
30
 
20
31
  let activeValidator: ModelValidator | undefined;
@@ -72,9 +72,9 @@ export class TooManyRequestsError extends HttpError {
72
72
  * of validation problems.
73
73
  */
74
74
  export class ModelValidationError extends BadRequestError {
75
- constructor(readonly errors: readonly ModelValidationIssue[]) {
75
+ constructor(readonly errors: readonly ModelValidationIssue[], title = "Validation failed") {
76
76
  super(
77
- "Validation failed",
77
+ title,
78
78
  errors.map((error) => ({ property: error.property, message: error.message, code: error.code })),
79
79
  );
80
80
  }
@@ -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.96.5" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.96.6" }, 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");
@@ -6,11 +6,16 @@ export type MessageParams = Readonly<Record<string, unknown>>;
6
6
 
7
7
  /** Built-in default messages (English). */
8
8
  const BUILT_IN: Readonly<Record<string, string>> = {
9
+ validationFailed: "Validation failed",
10
+ invalid: 'Field "{property}" is invalid ({code})',
9
11
  required: 'Field "{property}" is required',
10
12
  notEmpty: 'Field "{property}" must not be empty',
11
13
  minLength: 'Field "{property}" must be at least {min} characters long',
12
14
  maxLength: 'Field "{property}" must be at most {max} characters long',
13
15
  length: 'Field "{property}" must be {min} to {max} characters long',
16
+ minItems: 'Field "{property}" must contain at least {min} items',
17
+ maxItems: 'Field "{property}" must contain at most {max} items',
18
+ itemCount: 'Field "{property}" must contain {min} to {max} items',
14
19
  contains: 'Field "{property}" must contain "{contains}"',
15
20
  notContains: 'Field "{property}" must not contain "{contains}"',
16
21
  pattern: 'Field "{property}" does not match the format {pattern}',
@@ -41,11 +46,16 @@ const BUILT_IN: Readonly<Record<string, string>> = {
41
46
  * `MessageRegistry.setDefaults(RU_VALIDATION_MESSAGES)`.
42
47
  */
43
48
  export const RU_VALIDATION_MESSAGES: Readonly<Record<string, string>> = Object.freeze({
49
+ validationFailed: "Проверка данных не пройдена",
50
+ invalid: 'Поле "{property}" не прошло проверку ({code})',
44
51
  required: 'Поле "{property}" обязательно для заполнения',
45
52
  notEmpty: 'Поле "{property}" не должно быть пустым',
46
53
  minLength: 'Поле "{property}" должно содержать не менее {min} символов',
47
54
  maxLength: 'Поле "{property}" должно содержать не более {max} символов',
48
55
  length: 'Поле "{property}" должно содержать от {min} до {max} символов',
56
+ minItems: 'Поле "{property}" должно содержать не менее {min} элементов',
57
+ maxItems: 'Поле "{property}" должно содержать не более {max} элементов',
58
+ itemCount: 'Поле "{property}" должно содержать от {min} до {max} элементов',
49
59
  contains: 'Поле "{property}" должно содержать "{contains}"',
50
60
  notContains: 'Поле "{property}" не должно содержать "{contains}"',
51
61
  pattern: 'Поле "{property}" не соответствует формату {pattern}',
@@ -118,7 +128,7 @@ export class MessageRegistry {
118
128
  localMessage ??
119
129
  (Object.prototype.hasOwnProperty.call(MessageRegistry.defaults, code)
120
130
  ? MessageRegistry.defaults[code]!
121
- : 'Поле "{property}" не прошло проверку ({code})');
131
+ : MessageRegistry.defaults.invalid ?? BUILT_IN.invalid!);
122
132
  return MessageRegistry.interpolate(template, params);
123
133
  }
124
134
 
@@ -115,11 +115,14 @@ export class RuleEngine {
115
115
 
116
116
  // 3. String rules: first one type check, then the rules themselves.
117
117
  if (rule.needsString) {
118
- if (typeof value !== "string") {
118
+ if (Array.isArray(value) && RuleEngine.onlyLengthRules(o)) {
119
+ RuleEngine.applyArrayLengthRules(o, value, path, errors);
120
+ } else if (typeof value !== "string") {
119
121
  RuleEngine.failType(errors, path, value, o, "string");
120
122
  return;
123
+ } else {
124
+ RuleEngine.applyStringRules(rule, value, path, errors);
121
125
  }
122
- RuleEngine.applyStringRules(rule, value, path, errors);
123
126
  }
124
127
 
125
128
  // 4. Number rules.
@@ -167,6 +170,27 @@ export class RuleEngine {
167
170
  }
168
171
  }
169
172
 
173
+ /** notEmpty/minLength/maxLength/length also apply to arrays; any other string rule needs a string. */
174
+ private static onlyLengthRules(o: ValidatorOptions): boolean {
175
+ return o.type === undefined && o.contains === undefined && o.notContains === undefined && o.pattern === undefined
176
+ && o.email !== true && o.url !== true && o.uuid !== true && o.json !== true && o.phone !== true;
177
+ }
178
+
179
+ private static applyArrayLengthRules(o: ValidatorOptions, value: readonly unknown[], path: string, errors: ValidationError[]): void {
180
+ if (o.notEmpty === true && value.length === 0) {
181
+ RuleEngine.fail(errors, ValidationCodes.notEmpty, path, value, o, undefined);
182
+ }
183
+ if (o.minLength !== undefined && value.length < o.minLength) {
184
+ RuleEngine.fail(errors, ValidationCodes.minItems, path, value, o, { min: o.minLength });
185
+ }
186
+ if (o.maxLength !== undefined && value.length > o.maxLength) {
187
+ RuleEngine.fail(errors, ValidationCodes.maxItems, path, value, o, { max: o.maxLength });
188
+ }
189
+ if (o.length !== undefined && (value.length < o.length[0] || value.length > o.length[1])) {
190
+ RuleEngine.fail(errors, ValidationCodes.itemCount, path, value, o, { min: o.length[0], max: o.length[1] });
191
+ }
192
+ }
193
+
170
194
  private static applyStringRules(rule: CompiledRule, value: string, path: string, errors: ValidationError[]): void {
171
195
  const o = rule.options;
172
196
  if (o.notEmpty === true && value.length === 0) {
@@ -361,7 +385,7 @@ export class RuleEngine {
361
385
  }
362
386
 
363
387
  private static failType(errors: ValidationError[], path: string, value: unknown, o: ValidatorOptions, expected: string): void {
364
- const actual = typeof value === "number" && Number.isNaN(value) ? "NaN" : value === null ? "null" : typeof value;
388
+ const actual = typeof value === "number" && Number.isNaN(value) ? "NaN" : value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
365
389
  RuleEngine.fail(errors, ValidationCodes.type, path, value, o, { expected, actual });
366
390
  }
367
391
 
@@ -114,6 +114,11 @@ Any of these rules requires the value to be a string (otherwise one `type` error
114
114
  `maxLength` and the upper bound of `length` limit the input of the later content checks
115
115
  **of the same decorator**: `contains`, `notContains`, `pattern`, `email`, `url`, `uuid`,
116
116
  `json`, `phone`. When exceeded, the length errors stay and the listed checks do not run.
117
+
118
+ On an **array** value, `notEmpty`, `minLength`, `maxLength` and `length` count its
119
+ items (codes `notEmpty`, `minItems`, `maxItems`, `itemCount`), as long as the decorator
120
+ has no other string rule and no `type` hint. Any other string rule on an array is a
121
+ `type` error (`got: array`).
117
122
  The lower length bound, the other fields/decorators and `custom` keep their behavior.
118
123
  For an untrusted `pattern`, set the upper bound in the same decorator; a separate
119
124
  limiting decorator does not change the neighboring rules.
@@ -480,7 +485,8 @@ from your own `ValidationError`.
480
485
  | Code | When |
481
486
  |---|---|
482
487
  | `required` | the value is `undefined`/`null` with `required: true` |
483
- | `notEmpty`, `minLength`, `maxLength`, `length` | the string length |
488
+ | `notEmpty`, `minLength`, `maxLength`, `length` | the string length (`notEmpty` also an empty array) |
489
+ | `minItems`, `maxItems`, `itemCount` | the array item count (`minLength`, `maxLength`, `length` on an array) |
484
490
  | `contains`, `notContains`, `pattern` | the string content |
485
491
  | `email`, `url`, `uuid`, `json`, `phone`, `date` | the format |
486
492
  | `type` | the value has the wrong type (including `NaN` for number rules) |
@@ -1,5 +1,7 @@
1
1
  import { Validator } from "./Validator";
2
2
  import { rulesOf } from "./metadata";
3
+ import { MessageRegistry } from "./MessageRegistry";
4
+ import type { ValidationResult } from "./ValidationResult";
3
5
 
4
6
  interface AdapterValidationIssue {
5
7
  readonly property: string;
@@ -27,11 +29,31 @@ interface AdapterValidationIssue {
27
29
  * kernel unaware of the concrete validation engine (dependency inversion).
28
30
  */
29
31
  export const modelValidatorAdapter = {
30
- validate(instance: object): {
31
- readonly isValid: boolean;
32
- readonly errors: readonly { readonly property: string; readonly message: string; readonly code?: string }[];
33
- } {
34
- const result = Validator.validate(instance);
32
+ validate(instance: object): AdapterValidationResult {
33
+ return toAdapterResult(instance, Validator.validate(instance));
34
+ },
35
+
36
+ /** Runs async `custom` rules too; HTTP request binding uses this. */
37
+ async validateAsync(instance: object): Promise<AdapterValidationResult> {
38
+ return toAdapterResult(instance, await Validator.validateAsync(instance));
39
+ },
40
+
41
+ /** A JSON value of the wrong type, in the current `MessageRegistry` language. */
42
+ typeMismatchMessage(property: string, expected: string, actual: string): string {
43
+ return MessageRegistry.format("type", undefined, { property, expected, actual });
44
+ },
45
+
46
+ failureTitle(): string {
47
+ return MessageRegistry.format("validationFailed", undefined, {});
48
+ },
49
+ };
50
+
51
+ interface AdapterValidationResult {
52
+ readonly isValid: boolean;
53
+ readonly errors: readonly AdapterValidationIssue[];
54
+ }
55
+
56
+ function toAdapterResult(instance: object, result: ValidationResult): AdapterValidationResult {
35
57
  const nestedInstanceErrors = validateExplicitNestedInstances(instance);
36
58
  const errors: AdapterValidationIssue[] = result.errors.map((error) => ({
37
59
  property: error.property,
@@ -43,8 +65,7 @@ export const modelValidatorAdapter = {
43
65
  isValid: errors.length === 0,
44
66
  errors,
45
67
  };
46
- },
47
- };
68
+ }
48
69
 
49
70
  /**
50
71
  * `nested: true` must never silently validate a plain object with no class
@@ -8,7 +8,7 @@
8
8
  export const ValidationCodes = {
9
9
  /** The value is missing (`undefined` or `null`) but the field is required. */
10
10
  required: "required",
11
- /** The string is empty but the rule requires a non-empty one. */
11
+ /** The string or array is empty but the rule requires a non-empty one. */
12
12
  notEmpty: "notEmpty",
13
13
  /** The string is shorter than the minimum. */
14
14
  minLength: "minLength",
@@ -16,6 +16,12 @@ export const ValidationCodes = {
16
16
  maxLength: "maxLength",
17
17
  /** The string length is outside `length: [min, max]`. */
18
18
  length: "length",
19
+ /** The array has fewer items than `minLength`. */
20
+ minItems: "minItems",
21
+ /** The array has more items than `maxLength`. */
22
+ maxItems: "maxItems",
23
+ /** The array item count is outside `length: [min, max]`. */
24
+ itemCount: "itemCount",
19
25
  /** The string does not contain the required substring. */
20
26
  contains: "contains",
21
27
  /** The string contains a forbidden substring. */
@@ -88,13 +88,13 @@ export interface ValidatorOptions {
88
88
 
89
89
  // ── Strings ──────────────────────────────────────────────────────────────
90
90
 
91
- /** The string must not be empty (`""`). */
91
+ /** The string (`""`) or array (`[]`) must not be empty. */
92
92
  notEmpty?: boolean;
93
- /** Minimum string length (inclusive). */
93
+ /** Minimum string length or array item count (inclusive). */
94
94
  minLength?: number;
95
- /** Maximum string length (inclusive); exceeding it skips this decorator's content checks. */
95
+ /** Maximum string length or array item count (inclusive); for a string, exceeding it skips this decorator's content checks. */
96
96
  maxLength?: number;
97
- /** String length range `[min, max]` (inclusive); exceeding max skips this decorator's content checks. */
97
+ /** String length or array item count range `[min, max]` (inclusive); for a string, exceeding max skips this decorator's content checks. */
98
98
  length?: readonly [number, number];
99
99
  /** The string must contain the substring. */
100
100
  contains?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.96.5",
3
+ "version": "0.96.6",
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",