Skip to content

Commit

Permalink
feat: support passing data to middleware from actions (#67)
Browse files Browse the repository at this point in the history
Pass data from actions to the middleware. This is especially useful when you want to handle the auth logic inside the middleware and you need to check if user has specific roles or permissions to execute the function.

---------

Co-authored-by: Edoardo Ranghieri <[email protected]>
  • Loading branch information
JsSusenka and TheEdoRan authored Mar 9, 2024
1 parent 547a312 commit 8192ee3
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 11 deletions.
17 changes: 12 additions & 5 deletions packages/next-safe-action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import { buildValidationErrors, isError } from "./utils";
/**
* Type of options when creating a new safe action client.
*/
export type SafeClientOpts<Context> = {
export type SafeClientOpts<Context, MiddlewareData> = {
handleServerErrorLog?: (e: Error) => MaybePromise<void>;
handleReturnedServerError?: (e: Error) => MaybePromise<string>;
middleware?: (parsedInput: unknown) => MaybePromise<Context>;
middleware?: (parsedInput: any, data?: MiddlewareData) => MaybePromise<Context>;
};

/**
Expand Down Expand Up @@ -46,7 +46,9 @@ export const DEFAULT_SERVER_ERROR = "Something went wrong while executing the op
*
* {@link https://next-safe-action.dev/docs/getting-started See an example}
*/
export const createSafeActionClient = <Context>(createOpts?: SafeClientOpts<Context>) => {
export const createSafeActionClient = <Context, MiddlewareData>(
createOpts?: SafeClientOpts<Context, MiddlewareData>
) => {
// If server log function is not provided, default to `console.error` for logging
// server error messages.
const handleServerErrorLog =
Expand All @@ -67,7 +69,10 @@ export const createSafeActionClient = <Context>(createOpts?: SafeClientOpts<Cont
// It returns a function callable by the client.
const actionBuilder = <const S extends Schema, const Data>(
schema: S,
serverCode: ServerCodeFn<S, Data, Context>
serverCode: ServerCodeFn<S, Data, Context>,
utils?: {
middlewareData?: MiddlewareData;
}
): SafeAction<S, Data> => {
// This is the function called by client. If `input` fails the schema
// parsing, the function will return a `validationError` object, containing
Expand All @@ -84,7 +89,9 @@ export const createSafeActionClient = <Context>(createOpts?: SafeClientOpts<Cont
}

// Get the context if `middleware` is provided.
const ctx = (await Promise.resolve(createOpts?.middleware?.(parsedInput.data))) as Context;
const ctx = (await Promise.resolve(
createOpts?.middleware?.(parsedInput.data, utils?.middlewareData)
)) as Context;

// Get `result.data` from the server code function. If it doesn't return
// anything, `data` will be `null`.
Expand Down
52 changes: 48 additions & 4 deletions website/docs/safe-action-client/using-a-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ You can provide a middleware to the safe action client to perform custom logic b

So, let's say, you want to be sure that the user is authenticated before executing an action. In this case, you would create an `authAction` client and check if the user session is valid:

```typescript title=src/lib/safe-action.ts
```typescript title="src/lib/safe-action.ts"
import { createSafeActionClient } from "next-safe-action";
import { cookies } from "next/headers";
import { getUserIdFromSessionId } from "./db";

export const authAction = createSafeActionClient({
// If you need to access validated input, you can use `parsedInput` how you want
// in your middleware. Please note that `parsedInput` is typed unknown, as it
// in your middleware. Please note that `parsedInput` is typed any, as it
// comes from an action, while middleware is an (optional) instance function.
// Can also be a non async function.
async middleware(parsedInput) {
Expand All @@ -43,7 +43,7 @@ As you can see, you can use the `cookies()` and `headers()` functions from `next

Middleware can also be used to return a context, that will be passed as the second argument of the action server code function. This is very useful if you want, for example, find out which user executed the action. Here's an example reusing the `authAction` client defined above:

```typescript title=src/app/send-message-action.ts
```typescript title="src/app/send-message-action.ts"
import { authAction } from "@/lib/safe-action";
import { z } from "zod";
import { createMessage } from "./db";
Expand All @@ -62,4 +62,48 @@ const sendMessage = authAction(schema, async ({ text }, { userId }) => {
});
```

If the user session is not valid this server code is never executed. So in this case, we're sure the user is authenticated.
If the user session is not valid this server code is never executed. So in this case, we're sure the user is authenticated.

## Passing data to middleware from actions

You can pass data to your middleware from actions. This is useful, for example, if you want to restrict action execution to certain user roles or permissions. We'll redefine the `authAction` client to pass a user role to the middleware.

```typescript title="src/lib/safe-action.ts"
type MiddlewareData = {
userRole: "admin" | "user";
}

export const authAction = createSafeActionClient({
// Second argument is always optional and you can give a type to it. Doing so, you'll get inference
// when passing data from actions.
async middleware(parsedInput, data?: MiddlewareData) {
// ...

// Restrict actions execution to admins.
if (data?.userRole !== "admin") {
throw new Error("Only admins can execute this action!");
}

// ...
},
});
```

And then, you can pass the data to the middleware as the last argument of the action, after defining your server code function, in an object called `utils`, using an optional property named `middlewareData`, which has the same type as the second argument of the middleware function.

```typescript title="src/app/actions.ts"
"use server";

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

const schema = z.object({
username: z.string(),
})

export const deleteUser = action(schema, async ({ username }, { userId }) => {
// Action server code here...
},
{ middlewareData: { userRole: "admin" } } // type safe data
);
```
4 changes: 2 additions & 2 deletions website/docs/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ description: List of exported types.
Type of options when creating a new safe action client.

```typescript
export type SafeClientOpts<Context> = {
export type SafeClientOpts<Context, MiddlewareData> = {
handleServerErrorLog?: (e: Error) => MaybePromise<void>;
handleReturnedServerError?: (e: Error) => MaybePromise<string>;
middleware?: (parsedInput: unknown) => MaybePromise<Context>;
middleware?: (parsedInput: any, data?: MiddlewareData) => MaybePromise<Context>;
};
```

Expand Down

0 comments on commit 8192ee3

Please sign in to comment.