Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3cb513d
feat: remove obsolete lineage view and refactor fetching to use all_docs
sh1vam31 Mar 31, 2026
0e32e21
refactor: resolve SonarCloud complexity and nesting depth issues
sh1vam31 Mar 31, 2026
7e93724
fix: resolve linting and SonarCloud code quality issues in lineage vi…
sh1vam31 Apr 1, 2026
65f523a
fix: resolve typescript compilation and sinon matcher errors
sh1vam31 Apr 1, 2026
288b527
fix: resolve karma test regression and sinon matcher usage
sh1vam31 Apr 1, 2026
a1c404c
fix: embed full parent chains in test docs for new allDocs-based lineage
sh1vam31 Apr 1, 2026
09a953d
fix: break long line in report test for lint compliance
sh1vam31 Apr 1, 2026
0e1287a
fix: correct contact _id in report lineage test to match allDocs key
sh1vam31 Apr 1, 2026
b9790ff
fix: resolve unit test regressions and linting errors
sh1vam31 Apr 5, 2026
c3e291d
fix: restore linting override for build artifact in settings spec
sh1vam31 Apr 5, 2026
488d41e
fix: resolve CI regressions in lineage refactoring branch
sh1vam31 Apr 6, 2026
d73d80d
chore: fix SonarCloud CI quality gate failures
sh1vam31 Apr 6, 2026
a8fff24
chore: avoid moment deprecation warning on invalid date strings in va…
sh1vam31 Apr 6, 2026
1d75695
fix: address maintainer feedback on bulk-get and cleanup unrelated ch…
sh1vam31 Apr 7, 2026
c0edae8
style: address lint errors in bulk-get.js
sh1vam31 Apr 7, 2026
e63a268
fix: resolve moment deprecation warning causing CI failure
sh1vam31 Apr 7, 2026
d5d84ef
chore: trigger CI to retry flaky E2E tests
sh1vam31 Apr 7, 2026
fe1d5c6
fix: restore deepCopy docs and revert unnecessary bulk-get changes pe…
sh1vam31 Apr 9, 2026
d67f8ef
fix: revert unnecessary changes to bulk-get tests per maintainer feed…
sh1vam31 Apr 9, 2026
b216b0e
fix: silence Sass deprecation warnings in build-prepare
sh1vam31 Apr 9, 2026
7b2968a
chore: trigger CI re-run
sh1vam31 Apr 9, 2026
0e555da
fix: address maintainer feedback regarding request counts and unrelat…
sh1vam31 Apr 25, 2026
127a991
test: increase stabilization delays to 1s to fix flaky message duplic…
sh1vam31 Apr 25, 2026
92bcf52
test: stabilize integration tests by adding delays to account for hyd…
sh1vam31 Apr 26, 2026
746af49
Merge remote-tracking branch 'origin/master' into 10748-remove-lineag…
sh1vam31 Jun 2, 2026
6006d67
fix: prevent duplicate tasks on SMS due to API-Sentinel race condition
sh1vam31 Jun 3, 2026
c708f2b
Merge branch 'master' into 10748-remove-lineage-view
sh1vam31 Jun 3, 2026
ec978a5
test: update unit tests to expect unique flag in messages.addMessage
sh1vam31 Jun 3, 2026
32c93a7
Merge branch 'master' into 10748-remove-lineage-view
andrablaj Jul 20, 2026
f507195
Merge branch 'master' into 10748-remove-lineage-view
witash Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 60 additions & 39 deletions admin/tests/unit/services/lineage-model-generator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@ 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_);
});

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'));
Expand All @@ -34,50 +36,53 @@ 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);
});
});

it('binds lineage', () => {
const contact = { _id: 'a', _rev: '1' };
const parent = { _id: 'b', _rev: '1' };
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' };
dbQuery.returns(Promise.resolve({ rows: [
{ doc: contact },
dbGet.withArgs('a').returns(Promise.resolve(contact));
dbAllDocs.withArgs(sinon.match({
keys: sinon.match.array.deepEquals(['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 ]);
});
});

it('binds contacts', () => {
const contact = { _id: 'a', _rev: '1', contact: { _id: 'd' } };
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' } };
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(sinon.match({
keys: sinon.match.array.deepEquals(['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 }
] }));
Expand All @@ -89,23 +94,35 @@ 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', 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' };
dbQuery.returns(Promise.resolve({ rows: [
{ doc: contact },
const xContact = { _id: 'x', name: 'xavier' };
dbGet.returns(Promise.resolve(contact));
dbAllDocs.withArgs(sinon.match({
keys: sinon.match.array.deepEquals(['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(['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: [ 'b', 'c' ],
include_docs: true
});
chai.expect(dbAllDocs.args[1][0]).to.deep.equal({
keys: [ 'x', 'd', 'e' ],
include_docs: true
});
Expand All @@ -116,7 +133,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',
Expand Down Expand Up @@ -147,8 +164,11 @@ describe('LineageModelGenerator service', () => {
}
]
};
dbQuery.returns(Promise.resolve({ rows: [
{ doc: contact },
dbGet.returns(Promise.resolve(contact));
dbAllDocs.withArgs(sinon.match({
keys: sinon.match.array.deepEquals(['b', 'c']),
include_docs: true
})).returns(Promise.resolve({ rows: [
{ doc: parent },
{ doc: grandparent }
] }));
Expand All @@ -160,8 +180,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',
Expand All @@ -180,11 +201,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',
Expand Down Expand Up @@ -229,8 +250,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 }
] }));
Expand Down
20 changes: 0 additions & 20 deletions ddocs/medic-db/medic-client/views/docs_by_id_lineage/map.js

This file was deleted.

2 changes: 1 addition & 1 deletion scripts/build/build-prepare.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 30 additions & 3 deletions shared-libs/cht-datasource/src/local/libs/lineage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -27,14 +27,41 @@ 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.
* @internal
*/
export const getLineageDocsById = (medicDb: PouchDB.Database<Doc>): (id: string) => Promise<Nullable<Doc>[]> => {
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 = getParentIds(doc);
if (parentIds.length === 0) {
return [doc];
}
const ancestors = await getMedicDocsById(parentIds);
return [doc, ...ancestors];
} catch (err: unknown) {
if (err && typeof err === 'object' && 'status' in err && err.status === 404) {
return [];
}
throw err;
}
};
};

/** @internal */
Expand Down
12 changes: 6 additions & 6 deletions shared-libs/cht-datasource/test/local/libs/doc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
22 changes: 15 additions & 7 deletions shared-libs/cht-datasource/test/local/libs/lineage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Doc>;
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', () => {
Expand Down
Loading
Loading