@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 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"}
@@ -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"}
@@ -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
+ }