Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
84bc7c0
feat: remove obsolete lineage view and refactor fetching to use all_docs
sh1vam31 Mar 31, 2026
4bf5916
refactor: resolve SonarCloud complexity and nesting depth issues
sh1vam31 Mar 31, 2026
f145b2f
Merge branch 'master' into 10748-remove-lineage-view
sh1vam31 Apr 1, 2026
a535ee8
Merge branch 'master' into 10748-remove-lineage-view
sh1vam31 Apr 1, 2026
967dc31
fix: resolve linting and SonarCloud code quality issues in lineage vi…
sh1vam31 Apr 1, 2026
f19175c
fix: resolve typescript compilation and sinon matcher errors
sh1vam31 Apr 1, 2026
0cedb38
fix: resolve karma test regression and sinon matcher usage
sh1vam31 Apr 1, 2026
e80d611
fix: embed full parent chains in test docs for new allDocs-based lineage
sh1vam31 Apr 1, 2026
856d450
fix: break long line in report test for lint compliance
sh1vam31 Apr 1, 2026
4aec1a3
fix: correct contact _id in report lineage test to match allDocs key
sh1vam31 Apr 1, 2026
b1ece2f
fix: resolve unit test regressions and linting errors
sh1vam31 Apr 5, 2026
3d04631
Merge branch 'master' into 10748-remove-lineage-view
sh1vam31 Apr 5, 2026
4f8491f
fix: restore linting override for build artifact in settings spec
sh1vam31 Apr 5, 2026
41c9139
fix: resolve CI regressions in lineage refactoring branch
sh1vam31 Apr 6, 2026
8e2e265
Merge branch 'master' into 10748-remove-lineage-view
sh1vam31 Apr 6, 2026
8032666
chore: fix SonarCloud CI quality gate failures
sh1vam31 Apr 6, 2026
2ff4aa7
chore: avoid moment deprecation warning on invalid date strings in va…
sh1vam31 Apr 6, 2026
66279a1
fix: address maintainer feedback on bulk-get and cleanup unrelated ch…
sh1vam31 Apr 7, 2026
88fbe04
style: address lint errors in bulk-get.js
sh1vam31 Apr 7, 2026
61e5e7d
fix: resolve moment deprecation warning causing CI failure
sh1vam31 Apr 7, 2026
a4045a6
chore: trigger CI to retry flaky E2E tests
sh1vam31 Apr 7, 2026
e2476bc
fix: restore deepCopy docs and revert unnecessary bulk-get changes pe…
sh1vam31 Apr 9, 2026
ec04295
Merge branch 'master' into 10748-remove-lineage-view
sh1vam31 Apr 9, 2026
e78e598
fix: revert unnecessary changes to bulk-get tests per maintainer feed…
sh1vam31 Apr 9, 2026
b72f084
fix: silence Sass deprecation warnings in build-prepare
sh1vam31 Apr 9, 2026
4194c20
Merge branch 'master' into 10748-remove-lineage-view
sh1vam31 Apr 9, 2026
0c9ec28
chore: trigger CI re-run
sh1vam31 Apr 9, 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 @@ -29,18 +29,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