Skip to content

Commit

Permalink
feat(validation-errors): support flattening via `flattenValidationErr…
Browse files Browse the repository at this point in the history
…ors` 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"],
  }
}
```
  • Loading branch information
TheEdoRan authored Apr 9, 2024
1 parent 3eddac8 commit 9ae6764
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 7 deletions.
10 changes: 4 additions & 6 deletions packages/next-safe-action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -291,19 +292,16 @@ export const createSafeActionClient = <const ServerError = string>(
});
};

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,
};
39 changes: 38 additions & 1 deletion packages/next-safe-action/src/validation-errors.ts
Original file line number Diff line number Diff line change
@@ -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 = <const S extends Schema>(issues: ValidationIssue[]) => {
Expand Down Expand Up @@ -69,3 +73,36 @@ export function returnValidationErrors<S extends Schema>(
): never {
throw new ServerValidationError<S>(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<S>,
>(validationErrors?: VE) {
const flattened: FlattenedValidationErrors<S, VE> = {
rootErrors: [],
fieldErrors: {},
};

if (!validationErrors) {
return flattened;
}

for (const [key, value] of Object.entries<string[] | { _errors: string[] }>(validationErrors)) {
if (key === "_errors" && Array.isArray(value)) {
flattened.rootErrors = [...value];
} else {
if ("_errors" in value) {
flattened.fieldErrors[key as keyof Omit<VE, "_errors">] = [...value._errors];
}
}
}

return flattened;
}
11 changes: 11 additions & 0 deletions packages/next-safe-action/src/validation-errors.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,14 @@ export type ValidationErrors<S extends Schema> = Extend<ErrorList & SchemaErrors
export type BindArgsValidationErrors<BAS extends Schema[]> = (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<S extends Schema, VE extends ValidationErrors<S>> = {
rootErrors: string[];
fieldErrors: {
[K in keyof Omit<VE, "_errors">]?: string[];
};
};

0 comments on commit 9ae6764

Please sign in to comment.