Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] [v7] Support custom validation errors shape #98

Closed
1 task done
herkulano opened this issue Apr 8, 2024 · 9 comments · Fixed by #101
Closed
1 task done

[FEATURE] [v7] Support custom validation errors shape #98

herkulano opened this issue Apr 8, 2024 · 9 comments · Fixed by #101
Labels
enhancement New feature or request
Milestone

Comments

@herkulano
Copy link

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

@herkulano herkulano added the question Further information is requested label Apr 8, 2024
@herkulano
Copy link
Author

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.

@TheEdoRan
Copy link
Owner

TheEdoRan commented Apr 8, 2024

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!

@herkulano
Copy link
Author

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.

@TheEdoRan
Copy link
Owner

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.

@herkulano
Copy link
Author

@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.

@TheEdoRan
Copy link
Owner

TheEdoRan commented Apr 10, 2024

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.

@herkulano
Copy link
Author

@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 😊

@TheEdoRan
Copy link
Owner

TheEdoRan commented Apr 10, 2024

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).

@TheEdoRan TheEdoRan changed the title [ASK] Support for flattened validation errors [FEATURE] [v7] Support for custom validation errors format Apr 10, 2024
@TheEdoRan TheEdoRan added this to the v7 milestone Apr 10, 2024
@TheEdoRan TheEdoRan added enhancement New feature or request and removed question Further information is requested labels Apr 10, 2024
TheEdoRan added a commit that referenced this issue Apr 16, 2024
…ormat (#101)

This PR adds the ability to return a custom validation errors format (both main argument validation errors and bind arguments validation errors) to the client, via `formatValidationErrors` in `schema` method and `formatBindArgsValidationErrors` in `bindArgsSchemas` method.

re #98
@TheEdoRan
Copy link
Owner

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

@TheEdoRan TheEdoRan changed the title [FEATURE] [v7] Support for custom validation errors format [FEATURE] [v7] Support custom validation errors format May 20, 2024
@TheEdoRan TheEdoRan changed the title [FEATURE] [v7] Support custom validation errors format [FEATURE] [v7] Support custom validation errors shape Jun 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
2 participants