ciscoheat / sveltekit-superforms

Making SvelteKit forms a pleasure to use!
https://superforms.rocks
MIT License
2.13k stars 63 forks source link

Not able to get refine to work with superValidate and zod adapter #458

Closed kristianmandrup closed 1 month ago

kristianmandrup commented 1 month ago

Description

package.json

"sveltekit-superforms": "^2.16.1",
"zod": "^3.22.4"

AuthZodSchemas.ts

export const passwordValidationSchema = z.object({
    newPassword: z.string().min(8, 'Password must be at least 8 characters long'),
    confirmPassword: z.string().min(8, 'Password must be at least 8 characters long'),
    passwordResetToken: z.string().optional()
});

export const PasswordResetZodSchema = passwordValidationSchema.refine(
    (data) => data.newPassword === data.confirmPassword,
    {
        message: " Passwords don't match",
        path: ['confirmPassword']
    }
);

+page.server.ts

import { zod } from 'sveltekit-superforms/adapters';
import { message, superValidate } from 'sveltekit-superforms/client';

export const load = (async (event) => {
    const { cookies, locals } = event;

    if (!locals.user) {
        throw redirect(
            route('/auth/login'),
            {
                type: 'error',
                message: 'You must be logged in to view the dashboard.'
            },
            cookies
        );
    }

    await passwordResetDashboardPageActionRateLimiter.cookieLimiter?.preflight(event);

    return {
        loggedInUser: locals.user,
        allUsers: await getAllUsers(),
        passwordResetFormData: await superValidate(zod(PasswordResetZodSchema as any))
    };
}) satisfies PageServerLoad;

It complains with the following:

Argument of type 'ZodEffects<ZodObject<{ newPassword: ZodEffects<ZodEffects<ZodEffects<ZodEffects<ZodString, string, string>, string, string>, string, string>, string, string>; confirmPassword: ZodEffects<...>; passwordResetToken: ZodOptional<...>; }, "strip", ZodTypeAny, { ...; }, { ...; }>, { ...; }, { ...; }>' is not assignable to parameter of type 'ZodValidation<ZodObjectTypes>'.
  Type 'ZodEffects<ZodObject<{ newPassword: ZodEffects<ZodEffects<ZodEffects<ZodEffects<ZodString, string, string>, string, string>, string, string>, string, string>; confirmPassword: ZodEffects<...>; passwordResetToken: ZodOptional<...>; }, "strip", ZodTypeAny, { ...; }, { ...; }>, { ...; }, { ...; }>' is not assignable to type 'ZodEffects<ZodEffects<ZodEffects<ZodEffects<ZodEffects<ZodEffects<ZodEffects<ZodObjectTypes, Record<string, unknown> | { [x: string]: any; }, Record<string, unknown> | { [x: string]: any; } | undefined>, Record<...> | { ...; }, Record<...> | ... 1 more ... | undefined>, Record<...> | { ...; }, Record<...> | ... 1 mo...'.
    The types returned by 'innerType()' are incompatible between these types.
      Type 'ZodObject<{ newPassword: ZodEffects<ZodEffects<ZodEffects<ZodEffects<ZodString, string, string>, string, string>, string, string>, string, string>; confirmPassword: ZodEffects<...>; passwordResetToken: ZodOptional<...>; }, "strip", ZodTypeAny, { ...; }, { ...; }>' is missing the following properties from type 'ZodEffects<ZodEffects<ZodEffects<ZodEffects<ZodEffects<ZodEffects<ZodObjectTypes, Record<string, unknown> | { [x: string]: any; }, Record<string, unknown> | { [x: string]: any; } | undefined>, Record<...> | { ...; }, Record<...> | ... 1 more ... | undefined>, Record<...> | { ...; }, Record<...> | ... 1 more ... | un...': innerType, sourceType

Makes no sense to me. I assumed refine was the issue, but refine is mentioned explicitly in the docs: https://superforms.rocks/concepts/error-handling#form-level-and-array-errors

The code was taken from an app where it used to work just fine with a previous version of zod and superforms (before using adapter).

I looked at the following issues:

A the end @ciscoheat states: "Superforms v2 will handle this properly."

If applicable, a MRE Use this template project to create a minimal reproducible example that you can link to here: https://sveltelab.dev/github.com/ciscoheat/superforms-examples/tree/zod (right click to open in a new tab)

I tried to update the template, but was not clear how to save and share it. The following modifications to showcase the issue seems to work just fine however...

<form method="POST" use:enhance>
    <label>
        new Password<br />
        <input name="newPassword" aria-invalid={$errors.newPassword ? 'true' : undefined} bind:value={$form.newPassword} />
        {#if $errors.newPassword}<span class="invalid">{$errors.newPassword}</span>{/if}
    </label>

    <label>
        confirm Password<br />
        <input
            name="confirmPassword"
            aria-invalid={$errors.confirmPassword ? 'true' : undefined}
            bind:value={$form.confirmPassword}
        />
        {#if $errors.confirmPassword}<span class="invalid">{$errors.confirmPassword}</span>{/if}
    </label>

    <button>Submit</button>
</form>
import { z } from 'zod';

export const schema = z
    .object({
        newPassword: z.string().min(8, 'Password must be at least 8 characters long'),
        confirmPassword: z.string().min(8, 'Password must be at least 8 characters long'),
    })
    .refine((data) => data.newPassword === data.confirmPassword, {
        message: " Passwords don't match",
        path: ['confirmPassword']
    });

In my project when I mirrored this it didn't work, so I wonder what the versions are in that MRE sample repo. Didn't seem to be able to view package.json however

kristianmandrup commented 1 month ago

I found the following in the tests:

t('with form-level errors', async () => {
        const schema = z
            .object({
                name: z.string()
            })
            .refine((a) => a.name == 'OK', {
                message: 'Name is not OK'
            });

        const form = await superValidate({ name: 'Test' }, zod(schema));

        expect(form.errors).toEqual({
            _errors: ['Name is not OK']
        });
    });

Further investigation required

ciscoheat commented 1 month ago

Source and package.json for the example code, in different branches, here's the zod version: https://github.com/ciscoheat/superforms-examples/tree/zod

kristianmandrup commented 1 month ago

Thanks a lot for reaching out so quickly.

Seems to be my VS Code that was caching and not restarting the TS server. Now it no longer complains with the following schema:

export const schema = z
    .object({
        // newPassword: advancedPasswordSchema,
        // confirmPassword: advancedPasswordSchema,
        newPassword: z.string().min(8, 'Password must be at least 8 characters long'),
        confirmPassword: z.string().min(8, 'Password must be at least 8 characters long'),
        passwordResetToken: z.string().optional()
    })
    .refine((data) => data.newPassword === data.confirmPassword, {
        message: " Passwords don't match",
        path: ['confirmPassword']
    });

However when I run my project I get the following, not sure how to debug.

TypeError: Cannot read properties of undefined (reading 'defaults')
    at Module.superValidate 

I assume it is from await superValidate(zod(schema)) in my load function

import type { Actions, PageServerLoad } from './$types';

import { eq } from 'drizzle-orm';
import { Argon2id } from 'oslo/password';
import { redirect } from 'sveltekit-flash-message/server';
import { message, superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';

import { route } from '$lib/ROUTES';
import {
    deleteAllUsers,
    deleteSessionCookie,
    getAllUsers,
    isSameAsOldPassword,
    passwordResetDashboardPageActionRateLimiter
} from '$lib/database/authUtils.server';
import { database } from '$lib/database/database.server';
import { lucia } from '$lib/database/luciaAuth.server';
import { usersTable } from '$lib/database/schema';
import { LOGIN_ROUTE } from '$lib/utils/navLinks';
import { schema } from '$validations/AuthZodSchemas';
// import { PasswordResetZodSchema } from '$validations/AuthZodSchemas';

export const load = (async (event) => {
    const { cookies, locals } = event;

    if (!locals.user) {
        throw redirect(
            route('/auth/login'),
            {
                type: 'error',
                message: 'You must be logged in to view the dashboard.'
            },
            cookies
        );
    }

    await passwordResetDashboardPageActionRateLimiter.cookieLimiter?.preflight(event);

    return {
        loggedInUser: locals.user,
        allUsers: await getAllUsers(),
        passwordResetFormData: await superValidate(zod(schema))
    };
}) satisfies PageServerLoad;
kristianmandrup commented 1 month ago

Not sure I understand why defaults is undefined:

With the library installed and the adapter imported, all you need to do is wrap the schema with it:

import { superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';

const form = await superValidate(zod(schema));

The libraries in the list that requires defaults don’t have full introspection capabilities (yet), in which case you need to supply the default values for the form data as an option:

import { type } from 'arktype';

// Arktype schema, powerful stuff
const schema = type({
  name: 'string',
  email: 'email',
  tags: '(string>=2)[]>=3',
  score: 'integer>=0'
});

const defaults = { name: '', email: '', tags: [], score: 0 };

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

But zod shouldn't require defaults?

ciscoheat commented 1 month ago

Zod doesn't require defaults, and works in hundreds of tests, so I'm not sure why that happens for you.

kristianmandrup commented 1 month ago

I'm sorry, I think I'm confused about which superValidate to use. The error references the following:

/**
 * Validates a schema for data validation and usage in superForm.
 * @param data Data corresponding to a schema, or RequestEvent/FormData/URL. If falsy, the schema's default values will be used.
 * @param schema The schema to validate against.
 */
export async function superValidate(data, adapter, options) {
    if (data && 'superFormValidationLibrary' in data) {
        options = adapter;
        adapter = data;
        data = undefined;
    }
    const validator = adapter;
    const defaults = options?.defaults ?? validator.defaults;

and here validator is undefined.

So it seems there are the following variants?

import { superValidate } from 'sveltekit-superforms';
import { superValidate } from 'sveltekit-superforms/client';
import { superValidate } from 'sveltekit-superforms/server';

But both client and server references the root superValidate. Now the error seems to stem from calling it with a single argument zod(schema) which is the data argument, leaving the adapter argument empty and hence this part failing:

const defaults = options?.defaults ?? validator.defaults;

Strange.

kristianmandrup commented 1 month ago

Which is in line with the example in the docs:

import { error } from '@sveltejs/kit';

export const load = async ({ params }) => {
  // Replace with your database
  const user = db.users.findUnique({
    where: { id: params.id }
  });

  if (!user) error(404, 'Not found');

  const form = await superValidate(user, your_adapter(schema));

  // Always return { form } in load functions
  return { form };
};

So I guess I could use the following for initial load?

    return {
        loggedInUser: locals.user,
        allUsers: await getAllUsers(),
        passwordResetFormData: await superValidate({}, zod(schema))
    };
kristianmandrup commented 1 month ago

Works now :)