diff --git a/packages/next-safe-action/package.json b/packages/next-safe-action/package.json index d2437350..850d35cb 100644 --- a/packages/next-safe-action/package.json +++ b/packages/next-safe-action/package.json @@ -67,6 +67,7 @@ }, "devDependencies": { "@eslint/js": "^9.2.0", + "@sinclair/typebox": "^0.33.3", "@types/node": "^20.14.11", "@types/react": "^18.3.1", "@types/react-dom": "18.3.0", @@ -92,8 +93,9 @@ "react": ">= 18.2.0", "react-dom": ">= 18.2.0", "valibot": ">= 0.36.0", + "yup": ">= 1.0.0", "zod": ">= 3.0.0", - "yup": ">= 1.0.0" + "@sinclair/typebox": ">= 0.33.3" }, "peerDependenciesMeta": { "zod": { @@ -104,6 +106,9 @@ }, "yup": { "optional": true + }, + "@sinclair/typebox": { + "optional": true } }, "repository": { diff --git a/packages/next-safe-action/src/adapters/typebox.ts b/packages/next-safe-action/src/adapters/typebox.ts new file mode 100644 index 00000000..967b888e --- /dev/null +++ b/packages/next-safe-action/src/adapters/typebox.ts @@ -0,0 +1,52 @@ +// Code courtesy of https://github.com/decs/typeschema/blob/main/packages/typebox/src/validation.ts + +// MIT License + +// Copyright (c) 2023 André Costa + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import type { TSchema } from "@sinclair/typebox"; +import { TypeCompiler } from "@sinclair/typebox/compiler"; +import type { IfInstalled, Infer, ValidationAdapter } from "./types"; + +class TypeboxAdapter implements ValidationAdapter { + async validate>(schema: S, data: unknown) { + const result = TypeCompiler.Compile(schema); + + if (result.Check(data)) { + return { + success: true, + data: data as Infer, + } as const; + } + + return { + success: false, + issues: [...result.Errors(data)].map(({ message, path }) => ({ + message, + path: path.split("/").slice(1), + })), + } as const; + } +} + +export function typeboxAdapter() { + return new TypeboxAdapter(); +} diff --git a/packages/next-safe-action/src/adapters/types.ts b/packages/next-safe-action/src/adapters/types.ts index 8a0bda61..cfe857d4 100644 --- a/packages/next-safe-action/src/adapters/types.ts +++ b/packages/next-safe-action/src/adapters/types.ts @@ -1,5 +1,6 @@ // Code courtesy of/highly inspired by https://github.com/decs/typeschema +import type { Static, TSchema } from "@sinclair/typebox"; import type { GenericSchema, GenericSchemaAsync, InferInput, InferOutput } from "valibot"; import type { InferType, Schema as YupSchema } from "yup"; import type { z } from "zod"; @@ -10,7 +11,8 @@ export type Schema = | IfInstalled | IfInstalled | IfInstalled - | IfInstalled; + | IfInstalled + | IfInstalled; export type Infer = S extends IfInstalled @@ -21,7 +23,9 @@ export type Infer = ? InferOutput : S extends IfInstalled ? InferType - : never; + : S extends IfInstalled + ? Static + : never; export type InferIn = S extends IfInstalled @@ -32,7 +36,9 @@ export type InferIn = ? InferInput : S extends IfInstalled ? InferType - : never; + : S extends IfInstalled + ? Static + : never; export type InferArray = { [K in keyof BAS]: Infer; @@ -71,4 +77,9 @@ export interface ValidationAdapter { schema: S, data: unknown ): Promise<{ success: true; data: Infer } | { success: false; issues: ValidationIssue[] }>; + // typebox + validate>( + schema: S, + data: unknown + ): Promise<{ success: true; data: Infer } | { success: false; issues: ValidationIssue[] }>; } diff --git a/packages/next-safe-action/src/adapters/valibot.ts b/packages/next-safe-action/src/adapters/valibot.ts index 8d48a8cd..87164791 100644 --- a/packages/next-safe-action/src/adapters/valibot.ts +++ b/packages/next-safe-action/src/adapters/valibot.ts @@ -1,5 +1,27 @@ // Code courtesy of https://github.com/decs/typeschema/blob/main/packages/valibot/src/validation.ts +// MIT License + +// Copyright (c) 2023 André Costa + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + import { getDotPath, safeParseAsync, type GenericSchema, type GenericSchemaAsync } from "valibot"; import type { IfInstalled, Infer, ValidationAdapter } from "./types"; diff --git a/packages/next-safe-action/src/adapters/yup.ts b/packages/next-safe-action/src/adapters/yup.ts index d9ef096b..c771dcbc 100644 --- a/packages/next-safe-action/src/adapters/yup.ts +++ b/packages/next-safe-action/src/adapters/yup.ts @@ -1,4 +1,26 @@ -// https://github.com/decs/typeschema/blob/main/packages/yup/src/validation.ts +// Code courtesy of https://github.com/decs/typeschema/blob/main/packages/yup/src/validation.ts + +// MIT License + +// Copyright (c) 2023 André Costa + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import type { Schema as YupSchema } from "yup"; import { ValidationError } from "yup"; @@ -22,7 +44,7 @@ class YupAdapter implements ValidationAdapter { issues: [ { message, - path: path && path.length > 0 ? [path] : undefined, + path: path && path.length > 0 ? path.split(".") : undefined, }, ] as ValidationIssue[], } as const; diff --git a/packages/next-safe-action/src/adapters/zod.ts b/packages/next-safe-action/src/adapters/zod.ts index f41c0d01..dc796b46 100644 --- a/packages/next-safe-action/src/adapters/zod.ts +++ b/packages/next-safe-action/src/adapters/zod.ts @@ -1,10 +1,30 @@ -// https://github.com/decs/typeschema/blob/main/packages/zod/src/validation.ts +// Code courtesy of https://github.com/decs/typeschema/blob/main/packages/zod/src/validation.ts + +// MIT License + +// Copyright (c) 2023 André Costa + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import type { z } from "zod"; import type { IfInstalled, Infer, ValidationAdapter } from "./types"; -export type ZodSchema = z.ZodType; - class ZodAdapter implements ValidationAdapter { async validate>(schema: S, data: unknown) { const result = await schema.safeParseAsync(data); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e4865a2..1e42c9fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: '@eslint/js': specifier: ^9.2.0 version: 9.3.0 + '@sinclair/typebox': + specifier: ^0.33.3 + version: 0.33.3 '@types/node': specifier: ^20.14.11 version: 20.14.11 @@ -1027,6 +1030,9 @@ packages: peerDependencies: semantic-release: '>=20.1.0' + '@sinclair/typebox@0.33.3': + resolution: {integrity: sha512-2MputLKNw0OxPGWF8KWkLt8B/csTZUkK2tCtiZwJT3NtVOfYPVi6ZEchZAHEHre/qJ4skcS9fL7TuHG36Dk3WQ==} + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -4687,6 +4693,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@sinclair/typebox@0.33.3': {} + '@sindresorhus/is@4.6.0': {} '@sindresorhus/is@5.6.0': {} @@ -5571,8 +5579,8 @@ snapshots: '@typescript-eslint/parser': 7.10.0(eslint@8.57.0)(typescript@5.5.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) eslint-plugin-react: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -5596,13 +5604,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 4.3.4 enhanced-resolve: 5.16.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.13.1 @@ -5613,18 +5621,18 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.10.0(eslint@8.57.0)(typescript@5.5.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -5634,7 +5642,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.10.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 diff --git a/website/docs/getting-started.md b/website/docs/getting-started.md index 1bfe41f5..2d39a4b3 100644 --- a/website/docs/getting-started.md +++ b/website/docs/getting-started.md @@ -10,7 +10,7 @@ description: Getting started with next-safe-action version 7. - Next.js >= 14 (>= 15 for [`useStateAction`](/docs/execution/hooks/usestateaction) hook) - React >= 18.2.0 - TypeScript >= 5 -- Zod or Valibot or Yup +- Zod or Valibot or Yup or TypeBox ::: **next-safe-action** provides a typesafe Server Actions implementation for Next.js App Router. diff --git a/website/docs/recipes/validation-libraries-support.md b/website/docs/recipes/validation-libraries-support.md index 7ecc208f..1be70dc8 100644 --- a/website/docs/recipes/validation-libraries-support.md +++ b/website/docs/recipes/validation-libraries-support.md @@ -7,14 +7,20 @@ description: Use a validation library of your choice with next-safe-action. Starting from version 6.0.0, and up to version 7.1.3, next-safe-action used [TypeSchema](https://typeschema.com/) to enable support for multiple validation libraries. This has worked pretty well, but caused some issues too, such as the [Edge Runtime incompatibility](/docs/troubleshooting#typeschema-issues-with-edge-runtime) or [lack of support for TypeScript >= 5.5](/docs/troubleshooting#schema-and-parsedinput-are-typed-any-broken-types-and-build-issues). -To solve these issues, next-safe-action v7.2.0 and later versions ship with a built-in modular support for multiple validation libraries, at this time: Zod, Valibot and Yup. +To solve these issues, next-safe-action v7.2.0 and later versions ship with a built-in modular support for multiple validation libraries, at this time: +- Zod +- Valibot +- Yup +- TypeBox + +## Instructions If you used a TypeSchema adapter before, you should uninstall it, since you just need the validation library of your choice from now on. The configuration is pretty simple. If you use Zod, you don't have to do anything. If you choose to use Valibot or Yup, other than obviously installing the validation library itself, you need to specify the correct validation adapter when you're initializing the safe action client: -For Valibot: +### Valibot ```typescript title="@/lib/safe-action.ts" import { createSafeActionClient } from "next-safe-action"; @@ -26,7 +32,7 @@ export const actionClient = createSafeActionClient({ }); ``` -For Yup: +### Yup ```typescript title="@/lib/safe-action.ts" import { createSafeActionClient } from "next-safe-action"; @@ -38,6 +44,18 @@ export const actionClient = createSafeActionClient({ }); ``` +### TypeBox + +```typescript title="@/lib/safe-action.ts" +import { createSafeActionClient } from "next-safe-action"; +import { typeboxAdapter } from "next-safe-action/adapters/typebox"; // import the adapter + +export const actionClient = createSafeActionClient({ + validationAdapter: typeboxAdapter(), // <-- and then pass it to the client + // other options here... +}); +``` + And you're done! You could also do the same thing for Zod, but it's not required right now, as it's the default validation library. If you want more information about the TypeSchema to built-in system change, there's a dedicated discussion on GitHub for that, [here](https://github.com/TheEdoRan/next-safe-action/discussions/201).