Skip to content

Commit

Permalink
fix: add zod compatibility mode
Browse files Browse the repository at this point in the history
TypeSchema currently breaks compatibility with Turbopack and edge environment. /zod export has the
same behavior of next-safe-action v5, so it works in those environments, but you're required to use
Zod schemas for validation.

re #49
  • Loading branch information
TheEdoRan committed Jan 6, 2024
1 parent d053028 commit 2739f3a
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 3 deletions.
110 changes: 109 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion packages/next-safe-action/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
],
"exports": {
".": "./dist/index.mjs",
"./hooks": "./dist/hooks.mjs"
"./hooks": "./dist/hooks.mjs",
"./zod": "./dist/zod.mjs"
},
"typesVersions": {
"*": {
Expand All @@ -20,6 +21,9 @@
],
"hooks": [
"./dist/hooks.d.mts"
],
"zod": [
"./dist/zod.d.mts"
]
}
},
Expand Down Expand Up @@ -69,5 +73,8 @@
},
"dependencies": {
"@decs/typeschema": "^0.12.1"
},
"optionalDependencies": {
"zod": "^3.22.4"
}
}
92 changes: 92 additions & 0 deletions packages/next-safe-action/src/zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { isNotFoundError } from "next/dist/client/components/not-found.js";
import { isRedirectError } from "next/dist/client/components/redirect.js";
import type { z } from "zod";
import type { SafeAction, SafeClientOpts, ServerCodeFn } from ".";
import { isError } from "./utils";

// UTILS

export const DEFAULT_SERVER_ERROR = "Something went wrong while executing the operation";

// SAFE ACTION CLIENT

/**
* Initialize a new action client.
* @param createOpts Options for creating a new action client.
* @returns {Function} A function that creates a new action, to be used in server files.
*
* {@link https://next-safe-action.dev/docs/getting-started See an example}
*/
export const createSafeActionClient = <Context>(createOpts?: SafeClientOpts<Context>) => {
// If server log function is not provided, default to `console.error` for logging
// server error messages.
const handleServerErrorLog =
createOpts?.handleServerErrorLog ||
((e) => {
console.error("Action error:", (e as Error).message);
});

// If `handleReturnedServerError` is provided, use it to handle server error
// messages returned on the client.
// Otherwise mask the error and use a generic message.
const handleReturnedServerError = (e: Error) =>
createOpts?.handleReturnedServerError?.(e) || DEFAULT_SERVER_ERROR;

// `actionBuilder` is the server function that creates a new action.
// It expects an input schema and a `serverCode` function, so the action
// knows what to do on the server when called by the client.
// It returns a function callable by the client.
const actionBuilder = <const S extends z.ZodTypeAny, const Data>(
schema: S,
serverCode: ServerCodeFn<S, Data, Context>
): SafeAction<S, Data> => {
// This is the function called by client. If `input` fails the schema
// parsing, the function will return a `validationError` object, containing
// all the invalid fields provided.
return async (clientInput) => {
try {
const parsedInput = await schema.safeParseAsync(clientInput);

// If schema validation fails.
if (!parsedInput.success) {
const { formErrors, fieldErrors } = parsedInput.error.flatten();
return {
validationErrors: {
_root: formErrors && formErrors.length ? formErrors : undefined,
...fieldErrors,
} as Partial<Record<keyof z.infer<S> | "_root", string[]>>,
};
}

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

// Get `result.data` from the server code function. If it doesn't return
// anything, `data` will be `null`.
const data = ((await serverCode(parsedInput.data, ctx)) ?? null) as Data;

return { data };
} catch (e: unknown) {
// next/navigation functions work by throwing an error that will be
// processed internally by Next.js. So, in this case we need to rethrow it.
if (isRedirectError(e) || isNotFoundError(e)) {
throw e;
}

// If error cannot be handled, warn the user and return a generic message.
if (!isError(e)) {
console.warn("Could not handle server error. Not an instance of Error: ", e);
return { serverError: DEFAULT_SERVER_ERROR };
}

await Promise.resolve(handleServerErrorLog(e));

return {
serverError: await Promise.resolve(handleReturnedServerError(e)),
};
}
};
};

return actionBuilder;
};
2 changes: 1 addition & 1 deletion packages/next-safe-action/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts", "src/hooks.ts"],
entry: ["src/index.ts", "src/hooks.ts", "src/zod.ts"],
format: ["esm"],
clean: true,
splitting: false,
Expand Down

1 comment on commit 2739f3a

@vercel
Copy link

@vercel vercel bot commented on 2739f3a Jan 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.