@zerotal/validator 1.7.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/validator",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -30,7 +30,7 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@zerotal/core": "1.7.0"
33
+ "@zerotal/core": "1.7.2"
34
34
  },
35
35
  "devDependencies": {
36
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