Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Old custom fields from migrated contacts are not ignored on update #34139

Merged
merged 11 commits into from
Dec 18, 2024
Merged
5 changes: 5 additions & 0 deletions .changeset/wet-chicken-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes contact update failing in case a custom field is removed from the workspace
24 changes: 20 additions & 4 deletions apps/meteor/app/livechat/server/lib/contacts/updateContact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export type UpdateContactParams = {
export async function updateContact(params: UpdateContactParams): Promise<ILivechatContact> {
const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels, wipeConflicts } = params;

const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'name'>>(contactId, {
projection: { _id: 1, name: 1 },
const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'name' | 'customFields'>>(contactId, {
projection: { _id: 1, name: 1, customFields: 1 },
});

if (!contact) {
Expand All @@ -36,15 +36,31 @@ export async function updateContact(params: UpdateContactParams): Promise<ILivec
await validateContactManager(contactManager);
}

const customFields = receivedCustomFields && validateCustomFields(await getAllowedCustomFields(), receivedCustomFields);
const workspaceAllowedCustomFields = await getAllowedCustomFields();
const workspaceAllowedCustomFieldsIds = new Set([...workspaceAllowedCustomFields.map((customField) => customField._id)]);
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
const currentCustomFieldsIds = Object.keys(contact.customFields || {});
const notRegisteredCustomFields = currentCustomFieldsIds
.filter((customFieldId) => !workspaceAllowedCustomFieldsIds.has(customFieldId))
.map((customFieldId) => ({ _id: customFieldId }));

const customFieldsToUpdate =
receivedCustomFields &&
validateCustomFields(workspaceAllowedCustomFields, receivedCustomFields, {
ignoreAdditionalFields: !!notRegisteredCustomFields.length,
});

if (receivedCustomFields && notRegisteredCustomFields.length) {
const allowedCustomFields = [...workspaceAllowedCustomFields, ...notRegisteredCustomFields];
validateCustomFields(allowedCustomFields, receivedCustomFields);
}

const updatedContact = await LivechatContacts.updateContact(contactId, {
name,
emails: emails?.map((address) => ({ address })),
phones: phones?.map((phoneNumber) => ({ phoneNumber })),
contactManager,
channels,
customFields,
customFields: customFieldsToUpdate,
...(wipeConflicts && { conflictingFields: [] }),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { trim } from '../../../../../lib/utils/stringUtils';
import { i18n } from '../../../../utils/lib/i18n';

export function validateCustomFields(
allowedCustomFields: AtLeast<ILivechatCustomField, '_id' | 'label' | 'regexp' | 'required'>[],
allowedCustomFields: AtLeast<ILivechatCustomField, '_id'>[],
customFields: Record<string, string | unknown>,
{
ignoreAdditionalFields = false,
Expand All @@ -16,15 +16,15 @@ export function validateCustomFields(
for (const cf of allowedCustomFields) {
if (!customFields.hasOwnProperty(cf._id)) {
if (cf.required && !ignoreValidationErrors) {
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label || cf._id }));
}
continue;
}
const cfValue: string = trim(customFields[cf._id]);

if (!cfValue || typeof cfValue !== 'string') {
if (cf.required && !ignoreValidationErrors) {
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label || cf._id }));
}
continue;
}
Expand All @@ -36,7 +36,7 @@ export function validateCustomFields(
continue;
}

throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label || cf._id }));
}
}

Expand Down
65 changes: 65 additions & 0 deletions apps/meteor/tests/end-to-end/api/livechat/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ describe('LIVECHAT - contacts', () => {
});

describe('Custom Fields', () => {
let contactId: string;
before(async () => {
await createCustomField({
field: 'cf1',
Expand All @@ -154,6 +155,17 @@ describe('LIVECHAT - contacts', () => {
searchable: true,
public: true,
});
await createCustomField({
field: 'cf2',
label: 'Custom Field 2',
scope: 'visitor',
visibility: 'public',
type: 'input',
required: false,
regexp: '^[0-9]+$',
searchable: true,
public: true,
});
});

after(async () => {
Expand Down Expand Up @@ -211,6 +223,59 @@ describe('LIVECHAT - contacts', () => {
expect(res.body).to.have.property('error');
expect(res.body.error).to.be.equal('Invalid value for Custom Field 1 field');
});

it('should remove an old custom field from a contact on update if it is not registered in the workspace anymore', async () => {
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
const createRes = await request
.post(api('omnichannel/contacts'))
.set(credentials)
.send({
name: faker.person.fullName(),
emails: [faker.internet.email().toLowerCase()],
phones: [faker.phone.number()],
customFields: {
cf1: '123',
cf2: '456',
},
});
expect(createRes.body).to.have.property('success', true);
expect(createRes.body).to.have.property('contactId').that.is.a('string');
contactId = createRes.body.contactId;

await deleteCustomField('cf2');

const updateRes = await request
.post(api('omnichannel/contacts.update'))
.set(credentials)
.send({
contactId,
customFields: {
cf1: '456',
cf2: '789',
},
});
expect(updateRes.body).to.have.property('success', true);
expect(updateRes.body).to.have.property('contact').that.is.an('object');
expect(updateRes.body.contact).to.have.property('_id', contactId);
expect(updateRes.body.contact).to.have.property('customFields').that.is.an('object');
expect(updateRes.body.contact.customFields).to.have.property('cf1', '456');
expect(updateRes.body.contact.customFields).to.not.have.property('cf2');
});

it('should throw an error if trying to update a custom field that is not registered in the workspace and does not exist in the contact', async () => {
const updateRes = await request
.post(api('omnichannel/contacts.update'))
.set(credentials)
.send({
contactId,
customFields: {
cf1: '123',
cf3: 'invalid',
},
});
expect(updateRes.body).to.have.property('success', false);
expect(updateRes.body).to.have.property('error');
expect(updateRes.body.error).to.be.equal('Custom field cf3 is not allowed');
});
});

describe('Fields Validation', () => {
Expand Down
Loading