feat: introduce reusable Zod validator registry with comprehensive atomic schema modifiers and localization support

This commit is contained in:
Firman Ramdhani
2026-06-15 20:44:27 +07:00
parent c9278d09cb
commit f7c7bc6907
7 changed files with 340 additions and 20 deletions
+65 -9
View File
@@ -260,17 +260,11 @@ To prevent over-engineering and package fatigue, we house the validation layer d
```tsx
// packages/ui/src/validators/sample.validator.ts
import { z } from 'zod';
import { compose, emailValidator, minLength } from './registry.validator';
export const sampleValidator = z.object({
email: z.string().email({
message: JSON.stringify({ key: 'validation:invalid_email' }),
}),
name: z.string().min(3, {
message: JSON.stringify({
key: 'validation:min_length',
values: { field: 'Nama', min: 3 },
}),
}),
email: compose(z.string(), emailValidator()),
name: compose(z.string(), minLength(3, 'Nama')),
});
export type SampleValidatorType = z.infer<typeof sampleValidator>;
@@ -306,6 +300,68 @@ function ExampleForm() {
---
## Validator Bank Reference
The `registry.validator.ts` provides a set of pre-configured atomic validators returning modified Zod schemas that automatically emit translated JSON payloads.
### Available Atomic Validators
| Category | Validator | Target Type | Description |
|---|---|---|---|
| **Numeric** | `minValue(min, field?)` | `ZodNumber` | Minimum numeric value |
| **Numeric** | `maxValue(max, field?)` | `ZodNumber` | Maximum numeric value |
| **Numeric** | `rangeValue(min, max, field?)` | `ZodNumber` | Restricts value between `min` and `max` limits |
| **Numeric** | `positiveNumber(field?)` | `ZodNumber` | Restricts to positive numbers |
| **String** | `minLength(len, field?)` | `ZodString` | Minimum string character length |
| **String** | `maxLength(len, field?)` | `ZodString` | Maximum string character length |
| **String** | `rangeLength(min, max, field?)` | `ZodString` | Restricts string length between `min` and `max` bounds |
| **Security** | `simplePassword(min)` | `ZodString` | Checks password string length bounds only |
| **Security** | `complexPassword(min)` | `ZodString` | Enforces length, 1 uppercase, 1 lowercase, 1 number, and 1 special char |
| **Technical** | `emailValidator()` | `ZodString` | Standard email format |
| **Technical** | `phoneValidator()` | `ZodString` | Enforces Indonesian (+62) phone number format |
> [!WARNING]
> Always distinguish between `rangeValue` (which bounds the actual numeric integer/float) and `rangeLength` (which bounds the amount of characters in a string).
### Composition Guide
Instead of manually chaining long `.min().max().regex()` methods, use the `compose()` helper utility to elegantly stack atomic validators onto a base primitive.
**Example: User Registration Password Field**
```tsx
import { z } from 'zod';
import { compose, required, minLength, complexPassword } from '@repo/ui/validators';
export const userRegistrationSchema = z.object({
password: compose(
z.string(),
required('Password'),
complexPassword(8)
)
});
```
### Testing Validators
We enforce strict test coverage for our Validation Bank. If you add a new atomic validator to `registry.validator.ts`, you MUST add corresponding tests to `__tests__/registry.validator.test.ts`.
Tests must explicitly verify the JSON stringified i18n payload:
```typescript
it('minValue() should enforce min', () => {
const schema = compose(z.number(), minValue(10, 'Age'));
const res = schema.safeParse(5);
expect(res.success).toBe(false);
expect(res.error?.issues[0].message).toBe(
JSON.stringify({ key: 'validation:min_val', values: { min: 10, field: 'Age' } })
);
});
```
---
## Usage Examples
### Basic Form