Skip to content

Commit

Permalink
feat: switch context based clients (#14)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `getAuthData` removed from `createOpts`; `withAuth: true` removed from action opts;
use context based clients

Closes #13
  • Loading branch information
TheEdoRan authored Jul 10, 2023
1 parent ee1eed1 commit 5246ff1
Show file tree
Hide file tree
Showing 16 changed files with 542 additions and 149 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
> `next-safe-action` is a library that takes full advantage of the latest and greatest Next.js, React and TypeScript features, using Zod, to let you define typesafe actions on the server and call them from Client Components.
## Take a look at:
- [next-safe-action](https:/TheEdoRan/next-safe-action/tree/main/packages/next-safe-action) - Library code and documentation
- [next-safe-action](packages/next-safe-action) - Library code and documentation

- [example-app](https:/TheEdoRan/next-safe-action/tree/main/packages/example-app) - A basic implementation of the library
- [example-app](packages/example-app) - A basic implementation of the library
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"dev": "turbo run dev --filter=example-app",
"lint": "turbo run lint",
"build": "turbo run build",
"build:lib": "turbo run build --filter=next-safe-action",
"deploy": "turbo run deploy"
},
"author": "Edoardo Ranghieri",
Expand Down
2 changes: 1 addition & 1 deletion packages/example-app/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Try it yourself: [Link to example on Vercel](https://next-safe-action.vercel.app/).

This is a basic implementation of the [next-safe-action](https:/TheEdoRan/next-safe-action/tree/main/packages/next-safe-action) library.
This is a basic implementation of the [next-safe-action](../next-safe-action) library.
25 changes: 11 additions & 14 deletions packages/example-app/src/app/hook/deleteuser-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,20 @@ import { revalidatePath } from "next/cache";
import { z } from "zod";
import { updateUserId } from "./db";

const inputValidator = z.object({
const input = z.object({
userId: z.string().min(1).max(10),
});

export const deleteUser = action(
{ input: inputValidator },
async ({ userId }) => {
await new Promise((res) => setTimeout(res, 1000));
export const deleteUser = action(input, async ({ userId }) => {
await new Promise((res) => setTimeout(res, 1000));

updateUserId(userId);
updateUserId(userId);

// This Next.js function revalidates the provided path.
// More info here: https://nextjs.org/docs/app/api-reference/functions/revalidatePath
revalidatePath("/hook");
// This Next.js function revalidates the provided path.
// More info here: https://nextjs.org/docs/app/api-reference/functions/revalidatePath
revalidatePath("/hook");

return {
deleted: true,
};
}
);
return {
deleted: true,
};
});
37 changes: 17 additions & 20 deletions packages/example-app/src/app/login-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,29 @@
import { action } from "@/lib/safe-action";
import { z } from "zod";

const inputValidator = z.object({
const input = z.object({
username: z.string().min(3).max(10),
password: z.string().min(8).max(100),
});

export const loginUser = action(
{ input: inputValidator },
async ({ username, password }) => {
if (username === "johndoe") {
return {
error: {
reason: "user_suspended",
},
};
}

if (username === "user" && password === "password") {
return {
success: true,
};
}

export const loginUser = action(input, async ({ username, password }) => {
if (username === "johndoe") {
return {
error: {
reason: "incorrect_credentials",
reason: "user_suspended",
},
};
}
);

if (username === "user" && password === "password") {
return {
success: true,
};
}

return {
error: {
reason: "incorrect_credentials",
},
};
});
25 changes: 11 additions & 14 deletions packages/example-app/src/app/optimistic-hook/addlikes-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,20 @@ import { revalidatePath } from "next/cache";
import { z } from "zod";
import { incrementLikes } from "./db";

const inputValidator = z.object({
const input = z.object({
incrementBy: z.number(),
});

export const addLikes = action(
{ input: inputValidator },
async ({ incrementBy }) => {
await new Promise((res) => setTimeout(res, 2000));
export const addLikes = action(input, async ({ incrementBy }) => {
await new Promise((res) => setTimeout(res, 2000));

const likesCount = incrementLikes(incrementBy);
const likesCount = incrementLikes(incrementBy);

// This Next.js function revalidates the provided path.
// More info here: https://nextjs.org/docs/app/api-reference/functions/revalidatePath
revalidatePath("/optimistic-hook");
// This Next.js function revalidates the provided path.
// More info here: https://nextjs.org/docs/app/api-reference/functions/revalidatePath
revalidatePath("/optimistic-hook");

return {
likesCount,
};
}
);
return {
likesCount,
};
});
2 changes: 1 addition & 1 deletion packages/example-app/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const metadata = {
export default function Home() {
return (
<>
<Link href="/withauth">Go to /withauth</Link>
<Link href="/with-context">Go to /with-context</Link>
<Link href="/hook">Go to /hook</Link>
<Link href="/optimistic-hook">Go to /optimistic-hook</Link>
<h1>Action without auth</h1>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"use server";

import { action } from "@/lib/safe-action";
import { authAction } from "@/lib/safe-action";
import { z } from "zod";

const inputValidator = z.object({
const input = z.object({
fullName: z.string().min(3).max(50),
age: z.string().min(1).max(3),
});

export const editUser = action(
{ input: inputValidator, withAuth: true }, // auth action
// Here you have access to `userId`, which comes from `getAuthData`
export const editUser = authAction(
input,
// Here you have access to `userId`, which comes from `buildContext`
// return object in src/lib/safe-action.ts.
// \\\\\
async ({ fullName, age }, { userId }) => {
Expand Down
25 changes: 10 additions & 15 deletions packages/example-app/src/lib/safe-action.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { randomBytes } from "crypto";
import { randomUUID } from "crypto";
import { createSafeActionClient } from "next-safe-action";

const action = createSafeActionClient({
// You can provide a custom function, otherwise the lib will use `console.error`
export const action = createSafeActionClient({
// You can provide a custom log function, otherwise the lib will use `console.error`
// as the default logging system. If you want to disable server errors logging,
// just pass an empty function.
serverErrorLogFunction: (e) => {
console.error("CUSTOM ERROR LOG FUNCTION:", e);
},
// This is required when you pass `withAuth: true` to safe actions.
// Defining an action with `withAuth: true` option without implementing
// the `getAuthData` function, results in a server error on action execution.
// The return object of this function will be passed as the second parameter of
// a server action definition function, where you provided `withAuth: true` as
// an option.
// Check `editUser` action in `src/app/withauth/edituser-action.ts` file for a
// practical example.
getAuthData: async () => {
});

export const authAction = createSafeActionClient({
// You can provide a context builder function. In this case, context is used
// for (fake) auth purposes.
buildContext: async () => {
return {
userId: randomBytes(6).toString("hex"),
userId: randomUUID(),
};
},
});

export { action };
56 changes: 29 additions & 27 deletions packages/next-safe-action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

> `next-safe-action` is a library that takes full advantage of the latest and greatest Next.js, React and TypeScript features, using Zod, to let you define typesafe actions on the server and call them from Client Components.
This is the new documentation, for version 3 of the library. If you want to check out the old documentation, [you can find it here](README_v2.md).

## Features
- ✅ Pretty simple
- ✅ End to end type safety
- ✅ Context based clients
- ✅ Input validation
- ✅ Direct or hook usage from client
- ✅ Optimistic updates
- ✅ Authenticated actions


## Requirements

Expand All @@ -22,7 +25,7 @@ npm i next-safe-action zod

## Code example ⬇️

### Check out [this Next.js demo](https:/TheEdoRan/next-safe-action/tree/main/packages/example-app) to see a basic implementation of this library and to experiment a bit with it.
### Check out [this Next.js demo](../example-app) to see a basic implementation of this library and to experiment a bit with it.

---

Expand Down Expand Up @@ -74,7 +77,7 @@ const input = z.object({
// parsed input, and defines what happens on the server when the action is
// called from the client.
// In short, this is your backend code. It never runs on the client.
export const loginUser = action({ input }, async ({ username, password }) => {
export const loginUser = action(input, async ({ username, password }) => {
if (username === "johndoe") {
return {
failure: {
Expand Down Expand Up @@ -161,7 +164,7 @@ On the client you get back a typesafe response object, with three optional keys:
}
```

- `serverError`: if an unexpected error occurs in the server action body, it will be caught, and the client will only get back a `serverError` response. By default, the server error will be logged via `console.error`, but [this is configurable](https:/TheEdoRan/next-safe-action/tree/main/packages/next-safe-action#custom-server-error-logging).
- `serverError`: if an unexpected error occurs in the server action body, it will be caught, and the client will only get back a `serverError` response. By default, the server error will be logged via `console.error`, but [this is configurable](#custom-server-error-logging).

### 2. The hook way

Expand Down Expand Up @@ -263,13 +266,11 @@ import { z } from "zod";
import { action } from "@/lib/safe-action";
import { incrementLikes } from "./db";

const inputValidator = z.object({
const input = z.object({
incrementBy: z.number(),
});

export const addLikes = action(
{ input: inputValidator },
async ({ incrementBy }) => {
export const addLikes = action(input, async ({ incrementBy }) => {
// Add delay to simulate db call.
await new Promise((res) => setTimeout(res, 2000));

Expand Down Expand Up @@ -348,21 +349,26 @@ It returns the same seven keys as the regular `useAction` hook, plus one additio

---

## Authenticated action
## Define a context object

The library also supports creating protected actions, that will return a `serverError` back if user is not authenticated. You need to make some changes to the above code in order to use them.
A key feature of this library is the ability to define a context builder function when initializing a new action client. This object will then be passed as the second argument of the server action function.

First, when creating the safe action client, you **must** provide an `async function` called `getAuthData` as an option. You can return anything you want from here. If you find out that the user is not authenticated, you can safely throw an error in this function. It will be caught, and the client will receive a `serverError` response.

To build your context, first, when creating the safe action client, you have to provide an async function called `buildContext` as an option. You can return any object you want from here, and safely throw an error in this function's body. It will be caught, and the client will receive a `serverError` response.

```typescript
// src/lib/safe-action.ts

import { createSafeActionClient } from "next-safe-action";

export const action = createSafeActionClient({
// This is the base safe action client.
export const action = createSafeActionClient();

// This is a safe action client with an auth context.
export const authAction = createSafeActionClient({
// Here you can use functions such as `cookies()` or `headers()`
// from next/headers, or utilities like `getServerSession()` from NextAuth.
getAuthData: async () => {
// from next/headers, or utilities like `getServerSession()` from NextAuth here.
buildContext: async () => {
const session = true;

if (!session) {
Expand All @@ -376,33 +382,29 @@ export const action = createSafeActionClient({
});
```

Then, you can provide a `withAuth: true` option to the safe action you're creating:
Then, you can use the previously defined client and access the context object:

```typescript
"use server"; // don't forget to add this

import { z } from "zod";
import { action } from "@/lib/safe-action";
import { authAction } from "@/lib/safe-action";

...

// [1] For protected actions, you need to provide `withAuth: true` here.
// [2] Then, you'll have access to the auth object, in this case it's just
// `{ userId }`, which comes from the return type of the `getAuthData` function
// [1]: Here you have access to the context object, in this case it's just
// `{ userId }`, which comes from the return type of the `buildContext` function
// declared in the previous step.
export const editUser = action({ input, withAuth: true }, // [1]
async (parsedInput, { userId }) => { // [2]
console.log(userId); // will output: "coolest_user_id",
...
export const editUser = authAction(input, async (parsedInput, { userId /* [1] */ }) => {
console.log(userId); // will output: "coolest_user_id",
...
}
);
```

If you set `withAuth` to `true` in the safe action you're creating, but you forgot to define a `getAuthData` function when creating the client (above step), an error will be thrown when calling the action from client, that results in a `serverError` response for the client.

## Custom server error logging

As you just saw, you can provide a `getAuthData` function to `createSafeActionClient` function.
As you just saw, you can provide a `buildContext` function to `createSafeActionClient` function.

You can also provide a custom logger function for server errors. By default, they'll be logged via `console.error` (on the server, obviously), but this is configurable:

Expand All @@ -425,4 +427,4 @@ export const action = createSafeActionClient({

## License

This project is licensed under the [MIT License](https:/TheEdoRan/next-safe-action/blob/main/LICENSE).
This project is licensed under the [MIT License](LICENSE).
Loading

0 comments on commit 5246ff1

Please sign in to comment.