@kontsedal/olas-zod 0.0.1-rc.1 → 0.0.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Zod ↔ Olas forms adapter. Two helpers — `zodValidator` (single field) and `formFromZod` (whole form, inferred from schema).
4
4
 
5
- Olas core stays Zod-free. This package has a peer dep on `zod ^3`.
5
+ Olas core stays Zod-free. This package has a peer dep on `zod ^4`.
6
6
 
7
7
  ## Install
8
8
 
@@ -70,7 +70,7 @@ function formFromZod<S extends z.ZodObject<z.ZodRawShape>>(
70
70
 
71
71
  `zodValidator` runs `schema.safeParse(value)` and reports the first `ZodIssue`'s `message`. `zodValidatorAsync` awaits `.safeParseAsync(...)` for schemas with async `.refine` / `.transform`.
72
72
 
73
- Form-level `.refine(...)` on the root `z.object(...)` is attached as a top-level form validator (surfaces via `form.topLevelErrors`).
73
+ Root-level `.refine(...)` rules on `z.object(...)` are **not** auto-promoted to a form-level validator today. Wire one manually with `ctx.form(fields, { validators: [zodValidator(schema)] })`, or assert on `form.isValid` for leaf-level rules. Tracked in [`../../BACKLOG.md`](../../BACKLOG.md).
74
74
 
75
75
  ## Limitation
76
76
 
package/dist/index.cjs CHANGED
@@ -23,6 +23,23 @@ function zodValidatorAsync(schema) {
23
23
  return result.error.issues[0]?.message ?? "Invalid";
24
24
  };
25
25
  }
26
+ /**
27
+ * Run the schema and report only **root-level** issues (those with empty
28
+ * `path`). Leaf issues are already covered by `zodValidator(propSchema)` on
29
+ * each leaf field — surfacing them here would double-count.
30
+ *
31
+ * Used by `formFromZod` to lift root-level `.refine(...)` rules into a
32
+ * form-level validator. Returns `null` when every issue belongs to a leaf
33
+ * (or there are no issues at all).
34
+ */
35
+ function rootOnlyZodValidator(schema) {
36
+ return (value, signal) => {
37
+ const result = schema.safeParse(value);
38
+ if (result.success) return null;
39
+ for (const issue of result.error.issues) if (issue.path.length === 0) return issue.message;
40
+ return null;
41
+ };
42
+ }
26
43
  function unwrap(schema) {
27
44
  let s = schema;
28
45
  for (let i = 0; i < 5; i++) if (s instanceof zod.z.ZodDefault) s = s.def.innerType;
@@ -46,50 +63,36 @@ function defaultInitial(schema) {
46
63
  return typeof first === "string" ? first : "";
47
64
  }
48
65
  }
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
66
  function formFromZod(ctx, schema, options) {
66
- return buildForm(ctx, schema, options?.initials);
67
+ return buildForm(ctx, schema, options?.initials, "", options?.extraValidators, schema);
67
68
  }
68
- function buildForm(ctx, schema, initials) {
69
+ function buildForm(ctx, schema, initials, path, extras, rootSchema) {
69
70
  const shape = schema.shape;
70
71
  const fields = {};
71
72
  for (const key of Object.keys(shape)) {
72
73
  const propSchema = shape[key];
73
74
  const initial = initials?.[key];
74
- fields[key] = buildLeaf(ctx, propSchema, initial);
75
+ fields[key] = buildLeaf(ctx, propSchema, initial, path === "" ? key : `${path}.${key}`, extras);
75
76
  }
76
- const formOpts = {};
77
- const topLevelValidators = [];
78
- if (topLevelValidators.length > 0) formOpts.validators = topLevelValidators;
79
- return ctx.form(fields, formOpts);
77
+ if (rootSchema !== void 0) return ctx.form(fields, { validators: [rootOnlyZodValidator(rootSchema)] });
78
+ return ctx.form(fields);
80
79
  }
81
- function buildLeaf(ctx, schema, initial) {
80
+ function buildLeaf(ctx, schema, initial, path, extras) {
82
81
  const inner = unwrap(schema);
83
- if (inner instanceof zod.z.ZodObject) return buildForm(ctx, inner, initial);
82
+ if (inner instanceof zod.z.ZodObject) return buildForm(ctx, inner, initial, path, extras);
84
83
  if (inner instanceof zod.z.ZodArray) {
85
84
  const elementSchema = inner.element;
86
- return ctx.fieldArray((itemInitial) => buildLeaf(ctx, elementSchema, itemInitial), initial !== void 0 ? { initial } : void 0);
85
+ return ctx.fieldArray((itemInitial) => buildLeaf(ctx, elementSchema, itemInitial, path, extras), initial !== void 0 ? { initial } : void 0);
87
86
  }
88
87
  const ini = initial !== void 0 ? initial : defaultInitial(schema);
89
- return ctx.field(ini, [zodValidator(schema)]);
88
+ const validators = [zodValidator(schema)];
89
+ const extra = extras?.[path];
90
+ if (extra !== void 0) validators.push(extra);
91
+ return ctx.field(ini, validators);
90
92
  }
91
93
  //#endregion
92
94
  exports.formFromZod = formFromZod;
95
+ exports.rootOnlyZodValidator = rootOnlyZodValidator;
93
96
  exports.zodValidator = zodValidator;
94
97
  exports.zodValidatorAsync = zodValidatorAsync;
95
98
 
@@ -1 +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"}
1
+ {"version":3,"file":"index.cjs","names":["z"],"sources":["../src/index.ts"],"sourcesContent":["import type { Ctx, Field, FieldArray, Form, 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/**\n * Run the schema and report only **root-level** issues (those with empty\n * `path`). Leaf issues are already covered by `zodValidator(propSchema)` on\n * each leaf field — surfacing them here would double-count.\n *\n * Used by `formFromZod` to lift root-level `.refine(...)` rules into a\n * form-level validator. Returns `null` when every issue belongs to a leaf\n * (or there are no issues at all).\n */\nexport function rootOnlyZodValidator<T>(schema: z.ZodType<T>): Validator<T> {\n return (value, signal) => {\n void signal\n const result = schema.safeParse(value)\n if (result.success) return null\n for (const issue of result.error.issues) {\n if (issue.path.length === 0) return issue.message\n }\n return null\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 */\n/**\n * Per-leaf extra validators keyed by dotted path. Match the leaf field's\n * position inside the schema:\n *\n * - top-level: `'title'`\n * - nested form: `'address.street'`\n *\n * `FieldArray` items aren't separately addressable — the schema walker\n * generates one factory per array, so a path of `'tags'` matches the\n * `FieldArray` (validators attached there apply to the array as a whole;\n * use Olas's `FieldArrayOptions.validators` shape). Per-element rules\n * already live on the Zod element schema and are attached automatically.\n *\n * Validators run alongside `zodValidator(schema)` — both must pass.\n */\nexport type ExtraValidators = Record<string, Validator<any>>\n\nexport type FormFromZodOptions<T extends z.ZodObject<z.ZodRawShape>> = {\n initials?: Partial<z.infer<T>>\n extraValidators?: ExtraValidators\n}\n\nexport function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(\n ctx: Ctx,\n schema: T,\n options?: FormFromZodOptions<T>,\n): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }> {\n return buildForm(ctx, schema, options?.initials, '', options?.extraValidators, schema) as never\n}\n\nfunction buildForm(\n ctx: Ctx,\n schema: z.ZodObject<z.ZodRawShape>,\n initials: Record<string, unknown> | undefined,\n path: string,\n extras: ExtraValidators | undefined,\n /**\n * The original top-level schema. Passed only when constructing the ROOT\n * form — nested `buildForm` calls (from object-typed leaves) pass\n * `undefined`. Used to attach a root-only Zod validator so\n * `z.object({...}).refine(fn)` rules surface as form-level errors\n * without double-reporting leaf issues. See `rootOnlyZodValidator`.\n */\n rootSchema?: z.ZodObject<z.ZodRawShape>,\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 const leafPath = path === '' ? key : `${path}.${key}`\n fields[key] = buildLeaf(ctx, propSchema, initial, leafPath, extras)\n }\n // Lift root-level `.refine(...)` checks on the top-level object into a\n // form-level validator. Leaf checks remain owned by leaf-level\n // `zodValidator(propSchema)`; `rootOnlyZodValidator` filters to issues\n // whose `path` is empty so leaf issues are not double-reported.\n if (rootSchema !== undefined) {\n return ctx.form(fields, {\n validators: [rootOnlyZodValidator(rootSchema as z.ZodType<unknown>) as never],\n }) as AnyForm\n }\n return ctx.form(fields) as AnyForm\n}\n\nfunction buildLeaf(\n ctx: Ctx,\n schema: AnyZodType,\n initial: unknown,\n path: string,\n extras: ExtraValidators | undefined,\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 path,\n extras,\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 // Array items aren't enumerable at schema-build time; we don't extend\n // the dotted path with an index here. Per-item validators belong on\n // the Zod element schema (which `buildLeaf` already wraps via\n // `zodValidator`).\n (itemInitial) =>\n buildLeaf(ctx, elementSchema, itemInitial, path, extras) 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 const validators: Array<Validator<unknown>> = [zodValidator(schema as z.ZodType<unknown>)]\n const extra = extras?.[path]\n if (extra !== undefined) validators.push(extra as Validator<unknown>)\n return ctx.field(ini, validators)\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -12,6 +12,16 @@ declare function zodValidator<T>(schema: z.ZodType<T>): Validator<T>;
12
12
  * Returns a Promise<string | null>.
13
13
  */
14
14
  declare function zodValidatorAsync<T>(schema: z.ZodType<T>): Validator<T>;
15
+ /**
16
+ * Run the schema and report only **root-level** issues (those with empty
17
+ * `path`). Leaf issues are already covered by `zodValidator(propSchema)` on
18
+ * each leaf field — surfacing them here would double-count.
19
+ *
20
+ * Used by `formFromZod` to lift root-level `.refine(...)` rules into a
21
+ * form-level validator. Returns `null` when every issue belongs to a leaf
22
+ * (or there are no issues at all).
23
+ */
24
+ declare function rootOnlyZodValidator<T>(schema: z.ZodType<T>): Validator<T>;
15
25
  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
26
  /**
17
27
  * Recursively map a Zod schema to its Olas form leaf:
@@ -41,9 +51,27 @@ type ZodToLeaf<S> = UnwrapZod<S> extends z.ZodObject<infer RawShape> ? Form<{ [K
41
51
  * accepts the exact item shape, etc. Consumers do not need to hand-write
42
52
  * a `CardForm = Form<{...}>` matching the schema.
43
53
  */
44
- declare function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(ctx: Ctx, schema: T, options?: {
54
+ /**
55
+ * Per-leaf extra validators keyed by dotted path. Match the leaf field's
56
+ * position inside the schema:
57
+ *
58
+ * - top-level: `'title'`
59
+ * - nested form: `'address.street'`
60
+ *
61
+ * `FieldArray` items aren't separately addressable — the schema walker
62
+ * generates one factory per array, so a path of `'tags'` matches the
63
+ * `FieldArray` (validators attached there apply to the array as a whole;
64
+ * use Olas's `FieldArrayOptions.validators` shape). Per-element rules
65
+ * already live on the Zod element schema and are attached automatically.
66
+ *
67
+ * Validators run alongside `zodValidator(schema)` — both must pass.
68
+ */
69
+ type ExtraValidators = Record<string, Validator<any>>;
70
+ type FormFromZodOptions<T extends z.ZodObject<z.ZodRawShape>> = {
45
71
  initials?: Partial<z.infer<T>>;
46
- }): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }>;
72
+ extraValidators?: ExtraValidators;
73
+ };
74
+ declare function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(ctx: Ctx, schema: T, options?: FormFromZodOptions<T>): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }>;
47
75
  //#endregion
48
- export { ZodToLeaf, formFromZod, zodValidator, zodValidatorAsync };
76
+ export { ExtraValidators, FormFromZodOptions, ZodToLeaf, formFromZod, rootOnlyZodValidator, zodValidator, zodValidatorAsync };
49
77
  //# sourceMappingURL=index.d.cts.map
@@ -1 +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"}
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;;;;;;;;;;iBAkBtD,oBAAA,GAAA,CAAwB,MAAA,EAAQ,CAAA,CAAE,OAAA,CAAQ,CAAA,IAAK,SAAA,CAAU,CAAA;AAAA,KA6DpE,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;;;;;;;;;;;;KAaE,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;;;AAxGgC;AAkBvE;;;;;;;;;;;;;;;;;AAA0E;AAUzE;;;;;;;;;;KA6GW,eAAA,GAAkB,MAAM,SAAS,SAAA;AAAA,KAEjC,kBAAA,WAA6B,CAAA,CAAE,SAAA,CAAU,CAAA,CAAE,WAAA;EACrD,QAAA,GAAW,OAAA,CAAQ,CAAA,CAAE,KAAA,CAAM,CAAA;EAC3B,eAAA,GAAkB,eAAA;AAAA;AAAA,iBAGJ,WAAA,WAAsB,CAAA,CAAE,SAAA,CAAU,CAAA,CAAE,WAAA,EAAA,CAClD,GAAA,EAAK,GAAA,EACL,MAAA,EAAQ,CAAA,EACR,OAAA,GAAU,kBAAA,CAAmB,CAAA,IAC5B,IAAA,eAAmB,CAAA,YAAa,SAAA,CAAU,CAAA,UAAW,CAAA"}
package/dist/index.d.mts CHANGED
@@ -12,6 +12,16 @@ declare function zodValidator<T>(schema: z.ZodType<T>): Validator<T>;
12
12
  * Returns a Promise<string | null>.
13
13
  */
14
14
  declare function zodValidatorAsync<T>(schema: z.ZodType<T>): Validator<T>;
15
+ /**
16
+ * Run the schema and report only **root-level** issues (those with empty
17
+ * `path`). Leaf issues are already covered by `zodValidator(propSchema)` on
18
+ * each leaf field — surfacing them here would double-count.
19
+ *
20
+ * Used by `formFromZod` to lift root-level `.refine(...)` rules into a
21
+ * form-level validator. Returns `null` when every issue belongs to a leaf
22
+ * (or there are no issues at all).
23
+ */
24
+ declare function rootOnlyZodValidator<T>(schema: z.ZodType<T>): Validator<T>;
15
25
  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
26
  /**
17
27
  * Recursively map a Zod schema to its Olas form leaf:
@@ -41,9 +51,27 @@ type ZodToLeaf<S> = UnwrapZod<S> extends z.ZodObject<infer RawShape> ? Form<{ [K
41
51
  * accepts the exact item shape, etc. Consumers do not need to hand-write
42
52
  * a `CardForm = Form<{...}>` matching the schema.
43
53
  */
44
- declare function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(ctx: Ctx, schema: T, options?: {
54
+ /**
55
+ * Per-leaf extra validators keyed by dotted path. Match the leaf field's
56
+ * position inside the schema:
57
+ *
58
+ * - top-level: `'title'`
59
+ * - nested form: `'address.street'`
60
+ *
61
+ * `FieldArray` items aren't separately addressable — the schema walker
62
+ * generates one factory per array, so a path of `'tags'` matches the
63
+ * `FieldArray` (validators attached there apply to the array as a whole;
64
+ * use Olas's `FieldArrayOptions.validators` shape). Per-element rules
65
+ * already live on the Zod element schema and are attached automatically.
66
+ *
67
+ * Validators run alongside `zodValidator(schema)` — both must pass.
68
+ */
69
+ type ExtraValidators = Record<string, Validator<any>>;
70
+ type FormFromZodOptions<T extends z.ZodObject<z.ZodRawShape>> = {
45
71
  initials?: Partial<z.infer<T>>;
46
- }): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }>;
72
+ extraValidators?: ExtraValidators;
73
+ };
74
+ declare function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(ctx: Ctx, schema: T, options?: FormFromZodOptions<T>): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }>;
47
75
  //#endregion
48
- export { ZodToLeaf, formFromZod, zodValidator, zodValidatorAsync };
76
+ export { ExtraValidators, FormFromZodOptions, ZodToLeaf, formFromZod, rootOnlyZodValidator, zodValidator, zodValidatorAsync };
49
77
  //# sourceMappingURL=index.d.mts.map
@@ -1 +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"}
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;;;;;;;;;;iBAkBtD,oBAAA,GAAA,CAAwB,MAAA,EAAQ,CAAA,CAAE,OAAA,CAAQ,CAAA,IAAK,SAAA,CAAU,CAAA;AAAA,KA6DpE,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;;;;;;;;;;;;KAaE,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;;;AAxGgC;AAkBvE;;;;;;;;;;;;;;;;;AAA0E;AAUzE;;;;;;;;;;KA6GW,eAAA,GAAkB,MAAM,SAAS,SAAA;AAAA,KAEjC,kBAAA,WAA6B,CAAA,CAAE,SAAA,CAAU,CAAA,CAAE,WAAA;EACrD,QAAA,GAAW,OAAA,CAAQ,CAAA,CAAE,KAAA,CAAM,CAAA;EAC3B,eAAA,GAAkB,eAAA;AAAA;AAAA,iBAGJ,WAAA,WAAsB,CAAA,CAAE,SAAA,CAAU,CAAA,CAAE,WAAA,EAAA,CAClD,GAAA,EAAK,GAAA,EACL,MAAA,EAAQ,CAAA,EACR,OAAA,GAAU,kBAAA,CAAmB,CAAA,IAC5B,IAAA,eAAmB,CAAA,YAAa,SAAA,CAAU,CAAA,UAAW,CAAA"}
package/dist/index.mjs CHANGED
@@ -22,6 +22,23 @@ function zodValidatorAsync(schema) {
22
22
  return result.error.issues[0]?.message ?? "Invalid";
23
23
  };
24
24
  }
25
+ /**
26
+ * Run the schema and report only **root-level** issues (those with empty
27
+ * `path`). Leaf issues are already covered by `zodValidator(propSchema)` on
28
+ * each leaf field — surfacing them here would double-count.
29
+ *
30
+ * Used by `formFromZod` to lift root-level `.refine(...)` rules into a
31
+ * form-level validator. Returns `null` when every issue belongs to a leaf
32
+ * (or there are no issues at all).
33
+ */
34
+ function rootOnlyZodValidator(schema) {
35
+ return (value, signal) => {
36
+ const result = schema.safeParse(value);
37
+ if (result.success) return null;
38
+ for (const issue of result.error.issues) if (issue.path.length === 0) return issue.message;
39
+ return null;
40
+ };
41
+ }
25
42
  function unwrap(schema) {
26
43
  let s = schema;
27
44
  for (let i = 0; i < 5; i++) if (s instanceof z.ZodDefault) s = s.def.innerType;
@@ -45,49 +62,34 @@ function defaultInitial(schema) {
45
62
  return typeof first === "string" ? first : "";
46
63
  }
47
64
  }
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
65
  function formFromZod(ctx, schema, options) {
65
- return buildForm(ctx, schema, options?.initials);
66
+ return buildForm(ctx, schema, options?.initials, "", options?.extraValidators, schema);
66
67
  }
67
- function buildForm(ctx, schema, initials) {
68
+ function buildForm(ctx, schema, initials, path, extras, rootSchema) {
68
69
  const shape = schema.shape;
69
70
  const fields = {};
70
71
  for (const key of Object.keys(shape)) {
71
72
  const propSchema = shape[key];
72
73
  const initial = initials?.[key];
73
- fields[key] = buildLeaf(ctx, propSchema, initial);
74
+ fields[key] = buildLeaf(ctx, propSchema, initial, path === "" ? key : `${path}.${key}`, extras);
74
75
  }
75
- const formOpts = {};
76
- const topLevelValidators = [];
77
- if (topLevelValidators.length > 0) formOpts.validators = topLevelValidators;
78
- return ctx.form(fields, formOpts);
76
+ if (rootSchema !== void 0) return ctx.form(fields, { validators: [rootOnlyZodValidator(rootSchema)] });
77
+ return ctx.form(fields);
79
78
  }
80
- function buildLeaf(ctx, schema, initial) {
79
+ function buildLeaf(ctx, schema, initial, path, extras) {
81
80
  const inner = unwrap(schema);
82
- if (inner instanceof z.ZodObject) return buildForm(ctx, inner, initial);
81
+ if (inner instanceof z.ZodObject) return buildForm(ctx, inner, initial, path, extras);
83
82
  if (inner instanceof z.ZodArray) {
84
83
  const elementSchema = inner.element;
85
- return ctx.fieldArray((itemInitial) => buildLeaf(ctx, elementSchema, itemInitial), initial !== void 0 ? { initial } : void 0);
84
+ return ctx.fieldArray((itemInitial) => buildLeaf(ctx, elementSchema, itemInitial, path, extras), initial !== void 0 ? { initial } : void 0);
86
85
  }
87
86
  const ini = initial !== void 0 ? initial : defaultInitial(schema);
88
- return ctx.field(ini, [zodValidator(schema)]);
87
+ const validators = [zodValidator(schema)];
88
+ const extra = extras?.[path];
89
+ if (extra !== void 0) validators.push(extra);
90
+ return ctx.field(ini, validators);
89
91
  }
90
92
  //#endregion
91
- export { formFromZod, zodValidator, zodValidatorAsync };
93
+ export { formFromZod, rootOnlyZodValidator, zodValidator, zodValidatorAsync };
92
94
 
93
95
  //# sourceMappingURL=index.mjs.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Ctx, Field, FieldArray, Form, 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/**\n * Run the schema and report only **root-level** issues (those with empty\n * `path`). Leaf issues are already covered by `zodValidator(propSchema)` on\n * each leaf field — surfacing them here would double-count.\n *\n * Used by `formFromZod` to lift root-level `.refine(...)` rules into a\n * form-level validator. Returns `null` when every issue belongs to a leaf\n * (or there are no issues at all).\n */\nexport function rootOnlyZodValidator<T>(schema: z.ZodType<T>): Validator<T> {\n return (value, signal) => {\n void signal\n const result = schema.safeParse(value)\n if (result.success) return null\n for (const issue of result.error.issues) {\n if (issue.path.length === 0) return issue.message\n }\n return null\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 */\n/**\n * Per-leaf extra validators keyed by dotted path. Match the leaf field's\n * position inside the schema:\n *\n * - top-level: `'title'`\n * - nested form: `'address.street'`\n *\n * `FieldArray` items aren't separately addressable — the schema walker\n * generates one factory per array, so a path of `'tags'` matches the\n * `FieldArray` (validators attached there apply to the array as a whole;\n * use Olas's `FieldArrayOptions.validators` shape). Per-element rules\n * already live on the Zod element schema and are attached automatically.\n *\n * Validators run alongside `zodValidator(schema)` — both must pass.\n */\nexport type ExtraValidators = Record<string, Validator<any>>\n\nexport type FormFromZodOptions<T extends z.ZodObject<z.ZodRawShape>> = {\n initials?: Partial<z.infer<T>>\n extraValidators?: ExtraValidators\n}\n\nexport function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(\n ctx: Ctx,\n schema: T,\n options?: FormFromZodOptions<T>,\n): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }> {\n return buildForm(ctx, schema, options?.initials, '', options?.extraValidators, schema) as never\n}\n\nfunction buildForm(\n ctx: Ctx,\n schema: z.ZodObject<z.ZodRawShape>,\n initials: Record<string, unknown> | undefined,\n path: string,\n extras: ExtraValidators | undefined,\n /**\n * The original top-level schema. Passed only when constructing the ROOT\n * form — nested `buildForm` calls (from object-typed leaves) pass\n * `undefined`. Used to attach a root-only Zod validator so\n * `z.object({...}).refine(fn)` rules surface as form-level errors\n * without double-reporting leaf issues. See `rootOnlyZodValidator`.\n */\n rootSchema?: z.ZodObject<z.ZodRawShape>,\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 const leafPath = path === '' ? key : `${path}.${key}`\n fields[key] = buildLeaf(ctx, propSchema, initial, leafPath, extras)\n }\n // Lift root-level `.refine(...)` checks on the top-level object into a\n // form-level validator. Leaf checks remain owned by leaf-level\n // `zodValidator(propSchema)`; `rootOnlyZodValidator` filters to issues\n // whose `path` is empty so leaf issues are not double-reported.\n if (rootSchema !== undefined) {\n return ctx.form(fields, {\n validators: [rootOnlyZodValidator(rootSchema as z.ZodType<unknown>) as never],\n }) as AnyForm\n }\n return ctx.form(fields) as AnyForm\n}\n\nfunction buildLeaf(\n ctx: Ctx,\n schema: AnyZodType,\n initial: unknown,\n path: string,\n extras: ExtraValidators | undefined,\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 path,\n extras,\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 // Array items aren't enumerable at schema-build time; we don't extend\n // the dotted path with an index here. Per-item validators belong on\n // the Zod element schema (which `buildLeaf` already wraps via\n // `zodValidator`).\n (itemInitial) =>\n buildLeaf(ctx, elementSchema, itemInitial, path, extras) 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 const validators: Array<Validator<unknown>> = [zodValidator(schema as z.ZodType<unknown>)]\n const extra = extras?.[path]\n if (extra !== undefined) validators.push(extra as Validator<unknown>)\n return ctx.field(ini, validators)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontsedal/olas-zod",
3
- "version": "0.0.1-rc.1",
3
+ "version": "0.0.1",
4
4
  "description": "Olas Zod integration — zodValidator and formFromZod for end-to-end typed forms.",
5
5
  "keywords": [
6
6
  "olas",
@@ -46,11 +46,11 @@
46
46
  "sideEffects": false,
47
47
  "peerDependencies": {
48
48
  "zod": "^4.0.0",
49
- "@kontsedal/olas-core": "^0.0.1-rc.1"
49
+ "@kontsedal/olas-core": "^0.0.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "zod": "^4.4.3",
53
- "@kontsedal/olas-core": "^0.0.1-rc.1"
53
+ "@kontsedal/olas-core": "^0.0.1"
54
54
  },
55
55
  "publishConfig": {
56
56
  "access": "public"
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Ctx, Field, FieldArray, Form, FormOptions, Validator } from '@kontsedal/olas-core'
1
+ import type { Ctx, Field, FieldArray, Form, Validator } from '@kontsedal/olas-core'
2
2
  import { z } from 'zod'
3
3
 
4
4
  /**
@@ -28,6 +28,27 @@ export function zodValidatorAsync<T>(schema: z.ZodType<T>): Validator<T> {
28
28
  }
29
29
  }
30
30
 
31
+ /**
32
+ * Run the schema and report only **root-level** issues (those with empty
33
+ * `path`). Leaf issues are already covered by `zodValidator(propSchema)` on
34
+ * each leaf field — surfacing them here would double-count.
35
+ *
36
+ * Used by `formFromZod` to lift root-level `.refine(...)` rules into a
37
+ * form-level validator. Returns `null` when every issue belongs to a leaf
38
+ * (or there are no issues at all).
39
+ */
40
+ export function rootOnlyZodValidator<T>(schema: z.ZodType<T>): Validator<T> {
41
+ return (value, signal) => {
42
+ void signal
43
+ const result = schema.safeParse(value)
44
+ if (result.success) return null
45
+ for (const issue of result.error.issues) {
46
+ if (issue.path.length === 0) return issue.message
47
+ }
48
+ return null
49
+ }
50
+ }
51
+
31
52
  // Zod 4 typed every wrapper as `z.ZodType`-compatible; the public unwrap path
32
53
  // is `.unwrap()` for optional/nullable and `.def.innerType` for default.
33
54
  type AnyZodType = z.ZodType
@@ -120,41 +141,77 @@ export type ZodToLeaf<S> =
120
141
  * accepts the exact item shape, etc. Consumers do not need to hand-write
121
142
  * a `CardForm = Form<{...}>` matching the schema.
122
143
  */
144
+ /**
145
+ * Per-leaf extra validators keyed by dotted path. Match the leaf field's
146
+ * position inside the schema:
147
+ *
148
+ * - top-level: `'title'`
149
+ * - nested form: `'address.street'`
150
+ *
151
+ * `FieldArray` items aren't separately addressable — the schema walker
152
+ * generates one factory per array, so a path of `'tags'` matches the
153
+ * `FieldArray` (validators attached there apply to the array as a whole;
154
+ * use Olas's `FieldArrayOptions.validators` shape). Per-element rules
155
+ * already live on the Zod element schema and are attached automatically.
156
+ *
157
+ * Validators run alongside `zodValidator(schema)` — both must pass.
158
+ */
159
+ export type ExtraValidators = Record<string, Validator<any>>
160
+
161
+ export type FormFromZodOptions<T extends z.ZodObject<z.ZodRawShape>> = {
162
+ initials?: Partial<z.infer<T>>
163
+ extraValidators?: ExtraValidators
164
+ }
165
+
123
166
  export function formFromZod<T extends z.ZodObject<z.ZodRawShape>>(
124
167
  ctx: Ctx,
125
168
  schema: T,
126
- options?: { initials?: Partial<z.infer<T>> },
169
+ options?: FormFromZodOptions<T>,
127
170
  ): Form<{ [K in keyof T['shape']]: ZodToLeaf<T['shape'][K]> }> {
128
- return buildForm(ctx, schema, options?.initials) as never
171
+ return buildForm(ctx, schema, options?.initials, '', options?.extraValidators, schema) as never
129
172
  }
130
173
 
131
174
  function buildForm(
132
175
  ctx: Ctx,
133
176
  schema: z.ZodObject<z.ZodRawShape>,
134
- initials?: Record<string, unknown>,
177
+ initials: Record<string, unknown> | undefined,
178
+ path: string,
179
+ extras: ExtraValidators | undefined,
180
+ /**
181
+ * The original top-level schema. Passed only when constructing the ROOT
182
+ * form — nested `buildForm` calls (from object-typed leaves) pass
183
+ * `undefined`. Used to attach a root-only Zod validator so
184
+ * `z.object({...}).refine(fn)` rules surface as form-level errors
185
+ * without double-reporting leaf issues. See `rootOnlyZodValidator`.
186
+ */
187
+ rootSchema?: z.ZodObject<z.ZodRawShape>,
135
188
  ): AnyForm {
136
189
  const shape = schema.shape
137
190
  const fields: Record<string, Field<unknown> | Form<any> | FieldArray<any>> = {}
138
191
  for (const key of Object.keys(shape)) {
139
192
  const propSchema = shape[key] as AnyZodType
140
193
  const initial = initials?.[key]
141
- fields[key] = buildLeaf(ctx, propSchema, initial)
194
+ const leafPath = path === '' ? key : `${path}.${key}`
195
+ fields[key] = buildLeaf(ctx, propSchema, initial, leafPath, extras)
142
196
  }
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
197
+ // Lift root-level `.refine(...)` checks on the top-level object into a
198
+ // form-level validator. Leaf checks remain owned by leaf-level
199
+ // `zodValidator(propSchema)`; `rootOnlyZodValidator` filters to issues
200
+ // whose `path` is empty so leaf issues are not double-reported.
201
+ if (rootSchema !== undefined) {
202
+ return ctx.form(fields, {
203
+ validators: [rootOnlyZodValidator(rootSchema as z.ZodType<unknown>) as never],
204
+ }) as AnyForm
150
205
  }
151
- return ctx.form(fields, formOpts) as AnyForm
206
+ return ctx.form(fields) as AnyForm
152
207
  }
153
208
 
154
209
  function buildLeaf(
155
210
  ctx: Ctx,
156
211
  schema: AnyZodType,
157
212
  initial: unknown,
213
+ path: string,
214
+ extras: ExtraValidators | undefined,
158
215
  ): Field<unknown> | Form<any> | FieldArray<any> {
159
216
  const inner = unwrap(schema)
160
217
 
@@ -163,17 +220,27 @@ function buildLeaf(
163
220
  ctx,
164
221
  inner as z.ZodObject<z.ZodRawShape>,
165
222
  initial as Record<string, unknown> | undefined,
223
+ path,
224
+ extras,
166
225
  )
167
226
  }
168
227
 
169
228
  if (inner instanceof z.ZodArray) {
170
229
  const elementSchema = (inner as z.ZodArray<AnyZodType>).element as AnyZodType
171
230
  return ctx.fieldArray(
172
- (itemInitial) => buildLeaf(ctx, elementSchema, itemInitial) as Field<unknown> | Form<any>,
231
+ // Array items aren't enumerable at schema-build time; we don't extend
232
+ // the dotted path with an index here. Per-item validators belong on
233
+ // the Zod element schema (which `buildLeaf` already wraps via
234
+ // `zodValidator`).
235
+ (itemInitial) =>
236
+ buildLeaf(ctx, elementSchema, itemInitial, path, extras) as Field<unknown> | Form<any>,
173
237
  initial !== undefined ? { initial: initial as Array<unknown> } : undefined,
174
238
  )
175
239
  }
176
240
 
177
241
  const ini = initial !== undefined ? initial : defaultInitial(schema)
178
- return ctx.field(ini, [zodValidator(schema as z.ZodType<unknown>)])
242
+ const validators: Array<Validator<unknown>> = [zodValidator(schema as z.ZodType<unknown>)]
243
+ const extra = extras?.[path]
244
+ if (extra !== undefined) validators.push(extra as Validator<unknown>)
245
+ return ctx.field(ini, validators)
179
246
  }