mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
refactor: enhance error handling and type safety in contact attribute actions
- Introduced specific error handling using ResourceNotFoundError for missing contact attribute keys. - Updated action input types for create, update, and delete contact attribute key actions to improve type safety. - Refactored error handling in create and update attribute modals to use try-catch blocks for better user feedback.
This commit is contained in:
@@ -2,42 +2,44 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getProjectIdFromEnvironmentId,
|
||||
} from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { isSafeIdentifier } from "@/lib/utils/safe-identifier";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import {
|
||||
createContactAttributeKey,
|
||||
updateContactAttributeKey,
|
||||
deleteContactAttributeKey,
|
||||
getContactAttributeKeyById,
|
||||
updateContactAttributeKey,
|
||||
} from "@/modules/ee/contacts/lib/contact-attribute-keys";
|
||||
|
||||
const ZCreateContactAttributeKeyAction = z.object({
|
||||
environmentId: ZId,
|
||||
key: z.string().refine(
|
||||
(val) => isSafeIdentifier(val),
|
||||
{
|
||||
message:
|
||||
"Key must be a safe identifier: only lowercase letters, numbers, and underscores, and must start with a letter",
|
||||
}
|
||||
),
|
||||
key: z.string().refine((val) => isSafeIdentifier(val), {
|
||||
message:
|
||||
"Key must be a safe identifier: only lowercase letters, numbers, and underscores, and must start with a letter",
|
||||
}),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
type TCreateContactAttributeKeyActionInput = z.infer<typeof ZCreateContactAttributeKeyAction>;
|
||||
export const createContactAttributeKeyAction = authenticatedActionClient
|
||||
.schema(ZCreateContactAttributeKeyAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"contactAttributeKey",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: TCreateContactAttributeKeyActionInput;
|
||||
}) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
const projectId = await getProjectIdFromEnvironmentId(parsedInput.environmentId);
|
||||
|
||||
@@ -78,19 +80,25 @@ const ZUpdateContactAttributeKeyAction = z.object({
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
type TUpdateContactAttributeKeyActionInput = z.infer<typeof ZUpdateContactAttributeKeyAction>;
|
||||
export const updateContactAttributeKeyAction = authenticatedActionClient
|
||||
.schema(ZUpdateContactAttributeKeyAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"contactAttributeKey",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: TUpdateContactAttributeKeyActionInput;
|
||||
}) => {
|
||||
// Fetch existing key to check authorization and get environmentId
|
||||
const existingKey = await getContactAttributeKeyById(parsedInput.id);
|
||||
|
||||
if (!existingKey) {
|
||||
throw new Error("Contact attribute key not found");
|
||||
throw new ResourceNotFoundError("contactAttributeKey", parsedInput.id);
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(existingKey.environmentId);
|
||||
@@ -130,6 +138,7 @@ export const updateContactAttributeKeyAction = authenticatedActionClient
|
||||
const ZDeleteContactAttributeKeyAction = z.object({
|
||||
id: ZId,
|
||||
});
|
||||
type TDeleteContactAttributeKeyActionInput = z.infer<typeof ZDeleteContactAttributeKeyAction>;
|
||||
|
||||
export const deleteContactAttributeKeyAction = authenticatedActionClient
|
||||
.schema(ZDeleteContactAttributeKeyAction)
|
||||
@@ -137,12 +146,18 @@ export const deleteContactAttributeKeyAction = authenticatedActionClient
|
||||
withAuditLogging(
|
||||
"deleted",
|
||||
"contactAttributeKey",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: TDeleteContactAttributeKeyActionInput;
|
||||
}) => {
|
||||
// Fetch existing key to check authorization and get environmentId
|
||||
const existingKey = await getContactAttributeKeyById(parsedInput.id);
|
||||
|
||||
if (!existingKey) {
|
||||
throw new Error("Contact attribute key not found");
|
||||
throw new ResourceNotFoundError("contactAttributeKey", parsedInput.id);
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(existingKey.environmentId);
|
||||
@@ -173,4 +188,3 @@ export const deleteContactAttributeKeyAction = authenticatedActionClient
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { VisibilityState, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
@@ -220,7 +219,7 @@ export const AttributesTable = ({
|
||||
const deleteContactAttributeKeyResponse = await deleteContactAttributeKeyAction({ id: attributeId });
|
||||
if (!deleteContactAttributeKeyResponse?.data) {
|
||||
const errorMessage = getFormattedErrorMessage(deleteContactAttributeKeyResponse);
|
||||
toast.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -85,24 +85,30 @@ export function CreateAttributeModal({ environmentId }: Readonly<CreateAttribute
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
const createContactAttributeKeyResponse = await createContactAttributeKeyAction({
|
||||
environmentId,
|
||||
key: formData.key,
|
||||
name: formData.name || formData.key,
|
||||
description: formData.description || undefined,
|
||||
});
|
||||
|
||||
if (!createContactAttributeKeyResponse?.data) {
|
||||
const errorMessage = getFormattedErrorMessage(createContactAttributeKeyResponse);
|
||||
try {
|
||||
const createContactAttributeKeyResponse = await createContactAttributeKeyAction({
|
||||
environmentId,
|
||||
key: formData.key,
|
||||
name: formData.name || formData.key,
|
||||
description: formData.description || undefined,
|
||||
});
|
||||
|
||||
if (!createContactAttributeKeyResponse?.data) {
|
||||
const errorMessage = getFormattedErrorMessage(createContactAttributeKeyResponse);
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(t("environments.contacts.attribute_created_successfully"));
|
||||
handleResetState();
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : t("common.something_went_wrong");
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(t("environments.contacts.attribute_created_successfully"));
|
||||
handleResetState();
|
||||
router.refresh();
|
||||
setIsCreating(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
|
||||
@@ -36,23 +36,29 @@ export function EditAttributeModal({ attribute, open, setOpen }: Readonly<EditAt
|
||||
|
||||
const handleUpdate = async () => {
|
||||
setIsUpdating(true);
|
||||
const updateContactAttributeKeyResponse = await updateContactAttributeKeyAction({
|
||||
id: attribute.id,
|
||||
name: formData.name || undefined,
|
||||
description: formData.description || undefined,
|
||||
});
|
||||
|
||||
if (!updateContactAttributeKeyResponse?.data) {
|
||||
const errorMessage = getFormattedErrorMessage(updateContactAttributeKeyResponse);
|
||||
try {
|
||||
const updateContactAttributeKeyResponse = await updateContactAttributeKeyAction({
|
||||
id: attribute.id,
|
||||
name: formData.name || undefined,
|
||||
description: formData.description || undefined,
|
||||
});
|
||||
|
||||
if (!updateContactAttributeKeyResponse?.data) {
|
||||
const errorMessage = getFormattedErrorMessage(updateContactAttributeKeyResponse);
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(t("environments.contacts.attribute_updated_successfully"));
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : t("common.something_went_wrong");
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(t("environments.contacts.attribute_updated_successfully"));
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
setIsUpdating(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { DatabaseError, OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { InvalidInputError, OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const getContactAttributeKeys = reactCache(
|
||||
async (environmentId: string): Promise<TContactAttributeKey[]> => {
|
||||
@@ -44,7 +44,7 @@ export const createContactAttributeKey = async (data: {
|
||||
} catch (error) {
|
||||
if (error instanceof Error && "code" in error) {
|
||||
if (error.code === PrismaErrorType.UniqueConstraintViolation) {
|
||||
throw new DatabaseError("Attribute key already exists");
|
||||
throw new InvalidInputError("Attribute key already exists");
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
@@ -63,6 +63,7 @@ export const updateContactAttributeKey = async (
|
||||
});
|
||||
|
||||
if (!existingKey) {
|
||||
console.log("throwing resource not found error");
|
||||
throw new ResourceNotFoundError("contactAttributeKey", id);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user