Skip to content

Commit

Permalink
chore: Single Contact ID: improved typings and removed some duplicate…
Browse files Browse the repository at this point in the history
…d code (#33324)
  • Loading branch information
pierre-lehnen-rc authored Sep 27, 2024
1 parent 34087b0 commit 5965a1d
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 61 deletions.
14 changes: 7 additions & 7 deletions apps/meteor/app/livechat/server/api/v1/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { API } from '../../../../api/server';
import { Contacts, createContact, updateContact } from '../../lib/Contacts';
import { Contacts, createContact, updateContact, isSingleContactEnabled } from '../../lib/Contacts';

API.v1.addRoute(
'omnichannel/contact',
Expand Down Expand Up @@ -96,8 +96,8 @@ API.v1.addRoute(
{ authRequired: true, permissionsRequired: ['create-livechat-contact'], validateParams: isPOSTOmnichannelContactsProps },
{
async post() {
if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') {
throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode');
if (!isSingleContactEnabled()) {
return API.v1.unauthorized();
}
const contactId = await createContact({ ...this.bodyParams, unknown: false });

Expand All @@ -111,8 +111,8 @@ API.v1.addRoute(
{ authRequired: true, permissionsRequired: ['update-livechat-contact'], validateParams: isPOSTUpdateOmnichannelContactsProps },
{
async post() {
if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') {
throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode');
if (!isSingleContactEnabled()) {
return API.v1.unauthorized();
}

const contact = await updateContact({ ...this.bodyParams });
Expand All @@ -127,8 +127,8 @@ API.v1.addRoute(
{ authRequired: true, permissionsRequired: ['view-livechat-contact'], validateParams: isGETOmnichannelContactsProps },
{
async get() {
if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') {
throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode');
if (!isSingleContactEnabled()) {
return API.v1.unauthorized();
}
const contact = await LivechatContacts.findOneById(this.queryParams.contactId);

Expand Down
87 changes: 38 additions & 49 deletions apps/meteor/app/livechat/server/lib/Contacts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
AtLeast,
ILivechatContact,
ILivechatContactChannel,
ILivechatCustomField,
Expand Down Expand Up @@ -113,41 +114,8 @@ export const Contacts = {
}
}

const allowedCF = LivechatCustomField.findByScope<Pick<ILivechatCustomField, '_id' | 'label' | 'regexp' | 'required' | 'visibility'>>(
'visitor',
{
projection: { _id: 1, label: 1, regexp: 1, required: 1 },
},
false,
);

const livechatData: Record<string, string> = {};

for await (const cf of allowedCF) {
if (!customFields.hasOwnProperty(cf._id)) {
if (cf.required) {
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
}
continue;
}
const cfValue: string = trim(customFields[cf._id]);

if (!cfValue || typeof cfValue !== 'string') {
if (cf.required) {
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
}
continue;
}

if (cf.regexp) {
const regex = new RegExp(cf.regexp);
if (!regex.test(cfValue)) {
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
}
}

livechatData[cf._id] = cfValue;
}
const allowedCF = await getAllowedCustomFields();
const livechatData: Record<string, string> = validateCustomFields(allowedCF, customFields, { ignoreAdditionalFields: true });

const fieldsToRemove = {
// if field is explicitely set to empty string, remove
Expand Down Expand Up @@ -202,15 +170,20 @@ export const Contacts = {
},
};

export function isSingleContactEnabled(): boolean {
// The Single Contact feature is not yet available in production, but can already be partially used in test environments.
return process.env.TEST_MODE?.toUpperCase() === 'TRUE';
}

export async function createContact(params: CreateContactParams): Promise<string> {
const { name, emails, phones, customFields = {}, contactManager, channels, unknown } = params;
const { name, emails, phones, customFields: receivedCustomFields = {}, contactManager, channels, unknown } = params;

if (contactManager) {
await validateContactManager(contactManager);
}

const allowedCustomFields = await getAllowedCustomFields();
validateCustomFields(allowedCustomFields, customFields);
const customFields = validateCustomFields(allowedCustomFields, receivedCustomFields);

const { insertedId } = await LivechatContacts.insertOne({
name,
Expand All @@ -226,7 +199,7 @@ export async function createContact(params: CreateContactParams): Promise<string
}

export async function updateContact(params: UpdateContactParams): Promise<ILivechatContact> {
const { contactId, name, emails, phones, customFields, contactManager, channels } = params;
const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels } = params;

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

Expand All @@ -238,17 +211,21 @@ export async function updateContact(params: UpdateContactParams): Promise<ILivec
await validateContactManager(contactManager);
}

if (customFields) {
const allowedCustomFields = await getAllowedCustomFields();
validateCustomFields(allowedCustomFields, customFields);
}
const customFields = receivedCustomFields && validateCustomFields(await getAllowedCustomFields(), receivedCustomFields);

const updatedContact = await LivechatContacts.updateContact(contactId, { name, emails, phones, contactManager, channels, customFields });
const updatedContact = await LivechatContacts.updateContact(contactId, {
name,
emails,
phones,
contactManager,
channels,
customFields,
});

return updatedContact;
}

async function getAllowedCustomFields(): Promise<ILivechatCustomField[]> {
async function getAllowedCustomFields(): Promise<Pick<ILivechatCustomField, '_id' | 'label' | 'regexp' | 'required'>[]> {
return LivechatCustomField.findByScope(
'visitor',
{
Expand All @@ -258,7 +235,13 @@ async function getAllowedCustomFields(): Promise<ILivechatCustomField[]> {
).toArray();
}

export function validateCustomFields(allowedCustomFields: ILivechatCustomField[], customFields: Record<string, string | unknown>) {
export function validateCustomFields(
allowedCustomFields: AtLeast<ILivechatCustomField, '_id' | 'label' | 'regexp' | 'required'>[],
customFields: Record<string, string | unknown>,
options?: { ignoreAdditionalFields?: boolean },
): Record<string, string> {
const validValues: Record<string, string> = {};

for (const cf of allowedCustomFields) {
if (!customFields.hasOwnProperty(cf._id)) {
if (cf.required) {
Expand All @@ -281,14 +264,20 @@ export function validateCustomFields(allowedCustomFields: ILivechatCustomField[]
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
}
}

validValues[cf._id] = cfValue;
}

const allowedCustomFieldIds = new Set(allowedCustomFields.map((cf) => cf._id));
for (const key in customFields) {
if (!allowedCustomFieldIds.has(key)) {
throw new Error(i18n.t('error-custom-field-not-allowed', { key }));
if (!options?.ignoreAdditionalFields) {
const allowedCustomFieldIds = new Set(allowedCustomFields.map((cf) => cf._id));
for (const key in customFields) {
if (!allowedCustomFieldIds.has(key)) {
throw new Error(i18n.t('error-custom-field-not-allowed', { key }));
}
}
}

return validValues;
}

export async function validateContactManager(contactManagerUserId: string) {
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ import * as Mailer from '../../../mailer/server/api';
import { metrics } from '../../../metrics/server';
import { settings } from '../../../settings/server';
import { businessHourManager } from '../business-hour';
import { createContact } from './Contacts';
import { createContact, isSingleContactEnabled } from './Contacts';
import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper';
import { QueueManager } from './QueueManager';
import { RoutingManager } from './RoutingManager';
Expand Down Expand Up @@ -669,7 +669,7 @@ class LivechatClass {
}
}

if (process.env.TEST_MODE?.toUpperCase() === 'TRUE') {
if (isSingleContactEnabled()) {
const contactId = await createContact({
name: name ?? (visitorDataToUpdate.username as string),
emails: email ? [email] : [],
Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/server/models/raw/LivechatCustomField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ export class LivechatCustomFieldRaw extends BaseRaw<ILivechatCustomField> implem
return [{ key: { scope: 1 } }];
}

findByScope(
findByScope<T extends ILivechatCustomField>(
scope: ILivechatCustomField['scope'],
options?: FindOptions<ILivechatCustomField>,
includeHidden = true,
): FindCursor<ILivechatCustomField> {
return this.find({ scope, ...(includeHidden === true ? {} : { visibility: { $ne: 'hidden' } }) }, options);
): FindCursor<T> {
return this.find<T>({ scope, ...(includeHidden === true ? {} : { visibility: { $ne: 'hidden' } }) }, options);
}

findMatchingCustomFields(
Expand Down

0 comments on commit 5965a1d

Please sign in to comment.