Skip to content

Commit

Permalink
Client-side validation was ignored when a data property was missing o…
Browse files Browse the repository at this point in the history
…r didn't match the schema type.

Fixes #243.
  • Loading branch information
ciscoheat committed Aug 9, 2023
1 parent 8c22a48 commit 0dddb36
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 41 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- In rare cases, timers weren't resetted when redirecting to the same route.
- Client-side validation was ignored when a data property was missing or didn't match the schema type. It now fails with a console error. ([#243](https://github.com/ciscoheat/sveltekit-superforms/issues/243))

## [1.5.0] - 2023-07-23

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"slugify": "^1.6.6",
"svelte": "^4.1.2",
"svelte-check": "^3.4.6",
"sveltekit-flash-message": "^2.1.1",
"sveltekit-flash-message": "^2.1.3",
"sveltekit-rate-limiter": "^0.3.2",
"throttle-debounce": "^5.0.0",
"tslib": "^2.6.1",
Expand Down
14 changes: 7 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 45 additions & 30 deletions src/lib/client/clientValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ async function _clientValidation<T extends AnyZodObject, M = unknown>(
constraints: SuperValidated<ZodValidation<T>>['constraints'],
posted: boolean
): Promise<SuperValidated<ZodValidation<T>>> {
if (validators) {
let valid: boolean;
let clientErrors: ValidationErrors<T> = {};
let valid = true;
let clientErrors: ValidationErrors<T> = {};

if (validators) {
if ('safeParseAsync' in validators) {
// Zod validator
const validator = validators as AnyZodObject;
Expand All @@ -113,8 +113,15 @@ async function _clientValidation<T extends AnyZodObject, M = unknown>(
}
} else {
// SuperForms validator

valid = true;
checkData = { ...checkData };
// Add top-level validator fields to non-existing checkData fields
// so they will be validated even if the field doesn't exist
for (const [key, value] of Object.entries(validators)) {
if (typeof value === 'function' && !(key in checkData)) {
// @ts-expect-error Setting undefined fields so they will be validated based on field existance.
checkData[key] = undefined;
}
}

const validator = validators as Validators<T>;
const newErrors: {
Expand All @@ -133,29 +140,49 @@ async function _clientValidation<T extends AnyZodObject, M = unknown>(
if (typeof maybeValidator?.value === 'function') {
const check = maybeValidator.value as Validator<unknown>;

let errors: string | string[] | null | undefined;

if (Array.isArray(value)) {
for (const key in value) {
const errors = await check(value[key]);
try {
errors = await check(value[key]);
if (errors) {
valid = false;
newErrors.push({
path: path.concat([key]),
errors:
typeof errors === 'string'
? [errors]
: errors ?? undefined
});
}
} catch (e) {
valid = false;
console.error(
`Error in form validators for field "${path}":`,
e
);
}
}
} else {
try {
errors = await check(value);
if (errors) {
valid = false;
newErrors.push({
path: path.concat([key]),
path,
errors:
typeof errors === 'string'
? [errors]
: errors ?? undefined
});
}
}
} else {
const errors = await check(value);
if (errors) {
} catch (e) {
valid = false;
newErrors.push({
path,
errors:
typeof errors === 'string' ? [errors] : errors ?? undefined
});
console.error(
`Error in form validators for field "${path}":`,
e
);
}
}
}
Expand All @@ -177,24 +204,12 @@ async function _clientValidation<T extends AnyZodObject, M = unknown>(
}
}
}

if (!valid) {
return {
valid: false,
posted,
errors: clientErrors,
data: checkData,
constraints,
message: undefined,
id: formId
};
}
}

return {
valid: true,
valid,
posted,
errors: {},
errors: clientErrors,
data: checkData,
constraints,
message: undefined,
Expand Down
9 changes: 6 additions & 3 deletions src/routes/spa/without-zod/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@
}
},
onUpdated({ form }) {
console.log('onUpdated, valid:', form.valid);
console.log('onUpdated, valid:', form.valid, form);
},
validators: {
tags: {
id: (id) => (id < 3 ? 'Id must be larger than 2' : null),
id: (id) =>
isNaN(id) || id < 3 ? 'Id must be larger than 2' : null,
name: (name) =>
name.length < 2 ? 'Tags must be at least two characters' : null
!name || name.length < 2
? 'Tags must be at least two characters'
: null
}
}
});
Expand Down
25 changes: 25 additions & 0 deletions src/routes/tests/missing-data-validate/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { message, superValidate } from '$lib/server';
import { schema } from './schema';
import { fail } from '@sveltejs/kit';

export const load = async () => {
const form = await superValidate(schema);
return { form };
};

export const actions = {
default: async ({ request }) => {
const formData = await request.formData();
console.log(formData);

const form = await superValidate(formData, schema);
console.log('POST', form);

if (!form.valid) {
form.message = 'Failed on server.';
return fail(400, { form });
}

return message(form, 'Posted OK!');
}
};
52 changes: 52 additions & 0 deletions src/routes/tests/missing-data-validate/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script lang="ts">
import { superForm } from '$lib/client';
import type { PageData } from './$types';
import SuperDebug from '$lib/client/SuperDebug.svelte';
export let data: PageData;
// Missing a field, which will result in a console error.
// @ts-ignore
data.form.data = {};
const { form, errors, tainted, message, enhance } = superForm(data.form, {
//dataType: 'json',
validators: {
age: (age) =>
isNaN(age) || age < 30 ? 'At least 30 years, please!' : null,
name: (name) =>
name.length < 2 ? 'At least two characters, please!' : null
},
taintedMessage: null
});
</script>

{#if $message}<h4>{$message}</h4>{/if}

<form method="POST" use:enhance>
<label>
Age: <input name="age" type="number" bind:value={$form.age} />
{#if $errors.age}<span class="invalid">{$errors.age}</span>{/if}
</label>
<label>
Name: <input name="name" bind:value={$form.name} />
{#if $errors.name}<span class="invalid">{$errors.name}</span>{/if}
</label>
<div>
<button>Submit</button>
</div>
</form>

<style lang="scss">
form {
margin: 2rem 0;
input {
background-color: #dedede;
}
.invalid {
color: crimson;
}
}
</style>
6 changes: 6 additions & 0 deletions src/routes/tests/missing-data-validate/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { z } from 'zod';

export const schema = z.object({
age: z.number().min(30),
name: z.string().min(1)
});

0 comments on commit 0dddb36

Please sign in to comment.