Skip to content
Draft
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
30 changes: 24 additions & 6 deletions admin/src/js/services/message-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ const registrationUtils = require('@medic/registration-utils');
const constants = require('@medic/constants');
const DOC_TYPES = constants.DOC_TYPES;

const collect = function(generator) {
const results = [];
const iterate = function() {
return generator.next().then(function(result) {
if (result.done) {
return results;
}
results.push(result.value);
return iterate();
});
};
return iterate();
};

angular.module('services').factory('MessageQueueUtils',
function(
$q,
Expand All @@ -26,6 +40,7 @@ angular.module('services').factory('MessageQueue',
$q,
$translate,
DB,
DataContext,
GetSummaries,
Languages,
MessageQueueUtils,
Expand Down Expand Up @@ -93,12 +108,15 @@ angular.module('services').factory('MessageQueue',
return messages;
}

return DB({ remote: true })
.query('medic-client/contacts_by_phone', { keys: phoneNumbers })
.then(function(contactsByPhone) {
const ids = contactsByPhone.rows.map(function(row) {
return row.id;
});
return DataContext
.then(function(dataContext) {
const contact = dataContext.getDatasource().v1.contact;
return $q.all(phoneNumbers.map(function(phone) {
return collect(contact.getUuidsByPhone(String(phone)));
}));
})
.then(function(idsPerPhone) {
const ids = compactUnique(idsPerPhone.flat());

return GetSummaries.getContacts(ids);
})
Expand Down
64 changes: 27 additions & 37 deletions admin/tests/unit/services/message-queue.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,22 @@ describe('MessageQueue service', function() {
let utils;
let query;
let GetSummaries;
let getUuidsByPhone;
let translate;
let clock;

const asyncGeneratorOf = (ids) => (async function* () {
for (const id of ids) {
yield id;
}
})();

beforeEach(() => {
Settings = sinon.stub();
Languages = sinon.stub();
query = sinon.stub();
GetSummaries = sinon.stub();
getUuidsByPhone = sinon.stub().returns(asyncGeneratorOf([]));
translate = sinon.stub();
translate.instant = sinon.stub();
translate.storageKey = sinon.stub();
Expand Down Expand Up @@ -46,6 +54,9 @@ describe('MessageQueue service', function() {
$provide.value('Languages', Languages);
$provide.value('MessageQueueUtils', utils);
$provide.value('GetSummaries', { getContacts: GetSummaries });
$provide.value('DataContext', Q.resolve({
getDatasource: () => ({ v1: { contact: { getUuidsByPhone } } })
}));
$provide.factory('DB', KarmaUtils.mockDB({ query: query }));
});

Expand Down Expand Up @@ -288,9 +299,8 @@ describe('MessageQueue service', function() {
.withArgs('medic-admin/message_queue', sinon.match({ reduce: false }))
.resolves({ rows: messages });

query.withArgs('medic-client/contacts_by_phone').resolves({
rows: [{ id: 'contact1', value: 'contact1', key: 'phone1' }]
});
getUuidsByPhone.withArgs('phone1').returns(asyncGeneratorOf(['contact1']));
getUuidsByPhone.withArgs('phone2').returns(asyncGeneratorOf([]));

GetSummaries
.withArgs(['contact1'])
Expand All @@ -306,12 +316,9 @@ describe('MessageQueue service', function() {

return service.query('due').then(result => {
chai.expect(result.total).to.equal(2);
chai.expect(query.callCount).to.equal(3);
chai.expect(query.callCount).to.equal(2);

chai.expect(query.args[2]).to.deep.equal([
'medic-client/contacts_by_phone',
{ keys: ['phone1', 'phone2'] }
]);
chai.expect(getUuidsByPhone.args).to.deep.equal([['phone1'], ['phone2']]);

chai.expect(GetSummaries.callCount).to.equal(1);
chai.expect(GetSummaries.args[0][0]).to.deep.equal(['contact1']);
Expand Down Expand Up @@ -409,20 +416,17 @@ describe('MessageQueue service', function() {
.withArgs('medic-admin/message_queue', sinon.match({ reduce: false }))
.resolves({ rows: messages });

query
.withArgs('medic-client/contacts_by_phone')
.resolves({ rows: [ { value: 'contact1', key: 'phone1' }, { value: 'contact2', key: 'phone2' } ] });
getUuidsByPhone.withArgs('phone1').returns(asyncGeneratorOf(['contact1']));
getUuidsByPhone.withArgs('phone2').returns(asyncGeneratorOf(['contact2']));

GetSummaries.resolves([
{ _id: 'contact1', type: 'person', name: 'contact one', phone: 'phone1' },
{ _id: 'contact2', type: 'person', name: 'contact two', phone: 'phone2' },
]);

return service.query('due').then(result => {
chai.expect(query.callCount).to.equal(3);
chai.expect(query.args[2]).to.deep.equal([
'medic-client/contacts_by_phone', { keys: [ 'phone1', 'phone2' ]}
]);
chai.expect(query.callCount).to.equal(2);
chai.expect(getUuidsByPhone.args).to.deep.equal([['phone1'], ['phone2']]);

chai.expect(result.messages[0].recipient).to.equal('contact one');
chai.expect(result.messages[1].recipient).to.equal('contact one');
Expand Down Expand Up @@ -584,14 +588,12 @@ describe('MessageQueue service', function() {
to: recipient
}]));

query
.withArgs('medic-client/contacts_by_phone')
.resolves({ rows: [{ key: 'recipient_id', value: 'recipient' }]});
getUuidsByPhone.withArgs('recipient').returns(asyncGeneratorOf(['recipient_id']));
GetSummaries.resolves([{ _id: 'recipient_id', type: 'person', phone: 'recipient' }]);

return service.query('tab').then(result => {
chai.expect(result.messages.length).to.equal(15);
chai.expect(query.callCount).to.equal(5);
chai.expect(query.callCount).to.equal(4);
chai.expect(query.args[2]).to.deep.equal([
'medic-client/contacts_by_reference',
{
Expand All @@ -615,10 +617,7 @@ describe('MessageQueue service', function() {
[ 'patient1', 'patient2', 'patient3', 'place1', 'place2', 'place3' ],
]);

chai.expect(query.args[4]).to.deep.equal([
'medic-client/contacts_by_phone',
{ keys: [ 'recipient' ]}
]);
chai.expect(getUuidsByPhone.args).to.deep.equal([['recipient']]);
});
});

Expand Down Expand Up @@ -749,13 +748,9 @@ describe('MessageQueue service', function() {
.withArgs(sinon.match({ type: 'valid' })).returns(true)
.withArgs(sinon.match({ type: 'invalid' })).returns(false);

query
.withArgs('medic-client/contacts_by_phone')
.resolves({ rows: [
{ key: 'recipient1', id: 'recipient1_id' },
{ key: 'recipient2', id: 'recipient2_id' },
{ key: 'recipient3', id: 'recipient3_id' }
]});
getUuidsByPhone.withArgs('recipient1').returns(asyncGeneratorOf(['recipient1_id']));
getUuidsByPhone.withArgs('recipient2').returns(asyncGeneratorOf(['recipient2_id']));
getUuidsByPhone.withArgs('recipient3').returns(asyncGeneratorOf(['recipient3_id']));
GetSummaries.resolves([
{ _id: 'recipient1_id', type: 'person', phone: 'recipient1', name: 'recipient 1' },
{ _id: 'recipient2_id', type: 'person', phone: 'recipient2', name: 'recipient 2' },
Expand Down Expand Up @@ -924,10 +919,7 @@ describe('MessageQueue service', function() {
}
]);

chai.expect(query.withArgs('medic-client/contacts_by_phone').callCount).to.equal(1);
chai.expect(query.withArgs('medic-client/contacts_by_phone').args[0][1]).to.deep.equal({
keys: ['recipient1', 'recipient2', 'recipient3']
});
chai.expect(getUuidsByPhone.args).to.deep.equal([['recipient1'], ['recipient2'], ['recipient3']]);

chai.expect(result.messages).to.deep.equal([
{
Expand Down Expand Up @@ -1069,9 +1061,7 @@ describe('MessageQueue service', function() {
.withArgs(sinon.match({ type: 'valid' })).returns(true)
.withArgs(sinon.match({ type: 'invalid' })).returns(false);

query
.withArgs('medic-client/contacts_by_phone')
.resolves({ rows: [{ key: 'recipient1', id: 'recipien_id' }]});
getUuidsByPhone.withArgs('recipient1').returns(asyncGeneratorOf(['recipien_id']));
GetSummaries.resolves([
{ _id: 'recipien_id', type: 'person', phone: 'recipient1', name: 'recipient' },
]);
Expand Down
80 changes: 57 additions & 23 deletions api/src/controllers/contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ const buildIdsQualifier = (ids) => {
return Qualifier.byIds(idsArray);
};

const buildFreetextTypePhoneQualifier = (query) => {
if (query.phone) {
return Qualifier.byPhone(query.phone);
}
const qualifier = {};
if (query.freetext) {
Object.assign(qualifier, Qualifier.byFreetext(query.freetext));
}
if (query.type) {
Object.assign(qualifier, Qualifier.byContactType(query.type));
}
return qualifier;
};

/**
* @openapi
* tags:
Expand Down Expand Up @@ -81,7 +95,8 @@ module.exports = {
* operationId: v1ContactUuidGet
* description: >
* Returns a paginated array of contact identifier strings matching the given filter criteria.
* At least one of `type` or `freetext` must be provided.
* At least one of `type`, `freetext`, or `phone` must be provided. When `phone` is provided it is used
* on its own (it cannot be combined with `type` or `freetext`) and takes precedence over them.
* tags: [Contact]
* x-since: 4.18.0
* x-permissions:
Expand All @@ -92,16 +107,23 @@ module.exports = {
* schema:
* type: string
* description: >
* The contact_type id for the type of contacts to fetch. Required if `freetext` is not provided
* and may be combined with `freetext`.
* The contact_type id for the type of contacts to fetch. Required if `freetext` and `phone` are not
* provided and may be combined with `freetext`.
* - in: query
* name: freetext
* schema:
* type: string
* minLength: 3
* description: >
* A search term for filtering contacts. Must be at least 3 characters and not contain whitespace.
* Required if `type` is not provided and may be combined with `type`.
* Required if `type` and `phone` are not provided and may be combined with `type`.
* - in: query
* name: phone
* schema:
* type: string
* description: >
* A phone number to fetch matching contacts by. Matched verbatim (no normalisation). Required if
* `type` and `freetext` are not provided. Takes precedence over `type` and `freetext`.
* - $ref: '#/components/parameters/cursor'
* - $ref: '#/components/parameters/limitId'
* responses:
Expand Down Expand Up @@ -129,16 +151,12 @@ module.exports = {
*/
getUuids: serverUtils.doOrError(async (req, res) => {
await auth.assertPermissions(req, { isOnline: true, hasAll: ['can_view_contacts'] });
if (!req.query.freetext && !req.query.type) {
return serverUtils.error({ status: 400, message: 'Either query param freetext or type is required' }, req, res);
}
const qualifier = {};
if (req.query.freetext) {
Object.assign(qualifier, Qualifier.byFreetext(req.query.freetext));
}
if (req.query.type) {
Object.assign(qualifier, Qualifier.byContactType(req.query.type));
if (!req.query.freetext && !req.query.type && !req.query.phone) {
return serverUtils.error(
{ status: 400, message: 'Either query param freetext, type or phone is required' }, req, res
);
}
const qualifier = buildFreetextTypePhoneQualifier(req.query);
const docs = await getContactIds(qualifier, req.query.cursor, req.query.limit);
return res.json(docs);
}),
Expand All @@ -151,8 +169,9 @@ module.exports = {
* operationId: v1ContactGet
* description: >
* Returns a paginated array of contact records (persons and places) matching the given filter criteria.
* At least one of `ids` or `type` must be provided. If both are provided, `ids` takes precedence over
* `type`. Use the `cursor` returned in each response to retrieve subsequent pages.
* At least one of `ids`, `type`, or `phone` must be provided. When more than one is provided the
* precedence is `ids`, then `phone`, then `type`. Use the `cursor` returned in each response to retrieve
* subsequent pages.
* tags: [Contact]
* x-since: 5.3.0
* x-permissions:
Expand All @@ -163,14 +182,22 @@ module.exports = {
* schema:
* type: string
* description: >
* A comma-separated list of contact ids to fetch. Required if `type` is not provided. Takes
* precedence over `type` when both are provided.
* A comma-separated list of contact ids to fetch. Required if `type` and `phone` are not provided.
* Takes precedence over `phone` and `type` when more than one is provided.
* - in: query
* name: type
* schema:
* type: string
* description: >
* The contact_type id for the type of contacts to fetch. Required if `ids` is not provided.
* The contact_type id for the type of contacts to fetch. Required if `ids` and `phone` are not
* provided.
* - in: query
* name: phone
* schema:
* type: string
* description: >
* A phone number to fetch matching contacts by. Matched verbatim (no normalisation). Required if
* `ids` and `type` are not provided. Takes precedence over `type`.
* - $ref: '#/components/parameters/cursor'
* - $ref: '#/components/parameters/limitEntity'
* responses:
Expand Down Expand Up @@ -198,12 +225,19 @@ module.exports = {
*/
getAll: serverUtils.doOrError(async (req, res) => {
await auth.assertPermissions(req, { isOnline: true, hasAll: ['can_view_contacts'] });
if (!req.query.ids && !req.query.type) {
return serverUtils.error({ status: 400, message: 'Either query param ids or type is required' }, req, res);
if (!req.query.ids && !req.query.type && !req.query.phone) {
return serverUtils.error(
{ status: 400, message: 'Either query param ids, type or phone is required' }, req, res
);
}
let qualifier;
if (req.query.ids) {
qualifier = buildIdsQualifier(req.query.ids);
} else if (req.query.phone) {
qualifier = Qualifier.byPhone(req.query.phone);
} else {
qualifier = Qualifier.byContactType(req.query.type);
}
const qualifier = req.query.ids
? buildIdsQualifier(req.query.ids)
: Qualifier.byContactType(req.query.type);
const docs = await getContactDocs(qualifier, req.query.cursor, req.query.limit);
return res.json(docs);
}),
Expand Down
Loading