TheEdoRan / next-safe-action

Type safe and validated Server Actions in your Next.js (App Router) project.

Home Page:https://next-safe-action.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[FEATURE] [v7] Support custom validation errors shape

herkulano opened this issue · comments

commented

Is there an existing issue for this?

  • I have searched the existing issues and found nothing that matches

Library version (optional)

No response

Ask a question

In React Aria Components the Form takes a flattened error list: https://react-spectrum.adobe.com/react-aria/forms.html#schema-validation

Any ideas on how to change the handling of validation errors from the library, instead of having to manipulate the result from the response?

Additional context

No response

commented

Conform uses yet another format for the errors: https://conform.guide/api/zod/parseWithZod

It would be nice to change the way schema is parsed so that it would be flexible for any case.

next-safe-action v7 (currently next branch/channel) follows Zod's format() output to build validation errors, since it correctly handles validation for nested objects in schemas, see #51.

I agree that in some cases it's better to just deal with a flattened error object, for instance when you don't need to use nested schemas. My idea is to export a flattenValidationErrors function that will flatten the formatted validation errors, so something like this:

Supposing we have defined this schema using Zod:

import { z } from "zod";

const schema = z.object({
  username: z.string().min(3).max(30),
  email: z.string().email(),
  age: z.number().positive(),
});

We can import flattenValidationErrors from the library and pass result.ValidationErrors to it.

import { flattenValidationErrors } from "next-safe-action";

const flattenedErrors = flattenValidationErrors(result.validationErrors);

flattenedErrors, in this case, will have this structure:

type FlattenedErrors = {
  formErrors: string[],
  fieldErrors: {
    username?: string[] | undefined,
    email?: string[] | undefined,
    age?: string[] | undefined,
  }
}

Please let me know your thoughts on this, thanks!

commented

It would be nice to have that directly in the action, so that it could be flexible and the frontend wouldn't need to do anything.

const editProfile = authActionClient
  .metadata({ actionName: "editProfile" })
  .schema(z.object({ newUsername: z.string() }), (errors) => flattenValidationErrors(errors).fieldErrors)
  .action(...);

I believe the server action should be responsible for giving back the correct values, and thinking of supporting react aria and useFormState or useActionState that expect the result to be already well formed.

Another issue with the schema is i18n. If you use something like zod-i18n-map for localization of the error messages we need to do this before performing the zod parse.

import { makeZodI18nMap } from "zod-i18n-map"

z.setErrorMap(makeZodI18nMap({ t }))

My understanding is that this will be fixed with v7 because the middleware functions can be run before the schema function:

const deleteUser = authActionClient
  .use(async ({ next, ctx }) => {
    z.setErrorMap(makeZodI18nMap({ t }))
    return next({ ctx });
  })
  .metadata({ actionName: "deleteUser" })
  .schema(z.void())
  .action(...);

If this is correct then the problem of the i18n of the schema is solved.

It would be nice to have that directly in the action

Yeah, I agree, this is a good idea. As a default though, I think it's good to keep Zod's emulated format function, so nested errors are not discarded.

If you use something like zod-i18n-map ... it will be fixed with v7 because the middleware functions can be run before the schema function

I'm not familiar with zod-i18n-map, but yes, middleware fns run before input validation. Just a note: schema method doesn't actually validate input data, you just pass a validation schema to it, and then the parsing is performed inside the action method.

commented

@TheEdoRan I thought that by using .schema(z.object({ newUsername: z.string() })) the schema would be validated server side and sending back the validationErrors from the server to the client.

If the only purpose of the schema is typing, I believe it would be clearer to have the schema as a type instead of a value:

const editProfile = authActionClient<SchemaXYZ>
  .metadata({ actionName: "editProfile" })
  .action(...)

I still think it would be nice to have the validation done on the server automatically.

the schema would be validated server side and sending back the validationErrors from the server to the client. ... If the only purpose of the schema is typing, I believe it would be clearer to have the schema as a type instead of a value

It's not the schema that gets validated server side, it's the input data, thanks to the schema. What you pass to schema is a validation schema function, that is then passed to the action method (returned by schema) as argument, to parse and validate the input data server side. So yes, it does something and it isn't just a type. What I'm saying is that the actual parsing is done inside action and not in schema, and middleware functions run before this step (unless you await the next function, as explained in the logging middleware example).

You can check out the relevant code here, for the schema method and here, for parsing and validation inside action method.

commented

@TheEdoRan got it! Thanks for explain it 🙏

If I understand correctly, in the recursive middleware function they only have access to the raw input, not to the validated input: https://github.com/TheEdoRan/next-safe-action/blob/next/packages/next-safe-action/src/index.ts#L110

I had a different model in my mind. I thought the sequence in the chained functions mattered, e.g.:

const editProfile = authActionClient
  .metadata({ actionName: "editProfile" })
  .use(...) // <-- runs before schema and only has access to the raw input
  .schema(...) // <-- input is validated and returns the validated input to continue the chain or returns errors to the client
  .use(...) // <-- runs after schema validation and has access to the validated input
  .action(...)

From what I understand this is what really happens:

const editProfile = authActionClient
  .metadata({ actionName: "editProfile" })
  .use(...) // <-- is put on a queue [0] to run before the schema validation
  .schema(...) // <-- sets the schema to be run after the middleware queue
  .use(...) // <-- is put on a queue [1] to run before the schema validation
  .action(...)

I feel this API can be misleading because of the chaining of functions, but maybe it's just me 😊

If I understand correctly, in the recursive middleware function they only have access to the raw input, not to the validated input

As arguments of middleware functions, yes, but they return a MiddlewareResult object when you call the next function, that extends the SafeActionResult with additional data. So, if you await the next function, you can access parsedInput and do whatever you want with it, after the execution of middleware stack and server code function. Obviously parsedInput is typed unknown in this case, because the middleware works for every action defined using it.

From what I understand this is what really happens

You can't use use after schema, only before it. schema returns bindArgsSchemas and action methods, as explained here.

I feel this API can be misleading because of the chaining of functions, but maybe it's just me

It works the same way as the tRPC middleware implementation, which I think is great (very composable, flexible and powerful).

So:

  • if you simply return next function at the end of your middleware functions body, the next middleware in the chain will execute;
  • if you await the next function and then return its result in your middleware functions body, the entire stack, from that point in the chain, of middleware fns/action function gets executed. Result object will contain information about the executed action (very useful, for instance, to log action execution infos).

next-safe-action now supports this feature in v7.0.0-next.21. Documentation for it is currently available here.