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:
Dhruwang
2025-12-25 17:00:21 +05:30
parent 32f96392a3
commit 64640a3427
5 changed files with 76 additions and 50 deletions

View File

@@ -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
}
)
);

View File

@@ -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);
}
};

View File

@@ -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>) => {

View File

@@ -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>) => {

View File

@@ -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);
}