Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
67e737e
perf(10749): move tasks_by_contact view to offline-only design document
sh1vam31 Mar 28, 2026
f1d1dc2
feat(api): whitelist new offline tasks design document
sh1vam31 Mar 30, 2026
bbd8b62
fix(api): restore missing constant and fix lint errors in authorizati…
sh1vam31 Mar 30, 2026
2e9ced3
Merge branch 'master' into 10749-move-tasks-by-contact-to-offline-cli…
sh1vam31 Mar 30, 2026
bdc21d4
Merge branch 'master' into 10749-move-tasks-by-contact-to-offline-cli…
sh1vam31 Apr 1, 2026
052c692
revert: remove unnecessary authorization changes for offline ddoc
sh1vam31 Apr 7, 2026
bd175bd
Merge branch 'master' into 10749-move-tasks-by-contact-to-offline-cli…
sh1vam31 Apr 7, 2026
5d3ed0f
test: stabilize flaky Import Records JSON test by allowing api reload
sh1vam31 Apr 7, 2026
bb38b2d
test: include medic-offline-tasks in local only docs assertion
sh1vam31 Apr 9, 2026
9fa26b2
Merge branch 'master' into 10749-move-tasks-by-contact-to-offline-cli…
sh1vam31 Apr 9, 2026
eddcd45
test: ignore flaky nouveau doc_count variations in monitoring checks
sh1vam31 Apr 9, 2026
3dbf503
chore: retry famously flaky message duplicates test
sh1vam31 Apr 9, 2026
fbd124c
chore: remove unrelated changes in integration tests as requested
sh1vam31 Apr 10, 2026
b9f87cb
fix(tests): add request retry logic and clean up unrelated changes
sh1vam31 Apr 10, 2026
72cb37c
fix(tests): resolve linting errors in request retry logs
sh1vam31 Apr 10, 2026
e332890
Merge branch 'master' into 10749-move-tasks-by-contact-to-offline-cli…
sh1vam31 Apr 14, 2026
a8d18d2
chore: revert request retry logic as per maintainer feedback
sh1vam31 Apr 15, 2026
cf08e7e
Merge branch 'master' into 10749-move-tasks-by-contact-to-offline-cli…
sh1vam31 Apr 15, 2026
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
15 changes: 12 additions & 3 deletions api/src/services/authorization.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ const { DOC_IDS } = require('@medic/constants');
const ALL_KEY = '_all'; // key in the docs_by_replication_key view for records everyone can access
const UNASSIGNED_KEY = '_unassigned'; // key in the docs_by_replication_key view for unassigned records
const MEDIC_CLIENT_DDOC = '_design/medic-client';
const MEDIC_OFFLINE_TASKS_DDOC = '_design/medic-offline-tasks';
const DEFAULT_DDOCS = [
MEDIC_CLIENT_DDOC,
MEDIC_OFFLINE_TASKS_DDOC,
DOC_IDS.SERVICE_WORKER_META,
DOC_IDS.SETTINGS,
];
Expand Down Expand Up @@ -188,9 +190,12 @@ const updateContext = (allowed, authorizationContext, { contactsByDepth }) => {
* @param {DocByReplicationKey} docsByReplicationKey - result of `medic/_nouveau/docs_by_replication_key` index
* @param {Array} contactsByDepth - results of `medic/contacts_by_depth` view against doc
* @returns {Boolean}
*/
const allowedDoc = (docId, authorizationContext, { docsByReplicationKey, contactsByDepth }) => {
if ([MEDIC_CLIENT_DDOC, getUserSettingsId(authorizationContext.userCtx.name)].includes(docId)) {
if ([
MEDIC_CLIENT_DDOC,
MEDIC_OFFLINE_TASKS_DDOC,
getUserSettingsId(authorizationContext.userCtx.name),
].includes(docId)) {
return true;
}

Expand Down Expand Up @@ -691,7 +696,11 @@ const getDocsByReplicationKey = async (authorizationContext) => {
* @returns {string[]}
*/
const filterAllowedDocIds = (authCtx, docsByReplicationKey, { includeTasks = true } = {}) => {
const validatedIds = [MEDIC_CLIENT_DDOC, getUserSettingsId(authCtx.userCtx.name)];
const validatedIds = [
MEDIC_CLIENT_DDOC,
MEDIC_OFFLINE_TASKS_DDOC,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this change is necessarry? This list is of documents on the server side that will be replicated to offline clients...but MEDIC_OFFLINE_TASKS_DDOC doesn't exist on the server at all; we don't need to add medic-offline-freetext here so also shouldn't for medic-offline-tasks

getUserSettingsId(authCtx.userCtx.name),
];

if (!docsByReplicationKey || !docsByReplicationKey.length) {
return validatedIds;
Expand Down
10 changes: 10 additions & 0 deletions shared-libs/memdown/src/memdown-medic.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ module.exports = (rootDir='./') => {
if (!ddocs) {
ddocs = [];
filesIn(`${rootDir}/ddocs/medic-db`).forEach(ddoc => loadDdoc(rootDir, 'medic-db', ddoc));

// Load offline-only tasks view for tests
const tasksMapPath = `${rootDir}/webapp/src/js/bootstrapper/offline-ddocs/medic-offline-tasks/tasks_by_contact.js`;
if (fs.existsSync(tasksMapPath)) {
const tasksMap = readFile(tasksMapPath).replace(/module.exports.map = /, '');
ddocs.push({
_id: '_design/medic-offline-tasks',
views: { tasks_by_contact: { map: tasksMap } }
});
}
}
const db = new PouchDB(uuid(), { adapter: 'memory' });
return Promise.all(ddocs.map(ddoc => db.put(ddoc)))
Expand Down
8 changes: 4 additions & 4 deletions shared-libs/rules-engine/src/pouchdb-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const medicPouchProvider = db => {
// For users with ~1000 contacts it is ~50x faster to provider a start/end key instead of specifying all ids
allTasks: prefix => {
const options = { startkey: `${prefix}-`, endkey: `${prefix}-\ufff0`, include_docs: true };
return docsOf(dbQuery('medic-client/tasks_by_contact', options));
return docsOf(dbQuery('medic-offline-tasks/tasks_by_contact', options));
},

allTaskData: userSettingsDoc => {
Expand Down Expand Up @@ -108,12 +108,12 @@ const medicPouchProvider = db => {

tasksByRelation: (contactIds, prefix) => {
const keys = contactIds.map(contactId => `${prefix}-${contactId}`);
return docsOf(dbQuery( 'medic-client/tasks_by_contact', { keys, include_docs: true }));
return docsOf(dbQuery( 'medic-offline-tasks/tasks_by_contact', { keys, include_docs: true }));
},

allTaskRowsByOwner: (contactIds) => {
const keys = contactIds.map(contactId => (['owner', 'all', contactId]));
return rowsOf(dbQuery( 'medic-client/tasks_by_contact', { keys }));
return rowsOf(dbQuery( 'medic-offline-tasks/tasks_by_contact', { keys }));
},

allTaskRows: () => {
Expand All @@ -122,7 +122,7 @@ const medicPouchProvider = db => {
endkey: ['owner', 'all', '\ufff0'],
};

return rowsOf(dbQuery( 'medic-client/tasks_by_contact', options));
return rowsOf(dbQuery( 'medic-offline-tasks/tasks_by_contact', options));
},

taskDataFor: async (contactIds, userSettingsDoc) => {
Expand Down
14 changes: 7 additions & 7 deletions shared-libs/rules-engine/test/integration.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ const reportByPatientIdOnly = {
const expectedQueriesForAllFreshData = [
'medic-client/contacts_by_type',
'medic-client/reports_by_subject',
'medic-client/tasks_by_contact'
'medic-offline-tasks/tasks_by_contact'
];
const expectedQueriesForFreshData = [
'medic-client/reports_by_subject',
'medic-client/tasks_by_contact',
'medic-client/tasks_by_contact',
'medic-offline-tasks/tasks_by_contact',
'medic-offline-tasks/tasks_by_contact',
];

const fetchTargets = async (interval) => {
Expand Down Expand Up @@ -455,7 +455,7 @@ describe(`Rules Engine Integration Tests`, () => {
const tasksAfterPurge = await rulesEngine.fetchTasksFor();
expect(tasksAfterPurge).to.have.property('length', 0);

const allTasks = await db.query('medic-client/tasks_by_contact');
const allTasks = await db.query('medic-offline-tasks/tasks_by_contact');
expect(allTasks.total_rows).to.eq(0);
});

Expand Down Expand Up @@ -484,7 +484,7 @@ describe(`Rules Engine Integration Tests`, () => {
expect(secondTasks).to.deep.eq(firstTasks);
expect(db.query.args.map(args => args[0])).to.deep.eq([
...expectedQueriesForAllFreshData,
'medic-client/tasks_by_contact',
'medic-offline-tasks/tasks_by_contact',
'medic-client/contacts_by_reference',
...expectedQueriesForFreshData
]);
Expand Down Expand Up @@ -542,7 +542,7 @@ describe(`Rules Engine Integration Tests`, () => {
expect(rulesEmitter.getEmissionsFor.args).excludingEvery(['_rev', 'state', 'stateHistory'])
.to.deep.eq([[[], [headlessReport, headlessReport2], [taskEmittedByHeadless2]]]);
expect(db.query.args.map(args => args[0]))
.to.deep.eq([...expectedQueriesForAllFreshData, 'medic-client/tasks_by_contact']);
.to.deep.eq([...expectedQueriesForAllFreshData, 'medic-offline-tasks/tasks_by_contact']);
expect(firstResult).excludingEvery('_rev').to.deep.eq([taskOwnedByHeadless]);
expect(db.bulkDocs.callCount).to.eq(2); // taskEmittedByHeadless2 gets cancelled

Expand Down Expand Up @@ -770,7 +770,7 @@ const triggerFacilityReminderInReadyState = async (selectBy, docs = [patientCont
const tasks = await rulesEngine.fetchTasksFor(selectBy);
expect(tasks).to.have.property('length', 1);
expect(db.query.args.map(args => args[0])).to.deep.eq(
selectBy ? expectedQueriesForFreshData : [...expectedQueriesForAllFreshData, 'medic-client/tasks_by_contact']
selectBy ? expectedQueriesForFreshData : [...expectedQueriesForAllFreshData, 'medic-offline-tasks/tasks_by_contact']
);
expect(db.bulkDocs.callCount).to.eq(2);
expect(tasks[0]).to.deep.include({
Expand Down
35 changes: 27 additions & 8 deletions shared-libs/rules-engine/test/pouchdb-provider.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,10 @@ describe('pouchdb provider', () => {
});
expect(db.query.args).to.deep.equal([
['medic-client/reports_by_subject', { keys: ['abc'], include_docs: true, ...defaultQueryParams }],
['medic-client/tasks_by_contact', { keys: ['requester-abc'], include_docs: true, ...defaultQueryParams }],
[
'medic-offline-tasks/tasks_by_contact',
{ keys: ['requester-abc'], include_docs: true, ...defaultQueryParams },
],
]);
});
it('cht contact yields', async() => {
Expand All @@ -369,11 +372,19 @@ describe('pouchdb provider', () => {
expect(db.query.args).to.deep.equal([
[
'medic-client/reports_by_subject',
{ keys: [chtDocs.contact._id, 'abc', chtDocs.contact.patient_id], include_docs: true, ...defaultQueryParams },
{
keys: [chtDocs.contact._id, 'abc', chtDocs.contact.patient_id],
include_docs: true,
...defaultQueryParams,
},
],
[
'medic-client/tasks_by_contact',
{ keys: [`requester-${chtDocs.contact._id}`, 'requester-abc'], include_docs: true, ...defaultQueryParams }
'medic-offline-tasks/tasks_by_contact',
{
keys: [`requester-${chtDocs.contact._id}`, 'requester-abc'],
include_docs: true,
...defaultQueryParams,
},
],
]);
});
Expand Down Expand Up @@ -439,11 +450,19 @@ describe('pouchdb provider', () => {
expect(db.query.args).to.deep.equal([
[
'medic-client/reports_by_subject',
{ keys: [ ...contactIds, 'place_id', 'patient_id' ], include_docs: true, ...defaultQueryParams },
{
keys: [ ...contactIds, 'place_id', 'patient_id' ],
include_docs: true,
...defaultQueryParams,
},
],
[
'medic-client/tasks_by_contact',
{ keys: contactIds.map(id => `requester-${id}`), include_docs: true, ...defaultQueryParams }
'medic-offline-tasks/tasks_by_contact',
{
keys: contactIds.map(id => `requester-${id}`),
include_docs: true,
...defaultQueryParams,
},
],
]);

Expand Down Expand Up @@ -481,7 +500,7 @@ describe('pouchdb provider', () => {
{ include_docs: true, ...defaultQueryParams },
],
[
'medic-client/tasks_by_contact',
'medic-offline-tasks/tasks_by_contact',
{ include_docs: true, ...defaultQueryParams }
],
]);
Expand Down
2 changes: 1 addition & 1 deletion shared-libs/rules-engine/test/provider-wireup.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ describe('provider-wireup integration tests', () => {
expect(actual).to.be.empty;
expect(rulesEmitter.getEmissionsFor.callCount).to.eq(1);
expect(db.query.callCount).to.eq(3);
expect(db.query.args[2][0]).to.eq('medic-client/tasks_by_contact');
expect(db.query.args[2][0]).to.eq('medic-offline-tasks/tasks_by_contact');
expect(db.query.args[2][1]).to.not.have.property('keys');
});

Expand Down
6 changes: 5 additions & 1 deletion webapp/src/js/bootstrapper/offline-ddocs/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const contactsByFreetext = require('./medic-offline-freetext');
const tasks = require('./medic-offline-tasks');

const getRev = async (db, id) => db
.get(id)
Expand All @@ -15,4 +16,7 @@ const initDdoc = async (db, ddoc) => db.put({
_rev: await getRev(db, ddoc._id),
});

module.exports.init = async (db) => initDdoc(db, contactsByFreetext);
module.exports.init = async (db) => {
await initDdoc(db, contactsByFreetext);
await initDdoc(db, tasks);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const tasksByContact = require('./tasks_by_contact');

const packageView = ({ map }) => ({ map: map.toString() });

module.exports = {
_id: '_design/medic-offline-tasks',
views: {
tasks_by_contact: packageView(tasksByContact),
}
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
function(doc) {
module.exports.map = function(doc) {
if (doc.type === 'task') {
var isTerminalState = ['Cancelled', 'Completed', 'Failed'].indexOf(doc.state) >= 0;
var owner = (doc.owner || '_unassigned');
const isTerminalState = ['Cancelled', 'Completed', 'Failed'].includes(doc.state);
const owner = (doc.owner || '_unassigned');

if (!isTerminalState) {
emit('owner-' + owner);
Expand All @@ -13,4 +13,4 @@ function(doc) {

emit(['owner', 'all', owner], { state: doc.state });
}
}
};
Loading