From 9ae6764a68b0e91424bf1da2b9b0f94384213e4e Mon Sep 17 00:00:00 2001 From: Edoardo Ranghieri Date: Tue, 9 Apr 2024 17:58:47 +0200 Subject: [PATCH] feat(validation-errors): support flattening via `flattenValidationErrors` function (#100) Sometimes it's better to deal with a flattened error object instead of a formatted one, for instance when you don't need to use nested objects in validation schemas. This PR exports a function called `flattenValidationErrors` that does what it says. Be aware that it works just one level deep, as it discards nested schema errors. This is a known limitation of this approach, since it can't prevent key conflicts. Suppose this is a returned formatted `validationErrors` object: ```typescript validationErrors = { _errors: ["Global error"], username: { _errors: ["Too short", "Username is invalid"], }, email: { _errors: ["Email is invalid"], } } ``` After passing it to `flattenValidationErrors`: ```typescript import { flattenValidationErrors } from "next-safe-action"; const flattenedErrors = flattenValidationErrors(validationErrors); ``` It becomes this: ```typescript flattenedErrors = { rootErrors: ["Global error"], fieldErrors: { username: ["Too short", "Username is invalid"], email: ["Email is invalid"], } } ``` --- packages/next-safe-action/src/index.ts | 10 ++--- .../next-safe-action/src/validation-errors.ts | 39 ++++++++++++++++++- .../src/validation-errors.types.ts | 11 ++++++ 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/packages/next-safe-action/src/index.ts b/packages/next-safe-action/src/index.ts index 292ec353..c0078731 100644 --- a/packages/next-safe-action/src/index.ts +++ b/packages/next-safe-action/src/index.ts @@ -16,6 +16,7 @@ import { DEFAULT_SERVER_ERROR_MESSAGE, isError } from "./utils"; import { ServerValidationError, buildValidationErrors, + flattenValidationErrors, returnValidationErrors, } from "./validation-errors"; import type { BindArgsValidationErrors, ValidationErrors } from "./validation-errors.types"; @@ -291,19 +292,16 @@ export const createSafeActionClient = ( }); }; -export { - DEFAULT_SERVER_ERROR_MESSAGE, - returnValidationErrors, - type BindArgsValidationErrors, - type ValidationErrors, -}; +export { DEFAULT_SERVER_ERROR_MESSAGE, flattenValidationErrors, returnValidationErrors }; export type { ActionMetadata, + BindArgsValidationErrors, MiddlewareFn, MiddlewareResult, SafeActionClientOpts, SafeActionFn, SafeActionResult, ServerCodeFn, + ValidationErrors, }; diff --git a/packages/next-safe-action/src/validation-errors.ts b/packages/next-safe-action/src/validation-errors.ts index 788ede02..709b28a1 100644 --- a/packages/next-safe-action/src/validation-errors.ts +++ b/packages/next-safe-action/src/validation-errors.ts @@ -1,6 +1,10 @@ import type { ValidationIssue } from "@typeschema/core"; import type { Schema } from "@typeschema/main"; -import type { ErrorList, ValidationErrors } from "./validation-errors.types"; +import type { + ErrorList, + FlattenedValidationErrors, + ValidationErrors, +} from "./validation-errors.types"; // This function is used internally to build the validation errors object from a list of validation issues. export const buildValidationErrors = (issues: ValidationIssue[]) => { @@ -69,3 +73,36 @@ export function returnValidationErrors( ): never { throw new ServerValidationError(validationErrors); } + +/** + * Transform default formatted validation errors into flattened structure. + * `rootErrors` contains global errors, and `fieldErrors` contains errors for each field, + * one level deep. It skips errors for nested fields. + * @param {ValidationErrors} [validationErrors] Validation errors object + * @returns {FlattenedValidationErrors} Flattened validation errors + */ +export function flattenValidationErrors< + const S extends Schema, + const VE extends ValidationErrors, +>(validationErrors?: VE) { + const flattened: FlattenedValidationErrors = { + rootErrors: [], + fieldErrors: {}, + }; + + if (!validationErrors) { + return flattened; + } + + for (const [key, value] of Object.entries(validationErrors)) { + if (key === "_errors" && Array.isArray(value)) { + flattened.rootErrors = [...value]; + } else { + if ("_errors" in value) { + flattened.fieldErrors[key as keyof Omit] = [...value._errors]; + } + } + } + + return flattened; +} diff --git a/packages/next-safe-action/src/validation-errors.types.ts b/packages/next-safe-action/src/validation-errors.types.ts index a194c998..18ab07a9 100644 --- a/packages/next-safe-action/src/validation-errors.types.ts +++ b/packages/next-safe-action/src/validation-errors.types.ts @@ -24,3 +24,14 @@ export type ValidationErrors = Extend = (ValidationErrors< BAS[number] > | null)[]; + +/** + * Type of flattened validation errors. `rootErrors` contains global errors, and `fieldErrors` + * contains errors for each field, one level deep. + */ +export type FlattenedValidationErrors> = { + rootErrors: string[]; + fieldErrors: { + [K in keyof Omit]?: string[]; + }; +};