@kontsedal/olas-zod 0.0.1-rc.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 +21 -0
- package/README.md +83 -0
- package/dist/index.cjs +96 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +49 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +49 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +93 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +39 -0
- package/src/index.ts +179 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bohdan
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @kontsedal/olas-zod
|
|
2
|
+
|
|
3
|
+
Zod ↔ Olas forms adapter. Two helpers — `zodValidator` (single field) and `formFromZod` (whole form, inferred from schema).
|
|
4
|
+
|
|
5
|
+
Olas core stays Zod-free. This package has a peer dep on `zod ^3`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @kontsedal/olas-zod @kontsedal/olas-core zod
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 30-second example
|
|
14
|
+
|
|
15
|
+
### Single-field validator
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { defineController } from '@kontsedal/olas-core'
|
|
19
|
+
import { zodValidator } from '@kontsedal/olas-zod'
|
|
20
|
+
import { z } from 'zod'
|
|
21
|
+
|
|
22
|
+
const signup = defineController((ctx) => ({
|
|
23
|
+
email: ctx.field('', [zodValidator(z.string().email())]),
|
|
24
|
+
}))
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Whole form inferred from schema
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { formFromZod } from '@kontsedal/olas-zod'
|
|
31
|
+
import { z } from 'zod'
|
|
32
|
+
|
|
33
|
+
const schema = z.object({
|
|
34
|
+
name: z.string().min(1),
|
|
35
|
+
age: z.number().int().min(0),
|
|
36
|
+
address: z.object({
|
|
37
|
+
street: z.string().min(1),
|
|
38
|
+
city: z.string().min(1),
|
|
39
|
+
}),
|
|
40
|
+
tags: z.array(z.string().min(1)),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const profileForm = defineController((ctx) => ({
|
|
44
|
+
form: formFromZod(ctx, schema),
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
// form.value.value: { name: string; age: number; address: { street, city }; tags: string[] }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`formFromZod` walks the schema:
|
|
51
|
+
|
|
52
|
+
- `z.object(...)` → `Form<...>` (recurses).
|
|
53
|
+
- `z.array(...)` → `FieldArray<...>` (recurses on element type).
|
|
54
|
+
- Anything else → `Field<...>` with `zodValidator(...)` attached.
|
|
55
|
+
|
|
56
|
+
Each leaf's initial value comes from the Zod schema's `.default(...)` if present, otherwise the empty value for the type (`''` for string, `0` for number, etc.). Override per-field with the `initials` option.
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
function zodValidator<T>(schema: z.ZodType<T>): Validator<T>
|
|
62
|
+
function zodValidatorAsync<T>(schema: z.ZodType<T>): Validator<T>
|
|
63
|
+
|
|
64
|
+
function formFromZod<S extends z.ZodObject<z.ZodRawShape>>(
|
|
65
|
+
ctx: Ctx,
|
|
66
|
+
schema: S,
|
|
67
|
+
options?: FormOptions<...>,
|
|
68
|
+
): Form<...>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`zodValidator` runs `schema.safeParse(value)` and reports the first `ZodIssue`'s `message`. `zodValidatorAsync` awaits `.safeParseAsync(...)` for schemas with async `.refine` / `.transform`.
|
|
72
|
+
|
|
73
|
+
Form-level `.refine(...)` on the root `z.object(...)` is attached as a top-level form validator (surfaces via `form.topLevelErrors`).
|
|
74
|
+
|
|
75
|
+
## Limitation
|
|
76
|
+
|
|
77
|
+
Array-level `.min(N)` rules from the outer Zod schema are *not* promoted to a `FieldArray`-level validator today — leaf and nested-object rules walk correctly. Workaround: write a manual `FieldArrayValidator` for that case, or assert on `form.isValid` (driven by leaf rules). Tracked in [`../../BACKLOG.md`](../../BACKLOG.md).
|
|
78
|
+
|
|
79
|
+
## Further reading
|
|
80
|
+
|
|
81
|
+
- [`../../API.md`](../../API.md#olaszod) — full reference.
|
|
82
|
+
- [`../../.wiki/modules/zod.md`](../../.wiki/modules/zod.md)
|
|
83
|
+
- SPEC §8.7 (Zod integration), §20.7 (form types).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let zod = require("zod");
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wrap a Zod schema as an Olas validator. Returns a sync or async Validator
|
|
6
|
+
* depending on whether the schema requires async parsing (e.g. `.refine(async ...)`).
|
|
7
|
+
*/
|
|
8
|
+
function zodValidator(schema) {
|
|
9
|
+
return (value, signal) => {
|
|
10
|
+
const result = schema.safeParse(value);
|
|
11
|
+
if (result.success) return null;
|
|
12
|
+
return result.error.issues[0]?.message ?? "Invalid";
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Async variant for schemas with `.refine(async ...)` or `.transform(async ...)`.
|
|
17
|
+
* Returns a Promise<string | null>.
|
|
18
|
+
*/
|
|
19
|
+
function zodValidatorAsync(schema) {
|
|
20
|
+
return async (value, signal) => {
|
|
21
|
+
const result = await schema.safeParseAsync(value);
|
|
22
|
+
if (result.success) return null;
|
|
23
|
+
return result.error.issues[0]?.message ?? "Invalid";
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function unwrap(schema) {
|
|
27
|
+
let s = schema;
|
|
28
|
+
for (let i = 0; i < 5; i++) if (s instanceof zod.z.ZodDefault) s = s.def.innerType;
|
|
29
|
+
else if (s instanceof zod.z.ZodOptional) s = s.unwrap();
|
|
30
|
+
else if (s instanceof zod.z.ZodNullable) s = s.unwrap();
|
|
31
|
+
else return s;
|
|
32
|
+
return s;
|
|
33
|
+
}
|
|
34
|
+
function defaultInitial(schema) {
|
|
35
|
+
if (schema instanceof zod.z.ZodDefault) {
|
|
36
|
+
const raw = schema.def.defaultValue;
|
|
37
|
+
return typeof raw === "function" ? raw() : raw;
|
|
38
|
+
}
|
|
39
|
+
const inner = unwrap(schema);
|
|
40
|
+
if (inner instanceof zod.z.ZodString) return "";
|
|
41
|
+
if (inner instanceof zod.z.ZodNumber) return 0;
|
|
42
|
+
if (inner instanceof zod.z.ZodBoolean) return false;
|
|
43
|
+
if (inner instanceof zod.z.ZodArray) return [];
|
|
44
|
+
if (inner instanceof zod.z.ZodEnum) {
|
|
45
|
+
const first = inner.options[0];
|
|
46
|
+
return typeof first === "string" ? first : "";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Walk a Zod schema and emit the equivalent Olas Form / FieldArray / Field
|
|
51
|
+
* tree, with validators auto-attached.
|
|
52
|
+
*
|
|
53
|
+
* - `z.object(...)` → `Form`
|
|
54
|
+
* - `z.array(...)` → `FieldArray` (recurses on the element)
|
|
55
|
+
* - leaf schemas → `Field` with `zodValidator(...)` attached
|
|
56
|
+
*
|
|
57
|
+
* Each leaf's initial value is the Zod default if present, otherwise an empty
|
|
58
|
+
* value for that type (`''` for strings, `0` for numbers, etc.).
|
|
59
|
+
*
|
|
60
|
+
* The return type is structurally precise — `form.fields.title.value` is
|
|
61
|
+
* `string` (not `string | boolean | …`), `form.fields.subtasks.add(...)`
|
|
62
|
+
* accepts the exact item shape, etc. Consumers do not need to hand-write
|
|
63
|
+
* a `CardForm = Form<{...}>` matching the schema.
|
|
64
|
+
*/
|
|
65
|
+
function formFromZod(ctx, schema, options) {
|
|
66
|
+
return buildForm(ctx, schema, options?.initials);
|
|
67
|
+
}
|
|
68
|
+
function buildForm(ctx, schema, initials) {
|
|
69
|
+
const shape = schema.shape;
|
|
70
|
+
const fields = {};
|
|
71
|
+
for (const key of Object.keys(shape)) {
|
|
72
|
+
const propSchema = shape[key];
|
|
73
|
+
const initial = initials?.[key];
|
|
74
|
+
fields[key] = buildLeaf(ctx, propSchema, initial);
|
|
75
|
+
}
|
|
76
|
+
const formOpts = {};
|
|
77
|
+
const topLevelValidators = [];
|
|
78
|
+
if (topLevelValidators.length > 0) formOpts.validators = topLevelValidators;
|
|
79
|
+
return ctx.form(fields, formOpts);
|
|
80
|
+
}
|
|
81
|
+
function buildLeaf(ctx, schema, initial) {
|
|
82
|
+
const inner = unwrap(schema);
|
|
83
|
+
if (inner instanceof zod.z.ZodObject) return buildForm(ctx, inner, initial);
|
|
84
|
+
if (inner instanceof zod.z.ZodArray) {
|
|
85
|
+
const elementSchema = inner.element;
|
|
86
|
+
return ctx.fieldArray((itemInitial) => buildLeaf(ctx, elementSchema, itemInitial), initial !== void 0 ? { initial } : void 0);
|
|
87
|
+
}
|
|
88
|
+
const ini = initial !== void 0 ? initial : defaultInitial(schema);
|
|
89
|
+
return ctx.field(ini, [zodValidator(schema)]);
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
exports.formFromZod = formFromZod;
|
|
93
|
+
exports.zodValidator = zodValidator;
|
|
94
|
+
exports.zodValidatorAsync = zodValidatorAsync;
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["z"],"sources":["../src/index.ts"],"sourcesContent":["import type { Ctx, Field, FieldArray, Form, FormOptions, Validator } from '@kontsedal/olas-core'\nimport { z } from 'zod'\n\n/**\n * Wrap a Zod schema as an Olas validator. Returns a sync or async Validator\n * depending on whether the schema requires async parsing (e.g. `.refine(async ...)`).\n */\nexport function zodValidator<T>(schema: z.ZodType<T>): Validator<T> {\n return (value, signal) => {\n // signal isn't used by Zod (parsing is sync) — kept for interface parity.\n void signal\n const result = schema.safeParse(value)\n if (result.success) return null\n return result.error.issues[0]?.message ?? 'Invalid'\n }\n}\n\n/**\n * Async variant for schemas with `.refine(async ...)` or `.transform(async ...)`.\n * Returns a Promise<string | null>.\n */\nexport function zodValidatorAsync<T>(schema: z.ZodType<T>): Validator<T> {\n return async (value, signal) => {\n void signal\n const result = await schema.safeParseAsync(value)\n if (result.success) return null\n return result.error.issues[0]?.message ?? 'Invalid'\n }\n}\n\n// Zod 4 typed every wrapper as `z.ZodType`-compatible; the public unwrap path\n// is `.unwrap()` for optional/nullable and `.def.innerType` for default.\ntype AnyZodType = z.ZodType\n\n// Strip the outer optional/nullable/default wrappers to find the inner schema.\nfunction unwrap(schema: AnyZodType): AnyZodType {\n let s: AnyZodType = schema\n // Unwrap default + optional + nullable, in any combination.\n for (let i = 0; i < 5; i++) {\n if (s instanceof z.ZodDefault) {\n // ZodDefault stores the inner schema on `def.innerType`. The runtime\n // shape is stable across 3.x and 4.x; the public type just shifts.\n s = (s as unknown as { def: { innerType: AnyZodType } }).def.innerType\n } else if (s instanceof z.ZodOptional) {\n s = (s as z.ZodOptional<AnyZodType>).unwrap() as AnyZodType\n } else if (s instanceof z.ZodNullable) {\n s = (s as z.ZodNullable<AnyZodType>).unwrap() as AnyZodType\n } else {\n return s\n }\n }\n return s\n}\n\nfunction defaultInitial(schema: AnyZodType): unknown {\n // Honor Zod default if present.\n if (schema instanceof z.ZodDefault) {\n const raw = (schema as unknown as { def: { defaultValue: unknown } }).def.defaultValue\n return typeof raw === 'function' ? (raw as () => unknown)() : raw\n }\n const inner = unwrap(schema)\n if (inner instanceof z.ZodString) return ''\n if (inner instanceof z.ZodNumber) return 0\n if (inner instanceof z.ZodBoolean) return false\n if (inner instanceof z.ZodArray) return []\n if (inner instanceof z.ZodEnum) {\n // Zod 4 widened ZodEnum's options to support record-style enums. The\n // runtime values are still iterable; pick the first.\n const opts = (inner as unknown as { options: readonly unknown[] }).options\n const first = opts[0]\n return typeof first === 'string' ? first : ''\n }\n // For unknown/any/dates etc., undefined is the safest starting point.\n return undefined\n}\n\ntype AnyForm = Form<Record<string, Field<any> | Form<any> | FieldArray<any>>>\n\n// Strip the same wrappers as the runtime `unwrap` helper, at the type level.\ntype UnwrapZod<S> =\n S extends z.ZodDefault<infer Inner>\n ? UnwrapZod<Inner>\n : S extends z.ZodOptional<infer Inner>\n ? UnwrapZod<Inner>\n : S extends z.ZodNullable<infer Inner>\n ? UnwrapZod<Inner>\n : S\n\n/**\n * Recursively map a Zod schema to its Olas form leaf:\n * - `ZodObject<S>` → `Form<{ [K]: ZodToLeaf<S[K]> }>`\n * - `ZodArray<E>` → `FieldArray<ZodToLeaf<E>>` (when E is object/array)\n * or `FieldArray<Field<infer<E>>>` for primitive elements.\n * - everything else → `Field<infer<S>>`.\n *\n * `ZodToLeaf<S>` matches what `buildLeaf(ctx, s, ...)` returns at runtime,\n * so the public `formFromZod<T>` can publish a precise structural type\n * without the consumer needing a hand-written `CardForm = Form<{...}>` cast.\n */\nexport type ZodToLeaf<S> =\n UnwrapZod<S> extends z.ZodObject<infer RawShape>\n ? Form<{ [K in keyof RawShape]: ZodToLeaf<RawShape[K]> }>\n : UnwrapZod<S> extends z.ZodArray<infer Element>\n ? FieldArray<ZodToLeaf<Element> extends Form<any> | Field<any> ? ZodToLeaf<Element> : never>\n : Field<z.infer<UnwrapZod<S> & z.ZodType>>\n\n/**\n * Walk a Zod schema and emit the equivalent Olas Form / FieldArray / Field\n * tree, with validators auto-attached.\n *\n * - `z.object(...)` → `Form`\n * - `z.array(...)` → `FieldArray` (recurses on the element)\n * - leaf schemas → `Field` with `zodValidator(...)` attached\n *\n * Each leaf's initial value is the Zod default if present, otherwise an empty\n * value for that type (`''` for strings, `0` for numbers, etc.).\n *\n * The return type is structurally precise — `form.fields.title.value` is\n * `string` (not `string | boolean | …`), `form.fields.subtasks.add(...)`\n * accepts the exact item shape, etc. Consumers do not need to hand-write\n * a `CardForm = Form<{...}>` matching the schema.\n */\nexport function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(\n ctx: Ctx,\n schema: T,\n options?: { initials?: Partial<z.infer<T>> },\n): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }> {\n return buildForm(ctx, schema, options?.initials) as never\n}\n\nfunction buildForm(\n ctx: Ctx,\n schema: z.ZodObject<z.ZodRawShape>,\n initials?: Record<string, unknown>,\n): AnyForm {\n const shape = schema.shape\n const fields: Record<string, Field<unknown> | Form<any> | FieldArray<any>> = {}\n for (const key of Object.keys(shape)) {\n const propSchema = shape[key] as AnyZodType\n const initial = initials?.[key]\n fields[key] = buildLeaf(ctx, propSchema, initial)\n }\n const formOpts: FormOptions<typeof fields> = {}\n // If the schema has top-level refinements (z.object().refine(...)), Zod\n // wraps it in ZodEffects. We expose those as form-level validators.\n const topLevelValidators: Validator<unknown>[] = []\n // Note: we received an unwrapped ZodObject here, so there's nothing extra.\n if (topLevelValidators.length > 0) {\n ;(formOpts as { validators: Validator<unknown>[] }).validators = topLevelValidators\n }\n return ctx.form(fields, formOpts) as AnyForm\n}\n\nfunction buildLeaf(\n ctx: Ctx,\n schema: AnyZodType,\n initial: unknown,\n): Field<unknown> | Form<any> | FieldArray<any> {\n const inner = unwrap(schema)\n\n if (inner instanceof z.ZodObject) {\n return buildForm(\n ctx,\n inner as z.ZodObject<z.ZodRawShape>,\n initial as Record<string, unknown> | undefined,\n )\n }\n\n if (inner instanceof z.ZodArray) {\n const elementSchema = (inner as z.ZodArray<AnyZodType>).element as AnyZodType\n return ctx.fieldArray(\n (itemInitial) => buildLeaf(ctx, elementSchema, itemInitial) as Field<unknown> | Form<any>,\n initial !== undefined ? { initial: initial as Array<unknown> } : undefined,\n )\n }\n\n const ini = initial !== undefined ? initial : defaultInitial(schema)\n return ctx.field(ini, [zodValidator(schema as z.ZodType<unknown>)])\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,aAAgB,QAAoC;CAClE,QAAQ,OAAO,WAAW;EAGxB,MAAM,SAAS,OAAO,UAAU,KAAK;EACrC,IAAI,OAAO,SAAS,OAAO;EAC3B,OAAO,OAAO,MAAM,OAAO,IAAI,WAAW;CAC5C;AACF;;;;;AAMA,SAAgB,kBAAqB,QAAoC;CACvE,OAAO,OAAO,OAAO,WAAW;EAE9B,MAAM,SAAS,MAAM,OAAO,eAAe,KAAK;EAChD,IAAI,OAAO,SAAS,OAAO;EAC3B,OAAO,OAAO,MAAM,OAAO,IAAI,WAAW;CAC5C;AACF;AAOA,SAAS,OAAO,QAAgC;CAC9C,IAAI,IAAgB;CAEpB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,IAAI,aAAaA,IAAAA,EAAE,YAGjB,IAAK,EAAoD,IAAI;MACxD,IAAI,aAAaA,IAAAA,EAAE,aACxB,IAAK,EAAgC,OAAO;MACvC,IAAI,aAAaA,IAAAA,EAAE,aACxB,IAAK,EAAgC,OAAO;MAE5C,OAAO;CAGX,OAAO;AACT;AAEA,SAAS,eAAe,QAA6B;CAEnD,IAAI,kBAAkBA,IAAAA,EAAE,YAAY;EAClC,MAAM,MAAO,OAAyD,IAAI;EAC1E,OAAO,OAAO,QAAQ,aAAc,IAAsB,IAAI;CAChE;CACA,MAAM,QAAQ,OAAO,MAAM;CAC3B,IAAI,iBAAiBA,IAAAA,EAAE,WAAW,OAAO;CACzC,IAAI,iBAAiBA,IAAAA,EAAE,WAAW,OAAO;CACzC,IAAI,iBAAiBA,IAAAA,EAAE,YAAY,OAAO;CAC1C,IAAI,iBAAiBA,IAAAA,EAAE,UAAU,OAAO,CAAC;CACzC,IAAI,iBAAiBA,IAAAA,EAAE,SAAS;EAI9B,MAAM,QADQ,MAAqD,QAChD;EACnB,OAAO,OAAO,UAAU,WAAW,QAAQ;CAC7C;AAGF;;;;;;;;;;;;;;;;;AAgDA,SAAgB,YACd,KACA,QACA,SAC6D;CAC7D,OAAO,UAAU,KAAK,QAAQ,SAAS,QAAQ;AACjD;AAEA,SAAS,UACP,KACA,QACA,UACS;CACT,MAAM,QAAQ,OAAO;CACrB,MAAM,SAAuE,CAAC;CAC9E,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAG;EACpC,MAAM,aAAa,MAAM;EACzB,MAAM,UAAU,WAAW;EAC3B,OAAO,OAAO,UAAU,KAAK,YAAY,OAAO;CAClD;CACA,MAAM,WAAuC,CAAC;CAG9C,MAAM,qBAA2C,CAAC;CAElD,IAAI,mBAAmB,SAAS,GAC7B,SAAmD,aAAa;CAEnE,OAAO,IAAI,KAAK,QAAQ,QAAQ;AAClC;AAEA,SAAS,UACP,KACA,QACA,SAC8C;CAC9C,MAAM,QAAQ,OAAO,MAAM;CAE3B,IAAI,iBAAiBA,IAAAA,EAAE,WACrB,OAAO,UACL,KACA,OACA,OACF;CAGF,IAAI,iBAAiBA,IAAAA,EAAE,UAAU;EAC/B,MAAM,gBAAiB,MAAiC;EACxD,OAAO,IAAI,YACR,gBAAgB,UAAU,KAAK,eAAe,WAAW,GAC1D,YAAY,KAAA,IAAY,EAAW,QAA0B,IAAI,KAAA,CACnE;CACF;CAEA,MAAM,MAAM,YAAY,KAAA,IAAY,UAAU,eAAe,MAAM;CACnE,OAAO,IAAI,MAAM,KAAK,CAAC,aAAa,MAA4B,CAAC,CAAC;AACpE"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Ctx, Field, FieldArray, Form, Validator } from "@kontsedal/olas-core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/index.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Wrap a Zod schema as an Olas validator. Returns a sync or async Validator
|
|
7
|
+
* depending on whether the schema requires async parsing (e.g. `.refine(async ...)`).
|
|
8
|
+
*/
|
|
9
|
+
declare function zodValidator<T>(schema: z.ZodType<T>): Validator<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Async variant for schemas with `.refine(async ...)` or `.transform(async ...)`.
|
|
12
|
+
* Returns a Promise<string | null>.
|
|
13
|
+
*/
|
|
14
|
+
declare function zodValidatorAsync<T>(schema: z.ZodType<T>): Validator<T>;
|
|
15
|
+
type UnwrapZod<S> = S extends z.ZodDefault<infer Inner> ? UnwrapZod<Inner> : S extends z.ZodOptional<infer Inner> ? UnwrapZod<Inner> : S extends z.ZodNullable<infer Inner> ? UnwrapZod<Inner> : S;
|
|
16
|
+
/**
|
|
17
|
+
* Recursively map a Zod schema to its Olas form leaf:
|
|
18
|
+
* - `ZodObject<S>` → `Form<{ [K]: ZodToLeaf<S[K]> }>`
|
|
19
|
+
* - `ZodArray<E>` → `FieldArray<ZodToLeaf<E>>` (when E is object/array)
|
|
20
|
+
* or `FieldArray<Field<infer<E>>>` for primitive elements.
|
|
21
|
+
* - everything else → `Field<infer<S>>`.
|
|
22
|
+
*
|
|
23
|
+
* `ZodToLeaf<S>` matches what `buildLeaf(ctx, s, ...)` returns at runtime,
|
|
24
|
+
* so the public `formFromZod<T>` can publish a precise structural type
|
|
25
|
+
* without the consumer needing a hand-written `CardForm = Form<{...}>` cast.
|
|
26
|
+
*/
|
|
27
|
+
type ZodToLeaf<S> = UnwrapZod<S> extends z.ZodObject<infer RawShape> ? Form<{ [K in keyof RawShape]: ZodToLeaf<RawShape[K]> }> : UnwrapZod<S> extends z.ZodArray<infer Element> ? FieldArray<ZodToLeaf<Element> extends Form<any> | Field<any> ? ZodToLeaf<Element> : never> : Field<z.infer<UnwrapZod<S> & z.ZodType>>;
|
|
28
|
+
/**
|
|
29
|
+
* Walk a Zod schema and emit the equivalent Olas Form / FieldArray / Field
|
|
30
|
+
* tree, with validators auto-attached.
|
|
31
|
+
*
|
|
32
|
+
* - `z.object(...)` → `Form`
|
|
33
|
+
* - `z.array(...)` → `FieldArray` (recurses on the element)
|
|
34
|
+
* - leaf schemas → `Field` with `zodValidator(...)` attached
|
|
35
|
+
*
|
|
36
|
+
* Each leaf's initial value is the Zod default if present, otherwise an empty
|
|
37
|
+
* value for that type (`''` for strings, `0` for numbers, etc.).
|
|
38
|
+
*
|
|
39
|
+
* The return type is structurally precise — `form.fields.title.value` is
|
|
40
|
+
* `string` (not `string | boolean | …`), `form.fields.subtasks.add(...)`
|
|
41
|
+
* accepts the exact item shape, etc. Consumers do not need to hand-write
|
|
42
|
+
* a `CardForm = Form<{...}>` matching the schema.
|
|
43
|
+
*/
|
|
44
|
+
declare function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(ctx: Ctx, schema: T, options?: {
|
|
45
|
+
initials?: Partial<z.infer<T>>;
|
|
46
|
+
}): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }>;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { ZodToLeaf, formFromZod, zodValidator, zodValidatorAsync };
|
|
49
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;AAOA;;iBAAgB,YAAA,GAAA,CAAgB,MAAA,EAAQ,CAAA,CAAE,OAAA,CAAQ,CAAA,IAAK,SAAA,CAAU,CAAA;;;;;iBAcjD,iBAAA,GAAA,CAAqB,MAAA,EAAQ,CAAA,CAAE,OAAA,CAAQ,CAAA,IAAK,SAAA,CAAU,CAAA;AAAA,KA0DjE,SAAA,MACH,CAAA,SAAU,CAAA,CAAE,UAAA,gBACR,SAAA,CAAU,KAAA,IACV,CAAA,SAAU,CAAA,CAAE,WAAA,gBACV,SAAA,CAAU,KAAA,IACV,CAAA,SAAU,CAAA,CAAE,WAAA,gBACV,SAAA,CAAU,KAAA,IACV,CAAA;;;;;;;;;AA/EwD;AAclE;;KA8EY,SAAA,MACV,SAAA,CAAU,CAAA,UAAW,CAAA,CAAE,SAAA,mBACnB,IAAA,eAAmB,QAAA,GAAW,SAAA,CAAU,QAAA,CAAS,CAAA,QACjD,SAAA,CAAU,CAAA,UAAW,CAAA,CAAE,QAAA,kBACrB,UAAA,CAAW,SAAA,CAAU,OAAA,UAAiB,IAAA,QAAY,KAAA,QAAa,SAAA,CAAU,OAAA,aACzE,KAAA,CAAM,CAAA,CAAE,KAAA,CAAM,SAAA,CAAU,CAAA,IAAK,CAAA,CAAE,OAAA;;;;;;;;;;;;;AAnFgC;AAOtE;;;iBA8Fe,WAAA,WAAsB,CAAA,CAAE,SAAA,CAAU,CAAA,CAAE,WAAA,EAAA,CAClD,GAAA,EAAK,GAAA,EACL,MAAA,EAAQ,CAAA,EACR,OAAA;EAAY,QAAA,GAAW,OAAA,CAAQ,CAAA,CAAE,KAAA,CAAM,CAAA;AAAA,IACtC,IAAA,eAAmB,CAAA,YAAa,SAAA,CAAU,CAAA,UAAW,CAAA"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Ctx, Field, FieldArray, Form, Validator } from "@kontsedal/olas-core";
|
|
3
|
+
|
|
4
|
+
//#region src/index.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Wrap a Zod schema as an Olas validator. Returns a sync or async Validator
|
|
7
|
+
* depending on whether the schema requires async parsing (e.g. `.refine(async ...)`).
|
|
8
|
+
*/
|
|
9
|
+
declare function zodValidator<T>(schema: z.ZodType<T>): Validator<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Async variant for schemas with `.refine(async ...)` or `.transform(async ...)`.
|
|
12
|
+
* Returns a Promise<string | null>.
|
|
13
|
+
*/
|
|
14
|
+
declare function zodValidatorAsync<T>(schema: z.ZodType<T>): Validator<T>;
|
|
15
|
+
type UnwrapZod<S> = S extends z.ZodDefault<infer Inner> ? UnwrapZod<Inner> : S extends z.ZodOptional<infer Inner> ? UnwrapZod<Inner> : S extends z.ZodNullable<infer Inner> ? UnwrapZod<Inner> : S;
|
|
16
|
+
/**
|
|
17
|
+
* Recursively map a Zod schema to its Olas form leaf:
|
|
18
|
+
* - `ZodObject<S>` → `Form<{ [K]: ZodToLeaf<S[K]> }>`
|
|
19
|
+
* - `ZodArray<E>` → `FieldArray<ZodToLeaf<E>>` (when E is object/array)
|
|
20
|
+
* or `FieldArray<Field<infer<E>>>` for primitive elements.
|
|
21
|
+
* - everything else → `Field<infer<S>>`.
|
|
22
|
+
*
|
|
23
|
+
* `ZodToLeaf<S>` matches what `buildLeaf(ctx, s, ...)` returns at runtime,
|
|
24
|
+
* so the public `formFromZod<T>` can publish a precise structural type
|
|
25
|
+
* without the consumer needing a hand-written `CardForm = Form<{...}>` cast.
|
|
26
|
+
*/
|
|
27
|
+
type ZodToLeaf<S> = UnwrapZod<S> extends z.ZodObject<infer RawShape> ? Form<{ [K in keyof RawShape]: ZodToLeaf<RawShape[K]> }> : UnwrapZod<S> extends z.ZodArray<infer Element> ? FieldArray<ZodToLeaf<Element> extends Form<any> | Field<any> ? ZodToLeaf<Element> : never> : Field<z.infer<UnwrapZod<S> & z.ZodType>>;
|
|
28
|
+
/**
|
|
29
|
+
* Walk a Zod schema and emit the equivalent Olas Form / FieldArray / Field
|
|
30
|
+
* tree, with validators auto-attached.
|
|
31
|
+
*
|
|
32
|
+
* - `z.object(...)` → `Form`
|
|
33
|
+
* - `z.array(...)` → `FieldArray` (recurses on the element)
|
|
34
|
+
* - leaf schemas → `Field` with `zodValidator(...)` attached
|
|
35
|
+
*
|
|
36
|
+
* Each leaf's initial value is the Zod default if present, otherwise an empty
|
|
37
|
+
* value for that type (`''` for strings, `0` for numbers, etc.).
|
|
38
|
+
*
|
|
39
|
+
* The return type is structurally precise — `form.fields.title.value` is
|
|
40
|
+
* `string` (not `string | boolean | …`), `form.fields.subtasks.add(...)`
|
|
41
|
+
* accepts the exact item shape, etc. Consumers do not need to hand-write
|
|
42
|
+
* a `CardForm = Form<{...}>` matching the schema.
|
|
43
|
+
*/
|
|
44
|
+
declare function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(ctx: Ctx, schema: T, options?: {
|
|
45
|
+
initials?: Partial<z.infer<T>>;
|
|
46
|
+
}): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }>;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { ZodToLeaf, formFromZod, zodValidator, zodValidatorAsync };
|
|
49
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;AAOA;;iBAAgB,YAAA,GAAA,CAAgB,MAAA,EAAQ,CAAA,CAAE,OAAA,CAAQ,CAAA,IAAK,SAAA,CAAU,CAAA;;;;;iBAcjD,iBAAA,GAAA,CAAqB,MAAA,EAAQ,CAAA,CAAE,OAAA,CAAQ,CAAA,IAAK,SAAA,CAAU,CAAA;AAAA,KA0DjE,SAAA,MACH,CAAA,SAAU,CAAA,CAAE,UAAA,gBACR,SAAA,CAAU,KAAA,IACV,CAAA,SAAU,CAAA,CAAE,WAAA,gBACV,SAAA,CAAU,KAAA,IACV,CAAA,SAAU,CAAA,CAAE,WAAA,gBACV,SAAA,CAAU,KAAA,IACV,CAAA;;;;;;;;;AA/EwD;AAclE;;KA8EY,SAAA,MACV,SAAA,CAAU,CAAA,UAAW,CAAA,CAAE,SAAA,mBACnB,IAAA,eAAmB,QAAA,GAAW,SAAA,CAAU,QAAA,CAAS,CAAA,QACjD,SAAA,CAAU,CAAA,UAAW,CAAA,CAAE,QAAA,kBACrB,UAAA,CAAW,SAAA,CAAU,OAAA,UAAiB,IAAA,QAAY,KAAA,QAAa,SAAA,CAAU,OAAA,aACzE,KAAA,CAAM,CAAA,CAAE,KAAA,CAAM,SAAA,CAAU,CAAA,IAAK,CAAA,CAAE,OAAA;;;;;;;;;;;;;AAnFgC;AAOtE;;;iBA8Fe,WAAA,WAAsB,CAAA,CAAE,SAAA,CAAU,CAAA,CAAE,WAAA,EAAA,CAClD,GAAA,EAAK,GAAA,EACL,MAAA,EAAQ,CAAA,EACR,OAAA;EAAY,QAAA,GAAW,OAAA,CAAQ,CAAA,CAAE,KAAA,CAAM,CAAA;AAAA,IACtC,IAAA,eAAmB,CAAA,YAAa,SAAA,CAAU,CAAA,UAAW,CAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* Wrap a Zod schema as an Olas validator. Returns a sync or async Validator
|
|
5
|
+
* depending on whether the schema requires async parsing (e.g. `.refine(async ...)`).
|
|
6
|
+
*/
|
|
7
|
+
function zodValidator(schema) {
|
|
8
|
+
return (value, signal) => {
|
|
9
|
+
const result = schema.safeParse(value);
|
|
10
|
+
if (result.success) return null;
|
|
11
|
+
return result.error.issues[0]?.message ?? "Invalid";
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Async variant for schemas with `.refine(async ...)` or `.transform(async ...)`.
|
|
16
|
+
* Returns a Promise<string | null>.
|
|
17
|
+
*/
|
|
18
|
+
function zodValidatorAsync(schema) {
|
|
19
|
+
return async (value, signal) => {
|
|
20
|
+
const result = await schema.safeParseAsync(value);
|
|
21
|
+
if (result.success) return null;
|
|
22
|
+
return result.error.issues[0]?.message ?? "Invalid";
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function unwrap(schema) {
|
|
26
|
+
let s = schema;
|
|
27
|
+
for (let i = 0; i < 5; i++) if (s instanceof z.ZodDefault) s = s.def.innerType;
|
|
28
|
+
else if (s instanceof z.ZodOptional) s = s.unwrap();
|
|
29
|
+
else if (s instanceof z.ZodNullable) s = s.unwrap();
|
|
30
|
+
else return s;
|
|
31
|
+
return s;
|
|
32
|
+
}
|
|
33
|
+
function defaultInitial(schema) {
|
|
34
|
+
if (schema instanceof z.ZodDefault) {
|
|
35
|
+
const raw = schema.def.defaultValue;
|
|
36
|
+
return typeof raw === "function" ? raw() : raw;
|
|
37
|
+
}
|
|
38
|
+
const inner = unwrap(schema);
|
|
39
|
+
if (inner instanceof z.ZodString) return "";
|
|
40
|
+
if (inner instanceof z.ZodNumber) return 0;
|
|
41
|
+
if (inner instanceof z.ZodBoolean) return false;
|
|
42
|
+
if (inner instanceof z.ZodArray) return [];
|
|
43
|
+
if (inner instanceof z.ZodEnum) {
|
|
44
|
+
const first = inner.options[0];
|
|
45
|
+
return typeof first === "string" ? first : "";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Walk a Zod schema and emit the equivalent Olas Form / FieldArray / Field
|
|
50
|
+
* tree, with validators auto-attached.
|
|
51
|
+
*
|
|
52
|
+
* - `z.object(...)` → `Form`
|
|
53
|
+
* - `z.array(...)` → `FieldArray` (recurses on the element)
|
|
54
|
+
* - leaf schemas → `Field` with `zodValidator(...)` attached
|
|
55
|
+
*
|
|
56
|
+
* Each leaf's initial value is the Zod default if present, otherwise an empty
|
|
57
|
+
* value for that type (`''` for strings, `0` for numbers, etc.).
|
|
58
|
+
*
|
|
59
|
+
* The return type is structurally precise — `form.fields.title.value` is
|
|
60
|
+
* `string` (not `string | boolean | …`), `form.fields.subtasks.add(...)`
|
|
61
|
+
* accepts the exact item shape, etc. Consumers do not need to hand-write
|
|
62
|
+
* a `CardForm = Form<{...}>` matching the schema.
|
|
63
|
+
*/
|
|
64
|
+
function formFromZod(ctx, schema, options) {
|
|
65
|
+
return buildForm(ctx, schema, options?.initials);
|
|
66
|
+
}
|
|
67
|
+
function buildForm(ctx, schema, initials) {
|
|
68
|
+
const shape = schema.shape;
|
|
69
|
+
const fields = {};
|
|
70
|
+
for (const key of Object.keys(shape)) {
|
|
71
|
+
const propSchema = shape[key];
|
|
72
|
+
const initial = initials?.[key];
|
|
73
|
+
fields[key] = buildLeaf(ctx, propSchema, initial);
|
|
74
|
+
}
|
|
75
|
+
const formOpts = {};
|
|
76
|
+
const topLevelValidators = [];
|
|
77
|
+
if (topLevelValidators.length > 0) formOpts.validators = topLevelValidators;
|
|
78
|
+
return ctx.form(fields, formOpts);
|
|
79
|
+
}
|
|
80
|
+
function buildLeaf(ctx, schema, initial) {
|
|
81
|
+
const inner = unwrap(schema);
|
|
82
|
+
if (inner instanceof z.ZodObject) return buildForm(ctx, inner, initial);
|
|
83
|
+
if (inner instanceof z.ZodArray) {
|
|
84
|
+
const elementSchema = inner.element;
|
|
85
|
+
return ctx.fieldArray((itemInitial) => buildLeaf(ctx, elementSchema, itemInitial), initial !== void 0 ? { initial } : void 0);
|
|
86
|
+
}
|
|
87
|
+
const ini = initial !== void 0 ? initial : defaultInitial(schema);
|
|
88
|
+
return ctx.field(ini, [zodValidator(schema)]);
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
export { formFromZod, zodValidator, zodValidatorAsync };
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Ctx, Field, FieldArray, Form, FormOptions, Validator } from '@kontsedal/olas-core'\nimport { z } from 'zod'\n\n/**\n * Wrap a Zod schema as an Olas validator. Returns a sync or async Validator\n * depending on whether the schema requires async parsing (e.g. `.refine(async ...)`).\n */\nexport function zodValidator<T>(schema: z.ZodType<T>): Validator<T> {\n return (value, signal) => {\n // signal isn't used by Zod (parsing is sync) — kept for interface parity.\n void signal\n const result = schema.safeParse(value)\n if (result.success) return null\n return result.error.issues[0]?.message ?? 'Invalid'\n }\n}\n\n/**\n * Async variant for schemas with `.refine(async ...)` or `.transform(async ...)`.\n * Returns a Promise<string | null>.\n */\nexport function zodValidatorAsync<T>(schema: z.ZodType<T>): Validator<T> {\n return async (value, signal) => {\n void signal\n const result = await schema.safeParseAsync(value)\n if (result.success) return null\n return result.error.issues[0]?.message ?? 'Invalid'\n }\n}\n\n// Zod 4 typed every wrapper as `z.ZodType`-compatible; the public unwrap path\n// is `.unwrap()` for optional/nullable and `.def.innerType` for default.\ntype AnyZodType = z.ZodType\n\n// Strip the outer optional/nullable/default wrappers to find the inner schema.\nfunction unwrap(schema: AnyZodType): AnyZodType {\n let s: AnyZodType = schema\n // Unwrap default + optional + nullable, in any combination.\n for (let i = 0; i < 5; i++) {\n if (s instanceof z.ZodDefault) {\n // ZodDefault stores the inner schema on `def.innerType`. The runtime\n // shape is stable across 3.x and 4.x; the public type just shifts.\n s = (s as unknown as { def: { innerType: AnyZodType } }).def.innerType\n } else if (s instanceof z.ZodOptional) {\n s = (s as z.ZodOptional<AnyZodType>).unwrap() as AnyZodType\n } else if (s instanceof z.ZodNullable) {\n s = (s as z.ZodNullable<AnyZodType>).unwrap() as AnyZodType\n } else {\n return s\n }\n }\n return s\n}\n\nfunction defaultInitial(schema: AnyZodType): unknown {\n // Honor Zod default if present.\n if (schema instanceof z.ZodDefault) {\n const raw = (schema as unknown as { def: { defaultValue: unknown } }).def.defaultValue\n return typeof raw === 'function' ? (raw as () => unknown)() : raw\n }\n const inner = unwrap(schema)\n if (inner instanceof z.ZodString) return ''\n if (inner instanceof z.ZodNumber) return 0\n if (inner instanceof z.ZodBoolean) return false\n if (inner instanceof z.ZodArray) return []\n if (inner instanceof z.ZodEnum) {\n // Zod 4 widened ZodEnum's options to support record-style enums. The\n // runtime values are still iterable; pick the first.\n const opts = (inner as unknown as { options: readonly unknown[] }).options\n const first = opts[0]\n return typeof first === 'string' ? first : ''\n }\n // For unknown/any/dates etc., undefined is the safest starting point.\n return undefined\n}\n\ntype AnyForm = Form<Record<string, Field<any> | Form<any> | FieldArray<any>>>\n\n// Strip the same wrappers as the runtime `unwrap` helper, at the type level.\ntype UnwrapZod<S> =\n S extends z.ZodDefault<infer Inner>\n ? UnwrapZod<Inner>\n : S extends z.ZodOptional<infer Inner>\n ? UnwrapZod<Inner>\n : S extends z.ZodNullable<infer Inner>\n ? UnwrapZod<Inner>\n : S\n\n/**\n * Recursively map a Zod schema to its Olas form leaf:\n * - `ZodObject<S>` → `Form<{ [K]: ZodToLeaf<S[K]> }>`\n * - `ZodArray<E>` → `FieldArray<ZodToLeaf<E>>` (when E is object/array)\n * or `FieldArray<Field<infer<E>>>` for primitive elements.\n * - everything else → `Field<infer<S>>`.\n *\n * `ZodToLeaf<S>` matches what `buildLeaf(ctx, s, ...)` returns at runtime,\n * so the public `formFromZod<T>` can publish a precise structural type\n * without the consumer needing a hand-written `CardForm = Form<{...}>` cast.\n */\nexport type ZodToLeaf<S> =\n UnwrapZod<S> extends z.ZodObject<infer RawShape>\n ? Form<{ [K in keyof RawShape]: ZodToLeaf<RawShape[K]> }>\n : UnwrapZod<S> extends z.ZodArray<infer Element>\n ? FieldArray<ZodToLeaf<Element> extends Form<any> | Field<any> ? ZodToLeaf<Element> : never>\n : Field<z.infer<UnwrapZod<S> & z.ZodType>>\n\n/**\n * Walk a Zod schema and emit the equivalent Olas Form / FieldArray / Field\n * tree, with validators auto-attached.\n *\n * - `z.object(...)` → `Form`\n * - `z.array(...)` → `FieldArray` (recurses on the element)\n * - leaf schemas → `Field` with `zodValidator(...)` attached\n *\n * Each leaf's initial value is the Zod default if present, otherwise an empty\n * value for that type (`''` for strings, `0` for numbers, etc.).\n *\n * The return type is structurally precise — `form.fields.title.value` is\n * `string` (not `string | boolean | …`), `form.fields.subtasks.add(...)`\n * accepts the exact item shape, etc. Consumers do not need to hand-write\n * a `CardForm = Form<{...}>` matching the schema.\n */\nexport function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(\n ctx: Ctx,\n schema: T,\n options?: { initials?: Partial<z.infer<T>> },\n): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }> {\n return buildForm(ctx, schema, options?.initials) as never\n}\n\nfunction buildForm(\n ctx: Ctx,\n schema: z.ZodObject<z.ZodRawShape>,\n initials?: Record<string, unknown>,\n): AnyForm {\n const shape = schema.shape\n const fields: Record<string, Field<unknown> | Form<any> | FieldArray<any>> = {}\n for (const key of Object.keys(shape)) {\n const propSchema = shape[key] as AnyZodType\n const initial = initials?.[key]\n fields[key] = buildLeaf(ctx, propSchema, initial)\n }\n const formOpts: FormOptions<typeof fields> = {}\n // If the schema has top-level refinements (z.object().refine(...)), Zod\n // wraps it in ZodEffects. We expose those as form-level validators.\n const topLevelValidators: Validator<unknown>[] = []\n // Note: we received an unwrapped ZodObject here, so there's nothing extra.\n if (topLevelValidators.length > 0) {\n ;(formOpts as { validators: Validator<unknown>[] }).validators = topLevelValidators\n }\n return ctx.form(fields, formOpts) as AnyForm\n}\n\nfunction buildLeaf(\n ctx: Ctx,\n schema: AnyZodType,\n initial: unknown,\n): Field<unknown> | Form<any> | FieldArray<any> {\n const inner = unwrap(schema)\n\n if (inner instanceof z.ZodObject) {\n return buildForm(\n ctx,\n inner as z.ZodObject<z.ZodRawShape>,\n initial as Record<string, unknown> | undefined,\n )\n }\n\n if (inner instanceof z.ZodArray) {\n const elementSchema = (inner as z.ZodArray<AnyZodType>).element as AnyZodType\n return ctx.fieldArray(\n (itemInitial) => buildLeaf(ctx, elementSchema, itemInitial) as Field<unknown> | Form<any>,\n initial !== undefined ? { initial: initial as Array<unknown> } : undefined,\n )\n }\n\n const ini = initial !== undefined ? initial : defaultInitial(schema)\n return ctx.field(ini, [zodValidator(schema as z.ZodType<unknown>)])\n}\n"],"mappings":";;;;;;AAOA,SAAgB,aAAgB,QAAoC;CAClE,QAAQ,OAAO,WAAW;EAGxB,MAAM,SAAS,OAAO,UAAU,KAAK;EACrC,IAAI,OAAO,SAAS,OAAO;EAC3B,OAAO,OAAO,MAAM,OAAO,IAAI,WAAW;CAC5C;AACF;;;;;AAMA,SAAgB,kBAAqB,QAAoC;CACvE,OAAO,OAAO,OAAO,WAAW;EAE9B,MAAM,SAAS,MAAM,OAAO,eAAe,KAAK;EAChD,IAAI,OAAO,SAAS,OAAO;EAC3B,OAAO,OAAO,MAAM,OAAO,IAAI,WAAW;CAC5C;AACF;AAOA,SAAS,OAAO,QAAgC;CAC9C,IAAI,IAAgB;CAEpB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,IAAI,aAAa,EAAE,YAGjB,IAAK,EAAoD,IAAI;MACxD,IAAI,aAAa,EAAE,aACxB,IAAK,EAAgC,OAAO;MACvC,IAAI,aAAa,EAAE,aACxB,IAAK,EAAgC,OAAO;MAE5C,OAAO;CAGX,OAAO;AACT;AAEA,SAAS,eAAe,QAA6B;CAEnD,IAAI,kBAAkB,EAAE,YAAY;EAClC,MAAM,MAAO,OAAyD,IAAI;EAC1E,OAAO,OAAO,QAAQ,aAAc,IAAsB,IAAI;CAChE;CACA,MAAM,QAAQ,OAAO,MAAM;CAC3B,IAAI,iBAAiB,EAAE,WAAW,OAAO;CACzC,IAAI,iBAAiB,EAAE,WAAW,OAAO;CACzC,IAAI,iBAAiB,EAAE,YAAY,OAAO;CAC1C,IAAI,iBAAiB,EAAE,UAAU,OAAO,CAAC;CACzC,IAAI,iBAAiB,EAAE,SAAS;EAI9B,MAAM,QADQ,MAAqD,QAChD;EACnB,OAAO,OAAO,UAAU,WAAW,QAAQ;CAC7C;AAGF;;;;;;;;;;;;;;;;;AAgDA,SAAgB,YACd,KACA,QACA,SAC6D;CAC7D,OAAO,UAAU,KAAK,QAAQ,SAAS,QAAQ;AACjD;AAEA,SAAS,UACP,KACA,QACA,UACS;CACT,MAAM,QAAQ,OAAO;CACrB,MAAM,SAAuE,CAAC;CAC9E,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAG;EACpC,MAAM,aAAa,MAAM;EACzB,MAAM,UAAU,WAAW;EAC3B,OAAO,OAAO,UAAU,KAAK,YAAY,OAAO;CAClD;CACA,MAAM,WAAuC,CAAC;CAG9C,MAAM,qBAA2C,CAAC;CAElD,IAAI,mBAAmB,SAAS,GAC7B,SAAmD,aAAa;CAEnE,OAAO,IAAI,KAAK,QAAQ,QAAQ;AAClC;AAEA,SAAS,UACP,KACA,QACA,SAC8C;CAC9C,MAAM,QAAQ,OAAO,MAAM;CAE3B,IAAI,iBAAiB,EAAE,WACrB,OAAO,UACL,KACA,OACA,OACF;CAGF,IAAI,iBAAiB,EAAE,UAAU;EAC/B,MAAM,gBAAiB,MAAiC;EACxD,OAAO,IAAI,YACR,gBAAgB,UAAU,KAAK,eAAe,WAAW,GAC1D,YAAY,KAAA,IAAY,EAAW,QAA0B,IAAI,KAAA,CACnE;CACF;CAEA,MAAM,MAAM,YAAY,KAAA,IAAY,UAAU,eAAe,MAAM;CACnE,OAAO,IAAI,MAAM,KAAK,CAAC,aAAa,MAA4B,CAAC,CAAC;AACpE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kontsedal/olas-zod",
|
|
3
|
+
"version": "0.0.1-rc.0",
|
|
4
|
+
"description": "Olas Zod adapter — zodValidator + formFromZod",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.cts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"default": "./dist/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/index.d.cts",
|
|
18
|
+
"default": "./dist/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"zod": "^4.0.0",
|
|
29
|
+
"@kontsedal/olas-core": "^0.0.1-rc.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"zod": "^4.4.3",
|
|
33
|
+
"@kontsedal/olas-core": "^0.0.1-rc.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsdown",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Ctx, Field, FieldArray, Form, FormOptions, Validator } from '@kontsedal/olas-core'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wrap a Zod schema as an Olas validator. Returns a sync or async Validator
|
|
6
|
+
* depending on whether the schema requires async parsing (e.g. `.refine(async ...)`).
|
|
7
|
+
*/
|
|
8
|
+
export function zodValidator<T>(schema: z.ZodType<T>): Validator<T> {
|
|
9
|
+
return (value, signal) => {
|
|
10
|
+
// signal isn't used by Zod (parsing is sync) — kept for interface parity.
|
|
11
|
+
void signal
|
|
12
|
+
const result = schema.safeParse(value)
|
|
13
|
+
if (result.success) return null
|
|
14
|
+
return result.error.issues[0]?.message ?? 'Invalid'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Async variant for schemas with `.refine(async ...)` or `.transform(async ...)`.
|
|
20
|
+
* Returns a Promise<string | null>.
|
|
21
|
+
*/
|
|
22
|
+
export function zodValidatorAsync<T>(schema: z.ZodType<T>): Validator<T> {
|
|
23
|
+
return async (value, signal) => {
|
|
24
|
+
void signal
|
|
25
|
+
const result = await schema.safeParseAsync(value)
|
|
26
|
+
if (result.success) return null
|
|
27
|
+
return result.error.issues[0]?.message ?? 'Invalid'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Zod 4 typed every wrapper as `z.ZodType`-compatible; the public unwrap path
|
|
32
|
+
// is `.unwrap()` for optional/nullable and `.def.innerType` for default.
|
|
33
|
+
type AnyZodType = z.ZodType
|
|
34
|
+
|
|
35
|
+
// Strip the outer optional/nullable/default wrappers to find the inner schema.
|
|
36
|
+
function unwrap(schema: AnyZodType): AnyZodType {
|
|
37
|
+
let s: AnyZodType = schema
|
|
38
|
+
// Unwrap default + optional + nullable, in any combination.
|
|
39
|
+
for (let i = 0; i < 5; i++) {
|
|
40
|
+
if (s instanceof z.ZodDefault) {
|
|
41
|
+
// ZodDefault stores the inner schema on `def.innerType`. The runtime
|
|
42
|
+
// shape is stable across 3.x and 4.x; the public type just shifts.
|
|
43
|
+
s = (s as unknown as { def: { innerType: AnyZodType } }).def.innerType
|
|
44
|
+
} else if (s instanceof z.ZodOptional) {
|
|
45
|
+
s = (s as z.ZodOptional<AnyZodType>).unwrap() as AnyZodType
|
|
46
|
+
} else if (s instanceof z.ZodNullable) {
|
|
47
|
+
s = (s as z.ZodNullable<AnyZodType>).unwrap() as AnyZodType
|
|
48
|
+
} else {
|
|
49
|
+
return s
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return s
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function defaultInitial(schema: AnyZodType): unknown {
|
|
56
|
+
// Honor Zod default if present.
|
|
57
|
+
if (schema instanceof z.ZodDefault) {
|
|
58
|
+
const raw = (schema as unknown as { def: { defaultValue: unknown } }).def.defaultValue
|
|
59
|
+
return typeof raw === 'function' ? (raw as () => unknown)() : raw
|
|
60
|
+
}
|
|
61
|
+
const inner = unwrap(schema)
|
|
62
|
+
if (inner instanceof z.ZodString) return ''
|
|
63
|
+
if (inner instanceof z.ZodNumber) return 0
|
|
64
|
+
if (inner instanceof z.ZodBoolean) return false
|
|
65
|
+
if (inner instanceof z.ZodArray) return []
|
|
66
|
+
if (inner instanceof z.ZodEnum) {
|
|
67
|
+
// Zod 4 widened ZodEnum's options to support record-style enums. The
|
|
68
|
+
// runtime values are still iterable; pick the first.
|
|
69
|
+
const opts = (inner as unknown as { options: readonly unknown[] }).options
|
|
70
|
+
const first = opts[0]
|
|
71
|
+
return typeof first === 'string' ? first : ''
|
|
72
|
+
}
|
|
73
|
+
// For unknown/any/dates etc., undefined is the safest starting point.
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type AnyForm = Form<Record<string, Field<any> | Form<any> | FieldArray<any>>>
|
|
78
|
+
|
|
79
|
+
// Strip the same wrappers as the runtime `unwrap` helper, at the type level.
|
|
80
|
+
type UnwrapZod<S> =
|
|
81
|
+
S extends z.ZodDefault<infer Inner>
|
|
82
|
+
? UnwrapZod<Inner>
|
|
83
|
+
: S extends z.ZodOptional<infer Inner>
|
|
84
|
+
? UnwrapZod<Inner>
|
|
85
|
+
: S extends z.ZodNullable<infer Inner>
|
|
86
|
+
? UnwrapZod<Inner>
|
|
87
|
+
: S
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Recursively map a Zod schema to its Olas form leaf:
|
|
91
|
+
* - `ZodObject<S>` → `Form<{ [K]: ZodToLeaf<S[K]> }>`
|
|
92
|
+
* - `ZodArray<E>` → `FieldArray<ZodToLeaf<E>>` (when E is object/array)
|
|
93
|
+
* or `FieldArray<Field<infer<E>>>` for primitive elements.
|
|
94
|
+
* - everything else → `Field<infer<S>>`.
|
|
95
|
+
*
|
|
96
|
+
* `ZodToLeaf<S>` matches what `buildLeaf(ctx, s, ...)` returns at runtime,
|
|
97
|
+
* so the public `formFromZod<T>` can publish a precise structural type
|
|
98
|
+
* without the consumer needing a hand-written `CardForm = Form<{...}>` cast.
|
|
99
|
+
*/
|
|
100
|
+
export type ZodToLeaf<S> =
|
|
101
|
+
UnwrapZod<S> extends z.ZodObject<infer RawShape>
|
|
102
|
+
? Form<{ [K in keyof RawShape]: ZodToLeaf<RawShape[K]> }>
|
|
103
|
+
: UnwrapZod<S> extends z.ZodArray<infer Element>
|
|
104
|
+
? FieldArray<ZodToLeaf<Element> extends Form<any> | Field<any> ? ZodToLeaf<Element> : never>
|
|
105
|
+
: Field<z.infer<UnwrapZod<S> & z.ZodType>>
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Walk a Zod schema and emit the equivalent Olas Form / FieldArray / Field
|
|
109
|
+
* tree, with validators auto-attached.
|
|
110
|
+
*
|
|
111
|
+
* - `z.object(...)` → `Form`
|
|
112
|
+
* - `z.array(...)` → `FieldArray` (recurses on the element)
|
|
113
|
+
* - leaf schemas → `Field` with `zodValidator(...)` attached
|
|
114
|
+
*
|
|
115
|
+
* Each leaf's initial value is the Zod default if present, otherwise an empty
|
|
116
|
+
* value for that type (`''` for strings, `0` for numbers, etc.).
|
|
117
|
+
*
|
|
118
|
+
* The return type is structurally precise — `form.fields.title.value` is
|
|
119
|
+
* `string` (not `string | boolean | …`), `form.fields.subtasks.add(...)`
|
|
120
|
+
* accepts the exact item shape, etc. Consumers do not need to hand-write
|
|
121
|
+
* a `CardForm = Form<{...}>` matching the schema.
|
|
122
|
+
*/
|
|
123
|
+
export function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(
|
|
124
|
+
ctx: Ctx,
|
|
125
|
+
schema: T,
|
|
126
|
+
options?: { initials?: Partial<z.infer<T>> },
|
|
127
|
+
): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }> {
|
|
128
|
+
return buildForm(ctx, schema, options?.initials) as never
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildForm(
|
|
132
|
+
ctx: Ctx,
|
|
133
|
+
schema: z.ZodObject<z.ZodRawShape>,
|
|
134
|
+
initials?: Record<string, unknown>,
|
|
135
|
+
): AnyForm {
|
|
136
|
+
const shape = schema.shape
|
|
137
|
+
const fields: Record<string, Field<unknown> | Form<any> | FieldArray<any>> = {}
|
|
138
|
+
for (const key of Object.keys(shape)) {
|
|
139
|
+
const propSchema = shape[key] as AnyZodType
|
|
140
|
+
const initial = initials?.[key]
|
|
141
|
+
fields[key] = buildLeaf(ctx, propSchema, initial)
|
|
142
|
+
}
|
|
143
|
+
const formOpts: FormOptions<typeof fields> = {}
|
|
144
|
+
// If the schema has top-level refinements (z.object().refine(...)), Zod
|
|
145
|
+
// wraps it in ZodEffects. We expose those as form-level validators.
|
|
146
|
+
const topLevelValidators: Validator<unknown>[] = []
|
|
147
|
+
// Note: we received an unwrapped ZodObject here, so there's nothing extra.
|
|
148
|
+
if (topLevelValidators.length > 0) {
|
|
149
|
+
;(formOpts as { validators: Validator<unknown>[] }).validators = topLevelValidators
|
|
150
|
+
}
|
|
151
|
+
return ctx.form(fields, formOpts) as AnyForm
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildLeaf(
|
|
155
|
+
ctx: Ctx,
|
|
156
|
+
schema: AnyZodType,
|
|
157
|
+
initial: unknown,
|
|
158
|
+
): Field<unknown> | Form<any> | FieldArray<any> {
|
|
159
|
+
const inner = unwrap(schema)
|
|
160
|
+
|
|
161
|
+
if (inner instanceof z.ZodObject) {
|
|
162
|
+
return buildForm(
|
|
163
|
+
ctx,
|
|
164
|
+
inner as z.ZodObject<z.ZodRawShape>,
|
|
165
|
+
initial as Record<string, unknown> | undefined,
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (inner instanceof z.ZodArray) {
|
|
170
|
+
const elementSchema = (inner as z.ZodArray<AnyZodType>).element as AnyZodType
|
|
171
|
+
return ctx.fieldArray(
|
|
172
|
+
(itemInitial) => buildLeaf(ctx, elementSchema, itemInitial) as Field<unknown> | Form<any>,
|
|
173
|
+
initial !== undefined ? { initial: initial as Array<unknown> } : undefined,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const ini = initial !== undefined ? initial : defaultInitial(schema)
|
|
178
|
+
return ctx.field(ini, [zodValidator(schema as z.ZodType<unknown>)])
|
|
179
|
+
}
|