Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/web/modules/ee/contacts/lib/attribute-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export const prepareNewSDKAttributeForStorage = (
};

const handleStringType = (value: TRawValue): TAttributeStorageColumns => {
// String type - only use value column
let stringValue: string;

if (value instanceof Date) {
Expand Down
18 changes: 18 additions & 0 deletions apps/web/modules/ee/contacts/lib/attributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,4 +437,22 @@ describe("updateAttributes", () => {
expect(result.success).toBe(true);
expect(result.messages).toContainEqual({ code: "email_or_userid_required", params: {} });
});

test("coerces boolean attribute values to strings", async () => {
vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys);
vi.mocked(getContactAttributes).mockResolvedValue({ name: "Jane", email: "jane@example.com" });
vi.mocked(hasEmailAttribute).mockResolvedValue(false);
vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });

const attributes = { name: true, email: "john@example.com" };
const result = await updateAttributes(contactId, userId, environmentId, attributes);

expect(result.success).toBe(true);
expect(prisma.$transaction).toHaveBeenCalled();
const transactionCall = vi.mocked(prisma.$transaction).mock.calls[0][0];
// Both name (coerced from boolean) and email should be upserted
expect(transactionCall).toHaveLength(2);
});
});
9 changes: 7 additions & 2 deletions apps/web/modules/ee/contacts/lib/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,12 @@ export const updateAttributes = async (
const messages: TAttributeUpdateMessage[] = [];
const errors: TAttributeUpdateMessage[] = [];

// Convert email and userId to strings for lookup (they should always be strings, but handle numbers gracefully)
// Coerce boolean values to strings (SDK may send booleans for string attributes)
const coercedAttributes: Record<string, string | number> = {};
for (const [key, value] of Object.entries(contactAttributesParam)) {
coercedAttributes[key] = typeof value === "boolean" ? String(value) : value;
}

const emailValue =
contactAttributesParam.email === null || contactAttributesParam.email === undefined
? null
Expand All @@ -154,7 +159,7 @@ export const updateAttributes = async (
const userIdExists = !!existingUserIdAttribute;

// Remove email and/or userId from attributes if they already exist on another contact
let contactAttributes = { ...contactAttributesParam };
let contactAttributes = { ...coercedAttributes };

// Determine what the final email and userId values will be after this update
// Only consider a value as "submitted" if it was explicitly included in the attributes
Expand Down
2 changes: 1 addition & 1 deletion packages/types/contact-attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ export type TContactAttribute = z.infer<typeof ZContactAttribute>;
export const ZContactAttributes = z.record(z.string());
export type TContactAttributes = z.infer<typeof ZContactAttributes>;

export const ZContactAttributesInput = z.record(z.union([z.string(), z.number()]));
export const ZContactAttributesInput = z.record(z.union([z.string(), z.number(), z.boolean()]));
export type TContactAttributesInput = z.infer<typeof ZContactAttributesInput>;
Loading