Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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.

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: unknown = 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 as PouchDB.Core.Error).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