Skip to content

Commit 451e101

Browse files
committed
WIP: Partial validation works with Zodeffects on top level.
1 parent fce803b commit 451e101

File tree

8 files changed

+256
-143
lines changed

8 files changed

+256
-143
lines changed

src/compare.test.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
intProxy,
2727
numberProxy
2828
} from '$lib/client';
29-
import type { FormPath } from '$lib';
29+
import type { FormPath, FieldPath } from '$lib';
3030
import { comparePaths, setPaths } from '$lib/entity';
3131

3232
const user = z.object({
@@ -79,32 +79,44 @@ test('Mapping errors', () => {
7979

8080
describe('Path traversals', () => {
8181
test('Basic path traversal', () => {
82-
const error = traversePath(mapped, ['friends', '1', 'id']);
82+
const path = ['friends', '1', 'id'];
83+
const error = traversePath(mapped, path as FieldPath<typeof mapped>);
8384

8485
expect(error).toStrictEqual({
8586
parent: mapped.friends![1],
8687
key: 'id',
87-
value: mapped.friends![1].id
88+
value: mapped.friends![1].id,
89+
path,
90+
isLeaf: true
8891
});
8992
});
9093

9194
test('Basic path traversal, async', async () => {
92-
const error = await traversePathAsync(mapped, ['friends', '1', 'id']);
95+
const path = ['friends', '1', 'id'];
96+
const error = await traversePathAsync(
97+
mapped,
98+
path as FieldPath<typeof mapped>
99+
);
93100

94101
expect(error).toStrictEqual({
95102
parent: mapped.friends![1],
96103
key: 'id',
97-
value: mapped.friends![1].id
104+
value: mapped.friends![1].id,
105+
path,
106+
isLeaf: true
98107
});
99108
});
100109

101110
test('Basic path traversal, non-existing leaf', () => {
102-
const error = traversePath(mapped, ['friends', '1', 'N/A']);
111+
const path = ['friends', '1', 'N/A'];
112+
const error = traversePath(mapped, path as FieldPath<typeof mapped>);
103113

104114
expect(error).toStrictEqual({
105115
parent: mapped.friends![1],
106116
key: 'N/A',
107-
value: undefined
117+
value: undefined,
118+
isLeaf: true,
119+
path
108120
});
109121
});
110122

@@ -114,9 +126,10 @@ describe('Path traversals', () => {
114126
});
115127

116128
test('Basic path traversal, non-existing node with modifier', async () => {
129+
const path = ['friends', '2', 'id'];
117130
const error = await traversePathAsync(
118131
mapped,
119-
['friends', '2', 'id'],
132+
path as FieldPath<typeof mapped>,
120133
({ parent, key, value }) => {
121134
if (value === undefined) parent[key] = {};
122135
return parent[key];
@@ -125,7 +138,9 @@ describe('Path traversals', () => {
125138
expect(error).toStrictEqual({
126139
parent: mapped.friends![2],
127140
key: 'id',
128-
value: undefined
141+
value: undefined,
142+
isLeaf: true,
143+
path
129144
});
130145
});
131146

@@ -436,7 +451,9 @@ test('Check path existence', () => {
436451
expect(pathExists(errors, ['tags', '0', 'id'])).toStrictEqual({
437452
parent: errors.tags[0],
438453
key: 'id',
439-
value: true
454+
value: true,
455+
isLeaf: true,
456+
path: ['tags', '0', 'id']
440457
});
441458
});
442459

src/lib/client/entity.ts

Lines changed: 0 additions & 43 deletions
This file was deleted.

src/lib/client/index.ts

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ import {
3030
type UnwrapEffects,
3131
type ZodValidation
3232
} from '../index.js';
33-
import type { z, AnyZodObject, ZodEffects } from 'zod';
33+
import type {
34+
z,
35+
AnyZodObject,
36+
ZodEffects,
37+
ZodTypeAny,
38+
SafeParseReturnType
39+
} from 'zod';
3440
import { stringify } from 'devalue';
3541
import type { FormFields } from '../index.js';
3642
import {
@@ -44,7 +50,8 @@ import {
4450
} from '../entity.js';
4551
import { fieldProxy } from './proxies.js';
4652
import { clone } from '../utils.js';
47-
import type { Entity } from '$lib/server/entity.js';
53+
import type { Entity } from '../schemaEntity.js';
54+
import { unwrapZodType } from '../schemaEntity.js';
4855

4956
enum FetchStatus {
5057
Idle = 0,
@@ -912,19 +919,18 @@ async function validateField<T extends AnyZodObject, M>(
912919
return defaultValidate();
913920
}
914921

915-
// Remove array indices, they don't exist in validators.
916-
const validationPath = path.filter((p) => isNaN(parseInt(p)));
917-
918922
function extractValidator(data: any, key: string) {
919-
const def = data?._def
923+
const zodData = unwrapZodType(data);
924+
if (zodData.effects)
925+
return { effects: true, validator: zodData.effects } as const;
926+
data = zodData.zodType;
920927

921-
if(def) {
922-
const objectShape = 'shape' in def ? def.shape() : def.schema?.shape;
923-
if (objectShape) return objectShape[key];
928+
// ZodObject
924929

925-
const arrayShape = data?.element?.shape;
926-
if (arrayShape) return arrayShape[key];
927-
}
930+
// ZodArray
931+
const arrayShape = data?.element?.shape;
932+
if (arrayShape)
933+
return { effects: false, validator: arrayShape[key] } as const;
928934

929935
throw new SuperFormError(
930936
'Invalid Zod validator for ' + key + ': ' + data
@@ -933,26 +939,54 @@ async function validateField<T extends AnyZodObject, M>(
933939

934940
if ('safeParseAsync' in validators) {
935941
// Zod validator
942+
let resultPromise: Promise<SafeParseReturnType<any, any>> | undefined =
943+
undefined;
936944

937-
const validator = traversePath(
945+
let effectPath = [];
946+
947+
const extracted = traversePath(
938948
validators as AnyZodObject,
939-
validationPath as FieldPath<AnyZodObject>,
940-
(data) => {
941-
return extractValidator(data.parent, data.key);
949+
path as FieldPath<AnyZodObject>,
950+
(pathdata) => {
951+
if (!isNaN(parseInt(pathdata.key))) {
952+
return value;
953+
}
954+
const extracted = extractValidator(pathdata.parent, pathdata.key);
955+
if (extracted.effects) {
956+
// ZodEffects found, validate tree from this point
957+
const validator = extracted.validator;
958+
const partialData = traversePath(
959+
get(data),
960+
pathdata.path as FieldPath<z.infer<T>>
961+
);
962+
resultPromise = validator.safeParseAsync(partialData?.parent);
963+
effectPath = pathdata.path.slice(0, -1);
964+
return undefined;
965+
} else {
966+
// No effects, keep going down the tree.
967+
return extracted.validator;
968+
}
942969
}
943970
);
944971

945-
if (!validator)
946-
throw new SuperFormError('No Zod validator found: ' + path);
972+
if (!resultPromise) {
973+
if (!extracted) {
974+
throw new SuperFormError('No Zod validator found: ' + path);
975+
}
976+
resultPromise = extracted.value.safeParseAsync(value);
977+
}
978+
979+
const result = await (resultPromise as NonNullable<
980+
typeof resultPromise
981+
>);
947982

948-
const result = await extractValidator(
949-
validator.parent,
950-
validator.key
951-
).safeParseAsync(value);
983+
console.log('Validated', path, result, get(data));
952984

953985
if (!result.success) {
954986
const msgs = mapErrors<T>(result.error.format());
955-
return setError(options.errors ?? msgs._errors);
987+
const current = traversePath(msgs, path as FieldPath<typeof msgs>);
988+
//if (!current) throw new SuperFormError('Error not found: ' + path);
989+
return setError(current?.value);
956990
} else {
957991
return setError(undefined);
958992
}
@@ -1461,7 +1495,10 @@ function formEnhance<T extends AnyZodObject, M>(
14611495
setTimeout(() => validationResponse({ result }), 0);
14621496
} else if (options.dataType === 'json') {
14631497
const postData = get(data);
1464-
const chunks = chunkSubstr(stringify(postData), options.jsonChunkSize ?? 500000);
1498+
const chunks = chunkSubstr(
1499+
stringify(postData),
1500+
options.jsonChunkSize ?? 500000
1501+
);
14651502

14661503
for (const chunk of chunks) {
14671504
submit.data.append('__superform_json', chunk);

0 commit comments

Comments
 (0)