Skip to content

Commit

Permalink
feat: support output data validation (#250)
Browse files Browse the repository at this point in the history
This PR adds the `outputSchema` method to allow for optional validation
of the action's return value.

re #245
  • Loading branch information
TheEdoRan authored Aug 29, 2024
1 parent 84e15e3 commit 81cd392
Show file tree
Hide file tree
Showing 16 changed files with 281 additions and 139 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1"
}
2 changes: 1 addition & 1 deletion packages/next-safe-action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 15 additions & 9 deletions packages/next-safe-action/src/__tests__/happy-path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -131,14 +136,15 @@ 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;

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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { ActionOutputDataValidationError } from "../validation-errors";

// Default client tests.

Expand Down Expand Up @@ -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 ActionOutputDataValidationError) {
actualResult.serverError =
(e.validationErrors as ValidationErrors<typeof outputSchema>).result?._errors?.[0] ?? "";
}
}

assert.deepStrictEqual(actualResult, expectedResult);
});

// Formatted shape tests (same as default).

const foac = createSafeActionClient({
Expand Down
Loading

0 comments on commit 81cd392

Please sign in to comment.