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
7 changes: 7 additions & 0 deletions .changeset/mean-roses-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
"@rocket.chat/models": patch
---

Fixes an issue that prevented all custom fields from being saved when multiple updates were issued on a single call
1 change: 1 addition & 0 deletions apps/meteor/app/livechat/server/api/v1/customField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ API.v1.addRoute(
throw new Error('invalid-token');
}

// TODO: do on one shot instead of multiple calls
const fields = await Promise.all(
this.bodyParams.customFields.map(
async (customField: {
Expand Down
105 changes: 49 additions & 56 deletions apps/meteor/app/livechat/server/api/v1/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,66 +82,59 @@ API.v1.addRoute(
),
);

if (customFields && Array.isArray(customFields) && customFields.length > 0) {
const errors: string[] = [];
const keys = customFields.map((field) => field.key);

const livechatCustomFields = await LivechatCustomField.findByScope(
'visitor',
{ projection: { _id: 1, required: 1 } },
false,
).toArray();
validateRequiredCustomFields(keys, livechatCustomFields);

const matchingCustomFields = livechatCustomFields.filter((field: ILivechatCustomField) => keys.includes(field._id));
const processedKeys = await Promise.all(
matchingCustomFields.map(async (field: ILivechatCustomField) => {
const customField = customFields.find((f) => f.key === field._id);
if (!customField) {
return;
}

const { key, value, overwrite } = customField;
// TODO: Change this to Bulk update
if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) {
errors.push(key);
}

// TODO deduplicate this code and the one at the function setCustomFields (apps/meteor/app/livechat/server/lib/custom-fields.ts)
const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray();
if (contacts.length > 0) {
await Promise.all(contacts.map((contact) => updateContactsCustomFields(contact, key, value, overwrite)));
}

return key;
}),
);

if (processedKeys.length !== keys.length) {
livechatLogger.warn({
msg: 'Some custom fields were not processed',
visitorId: visitor._id,
missingKeys: keys.filter((key) => !processedKeys.includes(key)),
});
}

if (errors.length > 0) {
livechatLogger.error({
msg: 'Error updating custom fields',
visitorId: visitor._id,
errors,
});
throw new Error('error-updating-custom-fields');
}

return API.v1.success({ visitor: await VisitorsRaw.findOneEnabledById(visitor._id) });
if (!Array.isArray(customFields) || !customFields.length) {
return API.v1.success({ visitor });
}

if (!visitor) {
throw new Meteor.Error('error-saving-visitor', 'An error ocurred while saving visitor');
const keys = customFields.map((field) => field.key);

const livechatCustomFields = await LivechatCustomField.findByScope(
'visitor',
{ projection: { _id: 1, required: 1 } },
false,
).toArray();
validateRequiredCustomFields(keys, livechatCustomFields);

const matchingCustomFields = livechatCustomFields.filter((field: ILivechatCustomField) => keys.includes(field._id));
const validCustomFields = customFields.filter((cf) => matchingCustomFields.find((mcf) => cf.key === mcf._id));
if (!validCustomFields.length) {
return API.v1.success({ visitor });
}

const visitorCustomFieldsToUpdate = validCustomFields.reduce(
(prev, curr) => {
if (curr.overwrite) {
prev[`livechatData.${curr.key}`] = curr.value;
return prev;
}

if (!visitor?.livechatData?.[curr.key]) {
prev[`livechatData.${curr.key}`] = curr.value;
}

return prev;
},
{} as Record<string, string>,
);

if (Object.keys(visitorCustomFieldsToUpdate).length) {
await VisitorsRaw.updateAllLivechatDataByToken(visitor.token, visitorCustomFieldsToUpdate);
}

const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray();
if (contacts.length) {
await Promise.all(contacts.map((contact) => updateContactsCustomFields(contact, validCustomFields)));
}

if (validCustomFields.length !== keys.length) {
livechatLogger.warn({
msg: 'Some custom fields were not processed',
visitorId: visitor._id,
missingKeys: keys.filter((key) => !validCustomFields.map((v) => v.key).includes(key)),
});
}

return API.v1.success({ visitor });
return API.v1.success({ visitor: await VisitorsRaw.findOneEnabledById(visitor._id) });
},
},
);
Expand Down
40 changes: 25 additions & 15 deletions apps/meteor/app/livechat/server/lib/custom-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,33 @@ export const validateRequiredCustomFields = (customFields: string[], livechatCus
}
};

export async function updateContactsCustomFields(contact: ILivechatContact, key: string, value: string, overwrite: boolean): Promise<void> {
const shouldUpdateCustomFields = overwrite || !contact.customFields || !contact.customFields[key];
export async function updateContactsCustomFields(
contact: ILivechatContact,
validCustomFields: {
key: string;
value: string;
overwrite: boolean;
}[],
): Promise<void> {
const contactCustomFieldsToUpdate = validCustomFields.reduce(
(prev, curr) => {
if (curr.overwrite || !contact?.customFields?.[curr.key]) {
prev[`customFields.${curr.key}`] = curr.value;
return prev;
}
prev.conflictingFields ??= contact.conflictingFields || [];
prev.conflictingFields.push({ field: `customFields.${curr.key}`, value: curr.value });
return prev;
},
{} as Record<string, any>,
);

if (shouldUpdateCustomFields) {
contact.customFields ??= {};
contact.customFields[key] = value;
} else {
contact.conflictingFields ??= [];
contact.conflictingFields.push({ field: `customFields.${key}`, value });
if (!Object.keys(contactCustomFieldsToUpdate).length) {
return;
}

await LivechatContacts.updateContactCustomFields(contact._id, {
...(shouldUpdateCustomFields && { customFields: contact.customFields }),
...(contact.conflictingFields && { conflictingFields: contact.conflictingFields }),
});

livechatLogger.debug({ msg: `Contact ${contact._id} updated with custom fields` });
livechatLogger.debug({ msg: 'Updating custom fields for contact', contactId: contact._id, contactCustomFieldsToUpdate });
await LivechatContacts.updateById(contact._id, { $set: contactCustomFieldsToUpdate });
}

export async function setCustomFields({
Expand Down Expand Up @@ -73,7 +83,7 @@ export async function setCustomFields({
if (visitor) {
const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray();
if (contacts.length > 0) {
await Promise.all(contacts.map((contact) => updateContactsCustomFields(contact, key, value, overwrite)));
await Promise.all(contacts.map((contact) => updateContactsCustomFields(contact, [{ key, value, overwrite }])));
}
}
}
Expand Down
176 changes: 164 additions & 12 deletions apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { ILivechatVisitor } from '@rocket.chat/core-typings';
import type { ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';

import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields';
import { createVisitor, deleteVisitor } from '../../../data/livechat/rooms';
import { closeOmnichannelRoom, createLivechatRoom, createVisitor, deleteVisitor } from '../../../data/livechat/rooms';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';

describe('LIVECHAT - custom fields', () => {
Expand Down Expand Up @@ -118,6 +118,52 @@ describe('LIVECHAT - custom fields', () => {
});

describe('livechat/custom.fields', () => {
const customFieldName = `new_custom_field_${Date.now()}_1`;
const customFieldName2 = `new_custom_field_${Date.now()}_2`;
const customFieldName3 = `new_custom_field_${Date.now()}_3`;
let visitor: ILivechatVisitor;
let visitorRoom: IOmnichannelRoom;

before(async () => {
await createCustomField({
searchable: true,
field: customFieldName,
label: customFieldName,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'public',
regexp: '',
});
await createCustomField({
searchable: true,
field: customFieldName2,
label: customFieldName2,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'public',
regexp: '',
});
await createCustomField({
searchable: true,
field: customFieldName3,
label: customFieldName3,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'public',
regexp: '',
});
visitor = await createVisitor();
// start a room for visitor2
visitorRoom = await createLivechatRoom(visitor.token);
});
after(async () => {
await Promise.all([
deleteCustomField(customFieldName),
deleteCustomField(customFieldName2),
deleteCustomField(customFieldName3),
closeOmnichannelRoom(visitorRoom._id),
]);
});
it('should fail when token is not on body params', async () => {
await request.post(api('livechat/custom.fields')).expect(400);
});
Expand Down Expand Up @@ -163,16 +209,6 @@ describe('LIVECHAT - custom fields', () => {
});
it('should save a custom field on visitor', async () => {
const visitor = await createVisitor();
const customFieldName = `new_custom_field_${Date.now()}`;
await createCustomField({
searchable: true,
field: customFieldName,
label: customFieldName,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'public',
regexp: '',
});

const { body } = await request
.post(api('livechat/custom.fields'))
Expand All @@ -188,6 +224,122 @@ describe('LIVECHAT - custom fields', () => {
expect(body.fields).to.have.lengthOf(1);
expect(body.fields[0]).to.have.property('value', 'test_address');
});
it('should save multiple custom fields on a visitor', async () => {
const visitor = await createVisitor();

const { body } = await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [
{ key: customFieldName, value: 'test_address', overwrite: true },
{ key: customFieldName2, value: 'test_address2', overwrite: true },
{ key: customFieldName3, value: 'test_address3', overwrite: true },
],
})
.expect(200);

expect(body).to.have.property('success', true);
expect(body).to.have.property('fields');
expect(body.fields).to.be.an('array');
expect(body.fields).to.have.lengthOf(3);
expect(body.fields[0]).to.have.property('value', 'test_address');
expect(body.fields[1]).to.have.property('value', 'test_address2');
expect(body.fields[2]).to.have.property('value', 'test_address3');
});
it('should save multiple custom fields on contact when visitor already has custom fields and an update with multiple fields is issued', async () => {
const { body } = await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [{ key: customFieldName, value: 'test_address', overwrite: true }],
})
.expect(200);

expect(body).to.have.property('success', true);
expect(body).to.have.property('fields');
expect(body.fields).to.be.an('array');
expect(body.fields).to.have.lengthOf(1);
expect(body.fields[0]).to.have.property('value', 'test_address');

await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [
{ key: customFieldName2, value: 'test_address2', overwrite: true },
{ key: customFieldName3, value: 'test_address3', overwrite: true },
],
})
.expect(200);

await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId: visitorRoom.contactId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(customFieldName, 'test_address');
expect(res.body.contact.customFields).to.have.property(customFieldName2, 'test_address2');
expect(res.body.contact.customFields).to.have.property(customFieldName3, 'test_address3');
});
});
it('should mark a conflict on a contact custom fields when overwrite is true and visitor already has the custom field set', async () => {
await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [{ key: customFieldName, value: 'test_address_conflict', overwrite: false }],
})
.expect(200);

await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId: visitorRoom.contactId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(customFieldName, 'test_address');
expect(res.body.contact.customFields).to.have.property(customFieldName2, 'test_address2');
expect(res.body.contact.customFields).to.have.property(customFieldName3, 'test_address3');
expect(res.body.contact).to.have.property('conflictingFields').that.is.an('array');
expect(res.body.contact.conflictingFields[0]).to.deep.equal({
field: `customFields.${customFieldName}`,
value: 'test_address_conflict',
});
});
});
it('should overwrite the contact custom field when overwrite is true', async () => {
await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [{ key: customFieldName2, value: 'test_new_add', overwrite: true }],
})
.expect(200);

await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId: visitorRoom.contactId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(customFieldName, 'test_address');
expect(res.body.contact.customFields).to.have.property(customFieldName2, 'test_new_add');
expect(res.body.contact.customFields).to.have.property(customFieldName3, 'test_address3');
expect(res.body.contact).to.have.property('conflictingFields').that.is.an('array');
expect(res.body.contact.conflictingFields[0]).to.deep.equal({
field: `customFields.${customFieldName}`,
value: 'test_address_conflict',
});
});
});
});

describe('livechat/custom.field [with Contacts]', () => {
Expand Down
Loading
Loading