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

feat: Allow searching by email and phone number in the contacts.search endpoint #34107

Closed
wants to merge 7 commits into from
Closed
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/serious-adults-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/model-typings": minor
"@rocket.chat/rest-typings": minor
---

Adds `email` and `phone` number query params to the `omnichannel/contacts.search` endpoint, which allow applying an exact search to these fields
6 changes: 4 additions & 2 deletions apps/meteor/app/livechat/server/lib/contacts/getContacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ export type GetContactsParams = {
offset: number;
sort: Sort;
unknown?: boolean;
email?: string;
phone?: string;
};

export async function getContacts(params: GetContactsParams): Promise<PaginatedResult<{ contacts: ILivechatContactWithManagerData[] }>> {
const { searchText, count, offset, sort, unknown } = params;
const { searchText, count, offset, sort, unknown, email, phone } = params;

const { cursor, totalCount } = LivechatContacts.findPaginatedContacts(
{ searchText, unknown },
{ searchText, unknown, email, phone },
{
limit: count,
skip: offset,
Expand Down
18 changes: 11 additions & 7 deletions apps/meteor/server/models/raw/LivechatContacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,21 @@ export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements IL
}

findPaginatedContacts(
search: { searchText?: string; unknown?: boolean },
search: { searchText?: string; unknown?: boolean; email?: string; phone?: string },
options?: FindOptions,
): FindPaginated<FindCursor<ILivechatContact>> {
const { searchText, unknown = false } = search;
const { searchText, unknown = false, email, phone } = search;
const searchRegex = escapeRegExp(searchText || '');
const match: Filter<ILivechatContact & RootFilterOperators<ILivechatContact>> = {
$or: [
{ name: { $regex: searchRegex, $options: 'i' } },
{ 'emails.address': { $regex: searchRegex, $options: 'i' } },
{ 'phones.phoneNumber': { $regex: searchRegex, $options: 'i' } },
],
...(searchText && {
$or: [
{ name: { $regex: searchRegex, $options: 'i' } },
{ 'emails.address': { $regex: searchRegex, $options: 'i' } },
{ 'phones.phoneNumber': { $regex: searchRegex, $options: 'i' } },
],
}),
...(email && { 'emails.address': email }),
...(phone && { 'phones.phoneNumber': phone }),
unknown,
};

Expand Down
76 changes: 74 additions & 2 deletions apps/meteor/tests/end-to-end/api/livechat/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ describe('LIVECHAT - contacts', () => {
expect(res.body.contacts[0].unknown).to.be.true;
});

it('should return only contacts that match the searchText using email', async () => {
it('should return only contacts that match the searchText when an email is provided', async () => {
const res = await request.get(api(`omnichannel/contacts.search`)).set(credentials).query({ searchText: contact.emails[0] });
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
Expand All @@ -1017,7 +1017,43 @@ describe('LIVECHAT - contacts', () => {
expect(res.body.contacts[0].emails[0].address).to.be.equal(contact.emails[0]);
});

it('should return only contacts that match the searchText using phone number', async () => {
it('should return only contacts that match the provided email when the email param is provided', async () => {
const res = await request.get(api(`omnichannel/contacts.search`)).set(credentials).query({ email: contact.emails[0] });
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body.contacts).to.be.an('array');
expect(res.body.contacts.length).to.be.equal(1);
expect(res.body.total).to.be.equal(1);
expect(res.body.contacts[0]._id).to.be.equal(contactId);
expect(res.body.contacts[0].name).to.be.equal(contact.name);
expect(res.body.contacts[0].emails[0].address).to.be.equal(contact.emails[0]);
});

it('should return an empty list when an invalid email is provided along with a valid searchText', async () => {
const res = await request
.get(api(`omnichannel/contacts.search`))
.set(credentials)
.query({ email: 'invalid', searchText: contact.name });
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body.contacts).to.be.an('array');
expect(res.body.contacts.length).to.be.equal(0);
expect(res.body.total).to.be.equal(0);
});

it('should return an empty list when an invalid searchText is provided along with a valid email', async () => {
const res = await request
.get(api(`omnichannel/contacts.search`))
.set(credentials)
.query({ email: contact.emails[0], searchText: 'invalid' });
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body.contacts).to.be.an('array');
expect(res.body.contacts.length).to.be.equal(0);
expect(res.body.total).to.be.equal(0);
});

it('should return only contacts that match the searchText when a phone number is provided', async () => {
const res = await request.get(api(`omnichannel/contacts.search`)).set(credentials).query({ searchText: contact.phones[0] });
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
Expand All @@ -1029,6 +1065,42 @@ describe('LIVECHAT - contacts', () => {
expect(res.body.contacts[0].phones[0].phoneNumber).to.be.equal(contact.phones[0]);
});

it('should return only contacts that match the provided phone number when the phone param is provided', async () => {
const res = await request.get(api(`omnichannel/contacts.search`)).set(credentials).query({ phone: contact.phones[0] });
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body.contacts).to.be.an('array');
expect(res.body.contacts.length).to.be.equal(1);
expect(res.body.total).to.be.equal(1);
expect(res.body.contacts[0]._id).to.be.equal(contactId);
expect(res.body.contacts[0].name).to.be.equal(contact.name);
expect(res.body.contacts[0].phones[0].phoneNumber).to.be.equal(contact.phones[0]);
});

it('should return an empty list when an invalid phone is provided along with a valid searchText', async () => {
const res = await request
.get(api(`omnichannel/contacts.search`))
.set(credentials)
.query({ phone: 'invalid', searchText: contact.name });
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body.contacts).to.be.an('array');
expect(res.body.contacts.length).to.be.equal(0);
expect(res.body.total).to.be.equal(0);
});

it('should return an empty list when an invalid searchText is provided along with a valid phone', async () => {
const res = await request
.get(api(`omnichannel/contacts.search`))
.set(credentials)
.query({ phone: contact.phones[0], searchText: 'invalid' });
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body.contacts).to.be.an('array');
expect(res.body.contacts.length).to.be.equal(0);
expect(res.body.total).to.be.equal(0);
});

it('should return only contacts that match the searchText using name', async () => {
const res = await request.get(api(`omnichannel/contacts.search`)).set(credentials).query({ searchText: contact.name });
expect(res.status).to.be.equal(200);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface ILivechatContactsModel extends IBaseModel<ILivechatContact> {
updateById(contactId: string, update: UpdateFilter<ILivechatContact>, options?: UpdateOptions): Promise<Document | UpdateResult>;
addChannel(contactId: string, channel: ILivechatContactChannel): Promise<void>;
findPaginatedContacts(
search: { searchText?: string; unknown?: boolean },
search: { searchText?: string; unknown?: boolean; email?: string; phone?: string },
options?: FindOptions<ILivechatContact>,
): FindPaginated<FindCursor<ILivechatContact>>;
updateLastChatById(
Expand Down
6 changes: 6 additions & 0 deletions packages/rest-typings/src/v1/omnichannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,12 @@ const GETOmnichannelContactsSearchSchema = {
searchText: {
type: 'string',
},
email: {
type: 'string',
},
phone: {
type: 'string',
},
unknown: {
type: 'boolean',
},
Expand Down
Loading