@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 +2 -2
- package/dist/index.cjs +30 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +31 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +30 -28
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +82 -15
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 ^
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
package/dist/index.d.cts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
package/dist/index.mjs.map
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
|
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?:
|
|
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
|
|
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
|
-
|
|
194
|
+
const leafPath = path === '' ? key : `${path}.${key}`
|
|
195
|
+
fields[key] = buildLeaf(ctx, propSchema, initial, leafPath, extras)
|
|
142
196
|
}
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|