@warlock.js/seal 5.22.1 → 5.23.1
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/CHANGELOG.md +16 -0
- package/cjs/index.cjs +13 -5
- package/cjs/index.cjs.map +1 -1
- package/cjs/{object-_2YHdBxJ.cjs → object-DPPttDAF.cjs} +2 -2
- package/cjs/{object-_2YHdBxJ.cjs.map → object-DPPttDAF.cjs.map} +1 -1
- package/cjs/object.cjs +2 -2
- package/cjs/{required-methods-GwIEE6hP.cjs → required-methods-DJTm7yNP.cjs} +2 -2
- package/cjs/required-methods-DJTm7yNP.cjs.map +1 -0
- package/cjs/{string-6m5Y83pJ.cjs → string-C_pAtZPC.cjs} +2 -2
- package/cjs/{string-6m5Y83pJ.cjs.map → string-C_pAtZPC.cjs.map} +1 -1
- package/cjs/string.cjs +2 -2
- package/esm/validators/array-validator.d.mts +4 -0
- package/esm/validators/array-validator.d.mts.map +1 -1
- package/esm/validators/array-validator.mjs +10 -2
- package/esm/validators/array-validator.mjs.map +1 -1
- package/esm/validators/base-validator.d.mts.map +1 -1
- package/esm/validators/base-validator.mjs +1 -1
- package/esm/validators/base-validator.mjs.map +1 -1
- package/package.json +1 -1
- package/skills/compose-seal-modifiers/SKILL.md +269 -271
- package/skills/define-structural-shape/object-methods.md +2 -2
- package/skills/seal-basics/SKILL.md +1 -1
- package/cjs/required-methods-GwIEE6hP.cjs.map +0 -1
|
@@ -1,271 +1,269 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: compose-seal-modifiers
|
|
3
|
-
description: 'Apply cross-cutting modifiers — `.optional` / `.nullable` / `.default` / `.catch` / `.omit` / membership rules — plus the mutator-vs-transformer pipeline and `Infer.Input` vs `Infer.Output`. Triggers: `.optional`, `.required`, `.nullable`, `.default`, `.catch`, `.present`, `.requiredIf`, `.requiredWith`, `.in`, `.oneOf`, `.notIn`, `.omit`, `.attribute`, `.mutable`, `.addMutator`, `Infer.Input`, `Infer.Output`; "mark field optional in seal", "default value in schema", "mutator vs transformer", "when does .catch fire"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: primitives — `@warlock.js/seal/pick-seal-primitive/SKILL.md`; containers — `@warlock.js/seal/define-structural-shape/SKILL.md`; errors — `@warlock.js/seal/handle-seal-errors/SKILL.md`; competing `zod` `.optional`/`.default`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Cross-cutting modifiers + the pipeline
|
|
7
|
-
|
|
8
|
-
This skill covers methods that work on **every** validator. Type-specific methods (`.email()`, `.min()` on strings, `.between()` on numbers, `.weekDay()` on dates, etc.) live in the per-type method references — see [`@warlock.js/seal/pick-seal-primitive/SKILL.md`](@warlock.js/seal/pick-seal-primitive/SKILL.md) and [`@warlock.js/seal/define-structural-shape/SKILL.md`](@warlock.js/seal/define-structural-shape/SKILL.md).
|
|
9
|
-
|
|
10
|
-
## The pipeline
|
|
11
|
-
|
|
12
|
-
When `validate()` runs against a value, the order is:
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
1. default — fills if input is undefined
|
|
16
|
-
2. mutators — reshape value (string → Date, .trim(), .toUTC())
|
|
17
|
-
3. optional/required check — decide whether to run rules
|
|
18
|
-
4. requiredRule — required-condition rule (.requiredIf, .requiredWith, etc.)
|
|
19
|
-
5. rules — every other rule, in declaration order
|
|
20
|
-
6. transformers — reshape value into data (.toISOString, .toLowerCase)
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
If a rule fails, transformers don't run for that field.
|
|
24
|
-
|
|
25
|
-
**Mutator vs transformer mental model:**
|
|
26
|
-
- **Mutator** = pre-validation reshape. `v.date()` ships one that parses strings. Use when you want rules to see the reshaped value.
|
|
27
|
-
- **Transformer** = post-validation reshape. Lands in `data`. Use when you only care about the output form.
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
// trim BEFORE length check — use a mutator
|
|
31
|
-
v.string().addMutator(s => s.trim()).min(3)
|
|
32
|
-
// " Hi " → mutator trims → "Hi" → fails min(3)
|
|
33
|
-
|
|
34
|
-
// trim only the OUTPUT — use a transformer (.trim() on string is a transformer)
|
|
35
|
-
v.string().min(3).trim()
|
|
36
|
-
// " Hi " → rules see " Hi " (length 6, passes min(3)) → trim → data = "Hi"
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## `.required()` / `.optional()` / `.present()`
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
v.string() // already required inside v.object
|
|
43
|
-
v.string().optional() // type: string | undefined — { isOptional: true } brand
|
|
44
|
-
v.string().present() // must exist, may be "" / null
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- `.
|
|
58
|
-
- `.
|
|
59
|
-
- `.
|
|
60
|
-
- `.
|
|
61
|
-
- `.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
v.
|
|
78
|
-
v.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
| Field
|
|
156
|
-
| Field
|
|
157
|
-
| Field present
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
|
164
|
-
|
|
|
165
|
-
| Field
|
|
166
|
-
| Field is
|
|
167
|
-
| Field
|
|
168
|
-
| Field is
|
|
169
|
-
| Field
|
|
170
|
-
| Field is `.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
v.string().
|
|
198
|
-
v.string().
|
|
199
|
-
v.
|
|
200
|
-
v.string().
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
v.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
`.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
- Don't
|
|
268
|
-
- Don't
|
|
269
|
-
- Don't
|
|
270
|
-
- Don't expect `.requiredIf()` to work on a standalone validator outside `v.object` — sibling resolution silently passes.
|
|
271
|
-
- Don't mutate a schema you handed to a Model. Default immutability protects you; opting into `.mutable` on shared schemas is asking for confusion.
|
|
1
|
+
---
|
|
2
|
+
name: compose-seal-modifiers
|
|
3
|
+
description: 'Apply cross-cutting modifiers — `.optional` / `.nullable` / `.default` / `.catch` / `.omit` / membership rules — plus the mutator-vs-transformer pipeline and `Infer.Input` vs `Infer.Output`. Triggers: `.optional`, `.required`, `.nullable`, `.default`, `.catch`, `.present`, `.requiredIf`, `.requiredWith`, `.in`, `.oneOf`, `.notIn`, `.omit`, `.attribute`, `.mutable`, `.addMutator`, `Infer.Input`, `Infer.Output`; "mark field optional in seal", "default value in schema", "mutator vs transformer", "when does .catch fire"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: primitives — `@warlock.js/seal/pick-seal-primitive/SKILL.md`; containers — `@warlock.js/seal/define-structural-shape/SKILL.md`; errors — `@warlock.js/seal/handle-seal-errors/SKILL.md`; competing `zod` `.optional`/`.default`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cross-cutting modifiers + the pipeline
|
|
7
|
+
|
|
8
|
+
This skill covers methods that work on **every** validator. Type-specific methods (`.email()`, `.min()` on strings, `.between()` on numbers, `.weekDay()` on dates, etc.) live in the per-type method references — see [`@warlock.js/seal/pick-seal-primitive/SKILL.md`](@warlock.js/seal/pick-seal-primitive/SKILL.md) and [`@warlock.js/seal/define-structural-shape/SKILL.md`](@warlock.js/seal/define-structural-shape/SKILL.md).
|
|
9
|
+
|
|
10
|
+
## The pipeline
|
|
11
|
+
|
|
12
|
+
When `validate()` runs against a value, the order is:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
1. default — fills if input is undefined
|
|
16
|
+
2. mutators — reshape value (string → Date, .trim(), .toUTC())
|
|
17
|
+
3. optional/required check — decide whether to run rules
|
|
18
|
+
4. requiredRule — required-condition rule (.requiredIf, .requiredWith, etc.)
|
|
19
|
+
5. rules — every other rule, in declaration order
|
|
20
|
+
6. transformers — reshape value into data (.toISOString, .toLowerCase)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If a rule fails, transformers don't run for that field.
|
|
24
|
+
|
|
25
|
+
**Mutator vs transformer mental model:**
|
|
26
|
+
- **Mutator** = pre-validation reshape. `v.date()` ships one that parses strings. Use when you want rules to see the reshaped value.
|
|
27
|
+
- **Transformer** = post-validation reshape. Lands in `data`. Use when you only care about the output form.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// trim BEFORE length check — use a mutator
|
|
31
|
+
v.string().addMutator(s => s.trim()).min(3)
|
|
32
|
+
// " Hi " → mutator trims → "Hi" → fails min(3)
|
|
33
|
+
|
|
34
|
+
// trim only the OUTPUT — use a transformer (.trim() on string is a transformer)
|
|
35
|
+
v.string().min(3).trim()
|
|
36
|
+
// " Hi " → rules see " Hi " (length 6, passes min(3)) → trim → data = "Hi"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## `.required()` / `.optional()` / `.present()`
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
v.string() // already required inside v.object
|
|
43
|
+
v.string().optional() // type: string | undefined — { isOptional: true } brand
|
|
44
|
+
v.string().present() // must exist, may be "" / null
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Required is the default inside `v.object`.** Most schemas read cleaner without `.required()` — `Infer<>` already shows what's required (no `?`) vs optional (`?`). Calling `.required()` explicitly is harmless and accepted, but the canonical seal style is to skip it. Keep `.optional()` explicit (it changes behavior); skip `.required()` (it doesn't).
|
|
48
|
+
|
|
49
|
+
The `{ isOptional: true }` brand survives chaining (`.optional().min(3)` is still optional) and `Infer<>` reads it to make the key optional.
|
|
50
|
+
|
|
51
|
+
**When `.required()` is still useful:**
|
|
52
|
+
- It's not needed for conditional rules — `.requiredIf(field, value)` and friends *replace* the default required-condition slot, so they work standalone.
|
|
53
|
+
|
|
54
|
+
Conditional variants (run inside `v.object` only):
|
|
55
|
+
- `.requiredIf(field, value)` / `.requiredIfSibling(field, value)`
|
|
56
|
+
- `.requiredWith(field)` / `.requiredWithSibling(field)`
|
|
57
|
+
- `.requiredWithout(field)` / `.requiredWithoutSibling(field)`
|
|
58
|
+
- `.requiredUnless(field, value)`
|
|
59
|
+
- `.requiredWhen(callback)` — predicate
|
|
60
|
+
- `.present()` / `.presentIf(field, value)` / `.presentUnless(field, value)`
|
|
61
|
+
- `.forbidden()` / `.forbiddenIf(field, value)` — opposite of present
|
|
62
|
+
|
|
63
|
+
## `.nullable()` / `.notNullable()` / `.nullish()`
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
v.string().nullable() // type: string | null
|
|
67
|
+
v.string().nullish() // sugar for .optional().nullable()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Independent of optional — a field can be required *and* nullable. Defaults to non-nullable. The `{ isNullable: true }` brand widens both `Infer.Input` and `Infer.Output` with `| null`.
|
|
71
|
+
|
|
72
|
+
## `.default(value | callback)`
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
v.string().default("guest")
|
|
76
|
+
v.int().default(0)
|
|
77
|
+
v.date().default(() => new Date()) // lazy — fresh each validation
|
|
78
|
+
v.array(v.string()).default([])
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If input is `undefined` (or missing), the default is used and rules run against it. Pass a callback for fresh-per-run values. The default fires *before* rules — so `v.string().min(3).default("a")` fails because `"a"` is shorter than 3.
|
|
82
|
+
|
|
83
|
+
Date sugar: `v.date().defaultNow()` ≡ `.default(() => new Date())`.
|
|
84
|
+
|
|
85
|
+
The `{ hasDefault: true }` brand makes the key optional in `Infer.Input` (caller doesn't have to send it) and required in `Infer.Output` (data always has it).
|
|
86
|
+
|
|
87
|
+
## `.catch(fallback)`
|
|
88
|
+
|
|
89
|
+
Rescues *failed* validation by substituting a fallback. Complement to `.default()`:
|
|
90
|
+
|
|
91
|
+
- `.default(x)` fires when input is **absent**
|
|
92
|
+
- `.catch(y)` fires when input is **present but invalid**
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const config = v.object({
|
|
96
|
+
retries: v.int().min(0).catch(3),
|
|
97
|
+
region: v.string().in(["us", "eu"]).catch("us"),
|
|
98
|
+
features: v.array(v.string()).catch([]),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await validate(config, { retries: "five", region: null, features: "x" });
|
|
102
|
+
// { isValid: true, data: { retries: 3, region: "us", features: [] } }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The fallback can be a value or a callback `(errors, originalInput) => fallback` — the callback variant is the only side-channel for the swallowed errors. Use it to log/alert before substituting.
|
|
106
|
+
|
|
107
|
+
**Scope (v1).** Catch is honoured for **leaf validators** (string, number, boolean, date, …) and for fields inside containers. It is a **no-op on container validators themselves** (`v.object`, `v.array`, `v.record`, `v.tuple`, `v.discriminatedUnion`) — those use their own iteration logic that bypasses the catch hook. To rescue a whole-container failure, wrap the call site in your own try/catch instead.
|
|
108
|
+
|
|
109
|
+
**Best used for:** LLM output parsing, third-party API responses, config files, any data where the cost of failure is higher than the cost of a wrong value. Overuse masks real bugs — reach for it deliberately.
|
|
110
|
+
|
|
111
|
+
The `{ hasCatch: true }` brand has the same effect on `Infer.Input`/`Infer.Output` as `{ hasDefault: true }`.
|
|
112
|
+
|
|
113
|
+
## `Infer.Input` vs `Infer.Output`
|
|
114
|
+
|
|
115
|
+
The two inference helpers describe the two halves of the pipeline:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const schema = v.object({
|
|
119
|
+
bio: v.string().optional(),
|
|
120
|
+
status: v.enum(Status).optional().default(Status.ACTIVE),
|
|
121
|
+
retries: v.int().catch(3),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
type In = Infer.Input<typeof schema>;
|
|
125
|
+
// {
|
|
126
|
+
// bio?: string;
|
|
127
|
+
// status?: Status; ← default makes caller optional
|
|
128
|
+
// retries?: number; ← catch makes caller optional
|
|
129
|
+
// }
|
|
130
|
+
|
|
131
|
+
type Out = Infer.Output<typeof schema>;
|
|
132
|
+
// {
|
|
133
|
+
// bio?: string;
|
|
134
|
+
// status: Status; ← default guarantees a value
|
|
135
|
+
// retries: number; ← catch guarantees a value
|
|
136
|
+
// }
|
|
137
|
+
|
|
138
|
+
type Default = Infer<typeof schema>; // alias for Infer.Input
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**When to reach for which:**
|
|
142
|
+
- `Infer.Input<T>` (or bare `Infer<T>`) — for HTTP request bodies, form payloads, DTOs, anything pre-validation. **The common case in HTTP-shaped code.**
|
|
143
|
+
- `Infer.Output<T>` — for Cascade `Model<>` params, validated state, anywhere downstream of `validate()`.
|
|
144
|
+
|
|
145
|
+
Both widen with `| null` when `.nullable()` is set.
|
|
146
|
+
|
|
147
|
+
## Absent vs empty vs invalid — what `data` actually contains
|
|
148
|
+
|
|
149
|
+
Three failure modes, three different rescue mechanisms:
|
|
150
|
+
|
|
151
|
+
| Input state | Rescued by | What appears in `data` |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| Field absent | `.default(x)` | `x` |
|
|
154
|
+
| Field absent (no default) | `.optional()` | Key omitted entirely |
|
|
155
|
+
| Field present and invalid | `.catch(y)` | `y` |
|
|
156
|
+
| Field is `null` | `.nullable()` | `null` |
|
|
157
|
+
| Field present, empty (`""`, `[]`, `{}`) | (none needed — empty is a valid value) | Preserved as-is |
|
|
158
|
+
|
|
159
|
+
Full truth table for an `.optional()` field inside `v.object(…)`:
|
|
160
|
+
|
|
161
|
+
| Input | What appears in `data` |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| Field absent | Key **omitted entirely** (not `undefined`-valued) |
|
|
164
|
+
| Field is explicit `undefined` | Key omitted (treated identically to absent) |
|
|
165
|
+
| Field is `null` (no `.nullable()`) | Key omitted — for an **optional** field, `null` coalesces to empty and the (cleared) required rule doesn't fire. On a **required** field the same `null` triggers a validation error. |
|
|
166
|
+
| Field is `null` with `.nullable()` | Key is `null` |
|
|
167
|
+
| Field present and empty (`""`, `[]`, `{}`) | Preserved as-is — empty ≠ absent |
|
|
168
|
+
| Field is `.default(x)` and absent | Key is `x` (default fires, then rules run on `x`) |
|
|
169
|
+
| Field is `.default(x)` and present | Caller value wins; default is unused |
|
|
170
|
+
| Field is `.catch(y)` and validation fails | Key is `y` (catch rescues) |
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const schema = v.object({
|
|
174
|
+
metadata: v.record(v.string()).optional(),
|
|
175
|
+
embedding: v.array(v.number()).optional(),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
(await validate(schema, {})).data
|
|
179
|
+
// → {} — neither key appears
|
|
180
|
+
// (NOT { metadata: {}, embedding: [] })
|
|
181
|
+
|
|
182
|
+
(await validate(schema, { metadata: {}, embedding: [] })).data
|
|
183
|
+
// → { metadata: {}, embedding: [] } — present-empty is preserved
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Why the distinction matters.** Persistence layers see `key in data` as "user touched this column". Synthesizing `{}` / `[]` for absent input would write empty values to the DB, defeat `$exists` filters, and confuse "I cleared this" vs "I never set this" downstream. Cascade models, Standard-Schema consumers, and JSON serializers all depend on this contract.
|
|
187
|
+
|
|
188
|
+
Collection validators (`v.record`, `v.array`, `v.tuple`) explicitly honor this — they used to coerce absent input to empty containers (a long-standing bug), but now propagate `undefined` so the parent `v.object` correctly omits the key.
|
|
189
|
+
|
|
190
|
+
## Membership rules (inherited by every primitive)
|
|
191
|
+
|
|
192
|
+
Available on `v.string()`, `v.number()`, `v.int()`, `v.float()`, `v.boolean()`, `v.scalar()`:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
v.string().in(["admin", "user", "guest"]) // value must match one
|
|
196
|
+
v.string().oneOf(["a", "b"]) // alias for .in
|
|
197
|
+
v.string().notIn(["banned", "blocked"]) // value must NOT match
|
|
198
|
+
v.string().forbids(["banned"]) // alias for .notIn
|
|
199
|
+
v.number().allowsOnly([1, 2, 3]) // stricter — explicit allowlist
|
|
200
|
+
v.string().enum(MyTSEnum) // accepts a TS enum object via Object.values
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
For literal-typed narrowing (`"admin" | "user" | "guest"` instead of `string`), use `v.literal(...)` instead — `oneOf` keeps the broader primitive type.
|
|
204
|
+
|
|
205
|
+
## `.omit()` / `.exclude()`
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
v.object({
|
|
209
|
+
email: v.string(),
|
|
210
|
+
password: v.string(),
|
|
211
|
+
passwordConfirm: v.string().sameAs("password").omit(),
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`.omit()` keeps the field in *validation* but drops it from `data` and from `Infer<>`. Use for confirmation/checksum fields that exist only for cross-field rules. `.exclude()` is the same idea, used internally for managed/computed plumbing.
|
|
216
|
+
|
|
217
|
+
## `.label("Display Name")` — field display name
|
|
218
|
+
|
|
219
|
+
To control the `:input` placeholder in a field's own messages, call `.label()` on the field validator:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
v.object({
|
|
223
|
+
email_address: v.string().label("Email Address"),
|
|
224
|
+
})
|
|
225
|
+
// Error message: "The Email Address is required" (instead of "The email_address is required")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`.label(x)` sets the field's `:input` attribute, so every rule message for that field renders the friendly name.
|
|
229
|
+
|
|
230
|
+
### `.attributes({ ... })` is a different tool
|
|
231
|
+
|
|
232
|
+
`.attributes()` does NOT relabel a field's own `:input`. It supplies named substitution values consumed by the translation layer and by rules that reference *other* fields (e.g. `matches`):
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
v.string().sameAs("confirmPassword").attributes({
|
|
236
|
+
matches: { confirmPassword: "Confirm Password" },
|
|
237
|
+
})
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
For per-field display names use `.label()`; for translated messages wire the `translateRule` / `translateAttribute` hooks via `configureSeal()`.
|
|
241
|
+
|
|
242
|
+
## Mutability — `.mutable` / `.immutable`
|
|
243
|
+
|
|
244
|
+
Validators are **immutable by default**. Every chain method returns a clone:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const baseString = v.string();
|
|
248
|
+
const short = baseString.min(3);
|
|
249
|
+
// baseString is unchanged
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
This matters because schemas are often shared (`Model.schema = v.object({...})`). If chaining mutated, every reuse would carry forward the previous chain's state.
|
|
253
|
+
|
|
254
|
+
Toggle in-place with the `.mutable` getter (rare):
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
const schema = v.string().mutable.min(3);
|
|
258
|
+
// Same instance throughout — useful when building dynamically
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Switch back with `.immutable`. Default is fine 99% of the time. Reach for `.mutable` only when you've thought about who else holds a reference.
|
|
262
|
+
|
|
263
|
+
## Things NOT to do
|
|
264
|
+
|
|
265
|
+
- Don't put a transformer where a mutator belongs — `.trim()` is a transformer; if you need trimming *before* `.min()`, use `.addMutator(s => s.trim())`.
|
|
266
|
+
- Don't combine `.required()` and `.optional()` on the same chain — last wins, but the intent is unclear; pick one.
|
|
267
|
+
- Don't chain `.default("a")` with `.min(3)` and expect `"a"` to pass — the default goes through rules.
|
|
268
|
+
- Don't expect `.requiredIf()` to work on a standalone validator outside `v.object` — sibling resolution silently passes.
|
|
269
|
+
- Don't mutate a schema you handed to a Model. Default immutability protects you; opting into `.mutable` on shared schemas is asking for confusion.
|
|
@@ -100,8 +100,8 @@ The default is to **reject** unknown keys, which is what you want for inbound pa
|
|
|
100
100
|
|
|
101
101
|
```ts
|
|
102
102
|
const publicArticle = v.object({
|
|
103
|
-
id: v.string()
|
|
104
|
-
title: v.string()
|
|
103
|
+
id: v.string(),
|
|
104
|
+
title: v.string(),
|
|
105
105
|
}).stripUnknown();
|
|
106
106
|
|
|
107
107
|
const { isValid, data } = await v.validate(publicArticle, record);
|
|
@@ -49,7 +49,7 @@ import { object } from "@warlock.js/seal/object";
|
|
|
49
49
|
import { string } from "@warlock.js/seal/string";
|
|
50
50
|
|
|
51
51
|
const commentSchema = object({
|
|
52
|
-
comment: string().trim().
|
|
52
|
+
comment: string().trim().minLength(3),
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
const result = await commentSchema["~standard"].validate({
|