Skip to content

Commit d905790

Browse files
committed
Preventing (almost) adding errors to nodes in the error structure.
1 parent 1cb0aac commit d905790

File tree

6 files changed

+197
-16
lines changed

6 files changed

+197
-16
lines changed

src/compare.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,14 @@ test('Check path existence', () => {
363363
});
364364
});
365365

366+
test('Check path existence with path longer than existing', () => {
367+
const data = {
368+
persons: ['Need at least 2 persons']
369+
};
370+
371+
expect(pathExists(data, ['persons', '0', 'name'])).toBeUndefined();
372+
});
373+
366374
const refined = z.object({
367375
id: z.number().int().positive(),
368376
name: z.string().min(2),

src/lib/client/index.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ import {
4949
setPaths,
5050
pathExists,
5151
type ZodTypeInfo,
52-
traversePaths
52+
traversePaths,
53+
isInvalidPath
5354
} from '../traversal.js';
5455
import { fieldProxy } from './proxies.js';
5556
import { clone } from '../utils.js';
@@ -642,7 +643,19 @@ export function superForm<
642643
const errorContent = get(Errors);
643644

644645
const errorNode = errorContent
645-
? pathExists(errorContent, path)
646+
? pathExists(errorContent, path, {
647+
modifier: (pathData) => {
648+
// Check if we have found a string in an error array.
649+
if (isInvalidPath(path, pathData)) {
650+
throw new SuperFormError(
651+
'Errors can only be added to form fields, not to arrays or objects in the schema. Path: ' +
652+
pathData.path.slice(0, -1)
653+
);
654+
}
655+
656+
return pathData.value;
657+
}
658+
})
646659
: undefined;
647660

648661
// Need a special check here, since if the error has never existed,
@@ -1089,7 +1102,12 @@ async function validateField<T extends AnyZodObject, M>(
10891102
errors,
10901103
path as FieldPath<typeof errors>,
10911104
(node) => {
1092-
if (node.value === undefined) {
1105+
if (isInvalidPath(path, node)) {
1106+
throw new SuperFormError(
1107+
'Errors can only be added to form fields, not to arrays or objects in the schema. Path: ' +
1108+
node.path.slice(0, -1)
1109+
);
1110+
} else if (node.value === undefined) {
10931111
node.parent[node.key] = {};
10941112
return node.parent[node.key];
10951113
} else {
@@ -1251,7 +1269,7 @@ async function validateField<T extends AnyZodObject, M>(
12511269
// We validated the whole data structure, so clear all errors on success
12521270
// but also set the current path to undefined, so it will be used in the tainted+error
12531271
// check in oninput.
1254-
Errors_clear({ undefinePath: path, clearFormLevelErrors: false });
1272+
Errors_clear({ undefinePath: path, clearFormLevelErrors: true });
12551273
return undefined;
12561274
}
12571275
} else {

src/lib/traversal.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,34 @@ function setPath<T extends object>(parent: T, key: keyof T, value: any) {
8282
return 'skip' as const;
8383
}
8484

85+
export function isInvalidPath(originalPath: string[], pathData: PathData) {
86+
return (
87+
pathData.value !== undefined &&
88+
typeof pathData.value !== 'object' &&
89+
pathData.path.length < originalPath.length
90+
);
91+
}
92+
93+
export function pathExists<T extends object>(
94+
obj: T,
95+
path: string[],
96+
options: {
97+
value?: (value: unknown) => boolean;
98+
modifier?: (data: PathData) => undefined | unknown | void;
99+
} = {}
100+
): PathData | undefined {
101+
if (!options.modifier) {
102+
options.modifier = (pathData) =>
103+
isInvalidPath(path, pathData) ? undefined : pathData.value;
104+
}
105+
106+
const exists = traversePath(obj, path as FieldPath<T>, options.modifier);
107+
if (!exists) return undefined;
108+
109+
if (options.value === undefined) return exists;
110+
return options.value(exists.value) ? exists : undefined;
111+
}
112+
85113
export async function traversePathAsync<T extends object>(
86114
obj: T,
87115
realPath: FieldPath<T>,
@@ -124,18 +152,6 @@ export async function traversePathAsync<T extends object>(
124152
};
125153
}
126154

127-
export function pathExists<T extends object>(
128-
obj: T,
129-
path: string[],
130-
value?: (value: unknown) => boolean
131-
): PathData | undefined {
132-
const exists = traversePath(obj, path as FieldPath<T>);
133-
if (!exists) return undefined;
134-
135-
if (value === undefined) return exists;
136-
return value(exists.value) ? exists : undefined;
137-
}
138-
139155
export function traversePath<T extends object>(
140156
obj: T,
141157
realPath: FieldPath<T>,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { Actions, PageServerLoad } from './$types';
2+
import { message, superValidate } from '$lib/server';
3+
import { schema } from './schema';
4+
import { fail } from '@sveltejs/kit';
5+
6+
///// Load //////////////////////////////////////////////////////////
7+
8+
export const load: PageServerLoad = async () => {
9+
const form = await superValidate(schema);
10+
return { form };
11+
};
12+
13+
///// Form actions //////////////////////////////////////////////////
14+
15+
export const actions: Actions = {
16+
default: async ({ request }) => {
17+
const form = await superValidate(request, schema);
18+
19+
console.log('POST', form);
20+
21+
if (!form.valid) return fail(400, { form });
22+
23+
return message(form, 'Form posted successfully!');
24+
}
25+
};
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<script lang="ts">
2+
import { superForm } from '$lib/client';
3+
import type { PageData } from './$types';
4+
import SuperDebug from '$lib/client/SuperDebug.svelte';
5+
import { page } from '$app/stores';
6+
import { schema } from './schema';
7+
8+
export let data: PageData;
9+
10+
const { form, errors, tainted, message, enhance } = superForm(data.form, {
11+
dataType: 'json',
12+
validators: schema
13+
});
14+
15+
function addPerson() {
16+
form.update(
17+
($form) => {
18+
$form.persons = [...$form.persons, { name: '' }];
19+
return $form;
20+
},
21+
{ taint: false }
22+
);
23+
}
24+
</script>
25+
26+
<h3>#161</h3>
27+
28+
{#if $message}
29+
<div
30+
class="status"
31+
class:error={$page.status >= 400}
32+
class:success={$page.status == 200}
33+
>
34+
{$message}
35+
</div>
36+
{/if}
37+
38+
<form method="POST" use:enhance>
39+
{#if $errors._errors}
40+
<div class="status error">{$errors._errors}</div>
41+
{/if}
42+
43+
{#each $form.persons as p, i}
44+
<label>
45+
Name:
46+
<input
47+
name="name"
48+
data-invalid={$errors.persons?.[i]?.name}
49+
bind:value={p.name}
50+
/>
51+
{#if $errors.persons?.[i]?.name}<span class="invalid"
52+
>{$errors.persons?.[i]?.name}</span
53+
>{/if}
54+
</label>
55+
{/each}
56+
57+
<button on:click|preventDefault={addPerson}>Add person</button>
58+
<button>Submit</button>
59+
</form>
60+
61+
<hr />
62+
<p>
63+
<a target="_blank" href="https://superforms.vercel.app/api"
64+
>API Reference</a
65+
>
66+
</p>
67+
68+
<style>
69+
.invalid {
70+
color: red;
71+
}
72+
73+
.status {
74+
color: white;
75+
padding: 4px;
76+
padding-left: 8px;
77+
border-radius: 2px;
78+
font-weight: 500;
79+
}
80+
81+
.status.success {
82+
background-color: seagreen;
83+
}
84+
85+
.status.error {
86+
background-color: #ff2a02;
87+
}
88+
89+
input {
90+
background-color: #ddd;
91+
}
92+
93+
a {
94+
text-decoration: underline;
95+
}
96+
97+
hr {
98+
margin-top: 4rem;
99+
}
100+
101+
form {
102+
padding-top: 1rem;
103+
padding-bottom: 1rem;
104+
}
105+
</style>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { z } from 'zod';
2+
3+
export const schema = z
4+
.object({
5+
persons: z.array(z.object({ name: z.string().min(1) }))
6+
})
7+
.refine((data) => (data.persons?.length ?? 0) >= 2, {
8+
message: 'Need at least two persons'
9+
});

0 commit comments

Comments
 (0)