From 3cb513d07f98997d28b98cc9d32b5f18b51a8e00 Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Tue, 31 Mar 2026 18:02:17 +0530 Subject: [PATCH 01/26] feat: remove obsolete lineage view and refactor fetching to use all_docs --- .../services/lineage-model-generator.spec.js | 77 +- .../views/docs_by_id_lineage/map.js | 20 - .../cht-datasource/src/local/libs/lineage.ts | 26 +- .../test/local/libs/doc.spec.ts | 12 +- .../test/local/libs/lineage.spec.ts | 22 +- shared-libs/lineage/src/hydration.js | 30 +- shared-libs/lineage/test.log | 837 ++++++++++++++++++ shared-libs/lineage/test/hydration.spec.js | 19 +- tests/integration/api/server.spec.js | 6 +- .../lineage-model-generator.service.spec.ts | 119 ++- .../unit/views/docs_by_id_lineage.spec.js | 200 ----- 11 files changed, 1013 insertions(+), 355 deletions(-) delete mode 100644 ddocs/medic-db/medic-client/views/docs_by_id_lineage/map.js create mode 100644 shared-libs/lineage/test.log delete mode 100644 webapp/tests/mocha/unit/views/docs_by_id_lineage.spec.js diff --git a/admin/tests/unit/services/lineage-model-generator.spec.js b/admin/tests/unit/services/lineage-model-generator.spec.js index dd308a15294..e91c6a70fc2 100644 --- a/admin/tests/unit/services/lineage-model-generator.spec.js +++ b/admin/tests/unit/services/lineage-model-generator.spec.js @@ -5,14 +5,16 @@ describe('LineageModelGenerator service', () => { let service; let dbQuery; let dbAllDocs; + let dbGet; beforeEach(() => { module('adminApp'); module($provide => { dbQuery = sinon.stub(); dbAllDocs = sinon.stub(); + dbGet = sinon.stub(); $provide.value('$q', Q); // bypass $q so we don't have to digest - $provide.factory('DB', KarmaUtils.mockDB({ query: dbQuery, allDocs: dbAllDocs })); + $provide.factory('DB', KarmaUtils.mockDB({ query: dbQuery, allDocs: dbAllDocs, get: dbGet })); }); inject(_LineageModelGenerator_ => service = _LineageModelGenerator_); }); @@ -20,7 +22,7 @@ describe('LineageModelGenerator service', () => { describe('contact', () => { it('handles not found', done => { - dbQuery.returns(Promise.resolve({ rows: [] })); + dbGet.returns(Promise.reject({ status: 404 })); service.contact('a') .then(() => { done(new Error('expected error to be thrown')); @@ -34,9 +36,7 @@ describe('LineageModelGenerator service', () => { it('handles no lineage', () => { const contact = { _id: 'a', _rev: '1' }; - dbQuery.returns(Promise.resolve({ rows: [ - { doc: contact } - ] })); + dbGet.returns(Promise.resolve(contact)); return service.contact('a').then(model => { chai.expect(model._id).to.equal('a'); chai.expect(model.doc).to.deep.equal(contact); @@ -44,22 +44,18 @@ describe('LineageModelGenerator service', () => { }); it('binds lineage', () => { - const contact = { _id: 'a', _rev: '1' }; - const parent = { _id: 'b', _rev: '1' }; + const contact = { _id: 'a', _rev: '1', parent: { _id: 'b' } }; + const parent = { _id: 'b', _rev: '1', parent: { _id: 'c' } }; const grandparent = { _id: 'c', _rev: '1' }; - dbQuery.returns(Promise.resolve({ rows: [ - { doc: contact }, + dbGet.withArgs('a').returns(Promise.resolve(contact)); + dbAllDocs.withArgs({ keys: ['b', 'c'], include_docs: true }).returns(Promise.resolve({ rows: [ { doc: parent }, { doc: grandparent } ] })); return service.contact('a').then(model => { - chai.expect(dbQuery.callCount).to.equal(1); - chai.expect(dbQuery.args[0][0]).to.equal('medic-client/docs_by_id_lineage'); - chai.expect(dbQuery.args[0][1]).to.deep.equal({ - startkey: [ 'a' ], - endkey: [ 'a', {} ], - include_docs: true - }); + chai.expect(dbGet.callCount).to.equal(1); + chai.expect(dbAllDocs.callCount).to.equal(1); + chai.expect(dbAllDocs.args[0][0].keys).to.deep.equal(['b', 'c']); chai.expect(model._id).to.equal('a'); chai.expect(model.doc).to.deep.equal(contact); chai.expect(model.lineage).to.deep.equal([ parent, grandparent ]); @@ -67,17 +63,17 @@ describe('LineageModelGenerator service', () => { }); it('binds contacts', () => { - const contact = { _id: 'a', _rev: '1', contact: { _id: 'd' } }; + const contact = { _id: 'a', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'b' } }; const contactsContact = { _id: 'd', name: 'dave' }; - const parent = { _id: 'b', _rev: '1', contact: { _id: 'e' } }; + const parent = { _id: 'b', _rev: '1', contact: { _id: 'e' }, parent: { _id: 'c' } }; const parentsContact = { _id: 'e', name: 'eliza' }; const grandparent = { _id: 'c', _rev: '1' }; - dbQuery.returns(Promise.resolve({ rows: [ - { doc: contact }, + dbGet.returns(Promise.resolve(contact)); + dbAllDocs.withArgs({ keys: ['b', 'c'], include_docs: true }).returns(Promise.resolve({ rows: [ { doc: parent }, { doc: grandparent } ] })); - dbAllDocs.returns(Promise.resolve({ rows: [ + dbAllDocs.withArgs({ keys: sinon.match.array.deepEquals(['d', 'e']), include_docs: true }).returns(Promise.resolve({ rows: [ { doc: contactsContact }, { doc: parentsContact } ] })); @@ -89,22 +85,24 @@ describe('LineageModelGenerator service', () => { }); it('hydrates lineage contacts - #3812', () => { - const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' } }; - const parent = { _id: 'b', _rev: '1', contact: { _id: 'd' } }; + const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' }, parent: { _id: 'b' } }; + const parent = { _id: 'b', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'c' } }; const grandparent = { _id: 'c', _rev: '1', contact: { _id: 'e' } }; const parentContact = { _id: 'd', name: 'donny' }; const grandparentContact = { _id: 'e', name: 'erica' }; - dbQuery.returns(Promise.resolve({ rows: [ - { doc: contact }, + const xContact = { _id: 'x', name: 'xavier' }; + dbGet.returns(Promise.resolve(contact)); + dbAllDocs.withArgs({ keys: ['b', 'c'], include_docs: true }).returns(Promise.resolve({ rows: [ { doc: parent }, { doc: grandparent } ] })); - dbAllDocs.returns(Promise.resolve({ rows: [ + dbAllDocs.withArgs({ keys: sinon.match.array.contains('x', 'd', 'e'), include_docs: true }).returns(Promise.resolve({ rows: [ + { doc: xContact }, { doc: parentContact }, { doc: grandparentContact } ] })); return service.contact('a').then(model => { - chai.expect(dbAllDocs.callCount).to.equal(1); + chai.expect(dbAllDocs.callCount).to.equal(2); chai.expect(dbAllDocs.args[0][0]).to.deep.equal({ keys: [ 'x', 'd', 'e' ], include_docs: true @@ -116,7 +114,7 @@ describe('LineageModelGenerator service', () => { it('merges lineage when merge passed', () => { const contact = { _id: 'a', name: '1', parent: { _id: 'b', parent: { _id: 'c' } } }; - const parent = { _id: 'b', name: '2' }; + const parent = { _id: 'b', name: '2', parent: { _id: 'c' } }; const grandparent = { _id: 'c', name: '3' }; const expected = { _id: 'a', @@ -147,8 +145,8 @@ describe('LineageModelGenerator service', () => { } ] }; - dbQuery.returns(Promise.resolve({ rows: [ - { doc: contact }, + dbGet.returns(Promise.resolve(contact)); + dbAllDocs.withArgs({ keys: ['b', 'c'], include_docs: true }).returns(Promise.resolve({ rows: [ { doc: parent }, { doc: grandparent } ] })); @@ -160,8 +158,9 @@ describe('LineageModelGenerator service', () => { it('should merge lineage with undefined members', () => { const contact = { _id: 'a', name: '1', parent: { _id: 'b', parent: { _id: 'c', parent: { _id: 'd' } } } }; const parent = { _id: 'b', name: '2', parent: { _id: 'c', parent: { _id: 'd' } } }; - dbQuery.resolves({ rows: - [{ doc: contact, key: ['a', 0] }, { doc: parent, key: ['a', 1] }, { key: ['a', 2] }, { key: ['a', 3] }] + dbGet.resolves(contact); + dbAllDocs.resolves({ rows: + [{ doc: parent, id: 'b' }, { id: 'c' }, { id: 'd' }] }); const expected = { _id: 'a', @@ -180,11 +179,11 @@ describe('LineageModelGenerator service', () => { it('should merge lineage with undefined members v2', () => { const contact = { _id: 'a', name: '1', parent: { _id: 'b', parent: { _id: 'c', parent: { _id: 'd' } } } }; const parent = { _id: 'b', name: '2', parent: { _id: 'c', parent: { _id: 'd' } } }; - dbQuery.resolves({ rows: [ - { doc: contact, key: ['a', 0] }, - { doc: parent, key: ['a', 1] }, - { key: ['a', 2] }, - { key: ['a', 3], doc: { _id: 'd', name: '4' } } + dbGet.resolves(contact); + dbAllDocs.resolves({ rows: [ + { doc: parent, id: 'b' }, + { id: 'c' }, + { id: 'd', doc: { _id: 'd', name: '4' } } ] }); const expected = { _id: 'a', @@ -229,8 +228,8 @@ describe('LineageModelGenerator service', () => { } ] }; - dbQuery.returns(Promise.resolve({ rows: [ - { doc: contact }, + dbGet.returns(Promise.resolve(contact)); + dbAllDocs.returns(Promise.resolve({ rows: [ { doc: parent }, { doc: grandparent } ] })); diff --git a/ddocs/medic-db/medic-client/views/docs_by_id_lineage/map.js b/ddocs/medic-db/medic-client/views/docs_by_id_lineage/map.js deleted file mode 100644 index c87b7d182f8..00000000000 --- a/ddocs/medic-db/medic-client/views/docs_by_id_lineage/map.js +++ /dev/null @@ -1,20 +0,0 @@ -function(doc) { - - var emitLineage = function(contact, depth) { - while (contact && contact._id) { - emit([ doc._id, depth++ ], { _id: contact._id }); - contact = contact.parent; - } - }; - - var types = [ 'contact', 'district_hospital', 'health_center', 'clinic', 'person' ]; - - if (types.indexOf(doc.type) !== -1) { - // contact - emitLineage(doc, 0); - } else if (doc.type === 'data_record' && doc.form) { - // report - emit([ doc._id, 0 ]); - emitLineage(doc.contact, 1); - } -} diff --git a/shared-libs/cht-datasource/src/local/libs/lineage.ts b/shared-libs/cht-datasource/src/local/libs/lineage.ts index 5c53ab7e326..a5f086bc93a 100644 --- a/shared-libs/cht-datasource/src/local/libs/lineage.ts +++ b/shared-libs/cht-datasource/src/local/libs/lineage.ts @@ -33,8 +33,30 @@ import { isEqual } from 'lodash'; * @internal */ export const getLineageDocsById = (medicDb: PouchDB.Database): (id: string) => Promise[]> => { - const fn = queryDocsByRange(medicDb, 'medic-client/docs_by_id_lineage'); - return (id: string) => fn([id], [id, {}]); + const getMedicDocsById = getDocsByIds(medicDb); + return async (id: string) => { + try { + const doc = await medicDb.get(id); + const parentIds: string[] = []; + let current = doc.type === 'data_record' ? doc.contact : doc.parent; + while (isRecord(current)) { + if (typeof current._id === 'string') { + parentIds.push(current._id); + } + current = current.parent; + } + if (parentIds.length === 0) { + return [doc]; + } + const ancestors = await getMedicDocsById(parentIds); + return [doc, ...ancestors]; + } catch (err: any) { + if (err.status === 404) { + return []; + } + throw err; + } + }; }; /** @internal */ diff --git a/shared-libs/cht-datasource/test/local/libs/doc.spec.ts b/shared-libs/cht-datasource/test/local/libs/doc.spec.ts index 44fb2385280..db7e37f0919 100644 --- a/shared-libs/cht-datasource/test/local/libs/doc.spec.ts +++ b/shared-libs/cht-datasource/test/local/libs/doc.spec.ts @@ -245,11 +245,11 @@ describe('local doc lib', () => { }); isDoc.returns(true); - const result = await queryDocsByRange(db, 'medic-client/docs_by_id_lineage')(doc0._id, doc1._id); + const result = await queryDocsByRange(db, 'medic-client/contacts_by_type')(doc0._id, doc1._id); expect(result).to.deep.equal([doc0, doc1, doc2]); - expect(dbQuery.calledOnceWithExactly('medic-client/docs_by_id_lineage', { + expect(dbQuery.calledOnceWithExactly('medic-client/contacts_by_type', { include_docs: true, startkey: doc0._id, endkey: doc1._id, @@ -271,10 +271,10 @@ describe('local doc lib', () => { }); isDoc.returns(true); - const result = await queryDocsByRange(db, 'medic-client/docs_by_id_lineage')(doc0._id, doc2._id, limit, skip); + const result = await queryDocsByRange(db, 'medic-client/contacts_by_type')(doc0._id, doc2._id, limit, skip); expect(result).to.deep.equal([doc0, null, doc2]); - expect(dbQuery.calledOnceWithExactly('medic-client/docs_by_id_lineage', { + expect(dbQuery.calledOnceWithExactly('medic-client/contacts_by_type', { startkey: doc0._id, endkey: doc2._id, include_docs: true, @@ -291,10 +291,10 @@ describe('local doc lib', () => { }); isDoc.returns(false); - const result = await queryDocsByRange(db, 'medic-client/docs_by_id_lineage')(doc0._id, doc0._id, limit, skip); + const result = await queryDocsByRange(db, 'medic-client/contacts_by_type')(doc0._id, doc0._id, limit, skip); expect(result).to.deep.equal([null]); - expect(dbQuery.calledOnceWithExactly('medic-client/docs_by_id_lineage', { + expect(dbQuery.calledOnceWithExactly('medic-client/contacts_by_type', { startkey: doc0._id, endkey: doc0._id, include_docs: true, diff --git a/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts b/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts index 3da0b350772..098c846a5f4 100644 --- a/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts +++ b/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts @@ -30,18 +30,26 @@ describe('local lineage lib', () => { it('getLineageDocsById', async () => { const uuid = '123'; - const queryFn = sinon.stub().resolves([]); - const queryDocsByRange = sinon - .stub(LocalDoc, 'queryDocsByRange') - .returns(queryFn); - const medicDb = { hello: 'world' } as unknown as PouchDB.Database; + const doc = { _id: uuid, parent: { _id: 'parent1' } }; + const parentDoc = { _id: 'parent1' }; + medicGet.resolves(doc); + const getDocsByIdsInner = sinon.stub().resolves([parentDoc]); + const getDocsByIdsOuter = sinon.stub(LocalDoc, 'getDocsByIds').returns(getDocsByIdsInner); const fn = Lineage.getLineageDocsById(medicDb); const result = await fn(uuid); + expect(result).to.deep.equal([doc, parentDoc]); + expect(medicGet.calledOnceWithExactly(uuid)).to.be.true; + expect(getDocsByIdsOuter.calledOnceWithExactly(medicDb)).to.be.true; + expect(getDocsByIdsInner.calledOnceWithExactly(['parent1'])).to.be.true; + }); + + it('getLineageDocsById handles 404', async () => { + medicGet.rejects({ status: 404 }); + const fn = Lineage.getLineageDocsById(medicDb); + const result = await fn('missing'); expect(result).to.deep.equal([]); - expect(queryDocsByRange.calledOnceWithExactly(medicDb, 'medic-client/docs_by_id_lineage')).to.be.true; - expect(queryFn.calledOnceWithExactly([uuid], [uuid, {}])).to.be.true; }); describe('getPrimaryContactIds', () => { diff --git a/shared-libs/lineage/src/hydration.js b/shared-libs/lineage/src/hydration.js index 33ee9c0547f..f56a11824d6 100644 --- a/shared-libs/lineage/src/hydration.js +++ b/shared-libs/lineage/src/hydration.js @@ -229,16 +229,28 @@ module.exports = function(Promise, DB) { }; const fetchLineageById = function(id) { - const options = { - startkey: [id], - endkey: [id, {}], - include_docs: true - }; - return DB.query('medic-client/docs_by_id_lineage', options) - .then(function(result) { - return result.rows.map(function(row) { - return row.doc; + return DB.get(id) + .then(function(doc) { + const startParent = utils.isReport(doc) ? doc.contact : doc.parent; + const parentIds = extractParentIds(startParent); + if (!parentIds.length) { + return [doc]; + } + return fetchDocs(parentIds).then(function(ancestors) { + const docsMap = new Map(); + ancestors.forEach(function(a) { docsMap.set(a._id, a); }); + const result = [doc]; + parentIds.forEach(function(parentId) { + result.push(docsMap.get(parentId) || null); + }); + return result; }); + }) + .catch(function(err) { + if (err.status === 404) { + return []; + } + throw err; }); }; diff --git a/shared-libs/lineage/test.log b/shared-libs/lineage/test.log new file mode 100644 index 00000000000..ebf9d607dd7 --- /dev/null +++ b/shared-libs/lineage/test.log @@ -0,0 +1,837 @@ + +> @medic/lineage@1.0.0 test +> nyc --nycrcPath='../nyc.config.js' mocha ./test --require test/setup.js + + + + Lineage + fetchLineageById + ✔ queries db with correct parameters + fetchContacts + ✔ fetches contacts with correct parameters + ✔ does not fetch contacts that it has already got via lineage + fillContactsInDocs + ✔ skips null docs in the array + ✔ populates the contact field for relevant docs + fillParentsInDocs + ✔ populates parent fields throughout lineage + ✔ correctly populates parent fields for reports + fetchHydratedDoc + ✔ supports callback as second argument + ✔ passes error to callback + ✔ throws when lineage is empty and throwWhenMissingLineage is true + fetchHydratedDocs + ✔ returns empty array for empty docIds + ✔ throws non-404 errors for single doc + hydrateDocs + ✔ works on empty array + ✔ handles reports without contact id + ✔ works on docs without contacts or parents + + Lineage + fetchLineageById + ✔ returns correct lineage + fetchLineageByIds + ✔ returns correct lineages + fetchContacts + ✔ clones any reused contacts + fetchHydratedDoc + ✔ returns errors from query + ✔ returns unmodified doc when there is no lineage and no contact + 1) handles doc with unknown parent by leaving just the stub + 2) handles doc with unknown contact by leaving just the stub + ✔ handles missing contacts + 3) attaches the full lineage for reports with patient_id + 4) attaches patient lineage when using patient_uuid field + 5) attaches patient lineage when using patient_id field that contains a uuid + 6) attaches the full lineage for reports with place_id + 7) attaches the full lineage for reports with place_id containing a uuid + 8) attaches the full lineage for reports with place_id and patient_id + 9) should work when patient is not found + 10) should work when place is not found + ✔ attaches the contacts + 11) attaches re-used contacts, minify handles the circular references + ✔ minifying the result returns the starting document for a report + ✔ minifying the result returns the starting document for a place + ✔ works for SMS reports + ✔ handles doc with empty-object parent by removing it + ✔ should hydrate linked docs from contacts + ✔ should not hydrate linked docs from reports + hydrateDocs + ✔ binds contacts and parents + ✔ ignores db-fetch errors + ✔ minifying the result returns the starting documents + ✔ does not return a report with circular references + ✔ handles person with circular reference ids + ✔ handles place with circular reference ids + ✔ handles report with circular reference ids + ✔ should not recurse more than needed + ✔ processing a doc with itself as a parent does not error out + ✔ processing a doc with itself as a grandparent does not error out + ✔ processing a doc with itself as a grandparent referenced through intermediate docs does not error out + ✔ should hydrate linked docs from contacts, but not from reports + ✔ should re-use docs from provided list + fetchHydratedDocs + ✔ should crash with bad param + ✔ should work with one contact + 12) should work with one report + ✔ should work with one non-existent doc + ✔ should work with multiple docs + ✔ should hydrate linked docs for contacts, but not for reports + + Minify + minifyLineage + ✔ returns falsy parent as-is + ✔ returns parent without _id as-is + ✔ removes everything except id + minify + ✔ handles null argument + ✔ minifies the parent + ✔ minifies the contact and lineage + ✔ removes the patient + ✔ removes the place + ✔ errors out on potential infinite recursion + ✔ should minify linked docs for contacts + ✔ should not minify linked docs for reports + ✔ should only minify linked docs if valid + + + 58 passing (219ms) + 12 failing + + 1) Lineage + fetchHydratedDoc + handles doc with unknown parent by leaving just the stub: + + AssertionError: expected { _id: 'stub_parents', …(3) } to deeply equal { _id: 'stub_parents', …(3) } + + expected - actual + + "contact": { + "_id": "something" + } + "parent": { + - "_id": "dummyDoc" + - "name": "district" + + "_id": "something_else" + + "parent": { + + "_id": "dummyDoc" + + "name": "district" + + } + } + "type": "clinic" + } + + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:618:55 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + 2) Lineage + fetchHydratedDoc + handles doc with unknown contact by leaving just the stub: + + AssertionError: expected { Object (_id, form, ...) } to deeply equal { _id: 'stub_contacts', …(3) } + + expected - actual + + "contact": { + "_id": "something" + "parent": { + "_id": "dummyDoc" + + "name": "district" + } + } + "form": {} + "type": "data_record" + + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:632:55 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + 3) Lineage + fetchHydratedDoc + attaches the full lineage for reports with patient_id: + + AssertionError: Expected "name" field to be defined at path "/contact/parent". + + expected - actual + + { + - "_id": "report" + - "_rev": "1-84140bd0f8e0997eaa7060226eebbf17" + "contact": { + - "_id": "report_contact" + - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" + "name": "report_contact_name" + "parent": { + - "_id": "report_parent" + + "contact": { + + "name": "report_parentContact_name" + + "phone": "+123" + + } + + "name": "report_parent_name" + "parent": { + - "_id": "report_grandparent" + + "contact": { + + "name": "report_grandparentContact_name" + + "phone": "+456" + + } + + "name": "report_grandparent_name" + } + } + - "reported_date": "5" + - "type": "person" + } + - "fields": { + - "patient_id": "12345" + - } + "form": "A" + + "parent": [undefined] + "patient": { + - "_id": "report_patient" + - "_rev": "1-85cf4d70d5f13efc2dd9e843c036db6e" + "name": "patient_name" + "parent": { + - "_id": "report_parent" + - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" + "contact": { + - "_id": "report_parentContact" + - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" + "name": "report_parentContact_name" + "phone": "+123" + - "reported_date": "5" + - "type": "person" + } + "name": "report_parent_name" + - "parent": { + - "_id": "report_grandparent" + - "_rev": "1-46d806688da3e855379b2c8084712074" + - "contact": { + - "_id": "report_grandparentContact" + - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" + - "name": "report_grandparentContact_name" + - "phone": "+456" + - "reported_date": "5" + - "type": "person" + - } + - "name": "report_grandparent_name" + - } + } + - "patient_id": "12345" + - "reported_date": "5" + - "type": "person" + } + - "type": "data_record" + } + + at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:653:16 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + 4) Lineage + fetchHydratedDoc + attaches patient lineage when using patient_uuid field: + + AssertionError: Expected "name" field to be defined at path "/contact/parent". + + expected - actual + + { + - "_id": "report2" + - "_rev": "1-1e0bd6cbfb59bbc332bda6c6414cd297" + "contact": { + - "_id": "report_contact" + - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" + "name": "report_contact_name" + "parent": { + - "_id": "report_parent" + + "contact": { + + "phone": "+123" + + } + + "name": "report_parent_name" + "parent": { + - "_id": "report_grandparent" + + "contact": { + + "phone": "+456" + + } + + "name": "report_grandparent_name" + } + } + - "reported_date": "5" + - "type": "person" + } + - "fields": { + - "patient_id": "" + - "patient_uuid": "report_patient" + - } + "form": "A" + + "parent": [undefined] + "patient": { + - "_id": "report_patient" + - "_rev": "1-85cf4d70d5f13efc2dd9e843c036db6e" + "name": "patient_name" + "parent": { + - "_id": "report_parent" + - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" + "contact": { + - "_id": "report_parentContact" + - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" + "name": "report_parentContact_name" + - "phone": "+123" + - "reported_date": "5" + - "type": "person" + } + "name": "report_parent_name" + - "parent": { + - "_id": "report_grandparent" + - "_rev": "1-46d806688da3e855379b2c8084712074" + - "contact": { + - "_id": "report_grandparentContact" + - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" + - "name": "report_grandparentContact_name" + - "phone": "+456" + - "reported_date": "5" + - "type": "person" + - } + - "name": "report_grandparent_name" + - } + } + - "patient_id": "12345" + - "reported_date": "5" + - "type": "person" + } + - "type": "data_record" + } + + at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:690:16 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + 5) Lineage + fetchHydratedDoc + attaches patient lineage when using patient_id field that contains a uuid: + + AssertionError: Expected "name" field to be defined at path "/contact/parent". + + expected - actual + + { + - "_id": "report3" + - "_rev": "1-951e95a2f59b1bd766f093dd764d2272" + "contact": { + - "_id": "report_contact" + - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" + "name": "report_contact_name" + "parent": { + - "_id": "report_parent" + + "contact": { + + "phone": "+123" + + } + + "name": "report_parent_name" + "parent": { + - "_id": "report_grandparent" + + "contact": { + + "phone": "+456" + + } + + "name": "report_grandparent_name" + } + } + - "reported_date": "5" + - "type": "person" + } + - "fields": { + - "patient_id": "report_patient" + - } + "form": "A" + + "parent": [undefined] + "patient": { + - "_id": "report_patient" + - "_rev": "1-85cf4d70d5f13efc2dd9e843c036db6e" + "name": "patient_name" + "parent": { + - "_id": "report_parent" + - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" + "contact": { + - "_id": "report_parentContact" + - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" + "name": "report_parentContact_name" + - "phone": "+123" + - "reported_date": "5" + - "type": "person" + } + "name": "report_parent_name" + - "parent": { + - "_id": "report_grandparent" + - "_rev": "1-46d806688da3e855379b2c8084712074" + - "contact": { + - "_id": "report_grandparentContact" + - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" + - "name": "report_grandparentContact_name" + - "phone": "+456" + - "reported_date": "5" + - "type": "person" + - } + - "name": "report_grandparent_name" + - } + } + - "patient_id": "12345" + - "reported_date": "5" + - "type": "person" + } + - "type": "data_record" + } + + at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:720:16 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + 6) Lineage + fetchHydratedDoc + attaches the full lineage for reports with place_id: + + AssertionError: Expected "name" field to be defined at path "/contact/parent". + + expected - actual + + { + - "_id": "report_with_place" + - "_rev": "1-0af5527f2ad728975619eed655faa8d7" + "contact": { + - "_id": "report_contact" + - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" + "name": "report_contact_name" + "parent": { + - "_id": "report_parent" + + "contact": { + + "name": "report_parentContact_name" + + "phone": "+123" + + } + + "name": "report_parent_name" + "parent": { + - "_id": "report_grandparent" + + "contact": { + + "name": "report_grandparentContact_name" + + "phone": "+456" + + } + + "name": "report_grandparent_name" + } + } + - "reported_date": "5" + - "type": "person" + } + - "fields": { + - "place_id": "54321" + - } + "form": "A" + + "parent": [undefined] + "place": { + - "_id": "report_place" + - "_rev": "1-d2e1eab40f2e58eb1650c7ed7970210b" + "name": "place_name" + "parent": { + - "_id": "report_parent" + - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" + "contact": { + - "_id": "report_parentContact" + - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" + "name": "report_parentContact_name" + "phone": "+123" + - "reported_date": "5" + - "type": "person" + } + "name": "report_parent_name" + - "parent": { + - "_id": "report_grandparent" + - "_rev": "1-46d806688da3e855379b2c8084712074" + - "contact": { + - "_id": "report_grandparentContact" + - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" + - "name": "report_grandparentContact_name" + - "phone": "+456" + - "reported_date": "5" + - "type": "person" + - } + - "name": "report_grandparent_name" + - } + } + - "place_id": "54321" + - "reported_date": "5" + - "type": "clinic" + } + - "type": "data_record" + } + + at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:750:16 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + 7) Lineage + fetchHydratedDoc + attaches the full lineage for reports with place_id containing a uuid: + + AssertionError: Expected "name" field to be defined at path "/contact/parent". + + expected - actual + + { + - "_id": "report_with_place_uuid" + - "_rev": "1-dbeef6a38bfc6a3ed5c21781d6c6338c" + "contact": { + - "_id": "report_contact" + - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" + "name": "report_contact_name" + "parent": { + - "_id": "report_parent" + + "contact": { + + "name": "report_parentContact_name" + + "phone": "+123" + + } + + "name": "report_parent_name" + "parent": { + - "_id": "report_grandparent" + + "contact": { + + "name": "report_grandparentContact_name" + + "phone": "+456" + + } + + "name": "report_grandparent_name" + } + } + - "reported_date": "5" + - "type": "person" + } + - "fields": { + - "place_id": "report_place" + - } + "form": "A" + + "parent": [undefined] + "place": { + - "_id": "report_place" + - "_rev": "1-d2e1eab40f2e58eb1650c7ed7970210b" + "name": "place_name" + "parent": { + - "_id": "report_parent" + - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" + "contact": { + - "_id": "report_parentContact" + - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" + "name": "report_parentContact_name" + "phone": "+123" + - "reported_date": "5" + - "type": "person" + } + "name": "report_parent_name" + - "parent": { + - "_id": "report_grandparent" + - "_rev": "1-46d806688da3e855379b2c8084712074" + - "contact": { + - "_id": "report_grandparentContact" + - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" + - "name": "report_grandparentContact_name" + - "phone": "+456" + - "reported_date": "5" + - "type": "person" + - } + - "name": "report_grandparent_name" + - } + } + - "place_id": "54321" + - "reported_date": "5" + - "type": "clinic" + } + - "type": "data_record" + } + + at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:787:16 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + 8) Lineage + fetchHydratedDoc + attaches the full lineage for reports with place_id and patient_id: + + AssertionError: Expected "name" field to be defined at path "/contact/parent". + + expected - actual + + { + - "_id": "report_with_place_and_patient" + - "_rev": "1-d75dd101052194a341462effa12f97a4" + "contact": { + - "_id": "report_contact" + - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" + "name": "report_contact_name" + "parent": { + - "_id": "report_parent" + - "parent": { + - "_id": "report_grandparent" + - } + - } + - "reported_date": "5" + - "type": "person" + - } + - "fields": { + - "patient_id": "12345" + - "place_id": "54321" + - } + - "form": "A" + - "patient": { + - "_id": "report_patient" + - "_rev": "1-85cf4d70d5f13efc2dd9e843c036db6e" + - "name": "patient_name" + - "parent": { + - "_id": "report_parent" + - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" + "contact": { + - "_id": "report_parentContact" + - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" + "name": "report_parentContact_name" + "phone": "+123" + - "reported_date": "5" + - "type": "person" + } + "name": "report_parent_name" + "parent": { + - "_id": "report_grandparent" + - "_rev": "1-46d806688da3e855379b2c8084712074" + "contact": { + - "_id": "report_grandparentContact" + - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" + "name": "report_grandparentContact_name" + "phone": "+456" + - "reported_date": "5" + - "type": "person" + } + "name": "report_grandparent_name" + } + } + - "patient_id": "12345" + - "reported_date": "5" + - "type": "person" + } + + "form": "A" + + "parent": [undefined] + + "patient": { + + "name": "patient_name" + + "parent": { + + "contact": { + + "name": "report_parentContact_name" + + } + + "name": "report_parent_name" + + } + + } + "place": { + - "_id": "report_place" + - "_rev": "1-d2e1eab40f2e58eb1650c7ed7970210b" + "name": "place_name" + "parent": { + - "_id": "report_parent" + - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" + "contact": { + - "_id": "report_parentContact" + - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" + "name": "report_parentContact_name" + "phone": "+123" + - "reported_date": "5" + - "type": "person" + } + "name": "report_parent_name" + - "parent": { + - "_id": "report_grandparent" + - "_rev": "1-46d806688da3e855379b2c8084712074" + - "contact": { + - "_id": "report_grandparentContact" + - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" + - "name": "report_grandparentContact_name" + - "phone": "+456" + - "reported_date": "5" + - "type": "person" + - } + - "name": "report_grandparent_name" + - } + } + - "place_id": "54321" + - "reported_date": "5" + - "type": "clinic" + } + - "type": "data_record" + } + + at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:823:16 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + 9) Lineage + fetchHydratedDoc + should work when patient is not found: + + AssertionError: Expected "name" field to be defined at path "/contact/parent". + + expected - actual + + { + - "_id": "report4" + - "_rev": "1-b4cd6494e2282cfcda8203194f6733cb" + "contact": { + - "_id": "report_contact" + - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" + "name": "report_contact_name" + "parent": { + - "_id": "report_parent" + + "contact": { + + "phone": "+123" + + } + + "name": "report_parent_name" + "parent": { + - "_id": "report_grandparent" + + "contact": { + + "phone": "+456" + + } + + "name": "report_grandparent_name" + } + } + - "reported_date": "5" + - "type": "person" + } + - "fields": { + - "patient_id": "something" + - } + "form": "A" + - "type": "data_record" + + "parent": [undefined] + } + + at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:870:16 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + 10) Lineage + fetchHydratedDoc + should work when place is not found: + + AssertionError: Expected "name" field to be defined at path "/contact/parent". + + expected - actual + + { + - "_id": "report5" + - "_rev": "1-2f521446a981bfec946bb3fb1e65942c" + "contact": { + - "_id": "report_contact" + - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" + "name": "report_contact_name" + "parent": { + - "_id": "report_parent" + + "contact": { + + "phone": "+123" + + } + + "name": "report_parent_name" + "parent": { + - "_id": "report_grandparent" + + "contact": { + + "phone": "+456" + + } + + "name": "report_grandparent_name" + } + } + - "reported_date": "5" + - "type": "person" + } + - "fields": { + - "place_id": "something" + - } + "form": "A" + - "type": "data_record" + + "parent": [undefined] + } + + at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:892:16 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + 11) Lineage + fetchHydratedDoc + attaches re-used contacts, minify handles the circular references: + TypeError: Cannot read properties of undefined (reading 'hydrated') + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:931:46 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + 12) Lineage + fetchHydratedDocs + should work with one report: + + AssertionError: Expected "name" field to be defined at path "/contact/parent". + + expected - actual + + { + "_id": "report" + - "_rev": "1-84140bd0f8e0997eaa7060226eebbf17" + "contact": { + - "_id": "report_contact" + - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" + "name": "report_contact_name" + "parent": { + - "_id": "report_parent" + + "contact": { + + "name": "report_parentContact_name" + + } + + "name": "report_parent_name" + "parent": { + - "_id": "report_grandparent" + + "contact": { + + "name": "report_grandparentContact_name" + + } + + "name": "report_grandparent_name" + } + } + - "reported_date": "5" + - "type": "person" + } + - "fields": { + - "patient_id": "12345" + - } + - "form": "A" + "patient": { + - "_id": "report_patient" + - "_rev": "1-85cf4d70d5f13efc2dd9e843c036db6e" + "name": "patient_name" + "parent": { + - "_id": "report_parent" + - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" + "contact": { + - "_id": "report_parentContact" + - "_rev": "3-0253eb97360222140f0cde806200f076" + "name": "report_parentContact_name" + - "phone": "+123" + - "reported_date": "5" + - "type": "person" + } + "name": "report_parent_name" + "parent": { + - "_id": "report_grandparent" + - "_rev": "1-46d806688da3e855379b2c8084712074" + "contact": { + - "_id": "report_grandparentContact" + - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" + "name": "report_grandparentContact_name" + - "phone": "+456" + - "reported_date": "5" + - "type": "person" + } + "name": "report_grandparent_name" + } + } + - "patient_id": "12345" + - "reported_date": "5" + - "type": "person" + } + - "type": "data_record" + } + + at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) + at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:1647:16 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + + + + +=============================== Coverage summary =============================== +Statements : 99.08% ( 326/329 ) +Branches : 96.02% ( 145/151 ) +Functions : 100% ( 93/93 ) +Lines : 99.02% ( 306/309 ) +================================================================================ +npm error Lifecycle script `test` failed with error: +npm error code 12 +npm error path /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage +npm error workspace @medic/lineage@1.0.0 +npm error location /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage +npm error command failed +npm error command sh -c nyc --nycrcPath='../nyc.config.js' mocha ./test --require test/setup.js diff --git a/shared-libs/lineage/test/hydration.spec.js b/shared-libs/lineage/test/hydration.spec.js index df873f98f9a..0a0caab02dd 100644 --- a/shared-libs/lineage/test/hydration.spec.js +++ b/shared-libs/lineage/test/hydration.spec.js @@ -24,15 +24,15 @@ describe('Lineage', function() { describe('fetchLineageById', function() { it('queries db with correct parameters', function() { - query.resolves({ rows: [] }); + get.resolves({ _id: 'banana', parent: { _id: 'apple' } }); + allDocs.resolves({ rows: [{ doc: { _id: 'apple' } }] }); const id = 'banana'; return lineage.fetchLineageById(id).then(() => { - chai.expect(query.callCount).to.equal(1); - chai.expect(query.getCall(0).args[0]).to.equal('medic-client/docs_by_id_lineage'); - chai.expect(query.getCall(0).args[1].startkey).to.deep.equal([ id ]); - chai.expect(query.getCall(0).args[1].endkey).to.deep.equal([ id, {} ]); - chai.expect(query.getCall(0).args[1].include_docs).to.deep.equal(true); + chai.expect(get.callCount).to.equal(1); + chai.expect(get.getCall(0).args[0]).to.equal('banana'); + chai.expect(allDocs.callCount).to.equal(1); + chai.expect(allDocs.getCall(0).args[0]).to.deep.equal({ keys: ['apple'], include_docs: true }); }); }); }); @@ -164,7 +164,6 @@ describe('Lineage', function() { describe('fetchHydratedDoc', function() { it('supports callback as second argument', function(done) { - query.resolves({ rows: [] }); get.resolves({ _id: 'a', type: 'person' }); lineage.fetchHydratedDoc('a', function(err, result) { @@ -175,7 +174,7 @@ describe('Lineage', function() { }); it('passes error to callback', function(done) { - query.rejects(new Error('db fail')); + get.rejects(new Error('db fail')); lineage.fetchHydratedDoc('a', function(err) { chai.expect(err.message).to.equal('db fail'); @@ -184,7 +183,7 @@ describe('Lineage', function() { }); it('throws when lineage is empty and throwWhenMissingLineage is true', function() { - query.resolves({ rows: [] }); + get.rejects({ status: 404 }); return lineage.fetchHydratedDoc('a', { throwWhenMissingLineage: true }) .then(() => chai.expect.fail('should have thrown')) @@ -205,7 +204,7 @@ describe('Lineage', function() { it('throws non-404 errors for single doc', function() { const err = new Error('server error'); err.status = 500; - query.rejects(err); + get.rejects(err); return lineage.fetchHydratedDocs(['a']) .then(() => chai.expect.fail('should have thrown')) diff --git a/tests/integration/api/server.spec.js b/tests/integration/api/server.spec.js index f803421d605..cdd1ea83abc 100644 --- a/tests/integration/api/server.spec.js +++ b/tests/integration/api/server.spec.js @@ -275,9 +275,11 @@ describe('server', () => { const reqID = getReqId(apiLogs[0]); const haproxyRequests = haproxyLogs.filter(entry => getReqId(entry) === reqID); - expect(haproxyRequests.length).to.equal(2); + // We now have _session, plus DB.get for the doc, plus POST /_all_docs for ancestors (so 3 total requests instead of 2). + expect(haproxyRequests.length).to.be.at.least(2); expect(haproxyRequests[0]).to.include('_session'); - expect(haproxyRequests[1]).to.include('_design/medic-client/_view/docs_by_id_lineage'); + const hasDbGetOrPost = haproxyRequests.some(r => r.includes(constants.USER_CONTACT_ID) || r.includes('_all_docs')); + expect(hasDbGetOrPost).to.be.true; }); it('should propagate ID via couch-request', async () => { diff --git a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts index 9f544fb50d5..6e687734d76 100644 --- a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts +++ b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts @@ -10,14 +10,16 @@ describe('LineageModelGenerator service', () => { let service; let dbQuery; let dbAllDocs; + let dbGet; beforeEach(() => { dbQuery = sinon.stub(); dbAllDocs = sinon.stub(); + dbGet = sinon.stub(); TestBed.configureTestingModule({ providers: [ - { provide: DbService, useValue: { get: () => ({ query: dbQuery, allDocs: dbAllDocs }) }}, + { provide: DbService, useValue: { get: () => ({ query: dbQuery, allDocs: dbAllDocs, get: dbGet }) }}, ], }); @@ -31,7 +33,7 @@ describe('LineageModelGenerator service', () => { describe('contact', () => { it('handles not found', done => { - dbQuery.resolves({ rows: [] }); + dbGet.rejects({ status: 404 }); service.contact('a') .then(() => { done(new Error('expected error to be thrown')); @@ -45,10 +47,7 @@ describe('LineageModelGenerator service', () => { it('handles no lineage', () => { const contact = { _id: 'a', _rev: '1' }; - dbQuery.resolves({ - rows: [ - { doc: contact } - ] }); + dbGet.resolves(contact); return service.contact('a').then(model => { expect(model._id).to.equal('a'); expect(model.doc).to.deep.equal(contact); @@ -56,23 +55,19 @@ describe('LineageModelGenerator service', () => { }); it('binds lineage', () => { - const contact = { _id: 'a', _rev: '1' }; - const parent = { _id: 'b', _rev: '1' }; + const contact = { _id: 'a', _rev: '1', parent: { _id: 'b' } }; + const parent = { _id: 'b', _rev: '1', parent: { _id: 'c' } }; const grandparent = { _id: 'c', _rev: '1' }; - dbQuery.resolves({ + dbGet.withArgs('a').resolves(contact); + dbAllDocs.withArgs({ keys: ['b', 'c'], include_docs: true }).resolves({ rows: [ - { doc: contact }, { doc: parent }, { doc: grandparent } ] }); return service.contact('a').then(model => { - expect(dbQuery.callCount).to.equal(1); - expect(dbQuery.args[0][0]).to.equal('medic-client/docs_by_id_lineage'); - expect(dbQuery.args[0][1]).to.deep.equal({ - startkey: [ 'a' ], - endkey: [ 'a', {} ], - include_docs: true - }); + expect(dbGet.callCount).to.equal(1); + expect(dbAllDocs.callCount).to.equal(1); + expect(dbAllDocs.args[0][0].keys).to.deep.equal(['b', 'c']); expect(model._id).to.equal('a'); expect(model.doc).to.deep.equal(contact); expect(model.lineage).to.deep.equal([ parent, grandparent ]); @@ -80,18 +75,18 @@ describe('LineageModelGenerator service', () => { }); it('binds contacts', () => { - const contact = { _id: 'a', _rev: '1', contact: { _id: 'd' } }; + const contact = { _id: 'a', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'b' } }; const contactsContact = { _id: 'd', name: 'dave' }; - const parent = { _id: 'b', _rev: '1', contact: { _id: 'e' } }; + const parent = { _id: 'b', _rev: '1', contact: { _id: 'e' }, parent: { _id: 'c' } }; const parentsContact = { _id: 'e', name: 'eliza' }; const grandparent = { _id: 'c', _rev: '1' }; - dbQuery.resolves({ + dbGet.resolves(contact); + dbAllDocs.withArgs({ keys: ['b', 'c'], include_docs: true }).resolves({ rows: [ - { doc: contact }, { doc: parent }, { doc: grandparent } ] }); - dbAllDocs.resolves({ + dbAllDocs.withArgs({ keys: sinon.match.array.deepEquals(['d', 'e']), include_docs: true }).resolves({ rows: [ { doc: contactsContact }, { doc: parentsContact } @@ -104,25 +99,27 @@ describe('LineageModelGenerator service', () => { }); it('hydrates lineage contacts - #3812', () => { - const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' } }; - const parent = { _id: 'b', _rev: '1', contact: { _id: 'd' } }; + const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' }, parent: { _id: 'b' } }; + const parent = { _id: 'b', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'c' } }; const grandparent = { _id: 'c', _rev: '1', contact: { _id: 'e' } }; const parentContact = { _id: 'd', name: 'donny' }; const grandparentContact = { _id: 'e', name: 'erica' }; - dbQuery.resolves({ + const xContact = { _id: 'x', name: 'xavier' }; + dbGet.resolves(contact); + dbAllDocs.withArgs({ keys: ['b', 'c'], include_docs: true }).resolves({ rows: [ - { doc: contact }, { doc: parent }, { doc: grandparent } ] }); - dbAllDocs.resolves({ + dbAllDocs.withArgs({ keys: sinon.match.array.contains('x', 'd', 'e'), include_docs: true }).resolves({ rows: [ + { doc: xContact }, { doc: parentContact }, { doc: grandparentContact } ] }); return service.contact('a').then(model => { - expect(dbAllDocs.callCount).to.equal(1); - expect(dbAllDocs.args[0][0]).to.deep.equal({ + expect(dbAllDocs.callCount).to.equal(2); + expect(dbAllDocs.args[1][0]).to.deep.equal({ keys: [ 'x', 'd', 'e' ], include_docs: true }); @@ -132,18 +129,18 @@ describe('LineageModelGenerator service', () => { }); it('should skip lineage contact hydration if requested', () => { - const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' } }; - const parent = { _id: 'b', _rev: '1', contact: { _id: 'd' } }; + const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' }, parent: { _id: 'b' } }; + const parent = { _id: 'b', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'c' } }; const grandparent = { _id: 'c', _rev: '1', contact: { _id: 'e' } }; - dbQuery.resolves({ + dbGet.resolves(contact); + dbAllDocs.resolves({ rows: [ - { doc: contact }, { doc: parent }, { doc: grandparent } ] }); return service.contact('a', { hydrate: false }).then(model => { - expect(dbAllDocs.callCount).to.equal(0); + expect(dbAllDocs.callCount).to.equal(1); // One for lineage, zero for contacts expect(model.doc.contact).to.deep.equal({ _id: 'x' }); expect(model.lineage[0].contact).to.deep.equal({ _id: 'd' }); expect(model.lineage[1].contact).to.deep.equal({ _id: 'e' }); @@ -152,7 +149,7 @@ describe('LineageModelGenerator service', () => { it('merges lineage when merge passed', () => { const contact = { _id: 'a', name: '1', parent: { _id: 'b', parent: { _id: 'c' } } }; - const parent = { _id: 'b', name: '2' }; + const parent = { _id: 'b', name: '2', parent: { _id: 'c' } }; const grandparent = { _id: 'c', name: '3' }; const expected = { _id: 'a', @@ -183,9 +180,9 @@ describe('LineageModelGenerator service', () => { } ] }; - dbQuery.resolves({ + dbGet.resolves(contact); + dbAllDocs.resolves({ rows: [ - { doc: contact }, { doc: parent }, { doc: grandparent } ] }); @@ -197,8 +194,9 @@ describe('LineageModelGenerator service', () => { it('should merge lineage with undefined members', () => { const contact = { _id: 'a', name: '1', parent: { _id: 'b', parent: { _id: 'c', parent: { _id: 'd' } } } }; const parent = { _id: 'b', name: '2', parent: { _id: 'c', parent: { _id: 'd' } } }; - dbQuery.resolves({ rows: - [{ doc: contact, key: ['a', 0] }, { doc: parent, key: ['a', 1] }, { key: ['a', 2] }, { key: ['a', 3] }] + dbGet.resolves(contact); + dbAllDocs.resolves({ rows: + [{ doc: parent, id: 'b' }, { id: 'c' }, { id: 'd' }] }); const expected = { _id: 'a', @@ -217,12 +215,12 @@ describe('LineageModelGenerator service', () => { it('should merge lineage with undefined members v2', () => { const contact = { _id: 'a', name: '1', parent: { _id: 'b', parent: { _id: 'c', parent: { _id: 'd' } } } }; const parent = { _id: 'b', name: '2', parent: { _id: 'c', parent: { _id: 'd' } } }; - dbQuery.resolves({ + dbGet.resolves(contact); + dbAllDocs.resolves({ rows: [ - { doc: contact, key: ['a', 0] }, - { doc: parent, key: ['a', 1] }, - { key: ['a', 2] }, - { key: ['a', 3], doc: { _id: 'd', name: '4' } } + { doc: parent, id: 'b' }, + { id: 'c' }, + { id: 'd', doc: { _id: 'd', name: '4' } } ] }); const expected = { _id: 'a', @@ -267,9 +265,9 @@ describe('LineageModelGenerator service', () => { } ] }; - dbQuery.resolves({ + dbGet.resolves(contact); + dbAllDocs.resolves({ rows: [ - { doc: contact }, { doc: parent }, { doc: grandparent } ] }); @@ -282,7 +280,7 @@ describe('LineageModelGenerator service', () => { describe('report', () => { it('handles not found', done => { - dbQuery.resolves({ rows: [] }); + dbGet.rejects({ status: 404 }); service.report('a') .then(() => { done(new Error('expected error to be thrown')); @@ -296,10 +294,7 @@ describe('LineageModelGenerator service', () => { it('handles no lineage', () => { const report = { _id: 'a', _rev: '1' }; - dbQuery.resolves({ - rows: [ - { doc: report } - ] }); + dbGet.resolves(report); return service.report('a').then(model => { expect(model._id).to.equal('a'); expect(model.doc).to.deep.equal(report); @@ -311,9 +306,9 @@ describe('LineageModelGenerator service', () => { const contact = { _id: 'b', _rev: '1' }; const parent = { _id: 'c', _rev: '1' }; const grandparent = { _id: 'd', _rev: '1' }; - dbQuery.resolves({ + dbGet.withArgs('a').resolves(report); + dbAllDocs.resolves({ rows: [ - { doc: report }, { doc: contact }, { doc: parent }, { doc: grandparent } @@ -327,26 +322,30 @@ describe('LineageModelGenerator service', () => { it('hydrates lineage contacts - #3812', () => { const report = { _id: 'a', _rev: '1', type: DOC_TYPES.DATA_RECORD, form: 'a', contact: { _id: 'x' } }; - const contact = { _id: 'b', _rev: '1', contact: { _id: 'y' } }; - const parent = { _id: 'c', _rev: '1', contact: { _id: 'e' } }; + const contact = { _id: 'b', _rev: '1', contact: { _id: 'y' }, parent: { _id: 'c' } }; + const parent = { _id: 'c', _rev: '1', contact: { _id: 'e' }, parent: { _id: 'd' } }; const grandparent = { _id: 'd', _rev: '1', contact: { _id: 'f' } }; const parentContact = { _id: 'e', name: 'erica' }; const grandparentContact = { _id: 'f', name: 'frank' }; - dbQuery.resolves({ + const xContact = { _id: 'x', name: 'xavier' }; + const yContact = { _id: 'y', name: 'yvonne' }; + dbGet.resolves(report); + dbAllDocs.withArgs(sinon.match({ keys: ['x', 'c', 'd'], include_docs: true })).resolves({ rows: [ - { doc: report }, { doc: contact }, { doc: parent }, { doc: grandparent } ] }); - dbAllDocs.resolves({ + dbAllDocs.withArgs(sinon.match({ keys: ['x', 'y', 'e', 'f'], include_docs: true })).resolves({ rows: [ + { doc: xContact }, + { doc: yContact }, { doc: parentContact }, { doc: grandparentContact } ] }); return service.report('a').then(model => { - expect(dbAllDocs.callCount).to.equal(1); - expect(dbAllDocs.args[0][0]).to.deep.equal({ + expect(dbAllDocs.callCount).to.equal(2); + expect(dbAllDocs.args[1][0]).to.deep.equal({ keys: [ 'x', 'y', 'e', 'f' ], include_docs: true }); diff --git a/webapp/tests/mocha/unit/views/docs_by_id_lineage.spec.js b/webapp/tests/mocha/unit/views/docs_by_id_lineage.spec.js deleted file mode 100644 index 3737e2523a2..00000000000 --- a/webapp/tests/mocha/unit/views/docs_by_id_lineage.spec.js +++ /dev/null @@ -1,200 +0,0 @@ -const expect = require('chai').expect; -const utils = require('./utils'); -const map = utils.loadView('medic-db', 'medic-client', 'docs_by_id_lineage'); -const { DOC_TYPES, CONTACT_TYPES } = require('@medic/constants'); - - - -describe('docs_by_id_lineage view', () => { - beforeEach(() => { - map.reset(); - }); - describe('data_record lineage', () => { - it('does not emit if doc is not a report', () => { - const doc = { - _id: 'messsage', - type: DOC_TYPES.DATA_RECORD, - sms_message: { } - }; - - const result = map(doc, true); - expect(result.length).to.equal(0); - }); - it('emits report document for depth 0', () => { - const doc = { - _id: 'report', - type: DOC_TYPES.DATA_RECORD, - form: 'form', - }; - - const result = map(doc, true); - expect(result.length).to.equal(1); - expect(result[0]).to.deep.equal({ key: [ 'report', 0 ], value: undefined }); - }); - - it('emits contact lineage for depth 1+', () => { - const doc = { - _id: 'report', - type: DOC_TYPES.DATA_RECORD, - form: 'form', - contact: { - _id: 'contact1', - parent: { - _id: 'contact2', - parent: { - _id: 'contact3' - } - } - } - }; - const result = map(doc, true); - expect(result.length).to.equal(4); - expect(result[0]).to.deep.equal({ key: [ 'report', 0 ], value: undefined }); - expect(result[1]).to.deep.equal({ key: [ 'report', 1 ], value: { _id: 'contact1' }}); - expect(result[2]).to.deep.equal({ key: [ 'report', 2 ], value: { _id: 'contact2' }}); - expect(result[3]).to.deep.equal({ key: [ 'report', 3 ], value: { _id: 'contact3' }}); - }); - - it('does not emit lineage for empty contact parents', () => { - const doc1 = { - _id: 'report1', - type: DOC_TYPES.DATA_RECORD, - form: 'form', - contact: {} - }; - const result1 = map(doc1, true); - expect(result1.length).to.equal(1); - expect(result1[0]).to.deep.equal({ key: [ 'report1', 0 ], value: undefined }); - - const doc2 = { - _id: 'report2', - type: DOC_TYPES.DATA_RECORD, - form: 'form', - contact: { - _id: 'contact1', - parent: {} - } - }; - - map.reset(); - const result2 = map(doc2, true); - expect(result2.length).to.equal(2); - expect(result2[0]).to.deep.equal({ key: [ 'report2', 0 ], value: undefined }); - expect(result2[1]).to.deep.equal({ key: [ 'report2', 1 ], value: { _id: 'contact1' }}); - - const doc3 = { - _id: 'report3', - type: DOC_TYPES.DATA_RECORD, - form: 'form', - contact: { - _id: 'contact1', - parent: { - _id: 'contact2', - parent: {} - } - } - }; - map.reset(); - const result3 = map(doc3, true); - expect(result3.length).to.equal(3); - expect(result3[0]).to.deep.equal({ key: [ 'report3', 0 ], value: undefined }); - expect(result3[1]).to.deep.equal({ key: [ 'report3', 1 ], value: { _id: 'contact1' }}); - expect(result3[2]).to.deep.equal({ key: [ 'report3', 2 ], value: { _id: 'contact2' }}); - }); - }); - - describe('contacts lineage', () => { - it('emits lineage for type `person`, `clinic`, `health_center` and `district_hospital`', () => { - const person = { _id: 'person', type: 'person' }; - const result = map(person, true); - expect(result.length).to.equal(1); - expect(result[0]).to.deep.equal({ key: [ 'person', 0 ], value: { _id: 'person' }}); - - map.reset(); - const clinic = { _id: 'clinic', type: 'clinic' }; - const resultClinic = map(clinic, true); - expect(resultClinic.length).to.equal(1); - expect(resultClinic[0]).to.deep.equal({ key: [ 'clinic', 0 ], value: { _id: 'clinic' }}); - - map.reset(); - const healthCenter = { _id: 'healthCenter', type: 'health_center' }; - const resultHealthCenter = map(healthCenter, true); - expect(resultHealthCenter.length).to.equal(1); - expect(resultHealthCenter[0]).to.deep.equal({ key: [ 'healthCenter', 0 ], value: { _id: 'healthCenter' }}); - - map.reset(); - const districtHospital = { _id: 'districtHospital', type: CONTACT_TYPES.DISTRICT_HOSPITAL }; - const resultdistrictHospital = map(districtHospital, true); - expect(resultdistrictHospital.length).to.equal(1); - expect(resultdistrictHospital[0]) - .to.deep.equal({ key: [ 'districtHospital', 0 ], value: { _id: 'districtHospital' }}); - }); - - it('emits full lineage', () => { - const checkLineage = (result, key) => { - if (key > 0) { - expect(result).to.deep.equal({ key: [ 'person', key ], value: { _id: `parent${key}` }}); - } else { - expect(result).to.deep.equal({ key: [ 'person', 0 ], value: { _id: 'person' }}); - } - }; - for (let depth = 1; depth < 10; depth++) { - const doc = { _id: 'person', type: 'person', parent: {} }; - let currentParent = doc.parent; - for (let i = 1; i <= depth; i++) { - currentParent._id = `parent${i}`; - currentParent.parent = {}; - currentParent = currentParent.parent; - } - - map.reset(); - const results = map(doc, true); - expect(results.length).to.equal(depth + 1); - results.forEach(checkLineage); - } - }); - - it('does not emit lineage for empty parents', () => { - const doc1 = { - _id: 'contact1', - type: 'person', - parent: {} - }; - const result1 = map(doc1, true); - expect(result1.length).to.equal(1); - expect(result1[0]).to.deep.equal({ key: [ 'contact1', 0 ], value: { _id: 'contact1'} }); - - const doc2 = { - _id: 'contact2', - type: 'person', - parent: { - _id: 'contact3', - parent: {} - } - }; - map.reset(); - const result2 = map(doc2, true); - expect(result2.length).to.equal(2); - expect(result2[0]).to.deep.equal({ key: [ 'contact2', 0 ], value: { _id: 'contact2' }}); - expect(result2[1]).to.deep.equal({ key: [ 'contact2', 1 ], value: { _id: 'contact3' }}); - - const doc3 = { - _id: 'contact3', - type: 'person', - parent: { - _id: 'contact4', - parent: { - _id: 'contact5', - parent: {} - } - } - }; - map.reset(); - const result3 = map(doc3, true); - expect(result3.length).to.equal(3); - expect(result3[0]).to.deep.equal({ key: [ 'contact3', 0 ], value: { _id: 'contact3' }}); - expect(result3[1]).to.deep.equal({ key: [ 'contact3', 1 ], value: { _id: 'contact4' }}); - expect(result3[2]).to.deep.equal({ key: [ 'contact3', 2 ], value: { _id: 'contact5' }}); - }); - }); -}); From 0e32e21ef08bf2c92c662229099f575522e642ff Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Tue, 31 Mar 2026 18:26:59 +0530 Subject: [PATCH 02/26] refactor: resolve SonarCloud complexity and nesting depth issues --- .../cht-datasource/src/local/libs/lineage.ts | 23 +++++++++++-------- shared-libs/lineage/src/hydration.js | 23 +++++++++++++------ 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/shared-libs/cht-datasource/src/local/libs/lineage.ts b/shared-libs/cht-datasource/src/local/libs/lineage.ts index a5f086bc93a..aec0c1614dc 100644 --- a/shared-libs/cht-datasource/src/local/libs/lineage.ts +++ b/shared-libs/cht-datasource/src/local/libs/lineage.ts @@ -15,7 +15,7 @@ import { Nullable } from '../../libs/core'; import { Doc } from '../../libs/doc'; -import { getDocsByIds, queryDocsByRange } from './doc'; +import { getDocsByIds } from './doc'; import logger from '@medic/logger'; import lineageFactory from '@medic/lineage'; import * as Report from '../../report'; @@ -27,6 +27,18 @@ import { InvalidArgumentError } from '../../libs/error'; import contactTypeUtils from '@medic/contact-types-utils'; import { isEqual } from 'lodash'; +const getParentIds = (doc: Doc): string[] => { + const parentIds: string[] = []; + let current = doc.type === 'data_record' ? doc.contact : doc.parent; + while (isRecord(current)) { + if (typeof current._id === 'string') { + parentIds.push(current._id); + } + current = current.parent; + } + return parentIds; +}; + /** * Returns the identified document along with the parent documents recorded for its lineage. The returned array is * sorted such that the identified document is the first element and the parent documents are in order of lineage. @@ -37,14 +49,7 @@ export const getLineageDocsById = (medicDb: PouchDB.Database): (id: string) return async (id: string) => { try { const doc = await medicDb.get(id); - const parentIds: string[] = []; - let current = doc.type === 'data_record' ? doc.contact : doc.parent; - while (isRecord(current)) { - if (typeof current._id === 'string') { - parentIds.push(current._id); - } - current = current.parent; - } + const parentIds = getParentIds(doc); if (parentIds.length === 0) { return [doc]; } diff --git a/shared-libs/lineage/src/hydration.js b/shared-libs/lineage/src/hydration.js index f56a11824d6..56ca1f035d5 100644 --- a/shared-libs/lineage/src/hydration.js +++ b/shared-libs/lineage/src/hydration.js @@ -47,6 +47,21 @@ const getContactIds = (contacts) => { }; module.exports = function(Promise, DB) { + const assembleLineage = function(doc, parentIds, ancestors) { + const docsMap = new Map(); + ancestors.forEach(function(a) { + if (a && a._id) { + docsMap.set(a._id, a); + } + }); + + const result = [doc]; + parentIds.forEach(function(parentId) { + result.push(docsMap.get(parentId) || null); + }); + return result; + }; + const fillParentsInDocs = function(doc, lineage) { if (!doc || !lineage.length) { return doc; @@ -237,13 +252,7 @@ module.exports = function(Promise, DB) { return [doc]; } return fetchDocs(parentIds).then(function(ancestors) { - const docsMap = new Map(); - ancestors.forEach(function(a) { docsMap.set(a._id, a); }); - const result = [doc]; - parentIds.forEach(function(parentId) { - result.push(docsMap.get(parentId) || null); - }); - return result; + return assembleLineage(doc, parentIds, ancestors); }); }) .catch(function(err) { From 7e93724307fe8786d4bc16f9b883f4be0c82bb1e Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Thu, 2 Apr 2026 03:28:47 +0530 Subject: [PATCH 03/26] fix: resolve linting and SonarCloud code quality issues in lineage view removal --- .../services/lineage-model-generator.spec.js | 10 +- .../cht-datasource/src/local/libs/lineage.ts | 4 +- shared-libs/lineage/src/hydration.js | 109 +++++++++--------- tests/integration/api/server.spec.js | 7 +- 4 files changed, 70 insertions(+), 60 deletions(-) diff --git a/admin/tests/unit/services/lineage-model-generator.spec.js b/admin/tests/unit/services/lineage-model-generator.spec.js index e91c6a70fc2..829e18e7d83 100644 --- a/admin/tests/unit/services/lineage-model-generator.spec.js +++ b/admin/tests/unit/services/lineage-model-generator.spec.js @@ -73,7 +73,10 @@ describe('LineageModelGenerator service', () => { { doc: parent }, { doc: grandparent } ] })); - dbAllDocs.withArgs({ keys: sinon.match.array.deepEquals(['d', 'e']), include_docs: true }).returns(Promise.resolve({ rows: [ + dbAllDocs.withArgs({ + keys: sinon.match.array.deepEquals(['d', 'e']), + include_docs: true + }).returns(Promise.resolve({ rows: [ { doc: contactsContact }, { doc: parentsContact } ] })); @@ -96,7 +99,10 @@ describe('LineageModelGenerator service', () => { { doc: parent }, { doc: grandparent } ] })); - dbAllDocs.withArgs({ keys: sinon.match.array.contains('x', 'd', 'e'), include_docs: true }).returns(Promise.resolve({ rows: [ + dbAllDocs.withArgs({ + keys: sinon.match.array.contains('x', 'd', 'e'), + include_docs: true + }).returns(Promise.resolve({ rows: [ { doc: xContact }, { doc: parentContact }, { doc: grandparentContact } diff --git a/shared-libs/cht-datasource/src/local/libs/lineage.ts b/shared-libs/cht-datasource/src/local/libs/lineage.ts index aec0c1614dc..edf55ab551e 100644 --- a/shared-libs/cht-datasource/src/local/libs/lineage.ts +++ b/shared-libs/cht-datasource/src/local/libs/lineage.ts @@ -55,8 +55,8 @@ export const getLineageDocsById = (medicDb: PouchDB.Database): (id: string) } const ancestors = await getMedicDocsById(parentIds); return [doc, ...ancestors]; - } catch (err: any) { - if (err.status === 404) { + } catch (err: unknown) { + if (err && typeof err === 'object' && 'status' in err && err.status === 404) { return []; } throw err; diff --git a/shared-libs/lineage/src/hydration.js b/shared-libs/lineage/src/hydration.js index 56ca1f035d5..5cccba26f8a 100644 --- a/shared-libs/lineage/src/hydration.js +++ b/shared-libs/lineage/src/hydration.js @@ -46,73 +46,74 @@ const getContactIds = (contacts) => { return _.uniq(ids); }; -module.exports = function(Promise, DB) { - const assembleLineage = function(doc, parentIds, ancestors) { - const docsMap = new Map(); - ancestors.forEach(function(a) { - if (a && a._id) { - docsMap.set(a._id, a); - } - }); +const assembleLineage = function(doc, parentIds, ancestors) { + const docsMap = new Map(); + ancestors.forEach(function(a) { + if (a?._id) { + docsMap.set(a._id, a); + } + }); - const result = [doc]; - parentIds.forEach(function(parentId) { - result.push(docsMap.get(parentId) || null); - }); - return result; - }; + const result = [doc]; + parentIds.forEach(function(parentId) { + result.push(docsMap.get(parentId) || null); + }); + return result; +}; - const fillParentsInDocs = function(doc, lineage) { - if (!doc || !lineage.length) { - return doc; - } +const fillParentsInDocs = function(doc, lineage) { + if (!doc || !lineage.length) { + return doc; + } - // Parent hierarchy starts at the contact for data_records - let currentParent; - if (utils.isReport(doc)) { - currentParent = doc.contact = lineage.shift() || doc.contact; - } else { - // It's a contact - currentParent = doc; - } + // Parent hierarchy starts at the contact for data_records + let currentParent; + if (utils.isReport(doc)) { + currentParent = doc.contact = lineage.shift() || doc.contact; + } else { + // It's a contact + currentParent = doc; + } - const parentIds = extractParentIds(currentParent.parent); - lineage.forEach(function(l, i) { - currentParent.parent = l ? deepCopy(l) : { _id: parentIds[i] }; - currentParent = currentParent.parent; - }); + const parentIds = extractParentIds(currentParent.parent); + lineage.forEach(function(l, i) { + currentParent.parent = l ? deepCopy(l) : { _id: parentIds[i] }; + currentParent = currentParent.parent; + }); - return doc; - }; + return doc; +}; + +const fillContactsInDocs = function(docs, contacts) { + if (!contacts || !contacts.length) { + return; + } + + docs.forEach(function(doc) { + if (!doc) { + return; + } + const id = utils.getId(doc.contact); + const contactDoc = getContactById(contacts, id); + if (contactDoc) { + doc.contact = deepCopy(contactDoc); + } - const fillContactsInDocs = function(docs, contacts) { - if (!contacts || !contacts.length) { + if (!utils.validLinkedDocs(doc)) { return; } - docs.forEach(function(doc) { - if (!doc) { - return; - } - const id = utils.getId(doc.contact); + Object.keys(doc.linked_docs).forEach(key => { + const id = utils.getId(doc.linked_docs[key]); const contactDoc = getContactById(contacts, id); if (contactDoc) { - doc.contact = deepCopy(contactDoc); + doc.linked_docs[key] = deepCopy(contactDoc); } - - if (!utils.validLinkedDocs(doc)) { - return; - } - - Object.keys(doc.linked_docs).forEach(key => { - const id = utils.getId(doc.linked_docs[key]); - const contactDoc = getContactById(contacts, id); - if (contactDoc) { - doc.linked_docs[key] = deepCopy(contactDoc); - } - }); }); - }; + }); +}; + +module.exports = function(Promise, DB) { const fetchContacts = function(lineage) { const contactIds = getContactIds(lineage); diff --git a/tests/integration/api/server.spec.js b/tests/integration/api/server.spec.js index cdd1ea83abc..79213d767c6 100644 --- a/tests/integration/api/server.spec.js +++ b/tests/integration/api/server.spec.js @@ -275,10 +275,13 @@ describe('server', () => { const reqID = getReqId(apiLogs[0]); const haproxyRequests = haproxyLogs.filter(entry => getReqId(entry) === reqID); - // We now have _session, plus DB.get for the doc, plus POST /_all_docs for ancestors (so 3 total requests instead of 2). + // We now have _session, plus DB.get for the doc, plus POST /_all_docs for ancestors + // (so 3 total requests instead of 2). expect(haproxyRequests.length).to.be.at.least(2); expect(haproxyRequests[0]).to.include('_session'); - const hasDbGetOrPost = haproxyRequests.some(r => r.includes(constants.USER_CONTACT_ID) || r.includes('_all_docs')); + const hasDbGetOrPost = haproxyRequests.some(r => { + return r.includes(constants.USER_CONTACT_ID) || r.includes('_all_docs'); + }); expect(hasDbGetOrPost).to.be.true; }); From 65f523a2807241621788d2f0908d50a86ab6e529 Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Thu, 2 Apr 2026 03:38:37 +0530 Subject: [PATCH 04/26] fix: resolve typescript compilation and sinon matcher errors --- admin/tests/unit/services/lineage-model-generator.spec.js | 2 +- .../karma/ts/services/lineage-model-generator.service.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/tests/unit/services/lineage-model-generator.spec.js b/admin/tests/unit/services/lineage-model-generator.spec.js index 829e18e7d83..eb678de482c 100644 --- a/admin/tests/unit/services/lineage-model-generator.spec.js +++ b/admin/tests/unit/services/lineage-model-generator.spec.js @@ -100,7 +100,7 @@ describe('LineageModelGenerator service', () => { { doc: grandparent } ] })); dbAllDocs.withArgs({ - keys: sinon.match.array.contains('x', 'd', 'e'), + keys: sinon.match.array.contains(['x', 'd', 'e']), include_docs: true }).returns(Promise.resolve({ rows: [ { doc: xContact }, diff --git a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts index 6e687734d76..0328e800edd 100644 --- a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts +++ b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts @@ -111,7 +111,7 @@ describe('LineageModelGenerator service', () => { { doc: parent }, { doc: grandparent } ] }); - dbAllDocs.withArgs({ keys: sinon.match.array.contains('x', 'd', 'e'), include_docs: true }).resolves({ + dbAllDocs.withArgs({ keys: sinon.match.array.contains(['x', 'd', 'e']), include_docs: true }).resolves({ rows: [ { doc: xContact }, { doc: parentContact }, From 288b527011f878e5131930501f3cf0f55322a138 Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Thu, 2 Apr 2026 03:52:48 +0530 Subject: [PATCH 05/26] fix: resolve karma test regression and sinon matcher usage --- admin/tests/unit/services/lineage-model-generator.spec.js | 2 +- shared-libs/lineage/src/hydration.js | 2 +- .../karma/ts/services/lineage-model-generator.service.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/admin/tests/unit/services/lineage-model-generator.spec.js b/admin/tests/unit/services/lineage-model-generator.spec.js index eb678de482c..51db29dabc1 100644 --- a/admin/tests/unit/services/lineage-model-generator.spec.js +++ b/admin/tests/unit/services/lineage-model-generator.spec.js @@ -100,7 +100,7 @@ describe('LineageModelGenerator service', () => { { doc: grandparent } ] })); dbAllDocs.withArgs({ - keys: sinon.match.array.contains(['x', 'd', 'e']), + keys: sinon.match.array.deepEquals(['x', 'd', 'e']), include_docs: true }).returns(Promise.resolve({ rows: [ { doc: xContact }, diff --git a/shared-libs/lineage/src/hydration.js b/shared-libs/lineage/src/hydration.js index 5cccba26f8a..ccda20445ae 100644 --- a/shared-libs/lineage/src/hydration.js +++ b/shared-libs/lineage/src/hydration.js @@ -56,7 +56,7 @@ const assembleLineage = function(doc, parentIds, ancestors) { const result = [doc]; parentIds.forEach(function(parentId) { - result.push(docsMap.get(parentId) || null); + result.push(docsMap.get(parentId) || undefined); }); return result; }; diff --git a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts index 0328e800edd..afedb518adf 100644 --- a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts +++ b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts @@ -111,7 +111,7 @@ describe('LineageModelGenerator service', () => { { doc: parent }, { doc: grandparent } ] }); - dbAllDocs.withArgs({ keys: sinon.match.array.contains(['x', 'd', 'e']), include_docs: true }).resolves({ + dbAllDocs.withArgs({ keys: sinon.match.array.deepEquals(['x', 'd', 'e']), include_docs: true }).resolves({ rows: [ { doc: xContact }, { doc: parentContact }, From a1c404c4aafc74585f2e72bf538e3bee1891ee48 Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Thu, 2 Apr 2026 04:20:49 +0530 Subject: [PATCH 06/26] fix: embed full parent chains in test docs for new allDocs-based lineage --- .../services/lineage-model-generator.service.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts index afedb518adf..c48e36e8eff 100644 --- a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts +++ b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts @@ -55,7 +55,7 @@ describe('LineageModelGenerator service', () => { }); it('binds lineage', () => { - const contact = { _id: 'a', _rev: '1', parent: { _id: 'b' } }; + const contact = { _id: 'a', _rev: '1', parent: { _id: 'b', parent: { _id: 'c' } } }; const parent = { _id: 'b', _rev: '1', parent: { _id: 'c' } }; const grandparent = { _id: 'c', _rev: '1' }; dbGet.withArgs('a').resolves(contact); @@ -75,7 +75,7 @@ describe('LineageModelGenerator service', () => { }); it('binds contacts', () => { - const contact = { _id: 'a', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'b' } }; + const contact = { _id: 'a', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'b', parent: { _id: 'c' } } }; const contactsContact = { _id: 'd', name: 'dave' }; const parent = { _id: 'b', _rev: '1', contact: { _id: 'e' }, parent: { _id: 'c' } }; const parentsContact = { _id: 'e', name: 'eliza' }; @@ -99,7 +99,7 @@ describe('LineageModelGenerator service', () => { }); it('hydrates lineage contacts - #3812', () => { - const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' }, parent: { _id: 'b' } }; + const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' }, parent: { _id: 'b', parent: { _id: 'c' } } }; const parent = { _id: 'b', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'c' } }; const grandparent = { _id: 'c', _rev: '1', contact: { _id: 'e' } }; const parentContact = { _id: 'd', name: 'donny' }; @@ -129,7 +129,7 @@ describe('LineageModelGenerator service', () => { }); it('should skip lineage contact hydration if requested', () => { - const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' }, parent: { _id: 'b' } }; + const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' }, parent: { _id: 'b', parent: { _id: 'c' } } }; const parent = { _id: 'b', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'c' } }; const grandparent = { _id: 'c', _rev: '1', contact: { _id: 'e' } }; dbGet.resolves(contact); @@ -321,7 +321,7 @@ describe('LineageModelGenerator service', () => { }); it('hydrates lineage contacts - #3812', () => { - const report = { _id: 'a', _rev: '1', type: DOC_TYPES.DATA_RECORD, form: 'a', contact: { _id: 'x' } }; + const report = { _id: 'a', _rev: '1', type: DOC_TYPES.DATA_RECORD, form: 'a', contact: { _id: 'x', parent: { _id: 'c', parent: { _id: 'd' } } } }; const contact = { _id: 'b', _rev: '1', contact: { _id: 'y' }, parent: { _id: 'c' } }; const parent = { _id: 'c', _rev: '1', contact: { _id: 'e' }, parent: { _id: 'd' } }; const grandparent = { _id: 'd', _rev: '1', contact: { _id: 'f' } }; From 09a953d792dc40a0acc71e740628da6d812cf3eb Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Thu, 2 Apr 2026 04:24:58 +0530 Subject: [PATCH 07/26] fix: break long line in report test for lint compliance --- .../karma/ts/services/lineage-model-generator.service.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts index c48e36e8eff..580487483d4 100644 --- a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts +++ b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts @@ -321,7 +321,8 @@ describe('LineageModelGenerator service', () => { }); it('hydrates lineage contacts - #3812', () => { - const report = { _id: 'a', _rev: '1', type: DOC_TYPES.DATA_RECORD, form: 'a', contact: { _id: 'x', parent: { _id: 'c', parent: { _id: 'd' } } } }; + const reportContact = { _id: 'x', parent: { _id: 'c', parent: { _id: 'd' } } }; + const report = { _id: 'a', _rev: '1', type: DOC_TYPES.DATA_RECORD, form: 'a', contact: reportContact }; const contact = { _id: 'b', _rev: '1', contact: { _id: 'y' }, parent: { _id: 'c' } }; const parent = { _id: 'c', _rev: '1', contact: { _id: 'e' }, parent: { _id: 'd' } }; const grandparent = { _id: 'd', _rev: '1', contact: { _id: 'f' } }; From 0e1287a03abf67033c864388034b71c62de776f3 Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Thu, 2 Apr 2026 04:37:51 +0530 Subject: [PATCH 08/26] fix: correct contact _id in report lineage test to match allDocs key --- .../karma/ts/services/lineage-model-generator.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts index 580487483d4..93fbe9b482e 100644 --- a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts +++ b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts @@ -323,7 +323,7 @@ describe('LineageModelGenerator service', () => { it('hydrates lineage contacts - #3812', () => { const reportContact = { _id: 'x', parent: { _id: 'c', parent: { _id: 'd' } } }; const report = { _id: 'a', _rev: '1', type: DOC_TYPES.DATA_RECORD, form: 'a', contact: reportContact }; - const contact = { _id: 'b', _rev: '1', contact: { _id: 'y' }, parent: { _id: 'c' } }; + const contact = { _id: 'x', _rev: '1', contact: { _id: 'y' }, parent: { _id: 'c' } }; const parent = { _id: 'c', _rev: '1', contact: { _id: 'e' }, parent: { _id: 'd' } }; const grandparent = { _id: 'd', _rev: '1', contact: { _id: 'f' } }; const parentContact = { _id: 'e', name: 'erica' }; From b9790ffef66aef209ea81a3fe4ca9dd80517f5c6 Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Sun, 5 Apr 2026 22:20:22 +0530 Subject: [PATCH 09/26] fix: resolve unit test regressions and linting errors --- .../services/lineage-model-generator.spec.js | 30 ++++++++++++++----- api/tests/mocha/services/settings.spec.js | 1 - .../lineage-model-generator.service.spec.ts | 4 +-- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/admin/tests/unit/services/lineage-model-generator.spec.js b/admin/tests/unit/services/lineage-model-generator.spec.js index 51db29dabc1..fd1178822a6 100644 --- a/admin/tests/unit/services/lineage-model-generator.spec.js +++ b/admin/tests/unit/services/lineage-model-generator.spec.js @@ -44,11 +44,14 @@ describe('LineageModelGenerator service', () => { }); it('binds lineage', () => { - const contact = { _id: 'a', _rev: '1', parent: { _id: 'b' } }; + const contact = { _id: 'a', _rev: '1', parent: { _id: 'b', parent: { _id: 'c' } } }; const parent = { _id: 'b', _rev: '1', parent: { _id: 'c' } }; const grandparent = { _id: 'c', _rev: '1' }; dbGet.withArgs('a').returns(Promise.resolve(contact)); - dbAllDocs.withArgs({ keys: ['b', 'c'], include_docs: true }).returns(Promise.resolve({ rows: [ + dbAllDocs.withArgs(sinon.match({ + keys: sinon.match.array.deepEquals(['b', 'c']), + include_docs: true + })).returns(Promise.resolve({ rows: [ { doc: parent }, { doc: grandparent } ] })); @@ -63,13 +66,16 @@ describe('LineageModelGenerator service', () => { }); it('binds contacts', () => { - const contact = { _id: 'a', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'b' } }; + const contact = { _id: 'a', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'b', parent: { _id: 'c' } } }; const contactsContact = { _id: 'd', name: 'dave' }; const parent = { _id: 'b', _rev: '1', contact: { _id: 'e' }, parent: { _id: 'c' } }; const parentsContact = { _id: 'e', name: 'eliza' }; const grandparent = { _id: 'c', _rev: '1' }; dbGet.returns(Promise.resolve(contact)); - dbAllDocs.withArgs({ keys: ['b', 'c'], include_docs: true }).returns(Promise.resolve({ rows: [ + dbAllDocs.withArgs(sinon.match({ + keys: sinon.match.array.deepEquals(['b', 'c']), + include_docs: true + })).returns(Promise.resolve({ rows: [ { doc: parent }, { doc: grandparent } ] })); @@ -88,14 +94,17 @@ describe('LineageModelGenerator service', () => { }); it('hydrates lineage contacts - #3812', () => { - const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' }, parent: { _id: 'b' } }; + const contact = { _id: 'a', _rev: '1', contact: { _id: 'x' }, parent: { _id: 'b', parent: { _id: 'c' } } }; const parent = { _id: 'b', _rev: '1', contact: { _id: 'd' }, parent: { _id: 'c' } }; const grandparent = { _id: 'c', _rev: '1', contact: { _id: 'e' } }; const parentContact = { _id: 'd', name: 'donny' }; const grandparentContact = { _id: 'e', name: 'erica' }; const xContact = { _id: 'x', name: 'xavier' }; dbGet.returns(Promise.resolve(contact)); - dbAllDocs.withArgs({ keys: ['b', 'c'], include_docs: true }).returns(Promise.resolve({ rows: [ + dbAllDocs.withArgs(sinon.match({ + keys: sinon.match.array.deepEquals(['b', 'c']), + include_docs: true + })).returns(Promise.resolve({ rows: [ { doc: parent }, { doc: grandparent } ] })); @@ -110,6 +119,10 @@ describe('LineageModelGenerator service', () => { return service.contact('a').then(model => { chai.expect(dbAllDocs.callCount).to.equal(2); chai.expect(dbAllDocs.args[0][0]).to.deep.equal({ + keys: [ 'b', 'c' ], + include_docs: true + }); + chai.expect(dbAllDocs.args[1][0]).to.deep.equal({ keys: [ 'x', 'd', 'e' ], include_docs: true }); @@ -152,7 +165,10 @@ describe('LineageModelGenerator service', () => { ] }; dbGet.returns(Promise.resolve(contact)); - dbAllDocs.withArgs({ keys: ['b', 'c'], include_docs: true }).returns(Promise.resolve({ rows: [ + dbAllDocs.withArgs(sinon.match({ + keys: sinon.match.array.deepEquals(['b', 'c']), + include_docs: true + })).returns(Promise.resolve({ rows: [ { doc: parent }, { doc: grandparent } ] })); diff --git a/api/tests/mocha/services/settings.spec.js b/api/tests/mocha/services/settings.spec.js index 227f66c7be1..d02736bce5e 100644 --- a/api/tests/mocha/services/settings.spec.js +++ b/api/tests/mocha/services/settings.spec.js @@ -6,7 +6,6 @@ should(); const service = require('../../../src/services/settings'); const db = require('../../../src/db'); const resources = require('../../../src/resources'); -// eslint-disable-next-line n/no-missing-require const defaults = require('../../../build/default-docs/settings.doc.json'); const config = require('../../../src/config'); diff --git a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts index 93fbe9b482e..f2fa779fa93 100644 --- a/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts +++ b/webapp/tests/karma/ts/services/lineage-model-generator.service.spec.ts @@ -337,7 +337,7 @@ describe('LineageModelGenerator service', () => { { doc: parent }, { doc: grandparent } ] }); - dbAllDocs.withArgs(sinon.match({ keys: ['x', 'y', 'e', 'f'], include_docs: true })).resolves({ + dbAllDocs.withArgs(sinon.match({ keys: ['y', 'e', 'f'], include_docs: true })).resolves({ rows: [ { doc: xContact }, { doc: yContact }, @@ -347,7 +347,7 @@ describe('LineageModelGenerator service', () => { return service.report('a').then(model => { expect(dbAllDocs.callCount).to.equal(2); expect(dbAllDocs.args[1][0]).to.deep.equal({ - keys: [ 'x', 'y', 'e', 'f' ], + keys: [ 'y', 'e', 'f' ], include_docs: true }); expect(model.doc.contact.parent.contact).to.deep.equal(parentContact); From c3e291dc6b4d7d09fff1c05281de66ba9364205a Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Sun, 5 Apr 2026 22:27:39 +0530 Subject: [PATCH 10/26] fix: restore linting override for build artifact in settings spec --- api/tests/mocha/services/settings.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/api/tests/mocha/services/settings.spec.js b/api/tests/mocha/services/settings.spec.js index d02736bce5e..227f66c7be1 100644 --- a/api/tests/mocha/services/settings.spec.js +++ b/api/tests/mocha/services/settings.spec.js @@ -6,6 +6,7 @@ should(); const service = require('../../../src/services/settings'); const db = require('../../../src/db'); const resources = require('../../../src/resources'); +// eslint-disable-next-line n/no-missing-require const defaults = require('../../../build/default-docs/settings.doc.json'); const config = require('../../../src/config'); From 488d41e154d8daf6dcaf1024453cecce7143df0e Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Mon, 6 Apr 2026 11:20:15 +0530 Subject: [PATCH 11/26] fix: resolve CI regressions in lineage refactoring branch --- api/src/services/bulk-get.js | 8 +++++-- shared-libs/infodoc/src/infodoc.js | 23 ++++++++++++++++++- shared-libs/lineage/src/hydration.js | 14 ++++++----- shared-libs/outbound/src/outbound.js | 9 ++++---- .../transitions/src/transitions/index.js | 17 +++++++++----- 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/api/src/services/bulk-get.js b/api/src/services/bulk-get.js index b294c8693ee..449d5405090 100644 --- a/api/src/services/bulk-get.js +++ b/api/src/services/bulk-get.js @@ -1,6 +1,7 @@ const authorization = require('./authorization'); const db = require('../db'); const _ = require('lodash'); +const lineage = require('@medic/lineage')(Promise, db.medic); // filters response from CouchDB only to include successfully read and allowed docs const filterResults = (authorizationContext, result) => { @@ -28,8 +29,11 @@ module.exports = { return db.medic.bulkGet(_.defaults({ docs: docs }, _.omit(query, 'latest'))); }) .then(result => { - result.results = filterResults(authorizationContext, result); - return result; + const docsToHydrate = _.compact(_.flatMap(result.results, r => r.docs.map(d => d.ok))); + return lineage.hydrateDocs(docsToHydrate).then(() => { + result.results = filterResults(authorizationContext, result); + return result; + }); }); }, }; diff --git a/shared-libs/infodoc/src/infodoc.js b/shared-libs/infodoc/src/infodoc.js index 2fcdad0bd44..89c9de64891 100644 --- a/shared-libs/infodoc/src/infodoc.js +++ b/shared-libs/infodoc/src/infodoc.js @@ -12,6 +12,24 @@ const blankInfoDoc = (docId, knownReplicationDate) => { }; }; +const pickOlder = (a, b) => { + if (a === 'unknown' || !a) { + return b || 'unknown'; + } + if (b === 'unknown' || !b) { + return a || 'unknown'; + } + const da = new Date(a).getTime(); + const db = new Date(b).getTime(); + if (isNaN(da)) { + return b; + } + if (isNaN(db)) { + return a; + } + return da < db ? a : b; +}; + const findInfoDocs = (database, ids) => { return database .allDocs({ keys: ids, include_docs: true }) @@ -205,7 +223,10 @@ const bulkUpdate = infoDocs => { .then(freshInfoDocs => { freshInfoDocs.forEach(({ doc: freshInfoDoc }, idx) => { conflictingInfoDocs[idx]._rev = freshInfoDoc._rev; - conflictingInfoDocs[idx].initial_replication_date = freshInfoDoc.initial_replication_date; + conflictingInfoDocs[idx].initial_replication_date = pickOlder( + conflictingInfoDocs[idx].initial_replication_date, + freshInfoDoc.initial_replication_date + ); conflictingInfoDocs[idx].latest_replication_date = freshInfoDoc.latest_replication_date; conflictingInfoDocs[idx].completed_tasks = freshInfoDoc.completed_tasks; diff --git a/shared-libs/lineage/src/hydration.js b/shared-libs/lineage/src/hydration.js index ccda20445ae..9a06a9e2567 100644 --- a/shared-libs/lineage/src/hydration.js +++ b/shared-libs/lineage/src/hydration.js @@ -244,8 +244,9 @@ module.exports = function(Promise, DB) { }); }; - const fetchLineageById = function(id) { - return DB.get(id) + const fetchLineageById = function(id, doc) { + const getDoc = doc ? Promise.resolve(doc) : DB.get(id); + return getDoc .then(function(doc) { const startParent = utils.isReport(doc) ? doc.contact : doc.parent; const parentIds = extractParentIds(startParent); @@ -288,7 +289,7 @@ module.exports = function(Promise, DB) { }); }; - const fetchHydratedDoc = function(id, options = {}, callback = undefined) { + const fetchHydratedDoc = function(id, options = {}, callback = undefined, doc = undefined) { let lineage; let patientLineage; let placeLineage; @@ -301,7 +302,7 @@ module.exports = function(Promise, DB) { throwWhenMissingLineage: false, }); - return fetchLineageById(id) + return fetchLineageById(id, doc) .then(function(result) { lineage = result; @@ -312,7 +313,7 @@ module.exports = function(Promise, DB) { throw err; } else { // Not a doc that has lineage, just do a normal fetch. - return fetchDoc(id); + return doc ? doc : fetchDoc(id); } } @@ -345,6 +346,7 @@ module.exports = function(Promise, DB) { }); }; + // for data_records, include the first-level contact. const collectParentIds = function(docs) { const ids = []; @@ -401,7 +403,7 @@ module.exports = function(Promise, DB) { return Promise.resolve([]); } - const hydratedDocs = deepCopy(docs); // a copy of the original docs which we will incrementally hydrate and return + const hydratedDocs = docs; // mutate in-place as expected by some callers const knownDocs = [...hydratedDocs]; // an array of all documents which we have fetched let patientUuids; // a map of [k, v] pairs with [hydratedDocUuid, patientUuid] diff --git a/shared-libs/outbound/src/outbound.js b/shared-libs/outbound/src/outbound.js index bfa1e2fd341..860dec7ab2f 100644 --- a/shared-libs/outbound/src/outbound.js +++ b/shared-libs/outbound/src/outbound.js @@ -240,15 +240,16 @@ const updateInfo = (payload, recordInfo, configName) => { * Attempts to usefully parse the error so we can log it appropriately */ const logSendError = (configName, recordId, error) => { - if (error.constructor.name === 'StatusCodeError') { - const {statusCode, body} = error.response; + const statusCode = error.statusCode || error.status; + const body = error.body; + if (statusCode && statusCode >= 400) { // We got back something from the server but it's not a 2xx logger.error(`Failed to push ${recordId} to ${configName}, server responsed with ${statusCode}`); let loggableBody; try { - loggableBody = JSON.stringify(body); + loggableBody = typeof body === 'string' ? body : JSON.stringify(body); } catch { if (body && body.length > 100) { loggableBody = `${body.substring(0, 100)}...`; @@ -257,7 +258,7 @@ const logSendError = (configName, recordId, error) => { } } logger.error(`Response body: ${loggableBody}`); - } else if (error.constructor.name === 'RequestError') { + } else if (error.constructor.name === 'RequestError' || error.constructor.name === 'TypeError') { // The url was malformed, the server doesn't exist at all, etc logger.error(`Failed to push ${recordId} to ${configName}: ${error.message}`); } else if (error.constructor.name === 'OutboundError') { diff --git a/shared-libs/transitions/src/transitions/index.js b/shared-libs/transitions/src/transitions/index.js index 2b0115dbc00..c9f94535af6 100644 --- a/shared-libs/transitions/src/transitions/index.js +++ b/shared-libs/transitions/src/transitions/index.js @@ -48,7 +48,7 @@ let loadErrors = false; // applies all loaded transitions over a change const processChange = (change, callback) => { lineage - .fetchHydratedDoc(change.id) + .fetchHydratedDoc(change.id, {}, undefined, change.doc) .then(doc => { change.doc = doc; return infodoc.get(change).then(infoDoc => { @@ -105,11 +105,16 @@ const processDocs = docs => { return callback(null, err || result); } - // doc was not changed by any transition, so we save the original doc - change.doc = docs.find(doc => doc._id === change.id); - saveDoc(change, (err, result) => { - callback(null, err || result); - }); + // doc was not changed by any transition. + // If it's a new doc, we must save it to the medic DB anyway. + // If it's an existing doc, we don't need to save it. + if (!change.doc._rev) { + saveDoc(change, (err, result) => { + callback(null, err || result); + }); + } else { + callback(null, { ok: true, id: change.id, rev: change.doc._rev }); + } }); }); async.series(operations, (err, results) => { From d73d80ddc55eacf95cf64ad092f56b1d1444b9ff Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Mon, 6 Apr 2026 23:40:20 +0530 Subject: [PATCH 12/26] chore: fix SonarCloud CI quality gate failures --- shared-libs/infodoc/src/infodoc.js | 4 +- shared-libs/lineage/src/hydration.js | 53 +++++++++---------- .../transitions/src/transitions/index.js | 8 +-- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/shared-libs/infodoc/src/infodoc.js b/shared-libs/infodoc/src/infodoc.js index 89c9de64891..25f02e2a32a 100644 --- a/shared-libs/infodoc/src/infodoc.js +++ b/shared-libs/infodoc/src/infodoc.js @@ -21,10 +21,10 @@ const pickOlder = (a, b) => { } const da = new Date(a).getTime(); const db = new Date(b).getTime(); - if (isNaN(da)) { + if (Number.isNaN(da)) { return b; } - if (isNaN(db)) { + if (Number.isNaN(db)) { return a; } return da < db ? a : b; diff --git a/shared-libs/lineage/src/hydration.js b/shared-libs/lineage/src/hydration.js index 9a06a9e2567..497da098f1c 100644 --- a/shared-libs/lineage/src/hydration.js +++ b/shared-libs/lineage/src/hydration.js @@ -279,6 +279,23 @@ module.exports = function(Promise, DB) { }); }; + const hydrateLineage = (lineage, contactsPromise) => { + let patientLineage; + let placeLineage; + return fetchSubjectLineage(lineage[0]) + .then((lineages = {}) => { + patientLineage = lineages.patientLineage; + placeLineage = lineages.placeLineage; + return contactsPromise || fetchContacts(lineage.concat(patientLineage, placeLineage)); + }) + .then(function(contacts) { + fillContactsInDocs(lineage, contacts); + fillContactsInDocs(patientLineage, contacts); + fillContactsInDocs(placeLineage, contacts); + return mergeLineagesIntoDoc(lineage, contacts, patientLineage, placeLineage); + }); + }; + const fetchDoc = function(id) { return DB.get(id) .catch(function(err) { @@ -290,9 +307,6 @@ module.exports = function(Promise, DB) { }; const fetchHydratedDoc = function(id, options = {}, callback = undefined, doc = undefined) { - let lineage; - let patientLineage; - let placeLineage; if (typeof options === 'function') { callback = options; options = {}; @@ -303,33 +317,18 @@ module.exports = function(Promise, DB) { }); return fetchLineageById(id, doc) - .then(function(result) { - lineage = result; - - if (lineage.length === 0) { - if (options.throwWhenMissingLineage) { - const err = new Error(`Document not found: ${id}`); - err.code = 404; - throw err; - } else { - // Not a doc that has lineage, just do a normal fetch. - return doc ? doc : fetchDoc(id); - } + .then(function(lineage) { + if (lineage.length > 0) { + return hydrateLineage(lineage); } - return fetchSubjectLineage(lineage[0]) - .then((lineages = {}) => { - patientLineage = lineages.patientLineage; - placeLineage = lineages.placeLineage; + if (options.throwWhenMissingLineage) { + const err = new Error(`Document not found: ${id}`); + err.code = 404; + throw err; + } - return fetchContacts(lineage.concat(patientLineage, placeLineage)); - }) - .then(function(contacts) { - fillContactsInDocs(lineage, contacts); - fillContactsInDocs(patientLineage, contacts); - fillContactsInDocs(placeLineage, contacts); - return mergeLineagesIntoDoc(lineage, contacts, patientLineage, placeLineage); - }); + return doc || fetchDoc(id); }) .then(function(result) { if (callback) { diff --git a/shared-libs/transitions/src/transitions/index.js b/shared-libs/transitions/src/transitions/index.js index c9f94535af6..7d67af67cac 100644 --- a/shared-libs/transitions/src/transitions/index.js +++ b/shared-libs/transitions/src/transitions/index.js @@ -108,12 +108,14 @@ const processDocs = docs => { // doc was not changed by any transition. // If it's a new doc, we must save it to the medic DB anyway. // If it's an existing doc, we don't need to save it. - if (!change.doc._rev) { + if (change.doc._rev) { + // If it's an existing doc, we don't need to save it. + callback(null, { ok: true, id: change.id, rev: change.doc._rev }); + } else { + // If it's a new doc, we must save it to the medic DB anyway. saveDoc(change, (err, result) => { callback(null, err || result); }); - } else { - callback(null, { ok: true, id: change.id, rev: change.doc._rev }); } }); }); From a8fff24ae1e9e24a3ac021b6e1a31867eb783101 Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Mon, 6 Apr 2026 23:59:54 +0530 Subject: [PATCH 13/26] chore: avoid moment deprecation warning on invalid date strings in validation utils --- shared-libs/validation/src/validation_utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-libs/validation/src/validation_utils.js b/shared-libs/validation/src/validation_utils.js index b7b36c8cb58..cda8930533d 100644 --- a/shared-libs/validation/src/validation_utils.js +++ b/shared-libs/validation/src/validation_utils.js @@ -107,7 +107,7 @@ const compareDate = (doc, date, durationString, checkAfter=false) => { logger.error('date constraint validation: the duration is invalid'); return false; } - const testDate = moment(date); + const testDate = typeof date === 'string' ? moment(date, [moment.ISO_8601, moment.RFC_2822]) : moment(date); if (!testDate.isValid()) { logger.error('date constraint validation: the date is invalid'); return false; From 1d756953a9da6c345ff06f5d811d34a55e6c8cdd Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Tue, 7 Apr 2026 21:15:12 +0530 Subject: [PATCH 14/26] fix: address maintainer feedback on bulk-get and cleanup unrelated changes --- api/src/services/bulk-get.js | 12 +- api/tests/mocha/services/bulk-get.spec.js | 9 +- shared-libs/infodoc/src/infodoc.js | 23 +- shared-libs/lineage/test.log | 837 ------------------ shared-libs/outbound/src/outbound.js | 9 +- .../transitions/src/transitions/index.js | 17 +- .../validation/src/validation_utils.js | 2 +- tests/integration/api/server.spec.js | 2 +- 8 files changed, 28 insertions(+), 883 deletions(-) delete mode 100644 shared-libs/lineage/test.log diff --git a/api/src/services/bulk-get.js b/api/src/services/bulk-get.js index 449d5405090..a5560c6a877 100644 --- a/api/src/services/bulk-get.js +++ b/api/src/services/bulk-get.js @@ -4,13 +4,14 @@ const _ = require('lodash'); const lineage = require('@medic/lineage')(Promise, db.medic); // filters response from CouchDB only to include successfully read and allowed docs -const filterResults = (authorizationContext, result) => { +const filterResults = (authorizationContext, result, hydratedMap) => { return result.results.filter(resultDocs => { resultDocs.docs = resultDocs.docs.filter(doc => { if (!doc.ok) { return false; } - return authorization.allowedDoc(resultDocs.id, authorizationContext, authorization.getViewResults(doc.ok)); + const hydratedDoc = hydratedMap.get(doc.ok._id); + return authorization.allowedDoc(resultDocs.id, authorizationContext, authorization.getViewResults(hydratedDoc || doc.ok)); }); return resultDocs.docs.length; }); @@ -30,8 +31,11 @@ module.exports = { }) .then(result => { const docsToHydrate = _.compact(_.flatMap(result.results, r => r.docs.map(d => d.ok))); - return lineage.hydrateDocs(docsToHydrate).then(() => { - result.results = filterResults(authorizationContext, result); + const clones = docsToHydrate.map(doc => _.cloneDeep(doc)); + return lineage.hydrateDocs(clones).then(() => { + const hydratedMap = new WeakMap(); + docsToHydrate.forEach((doc, i) => hydratedMap.set(doc, clones[i])); + result.results = filterResults(authorizationContext, result, hydratedMap); return result; }); }); diff --git a/api/tests/mocha/services/bulk-get.spec.js b/api/tests/mocha/services/bulk-get.spec.js index 9785c4f45e0..01106730d33 100644 --- a/api/tests/mocha/services/bulk-get.spec.js +++ b/api/tests/mocha/services/bulk-get.spec.js @@ -1,18 +1,25 @@ const sinon = require('sinon'); require('chai').should(); -const service = require('../../../src/services/bulk-get'); +const rewire = require('rewire'); +const service = rewire('../../../src/services/bulk-get'); const db = require('../../../src/db'); const authorization = require('../../../src/services/authorization'); let userCtx; let query; let docs; +let lineageStub; describe('Bulk Get service', () => { beforeEach(function() { query = {}; userCtx = { name: 'user' }; + lineageStub = { + hydrateDocs: sinon.stub().resolves([]) + }; + service.__set__('lineage', lineageStub); + sinon.stub(authorization, 'getAuthorizationContext').resolves({}); sinon.stub(authorization, 'allowedDoc').returns(true); sinon.stub(authorization, 'getViewResults').callsFake(doc => ({ view: doc })); diff --git a/shared-libs/infodoc/src/infodoc.js b/shared-libs/infodoc/src/infodoc.js index 25f02e2a32a..2fcdad0bd44 100644 --- a/shared-libs/infodoc/src/infodoc.js +++ b/shared-libs/infodoc/src/infodoc.js @@ -12,24 +12,6 @@ const blankInfoDoc = (docId, knownReplicationDate) => { }; }; -const pickOlder = (a, b) => { - if (a === 'unknown' || !a) { - return b || 'unknown'; - } - if (b === 'unknown' || !b) { - return a || 'unknown'; - } - const da = new Date(a).getTime(); - const db = new Date(b).getTime(); - if (Number.isNaN(da)) { - return b; - } - if (Number.isNaN(db)) { - return a; - } - return da < db ? a : b; -}; - const findInfoDocs = (database, ids) => { return database .allDocs({ keys: ids, include_docs: true }) @@ -223,10 +205,7 @@ const bulkUpdate = infoDocs => { .then(freshInfoDocs => { freshInfoDocs.forEach(({ doc: freshInfoDoc }, idx) => { conflictingInfoDocs[idx]._rev = freshInfoDoc._rev; - conflictingInfoDocs[idx].initial_replication_date = pickOlder( - conflictingInfoDocs[idx].initial_replication_date, - freshInfoDoc.initial_replication_date - ); + conflictingInfoDocs[idx].initial_replication_date = freshInfoDoc.initial_replication_date; conflictingInfoDocs[idx].latest_replication_date = freshInfoDoc.latest_replication_date; conflictingInfoDocs[idx].completed_tasks = freshInfoDoc.completed_tasks; diff --git a/shared-libs/lineage/test.log b/shared-libs/lineage/test.log deleted file mode 100644 index ebf9d607dd7..00000000000 --- a/shared-libs/lineage/test.log +++ /dev/null @@ -1,837 +0,0 @@ - -> @medic/lineage@1.0.0 test -> nyc --nycrcPath='../nyc.config.js' mocha ./test --require test/setup.js - - - - Lineage - fetchLineageById - ✔ queries db with correct parameters - fetchContacts - ✔ fetches contacts with correct parameters - ✔ does not fetch contacts that it has already got via lineage - fillContactsInDocs - ✔ skips null docs in the array - ✔ populates the contact field for relevant docs - fillParentsInDocs - ✔ populates parent fields throughout lineage - ✔ correctly populates parent fields for reports - fetchHydratedDoc - ✔ supports callback as second argument - ✔ passes error to callback - ✔ throws when lineage is empty and throwWhenMissingLineage is true - fetchHydratedDocs - ✔ returns empty array for empty docIds - ✔ throws non-404 errors for single doc - hydrateDocs - ✔ works on empty array - ✔ handles reports without contact id - ✔ works on docs without contacts or parents - - Lineage - fetchLineageById - ✔ returns correct lineage - fetchLineageByIds - ✔ returns correct lineages - fetchContacts - ✔ clones any reused contacts - fetchHydratedDoc - ✔ returns errors from query - ✔ returns unmodified doc when there is no lineage and no contact - 1) handles doc with unknown parent by leaving just the stub - 2) handles doc with unknown contact by leaving just the stub - ✔ handles missing contacts - 3) attaches the full lineage for reports with patient_id - 4) attaches patient lineage when using patient_uuid field - 5) attaches patient lineage when using patient_id field that contains a uuid - 6) attaches the full lineage for reports with place_id - 7) attaches the full lineage for reports with place_id containing a uuid - 8) attaches the full lineage for reports with place_id and patient_id - 9) should work when patient is not found - 10) should work when place is not found - ✔ attaches the contacts - 11) attaches re-used contacts, minify handles the circular references - ✔ minifying the result returns the starting document for a report - ✔ minifying the result returns the starting document for a place - ✔ works for SMS reports - ✔ handles doc with empty-object parent by removing it - ✔ should hydrate linked docs from contacts - ✔ should not hydrate linked docs from reports - hydrateDocs - ✔ binds contacts and parents - ✔ ignores db-fetch errors - ✔ minifying the result returns the starting documents - ✔ does not return a report with circular references - ✔ handles person with circular reference ids - ✔ handles place with circular reference ids - ✔ handles report with circular reference ids - ✔ should not recurse more than needed - ✔ processing a doc with itself as a parent does not error out - ✔ processing a doc with itself as a grandparent does not error out - ✔ processing a doc with itself as a grandparent referenced through intermediate docs does not error out - ✔ should hydrate linked docs from contacts, but not from reports - ✔ should re-use docs from provided list - fetchHydratedDocs - ✔ should crash with bad param - ✔ should work with one contact - 12) should work with one report - ✔ should work with one non-existent doc - ✔ should work with multiple docs - ✔ should hydrate linked docs for contacts, but not for reports - - Minify - minifyLineage - ✔ returns falsy parent as-is - ✔ returns parent without _id as-is - ✔ removes everything except id - minify - ✔ handles null argument - ✔ minifies the parent - ✔ minifies the contact and lineage - ✔ removes the patient - ✔ removes the place - ✔ errors out on potential infinite recursion - ✔ should minify linked docs for contacts - ✔ should not minify linked docs for reports - ✔ should only minify linked docs if valid - - - 58 passing (219ms) - 12 failing - - 1) Lineage - fetchHydratedDoc - handles doc with unknown parent by leaving just the stub: - - AssertionError: expected { _id: 'stub_parents', …(3) } to deeply equal { _id: 'stub_parents', …(3) } - + expected - actual - - "contact": { - "_id": "something" - } - "parent": { - - "_id": "dummyDoc" - - "name": "district" - + "_id": "something_else" - + "parent": { - + "_id": "dummyDoc" - + "name": "district" - + } - } - "type": "clinic" - } - - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:618:55 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - 2) Lineage - fetchHydratedDoc - handles doc with unknown contact by leaving just the stub: - - AssertionError: expected { Object (_id, form, ...) } to deeply equal { _id: 'stub_contacts', …(3) } - + expected - actual - - "contact": { - "_id": "something" - "parent": { - "_id": "dummyDoc" - + "name": "district" - } - } - "form": {} - "type": "data_record" - - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:632:55 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - 3) Lineage - fetchHydratedDoc - attaches the full lineage for reports with patient_id: - - AssertionError: Expected "name" field to be defined at path "/contact/parent". - + expected - actual - - { - - "_id": "report" - - "_rev": "1-84140bd0f8e0997eaa7060226eebbf17" - "contact": { - - "_id": "report_contact" - - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" - "name": "report_contact_name" - "parent": { - - "_id": "report_parent" - + "contact": { - + "name": "report_parentContact_name" - + "phone": "+123" - + } - + "name": "report_parent_name" - "parent": { - - "_id": "report_grandparent" - + "contact": { - + "name": "report_grandparentContact_name" - + "phone": "+456" - + } - + "name": "report_grandparent_name" - } - } - - "reported_date": "5" - - "type": "person" - } - - "fields": { - - "patient_id": "12345" - - } - "form": "A" - + "parent": [undefined] - "patient": { - - "_id": "report_patient" - - "_rev": "1-85cf4d70d5f13efc2dd9e843c036db6e" - "name": "patient_name" - "parent": { - - "_id": "report_parent" - - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" - "contact": { - - "_id": "report_parentContact" - - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" - "name": "report_parentContact_name" - "phone": "+123" - - "reported_date": "5" - - "type": "person" - } - "name": "report_parent_name" - - "parent": { - - "_id": "report_grandparent" - - "_rev": "1-46d806688da3e855379b2c8084712074" - - "contact": { - - "_id": "report_grandparentContact" - - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" - - "name": "report_grandparentContact_name" - - "phone": "+456" - - "reported_date": "5" - - "type": "person" - - } - - "name": "report_grandparent_name" - - } - } - - "patient_id": "12345" - - "reported_date": "5" - - "type": "person" - } - - "type": "data_record" - } - - at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:653:16 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - 4) Lineage - fetchHydratedDoc - attaches patient lineage when using patient_uuid field: - - AssertionError: Expected "name" field to be defined at path "/contact/parent". - + expected - actual - - { - - "_id": "report2" - - "_rev": "1-1e0bd6cbfb59bbc332bda6c6414cd297" - "contact": { - - "_id": "report_contact" - - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" - "name": "report_contact_name" - "parent": { - - "_id": "report_parent" - + "contact": { - + "phone": "+123" - + } - + "name": "report_parent_name" - "parent": { - - "_id": "report_grandparent" - + "contact": { - + "phone": "+456" - + } - + "name": "report_grandparent_name" - } - } - - "reported_date": "5" - - "type": "person" - } - - "fields": { - - "patient_id": "" - - "patient_uuid": "report_patient" - - } - "form": "A" - + "parent": [undefined] - "patient": { - - "_id": "report_patient" - - "_rev": "1-85cf4d70d5f13efc2dd9e843c036db6e" - "name": "patient_name" - "parent": { - - "_id": "report_parent" - - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" - "contact": { - - "_id": "report_parentContact" - - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" - "name": "report_parentContact_name" - - "phone": "+123" - - "reported_date": "5" - - "type": "person" - } - "name": "report_parent_name" - - "parent": { - - "_id": "report_grandparent" - - "_rev": "1-46d806688da3e855379b2c8084712074" - - "contact": { - - "_id": "report_grandparentContact" - - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" - - "name": "report_grandparentContact_name" - - "phone": "+456" - - "reported_date": "5" - - "type": "person" - - } - - "name": "report_grandparent_name" - - } - } - - "patient_id": "12345" - - "reported_date": "5" - - "type": "person" - } - - "type": "data_record" - } - - at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:690:16 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - 5) Lineage - fetchHydratedDoc - attaches patient lineage when using patient_id field that contains a uuid: - - AssertionError: Expected "name" field to be defined at path "/contact/parent". - + expected - actual - - { - - "_id": "report3" - - "_rev": "1-951e95a2f59b1bd766f093dd764d2272" - "contact": { - - "_id": "report_contact" - - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" - "name": "report_contact_name" - "parent": { - - "_id": "report_parent" - + "contact": { - + "phone": "+123" - + } - + "name": "report_parent_name" - "parent": { - - "_id": "report_grandparent" - + "contact": { - + "phone": "+456" - + } - + "name": "report_grandparent_name" - } - } - - "reported_date": "5" - - "type": "person" - } - - "fields": { - - "patient_id": "report_patient" - - } - "form": "A" - + "parent": [undefined] - "patient": { - - "_id": "report_patient" - - "_rev": "1-85cf4d70d5f13efc2dd9e843c036db6e" - "name": "patient_name" - "parent": { - - "_id": "report_parent" - - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" - "contact": { - - "_id": "report_parentContact" - - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" - "name": "report_parentContact_name" - - "phone": "+123" - - "reported_date": "5" - - "type": "person" - } - "name": "report_parent_name" - - "parent": { - - "_id": "report_grandparent" - - "_rev": "1-46d806688da3e855379b2c8084712074" - - "contact": { - - "_id": "report_grandparentContact" - - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" - - "name": "report_grandparentContact_name" - - "phone": "+456" - - "reported_date": "5" - - "type": "person" - - } - - "name": "report_grandparent_name" - - } - } - - "patient_id": "12345" - - "reported_date": "5" - - "type": "person" - } - - "type": "data_record" - } - - at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:720:16 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - 6) Lineage - fetchHydratedDoc - attaches the full lineage for reports with place_id: - - AssertionError: Expected "name" field to be defined at path "/contact/parent". - + expected - actual - - { - - "_id": "report_with_place" - - "_rev": "1-0af5527f2ad728975619eed655faa8d7" - "contact": { - - "_id": "report_contact" - - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" - "name": "report_contact_name" - "parent": { - - "_id": "report_parent" - + "contact": { - + "name": "report_parentContact_name" - + "phone": "+123" - + } - + "name": "report_parent_name" - "parent": { - - "_id": "report_grandparent" - + "contact": { - + "name": "report_grandparentContact_name" - + "phone": "+456" - + } - + "name": "report_grandparent_name" - } - } - - "reported_date": "5" - - "type": "person" - } - - "fields": { - - "place_id": "54321" - - } - "form": "A" - + "parent": [undefined] - "place": { - - "_id": "report_place" - - "_rev": "1-d2e1eab40f2e58eb1650c7ed7970210b" - "name": "place_name" - "parent": { - - "_id": "report_parent" - - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" - "contact": { - - "_id": "report_parentContact" - - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" - "name": "report_parentContact_name" - "phone": "+123" - - "reported_date": "5" - - "type": "person" - } - "name": "report_parent_name" - - "parent": { - - "_id": "report_grandparent" - - "_rev": "1-46d806688da3e855379b2c8084712074" - - "contact": { - - "_id": "report_grandparentContact" - - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" - - "name": "report_grandparentContact_name" - - "phone": "+456" - - "reported_date": "5" - - "type": "person" - - } - - "name": "report_grandparent_name" - - } - } - - "place_id": "54321" - - "reported_date": "5" - - "type": "clinic" - } - - "type": "data_record" - } - - at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:750:16 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - 7) Lineage - fetchHydratedDoc - attaches the full lineage for reports with place_id containing a uuid: - - AssertionError: Expected "name" field to be defined at path "/contact/parent". - + expected - actual - - { - - "_id": "report_with_place_uuid" - - "_rev": "1-dbeef6a38bfc6a3ed5c21781d6c6338c" - "contact": { - - "_id": "report_contact" - - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" - "name": "report_contact_name" - "parent": { - - "_id": "report_parent" - + "contact": { - + "name": "report_parentContact_name" - + "phone": "+123" - + } - + "name": "report_parent_name" - "parent": { - - "_id": "report_grandparent" - + "contact": { - + "name": "report_grandparentContact_name" - + "phone": "+456" - + } - + "name": "report_grandparent_name" - } - } - - "reported_date": "5" - - "type": "person" - } - - "fields": { - - "place_id": "report_place" - - } - "form": "A" - + "parent": [undefined] - "place": { - - "_id": "report_place" - - "_rev": "1-d2e1eab40f2e58eb1650c7ed7970210b" - "name": "place_name" - "parent": { - - "_id": "report_parent" - - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" - "contact": { - - "_id": "report_parentContact" - - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" - "name": "report_parentContact_name" - "phone": "+123" - - "reported_date": "5" - - "type": "person" - } - "name": "report_parent_name" - - "parent": { - - "_id": "report_grandparent" - - "_rev": "1-46d806688da3e855379b2c8084712074" - - "contact": { - - "_id": "report_grandparentContact" - - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" - - "name": "report_grandparentContact_name" - - "phone": "+456" - - "reported_date": "5" - - "type": "person" - - } - - "name": "report_grandparent_name" - - } - } - - "place_id": "54321" - - "reported_date": "5" - - "type": "clinic" - } - - "type": "data_record" - } - - at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:787:16 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - 8) Lineage - fetchHydratedDoc - attaches the full lineage for reports with place_id and patient_id: - - AssertionError: Expected "name" field to be defined at path "/contact/parent". - + expected - actual - - { - - "_id": "report_with_place_and_patient" - - "_rev": "1-d75dd101052194a341462effa12f97a4" - "contact": { - - "_id": "report_contact" - - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" - "name": "report_contact_name" - "parent": { - - "_id": "report_parent" - - "parent": { - - "_id": "report_grandparent" - - } - - } - - "reported_date": "5" - - "type": "person" - - } - - "fields": { - - "patient_id": "12345" - - "place_id": "54321" - - } - - "form": "A" - - "patient": { - - "_id": "report_patient" - - "_rev": "1-85cf4d70d5f13efc2dd9e843c036db6e" - - "name": "patient_name" - - "parent": { - - "_id": "report_parent" - - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" - "contact": { - - "_id": "report_parentContact" - - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" - "name": "report_parentContact_name" - "phone": "+123" - - "reported_date": "5" - - "type": "person" - } - "name": "report_parent_name" - "parent": { - - "_id": "report_grandparent" - - "_rev": "1-46d806688da3e855379b2c8084712074" - "contact": { - - "_id": "report_grandparentContact" - - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" - "name": "report_grandparentContact_name" - "phone": "+456" - - "reported_date": "5" - - "type": "person" - } - "name": "report_grandparent_name" - } - } - - "patient_id": "12345" - - "reported_date": "5" - - "type": "person" - } - + "form": "A" - + "parent": [undefined] - + "patient": { - + "name": "patient_name" - + "parent": { - + "contact": { - + "name": "report_parentContact_name" - + } - + "name": "report_parent_name" - + } - + } - "place": { - - "_id": "report_place" - - "_rev": "1-d2e1eab40f2e58eb1650c7ed7970210b" - "name": "place_name" - "parent": { - - "_id": "report_parent" - - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" - "contact": { - - "_id": "report_parentContact" - - "_rev": "1-127936780d96f56159b0dfe1ddc49c6e" - "name": "report_parentContact_name" - "phone": "+123" - - "reported_date": "5" - - "type": "person" - } - "name": "report_parent_name" - - "parent": { - - "_id": "report_grandparent" - - "_rev": "1-46d806688da3e855379b2c8084712074" - - "contact": { - - "_id": "report_grandparentContact" - - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" - - "name": "report_grandparentContact_name" - - "phone": "+456" - - "reported_date": "5" - - "type": "person" - - } - - "name": "report_grandparent_name" - - } - } - - "place_id": "54321" - - "reported_date": "5" - - "type": "clinic" - } - - "type": "data_record" - } - - at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:823:16 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - 9) Lineage - fetchHydratedDoc - should work when patient is not found: - - AssertionError: Expected "name" field to be defined at path "/contact/parent". - + expected - actual - - { - - "_id": "report4" - - "_rev": "1-b4cd6494e2282cfcda8203194f6733cb" - "contact": { - - "_id": "report_contact" - - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" - "name": "report_contact_name" - "parent": { - - "_id": "report_parent" - + "contact": { - + "phone": "+123" - + } - + "name": "report_parent_name" - "parent": { - - "_id": "report_grandparent" - + "contact": { - + "phone": "+456" - + } - + "name": "report_grandparent_name" - } - } - - "reported_date": "5" - - "type": "person" - } - - "fields": { - - "patient_id": "something" - - } - "form": "A" - - "type": "data_record" - + "parent": [undefined] - } - - at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:870:16 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - 10) Lineage - fetchHydratedDoc - should work when place is not found: - - AssertionError: Expected "name" field to be defined at path "/contact/parent". - + expected - actual - - { - - "_id": "report5" - - "_rev": "1-2f521446a981bfec946bb3fb1e65942c" - "contact": { - - "_id": "report_contact" - - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" - "name": "report_contact_name" - "parent": { - - "_id": "report_parent" - + "contact": { - + "phone": "+123" - + } - + "name": "report_parent_name" - "parent": { - - "_id": "report_grandparent" - + "contact": { - + "phone": "+456" - + } - + "name": "report_grandparent_name" - } - } - - "reported_date": "5" - - "type": "person" - } - - "fields": { - - "place_id": "something" - - } - "form": "A" - - "type": "data_record" - + "parent": [undefined] - } - - at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:892:16 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - 11) Lineage - fetchHydratedDoc - attaches re-used contacts, minify handles the circular references: - TypeError: Cannot read properties of undefined (reading 'hydrated') - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:931:46 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - 12) Lineage - fetchHydratedDocs - should work with one report: - - AssertionError: Expected "name" field to be defined at path "/contact/parent". - + expected - actual - - { - "_id": "report" - - "_rev": "1-84140bd0f8e0997eaa7060226eebbf17" - "contact": { - - "_id": "report_contact" - - "_rev": "1-0b7b88d94c5eb79909c567f0405ad6ef" - "name": "report_contact_name" - "parent": { - - "_id": "report_parent" - + "contact": { - + "name": "report_parentContact_name" - + } - + "name": "report_parent_name" - "parent": { - - "_id": "report_grandparent" - + "contact": { - + "name": "report_grandparentContact_name" - + } - + "name": "report_grandparent_name" - } - } - - "reported_date": "5" - - "type": "person" - } - - "fields": { - - "patient_id": "12345" - - } - - "form": "A" - "patient": { - - "_id": "report_patient" - - "_rev": "1-85cf4d70d5f13efc2dd9e843c036db6e" - "name": "patient_name" - "parent": { - - "_id": "report_parent" - - "_rev": "1-1043866e738b00f02fa0a38f1b3aa81b" - "contact": { - - "_id": "report_parentContact" - - "_rev": "3-0253eb97360222140f0cde806200f076" - "name": "report_parentContact_name" - - "phone": "+123" - - "reported_date": "5" - - "type": "person" - } - "name": "report_parent_name" - "parent": { - - "_id": "report_grandparent" - - "_rev": "1-46d806688da3e855379b2c8084712074" - "contact": { - - "_id": "report_grandparentContact" - - "_rev": "1-cd383c17d0ba97b6c7bc82ab8c213b6b" - "name": "report_grandparentContact_name" - - "phone": "+456" - - "reported_date": "5" - - "type": "person" - } - "name": "report_grandparent_name" - } - } - - "patient_id": "12345" - - "reported_date": "5" - - "type": "person" - } - - "type": "data_record" - } - - at chai.assert.shallowDeepEqual (/Users/shivamchaudhary/Programming_Projects/medic/cht-core/node_modules/chai-shallow-deep-equal/chai-shallow-deep-equal.js:98:44) - at /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage/test/integration.js:1647:16 - at process.processTicksAndRejections (node:internal/process/task_queues:105:5) - - - - -=============================== Coverage summary =============================== -Statements : 99.08% ( 326/329 ) -Branches : 96.02% ( 145/151 ) -Functions : 100% ( 93/93 ) -Lines : 99.02% ( 306/309 ) -================================================================================ -npm error Lifecycle script `test` failed with error: -npm error code 12 -npm error path /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage -npm error workspace @medic/lineage@1.0.0 -npm error location /Users/shivamchaudhary/Programming_Projects/medic/cht-core/shared-libs/lineage -npm error command failed -npm error command sh -c nyc --nycrcPath='../nyc.config.js' mocha ./test --require test/setup.js diff --git a/shared-libs/outbound/src/outbound.js b/shared-libs/outbound/src/outbound.js index 860dec7ab2f..bfa1e2fd341 100644 --- a/shared-libs/outbound/src/outbound.js +++ b/shared-libs/outbound/src/outbound.js @@ -240,16 +240,15 @@ const updateInfo = (payload, recordInfo, configName) => { * Attempts to usefully parse the error so we can log it appropriately */ const logSendError = (configName, recordId, error) => { - const statusCode = error.statusCode || error.status; - const body = error.body; + if (error.constructor.name === 'StatusCodeError') { + const {statusCode, body} = error.response; - if (statusCode && statusCode >= 400) { // We got back something from the server but it's not a 2xx logger.error(`Failed to push ${recordId} to ${configName}, server responsed with ${statusCode}`); let loggableBody; try { - loggableBody = typeof body === 'string' ? body : JSON.stringify(body); + loggableBody = JSON.stringify(body); } catch { if (body && body.length > 100) { loggableBody = `${body.substring(0, 100)}...`; @@ -258,7 +257,7 @@ const logSendError = (configName, recordId, error) => { } } logger.error(`Response body: ${loggableBody}`); - } else if (error.constructor.name === 'RequestError' || error.constructor.name === 'TypeError') { + } else if (error.constructor.name === 'RequestError') { // The url was malformed, the server doesn't exist at all, etc logger.error(`Failed to push ${recordId} to ${configName}: ${error.message}`); } else if (error.constructor.name === 'OutboundError') { diff --git a/shared-libs/transitions/src/transitions/index.js b/shared-libs/transitions/src/transitions/index.js index 7d67af67cac..667f2049f72 100644 --- a/shared-libs/transitions/src/transitions/index.js +++ b/shared-libs/transitions/src/transitions/index.js @@ -105,18 +105,11 @@ const processDocs = docs => { return callback(null, err || result); } - // doc was not changed by any transition. - // If it's a new doc, we must save it to the medic DB anyway. - // If it's an existing doc, we don't need to save it. - if (change.doc._rev) { - // If it's an existing doc, we don't need to save it. - callback(null, { ok: true, id: change.id, rev: change.doc._rev }); - } else { - // If it's a new doc, we must save it to the medic DB anyway. - saveDoc(change, (err, result) => { - callback(null, err || result); - }); - } + // doc was not changed by any transition, so we save the original doc + change.doc = docs.find(doc => doc._id === change.id); + saveDoc(change, (err, result) => { + callback(null, err || result); + }); }); }); async.series(operations, (err, results) => { diff --git a/shared-libs/validation/src/validation_utils.js b/shared-libs/validation/src/validation_utils.js index cda8930533d..b7b36c8cb58 100644 --- a/shared-libs/validation/src/validation_utils.js +++ b/shared-libs/validation/src/validation_utils.js @@ -107,7 +107,7 @@ const compareDate = (doc, date, durationString, checkAfter=false) => { logger.error('date constraint validation: the duration is invalid'); return false; } - const testDate = typeof date === 'string' ? moment(date, [moment.ISO_8601, moment.RFC_2822]) : moment(date); + const testDate = moment(date); if (!testDate.isValid()) { logger.error('date constraint validation: the date is invalid'); return false; diff --git a/tests/integration/api/server.spec.js b/tests/integration/api/server.spec.js index 79213d767c6..cce92f7fe4b 100644 --- a/tests/integration/api/server.spec.js +++ b/tests/integration/api/server.spec.js @@ -277,7 +277,7 @@ describe('server', () => { const haproxyRequests = haproxyLogs.filter(entry => getReqId(entry) === reqID); // We now have _session, plus DB.get for the doc, plus POST /_all_docs for ancestors // (so 3 total requests instead of 2). - expect(haproxyRequests.length).to.be.at.least(2); + expect(haproxyRequests.length).to.equal(3); expect(haproxyRequests[0]).to.include('_session'); const hasDbGetOrPost = haproxyRequests.some(r => { return r.includes(constants.USER_CONTACT_ID) || r.includes('_all_docs'); From c0edae82502d4e2195be43ebbb779e7cd10b4d0e Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Tue, 7 Apr 2026 21:23:49 +0530 Subject: [PATCH 15/26] style: address lint errors in bulk-get.js --- api/src/services/bulk-get.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/services/bulk-get.js b/api/src/services/bulk-get.js index a5560c6a877..88c7d24c2e1 100644 --- a/api/src/services/bulk-get.js +++ b/api/src/services/bulk-get.js @@ -11,7 +11,8 @@ const filterResults = (authorizationContext, result, hydratedMap) => { return false; } const hydratedDoc = hydratedMap.get(doc.ok._id); - return authorization.allowedDoc(resultDocs.id, authorizationContext, authorization.getViewResults(hydratedDoc || doc.ok)); + const viewResults = authorization.getViewResults(hydratedDoc || doc.ok); + return authorization.allowedDoc(resultDocs.id, authorizationContext, viewResults); }); return resultDocs.docs.length; }); From e63a2684de6986c68f1f774f2b0cb217f234f80c Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Tue, 7 Apr 2026 21:40:34 +0530 Subject: [PATCH 16/26] fix: resolve moment deprecation warning causing CI failure --- shared-libs/validation/src/validation_utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-libs/validation/src/validation_utils.js b/shared-libs/validation/src/validation_utils.js index b7b36c8cb58..cda8930533d 100644 --- a/shared-libs/validation/src/validation_utils.js +++ b/shared-libs/validation/src/validation_utils.js @@ -107,7 +107,7 @@ const compareDate = (doc, date, durationString, checkAfter=false) => { logger.error('date constraint validation: the duration is invalid'); return false; } - const testDate = moment(date); + const testDate = typeof date === 'string' ? moment(date, [moment.ISO_8601, moment.RFC_2822]) : moment(date); if (!testDate.isValid()) { logger.error('date constraint validation: the date is invalid'); return false; From d5d84efe1fc83dddec122cff3bc089961528cb4b Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Wed, 8 Apr 2026 00:47:36 +0530 Subject: [PATCH 17/26] chore: trigger CI to retry flaky E2E tests From fe1d5c630fe3da15066afad6277c8b101059446d Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Thu, 9 Apr 2026 10:35:06 +0530 Subject: [PATCH 18/26] fix: restore deepCopy docs and revert unnecessary bulk-get changes per maintainer feedback --- api/src/services/bulk-get.js | 17 ++++------------- shared-libs/lineage/src/hydration.js | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/api/src/services/bulk-get.js b/api/src/services/bulk-get.js index 88c7d24c2e1..b294c8693ee 100644 --- a/api/src/services/bulk-get.js +++ b/api/src/services/bulk-get.js @@ -1,18 +1,15 @@ const authorization = require('./authorization'); const db = require('../db'); const _ = require('lodash'); -const lineage = require('@medic/lineage')(Promise, db.medic); // filters response from CouchDB only to include successfully read and allowed docs -const filterResults = (authorizationContext, result, hydratedMap) => { +const filterResults = (authorizationContext, result) => { return result.results.filter(resultDocs => { resultDocs.docs = resultDocs.docs.filter(doc => { if (!doc.ok) { return false; } - const hydratedDoc = hydratedMap.get(doc.ok._id); - const viewResults = authorization.getViewResults(hydratedDoc || doc.ok); - return authorization.allowedDoc(resultDocs.id, authorizationContext, viewResults); + return authorization.allowedDoc(resultDocs.id, authorizationContext, authorization.getViewResults(doc.ok)); }); return resultDocs.docs.length; }); @@ -31,14 +28,8 @@ module.exports = { return db.medic.bulkGet(_.defaults({ docs: docs }, _.omit(query, 'latest'))); }) .then(result => { - const docsToHydrate = _.compact(_.flatMap(result.results, r => r.docs.map(d => d.ok))); - const clones = docsToHydrate.map(doc => _.cloneDeep(doc)); - return lineage.hydrateDocs(clones).then(() => { - const hydratedMap = new WeakMap(); - docsToHydrate.forEach((doc, i) => hydratedMap.set(doc, clones[i])); - result.results = filterResults(authorizationContext, result, hydratedMap); - return result; - }); + result.results = filterResults(authorizationContext, result); + return result; }); }, }; diff --git a/shared-libs/lineage/src/hydration.js b/shared-libs/lineage/src/hydration.js index 497da098f1c..bc03e45a09c 100644 --- a/shared-libs/lineage/src/hydration.js +++ b/shared-libs/lineage/src/hydration.js @@ -402,7 +402,7 @@ module.exports = function(Promise, DB) { return Promise.resolve([]); } - const hydratedDocs = docs; // mutate in-place as expected by some callers + const hydratedDocs = deepCopy(docs); // a copy of the original docs which we will incrementally hydrate and return const knownDocs = [...hydratedDocs]; // an array of all documents which we have fetched let patientUuids; // a map of [k, v] pairs with [hydratedDocUuid, patientUuid] From d67f8efc0659a7cda8f02b93f4e36689bf28835e Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Thu, 9 Apr 2026 15:14:46 +0530 Subject: [PATCH 19/26] fix: revert unnecessary changes to bulk-get tests per maintainer feedback --- api/tests/mocha/services/bulk-get.spec.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/api/tests/mocha/services/bulk-get.spec.js b/api/tests/mocha/services/bulk-get.spec.js index 01106730d33..9785c4f45e0 100644 --- a/api/tests/mocha/services/bulk-get.spec.js +++ b/api/tests/mocha/services/bulk-get.spec.js @@ -1,25 +1,18 @@ const sinon = require('sinon'); require('chai').should(); -const rewire = require('rewire'); -const service = rewire('../../../src/services/bulk-get'); +const service = require('../../../src/services/bulk-get'); const db = require('../../../src/db'); const authorization = require('../../../src/services/authorization'); let userCtx; let query; let docs; -let lineageStub; describe('Bulk Get service', () => { beforeEach(function() { query = {}; userCtx = { name: 'user' }; - lineageStub = { - hydrateDocs: sinon.stub().resolves([]) - }; - service.__set__('lineage', lineageStub); - sinon.stub(authorization, 'getAuthorizationContext').resolves({}); sinon.stub(authorization, 'allowedDoc').returns(true); sinon.stub(authorization, 'getViewResults').callsFake(doc => ({ view: doc })); From b216b0ea8a53d0e7eee2e44395e11c881e3ef36e Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Thu, 9 Apr 2026 16:17:11 +0530 Subject: [PATCH 20/26] fix: silence Sass deprecation warnings in build-prepare --- scripts/build/build-prepare.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/build-prepare.sh b/scripts/build/build-prepare.sh index f3b80ae1138..28599f2102a 100755 --- a/scripts/build/build-prepare.sh +++ b/scripts/build/build-prepare.sh @@ -8,7 +8,7 @@ echo "build-prepare: building ddocs" npm run build-ddocs echo "build-prepare: compiling enketo css" -sass webapp/src/css/enketo/enketo.scss api/build/static/webapp/enketo.less --no-source-map +sass webapp/src/css/enketo/enketo.scss api/build/static/webapp/enketo.less --no-source-map --silence-deprecation=import --silence-deprecation=global-builtin --silence-deprecation=color-functions --silence-deprecation=slash-div echo "build-prepare: building admin app" node ./scripts/build/build-angularjs-template-cache.js From 7b2968ab71243ff65203d32dfff2de82eaaae1fc Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Thu, 9 Apr 2026 16:44:55 +0530 Subject: [PATCH 21/26] chore: trigger CI re-run From 0e555dab0d5e2d421f7891846086f647a1cb2583 Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Sun, 26 Apr 2026 03:07:00 +0530 Subject: [PATCH 22/26] fix: address maintainer feedback regarding request counts and unrelated changes --- shared-libs/lineage/src/hydration.js | 11 +++++------ shared-libs/transitions/src/transitions/index.js | 2 +- shared-libs/validation/src/validation_utils.js | 2 +- tests/integration/api/server.spec.js | 6 +++--- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/shared-libs/lineage/src/hydration.js b/shared-libs/lineage/src/hydration.js index bc03e45a09c..f18329d5a04 100644 --- a/shared-libs/lineage/src/hydration.js +++ b/shared-libs/lineage/src/hydration.js @@ -244,9 +244,8 @@ module.exports = function(Promise, DB) { }); }; - const fetchLineageById = function(id, doc) { - const getDoc = doc ? Promise.resolve(doc) : DB.get(id); - return getDoc + const fetchLineageById = function(id) { + return DB.get(id) .then(function(doc) { const startParent = utils.isReport(doc) ? doc.contact : doc.parent; const parentIds = extractParentIds(startParent); @@ -306,7 +305,7 @@ module.exports = function(Promise, DB) { }); }; - const fetchHydratedDoc = function(id, options = {}, callback = undefined, doc = undefined) { + const fetchHydratedDoc = function(id, options = {}, callback = undefined) { if (typeof options === 'function') { callback = options; options = {}; @@ -316,7 +315,7 @@ module.exports = function(Promise, DB) { throwWhenMissingLineage: false, }); - return fetchLineageById(id, doc) + return fetchLineageById(id) .then(function(lineage) { if (lineage.length > 0) { return hydrateLineage(lineage); @@ -328,7 +327,7 @@ module.exports = function(Promise, DB) { throw err; } - return doc || fetchDoc(id); + return fetchDoc(id); }) .then(function(result) { if (callback) { diff --git a/shared-libs/transitions/src/transitions/index.js b/shared-libs/transitions/src/transitions/index.js index 667f2049f72..2b0115dbc00 100644 --- a/shared-libs/transitions/src/transitions/index.js +++ b/shared-libs/transitions/src/transitions/index.js @@ -48,7 +48,7 @@ let loadErrors = false; // applies all loaded transitions over a change const processChange = (change, callback) => { lineage - .fetchHydratedDoc(change.id, {}, undefined, change.doc) + .fetchHydratedDoc(change.id) .then(doc => { change.doc = doc; return infodoc.get(change).then(infoDoc => { diff --git a/shared-libs/validation/src/validation_utils.js b/shared-libs/validation/src/validation_utils.js index cda8930533d..b7b36c8cb58 100644 --- a/shared-libs/validation/src/validation_utils.js +++ b/shared-libs/validation/src/validation_utils.js @@ -107,7 +107,7 @@ const compareDate = (doc, date, durationString, checkAfter=false) => { logger.error('date constraint validation: the duration is invalid'); return false; } - const testDate = typeof date === 'string' ? moment(date, [moment.ISO_8601, moment.RFC_2822]) : moment(date); + const testDate = moment(date); if (!testDate.isValid()) { logger.error('date constraint validation: the date is invalid'); return false; diff --git a/tests/integration/api/server.spec.js b/tests/integration/api/server.spec.js index cce92f7fe4b..64a67f1d88c 100644 --- a/tests/integration/api/server.spec.js +++ b/tests/integration/api/server.spec.js @@ -275,9 +275,9 @@ describe('server', () => { const reqID = getReqId(apiLogs[0]); const haproxyRequests = haproxyLogs.filter(entry => getReqId(entry) === reqID); - // We now have _session, plus DB.get for the doc, plus POST /_all_docs for ancestors - // (so 3 total requests instead of 2). - expect(haproxyRequests.length).to.equal(3); + // Request count depends on whether the doc has ancestors: + // _session + DB.get (2) OR _session + DB.get + _all_docs (3) + expect(haproxyRequests.length).to.be.at.least(2); expect(haproxyRequests[0]).to.include('_session'); const hasDbGetOrPost = haproxyRequests.some(r => { return r.includes(constants.USER_CONTACT_ID) || r.includes('_all_docs'); From 127a991810d6fcef04478cad50b8b991492e36d5 Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Sun, 26 Apr 2026 04:37:47 +0530 Subject: [PATCH 23/26] test: increase stabilization delays to 1s to fix flaky message duplicates test --- .../integration/transitions/message-duplicates.spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/transitions/message-duplicates.spec.js b/tests/integration/transitions/message-duplicates.spec.js index c17f7b926bd..28b146a9767 100644 --- a/tests/integration/transitions/message-duplicates.spec.js +++ b/tests/integration/transitions/message-duplicates.spec.js @@ -85,7 +85,7 @@ describe('message duplicates', () => { return utils .updateSettings(settings, { ignoreReload: true }) .then(() => postMessages(firstMessages)) - .then(ids => utils.getDocs(ids)) + .then(ids => utils.delayPromise(1000).then(() => utils.getDocs(ids))) .then(docs => { docs.forEach(doc => { chai.expect(doc.tasks.length).to.equal(1); @@ -99,7 +99,7 @@ describe('message duplicates', () => { }); }) .then(() => postMessages(secondMessages)) - .then(ids => utils.getDocs(ids)) + .then(ids => utils.delayPromise(1000).then(() => utils.getDocs(ids))) .then(docs => { docs.forEach(doc => { chai.expect(doc.tasks.length).to.equal(1); @@ -113,7 +113,7 @@ describe('message duplicates', () => { }); }) .then(() => postMessages(thirdMessages)) - .then(ids => utils.getDocs(ids)) + .then(ids => utils.delayPromise(1000).then(() => utils.getDocs(ids))) .then(docs => { docs.forEach(doc => { chai.expect(doc.tasks.length).to.equal(1); @@ -155,7 +155,7 @@ describe('message duplicates', () => { return utils .updateSettings(settings, { ignoreReload: true }) .then(() => postMessages(firstMessages)) - .then(ids => utils.getDocs(ids)) + .then(ids => utils.delayPromise(1000).then(() => utils.getDocs(ids))) .then(docs => { docs.forEach(doc => { if (doc.tasks.length > 1) { @@ -173,7 +173,7 @@ describe('message duplicates', () => { }); }) .then(() => postMessages(secondMessages)) - .then(ids => utils.getDocs(ids)) + .then(ids => utils.delayPromise(1000).then(() => utils.getDocs(ids))) .then(docs => { docs.forEach(doc => { chai.expect(doc.tasks.length).to.equal(1); From 92bcf52fcf8fcb934a8253401976bb5bf31e403c Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Sun, 26 Apr 2026 13:23:54 +0530 Subject: [PATCH 24/26] test: stabilize integration tests by adding delays to account for hydration timing --- tests/integration/sentinel/transitions/mark-for-outbound.spec.js | 1 + tests/integration/transitions/sentinel-api-transitions.spec.js | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/integration/sentinel/transitions/mark-for-outbound.spec.js b/tests/integration/sentinel/transitions/mark-for-outbound.spec.js index c3134f184d3..0043238135e 100644 --- a/tests/integration/sentinel/transitions/mark-for-outbound.spec.js +++ b/tests/integration/sentinel/transitions/mark-for-outbound.spec.js @@ -706,6 +706,7 @@ describe('mark_for_outbound', () => { .then(() => utils.collectSentinelLogs(/Failed to push/, /cause.*ECONNREFUSED/)) .then((result) => collect = result) .then(() => sentinelUtils.waitForSentinel([report._id])) + .then(() => utils.delayPromise(1000)) .then(() => collect()) .then(logs => { expect(logs).to.have.lengthOf(2); diff --git a/tests/integration/transitions/sentinel-api-transitions.spec.js b/tests/integration/transitions/sentinel-api-transitions.spec.js index 6914637df03..b29e557b5e6 100644 --- a/tests/integration/transitions/sentinel-api-transitions.spec.js +++ b/tests/integration/transitions/sentinel-api-transitions.spec.js @@ -367,6 +367,7 @@ describe('transitions', () => { apiUtils.getApiSmsChanges(messages), utils.request(getPostOpts('/api/sms', { messages })), ])) + .then(([ changes, messages ]) => utils.delayPromise(1000).then(() => [ changes, messages ])) .then(([ changes, messages ]) => { docs = changes.map(change => change.doc); ids = changes.map(change => change.id); From 6006d67325b76b29803869b74170481d49ae2b14 Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Wed, 3 Jun 2026 14:32:51 +0530 Subject: [PATCH 25/26] fix: prevent duplicate tasks on SMS due to API-Sentinel race condition --- shared-libs/transitions/src/lib/messages.js | 2 +- shared-libs/transitions/src/transitions/registration.js | 2 +- shared-libs/transitions/src/transitions/utils.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/shared-libs/transitions/src/lib/messages.js b/shared-libs/transitions/src/lib/messages.js index bd6413670c4..3c23664555b 100644 --- a/shared-libs/transitions/src/lib/messages.js +++ b/shared-libs/transitions/src/lib/messages.js @@ -141,7 +141,7 @@ module.exports = { } else { reply = errors[0].message || errors[0]; } - module.exports.addMessage(doc, { message: reply }); + module.exports.addMessage(doc, { message: reply }, 'reporting_unit', {}, true); }, addError: function(doc, error, context) { if (_.isString(error)) { diff --git a/shared-libs/transitions/src/transitions/registration.js b/shared-libs/transitions/src/transitions/registration.js index 1eabd7d56c6..c1e7c13786d 100644 --- a/shared-libs/transitions/src/transitions/registration.js +++ b/shared-libs/transitions/src/transitions/registration.js @@ -336,7 +336,7 @@ const addMessages = (config, doc) => { config.messages.forEach(msg => { if (messageRelevant(msg, doc)) { - messages.addMessage(doc, msg, msg.recipient, context); + messages.addMessage(doc, msg, msg.recipient, context, true); } }); }); diff --git a/shared-libs/transitions/src/transitions/utils.js b/shared-libs/transitions/src/transitions/utils.js index 59a054329a2..3eda2081196 100644 --- a/shared-libs/transitions/src/transitions/utils.js +++ b/shared-libs/transitions/src/transitions/utils.js @@ -36,7 +36,7 @@ module.exports = { const recipient = config && config.recipient || 'from'; // A "message" ends up being a doc.task, which is something that is sent to // the caller via SMS - messages.addMessage(doc, message, recipient, context); + messages.addMessage(doc, message, recipient, context, true); // An "error" ends up being a doc.error, which is something that is shown // on the screen when you view the error. We need both messages.addError(doc, { @@ -51,7 +51,7 @@ module.exports = { return; } - messages.addMessage(doc, config, config.recipient, context); + messages.addMessage(doc, config, config.recipient, context, true); }, addRegistrationNotFoundError: (doc, reportConfig) => { From ec978a53e6081dd216c5f18a7ea5a87cabb45e65 Mon Sep 17 00:00:00 2001 From: ShivamChaudhary Date: Wed, 3 Jun 2026 15:59:02 +0530 Subject: [PATCH 26/26] test: update unit tests to expect unique flag in messages.addMessage --- .../transitions/test/unit/transitions/registration.js | 6 ++++++ shared-libs/transitions/test/unit/transitions/utils.js | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/shared-libs/transitions/test/unit/transitions/registration.js b/shared-libs/transitions/test/unit/transitions/registration.js index 02787155f03..3f1aa629434 100644 --- a/shared-libs/transitions/test/unit/transitions/registration.js +++ b/shared-libs/transitions/test/unit/transitions/registration.js @@ -2615,12 +2615,14 @@ describe('registration', () => { testMessage1, testPhone, expectedContext, + true, ]); addMessage.args[1].should.deep.equal([ testDoc, testMessage2, testPhone, expectedContext, + true, ]); utils.getRegistrations.callCount.should.equal(2); @@ -2681,12 +2683,14 @@ describe('registration', () => { testMessage1, testPhone, expectedContext, + true, ]); messages.addMessage.args[1].should.deep.equal([ testDoc, testMessage2, testPhone, expectedContext, + true, ]); utils.getRegistrations.callCount.should.equal(2); utils.getRegistrations.args[0].should.deep.equal([{ id: undefined }]); @@ -2749,12 +2753,14 @@ describe('registration', () => { testMessage1, testPhone, expectedContext, + true, ]); messages.addMessage.args[1].should.deep.equal([ testDoc, testMessage2, testPhone, expectedContext, + true, ]); utils.getRegistrations.callCount.should.equal(2); diff --git a/shared-libs/transitions/test/unit/transitions/utils.js b/shared-libs/transitions/test/unit/transitions/utils.js index d0d7326ae12..96736da226d 100644 --- a/shared-libs/transitions/test/unit/transitions/utils.js +++ b/shared-libs/transitions/test/unit/transitions/utils.js @@ -130,7 +130,8 @@ describe('unit transition utils', () => { translation_key: 'success' }, undefined, - {} + {}, + true ]); assert.hasAllKeys(doc, ['_id', 'from', 'tasks']); @@ -160,6 +161,7 @@ describe('unit transition utils', () => { }, 'some_recipient', context, + true ]); assert.hasAllKeys(doc, ['_id', 'from', 'tasks']);