@gravionlabs/helix-zod 22.0.0
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/LICENSE.md +21 -0
- package/README.md +395 -0
- package/fesm2022/gravionlabs-helix-zod.mjs +974 -0
- package/fesm2022/gravionlabs-helix-zod.mjs.map +1 -0
- package/package.json +45 -0
- package/types/gravionlabs-helix-zod.d.ts +457 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GravionLabs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# @gravionlabs/helix-zod
|
|
2
|
+
|
|
3
|
+
Zod v4 adapter for `@gravionlabs/helix-core` forms. Two independent features:
|
|
4
|
+
|
|
5
|
+
1. **Reactive-forms validator bridge** — `HelixZodValidators.fromZod()` converts a Zod field schema into an Angular `ValidatorFn` that emits `HelixValidatorKey`-keyed `ValidationErrors` — compatible with `HelixFormField`, `HelixFirstError`, and `helixFormErrorMap` out of the box.
|
|
6
|
+
2. **Dynamic forms** — `HelixDynamicForm` generates a complete, validated form from a single annotated Zod object schema, built on Angular's experimental signal forms (`@angular/forms/signals`, requires Angular **≥ 21.2**).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Peer Dependencies
|
|
11
|
+
|
|
12
|
+
| Package | Version |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `zod` | `^4.0.0` |
|
|
15
|
+
| `@gravionlabs/helix-core` | `>=0.2.0` |
|
|
16
|
+
| `@angular/core` / `@angular/common` / `@angular/forms` | `>=21` (dynamic forms: `>=21.2`) |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Dynamic Forms from Zod Schemas
|
|
21
|
+
|
|
22
|
+
> Built on `@angular/forms/signals`, which is **experimental** — minor Angular releases may introduce breaking API changes.
|
|
23
|
+
|
|
24
|
+
### Quick start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { z } from 'zod';
|
|
28
|
+
import { HelixDynamicForm, helixMeta, provideHelixDynamicForms } from '@gravionlabs/helix-zod';
|
|
29
|
+
|
|
30
|
+
const UserSchema = z.object({
|
|
31
|
+
email: helixMeta(z.email('Invalid email'), { label: 'E-mail', placeholder: 'you@example.com' }),
|
|
32
|
+
age: helixMeta(z.number().min(18, 'Must be 18+'), { label: 'Age' }),
|
|
33
|
+
role: helixMeta(z.enum(['user', 'editor']), { label: 'Role' }),
|
|
34
|
+
newsletter: helixMeta(z.boolean(), { label: 'Subscribe' }),
|
|
35
|
+
frequency: helixMeta(z.enum(['daily', 'weekly']), {
|
|
36
|
+
label: 'Frequency',
|
|
37
|
+
hiddenWhen: (root) => !root['newsletter'], // conditional visibility
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
@Component({
|
|
42
|
+
imports: [HelixDynamicForm],
|
|
43
|
+
providers: [provideHelixDynamicForms()],
|
|
44
|
+
template: `<helix-dynamic-form [schema]="schema" (submitted)="save($event)" />`,
|
|
45
|
+
})
|
|
46
|
+
export class UserForm {
|
|
47
|
+
readonly schema = UserSchema;
|
|
48
|
+
save(value: unknown) { /* value is UserSchema.parse()d */ }
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Validation runs through signal forms' `validateStandardSchema` against the whole Zod schema — issue paths route each error to its field, including cross-field `.refine(..., { path })` errors.
|
|
53
|
+
|
|
54
|
+
### Widget inference
|
|
55
|
+
|
|
56
|
+
| Zod type | Widget |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `z.string()` | `text` (`z.email()` → `email`) |
|
|
59
|
+
| `z.number()` / `z.int()` | `number` |
|
|
60
|
+
| `z.boolean()` | `checkbox` |
|
|
61
|
+
| `z.date()` | `date` (`Date | null` model) |
|
|
62
|
+
| `z.enum()` / `z.literal()` | `select` |
|
|
63
|
+
| `z.array()` | `array` (add/remove UI) |
|
|
64
|
+
| `z.object()` | `object` (fieldset recursion) |
|
|
65
|
+
| `z.discriminatedUnion()` | `union` (variant switcher; switching **resets** the union value) |
|
|
66
|
+
|
|
67
|
+
Anything else needs a `widget` override in its metadata pointing at a registered (custom) widget — unmapped types throw in dev mode.
|
|
68
|
+
|
|
69
|
+
### Metadata (`HelixFieldMeta`)
|
|
70
|
+
|
|
71
|
+
Attach UI metadata with `helixMeta(schema, meta)` — it registers on the **same schema instance** (unlike `.meta()`, which clones; metadata must sit on the exact instance composed into the `z.object()` shape). Alternatively `schema.meta({ title, description, helix: {...} })` works: `title` → `label`, `description` → `hint`.
|
|
72
|
+
|
|
73
|
+
Keys: `label`, `placeholder`, `hint`, `widget`, `options`, `order`, `addLabel`/`removeLabel` (arrays), and the conditional engine: `hiddenWhen`, `disabledWhen` (string return = disabled reason), `readonlyWhen`, `requiredWhen` — all predicates over the root form value — plus `extraSchema(path)` as an escape hatch for arbitrary signal-forms rules.
|
|
74
|
+
|
|
75
|
+
### Custom widgets & error messages
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
provideHelixDynamicForms({
|
|
79
|
+
widgets: [{ widget: 'rating', component: RatingWidget }], // or override built-ins
|
|
80
|
+
errorMessageResolver: (error, helixKey) =>
|
|
81
|
+
helixKey === HelixValidatorKey.Required ? 'Pflichtfeld' : null, // null → default message
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Custom widgets extend `HelixFieldWidgetBase` (inputs `field` + `descriptor`, computeds `state`/`firstError`/`label`/`hint`/`placeholder`) and bind their control via `[formField]="field()"`.
|
|
86
|
+
|
|
87
|
+
### Notes & caveats
|
|
88
|
+
|
|
89
|
+
- Passing a new `schema` input **recreates the form and resets its state**.
|
|
90
|
+
- Provide your own `WritableSignal` via `[model]` to control/observe the raw value; otherwise an initial model is derived from the schema (`buildDefaultValue`).
|
|
91
|
+
- `submitted` emits the `schema.parse()`d output (defaults/transforms applied) — only when the form is valid.
|
|
92
|
+
- Lower-level building blocks are exported for advanced use: `zodToFieldDescriptors`, `buildHelixSchema`, `buildDefaultValue`, `helixFirstErrorMessage`, `fieldAtPath`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Installation
|
|
97
|
+
|
|
98
|
+
This is a workspace library — no `npm install` needed. The path alias is already registered in `tsconfig.json`:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
"@gravionlabs/helix-zod": ["./projects/zod/src/public-api.ts"]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Import directly in your application:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { HelixZodValidators } from '@gravionlabs/helix-zod';
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Quick Start
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { z } from 'zod';
|
|
116
|
+
import { HelixZodValidators } from '@gravionlabs/helix-zod';
|
|
117
|
+
|
|
118
|
+
// Wrap any Zod field schema in a reactive form control
|
|
119
|
+
form = this.fb.group({
|
|
120
|
+
email: ['', HelixZodValidators.fromZod(z.string().email('Invalid email'))],
|
|
121
|
+
name: ['', HelixZodValidators.fromZod(z.string().min(1, 'Required'), { allowEmpty: false })],
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`HelixFormField` reads the resulting `ValidationErrors` keys directly — no template changes required.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## API
|
|
130
|
+
|
|
131
|
+
### `HelixZodValidators.fromZod(schema, options?)`
|
|
132
|
+
|
|
133
|
+
Converts a Zod field schema into a Helix-compatible Angular `ValidatorFn`.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import type { ValidatorFn } from '@angular/forms';
|
|
137
|
+
import type { ZodSchema } from 'zod';
|
|
138
|
+
import type { ZodHelixOptions } from '@gravionlabs/helix-zod';
|
|
139
|
+
|
|
140
|
+
fromZod(schema: ZodSchema, options?: ZodHelixOptions): ValidatorFn
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Parameters**
|
|
144
|
+
|
|
145
|
+
| Parameter | Type | Description |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `schema` | `ZodSchema` | A Zod field schema. Prefer `UserSchema.shape.email` over a full object schema. Do **not** pass schemas with `.transform()` — use the pre-transform shape for form controls. |
|
|
148
|
+
| `options` | `ZodHelixOptions` | Optional configuration (see below). |
|
|
149
|
+
|
|
150
|
+
### `ZodHelixOptions`
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
export interface ZodHelixOptions {
|
|
154
|
+
fallbackKey?: HelixValidatorKey; // required when schema uses .refine() / .superRefine()
|
|
155
|
+
allowEmpty?: boolean; // default: true
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Zod v4 → `HelixValidatorKey` Mapping
|
|
162
|
+
|
|
163
|
+
> This library targets **Zod v4**. Zod v4 introduced breaking changes from v3:
|
|
164
|
+
> `invalid_string` → `invalid_format` (with a `format` property), `too_small`/`too_big` use `origin` instead of `type`, and `not_integer` was replaced by `invalid_type` with `expected: 'int'`.
|
|
165
|
+
|
|
166
|
+
| Zod v4 issue code | Condition | `HelixValidatorKey` |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `invalid_type` | value is `''`, `null`, or `undefined` | `Required` |
|
|
169
|
+
| `invalid_type` | `expected === 'int'` | `Integer` |
|
|
170
|
+
| `invalid_type` | `expected === 'number'` / `'float'` | `Number` |
|
|
171
|
+
| `invalid_format` | `format === 'email'` | `Email` |
|
|
172
|
+
| `invalid_format` | `format === 'regex'` | `Pattern` |
|
|
173
|
+
| `invalid_format` | `format === 'datetime'` / `'date'` / `'time'` | `Date` |
|
|
174
|
+
| `too_small` | `origin === 'string'` or `'array'` | `MinLength` |
|
|
175
|
+
| `too_big` | `origin === 'string'` or `'array'` | `MaxLength` |
|
|
176
|
+
| `too_small` | `origin === 'number'` | `Min` |
|
|
177
|
+
| `too_big` | `origin === 'number'` | `Max` |
|
|
178
|
+
| `custom` | — | requires `fallbackKey` (see below) |
|
|
179
|
+
| anything else | — | requires `fallbackKey` (see below) |
|
|
180
|
+
|
|
181
|
+
All issues from a single `safeParse` are processed simultaneously, producing one error key per issue — equivalent to stacking multiple `HelixValidators` calls.
|
|
182
|
+
|
|
183
|
+
### Known gaps — no automatic mapping
|
|
184
|
+
|
|
185
|
+
| Scenario | Recommendation |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `z.enum()` → `invalid_value` | Use `fallbackKey` or keep using `HelixValidators.oneOf` |
|
|
188
|
+
| `z.array()` item-level errors | Use `fallbackKey` or `HelixValidators.allOf` |
|
|
189
|
+
| `invalid_type` for `boolean` | Helix has no `Boolean` key — use `fallbackKey` |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## `allowEmpty` Behaviour
|
|
194
|
+
|
|
195
|
+
`HelixValidators` defaults to `allowEmpty = true`: validation passes silently on empty values. `fromZod` mirrors this:
|
|
196
|
+
|
|
197
|
+
| Scenario | Recommended pattern |
|
|
198
|
+
|---|---|
|
|
199
|
+
| Optional field with format check | Default `allowEmpty: true` — empty passes, invalid format shows error |
|
|
200
|
+
| Mandatory field (required + format) | Stack `HelixValidators.required('msg')` alongside `fromZod(schema)` |
|
|
201
|
+
| Required via Zod only | `allowEmpty: false` — empty triggers `Required` (null/undefined) or `MinLength` (empty string) |
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// Optional — empty passes, bad format shows Email error
|
|
205
|
+
HelixZodValidators.fromZod(z.string().email('Invalid email'))
|
|
206
|
+
|
|
207
|
+
// Required — must have a value; empty string → MinLength, null → Required
|
|
208
|
+
HelixZodValidators.fromZod(z.string().min(1, 'Name is required'), { allowEmpty: false })
|
|
209
|
+
|
|
210
|
+
// Stacked — explicit required message + Zod format check
|
|
211
|
+
[
|
|
212
|
+
HelixValidators.required('Email is required'),
|
|
213
|
+
HelixZodValidators.fromZod(z.string().email('Invalid email')),
|
|
214
|
+
]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Note:** when `allowEmpty: false` and the value is `null` or `undefined`, Zod emits `invalid_type` with its default mismatch message, not the message from `.min()` or `.email()`. Stack `HelixValidators.required('...')` for a custom required message.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## `.refine()` and `fallbackKey`
|
|
222
|
+
|
|
223
|
+
`.refine()` and `.superRefine()` produce `ZodIssueCode.custom`, which has no automatic `HelixValidatorKey` mapping. Provide a `fallbackKey` to capture these errors:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const bannedUsernames = ['admin', 'root', 'system'];
|
|
227
|
+
|
|
228
|
+
HelixZodValidators.fromZod(
|
|
229
|
+
z.string()
|
|
230
|
+
.min(3, 'At least 3 characters')
|
|
231
|
+
.refine((v) => !bannedUsernames.includes(v), 'Username is not allowed'),
|
|
232
|
+
{ fallbackKey: HelixValidatorKey.Pattern },
|
|
233
|
+
)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Missing `fallbackKey` behaviour:**
|
|
237
|
+
|
|
238
|
+
- **Development** (`ngDevMode = true`): throws a descriptive error identifying the unmapped issue code and the failing issue JSON.
|
|
239
|
+
- **Production** (`ngDevMode = false`): the unmapped issue is silently skipped. The control remains invalid if other issues produce mapped errors.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Component Example
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
|
|
247
|
+
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
|
|
248
|
+
import { z } from 'zod';
|
|
249
|
+
import { HelixFormField, HelixValidators, HelixValidatorKey } from '@gravionlabs/helix-core';
|
|
250
|
+
import { HelixZodValidators } from '@gravionlabs/helix-zod';
|
|
251
|
+
|
|
252
|
+
// Define your schema once — reuse it for both API parsing and form validation
|
|
253
|
+
const UserSchema = z.object({
|
|
254
|
+
email: z.string().email('Invalid email'),
|
|
255
|
+
name: z.string().min(1, 'Name is required'),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const bannedUsernames = ['admin', 'root'];
|
|
259
|
+
|
|
260
|
+
@Component({
|
|
261
|
+
selector: 'app-register',
|
|
262
|
+
standalone: true,
|
|
263
|
+
imports: [ReactiveFormsModule, HelixFormField],
|
|
264
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
265
|
+
template: `
|
|
266
|
+
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
|
267
|
+
<helix-form-field label="Email" [control]="form.controls.email">
|
|
268
|
+
<input type="email" formControlName="email" />
|
|
269
|
+
</helix-form-field>
|
|
270
|
+
|
|
271
|
+
<helix-form-field label="Name" [control]="form.controls.name">
|
|
272
|
+
<input type="text" formControlName="name" />
|
|
273
|
+
</helix-form-field>
|
|
274
|
+
|
|
275
|
+
<helix-form-field label="Username" [control]="form.controls.username">
|
|
276
|
+
<input type="text" formControlName="username" />
|
|
277
|
+
</helix-form-field>
|
|
278
|
+
|
|
279
|
+
<button type="submit" [disabled]="form.invalid">Register</button>
|
|
280
|
+
</form>
|
|
281
|
+
`,
|
|
282
|
+
})
|
|
283
|
+
export class RegisterComponent {
|
|
284
|
+
readonly #fb = inject(FormBuilder);
|
|
285
|
+
|
|
286
|
+
form = this.#fb.group({
|
|
287
|
+
// Stack required + Zod for a custom required message
|
|
288
|
+
email: [
|
|
289
|
+
'',
|
|
290
|
+
[
|
|
291
|
+
HelixValidators.required('Email is required'),
|
|
292
|
+
HelixZodValidators.fromZod(UserSchema.shape.email),
|
|
293
|
+
],
|
|
294
|
+
],
|
|
295
|
+
|
|
296
|
+
// allowEmpty: false — Zod handles both empty and format validation
|
|
297
|
+
name: [
|
|
298
|
+
'',
|
|
299
|
+
HelixZodValidators.fromZod(UserSchema.shape.name, { allowEmpty: false }),
|
|
300
|
+
],
|
|
301
|
+
|
|
302
|
+
// .refine() requires fallbackKey
|
|
303
|
+
username: [
|
|
304
|
+
'',
|
|
305
|
+
HelixZodValidators.fromZod(
|
|
306
|
+
z.string()
|
|
307
|
+
.min(3, 'At least 3 characters')
|
|
308
|
+
.refine((v) => !bannedUsernames.includes(v), 'Username is not allowed'),
|
|
309
|
+
{ fallbackKey: HelixValidatorKey.Pattern },
|
|
310
|
+
),
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
protected onSubmit() {
|
|
315
|
+
if (this.form.invalid) {
|
|
316
|
+
this.form.markAllAsTouched();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// Parse the form value through the full schema for the API call
|
|
320
|
+
const payload = UserSchema.parse(this.form.value);
|
|
321
|
+
console.log(payload);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### How errors surface in `HelixFormField`
|
|
327
|
+
|
|
328
|
+
`HelixFormField` reads `control.errors` and takes the first string value. Since `fromZod` stores the Zod error message as the value (e.g. `{ Email: 'Invalid email' }`), `activeError` picks it up with no adapter layer.
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
// helixFormErrorMap also works identically
|
|
332
|
+
import { helixFormErrorMap } from '@gravionlabs/helix-core';
|
|
333
|
+
const errors = helixFormErrorMap(this.form);
|
|
334
|
+
// → { email: 'Invalid email', name: 'Name is required' }
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## `UserSchema.shape` — Single Source of Truth
|
|
340
|
+
|
|
341
|
+
Using `UserSchema.shape.<field>` directly in `fromZod` eliminates duplicated validation rules between your API schema and your form:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
// Without shape — rules written twice
|
|
345
|
+
z.string().email() // API parsing
|
|
346
|
+
HelixValidators.email('...') // form (same rule, again)
|
|
347
|
+
|
|
348
|
+
// With shape — one definition drives both
|
|
349
|
+
HelixZodValidators.fromZod(UserSchema.shape.email)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
> Do **not** pass fields with `.transform()` (e.g. a Luxon `IsoDateTime` field) into form controls. Use the input-only (pre-transform) field shape instead, or define a separate schema without the transform.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Running Tests
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
# Run helix-zod tests
|
|
360
|
+
pnpm ng test zod
|
|
361
|
+
|
|
362
|
+
# Run all library tests
|
|
363
|
+
pnpm test:lib
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Tests are written with [Vitest](https://vitest.dev) and cover all mapped issue codes, `allowEmpty` behaviour, `fallbackKey` usage, and the dev/prod error modes.
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Architecture Notes
|
|
371
|
+
|
|
372
|
+
This library is the `@gravionlabs/helix-zod` portion of the broader Zod integration architecture documented in [`ZOD_ARCHITECTURE_HELIX.md`](../../ZOD_ARCHITECTURE_HELIX.md) at the repo root. The arch doc also covers:
|
|
373
|
+
|
|
374
|
+
- App-level patterns: domain schemas, `UserSchema.shape`, schema composition
|
|
375
|
+
- REST endpoint validation with `HttpClient` and `httpResource`
|
|
376
|
+
- Luxon `IsoDateTime` transform schema
|
|
377
|
+
- Environment config validation at startup
|
|
378
|
+
- Generic `zodFieldValidator` (framework-agnostic, no `HelixValidatorKey` dependency)
|
|
379
|
+
|
|
380
|
+
Those patterns live in your application (`src/app/schemas/`, `src/app/api/`, etc.) — this library provides only the Angular `ValidatorFn` bridge.
|
|
381
|
+
|
|
382
|
+
### Implementation vs Architecture Plan
|
|
383
|
+
|
|
384
|
+
The library fully implements the `helix-zod` scope defined in `ZOD_ARCHITECTURE_HELIX.md §6b`:
|
|
385
|
+
|
|
386
|
+
| Plan item | Status |
|
|
387
|
+
|---|---|
|
|
388
|
+
| `ZodHelixOptions` interface (`fallbackKey`, `allowEmpty`) | Implemented |
|
|
389
|
+
| `HelixZodValidators.fromZod()` factory | Implemented |
|
|
390
|
+
| Full Zod v4 issue → `HelixValidatorKey` mapping | Implemented |
|
|
391
|
+
| `allowEmpty = true` default mirroring `HelixValidators` | Implemented |
|
|
392
|
+
| `ngDevMode` throw on unmapped issue without `fallbackKey` | Implemented |
|
|
393
|
+
| Silent skip in production builds | Implemented |
|
|
394
|
+
| `z.array().min()` → `MinLength` | Implemented |
|
|
395
|
+
| App-level patterns (schemas, API services, Luxon) | Out of scope — application layer |
|