From 2e0448334394d2649190a35b63349324bf66e11b Mon Sep 17 00:00:00 2001 From: Sianida26 Date: Sun, 28 Jan 2024 05:00:03 +0700 Subject: [PATCH] Added base error handling --- .../roles/_modals/DeleteModal/DeleteModal.tsx | 68 ++++++++++------- src/db/index.ts | 16 +++- .../dashboard/errors/DashboardError.ts | 73 +++++++++++++++++++ .../dashboard/roles/actions/deleteRole.ts | 24 +++--- .../dashboard/utils/withServerAction.ts | 25 +++++++ src/types/Action.d.ts | 27 ++++--- 6 files changed, 183 insertions(+), 50 deletions(-) create mode 100644 src/features/dashboard/errors/DashboardError.ts create mode 100644 src/features/dashboard/utils/withServerAction.ts diff --git a/src/app/dashboard/(auth)/roles/_modals/DeleteModal/DeleteModal.tsx b/src/app/dashboard/(auth)/roles/_modals/DeleteModal/DeleteModal.tsx index 695fec3..58e1557 100644 --- a/src/app/dashboard/(auth)/roles/_modals/DeleteModal/DeleteModal.tsx +++ b/src/app/dashboard/(auth)/roles/_modals/DeleteModal/DeleteModal.tsx @@ -12,55 +12,71 @@ import { Stack, TextInput, Title, + Alert, } from "@mantine/core"; import { showNotification } from "@/utils/notifications"; import deleteRole from "@/features/dashboard/roles/actions/deleteRole"; +import withErrorHandling from "@/features/dashboard/utils/withServerAction"; +import { error } from "console"; +import DashboardError from "@/features/dashboard/errors/DashboardError"; +import { revalidatePath } from "next/cache"; export interface DeleteModalProps { data?: { - id: string, - name: string, - }; - onClose: () => void + id: string; + name: string; + }; + onClose: () => void; } export default function DeleteModal(props: DeleteModalProps) { const router = useRouter(); const [isSubmitting, setSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); /** * Closes the modal. It won't close if a submission is in progress. */ const closeModal = () => { if (isSubmitting) return; - props.onClose() + setErrorMessage("") + props.onClose(); }; - const confirmAction = () => { - if (!props.data?.id) return; - setSubmitting(true) - deleteRole(props.data.id) - .then((response) => { - if (response.success){ - showNotification(response.message); - setSubmitting(false) - props.onClose() - return; - } else { - showNotification(response.message, "error") + const confirmAction = () => { + if (!props.data?.id) return; + setSubmitting(true); + + withErrorHandling(() => deleteRole(props.data!.id)) + .then((response) => { + showNotification( + response.message ?? "Role deleted successfully" + ); + setSubmitting(false); + props.onClose() + }) + .catch((e) => { + if (e instanceof DashboardError){ + setErrorMessage(`ERROR: ${e.message} (${e.errorCode})`) } - }) - .catch(() => { - //TODO: Handle Error - }) + else if (e instanceof Error) { + setErrorMessage(`ERROR: ${e.message}`) + } else { + setErrorMessage(`Unkown error is occured. Please contact administrator`) + } + }) .finally(() => { setSubmitting(false) - }) - } + }); + }; return ( - + Are you sure you want to delete role{" "} @@ -68,6 +84,8 @@ export default function DeleteModal(props: DeleteModalProps) { ? This action is irreversible. + + {errorMessage && {errorMessage}} {/* Buttons */} diff --git a/src/db/index.ts b/src/db/index.ts index b5bf6ce..77d6b4b 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,5 +1,15 @@ -import { PrismaClient } from "@prisma/client"; +import { PrismaClient } from '@prisma/client' -const prisma = new PrismaClient(); +const prismaClientSingleton = () => { + return new PrismaClient() +} -export default prisma; +declare global { + var prisma: undefined | ReturnType +} + +const prisma = globalThis.prisma ?? prismaClientSingleton() + +export default prisma + +if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma \ No newline at end of file diff --git a/src/features/dashboard/errors/DashboardError.ts b/src/features/dashboard/errors/DashboardError.ts new file mode 100644 index 0000000..f3e8213 --- /dev/null +++ b/src/features/dashboard/errors/DashboardError.ts @@ -0,0 +1,73 @@ +import { Prisma } from "@prisma/client"; + +export const DashboardErrorCodes = [ + "UNAUTHORIZED", + "NOT_FOUND", + "UNKNOWN_ERROR", + "INVALID_FORM_DATA" +] as const; + +interface ErrorOptions { + message?: string, + errorCode?: typeof DashboardErrorCodes[number] | string & {}, + formErrors?: {[k: string]: string} +} + +export default class DashboardError extends Error { + public readonly errorCode: string; + public readonly formErrors?: {[k: string]: string} + // public readonly data: object; + + constructor(options: ErrorOptions) { + super(options.message ?? "Undetermined Error"); // Pass message to the Error parent class + this.errorCode = options.errorCode ?? "UNKNOWN_ERROR"; + this.formErrors = options.formErrors; + // this.data = data; + Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain + } + + getErrorReponseObject(){ + return { + success: false, + dashboardError: true, + error: { + message: `${this.message}`, + errorCode: this.errorCode, + errors: this.formErrors ?? undefined + } + } as const + } +} + +export const handleCatch = (e: unknown) => { + if (e instanceof DashboardError){ + return e.getErrorReponseObject() + } + if (e instanceof Prisma.PrismaClientKnownRequestError){ + //Not found + if (e.code === "P2025"){ + const error = new DashboardError({errorCode: "NOT_FOUND", message: "The requested data could not be located. It may have been deleted or relocated. Please verify the information or try a different request."}) + return error.getErrorReponseObject() + } + } + if (e instanceof Error) { + return { + success: false, + dashboardError: false, + message: e.message + } as const; + } else { + return { + success: false, + dashboardError: false, + message: "Unkown error" + } as const + } +} + +export const unauthorized = () => { + throw new DashboardError({ + errorCode: "UNAUTHORIZED", + message: "You are unauthorized to do this action" + }) +} \ No newline at end of file diff --git a/src/features/dashboard/roles/actions/deleteRole.ts b/src/features/dashboard/roles/actions/deleteRole.ts index 1fab8c7..2ba647b 100644 --- a/src/features/dashboard/roles/actions/deleteRole.ts +++ b/src/features/dashboard/roles/actions/deleteRole.ts @@ -1,26 +1,26 @@ "use server"; -import { unauthorized } from "@/BaseError"; import prisma from "@/db"; import checkPermission from "@/features/auth/tools/checkPermission"; +import { handleCatch, unauthorized } from "../../errors/DashboardError"; +import ServerResponse from "@/types/Action"; +import { revalidatePath } from "next/cache"; -export default async function deleteRole(id: string) { - if (!(await checkPermission("role.delete"))) return unauthorized(); - - try { +export default async function deleteRole(id: string): Promise { + try { + if (!(await checkPermission("role.delete"))) return unauthorized(); const role = await prisma.role.delete({ where: { id }, }); + + revalidatePath(".") + return { success: true, message: "The role has been deleted successfully", - } as const; - } catch (e) { - //TODO: Handle error - return { - success: false, - message: "Unable to delete the role", - } as const; + }; + } catch (e: unknown) { + return handleCatch(e) } } diff --git a/src/features/dashboard/utils/withServerAction.ts b/src/features/dashboard/utils/withServerAction.ts new file mode 100644 index 0000000..f2e0326 --- /dev/null +++ b/src/features/dashboard/utils/withServerAction.ts @@ -0,0 +1,25 @@ +import ServerResponse from "@/types/Action"; +import DashboardError from "../errors/DashboardError"; + +async function withErrorHandling( + asyncFunction: () => Promise +): Promise { + const result = await asyncFunction(); + if (result.success) { + return result; + } else { + if (result.dashboardError && result.error) { + const errorDetails = result.error; + throw new DashboardError({ + message: errorDetails.message, + errorCode: errorDetails.errorCode, + formErrors: errorDetails.errors, + }); + } else { + // Handle non-dashboard errors + throw new Error(result.message ?? "Unknown error occurred."); + } + } +} + +export default withErrorHandling; diff --git a/src/types/Action.d.ts b/src/types/Action.d.ts index 7805631..436e649 100644 --- a/src/types/Action.d.ts +++ b/src/types/Action.d.ts @@ -1,11 +1,18 @@ -type SuccessResponse = { - success: true, - data: T -} +type ServerResponse = + | { + success: false; + dashboardError?: boolean; + error?: { + message?: string; + errorCode?: string; + errors?: { [k: string]: string }; + }; + message?: string; + } + | { + success: true; + message?: string; + data?: T; + }; -type ErrorResponse = { - success: false, - error: E & {message: string} -} - -type Action = SuccessResponse<> | ErrorResponse<> \ No newline at end of file +export default ServerResponse;