From 3f30621cac6587d48d0ded108cbe3f33af541614 Mon Sep 17 00:00:00 2001 From: Edoardo Ranghieri Date: Thu, 29 Aug 2024 03:35:42 +0200 Subject: [PATCH 1/6] feat: add `outputSchema` method for output data validation --- .../next-safe-action/src/action-builder.ts | 86 +++++++++------- packages/next-safe-action/src/index.ts | 5 +- .../src/safe-action-client.ts | 99 +++++++++++++------ packages/next-safe-action/src/utils.ts | 28 +++++- .../next-safe-action/src/validation-errors.ts | 3 +- .../src/validation-errors.types.ts | 5 - 6 files changed, 144 insertions(+), 82 deletions(-) diff --git a/packages/next-safe-action/src/action-builder.ts b/packages/next-safe-action/src/action-builder.ts index 5cea50ca..31cef41a 100644 --- a/packages/next-safe-action/src/action-builder.ts +++ b/packages/next-safe-action/src/action-builder.ts @@ -14,7 +14,7 @@ import type { ServerCodeFn, StateServerCodeFn, } from "./index.types"; -import { ActionMetadataError, DEFAULT_SERVER_ERROR_MESSAGE, isError } from "./utils"; +import { ActionMetadataError, ActionOutputDataError, DEFAULT_SERVER_ERROR_MESSAGE, isError } from "./utils"; import type { MaybePromise } from "./utils.types"; import { ActionServerValidationError, ActionValidationError, buildValidationErrors } from "./validation-errors"; import type { @@ -27,18 +27,20 @@ import type { export function actionBuilder< ServerError, MetadataSchema extends Schema | undefined = undefined, - MD = MetadataSchema extends Schema ? Infer : undefined, + MD = MetadataSchema extends Schema ? Infer : undefined, // metadata type (inferred from metadata schema) Ctx extends object = {}, - SF extends (() => Promise) | undefined = undefined, // schema function - S extends Schema | undefined = SF extends Function ? Awaited> : undefined, + ISF extends (() => Promise) | undefined = undefined, // input schema function + IS extends Schema | undefined = ISF extends Function ? Awaited> : undefined, // input schema + OS extends Schema | undefined = undefined, // output schema const BAS extends readonly Schema[] = [], CVE = undefined, CBAVE = undefined, >(args: { - schemaFn?: SF; + inputSchemaFn?: ISF; bindArgsSchemas?: BAS; + outputSchema?: OS; validationAdapter: ValidationAdapter; - handleValidationErrorsShape: HandleValidationErrorsShapeFn; + handleValidationErrorsShape: HandleValidationErrorsShapeFn; handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn; metadataSchema: MetadataSchema; metadata: MD; @@ -53,29 +55,29 @@ export function actionBuilder< const bindArgsSchemas = (args.bindArgsSchemas ?? []) as BAS; function buildAction({ withState }: { withState: false }): { - action: ( - serverCodeFn: ServerCodeFn, - utils?: SafeActionUtils - ) => SafeActionFn; + action: : any>( + serverCodeFn: ServerCodeFn, + utils?: SafeActionUtils + ) => SafeActionFn; }; function buildAction({ withState }: { withState: true }): { - action: ( - serverCodeFn: StateServerCodeFn, - utils?: SafeActionUtils - ) => SafeStateActionFn; + action: : any>( + serverCodeFn: StateServerCodeFn, + utils?: SafeActionUtils + ) => SafeStateActionFn; }; function buildAction({ withState }: { withState: boolean }) { return { - action: ( + action: : any>( serverCodeFn: - | ServerCodeFn - | StateServerCodeFn, - utils?: SafeActionUtils + | ServerCodeFn + | StateServerCodeFn, + utils?: SafeActionUtils ) => { return async (...clientInputs: unknown[]) => { let currentCtx: object = {}; const middlewareResult: MiddlewareResult = { success: false }; - type PrevResult = SafeActionResult | undefined; + type PrevResult = SafeActionResult | undefined; let prevResult: PrevResult | undefined = undefined; const parsedInputDatas: any[] = []; let frameworkError: Error | null = null; @@ -107,10 +109,10 @@ export function actionBuilder< if (idx === 0) { if (args.metadataSchema) { // Validate metadata input. - if (!(await args.validationAdapter.validate(args.metadataSchema, args.metadata)).success) { - throw new ActionMetadataError( - "Invalid metadata input. Please be sure to pass metadata via `metadata` method before defining the action." - ); + const parsedMd = await args.validationAdapter.validate(args.metadataSchema, args.metadata); + + if (!parsedMd.success) { + throw new ActionMetadataError(buildValidationErrors(parsedMd.issues)); } } } @@ -124,7 +126,6 @@ export function actionBuilder< metadata: args.metadata, next: async (nextOpts) => { currentCtx = deepmerge(currentCtx, nextOpts?.ctx ?? {}); - // currentCtx = { ...cloneDeep(currentCtx), ...(nextOpts?.ctx ?? {}) }; await executeMiddlewareStack(idx + 1); return middlewareResult; }, @@ -137,7 +138,7 @@ export function actionBuilder< // Last client input in the array, main argument (no bind arg). if (i === clientInputs.length - 1) { // If schema is undefined, set parsed data to undefined. - if (typeof args.schemaFn === "undefined") { + if (typeof args.inputSchemaFn === "undefined") { return { success: true, data: undefined, @@ -145,7 +146,7 @@ export function actionBuilder< } // Otherwise, parse input with the schema. - return args.validationAdapter.validate(await args.schemaFn(), input); + return args.validationAdapter.validate(await args.inputSchemaFn(), input); } // Otherwise, we're processing bind args client inputs. @@ -172,7 +173,7 @@ export function actionBuilder< hasBindValidationErrors = true; } else { // Otherwise, we're processing the non-bind argument (the last one) in the array. - const validationErrors = buildValidationErrors(parsedInput.issues); + const validationErrors = buildValidationErrors(parsedInput.issues); middlewareResult.validationErrors = await Promise.resolve( args.handleValidationErrorsShape(validationErrors) @@ -193,11 +194,11 @@ export function actionBuilder< } // @ts-expect-error - const scfArgs: Parameters> = []; + const scfArgs: Parameters> = []; // Server code function always has this object as the first argument. scfArgs[0] = { - parsedInput: parsedInputDatas.at(-1) as S extends Schema ? Infer : undefined, + parsedInput: parsedInputDatas.at(-1) as IS extends Schema ? Infer : undefined, bindArgsParsedInputs: parsedInputDatas.slice(0, -1) as InferArray, ctx: currentCtx as Ctx, metadata: args.metadata, @@ -211,6 +212,15 @@ export function actionBuilder< const data = await serverCodeFn(...scfArgs); + // If a `outputSchema` is passed, validate the action return value. + if (typeof args.outputSchema !== "undefined") { + const parsedData = await args.validationAdapter.validate(args.outputSchema, data); + + if (!parsedData.success) { + throw new ActionOutputDataError(buildValidationErrors(parsedData.issues)); + } + } + middlewareResult.success = true; middlewareResult.data = data; middlewareResult.parsedInput = parsedInputDatas.at(-1); @@ -227,7 +237,7 @@ export function actionBuilder< // If error is `ActionServerValidationError`, return `validationErrors` as if schema validation would fail. if (e instanceof ActionServerValidationError) { - const ve = e.validationErrors as ValidationErrors; + const ve = e.validationErrors as ValidationErrors; middlewareResult.validationErrors = await Promise.resolve(args.handleValidationErrorsShape(ve)); } else { // If error is not an instance of Error, wrap it in an Error object with @@ -269,9 +279,9 @@ export function actionBuilder< data: undefined, metadata: args.metadata, ctx: currentCtx as Ctx, - clientInput: clientInputs.at(-1) as S extends Schema ? InferIn : undefined, + clientInput: clientInputs.at(-1) as IS extends Schema ? InferIn : undefined, bindArgsClientInputs: (bindArgsSchemas.length ? clientInputs.slice(0, -1) : []) as InferInArray, - parsedInput: parsedInputDatas.at(-1) as S extends Schema ? Infer : undefined, + parsedInput: parsedInputDatas.at(-1) as IS extends Schema ? Infer : undefined, bindArgsParsedInputs: parsedInputDatas.slice(0, -1) as InferArray, hasRedirected: isRedirectError(frameworkError), hasNotFound: isNotFoundError(frameworkError), @@ -282,7 +292,7 @@ export function actionBuilder< utils?.onSettled?.({ metadata: args.metadata, ctx: currentCtx as Ctx, - clientInput: clientInputs.at(-1) as S extends Schema ? InferIn : undefined, + clientInput: clientInputs.at(-1) as IS extends Schema ? InferIn : undefined, bindArgsClientInputs: (bindArgsSchemas.length ? clientInputs.slice(0, -1) : []) as InferInArray, result: {}, hasRedirected: isRedirectError(frameworkError), @@ -295,7 +305,7 @@ export function actionBuilder< throw frameworkError; } - const actionResult: SafeActionResult = {}; + const actionResult: SafeActionResult = {}; if (typeof middlewareResult.validationErrors !== "undefined") { // Throw validation errors if either `throwValidationErrors` property at the action or instance level is `true`. @@ -333,9 +343,9 @@ export function actionBuilder< metadata: args.metadata, ctx: currentCtx as Ctx, data: actionResult.data as Data, - clientInput: clientInputs.at(-1) as S extends Schema ? InferIn : undefined, + clientInput: clientInputs.at(-1) as IS extends Schema ? InferIn : undefined, bindArgsClientInputs: (bindArgsSchemas.length ? clientInputs.slice(0, -1) : []) as InferInArray, - parsedInput: parsedInputDatas.at(-1) as S extends Schema ? Infer : undefined, + parsedInput: parsedInputDatas.at(-1) as IS extends Schema ? Infer : undefined, bindArgsParsedInputs: parsedInputDatas.slice(0, -1) as InferArray, hasRedirected: false, hasNotFound: false, @@ -346,7 +356,7 @@ export function actionBuilder< utils?.onError?.({ metadata: args.metadata, ctx: currentCtx as Ctx, - clientInput: clientInputs.at(-1) as S extends Schema ? InferIn : undefined, + clientInput: clientInputs.at(-1) as IS extends Schema ? InferIn : undefined, bindArgsClientInputs: (bindArgsSchemas.length ? clientInputs.slice(0, -1) : []) as InferInArray, error: actionResult, }) @@ -358,7 +368,7 @@ export function actionBuilder< utils?.onSettled?.({ metadata: args.metadata, ctx: currentCtx as Ctx, - clientInput: clientInputs.at(-1) as S extends Schema ? InferIn : undefined, + clientInput: clientInputs.at(-1) as IS extends Schema ? InferIn : undefined, bindArgsClientInputs: (bindArgsSchemas.length ? clientInputs.slice(0, -1) : []) as InferInArray, result: actionResult, hasRedirected: false, diff --git a/packages/next-safe-action/src/index.ts b/packages/next-safe-action/src/index.ts index b40e2417..e116f60c 100644 --- a/packages/next-safe-action/src/index.ts +++ b/packages/next-safe-action/src/index.ts @@ -11,7 +11,7 @@ import { } from "./validation-errors"; export { createMiddleware } from "./middleware"; -export { ActionMetadataError, DEFAULT_SERVER_ERROR_MESSAGE } from "./utils"; +export { ActionOutputDataError as ActionDataError, ActionMetadataError, DEFAULT_SERVER_ERROR_MESSAGE } from "./utils"; export { ActionValidationError, flattenBindArgsValidationErrors, @@ -59,8 +59,9 @@ export const createSafeActionClient = < middlewareFns: [async ({ next }) => next({ ctx: {} })], handleServerErrorLog, handleReturnedServerError, - schemaFn: undefined, + inputSchemaFn: undefined, bindArgsSchemas: [], + outputSchema: undefined, validationAdapter: createOpts?.validationAdapter ?? zodAdapter(), // use zod adapter by default ctxType: {}, metadataSchema: (createOpts?.defineMetadataSchema?.() ?? undefined) as MetadataSchema, diff --git a/packages/next-safe-action/src/safe-action-client.ts b/packages/next-safe-action/src/safe-action-client.ts index 3d7e9f83..7a8b0e49 100644 --- a/packages/next-safe-action/src/safe-action-client.ts +++ b/packages/next-safe-action/src/safe-action-client.ts @@ -22,10 +22,11 @@ export class SafeActionClient< ServerError, ODVES extends DVES | undefined, // override default validation errors shape MetadataSchema extends Schema | undefined = undefined, - MD = MetadataSchema extends Schema ? Infer : undefined, + MD = MetadataSchema extends Schema ? Infer : undefined, // metadata type (inferred from metadata schema) Ctx extends object = {}, - SF extends (() => Promise) | undefined = undefined, // schema function - S extends Schema | undefined = SF extends Function ? Awaited> : undefined, + ISF extends (() => Promise) | undefined = undefined, // input schema function + IS extends Schema | undefined = ISF extends Function ? Awaited> : undefined, // input schema + OS extends Schema | undefined = undefined, // output schema const BAS extends readonly Schema[] = [], CVE = undefined, const CBAVE = undefined, @@ -39,11 +40,12 @@ export class SafeActionClient< readonly #middlewareFns: MiddlewareFn[]; readonly #metadataSchema: MetadataSchema; readonly #metadata: MD; - readonly #schemaFn: SF; + readonly #inputSchemaFn: ISF; + readonly #outputSchema: OS; readonly #ctxType: Ctx; readonly #bindArgsSchemas: BAS; readonly #validationAdapter: ValidationAdapter; - readonly #handleValidationErrorsShape: HandleValidationErrorsShapeFn; + readonly #handleValidationErrorsShape: HandleValidationErrorsShapeFn; readonly #handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn; readonly #defaultValidationErrorsShape: ODVES; readonly #throwValidationErrors: boolean; @@ -53,10 +55,11 @@ export class SafeActionClient< middlewareFns: MiddlewareFn[]; metadataSchema: MetadataSchema; metadata: MD; - schemaFn: SF; + inputSchemaFn: ISF; + outputSchema: OS; bindArgsSchemas: BAS; validationAdapter: ValidationAdapter; - handleValidationErrorsShape: HandleValidationErrorsShapeFn; + handleValidationErrorsShape: HandleValidationErrorsShapeFn; handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn; ctxType: Ctx; } & Required< @@ -71,7 +74,8 @@ export class SafeActionClient< this.#handleReturnedServerError = opts.handleReturnedServerError; this.#metadataSchema = opts.metadataSchema; this.#metadata = opts.metadata; - this.#schemaFn = (opts.schemaFn ?? undefined) as SF; + this.#inputSchemaFn = (opts.inputSchemaFn ?? undefined) as ISF; + this.#outputSchema = opts.outputSchema; this.#bindArgsSchemas = opts.bindArgsSchemas ?? []; this.#validationAdapter = opts.validationAdapter; this.#ctxType = opts.ctxType as unknown as Ctx; @@ -94,7 +98,8 @@ export class SafeActionClient< handleServerErrorLog: this.#handleServerErrorLog, metadataSchema: this.#metadataSchema, metadata: this.#metadata, - schemaFn: this.#schemaFn, + inputSchemaFn: this.#inputSchemaFn, + outputSchema: this.#outputSchema, bindArgsSchemas: this.#bindArgsSchemas, validationAdapter: this.#validationAdapter, handleValidationErrorsShape: this.#handleValidationErrorsShape, @@ -118,8 +123,9 @@ export class SafeActionClient< handleServerErrorLog: this.#handleServerErrorLog, metadataSchema: this.#metadataSchema, metadata: data, - schemaFn: this.#schemaFn, + inputSchemaFn: this.#inputSchemaFn, bindArgsSchemas: this.#bindArgsSchemas, + outputSchema: this.#outputSchema, validationAdapter: this.#validationAdapter, handleValidationErrorsShape: this.#handleValidationErrorsShape, handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape, @@ -131,19 +137,19 @@ export class SafeActionClient< /** * Define the input validation schema for the action. - * @param schema Input validation schema + * @param inputSchema Input validation schema * @param utils Optional utils object * - * {@link https://next-safe-action.dev/docs/safe-action-client/instance-methods#schema See docs for more information} + * {@link https://next-safe-action.dev/docs/safe-action-client/instance-methods#inputschema See docs for more information} */ schema< - OS extends Schema | ((prevSchema: S) => Promise), - AS extends Schema = OS extends (prevSchema: S) => Promise ? Awaited> : OS, // actual schema - OCVE = ODVES extends "flattened" ? FlattenedValidationErrors> : ValidationErrors, + OIS extends Schema | ((prevSchema: IS) => Promise), // override input schema + AIS extends Schema = OIS extends (prevSchema: IS) => Promise ? Awaited> : OIS, // actual input schema + OCVE = ODVES extends "flattened" ? FlattenedValidationErrors> : ValidationErrors, >( - schema: OS, + inputSchema: OIS, utils?: { - handleValidationErrorsShape?: HandleValidationErrorsShapeFn; + handleValidationErrorsShape?: HandleValidationErrorsShapeFn; } ) { return new SafeActionClient({ @@ -153,17 +159,18 @@ export class SafeActionClient< metadataSchema: this.#metadataSchema, metadata: this.#metadata, // @ts-expect-error - schemaFn: (schema[Symbol.toStringTag] === "AsyncFunction" + inputSchemaFn: (inputSchema[Symbol.toStringTag] === "AsyncFunction" ? async () => { - const prevSchema = await this.#schemaFn?.(); + const prevSchema = await this.#inputSchemaFn?.(); // @ts-expect-error - return schema(prevSchema as S) as AS; + return inputSchema(prevSchema as IS) as AIS; } - : async () => schema) as SF, + : async () => inputSchema) as ISF, bindArgsSchemas: this.#bindArgsSchemas, + outputSchema: this.#outputSchema, validationAdapter: this.#validationAdapter, handleValidationErrorsShape: (utils?.handleValidationErrorsShape ?? - this.#handleValidationErrorsShape) as HandleValidationErrorsShapeFn, + this.#handleValidationErrorsShape) as HandleValidationErrorsShapeFn, handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape, ctxType: {} as Ctx, defaultValidationErrorsShape: this.#defaultValidationErrorsShape, @@ -176,7 +183,7 @@ export class SafeActionClient< * @param bindArgsSchemas Bind args input validation schemas * @param utils Optional utils object * - * {@link https://next-safe-action.dev/docs/safe-action-client/instance-methods#schema See docs for more information} + * {@link https://next-safe-action.dev/docs/safe-action-client/instance-methods#bindargsschemas See docs for more information} */ bindArgsSchemas< const OBAS extends readonly Schema[], @@ -193,8 +200,9 @@ export class SafeActionClient< handleServerErrorLog: this.#handleServerErrorLog, metadataSchema: this.#metadataSchema, metadata: this.#metadata, - schemaFn: this.#schemaFn, + inputSchemaFn: this.#inputSchemaFn, bindArgsSchemas, + outputSchema: this.#outputSchema, validationAdapter: this.#validationAdapter, handleValidationErrorsShape: this.#handleValidationErrorsShape, handleBindArgsValidationErrorsShape: (utils?.handleBindArgsValidationErrorsShape ?? @@ -205,6 +213,31 @@ export class SafeActionClient< }); } + /** + * Define the output data validation schema for the action. + * @param schema Output data validation schema + * + * {@link https://next-safe-action.dev/docs/safe-action-client/instance-methods#outputschema See docs for more information} + */ + outputSchema(dataSchema: OOS) { + return new SafeActionClient({ + middlewareFns: this.#middlewareFns, + handleReturnedServerError: this.#handleReturnedServerError, + handleServerErrorLog: this.#handleServerErrorLog, + metadataSchema: this.#metadataSchema, + metadata: this.#metadata, + inputSchemaFn: this.#inputSchemaFn, + bindArgsSchemas: this.#bindArgsSchemas, + outputSchema: dataSchema, + validationAdapter: this.#validationAdapter, + handleValidationErrorsShape: this.#handleValidationErrorsShape, + handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape, + ctxType: {} as Ctx, + defaultValidationErrorsShape: this.#defaultValidationErrorsShape, + throwValidationErrors: this.#throwValidationErrors, + }); + } + /** * Define the action. * @param serverCodeFn Code that will be executed on the **server side** @@ -212,9 +245,9 @@ export class SafeActionClient< * * {@link https://next-safe-action.dev/docs/safe-action-client/instance-methods#action--stateaction See docs for more information} */ - action( - serverCodeFn: ServerCodeFn, - utils?: SafeActionUtils + action : any>( + serverCodeFn: ServerCodeFn, + utils?: SafeActionUtils ) { return actionBuilder({ handleReturnedServerError: this.#handleReturnedServerError, @@ -223,8 +256,9 @@ export class SafeActionClient< ctxType: this.#ctxType, metadataSchema: this.#metadataSchema, metadata: this.#metadata, - schemaFn: this.#schemaFn, + inputSchemaFn: this.#inputSchemaFn, bindArgsSchemas: this.#bindArgsSchemas, + outputSchema: this.#outputSchema, validationAdapter: this.#validationAdapter, handleValidationErrorsShape: this.#handleValidationErrorsShape, handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape, @@ -240,9 +274,9 @@ export class SafeActionClient< * * {@link https://next-safe-action.dev/docs/safe-action-client/instance-methods#action--stateaction See docs for more information} */ - stateAction( - serverCodeFn: StateServerCodeFn, - utils?: SafeActionUtils + stateAction : any>( + serverCodeFn: StateServerCodeFn, + utils?: SafeActionUtils ) { return actionBuilder({ handleReturnedServerError: this.#handleReturnedServerError, @@ -251,8 +285,9 @@ export class SafeActionClient< ctxType: this.#ctxType, metadataSchema: this.#metadataSchema, metadata: this.#metadata, - schemaFn: this.#schemaFn, + inputSchemaFn: this.#inputSchemaFn, bindArgsSchemas: this.#bindArgsSchemas, + outputSchema: this.#outputSchema, validationAdapter: this.#validationAdapter, handleValidationErrorsShape: this.#handleValidationErrorsShape, handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape, diff --git a/packages/next-safe-action/src/utils.ts b/packages/next-safe-action/src/utils.ts index 78def49c..12558529 100644 --- a/packages/next-safe-action/src/utils.ts +++ b/packages/next-safe-action/src/utils.ts @@ -1,3 +1,6 @@ +import type { Schema } from "./adapters/types"; +import type { ValidationErrors } from "./validation-errors.types"; + export const DEFAULT_SERVER_ERROR_MESSAGE = "Something went wrong while executing the operation."; export const isError = (error: unknown): error is Error => error instanceof Error; @@ -6,9 +9,28 @@ export const isError = (error: unknown): error is Error => error instanceof Erro * This error is thrown when an action's metadata input is invalid, i.e. when there's a mismatch between the * type of the metadata schema returned from `defineMetadataSchema` and the actual input. */ -export class ActionMetadataError extends Error { - constructor(message: string) { - super(message); +export class ActionMetadataError extends Error { + public validationErrors: ValidationErrors; + + constructor(validationErrors: ValidationErrors) { + super("Invalid metadata input. Please be sure to pass metadata via `metadata` method before defining the action."); this.name = "ActionMetadataError"; + this.validationErrors = validationErrors; + } +} + +/** + * This error is thrown when an action's data (output) is invalid, i.e. when there's a mismatch between the + * type of the data schema passed to `dataSchema` method and the actual return of the action. + */ +export class ActionOutputDataError extends Error { + public validationErrors: ValidationErrors; + + constructor(validationErrors: ValidationErrors) { + super( + "Invalid action data (output). Please be sure to return data following the shape of the schema passed to `dataSchema` method." + ); + this.name = "ActionOutputDataError"; + this.validationErrors = validationErrors; } } diff --git a/packages/next-safe-action/src/validation-errors.ts b/packages/next-safe-action/src/validation-errors.ts index 130e3fd5..a3db8650 100644 --- a/packages/next-safe-action/src/validation-errors.ts +++ b/packages/next-safe-action/src/validation-errors.ts @@ -1,11 +1,10 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */ -import type { Schema } from "./adapters/types"; +import type { Schema, ValidationIssue } from "./adapters/types"; import type { FlattenedBindArgsValidationErrors, FlattenedValidationErrors, ValidationErrors, - ValidationIssue, } from "./validation-errors.types"; // This function is used internally to build the validation errors object from a list of validation issues. diff --git a/packages/next-safe-action/src/validation-errors.types.ts b/packages/next-safe-action/src/validation-errors.types.ts index 9288800f..d225da2b 100644 --- a/packages/next-safe-action/src/validation-errors.types.ts +++ b/packages/next-safe-action/src/validation-errors.types.ts @@ -1,11 +1,6 @@ import type { Infer, Schema } from "./adapters/types"; import type { Prettify } from "./utils.types"; -export type ValidationIssue = { - message: string; - path?: Array; -}; - // Object with an optional list of validation errors. type VEList = Prettify<{ _errors?: string[] }>; From 671aede902aaee48e44bec60ab74fa6781f7a231 Mon Sep 17 00:00:00 2001 From: Edoardo Ranghieri Date: Thu, 29 Aug 2024 03:36:03 +0200 Subject: [PATCH 2/6] test: add `outputSchema` tests --- .../src/__tests__/happy-path.test.ts | 24 ++++--- .../src/__tests__/validation-errors.test.ts | 62 ++++++++++++++++++- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/packages/next-safe-action/src/__tests__/happy-path.test.ts b/packages/next-safe-action/src/__tests__/happy-path.test.ts index c3f368bd..d6fcd34d 100644 --- a/packages/next-safe-action/src/__tests__/happy-path.test.ts +++ b/packages/next-safe-action/src/__tests__/happy-path.test.ts @@ -38,14 +38,17 @@ test("action with no input schema and return data gives back an object with corr assert.deepStrictEqual(actualResult, expectedResult); }); -test("action with input schema and return data gives back an object with correct `data`", async () => { +test("action with input, output schema and return data gives back an object with correct `data`", async () => { const userId = "ed6f5b84-6bca-4d01-9a51-c3d0c49a7996"; - const action = ac.schema(z.object({ userId: z.string().uuid() })).action(async ({ parsedInput }) => { - return { - userId: parsedInput.userId, - }; - }); + const action = ac + .schema(z.object({ userId: z.string().uuid() })) + .outputSchema(z.object({ userId: z.string() })) + .action(async ({ parsedInput }) => { + return { + userId: parsedInput.userId, + }; + }); const actualResult = await action({ userId }); @@ -80,13 +83,14 @@ test("action with input schema passed via async function and return data gives b assert.deepStrictEqual(actualResult, expectedResult); }); -test("action with input schema extended via async function and return data gives back an object with correct `data`", async () => { +test("action with input schema extended via async function, ouput schema and return data gives back an object with correct `data`", async () => { const userId = "ed6f5b84-6bca-4d01-9a51-c3d0c49a7996"; const password = "password"; const action = ac .schema(z.object({ password: z.string() })) .schema(async (prevSchema) => prevSchema.extend({ userId: z.string().uuid() })) + .outputSchema(z.object({ userId: z.string(), password: z.string() })) .action(async ({ parsedInput }) => { return { userId: parsedInput.userId, @@ -106,12 +110,13 @@ test("action with input schema extended via async function and return data gives assert.deepStrictEqual(actualResult, expectedResult); }); -test("action with no input schema, bind args input schemas and return data gives back an object with correct `data`", async () => { +test("action with no input schema, with bind args input schemas, output schema and return data gives back an object with correct `data`", async () => { const username = "johndoe"; const age = 30; const action = ac .bindArgsSchemas<[username: z.ZodString, age: z.ZodNumber]>([z.string(), z.number()]) + .outputSchema(z.object({ username: z.string(), age: z.number() })) .action(async ({ bindArgsParsedInputs: [username, age] }) => { return { username, @@ -131,7 +136,7 @@ test("action with no input schema, bind args input schemas and return data gives assert.deepStrictEqual(actualResult, expectedResult); }); -test("action with input schema, bind args input schemas and return data gives back an object with correct `data`", async () => { +test("action with input schema, bind args input schemas, output schema and return data gives back an object with correct `data`", async () => { const userId = "ed6f5b84-6bca-4d01-9a51-c3d0c49a7996"; const username = "johndoe"; const age = 30; @@ -139,6 +144,7 @@ test("action with input schema, bind args input schemas and return data gives ba const action = ac .schema(z.object({ userId: z.string().uuid() })) .bindArgsSchemas<[username: z.ZodString, age: z.ZodNumber]>([z.string(), z.number()]) + .outputSchema(z.object({ userId: z.string(), username: z.string(), age: z.number() })) .action(async ({ parsedInput, bindArgsParsedInputs: [username, age] }) => { return { userId: parsedInput.userId, diff --git a/packages/next-safe-action/src/__tests__/validation-errors.test.ts b/packages/next-safe-action/src/__tests__/validation-errors.test.ts index 96b26d20..939f1e38 100644 --- a/packages/next-safe-action/src/__tests__/validation-errors.test.ts +++ b/packages/next-safe-action/src/__tests__/validation-errors.test.ts @@ -3,8 +3,16 @@ import assert from "node:assert"; import { test } from "node:test"; import { z } from "zod"; -import { createSafeActionClient, flattenValidationErrors, formatValidationErrors, returnValidationErrors } from ".."; +import type { ValidationErrors } from ".."; +import { + createSafeActionClient, + DEFAULT_SERVER_ERROR_MESSAGE, + flattenValidationErrors, + formatValidationErrors, + returnValidationErrors, +} from ".."; import { zodAdapter } from "../adapters/zod"; +import { ActionOutputDataError } from "../utils"; // Default client tests. @@ -144,6 +152,58 @@ test("action with invalid input gives back an object with correct `validationErr assert.deepStrictEqual(actualResult, expectedResult); }); +test("action with invalid output data returns the default `serverError`", async () => { + const action = dac.outputSchema(z.object({ result: z.string().min(3) })).action(async () => { + return { + result: "ok", + }; + }); + + const actualResult = await action(); + + const expectedResult = { + serverError: DEFAULT_SERVER_ERROR_MESSAGE, + }; + + assert.deepStrictEqual(actualResult, expectedResult); +}); + +test("action with invalid output data throws an error of the correct type", async () => { + const tac = createSafeActionClient({ + validationAdapter: zodAdapter(), + handleReturnedServerError: (e) => { + throw e; + }, + }); + + const outputSchema = z.object({ result: z.string().min(3) }); + + const action = tac.outputSchema(outputSchema).action(async () => { + return { + result: "ok", + }; + }); + + const expectedResult = { + serverError: "String must contain at least 3 character(s)", + }; + + const actualResult = { + serverError: "", + }; + + try { + await action(); + } catch (e) { + if (e instanceof ActionOutputDataError) { + actualResult.serverError = + (e.validationErrors as ValidationErrors).result?._errors?.[0] ?? ""; + } + } + + assert.deepStrictEqual(actualResult, expectedResult); +}); + // Formatted shape tests (same as default). const foac = createSafeActionClient({ From b25409130a140a7d8bbf8f523f29a4fd02b0e4f9 Mon Sep 17 00:00:00 2001 From: Edoardo Ranghieri Date: Thu, 29 Aug 2024 03:38:03 +0200 Subject: [PATCH 3/6] chore: upgrade Turborepo --- package.json | 2 +- pnpm-lock.yaml | 58 +++++++++++++++++++++++++------------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 3224deed..5a3affa4 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "cz-conventional-changelog": "^3.3.0", "husky": "^9.0.11", "is-ci": "^3.0.1", - "turbo": "^2.0.14" + "turbo": "^2.1.0" }, "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 234d0046..9928703d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^3.0.1 version: 3.0.1 turbo: - specifier: ^2.0.14 - version: 2.0.14 + specifier: ^2.1.0 + version: 2.1.0 apps/playground: dependencies: @@ -3718,38 +3718,38 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo-darwin-64@2.0.14: - resolution: {integrity: sha512-kwfDmjNwlNfvtrvT29+ZBg5n1Wvxl891bFHchMJyzMoR0HOE9N1NSNdSZb9wG3e7sYNIu4uDkNk+VBEqJW0HzQ==} + turbo-darwin-64@2.1.0: + resolution: {integrity: sha512-gHwpDk2gyB7qZ57gUUwDIS/IkglqEjjVtPZCTxmCRg28Tiwjui0azsLVKrnHP9UZHllozwbi28x8HXLXLEFF1w==} cpu: [x64] os: [darwin] - turbo-darwin-arm64@2.0.14: - resolution: {integrity: sha512-m3LXYEshCx3wc4ZClM6gb01KYpFmtjQ9IBF3A7ofjb6ahux3xlYZJZ3uFCLAGHuvGLuJ3htfiPbwlDPTdknqqw==} + turbo-darwin-arm64@2.1.0: + resolution: {integrity: sha512-GLaqGetNC6eS4eqXgsheLOHic/OcnGCGDi5boVf+TFZTXYH6YE15L4ugZha4xHXCr1KouCLILHh+f8EHEmWylg==} cpu: [arm64] os: [darwin] - turbo-linux-64@2.0.14: - resolution: {integrity: sha512-7vBzCPdoTtR92SNn2JMgj1FlMmyonGmpMaQdgAB1OVYtuQ6NVGoh7/lODfaILqXjpvmFSVbpBIDrKOT6EvcprQ==} + turbo-linux-64@2.1.0: + resolution: {integrity: sha512-VzBOsj7JyGoZtiNZZ6brjnY7UehRnClluw7pwznuLPzClkqOOPMd2jOcgkWxnP/xW4NBmOoFANXXrtvKBD4f2w==} cpu: [x64] os: [linux] - turbo-linux-arm64@2.0.14: - resolution: {integrity: sha512-jwH+c0bfjpBf26K/tdEFatmnYyXwGROjbr6bZmNcL8R+IkGAc/cglL+OToqJnQZTgZvH7uDGbeSyUo7IsHyjuA==} + turbo-linux-arm64@2.1.0: + resolution: {integrity: sha512-St7svJnOO5g4F6R7Z32e10I/0M3e6qpNjEYybXwPNul9NSfnUXeky4WoKaALwqNhyJ7nYemoFpZ1d+i8hFQTHg==} cpu: [arm64] os: [linux] - turbo-windows-64@2.0.14: - resolution: {integrity: sha512-w9/XwkHSzvLjmioo6cl3S1yRfI6swxsV1j1eJwtl66JM4/pn0H2rBa855R0n7hZnmI6H5ywLt/nLt6Ae8RTDmw==} + turbo-windows-64@2.1.0: + resolution: {integrity: sha512-iSobNud2MrJ1SZ1upVPlErT8xexsr0MQtKapdfq6z0M0rBnrDGEq5bUCSScWyGu+O4+glB4br9xkTAkGFqaxqQ==} cpu: [x64] os: [win32] - turbo-windows-arm64@2.0.14: - resolution: {integrity: sha512-XaQlyYk+Rf4xS5XWCo8XCMIpssgGGy8blzLfolN6YBp4baElIWMlkLZHDbGyiFmCbNf9I9gJI64XGRG+LVyyjA==} + turbo-windows-arm64@2.1.0: + resolution: {integrity: sha512-d61jN4rjE5PnUfF66GKrKoj8S8Ql4FGXzFFzZz4kjsHpZZzCTtqlzPZBmd1byzGYhDPTorTqG3G1USohbdyohA==} cpu: [arm64] os: [win32] - turbo@2.0.14: - resolution: {integrity: sha512-00JjdCMD/cpsjP0Izkjcm8Oaor5yUCfDwODtaLb+WyblyadkaDEisGhy3Dbd5az9n+5iLSPiUgf+WjPbns6MRg==} + turbo@2.1.0: + resolution: {integrity: sha512-A969/LO/sPHKlapIarY2VVzqQ5JnnW2/1kksZlnMEpsRD6gwOELvVL+ozfMiO7av9RILt3UeN02L17efr6HUCA==} hasBin: true type-check@0.4.0: @@ -7658,32 +7658,32 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - turbo-darwin-64@2.0.14: + turbo-darwin-64@2.1.0: optional: true - turbo-darwin-arm64@2.0.14: + turbo-darwin-arm64@2.1.0: optional: true - turbo-linux-64@2.0.14: + turbo-linux-64@2.1.0: optional: true - turbo-linux-arm64@2.0.14: + turbo-linux-arm64@2.1.0: optional: true - turbo-windows-64@2.0.14: + turbo-windows-64@2.1.0: optional: true - turbo-windows-arm64@2.0.14: + turbo-windows-arm64@2.1.0: optional: true - turbo@2.0.14: + turbo@2.1.0: optionalDependencies: - turbo-darwin-64: 2.0.14 - turbo-darwin-arm64: 2.0.14 - turbo-linux-64: 2.0.14 - turbo-linux-arm64: 2.0.14 - turbo-windows-64: 2.0.14 - turbo-windows-arm64: 2.0.14 + turbo-darwin-64: 2.1.0 + turbo-darwin-arm64: 2.1.0 + turbo-linux-64: 2.1.0 + turbo-linux-arm64: 2.1.0 + turbo-windows-64: 2.1.0 + turbo-windows-arm64: 2.1.0 type-check@0.4.0: dependencies: From c479f76fb01d04aef923cad191fb8dfa302288da Mon Sep 17 00:00:00 2001 From: Edoardo Ranghieri Date: Thu, 29 Aug 2024 04:00:35 +0200 Subject: [PATCH 4/6] chore(website): add docs for output validation --- website/docs/safe-action-client/instance-methods.md | 10 +++++++++- website/docs/types/index.md | 2 +- website/src/components/landing/features.tsx | 4 ++-- website/src/components/landing/hero.tsx | 3 ++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/website/docs/safe-action-client/instance-methods.md b/website/docs/safe-action-client/instance-methods.md index 9d6640b6..6695a802 100644 --- a/website/docs/safe-action-client/instance-methods.md +++ b/website/docs/safe-action-client/instance-methods.md @@ -28,7 +28,7 @@ metadata(data: Metadata) => new SafeActionClient() ## `schema` ```typescript -schema(schema: S, utils?: { handleValidationErrorsShape?: HandleValidationErrorsShapeFn } }) => new SafeActionClient() +schema(inputSchema: S, utils?: { handleValidationErrorsShape?: HandleValidationErrorsShapeFn } }) => new SafeActionClient() ``` `schema` accepts an input schema of type `Schema` or a function that returns a promise of type `Schema` and an optional `utils` object that accepts a [`handleValidationErrorsShape`](/docs/recipes/customize-validation-errors-format) function. The schema is used to define the arguments that the safe action will receive, the optional [`handleValidationErrorsShape`](/docs/recipes/customize-validation-errors-format) function is used to return a custom format for validation errors. If you don't pass an input schema, `parsedInput` and validation errors will be typed `undefined`, and `clientInput` will be typed `void`. It returns a new instance of the safe action client. @@ -41,6 +41,14 @@ bindArgsSchemas(bindArgsSchemas: BAS, bindArgsUtils?: { handleBindArgsValidation `bindArgsSchemas` accepts an array of bind input schemas of type `Schema[]` and an optional `bindArgsUtils` object that accepts a `handleBindArgsValidationErrorsShape` function. The schema is used to define the bind arguments that the safe action will receive, the optional `handleBindArgsValidationErrorsShape` function is used to [return a custom format for bind arguments validation errors](/docs/recipes/customize-validation-errors-format). It returns a new instance of the safe action client. +## `outputSchema` + +```typescript +outputSchema(outputSchema: S) => new SafeActionClient() +``` + +`outputSchema` accepts a schema of type `Schema`. That schema is used to define what the safe action will return. If you don't pass an output schema when you're defining an action, the return type will be inferred instead. If validation fails, an `ActionOutputDataValidationError` is internally thrown. You can catch it inside [`handleReturnedServerError`](/docs/safe-action-client/initialization-options#handlereturnedservererror)/[`handleServerErrorLog`](/docs/safe-action-client/initialization-options#handleservererrorlog) and access the `validationErrors` property to get the validation errors. It returns a new instance of the safe action client. + ## `action` / `stateAction` ```typescript diff --git a/website/docs/types/index.md b/website/docs/types/index.md index b54748a6..618c15f5 100644 --- a/website/docs/types/index.md +++ b/website/docs/types/index.md @@ -246,7 +246,7 @@ export type SafeActionUtils< ### `ValidationErrors` -Type of the returned object when input validation fails. +Type of the returned object when validation fails. ```typescript export type ValidationErrors = S extends Schema diff --git a/website/src/components/landing/features.tsx b/website/src/components/landing/features.tsx index 47be628e..7aa44db3 100644 --- a/website/src/components/landing/features.tsx +++ b/website/src/components/landing/features.tsx @@ -23,8 +23,8 @@ const features: { title: string; description: string }[] = [ "Manage authorization, log and halt execution, and much more with a composable middleware system.", }, { - title: "Input validation using multiple validation libraries", - description: `Input passed from the client to the server is validated using Zod, Valibot or Yup.`, + title: "Input/output validation using multiple validation libraries", + description: `Input and output are validated using your favorite library.`, }, { title: "Advanced server error handling", diff --git a/website/src/components/landing/hero.tsx b/website/src/components/landing/hero.tsx index 76838f56..e1a93c19 100644 --- a/website/src/components/landing/hero.tsx +++ b/website/src/components/landing/hero.tsx @@ -18,7 +18,8 @@ export function Hero() {

next-safe-action handles your Next.js app mutations type - safety, input validation, server errors and even more! + safety, input/output validation, server errors and even + more!

From a81e1f16e57ebc370ef8cda750a4d4c8ab740a5c Mon Sep 17 00:00:00 2001 From: Edoardo Ranghieri Date: Thu, 29 Aug 2024 04:00:50 +0200 Subject: [PATCH 5/6] docs(readme): add output validation --- README.md | 2 +- packages/next-safe-action/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 142e286f..88e768c5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ https://github.com/TheEdoRan/next-safe-action/assets/1337629/664eb3ee-92f3-4d4a- - ✅ End-to-end type safety - ✅ Form Actions support - ✅ Powerful middleware system -- ✅ Input validation using multiple validation libraries +- ✅ Input/output validation using multiple validation libraries - ✅ Advanced server error handling - ✅ Optimistic updates diff --git a/packages/next-safe-action/README.md b/packages/next-safe-action/README.md index 142e286f..88e768c5 100644 --- a/packages/next-safe-action/README.md +++ b/packages/next-safe-action/README.md @@ -17,7 +17,7 @@ https://github.com/TheEdoRan/next-safe-action/assets/1337629/664eb3ee-92f3-4d4a- - ✅ End-to-end type safety - ✅ Form Actions support - ✅ Powerful middleware system -- ✅ Input validation using multiple validation libraries +- ✅ Input/output validation using multiple validation libraries - ✅ Advanced server error handling - ✅ Optimistic updates From 4e7ba8ca5f800e74fe6a62e60124d15e3a764cb3 Mon Sep 17 00:00:00 2001 From: Edoardo Ranghieri Date: Thu, 29 Aug 2024 04:01:01 +0200 Subject: [PATCH 6/6] refactor: more appropriate names for metadata/output validation errors Renamed `ActionMetadataError` to `ActionMetadataValidationError` and `ActionOutputDataError` to `ActionOutputDataValidationError`. Moved them inside validation-errors.ts. --- .../src/__tests__/validation-errors.test.ts | 4 +-- .../next-safe-action/src/action-builder.ts | 14 +++++--- packages/next-safe-action/src/index.ts | 4 ++- packages/next-safe-action/src/utils.ts | 33 ------------------- .../next-safe-action/src/validation-errors.ts | 30 +++++++++++++++++ .../src/validation-errors.types.ts | 2 +- 6 files changed, 46 insertions(+), 41 deletions(-) diff --git a/packages/next-safe-action/src/__tests__/validation-errors.test.ts b/packages/next-safe-action/src/__tests__/validation-errors.test.ts index 939f1e38..d80797c6 100644 --- a/packages/next-safe-action/src/__tests__/validation-errors.test.ts +++ b/packages/next-safe-action/src/__tests__/validation-errors.test.ts @@ -12,7 +12,7 @@ import { returnValidationErrors, } from ".."; import { zodAdapter } from "../adapters/zod"; -import { ActionOutputDataError } from "../utils"; +import { ActionOutputDataValidationError } from "../validation-errors"; // Default client tests. @@ -195,7 +195,7 @@ test("action with invalid output data throws an error of the correct type", asyn try { await action(); } catch (e) { - if (e instanceof ActionOutputDataError) { + if (e instanceof ActionOutputDataValidationError) { actualResult.serverError = (e.validationErrors as ValidationErrors).result?._errors?.[0] ?? ""; } diff --git a/packages/next-safe-action/src/action-builder.ts b/packages/next-safe-action/src/action-builder.ts index 31cef41a..ac965be7 100644 --- a/packages/next-safe-action/src/action-builder.ts +++ b/packages/next-safe-action/src/action-builder.ts @@ -14,9 +14,15 @@ import type { ServerCodeFn, StateServerCodeFn, } from "./index.types"; -import { ActionMetadataError, ActionOutputDataError, DEFAULT_SERVER_ERROR_MESSAGE, isError } from "./utils"; +import { DEFAULT_SERVER_ERROR_MESSAGE, isError } from "./utils"; import type { MaybePromise } from "./utils.types"; -import { ActionServerValidationError, ActionValidationError, buildValidationErrors } from "./validation-errors"; +import { + ActionMetadataValidationError, + ActionOutputDataValidationError, + ActionServerValidationError, + ActionValidationError, + buildValidationErrors, +} from "./validation-errors"; import type { BindArgsValidationErrors, HandleBindArgsValidationErrorsShapeFn, @@ -112,7 +118,7 @@ export function actionBuilder< const parsedMd = await args.validationAdapter.validate(args.metadataSchema, args.metadata); if (!parsedMd.success) { - throw new ActionMetadataError(buildValidationErrors(parsedMd.issues)); + throw new ActionMetadataValidationError(buildValidationErrors(parsedMd.issues)); } } } @@ -217,7 +223,7 @@ export function actionBuilder< const parsedData = await args.validationAdapter.validate(args.outputSchema, data); if (!parsedData.success) { - throw new ActionOutputDataError(buildValidationErrors(parsedData.issues)); + throw new ActionOutputDataValidationError(buildValidationErrors(parsedData.issues)); } } diff --git a/packages/next-safe-action/src/index.ts b/packages/next-safe-action/src/index.ts index e116f60c..8207fa05 100644 --- a/packages/next-safe-action/src/index.ts +++ b/packages/next-safe-action/src/index.ts @@ -11,8 +11,10 @@ import { } from "./validation-errors"; export { createMiddleware } from "./middleware"; -export { ActionOutputDataError as ActionDataError, ActionMetadataError, DEFAULT_SERVER_ERROR_MESSAGE } from "./utils"; +export { DEFAULT_SERVER_ERROR_MESSAGE } from "./utils"; export { + ActionMetadataValidationError, + ActionOutputDataValidationError, ActionValidationError, flattenBindArgsValidationErrors, flattenValidationErrors, diff --git a/packages/next-safe-action/src/utils.ts b/packages/next-safe-action/src/utils.ts index 12558529..8c693134 100644 --- a/packages/next-safe-action/src/utils.ts +++ b/packages/next-safe-action/src/utils.ts @@ -1,36 +1,3 @@ -import type { Schema } from "./adapters/types"; -import type { ValidationErrors } from "./validation-errors.types"; - export const DEFAULT_SERVER_ERROR_MESSAGE = "Something went wrong while executing the operation."; export const isError = (error: unknown): error is Error => error instanceof Error; - -/** - * This error is thrown when an action's metadata input is invalid, i.e. when there's a mismatch between the - * type of the metadata schema returned from `defineMetadataSchema` and the actual input. - */ -export class ActionMetadataError extends Error { - public validationErrors: ValidationErrors; - - constructor(validationErrors: ValidationErrors) { - super("Invalid metadata input. Please be sure to pass metadata via `metadata` method before defining the action."); - this.name = "ActionMetadataError"; - this.validationErrors = validationErrors; - } -} - -/** - * This error is thrown when an action's data (output) is invalid, i.e. when there's a mismatch between the - * type of the data schema passed to `dataSchema` method and the actual return of the action. - */ -export class ActionOutputDataError extends Error { - public validationErrors: ValidationErrors; - - constructor(validationErrors: ValidationErrors) { - super( - "Invalid action data (output). Please be sure to return data following the shape of the schema passed to `dataSchema` method." - ); - this.name = "ActionOutputDataError"; - this.validationErrors = validationErrors; - } -} diff --git a/packages/next-safe-action/src/validation-errors.ts b/packages/next-safe-action/src/validation-errors.ts index a3db8650..9dd614e3 100644 --- a/packages/next-safe-action/src/validation-errors.ts +++ b/packages/next-safe-action/src/validation-errors.ts @@ -144,3 +144,33 @@ export function flattenBindArgsValidationErrors flattenValidationErrors(ve)) as FlattenedBindArgsValidationErrors; } + +/** + * This error is thrown when an action metadata is invalid, i.e. when there's a mismatch between the + * type of the metadata schema returned from `defineMetadataSchema` and the actual data passed. + */ +export class ActionMetadataValidationError extends Error { + public validationErrors: ValidationErrors; + + constructor(validationErrors: ValidationErrors) { + super("Invalid metadata input. Please be sure to pass metadata via `metadata` method before defining the action."); + this.name = "ActionMetadataError"; + this.validationErrors = validationErrors; + } +} + +/** + * This error is thrown when an action's data (output) is invalid, i.e. when there's a mismatch between the + * type of the data schema passed to `dataSchema` method and the actual return of the action. + */ +export class ActionOutputDataValidationError extends Error { + public validationErrors: ValidationErrors; + + constructor(validationErrors: ValidationErrors) { + super( + "Invalid action data (output). Please be sure to return data following the shape of the schema passed to `dataSchema` method." + ); + this.name = "ActionOutputDataError"; + this.validationErrors = validationErrors; + } +} diff --git a/packages/next-safe-action/src/validation-errors.types.ts b/packages/next-safe-action/src/validation-errors.types.ts index d225da2b..7f44f2db 100644 --- a/packages/next-safe-action/src/validation-errors.types.ts +++ b/packages/next-safe-action/src/validation-errors.types.ts @@ -10,7 +10,7 @@ type SchemaErrors = { } & {}; /** - * Type of the returned object when input validation fails. + * Type of the returned object when validation fails. */ export type ValidationErrors = S extends Schema ? Infer extends object