From 4d157bf8dcd1a4ae62e984fa50519adf407663da Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Fri, 16 May 2025 17:18:34 +0530 Subject: [PATCH] fix: user attributes updates api email fix (#5827) --- .../user/lib/update-user.test.ts | 122 ++++++++++++++++++ .../[environmentId]/user/lib/update-user.ts | 20 ++- .../web/modules/ee/contacts/lib/attributes.ts | 9 +- 3 files changed, 143 insertions(+), 8 deletions(-) diff --git a/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/lib/update-user.test.ts b/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/lib/update-user.test.ts index 2eaaa7a72d..f923182373 100644 --- a/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/lib/update-user.test.ts +++ b/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/lib/update-user.test.ts @@ -240,4 +240,126 @@ describe("updateUser", () => { expect(result.state.data).toEqual(expect.objectContaining(mockUserState)); expect(result.messages).toEqual([]); }); + + test("should handle email attribute update with ignoreEmailAttribute flag", async () => { + vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(mockContact); + const newAttributes = { email: "new@example.com", name: "John Doe" }; + vi.mocked(updateAttributes).mockResolvedValue({ + success: true, + messages: [], + ignoreEmailAttribute: true, + }); + + vi.mocked(getUserState).mockResolvedValue({ + ...mockUserState, + }); + + const result = await updateUser(mockEnvironmentId, mockUserId, "desktop", newAttributes); + + expect(updateAttributes).toHaveBeenCalledWith( + mockContactId, + mockUserId, + mockEnvironmentId, + newAttributes + ); + // Email should not be included in the final attributes + expect(result.state.data).toEqual( + expect.objectContaining({ + ...mockUserState, + }) + ); + }); + + test("should handle failed attribute update gracefully", async () => { + vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(mockContact); + const newAttributes = { company: "Formbricks" }; + vi.mocked(updateAttributes).mockResolvedValue({ + success: false, + messages: ["Update failed"], + }); + + const result = await updateUser(mockEnvironmentId, mockUserId, "desktop", newAttributes); + + expect(updateAttributes).toHaveBeenCalledWith( + mockContactId, + mockUserId, + mockEnvironmentId, + newAttributes + ); + // Should still return state even if update failed + expect(result.state.data).toEqual(expect.objectContaining(mockUserState)); + expect(result.messages).toEqual(["Update failed"]); + }); + + test("should handle multiple attribute updates correctly", async () => { + vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(mockContact); + const newAttributes = { + company: "Formbricks", + role: "Developer", + language: "en", + country: "US", + }; + vi.mocked(updateAttributes).mockResolvedValue({ + success: true, + messages: ["Attributes updated successfully"], + }); + + const result = await updateUser(mockEnvironmentId, mockUserId, "desktop", newAttributes); + + expect(updateAttributes).toHaveBeenCalledWith( + mockContactId, + mockUserId, + mockEnvironmentId, + newAttributes + ); + expect(result.state.data?.language).toBe("en"); + expect(result.messages).toEqual(["Attributes updated successfully"]); + }); + + test("should handle contact creation with multiple initial attributes", async () => { + vi.mocked(getContactByUserIdWithAttributes).mockResolvedValue(null); + const initialAttributes = { + userId: mockUserId, + email: "test@example.com", + name: "Test User", + }; + vi.mocked(prisma.contact.create).mockResolvedValue({ + id: mockContactId, + attributes: [ + { attributeKey: { key: "userId" }, value: mockUserId }, + { attributeKey: { key: "email" }, value: "test@example.com" }, + { attributeKey: { key: "name" }, value: "Test User" }, + ], + } as any); + + const result = await updateUser(mockEnvironmentId, mockUserId, "desktop", initialAttributes); + + expect(prisma.contact.create).toHaveBeenCalledWith({ + data: { + environment: { connect: { id: mockEnvironmentId } }, + attributes: { + create: [ + { + attributeKey: { + connect: { key_environmentId: { key: "userId", environmentId: mockEnvironmentId } }, + }, + value: mockUserId, + }, + ], + }, + }, + select: { + id: true, + attributes: { + select: { attributeKey: { select: { key: true } }, value: true }, + }, + }, + }); + expect(contactCache.revalidate).toHaveBeenCalledWith({ + environmentId: mockEnvironmentId, + userId: mockUserId, + id: mockContactId, + }); + expect(result.state.data).toEqual(expect.objectContaining(mockUserState)); + }); }); diff --git a/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/lib/update-user.ts b/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/lib/update-user.ts index 56846d1970..0ec1c017e1 100644 --- a/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/lib/update-user.ts +++ b/apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/lib/update-user.ts @@ -85,20 +85,26 @@ export const updateUser = async ( } if (shouldUpdate) { - const { success, messages: updateAttrMessages } = await updateAttributes( - contact.id, - userId, - environmentId, - attributes - ); + const { + success, + messages: updateAttrMessages, + ignoreEmailAttribute, + } = await updateAttributes(contact.id, userId, environmentId, attributes); messages = updateAttrMessages ?? []; // If the attributes update was successful and the language attribute was provided, set the language if (success) { + let attributesToUpdate = { ...attributes }; + + if (ignoreEmailAttribute) { + const { email, ...rest } = attributes; + attributesToUpdate = rest; + } + contactAttributes = { ...contactAttributes, - ...attributes, + ...attributesToUpdate, }; if (attributes.language) { diff --git a/apps/web/modules/ee/contacts/lib/attributes.ts b/apps/web/modules/ee/contacts/lib/attributes.ts index b25514a55c..4b6161c043 100644 --- a/apps/web/modules/ee/contacts/lib/attributes.ts +++ b/apps/web/modules/ee/contacts/lib/attributes.ts @@ -13,7 +13,7 @@ export const updateAttributes = async ( userId: string, environmentId: string, contactAttributesParam: TContactAttributes -): Promise<{ success: boolean; messages?: string[] }> => { +): Promise<{ success: boolean; messages?: string[]; ignoreEmailAttribute?: boolean }> => { validateInputs( [contactId, ZId], [userId, ZString], @@ -21,6 +21,8 @@ export const updateAttributes = async ( [contactAttributesParam, ZContactAttributes] ); + let ignoreEmailAttribute = false; + // Fetch contact attribute keys and email check in parallel const [contactAttributeKeys, existingEmailAttribute] = await Promise.all([ getContactAttributeKeys(environmentId), @@ -58,6 +60,10 @@ export const updateAttributes = async ( ? ["The email already exists for this environment and was not updated."] : []; + if (emailExists) { + ignoreEmailAttribute = true; + } + // First, update all existing attributes if (existingAttributes.length > 0) { await prisma.$transaction( @@ -124,5 +130,6 @@ export const updateAttributes = async ( return { success: true, messages, + ignoreEmailAttribute, }; };