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(#9544): add offline freetext search indexes #9661

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
23 changes: 12 additions & 11 deletions shared-libs/search/src/generate-search-requests.js
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes in shared-libs/search are just temporary. The proper changes will need to be made against the cht-datasource code once we re-base on top of that.

This is why I have not added any additional unit tests to cover this logic (or worried too much about code structure).

Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@ const makeCombinedParams = (freetextRequest, typeKey) => {
return params;
};

const getContactsByTypeAndFreetextRequest = (typeRequests, freetextRequest) => {
const getContactsByTypeAndFreetextRequest = (typeRequests, freetextRequest, freetextDdocName) => {
const result = {
view: 'medic-client/contacts_by_type_freetext',
view: `${freetextDdocName}/contacts_by_type_freetext`,
union: typeRequests.params.keys.length > 1
};

Expand All @@ -217,9 +217,9 @@ const getContactsByTypeAndFreetextRequest = (typeRequests, freetextRequest) => {
return result;
};

const getCombinedContactsRequests = (freetextRequests, contactsByParentRequest, typeRequest) => {
const getCombinedContactsRequests = (freetextRequests, contactsByParentRequest, typeRequest, freetextDdocName) => {
const combinedRequests = freetextRequests.map(freetextRequest => {
return getContactsByTypeAndFreetextRequest(typeRequest, freetextRequest);
return getContactsByTypeAndFreetextRequest(typeRequest, freetextRequest, freetextDdocName);
});
if (contactsByParentRequest) {
combinedRequests.unshift(contactsByParentRequest);
Expand All @@ -241,14 +241,14 @@ const setDefaultContactsRequests = (requests, shouldSortByLastVisitedDate) => {
};

const requestBuilders = {
reports: (filters) => {
reports: (filters, freetextDdocName) => {
let requests = [
reportedDateRequest(filters),
formRequest(filters),
validityRequest(filters),
verificationRequest(filters),
placeRequest(filters),
freetextRequest(filters, 'medic-client/reports_by_freetext'),
freetextRequest(filters, `${freetextDdocName}/reports_by_freetext`),
subjectRequest(filters)
];

Expand All @@ -258,10 +258,10 @@ const requestBuilders = {
}
return requests;
},
contacts: (filters, extensions) => {
contacts: (filters, freetextDdocName, extensions) => {
const shouldSortByLastVisitedDate = module.exports.shouldSortByLastVisitedDate(extensions);

const freetextRequests = freetextRequest(filters, 'medic-client/contacts_by_freetext');
const freetextRequests = freetextRequest(filters, `${freetextDdocName}/contacts_by_freetext`);
const contactsByParentRequest = getContactsByParentRequest(filters);
const typeRequest = contactTypeRequest(filters, shouldSortByLastVisitedDate);
const hasTypeRequest = typeRequest?.params.keys.length;
Expand All @@ -272,7 +272,7 @@ const requestBuilders = {
}

if (hasTypeRequest && freetextRequests?.length) {
return getCombinedContactsRequests(freetextRequests, contactsByParentRequest, typeRequest);
return getCombinedContactsRequests(freetextRequests, contactsByParentRequest, typeRequest, freetextDdocName);
}

const requests = _.compact(_.flatten([ freetextRequests, typeRequest, contactsByParentRequest ]));
Expand Down Expand Up @@ -313,12 +313,13 @@ const requestBuilders = {
//
// NB: options is not required: it is an optimisation shortcut
module.exports = {
generate: (type, filters, extensions) => {
generate: (type, filters, extensions, offline) => {
const freetextDdocName = offline ? 'medic-offline-freetext' : 'medic-client';
const builder = requestBuilders[type];
if (!builder) {
throw new Error('Unknown type: ' + type);
}
return builder(filters, extensions);
return builder(filters, freetextDdocName, extensions);
},
shouldSortByLastVisitedDate: (extensions) => {
return Boolean(extensions?.sortByLastVisitedDate);
Expand Down
10 changes: 8 additions & 2 deletions shared-libs/search/src/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ _.flatten = require('lodash/flatten');
_.intersection = require('lodash/intersection');
const GenerateSearchRequests = require('./generate-search-requests');

const ddocExists = (db, ddocId) => db
.get(ddocId)
.then(() => true)
.catch(() => false);

module.exports = function(Promise, DB) {
// Get the subset of rows, in appropriate order, according to options.
const getPageRows = function(type, rows, options) {
Expand Down Expand Up @@ -111,17 +116,18 @@ module.exports = function(Promise, DB) {
});
};

return function(type, filters, options, extensions) {
return async (type, filters, options, extensions) => {
options = options || {};
_.defaults(options, {
limit: 50,
skip: 0
});

const offline = await ddocExists(DB, '_design/medic-offline-freetext');
const cacheQueryResults = GenerateSearchRequests.shouldSortByLastVisitedDate(extensions);
let requests;
try {
requests = GenerateSearchRequests.generate(type, filters, extensions);
requests = GenerateSearchRequests.generate(type, filters, extensions, offline);
} catch (err) {
return Promise.reject(err);
}
Expand Down
3 changes: 2 additions & 1 deletion shared-libs/search/test/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ describe('Search service', function() {
GenerateSearchRequests.generate = sinon.stub();
GenerateSearchRequests.shouldSortByLastVisitedDate = sinon.stub();
DB = {
query: sinon.stub()
query: sinon.stub(),
get: sinon.stub().rejects()
};

service = Search(Promise, DB);
Expand Down
110 changes: 74 additions & 36 deletions tests/e2e/default/contacts/search-contacts.wdio-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,101 @@ const contactPage = require('@page-objects/default/contacts/contacts.wdio.page')
const commonPage = require('@page-objects/default/common/common.wdio.page');
const placeFactory = require('@factories/cht/contacts/place');
const personFactory = require('@factories/cht/contacts/person');
const userFactory = require('@factories/cht/users/users');

describe('Contact Search', () => {
const places = placeFactory.generateHierarchy();
const districtHospitalId = places.get('district_hospital')._id;

const sittuHospital = placeFactory.place().build({
name: 'Sittu Hospital',
type: 'district_hospital',
parent: { _id: '', parent: { _id: '' } }
const sittuHealthCenter = placeFactory.place().build({
name: 'Sittu Health Center',
type: 'health_center',
parent: { _id: districtHospitalId, parent: { _id: '' } }
});

const potuHospital = placeFactory.place().build({
name: 'Potu Hospital',
type: 'district_hospital',
parent: { _id: '', parent: { _id: '' } }
const potuHealthCenter = placeFactory.place().build({
Comment on lines +14 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: what's a sittu and potu?

name: 'Potu Health Center',
type: 'health_center',
parent: { _id: districtHospitalId, parent: { _id: '' } }
});

const sittuPerson = personFactory.build({
name: 'Sittu',
parent: { _id: sittuHospital._id, parent: sittuHospital.parent }
parent: { _id: sittuHealthCenter._id, parent: sittuHealthCenter.parent }
});

const potuPerson = personFactory.build({
name: 'Potu',
parent: { _id: sittuHospital._id, parent: sittuHospital.parent }
parent: { _id: sittuHealthCenter._id, parent: sittuHealthCenter.parent }
});

before(async () => {
await utils.saveDocs([...places.values(), sittuHospital, sittuPerson, potuHospital, potuPerson]);
await loginPage.cookieLogin();
await commonPage.goToPeople();
const supervisorPerson = personFactory.build({
name: 'Supervisor',
parent: { _id: districtHospitalId }
});

it('search by NON empty string should display results with contains match and clears search', async () => {
await contactPage.getAllLHSContactsNames();

await searchPage.performSearch('sittu');
expect(await contactPage.getAllLHSContactsNames()).to.have.members([
sittuPerson.name,
sittuHospital.name,
]);
const offlineUser = userFactory.build({
username: 'offline-search-user',
place: districtHospitalId,
roles: ['chw_supervisor'],
contact: supervisorPerson._id
});
const onlineUser = userFactory.build({
username: 'online-search-user',
place: districtHospitalId,
roles: ['program_officer'],
contact: supervisorPerson._id
});

await searchPage.clearSearch();
expect(await contactPage.getAllLHSContactsNames()).to.have.members([
potuHospital.name,
sittuHospital.name,
places.get('district_hospital').name,
before(async () => {
await utils.saveDocs([
...places.values(), sittuHealthCenter, sittuPerson, potuHealthCenter, potuPerson, supervisorPerson
]);
await utils.createUsers([offlineUser, onlineUser]);
});

it('search should clear RHS selected contact', async () => {
await contactPage.selectLHSRowByText(potuHospital.name, false);
await contactPage.waitForContactLoaded();
expect(await (await contactPage.contactCardSelectors.contactCardName()).getText()).to.equal(potuHospital.name);
after(() => utils.deleteUsers([offlineUser, onlineUser]));

await searchPage.performSearch('sittu');
await contactPage.waitForContactUnloaded();
const url = await browser.getUrl();
expect(url.endsWith('/contacts')).to.equal(true);
});
[
['online', onlineUser],
['offline', offlineUser],
].forEach(([userType, user]) => describe(`Logged in as an ${userType} user`, () => {
before(async () => {
await loginPage.login(user);
await commonPage.goToPeople();
});

after(commonPage.logout);

it('search by NON empty string should display results with contains match and clears search', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('search by NON empty string should display results with contains match and clears search', async () => {
it('search by NON empty string should display results which contains match and clears search', async () => {

await contactPage.getAllLHSContactsNames();

await searchPage.performSearch('sittu');
expect(await contactPage.getAllLHSContactsNames()).to.have.members([
sittuPerson.name,
sittuHealthCenter.name,
]);

await searchPage.clearSearch();
expect(await contactPage.getAllLHSContactsNames()).to.have.members([
potuHealthCenter.name,
sittuHealthCenter.name,
places.get('district_hospital').name,
places.get('health_center').name,
]);
});

it('search should clear RHS selected contact', async () => {
await contactPage.selectLHSRowByText(potuHealthCenter.name, false);
await contactPage.waitForContactLoaded();
expect(
await (await contactPage.contactCardSelectors.contactCardName()).getText()
).to.equal(potuHealthCenter.name);

await searchPage.performSearch('sittu');
await contactPage.waitForContactUnloaded();
const url = await browser.getUrl();
expect(url.endsWith('/contacts')).to.equal(true);
});
}));
});
6 changes: 4 additions & 2 deletions tests/e2e/default/db/initial-replication.wdio-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const commonPage = require('@page-objects/default/common/common.wdio.page');
const loginPage = require('@page-objects/default/login/login.wdio.page');
const dataFactory = require('@factories/cht/generate');

const LOCAL_ONLY_DOC_IDS = ['_design/medic-offline-freetext'];

describe('initial-replication', () => {
const LOCAL_LOG = '_local/initial-replication';

Expand Down Expand Up @@ -52,11 +54,11 @@ describe('initial-replication', () => {

await commonPage.sync(false, 7000);

const localAllDocs = await chtDbUtils.getDocs();
const localAllDocs = (await chtDbUtils.getDocs()).filter(doc => !LOCAL_ONLY_DOC_IDS.includes(doc.id));
const localDocIds = dataFactory.ids(localAllDocs);

// no additional docs to download
expect(docIdsPreSync).to.have.members(localDocIds);
expect(docIdsPreSync).to.have.members([...localDocIds, ...LOCAL_ONLY_DOC_IDS]);

const serverAllDocs = await getServerDocs(localDocIds);

Expand Down
Loading