Skip to content

Instantly share code, notes, and snippets.

@riyaadh-abrahams
Last active July 22, 2023 18:48
Show Gist options
  • Save riyaadh-abrahams/f7711d569a944d506b16a2df05537846 to your computer and use it in GitHub Desktop.
Save riyaadh-abrahams/f7711d569a944d506b16a2df05537846 to your computer and use it in GitHub Desktop.
import { type ReactNode } from "react";
import { XCircleIcon } from "@heroicons/react/20/solid";
import type z from "zod";
import { type ZodTypeAny } from "zod";
import { fromZodError } from "zod-validation-error";
type TResult<T> =
| { ok: true; data: T; errorAlert?: ReactNode }
| { ok: false; errorAlert?: ReactNode };
type ErrorMessageMappingItem = { pattern: string; message: string };
export class ActionError extends Error {
constructor(message?: string) {
super(message);
this.name = "ActionError";
}
}
const errorMessageMapping: ErrorMessageMappingItem[] = [
{
pattern: "Unique constraint failed on the",
message: "This item already exists.",
},
{
pattern: "Foreign key constraint failed on the",
message: "This item cannot be deleted because it is used elsewhere.",
},
];
export function safeAction<TInput extends ZodTypeAny, TOutput>({
schema: validator,
action,
customErrorMessageMapping,
}: {
schema: TInput;
action: (input: z.infer<TInput>) => Promise<TOutput>;
customErrorMessageMapping?: ErrorMessageMappingItem[];
}) {
return async (input: z.infer<TInput>): Promise<TResult<TOutput>> => {
const result = validator.safeParse(input);
if (!result.success) {
const validatedError = fromZodError(result.error);
return {
ok: false,
errorAlert: <ErrorAlert>{validatedError.message}</ErrorAlert>,
};
} else {
try {
const data = await action(result.data);
return { ok: true, data: data };
} catch (error: any) {
let message = "Oops, something went wrong";
// Check for ActionError instances
if (error instanceof ActionError) {
message = error.message;
} else if (typeof error.message === "string") {
const allMappings = [
...(customErrorMessageMapping || []),
...errorMessageMapping,
];
for (const { pattern, message: friendlyMessage } of allMappings) {
if (error.message.includes(pattern)) {
message = friendlyMessage;
break;
}
}
}
return {
ok: false,
errorAlert: <ErrorAlert>{message}</ErrorAlert>,
};
}
}
};
}
export default function ErrorAlert({ children }: { children: ReactNode }) {
return (
<div className="my-2 rounded-md bg-red-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm text-red-700">{children}</p>
</div>
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment