Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support passing data to middleware from actions #67

Merged
merged 5 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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