@zerotal/validator 1.6.3 → 1.7.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/api-surface.md ADDED
@@ -0,0 +1,369 @@
1
+ # @zerotal/validator — public API surface
2
+
3
+ <!-- AUTO-GENERATED by scripts/api-surface.ts. Do not edit by hand.
4
+ Run `bun run api:surface` to regenerate after an intentional API change. -->
5
+
6
+ ## . `(./src/index.ts)`
7
+
8
+ class ArrayRule = {
9
+ new <T extends FieldRule>(item: T): ArrayRule<T>
10
+ accepted: (message?: string) => ArrayRule<T>
11
+ bail: () => ArrayRule<T>
12
+ custom: (fn: CustomFn, message?: string) => ArrayRule<T>
13
+ declined: (message?: string) => ArrayRule<T>
14
+ default: (value: unknown) => ArrayRule<T>
15
+ max: (length: number, message?: string) => ArrayRule<T>
16
+ min: (length: number, message?: string) => ArrayRule<T>
17
+ nullable: () => ArrayRule<T>
18
+ optional: () => ArrayRule<T>
19
+ prohibitedIf: (field: string, value: unknown) => ArrayRule<T>
20
+ prohibitedUnless: (field: string, value: unknown) => ArrayRule<T>
21
+ readonly _def: ArrayDef<T['_def']>
22
+ required: (message?: string) => ArrayRule<T>
23
+ requiredIf: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => ArrayRule<T>
24
+ requiredUnless: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => ArrayRule<T>
25
+ requiredWith: (fields: string[]) => ArrayRule<T>
26
+ requiredWithAll: (fields: string[]) => ArrayRule<T>
27
+ requiredWithout: (fields: string[]) => ArrayRule<T>
28
+ requiredWithoutAll: (fields: string[]) => ArrayRule<T>
29
+ size: (n: number, message?: string) => ArrayRule<T>
30
+ sometimes: () => ArrayRule<T>
31
+ }
32
+
33
+ class BooleanRule = {
34
+ new (): BooleanRule
35
+ accepted: (message?: string) => BooleanRule
36
+ bail: () => BooleanRule
37
+ custom: (fn: CustomFn, message?: string) => BooleanRule
38
+ declined: (message?: string) => BooleanRule
39
+ default: (value: unknown) => BooleanRule
40
+ nullable: () => BooleanRule
41
+ optional: () => BooleanRule
42
+ prohibitedIf: (field: string, value: unknown) => BooleanRule
43
+ prohibitedUnless: (field: string, value: unknown) => BooleanRule
44
+ readonly _def: BooleanDef
45
+ required: (message?: string) => BooleanRule
46
+ requiredIf: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => BooleanRule
47
+ requiredUnless: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => BooleanRule
48
+ requiredWith: (fields: string[]) => BooleanRule
49
+ requiredWithAll: (fields: string[]) => BooleanRule
50
+ requiredWithout: (fields: string[]) => BooleanRule
51
+ requiredWithoutAll: (fields: string[]) => BooleanRule
52
+ sometimes: () => BooleanRule
53
+ }
54
+
55
+ class DateRule = {
56
+ new (): DateRule
57
+ accepted: (message?: string) => DateRule
58
+ after: (date: Date | string, message?: string) => DateRule
59
+ afterOrEqual: (date: Date | string, message?: string) => DateRule
60
+ bail: () => DateRule
61
+ before: (date: Date | string, message?: string) => DateRule
62
+ beforeOrEqual: (date: Date | string, message?: string) => DateRule
63
+ custom: (fn: CustomFn, message?: string) => DateRule
64
+ declined: (message?: string) => DateRule
65
+ default: (value: unknown) => DateRule
66
+ nullable: () => DateRule
67
+ optional: () => DateRule
68
+ prohibitedIf: (field: string, value: unknown) => DateRule
69
+ prohibitedUnless: (field: string, value: unknown) => DateRule
70
+ readonly _def: DateDef
71
+ required: (message?: string) => DateRule
72
+ requiredIf: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => DateRule
73
+ requiredUnless: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => DateRule
74
+ requiredWith: (fields: string[]) => DateRule
75
+ requiredWithAll: (fields: string[]) => DateRule
76
+ requiredWithout: (fields: string[]) => DateRule
77
+ requiredWithoutAll: (fields: string[]) => DateRule
78
+ sometimes: () => DateRule
79
+ }
80
+
81
+ class FieldRule = {
82
+ new (): FieldRule
83
+ accepted: (message?: string) => FieldRule
84
+ bail: () => FieldRule
85
+ custom: (fn: CustomFn, message?: string) => FieldRule
86
+ declined: (message?: string) => FieldRule
87
+ default: (value: unknown) => FieldRule
88
+ nullable: () => FieldRule
89
+ optional: () => FieldRule
90
+ prohibitedIf: (field: string, value: unknown) => FieldRule
91
+ prohibitedUnless: (field: string, value: unknown) => FieldRule
92
+ readonly _def: FieldRuleDefinition
93
+ required: (message?: string) => FieldRule
94
+ requiredIf: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => FieldRule
95
+ requiredUnless: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => FieldRule
96
+ requiredWith: (fields: string[]) => FieldRule
97
+ requiredWithAll: (fields: string[]) => FieldRule
98
+ requiredWithout: (fields: string[]) => FieldRule
99
+ requiredWithoutAll: (fields: string[]) => FieldRule
100
+ sometimes: () => FieldRule
101
+ }
102
+
103
+ class FileRule = {
104
+ new (): FileRule
105
+ accepted: (message?: string) => FileRule
106
+ bail: () => FileRule
107
+ custom: (fn: CustomFn, message?: string) => FileRule
108
+ declined: (message?: string) => FileRule
109
+ default: (value: unknown) => FileRule
110
+ max: (kb: number, message?: string) => FileRule
111
+ mimes: (extensions: string[], message?: string) => FileRule
112
+ min: (kb: number, message?: string) => FileRule
113
+ nullable: () => FileRule
114
+ optional: () => FileRule
115
+ prohibitedIf: (field: string, value: unknown) => FileRule
116
+ prohibitedUnless: (field: string, value: unknown) => FileRule
117
+ readonly _def: FileDef
118
+ required: (message?: string) => FileRule
119
+ requiredIf: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => FileRule
120
+ requiredUnless: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => FileRule
121
+ requiredWith: (fields: string[]) => FileRule
122
+ requiredWithAll: (fields: string[]) => FileRule
123
+ requiredWithout: (fields: string[]) => FileRule
124
+ requiredWithoutAll: (fields: string[]) => FileRule
125
+ sometimes: () => FileRule
126
+ }
127
+
128
+ class FormRequest = {
129
+ new (): FormRequest
130
+ static macro: (name: string, fn: (this: FormRequest, ...args: unknown[]) => unknown) => void
131
+ static validate: <T extends FormRequest>(this: new () => T) => Promise<Infer<ExtractDefs<ReturnType<T['rules']>>>>
132
+ authorize: () => boolean | Promise<boolean>
133
+ rules: (_r: RuleBuilder) => Record<string, FieldRule>
134
+ }
135
+
136
+ class NumberRule = {
137
+ new (): NumberRule
138
+ accepted: (message?: string) => NumberRule
139
+ bail: () => NumberRule
140
+ between: (min: number, max: number, message?: string) => NumberRule
141
+ custom: (fn: CustomFn, message?: string) => NumberRule
142
+ declined: (message?: string) => NumberRule
143
+ default: (value: unknown) => NumberRule
144
+ exists: (table: string, column: string, message?: string) => NumberRule
145
+ integer: (message?: string) => NumberRule
146
+ max: (value: number, message?: string) => NumberRule
147
+ min: (value: number, message?: string) => NumberRule
148
+ notIn: (values: number[], message?: string) => NumberRule
149
+ nullable: () => NumberRule
150
+ optional: () => NumberRule
151
+ positive: (message?: string) => NumberRule
152
+ prohibitedIf: (field: string, value: unknown) => NumberRule
153
+ prohibitedUnless: (field: string, value: unknown) => NumberRule
154
+ readonly _def: NumberDef
155
+ required: (message?: string) => NumberRule
156
+ requiredIf: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => NumberRule
157
+ requiredUnless: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => NumberRule
158
+ requiredWith: (fields: string[]) => NumberRule
159
+ requiredWithAll: (fields: string[]) => NumberRule
160
+ requiredWithout: (fields: string[]) => NumberRule
161
+ requiredWithoutAll: (fields: string[]) => NumberRule
162
+ sometimes: () => NumberRule
163
+ }
164
+
165
+ class ObjectRule = {
166
+ new <S extends Record<string, FieldRule>>(shape: S): ObjectRule<S>
167
+ accepted: (message?: string) => ObjectRule<S>
168
+ bail: () => ObjectRule<S>
169
+ custom: (fn: CustomFn, message?: string) => ObjectRule<S>
170
+ declined: (message?: string) => ObjectRule<S>
171
+ default: (value: unknown) => ObjectRule<S>
172
+ nullable: () => ObjectRule<S>
173
+ optional: () => ObjectRule<S>
174
+ prohibitedIf: (field: string, value: unknown) => ObjectRule<S>
175
+ prohibitedUnless: (field: string, value: unknown) => ObjectRule<S>
176
+ readonly _def: ObjectDef<{ [K in keyof S]: S[K]['_def']; }>
177
+ required: (message?: string) => ObjectRule<S>
178
+ requiredIf: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => ObjectRule<S>
179
+ requiredUnless: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => ObjectRule<S>
180
+ requiredWith: (fields: string[]) => ObjectRule<S>
181
+ requiredWithAll: (fields: string[]) => ObjectRule<S>
182
+ requiredWithout: (fields: string[]) => ObjectRule<S>
183
+ requiredWithoutAll: (fields: string[]) => ObjectRule<S>
184
+ sometimes: () => ObjectRule<S>
185
+ }
186
+
187
+ class PasswordRule = {
188
+ new (): PasswordRule
189
+ accepted: (message?: string) => PasswordRule
190
+ bail: () => PasswordRule
191
+ confirmed: (message?: string) => PasswordRule
192
+ custom: (fn: CustomFn, message?: string) => PasswordRule
193
+ declined: (message?: string) => PasswordRule
194
+ default: (value: unknown) => PasswordRule
195
+ min: (length: number, message?: string) => PasswordRule
196
+ mixedCase: (message?: string) => PasswordRule
197
+ nullable: () => PasswordRule
198
+ numbers: (message?: string) => PasswordRule
199
+ optional: () => PasswordRule
200
+ prohibitedIf: (field: string, value: unknown) => PasswordRule
201
+ prohibitedUnless: (field: string, value: unknown) => PasswordRule
202
+ readonly _def: FieldRuleDefinition & { type: 'string';}
203
+ required: (message?: string) => PasswordRule
204
+ requiredIf: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => PasswordRule
205
+ requiredUnless: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => PasswordRule
206
+ requiredWith: (fields: string[]) => PasswordRule
207
+ requiredWithAll: (fields: string[]) => PasswordRule
208
+ requiredWithout: (fields: string[]) => PasswordRule
209
+ requiredWithoutAll: (fields: string[]) => PasswordRule
210
+ sometimes: () => PasswordRule
211
+ symbols: (message?: string) => PasswordRule
212
+ uncompromised: (message?: string) => PasswordRule
213
+ }
214
+
215
+ class PrecognitionResponseError = {
216
+ new (precognitionStatus: 204 | 422, errors?: ValidationErrors): PrecognitionResponseError
217
+ readonly code: string
218
+ readonly context?: Record<string, unknown> | undefined
219
+ readonly errors: ValidationErrors
220
+ readonly name: 'PrecognitionResponse'
221
+ readonly precognitionStatus: 204 | 422
222
+ readonly status: number
223
+ toResponse: () => Response
224
+ }
225
+
226
+ class RuleBuilder = {
227
+ new (): RuleBuilder
228
+ array: <T extends FieldRule>(item: T) => ArrayRule<T>
229
+ boolean: () => BooleanRule
230
+ date: () => DateRule
231
+ email: (message?: string) => StringRule
232
+ file: () => FileRule
233
+ number: () => NumberRule
234
+ object: <S extends Record<string, FieldRule>>(shape: S) => ObjectRule<S>
235
+ password: () => PasswordRule
236
+ required: (message?: string) => StringRule
237
+ string: () => StringRule
238
+ url: (message?: string) => StringRule
239
+ uuid: (message?: string) => StringRule
240
+ }
241
+
242
+ class StringRule = {
243
+ new (): StringRule
244
+ accepted: (message?: string) => StringRule
245
+ alpha: (message?: string) => StringRule
246
+ alphaDash: (message?: string) => StringRule
247
+ alphaNum: (message?: string) => StringRule
248
+ bail: () => StringRule
249
+ confirmed: (message?: string) => StringRule
250
+ custom: (fn: CustomFn, message?: string) => StringRule
251
+ declined: (message?: string) => StringRule
252
+ default: (value: unknown) => StringRule
253
+ digits: (n: number, message?: string) => StringRule
254
+ digitsBetween: (min: number, max: number, message?: string) => StringRule
255
+ email: (message?: string) => StringRule
256
+ endsWith: (suffix: string, message?: string) => StringRule
257
+ exists: (table: string, column: string, message?: string) => StringRule
258
+ in: (values: string[], message?: string) => StringRule
259
+ ip: (message?: string) => StringRule
260
+ json: (message?: string) => StringRule
261
+ lowercase: () => StringRule
262
+ matches: (pattern: RegExp, message?: string) => StringRule
263
+ max: (length: number, message?: string) => StringRule
264
+ min: (length: number, message?: string) => StringRule
265
+ notIn: (values: string[], message?: string) => StringRule
266
+ nullable: () => StringRule
267
+ numeric: (message?: string) => StringRule
268
+ optional: () => StringRule
269
+ password: () => StringRule
270
+ present: (message?: string) => StringRule
271
+ prohibited: (message?: string) => StringRule
272
+ prohibitedIf: (field: string, value: unknown) => StringRule
273
+ prohibitedUnless: (field: string, value: unknown) => StringRule
274
+ readonly _def: StringDef
275
+ regex: (pattern: RegExp, message?: string) => StringRule
276
+ required: (message?: string) => StringRule
277
+ requiredIf: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => StringRule
278
+ requiredUnless: (fieldOrFn: string | ((input: Record<string, unknown>) => boolean), value?: unknown) => StringRule
279
+ requiredWith: (fields: string[]) => StringRule
280
+ requiredWithAll: (fields: string[]) => StringRule
281
+ requiredWithout: (fields: string[]) => StringRule
282
+ requiredWithoutAll: (fields: string[]) => StringRule
283
+ sameAs: (field: string, message?: string) => StringRule
284
+ size: (n: number, message?: string) => StringRule
285
+ sometimes: () => StringRule
286
+ startsWith: (prefix: string, message?: string) => StringRule
287
+ trim: () => StringRule
288
+ unique: (table: string, column: string, ignoreOrOpts?: string | number | UniqueOptions, message?: string) => StringRule
289
+ uppercase: () => StringRule
290
+ url: (message?: string) => StringRule
291
+ uuid: (message?: string) => StringRule
292
+ }
293
+
294
+ class ValidationJsonError = {
295
+ new (errors: ValidationErrors): ValidationJsonError
296
+ readonly code: string
297
+ readonly context?: Record<string, unknown> | undefined
298
+ readonly errors: ValidationErrors
299
+ readonly name: 'ValidationJsonError'
300
+ readonly status: number
301
+ }
302
+
303
+ class ValidationRedirectError = {
304
+ new (redirectTo: string, errors: ValidationErrors): ValidationRedirectError
305
+ readonly code: string
306
+ readonly context?: Record<string, unknown> | undefined
307
+ readonly errors: ValidationErrors
308
+ readonly name: 'ValidationRedirectError'
309
+ readonly redirectTo: string
310
+ readonly status: number
311
+ }
312
+
313
+ const Validator = { check<S extends Record<string, FieldRule>>(data: Record<string, unknown>, factory: (rules: RuleBuilder) => S): ValidationOutcome<Infer<ExtractDefs<S>>>;}
314
+
315
+ function registerDbRuleRunner = (runner: DbRuleRunner) => void
316
+
317
+ function runStringRules = (data: Record<string, unknown>, rules: StringRules) => Record<string, string[]>
318
+
319
+ function runStringRulesAsync = (data: Record<string, unknown>, rules: StringRules) => Promise<Record<string, string[]>>
320
+
321
+ function runValidation = <S extends Schema>(schema: S, input: Record<string, unknown>) => ValidationOutcome<unknown>
322
+
323
+ function runValidationAsync = <S extends Schema>(schema: S, input: Record<string, unknown>) => Promise<ValidationOutcome<unknown>>
324
+
325
+ function validate = <S extends Record<string, FieldRule>>(ctx: HttpContext, factory: (rules: RuleBuilder) => S) => Promise<Infer<ExtractDefs<S>>>
326
+
327
+ function ValidatorConfig = (options?: Partial<ValidatorConfigShape>) => ValidatorConfigShape
328
+
329
+ interface FieldRuleDefinition = {
330
+ bail?: boolean
331
+ children?: FieldRuleDefinition
332
+ defaultVal: unknown
333
+ nullable: boolean
334
+ prohibitedIf?: { field: string; value: unknown;}
335
+ prohibitedUnless?: { field: string; value: unknown;}
336
+ required: boolean
337
+ requiredIf?: { field: string; value: unknown;} | { fn: (input: Record<string, unknown>) => boolean;}
338
+ requiredMessage?: string
339
+ requiredUnless?: { field: string; value: unknown;} | { fn: (input: Record<string, unknown>) => boolean;}
340
+ requiredWith?: string[]
341
+ requiredWithAll?: string[]
342
+ requiredWithout?: string[]
343
+ requiredWithoutAll?: string[]
344
+ rules: { name: string; args: unknown[]; message?: string; fn?: CustomFn;}[]
345
+ shape?: Record<string, FieldRuleDefinition>
346
+ sometimes?: boolean
347
+ type: 'string' | 'number' | 'boolean' | 'object' | 'date' | 'file' | 'array'
348
+ }
349
+
350
+ interface UniqueOptions = {
351
+ ignoreId?: string | number
352
+ }
353
+
354
+ interface ValidatorConfigShape = {
355
+ locale: string
356
+ stopOnFirstFailure: boolean
357
+ }
358
+
359
+ type Infer = { [K in keyof S]: InferField<S[K]>; }
360
+
361
+ type InferFieldType = F['type'] extends 'string' ? string : F['type'] extends 'number' ? number : F['type'] extends 'boolean' ? boolean : F['type'] extends 'date' ? Date : F['type'] extends 'array' ? InferArrayType<F> : F['type'] extends 'object' ? InferObjectType<F> : unknown
362
+
363
+ type Schema = { [x: string]: FieldRuleDefinition;}
364
+
365
+ type StringRules = { [x: string]: string | string[];}
366
+
367
+ type ValidationErrors = { [x: string]: string;}
368
+
369
+ type ValidationOutcome = ValidationResult<T> | ValidationFailure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/validator",
3
- "version": "1.6.3",
3
+ "version": "1.7.2",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "files": [
14
14
  "CHANGELOG.md",
15
+ "api-surface.md",
15
16
  "src",
16
17
  "!src/**/*.test.ts",
17
18
  "!src/**/*.test.tsx",
@@ -29,7 +30,7 @@
29
30
  "typecheck": "tsc --noEmit"
30
31
  },
31
32
  "dependencies": {
32
- "@zerotal/core": "1.6.3"
33
+ "@zerotal/core": "1.7.2"
33
34
  },
34
35
  "devDependencies": {
35
36
  "typescript": "^5.8.0"
@@ -113,6 +113,30 @@ export abstract class FormRequest {
113
113
  throw new Error(`${this.constructor.name}.rules() is not implemented.`);
114
114
  }
115
115
 
116
+ /**
117
+ * Normalise the raw body before the rules see it. Default: unchanged.
118
+ *
119
+ * This is where the shape of the *transport* gets reconciled with the shape of
120
+ * the rules. An HTML form has no way to say `null`: an unselected `<select>`
121
+ * posts `""`, so a `r.number().nullable()` field fails on the one input the
122
+ * browser can actually produce for "nothing". A client-side form can patch
123
+ * that up before it posts; a plain `<form method="post">` cannot, and a rule
124
+ * only reachable from a build with JavaScript is not a shared rule.
125
+ *
126
+ * Runs after `authorize()` and after the body is parsed, so it sees exactly
127
+ * what arrived. Return the object to validate — mutating and returning the
128
+ * argument is fine.
129
+ *
130
+ * @example
131
+ * override prepareForValidation(body: Record<string, unknown>) {
132
+ * if (body["assigneeId"] === "") body["assigneeId"] = null;
133
+ * return body;
134
+ * }
135
+ */
136
+ prepareForValidation(body: Record<string, unknown>): Record<string, unknown> {
137
+ return body;
138
+ }
139
+
116
140
  /**
117
141
  * Attach a helper method to all FormRequest instances at runtime.
118
142
  * Designed for framework packages (e.g. @zerotal/auth) that expose
@@ -193,6 +217,10 @@ export abstract class FormRequest {
193
217
  schema[key] = rule._def;
194
218
  }
195
219
 
220
+ // The transport's shape reconciled with the rules' shape, before the rules
221
+ // run. See `prepareForValidation` for why an HTML form needs this at all.
222
+ body = instance.prepareForValidation(body);
223
+
196
224
  const result = await runValidationAsync(schema as Schema, body);
197
225
  const session = (ctx as unknown as { session?: SessionLike }).session;
198
226
 
package/src/Validator.ts CHANGED
@@ -135,7 +135,17 @@ export function runValidation<S extends Schema>(
135
135
  continue;
136
136
  }
137
137
 
138
- if (!isRequired && (value === undefined || value === null)) {
138
+ // Absent → skipped, which is what `optional()` means. An explicit `null` on
139
+ // a field that also declared `nullable()` is *not* absence: it is someone
140
+ // saying "clear this", and it falls through to the branch below so it
141
+ // reaches the output. Without this, `optional().nullable()` silently drops
142
+ // the null, `fill()` sees `undefined`, and "unassign" saves nothing while
143
+ // reporting success.
144
+ //
145
+ // `null` on a field that is optional but *not* nullable keeps the old
146
+ // behaviour — skipped rather than an error — so this widens what survives
147
+ // validation without newly rejecting anything.
148
+ if (!isRequired && (value === undefined || (value === null && !def.nullable))) {
139
149
  continue;
140
150
  }
141
151