diff --git a/admin/tests/unit/services/lineage-model-generator.spec.js b/admin/tests/unit/services/lineage-model-generator.spec.js index dd308a15294..fd1178822a6 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,21 @@ 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', 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 ]); @@ -67,17 +66,23 @@ 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', 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 } ] })); @@ -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 }); @@ -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', @@ -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 } ] })); @@ -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', @@ -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', @@ -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 } ] })); 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/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 diff --git a/shared-libs/cht-datasource/src/local/libs/lineage.ts b/shared-libs/cht-datasource/src/local/libs/lineage.ts index 5c53ab7e326..edf55ab551e 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,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): (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 = 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 */ 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 9312ef26632..895aee76bd9 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..f18329d5a04 100644 --- a/shared-libs/lineage/src/hydration.js +++ b/shared-libs/lineage/src/hydration.js @@ -46,58 +46,74 @@ const getContactIds = (contacts) => { return _.uniq(ids); }; -module.exports = function(Promise, DB) { - 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; +const assembleLineage = function(doc, parentIds, ancestors) { + const docsMap = new Map(); + ancestors.forEach(function(a) { + if (a?._id) { + docsMap.set(a._id, a); } + }); - const parentIds = extractParentIds(currentParent.parent); - lineage.forEach(function(l, i) { - currentParent.parent = l ? deepCopy(l) : { _id: parentIds[i] }; - currentParent = currentParent.parent; - }); + const result = [doc]; + parentIds.forEach(function(parentId) { + result.push(docsMap.get(parentId) || undefined); + }); + return result; +}; +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; + } + + const parentIds = extractParentIds(currentParent.parent); + lineage.forEach(function(l, i) { + currentParent.parent = l ? deepCopy(l) : { _id: parentIds[i] }; + currentParent = currentParent.parent; + }); + + return doc; +}; + +const fillContactsInDocs = function(docs, contacts) { + if (!contacts || !contacts.length) { + return; + } - const fillContactsInDocs = function(docs, contacts) { - if (!contacts || !contacts.length) { + docs.forEach(function(doc) { + if (!doc) { return; } + const id = utils.getId(doc.contact); + const contactDoc = getContactById(contacts, id); + if (contactDoc) { + doc.contact = deepCopy(contactDoc); + } - docs.forEach(function(doc) { - if (!doc) { - return; - } - const id = utils.getId(doc.contact); + 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.contact = deepCopy(contactDoc); - } - - if (!utils.validLinkedDocs(doc)) { - return; + doc.linked_docs[key] = deepCopy(contactDoc); } - - 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); @@ -229,16 +245,22 @@ 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) { + return assembleLineage(doc, parentIds, ancestors); }); + }) + .catch(function(err) { + if (err.status === 404) { + return []; + } + throw err; }); }; @@ -256,6 +278,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) { @@ -267,9 +306,6 @@ module.exports = function(Promise, DB) { }; const fetchHydratedDoc = function(id, options = {}, callback = undefined) { - let lineage; - let patientLineage; - let placeLineage; if (typeof options === 'function') { callback = options; options = {}; @@ -280,33 +316,18 @@ module.exports = function(Promise, DB) { }); return fetchLineageById(id) - .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 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 fetchDoc(id); }) .then(function(result) { if (callback) { @@ -323,6 +344,7 @@ module.exports = function(Promise, DB) { }); }; + // for data_records, include the first-level contact. const collectParentIds = function(docs) { const ids = []; diff --git a/shared-libs/lineage/test/hydration.spec.js b/shared-libs/lineage/test/hydration.spec.js index 6da39b4a5ef..f75e383a27a 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/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) => { 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']); diff --git a/tests/integration/api/server.spec.js b/tests/integration/api/server.spec.js index 1f8b2ee87f8..690303ca085 100644 --- a/tests/integration/api/server.spec.js +++ b/tests/integration/api/server.spec.js @@ -276,9 +276,14 @@ describe('server', () => { const reqID = getReqId(apiLogs[0]); const haproxyRequests = haproxyLogs.filter(entry => getReqId(entry) === reqID); - expect(haproxyRequests.length).to.equal(2); + // 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'); - expect(haproxyRequests[1]).to.include('_design/medic-client/_view/docs_by_id_lineage'); + const hasDbGetOrPost = haproxyRequests.some(r => { + return 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/tests/integration/sentinel/transitions/mark-for-outbound.spec.js b/tests/integration/sentinel/transitions/mark-for-outbound.spec.js index 6f5a1a60d0d..16ec89b9281 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((result) => collect = result) .then(() => utils.saveDoc(report)) .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/message-duplicates.spec.js b/tests/integration/transitions/message-duplicates.spec.js index 1cd22220635..8fe30fcb77a 100644 --- a/tests/integration/transitions/message-duplicates.spec.js +++ b/tests/integration/transitions/message-duplicates.spec.js @@ -86,7 +86,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, `doc: ${JSON.stringify(doc, null, 2)}`); @@ -100,7 +100,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, `doc: ${JSON.stringify(doc, null, 2)}`); @@ -114,7 +114,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, `doc: ${JSON.stringify(doc, null, 2)}`); @@ -156,7 +156,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, `doc: ${JSON.stringify(doc, null, 2)}`); @@ -170,7 +170,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, `doc: ${JSON.stringify(doc, null, 2)}`); diff --git a/tests/integration/transitions/sentinel-api-transitions.spec.js b/tests/integration/transitions/sentinel-api-transitions.spec.js index 0349059944d..1d68b2074c1 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); 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 302a7697d40..ed0b5af9176 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', parent: { _id: 'c' } } }; + 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', 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.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', 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.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.deepEquals(['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', parent: { _id: 'c' } } }; + 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 } @@ -326,28 +321,33 @@ 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 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: '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' }; 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: ['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({ - keys: [ 'x', 'y', 'e', 'f' ], + expect(dbAllDocs.callCount).to.equal(2); + expect(dbAllDocs.args[1][0]).to.deep.equal({ + keys: [ 'y', 'e', 'f' ], include_docs: true }); expect(model.doc.contact.parent.contact).to.deep.equal(parentContact); 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 cf88040d14e..00000000000 --- a/webapp/tests/mocha/unit/views/docs_by_id_lineage.spec.js +++ /dev/null @@ -1,198 +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: CONTACT_TYPES.CLINIC }; - const resultClinic = map(clinic, true); - expect(resultClinic.length).to.equal(1); - expect(resultClinic[0]).to.deep.equal({ key: [ CONTACT_TYPES.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' }}); - }); - }); -});