From 08e2b271184ca68d4ea07519938407b63663240b Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Mon, 15 Jul 2024 11:42:56 +0545 Subject: [PATCH 01/56] feat(#9237): Add functionality of getting people with pagination in cht-datasource --- shared-libs/cht-datasource/src/index.ts | 8 +++ .../cht-datasource/src/local/libs/doc.ts | 27 +++++--- .../cht-datasource/src/local/libs/lineage.ts | 7 +- .../cht-datasource/src/local/person.ts | 22 +++++- shared-libs/cht-datasource/src/local/place.ts | 2 +- shared-libs/cht-datasource/src/person.ts | 19 ++++++ .../cht-datasource/src/remote/person.ts | 9 +++ shared-libs/cht-datasource/test/index.spec.ts | 17 ++++- .../test/local/libs/doc.spec.ts | 61 +++++++++++++++-- .../test/local/libs/lineage.spec.ts | 6 +- .../cht-datasource/test/local/person.spec.ts | 67 +++++++++++++++++-- .../cht-datasource/test/local/place.spec.ts | 8 +-- .../cht-datasource/test/person.spec.ts | 32 +++++++++ .../cht-datasource/test/remote/person.spec.ts | 27 ++++++++ 14 files changed, 282 insertions(+), 30 deletions(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index 1ff1e485bc1..98d8b56da3e 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -85,6 +85,14 @@ export const getDatasource = (ctx: DataContext) => { * @throws Error if no UUID is provided */ getByUuidWithLineage: (uuid: string) => ctx.bind(Person.v1.getWithLineage)(Qualifier.byUuid(uuid)), + + /** + * Returns a list of people. + * @param limit the total number of records to retrieve + * @param skip the total number of records to skip + * @returns array of `Person` + */ + getPage: (limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)(limit, skip), } } }; diff --git a/shared-libs/cht-datasource/src/local/libs/doc.ts b/shared-libs/cht-datasource/src/local/libs/doc.ts index 37417ba6808..b89b965a67e 100644 --- a/shared-libs/cht-datasource/src/local/libs/doc.ts +++ b/shared-libs/cht-datasource/src/local/libs/doc.ts @@ -28,14 +28,25 @@ export const getDocsByIds = (db: PouchDB.Database) => async (uuids: string[ }; /** @internal */ -export const queryDocsByKey = ( +const queryDocs = (db: PouchDB.Database, view: string, options: PouchDB.Query.Options) => db + .query(view, {...options}) + .then(({ rows }) => rows.map(({ doc }) => isDoc(doc) ? doc : null)); + +/** @internal */ +export const queryDocsByRange = ( db: PouchDB.Database, view: string -) => async (key: string): Promise[]> => db - .query(view, { - startkey: [key], - endkey: [key, {}], - include_docs: true - }) - .then(({ rows }) => rows.map(({ doc }) => isDoc(doc) ? doc : null)); +) => async ( + startkey: unknown, + endkey: unknown +): Promise[]> => queryDocs(db, view, { include_docs: true, startkey: [startkey], endkey: [endkey, {}]}); +/** @internal */ +export const queryDocsByKey = ( + db: PouchDB.Database, + view: string +) => async ( + key: unknown, + limit: number, + skip: number +): Promise[]> => queryDocs(db, view, { key: [key], include_docs: true, limit, skip }); diff --git a/shared-libs/cht-datasource/src/local/libs/lineage.ts b/shared-libs/cht-datasource/src/local/libs/lineage.ts index c23bb40ab69..b62e7abeda1 100644 --- a/shared-libs/cht-datasource/src/local/libs/lineage.ts +++ b/shared-libs/cht-datasource/src/local/libs/lineage.ts @@ -10,7 +10,7 @@ import { Nullable } from '../../libs/core'; import { Doc } from '../../libs/doc'; -import { queryDocsByKey } from './doc'; +import { queryDocsByRange } from './doc'; import logger from '@medic/logger'; /** @@ -20,7 +20,10 @@ import logger from '@medic/logger'; */ export const getLineageDocsById = ( medicDb: PouchDB.Database -): (id: string) => Promise[]> => queryDocsByKey(medicDb, 'medic-client/docs_by_id_lineage'); +): ( + startkey: unknown, + endkey: unknown +) => Promise[]> => queryDocsByRange(medicDb, 'medic-client/docs_by_id_lineage'); /** @internal */ export const getPrimaryContactIds = (places: NonEmptyArray>): string[] => places diff --git a/shared-libs/cht-datasource/src/local/person.ts b/shared-libs/cht-datasource/src/local/person.ts index 2f23d6d5170..badea1a08f4 100644 --- a/shared-libs/cht-datasource/src/local/person.ts +++ b/shared-libs/cht-datasource/src/local/person.ts @@ -3,7 +3,7 @@ import contactTypeUtils from '@medic/contact-types-utils'; import { deepCopy, isNonEmptyArray, Nullable } from '../libs/core'; import { UuidQualifier } from '../qualifier'; import * as Person from '../person'; -import { getDocById, getDocsByIds } from './libs/doc'; +import { getDocById, getDocsByIds, queryDocsByKey } from './libs/doc'; import { LocalDataContext, SettingsService } from './libs/data-context'; import logger from '@medic/logger'; import { getLineageDocsById, getPrimaryContactIds, hydrateLineage, hydratePrimaryContact } from './libs/lineage'; @@ -40,7 +40,7 @@ export namespace v1 { const getLineageDocs = getLineageDocsById(medicDb); const getMedicDocsById = getDocsByIds(medicDb); return async (identifier: UuidQualifier): Promise> => { - const [person, ...lineagePlaces] = await getLineageDocs(identifier.uuid); + const [person, ...lineagePlaces] = await getLineageDocs(identifier.uuid, identifier.uuid); if (!isPerson(settings, identifier.uuid, person)) { return null; } @@ -58,4 +58,22 @@ export namespace v1 { return deepCopy(personWithLineage); }; }; + + /** @internal */ + export const getPage = ({ medicDb, settings }: LocalDataContext) => { + const personIdentifierRecord = contactTypeUtils.getPersonTypes(settings.getAll()); + + let personIdentifier: string; + if (Object.entries(personIdentifierRecord).length > 0) { + personIdentifier = personIdentifierRecord[0]?.id as string; + } else { + throw new Error('Person type not found'); + } + + const getDocsByPage = queryDocsByKey(medicDb, 'medic-client/contacts_by_type'); + + return async (limit: number, skip: number): Promise[]> => { + return await getDocsByPage(personIdentifier, limit, skip); + }; + }; } diff --git a/shared-libs/cht-datasource/src/local/place.ts b/shared-libs/cht-datasource/src/local/place.ts index 9cd38be30e7..e3f321b8b94 100644 --- a/shared-libs/cht-datasource/src/local/place.ts +++ b/shared-libs/cht-datasource/src/local/place.ts @@ -39,7 +39,7 @@ export namespace v1 { const getLineageDocs = getLineageDocsById(medicDb); const getMedicDocsById = getDocsByIds(medicDb); return async (identifier: UuidQualifier): Promise> => { - const [place, ...lineagePlaces] = await getLineageDocs(identifier.uuid); + const [place, ...lineagePlaces] = await getLineageDocs(identifier.uuid, identifier.uuid); if (!isPlace(settings, identifier.uuid, place)) { return null; } diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 9f1fd64c9f6..6ead30ea359 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -44,6 +44,18 @@ export namespace v1 { }; }; + const getPeople = ( + localFn: (c: LocalDataContext) => (limit: number, skip: number) => Promise, + remoteFn: (c: RemoteDataContext) => (limit: number, skip: number) => Promise + ) => (context: DataContext) => { + assertDataContext(context); + const fn = adapt(context, localFn, remoteFn); + + return async (limit = 100, skip = 0): Promise => { + return fn(limit, skip); + }; + }; + /** * Returns a person for the given qualifier. * @param context the current data context @@ -59,4 +71,11 @@ export namespace v1 { * @throws Error if the provided context or qualifier is invalid */ export const getWithLineage = getPerson(Local.Person.v1.getWithLineage, Remote.Person.v1.getWithLineage); + + /** + * Returns an array of people. + * @param context the current data context + * @returns an array of people + */ + export const getPage = getPeople(Local.Person.v1.getPage, Remote.Person.v1.getPage); } diff --git a/shared-libs/cht-datasource/src/remote/person.ts b/shared-libs/cht-datasource/src/remote/person.ts index 23544a5378d..41ced68b731 100644 --- a/shared-libs/cht-datasource/src/remote/person.ts +++ b/shared-libs/cht-datasource/src/remote/person.ts @@ -19,4 +19,13 @@ export namespace v1 { identifier.uuid, { with_lineage: 'true' } ); + + /** @internal */ + export const getPage = (remoteContext: RemoteDataContext) => ( + limit: number, + skip: number + ): Promise => getPerson(remoteContext)( + '', + {'limit': limit.toString(), 'skip': skip.toString()} + ); } diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index b53f03a7686..fc03ef8fdb8 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -7,6 +7,7 @@ import * as Qualifier from '../src/qualifier'; import sinon, { SinonStub } from 'sinon'; import * as Context from '../src/libs/data-context'; import { DataContext } from '../src'; +import { Doc } from '../src/libs/doc'; describe('CHT Script API - getDatasource', () => { let dataContext: DataContext; @@ -92,7 +93,7 @@ describe('CHT Script API - getDatasource', () => { beforeEach(() => person = v1.person); it('contains expected keys', () => { - expect(person).to.have.all.keys(['getByUuid', 'getByUuidWithLineage']); + expect(person).to.have.all.keys(['getByUuid', 'getByUuidWithLineage', 'getPage']); }); it('getByUuid', async () => { @@ -124,6 +125,20 @@ describe('CHT Script API - getDatasource', () => { expect(personGet.calledOnceWithExactly(qualifier)).to.be.true; expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; }); + + it('getPage', async () => { + const expectedPeople: Index.Nullable[] = []; + const personGetPage = sinon.stub().resolves(expectedPeople); + dataContextBind.returns(personGetPage); + const limit = 2; + const skip = 1; + + const returnedPeople = await person.getPage(limit, skip); + + expect(returnedPeople).to.equal(expectedPeople); + expect(dataContextBind.calledOnceWithExactly(Person.v1.getPage)).to.be.true; + expect(personGetPage.calledOnceWithExactly(limit, skip)).to.be.true; + }); }); }); }); 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 640237a1165..d1c2d0718e0 100644 --- a/shared-libs/cht-datasource/test/local/libs/doc.spec.ts +++ b/shared-libs/cht-datasource/test/local/libs/doc.spec.ts @@ -1,7 +1,7 @@ import * as Doc from '../../../src/libs/doc'; import sinon, { SinonStub } from 'sinon'; import logger from '@medic/logger'; -import { getDocById, getDocsByIds, queryDocsByKey } from '../../../src/local/libs/doc'; +import { getDocById, getDocsByIds, queryDocsByKey, queryDocsByRange } from '../../../src/local/libs/doc'; import { expect } from 'chai'; describe('local doc lib', () => { @@ -149,7 +149,7 @@ describe('local doc lib', () => { }); }); - describe('queryDocsByKey', () => { + describe('queryDocsByRange', () => { it('returns lineage docs for the given id', async () => { const doc0 = { _id: 'doc0' }; const doc1 = { _id: 'doc1' }; @@ -163,7 +163,7 @@ describe('local doc lib', () => { }); isDoc.returns(true); - const result = await queryDocsByKey(db, 'medic-client/docs_by_id_lineage')(doc0._id); + const result = await queryDocsByRange(db, 'medic-client/docs_by_id_lineage')(doc0._id, doc0._id); expect(result).to.deep.equal([doc0, doc1, doc2]); expect(dbQuery.calledOnceWithExactly('medic-client/docs_by_id_lineage', { @@ -186,7 +186,7 @@ describe('local doc lib', () => { }); isDoc.returns(true); - const result = await queryDocsByKey(db, 'medic-client/docs_by_id_lineage')(doc0._id); + const result = await queryDocsByRange(db, 'medic-client/docs_by_id_lineage')(doc0._id, doc0._id); expect(result).to.deep.equal([doc0, null, doc2]); expect(dbQuery.calledOnceWithExactly('medic-client/docs_by_id_lineage', { @@ -204,7 +204,7 @@ describe('local doc lib', () => { }); isDoc.returns(false); - const result = await queryDocsByKey(db, 'medic-client/docs_by_id_lineage')(doc0._id); + const result = await queryDocsByRange(db, 'medic-client/docs_by_id_lineage')(doc0._id, doc0._id); expect(result).to.deep.equal([null]); expect(dbQuery.calledOnceWithExactly('medic-client/docs_by_id_lineage', { @@ -215,4 +215,55 @@ describe('local doc lib', () => { expect(isDoc.calledOnceWithExactly(doc0)).to.be.true; }); }); + + describe('queryDocsByKey', () => { + const limit = 100; + const skip = 0; + const contactType = 'person'; + + it('returns docs on the basis of given key in pages', async () => { + const doc0 = { _id: 'doc0' }; + const doc1 = { _id: 'doc1' }; + const doc2 = { _id: 'doc2' }; + + dbQuery.resolves({ + rows: [ + { doc: doc0 }, + { doc: doc1 }, + { doc: doc2 } + ] + }); + isDoc.returns(true); + + const result = await queryDocsByKey(db, 'medic-client/contacts_by_type')(contactType, limit, skip); + + expect(result).to.deep.equal([doc0, doc1, doc2]); + expect(dbQuery.calledOnceWithExactly('medic-client/contacts_by_type', { + key: [contactType], + include_docs: true, + limit, + skip + })).to.be.true; + expect(isDoc.args).to.deep.equal([[doc0], [doc1], [doc2]]); + }); + + it('returns empty array if docs are not found', async () => { + dbQuery.resolves({ + rows: [ + ] + }); + isDoc.returns(true); + + const result = await queryDocsByKey(db, 'medic-client/contacts_by_type')(contactType, limit, skip); + + expect(result).to.deep.equal([]); + expect(dbQuery.calledOnceWithExactly('medic-client/contacts_by_type', { + key: [contactType], + include_docs: true, + limit, + skip + })).to.be.true; + expect(isDoc.args).to.deep.equal([]); + }); + }); }); 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 20adcd0a0cd..1326bdf5ca2 100644 --- a/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts +++ b/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts @@ -21,15 +21,15 @@ describe('local lineage lib', () => { it('getLineageDocsById', () => { const queryFn = sinon.stub(); - const queryDocsByKey = sinon - .stub(LocalDoc, 'queryDocsByKey') + const queryDocsByRange = sinon + .stub(LocalDoc, 'queryDocsByRange') .returns(queryFn); const medicDb = { hello: 'world' } as unknown as PouchDB.Database; const result = getLineageDocsById(medicDb); expect(result).to.equal(queryFn); - expect(queryDocsByKey.calledOnceWithExactly(medicDb, 'medic-client/docs_by_id_lineage')).to.be.true; + expect(queryDocsByRange.calledOnceWithExactly(medicDb, 'medic-client/docs_by_id_lineage')).to.be.true; }); describe('getPrimaryContactIds', () => { diff --git a/shared-libs/cht-datasource/test/local/person.spec.ts b/shared-libs/cht-datasource/test/local/person.spec.ts index 22d3c3b1e51..183e5ed3b2e 100644 --- a/shared-libs/cht-datasource/test/local/person.spec.ts +++ b/shared-libs/cht-datasource/test/local/person.spec.ts @@ -146,7 +146,7 @@ describe('local person', () => { const result = await Person.v1.getWithLineage(localContext)(identifier); expect(result).to.equal(copiedPerson); - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; expect(isPerson.calledOnceWithExactly(settings, person)).to.be.true; expect(warn.notCalled).to.be.true; expect(debug.notCalled).to.be.true; @@ -167,7 +167,7 @@ describe('local person', () => { const result = await Person.v1.getWithLineage(localContext)(identifier); expect(result).to.be.null; - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; expect(isPerson.notCalled).to.be.true; expect(warn.calledOnceWithExactly(`No person found for identifier [${identifier.uuid}].`)).to.be.true; expect(debug.notCalled).to.be.true; @@ -191,7 +191,7 @@ describe('local person', () => { const result = await Person.v1.getWithLineage(localContext)(identifier); expect(result).to.be.null; - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; expect(isPerson.calledOnceWithExactly(settings, person)).to.be.true; expect(warn.calledOnceWithExactly(`Document [${identifier.uuid}] is not a valid person.`)).to.be.true; expect(debug.notCalled).to.be.true; @@ -212,7 +212,7 @@ describe('local person', () => { const result = await Person.v1.getWithLineage(localContext)(identifier); expect(result).to.equal(person); - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; expect(isPerson.calledOnceWithExactly(settings, person)).to.be.true; expect(warn.notCalled).to.be.true; expect(debug.calledOnceWithExactly(`No lineage places found for person [${identifier.uuid}].`)).to.be.true; @@ -224,5 +224,64 @@ describe('local person', () => { expect(deepCopy.notCalled).to.be.true; }); }); + + describe('getPage', () => { + const limit = 3; + const skip = 0; + const personIdentifier = 'person'; + const personType = [{person: true, id: personIdentifier}] as Record[]; + let getPersonTypes: SinonStub; + let queryDocsByKeyInner: SinonStub; + let queryDocsByKeyOuter: SinonStub; + + beforeEach(() => { + queryDocsByKeyInner = sinon.stub(); + queryDocsByKeyOuter = sinon.stub(LocalDoc, 'queryDocsByKey').returns(queryDocsByKeyInner); + getPersonTypes = sinon.stub(contactTypeUtils, 'getPersonTypes').returns(personType); + settingsGetAll.returns(settings); + + }); + it('returns a page of people', async () => { + const doc = { type: 'person'}; + const docs = [doc, doc, doc]; + queryDocsByKeyInner.resolves(docs); + + const res = await Person.v1.getPage(localContext)(limit, skip); + + expect(res).to.equal(docs); + expect(settingsGetAll.calledOnce).to.be.true; + expect(getPersonTypes.calledOnceWithExactly(settings)).to.be.true; + expect( + queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') + ).to.be.true; + expect(queryDocsByKeyInner.calledOnceWithExactly(personIdentifier, limit, skip)).to.be.true; + }); + + it('returns empty array if person identifier does not exist', () => { + getPersonTypes.returns({}); + queryDocsByKeyInner.resolves([]); + + expect(() => Person.v1.getPage(localContext)(limit, skip)).to.throw('Person type not found'); + + expect(settingsGetAll.calledOnce).to.be.true; + expect(getPersonTypes.calledOnceWithExactly(settings)).to.be.true; + expect(queryDocsByKeyOuter.notCalled).to.be.true; + expect(queryDocsByKeyInner.notCalled).to.be.true; + }); + + it('returns empty array if people does not exist', async () => { + queryDocsByKeyInner.resolves([]); + + const res = await Person.v1.getPage(localContext)(limit, skip); + + expect(res).to.deep.equal([]); + expect(settingsGetAll.calledOnce).to.be.true; + expect(getPersonTypes.calledOnceWithExactly(settings)).to.be.true; + expect( + queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') + ).to.be.true; + expect(queryDocsByKeyInner.calledOnceWithExactly(personIdentifier, limit, skip)).to.be.true; + }); + }); }); }); diff --git a/shared-libs/cht-datasource/test/local/place.spec.ts b/shared-libs/cht-datasource/test/local/place.spec.ts index da8c9c50c61..2b08b36a16d 100644 --- a/shared-libs/cht-datasource/test/local/place.spec.ts +++ b/shared-libs/cht-datasource/test/local/place.spec.ts @@ -145,7 +145,7 @@ describe('local place', () => { const result = await Place.v1.getWithLineage(localContext)(identifier); expect(result).to.equal(copiedPlace); - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; expect(isPlace.calledOnceWithExactly(settings, place0)).to.be.true; expect(warn.notCalled).to.be.true; expect(debug.notCalled).to.be.true; @@ -166,7 +166,7 @@ describe('local place', () => { const result = await Place.v1.getWithLineage(localContext)(identifier); expect(result).to.be.null; - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; expect(isPlace.notCalled).to.be.true; expect(warn.calledOnceWithExactly(`No place found for identifier [${identifier.uuid}].`)).to.be.true; expect(debug.notCalled).to.be.true; @@ -189,7 +189,7 @@ describe('local place', () => { const result = await Place.v1.getWithLineage(localContext)(identifier); expect(result).to.be.null; - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; expect(isPlace.calledOnceWithExactly(settings, place0)).to.be.true; expect(warn.calledOnceWithExactly(`Document [${identifier.uuid}] is not a valid place.`)).to.be.true; expect(debug.notCalled).to.be.true; @@ -210,7 +210,7 @@ describe('local place', () => { const result = await Place.v1.getWithLineage(localContext)(identifier); expect(result).to.equal(place); - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; expect(isPlace.calledOnceWithExactly(settings, place)).to.be.true; expect(warn.notCalled).to.be.true; expect(debug.calledOnceWithExactly(`No lineage places found for place [${identifier.uuid}].`)).to.be.true; diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index 5dd3f0c1f2a..c474e1096fc 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -123,5 +123,37 @@ describe('person', () => { expect(getPersonWithLineage.notCalled).to.be.true; }); }); + + describe('getPage', () => { + const people = [{ _id: 'person1' }, { _id: 'person2' }, { _id: 'person3' }] as Person.v1.Person[]; + const limit = 3; + const skip = 1; + let getPage: SinonStub; + + beforeEach(() => { + getPage = sinon.stub(); + adapt.returns(getPage); + }); + + it('retrieves people from the data context', async () => { + getPage.resolves(people); + + const result = await Person.v1.getPage(dataContext)(limit, skip); + + expect(result).to.equal(people); + expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; + expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; + }); + + it('throws an error if the data context is invalid', () => { + assertDataContext.throws(new Error(`Invalid data context [null].`)); + + expect(() => Person.v1.getPage(dataContext)).to.throw(`Invalid data context [null].`); + + expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; + expect(adapt.notCalled).to.be.true; + expect(getPage.notCalled).to.be.true; + }); + }); }); }); diff --git a/shared-libs/cht-datasource/test/remote/person.spec.ts b/shared-libs/cht-datasource/test/remote/person.spec.ts index 41ac0a4b0b0..2f33c2516ff 100644 --- a/shared-libs/cht-datasource/test/remote/person.spec.ts +++ b/shared-libs/cht-datasource/test/remote/person.spec.ts @@ -64,5 +64,32 @@ describe('remote person', () => { expect(getResourceInner.calledOnceWithExactly(identifier.uuid, { with_lineage: 'true' })).to.be.true; }); }); + + describe('getPage', () => { + const limit = 3; + const skip = 1; + const queryParam = {limit: limit.toString(), skip: skip.toString()}; + + it('returns people', async () => { + const doc = [{ type: 'person' }, {type: 'person'}]; + getResourceInner.resolves(doc); + + const result = await Person.v1.getPage(remoteContext)(limit, skip); + + expect(result).to.equal(doc); + expect(getResourceOuter.calledOnceWithExactly(remoteContext, 'api/v1/person')).to.be.true; + expect(getResourceInner.calledOnceWithExactly('', queryParam)).to.be.true; + }); + + it('returns empty array if docs are not found', async () => { + getResourceInner.resolves([]); + + const result = await Person.v1.getPage(remoteContext)(limit, skip); + + expect(result).to.deep.equal([]); + expect(getResourceOuter.calledOnceWithExactly(remoteContext, 'api/v1/person')).to.be.true; + expect(getResourceInner.calledOnceWithExactly('', queryParam)).to.be.true; + }); + }); }); }); From ab92f0b1bf197b6519d2af2e0d26d39f4493ea5e Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Wed, 17 Jul 2024 11:13:50 +0545 Subject: [PATCH 02/56] Update shared-libs/cht-datasource/src/local/libs/doc.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/local/libs/doc.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/shared-libs/cht-datasource/src/local/libs/doc.ts b/shared-libs/cht-datasource/src/local/libs/doc.ts index b89b965a67e..30c51509290 100644 --- a/shared-libs/cht-datasource/src/local/libs/doc.ts +++ b/shared-libs/cht-datasource/src/local/libs/doc.ts @@ -27,7 +27,6 @@ export const getDocsByIds = (db: PouchDB.Database) => async (uuids: string[ .filter((doc): doc is Doc => isDoc(doc)); }; -/** @internal */ const queryDocs = (db: PouchDB.Database, view: string, options: PouchDB.Query.Options) => db .query(view, {...options}) .then(({ rows }) => rows.map(({ doc }) => isDoc(doc) ? doc : null)); From de63197bb8545bcd07492d8f30cf691f7b6ddf02 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Fri, 19 Jul 2024 16:15:03 +0545 Subject: [PATCH 03/56] feat(#9237): Address PR comments --- shared-libs/cht-datasource/src/index.ts | 5 ++- .../cht-datasource/src/local/libs/doc.ts | 4 +-- .../cht-datasource/src/local/libs/lineage.ts | 8 +++-- .../cht-datasource/src/local/person.ts | 24 ++++++------- shared-libs/cht-datasource/src/local/place.ts | 2 +- shared-libs/cht-datasource/src/person.ts | 20 ++++++++--- shared-libs/cht-datasource/src/qualifier.ts | 28 +++++++++++++++ .../src/remote/libs/data-context.ts | 18 ++++++++++ .../cht-datasource/src/remote/person.ts | 12 ++++--- shared-libs/cht-datasource/test/index.spec.ts | 8 +++-- .../test/local/libs/doc.spec.ts | 19 +++++----- .../test/local/libs/lineage.spec.ts | 11 +++--- .../cht-datasource/test/local/person.spec.ts | 36 +++++++++++-------- .../cht-datasource/test/local/place.spec.ts | 8 ++--- .../cht-datasource/test/person.spec.ts | 20 ++++++++++- .../cht-datasource/test/qualifier.spec.ts | 34 +++++++++++++++++- .../cht-datasource/test/remote/person.spec.ts | 27 +++++++++----- 17 files changed, 210 insertions(+), 74 deletions(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index 98d8b56da3e..56e3507868d 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -88,11 +88,14 @@ export const getDatasource = (ctx: DataContext) => { /** * Returns a list of people. + * @param personType the string that represents the person type * @param limit the total number of records to retrieve * @param skip the total number of records to skip * @returns array of `Person` */ - getPage: (limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)(limit, skip), + getPage: (personType: string, limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)( + Qualifier.byContactType(personType), limit, skip + ), } } }; diff --git a/shared-libs/cht-datasource/src/local/libs/doc.ts b/shared-libs/cht-datasource/src/local/libs/doc.ts index 30c51509290..a4c5181f468 100644 --- a/shared-libs/cht-datasource/src/local/libs/doc.ts +++ b/shared-libs/cht-datasource/src/local/libs/doc.ts @@ -38,7 +38,7 @@ export const queryDocsByRange = ( ) => async ( startkey: unknown, endkey: unknown -): Promise[]> => queryDocs(db, view, { include_docs: true, startkey: [startkey], endkey: [endkey, {}]}); +): Promise[]> => queryDocs(db, view, { include_docs: true, startkey, endkey}); /** @internal */ export const queryDocsByKey = ( @@ -48,4 +48,4 @@ export const queryDocsByKey = ( key: unknown, limit: number, skip: number -): Promise[]> => queryDocs(db, view, { key: [key], include_docs: true, limit, skip }); +): Promise[]> => queryDocs(db, view, { include_docs: true, key, limit, skip }); diff --git a/shared-libs/cht-datasource/src/local/libs/lineage.ts b/shared-libs/cht-datasource/src/local/libs/lineage.ts index b62e7abeda1..431408442e9 100644 --- a/shared-libs/cht-datasource/src/local/libs/lineage.ts +++ b/shared-libs/cht-datasource/src/local/libs/lineage.ts @@ -21,9 +21,11 @@ import logger from '@medic/logger'; export const getLineageDocsById = ( medicDb: PouchDB.Database ): ( - startkey: unknown, - endkey: unknown -) => Promise[]> => queryDocsByRange(medicDb, 'medic-client/docs_by_id_lineage'); + id: string +) => Promise[]> => { + const fn = queryDocsByRange(medicDb, 'medic-client/docs_by_id_lineage'); + return (id: string) => fn([id], [id, {}]); +}; /** @internal */ export const getPrimaryContactIds = (places: NonEmptyArray>): string[] => places diff --git a/shared-libs/cht-datasource/src/local/person.ts b/shared-libs/cht-datasource/src/local/person.ts index badea1a08f4..1e4e7cb6be5 100644 --- a/shared-libs/cht-datasource/src/local/person.ts +++ b/shared-libs/cht-datasource/src/local/person.ts @@ -1,7 +1,7 @@ import { Doc } from '../libs/doc'; import contactTypeUtils from '@medic/contact-types-utils'; import { deepCopy, isNonEmptyArray, Nullable } from '../libs/core'; -import { UuidQualifier } from '../qualifier'; +import { ContactTypeQualifier, UuidQualifier } from '../qualifier'; import * as Person from '../person'; import { getDocById, getDocsByIds, queryDocsByKey } from './libs/doc'; import { LocalDataContext, SettingsService } from './libs/data-context'; @@ -40,7 +40,7 @@ export namespace v1 { const getLineageDocs = getLineageDocsById(medicDb); const getMedicDocsById = getDocsByIds(medicDb); return async (identifier: UuidQualifier): Promise> => { - const [person, ...lineagePlaces] = await getLineageDocs(identifier.uuid, identifier.uuid); + const [person, ...lineagePlaces] = await getLineageDocs(identifier.uuid); if (!isPerson(settings, identifier.uuid, person)) { return null; } @@ -61,19 +61,19 @@ export namespace v1 { /** @internal */ export const getPage = ({ medicDb, settings }: LocalDataContext) => { - const personIdentifierRecord = contactTypeUtils.getPersonTypes(settings.getAll()); - - let personIdentifier: string; - if (Object.entries(personIdentifierRecord).length > 0) { - personIdentifier = personIdentifierRecord[0]?.id as string; - } else { - throw new Error('Person type not found'); - } + const personTypes = contactTypeUtils.getPersonTypes(settings.getAll()); + const personTypesIds = personTypes.map(item => item.id); const getDocsByPage = queryDocsByKey(medicDb, 'medic-client/contacts_by_type'); - return async (limit: number, skip: number): Promise[]> => { - return await getDocsByPage(personIdentifier, limit, skip); + return async (personType: ContactTypeQualifier, limit: number, skip: number): Promise => { + if (!personTypesIds.includes(personType.contactType)) { + throw new Error(`Invalid person type: ${personType.contactType}`); + } + + const docs = await getDocsByPage([personType.contactType], limit, skip); + + return docs.filter((doc): doc is Person.v1.Person => isPerson(settings, doc?._id ?? '', doc)); }; }; } diff --git a/shared-libs/cht-datasource/src/local/place.ts b/shared-libs/cht-datasource/src/local/place.ts index e3f321b8b94..9cd38be30e7 100644 --- a/shared-libs/cht-datasource/src/local/place.ts +++ b/shared-libs/cht-datasource/src/local/place.ts @@ -39,7 +39,7 @@ export namespace v1 { const getLineageDocs = getLineageDocsById(medicDb); const getMedicDocsById = getDocsByIds(medicDb); return async (identifier: UuidQualifier): Promise> => { - const [place, ...lineagePlaces] = await getLineageDocs(identifier.uuid, identifier.uuid); + const [place, ...lineagePlaces] = await getLineageDocs(identifier.uuid); if (!isPlace(settings, identifier.uuid, place)) { return null; } diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 6ead30ea359..0027b65d5d7 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -1,4 +1,4 @@ -import { isUuidQualifier, UuidQualifier } from './qualifier'; +import { isContactTypeQualifier, isUuidQualifier, ContactTypeQualifier, UuidQualifier } from './qualifier'; import { adapt, assertDataContext, DataContext } from './libs/data-context'; import { Contact, NormalizedParent } from './libs/contact'; import * as Remote from './remote'; @@ -32,6 +32,14 @@ export namespace v1 { } }; + const assertTypeQualifier: ( + qualifier: unknown + ) => asserts qualifier is ContactTypeQualifier = (qualifier: unknown) => { + if (!isContactTypeQualifier(qualifier)) { + throw new Error(`Invalid type [${JSON.stringify(qualifier)}].`); + } + }; + const getPerson = ( localFn: (c: LocalDataContext) => (qualifier: UuidQualifier) => Promise, remoteFn: (c: RemoteDataContext) => (qualifier: UuidQualifier) => Promise @@ -45,14 +53,15 @@ export namespace v1 { }; const getPeople = ( - localFn: (c: LocalDataContext) => (limit: number, skip: number) => Promise, - remoteFn: (c: RemoteDataContext) => (limit: number, skip: number) => Promise + localFn: (c: LocalDataContext) => (personType: ContactTypeQualifier, limit: number, skip: number) => Promise, + remoteFn: (c: RemoteDataContext) => (personType: ContactTypeQualifier, limit: number, skip: number) => Promise ) => (context: DataContext) => { assertDataContext(context); const fn = adapt(context, localFn, remoteFn); - return async (limit = 100, skip = 0): Promise => { - return fn(limit, skip); + return async (personType: ContactTypeQualifier, limit = 100, skip = 0): Promise => { + assertTypeQualifier(personType); + return fn(personType, limit, skip); }; }; @@ -76,6 +85,7 @@ export namespace v1 { * Returns an array of people. * @param context the current data context * @returns an array of people + * @throws Error if the provided context or personType qualifier is invalid */ export const getPage = getPeople(Local.Person.v1.getPage, Remote.Person.v1.getPage); } diff --git a/shared-libs/cht-datasource/src/qualifier.ts b/shared-libs/cht-datasource/src/qualifier.ts index 5b37a18edf5..a7149344c2a 100644 --- a/shared-libs/cht-datasource/src/qualifier.ts +++ b/shared-libs/cht-datasource/src/qualifier.ts @@ -27,3 +27,31 @@ export const byUuid = (uuid: string): UuidQualifier => { export const isUuidQualifier = (identifier: unknown): identifier is UuidQualifier => { return isRecord(identifier) && hasField(identifier, { name: 'uuid', type: 'string' }); }; + +/** + * A qualifier that identifies an entity based on type + */ +export type ContactTypeQualifier = Readonly<{ contactType: string }>; + +/** + * Build the TypeQualifier that categorizes an entity by its type + * @param contactType the type of the entity + * @returns the type + * @throws Error if the type is invalid + */ +export const byContactType = (contactType: string): ContactTypeQualifier => { + if (!isString(contactType) || contactType.length === 0) { + throw new Error(`Invalid ContactType [${JSON.stringify(contactType)}].`); + } + + return { contactType }; +}; + +/** + * Returns `true` if the given qualifier is a {@link ContactTypeQualifier} otherwise `false`. + * @param contactType the type to check + * @returns `true` if the given type is a {@link ContactTypeQualifier}, otherwise `false`. + */ +export const isContactTypeQualifier = (contactType: unknown): contactType is ContactTypeQualifier => { + return isRecord(contactType) && hasField(contactType, { name: 'contactType', type: 'string' }); +}; diff --git a/shared-libs/cht-datasource/src/remote/libs/data-context.ts b/shared-libs/cht-datasource/src/remote/libs/data-context.ts index 4154142dc35..800140e2e70 100644 --- a/shared-libs/cht-datasource/src/remote/libs/data-context.ts +++ b/shared-libs/cht-datasource/src/remote/libs/data-context.ts @@ -58,3 +58,21 @@ export const getResource = (context: RemoteDataContext, path: string) => async < throw error; } }; + +/** @internal */ +export const getResources = (context: RemoteDataContext, path: string) => async ( + queryParams?: Record, +): Promise => { + const params = new URLSearchParams(queryParams).toString(); + try { + const response = await fetch(`${context.url}/${path}}?${params}`); + if (!response.ok) { + throw new Error(response.statusText); + } + + return (await response.json()) as T; + } catch (error) { + logger.error(`Failed to fetch resources from ${context.url}/${path} with params: ${params}`, error); + throw error; + } +}; diff --git a/shared-libs/cht-datasource/src/remote/person.ts b/shared-libs/cht-datasource/src/remote/person.ts index 41ced68b731..305178c4453 100644 --- a/shared-libs/cht-datasource/src/remote/person.ts +++ b/shared-libs/cht-datasource/src/remote/person.ts @@ -1,12 +1,14 @@ import { Nullable } from '../libs/core'; -import { UuidQualifier } from '../qualifier'; +import { ContactTypeQualifier, UuidQualifier } from '../qualifier'; import * as Person from '../person'; -import { getResource, RemoteDataContext } from './libs/data-context'; +import { getResource, getResources, RemoteDataContext } from './libs/data-context'; /** @internal */ export namespace v1 { const getPerson = (remoteContext: RemoteDataContext) => getResource(remoteContext, 'api/v1/person'); + const getPeople = (remoteContext: RemoteDataContext) => getResources(remoteContext, 'api/v1/person'); + /** @internal */ export const get = (remoteContext: RemoteDataContext) => ( identifier: UuidQualifier @@ -22,10 +24,10 @@ export namespace v1 { /** @internal */ export const getPage = (remoteContext: RemoteDataContext) => ( + personType: ContactTypeQualifier, limit: number, skip: number - ): Promise => getPerson(remoteContext)( - '', - {'limit': limit.toString(), 'skip': skip.toString()} + ): Promise => getPeople(remoteContext)( + {'limit': limit.toString(), 'skip': skip.toString(), 'contactType': personType.contactType} ); } diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index fc03ef8fdb8..2253c629455 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -130,14 +130,18 @@ describe('CHT Script API - getDatasource', () => { const expectedPeople: Index.Nullable[] = []; const personGetPage = sinon.stub().resolves(expectedPeople); dataContextBind.returns(personGetPage); + const personType = 'person'; const limit = 2; const skip = 1; + const personTypeQualifier = { contactType: personType }; + const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); - const returnedPeople = await person.getPage(limit, skip); + const returnedPeople = await person.getPage(personType, limit, skip); expect(returnedPeople).to.equal(expectedPeople); expect(dataContextBind.calledOnceWithExactly(Person.v1.getPage)).to.be.true; - expect(personGetPage.calledOnceWithExactly(limit, skip)).to.be.true; + expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip)).to.be.true; + expect(byContactType.calledOnceWithExactly(personType)).to.be.true; }); }); }); 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 d1c2d0718e0..db6bf96a013 100644 --- a/shared-libs/cht-datasource/test/local/libs/doc.spec.ts +++ b/shared-libs/cht-datasource/test/local/libs/doc.spec.ts @@ -166,10 +166,11 @@ describe('local doc lib', () => { const result = await queryDocsByRange(db, 'medic-client/docs_by_id_lineage')(doc0._id, doc0._id); expect(result).to.deep.equal([doc0, doc1, doc2]); + expect(dbQuery.calledOnceWithExactly('medic-client/docs_by_id_lineage', { - startkey: [doc0._id], - endkey: [doc0._id, {}], - include_docs: true + include_docs: true, + startkey: doc0._id, + endkey: doc0._id })).to.be.true; expect(isDoc.args).to.deep.equal([[doc0], [doc1], [doc2]]); }); @@ -190,8 +191,8 @@ describe('local doc lib', () => { expect(result).to.deep.equal([doc0, null, doc2]); expect(dbQuery.calledOnceWithExactly('medic-client/docs_by_id_lineage', { - startkey: [doc0._id], - endkey: [doc0._id, {}], + startkey: doc0._id, + endkey: doc0._id, include_docs: true })).to.be.true; expect(isDoc.args).to.deep.equal([[doc0], [null], [doc2]]); @@ -208,8 +209,8 @@ describe('local doc lib', () => { expect(result).to.deep.equal([null]); expect(dbQuery.calledOnceWithExactly('medic-client/docs_by_id_lineage', { - startkey: [doc0._id], - endkey: [doc0._id, {}], + startkey: doc0._id, + endkey: doc0._id, include_docs: true })).to.be.true; expect(isDoc.calledOnceWithExactly(doc0)).to.be.true; @@ -239,8 +240,8 @@ describe('local doc lib', () => { expect(result).to.deep.equal([doc0, doc1, doc2]); expect(dbQuery.calledOnceWithExactly('medic-client/contacts_by_type', { - key: [contactType], include_docs: true, + key: contactType, limit, skip })).to.be.true; @@ -258,8 +259,8 @@ describe('local doc lib', () => { expect(result).to.deep.equal([]); expect(dbQuery.calledOnceWithExactly('medic-client/contacts_by_type', { - key: [contactType], include_docs: true, + key: contactType, limit, skip })).to.be.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 1326bdf5ca2..a70cfa95a42 100644 --- a/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts +++ b/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts @@ -19,17 +19,20 @@ describe('local lineage lib', () => { afterEach(() => sinon.restore()); - it('getLineageDocsById', () => { - const queryFn = sinon.stub(); + it('getLineageDocsById', async () => { + const uuid = '123'; + const queryFn = sinon.stub().returns(Promise.resolve([])); const queryDocsByRange = sinon .stub(LocalDoc, 'queryDocsByRange') .returns(queryFn); const medicDb = { hello: 'world' } as unknown as PouchDB.Database; - const result = getLineageDocsById(medicDb); + const fn = getLineageDocsById(medicDb); + const result = await fn(uuid); - expect(result).to.equal(queryFn); + 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/cht-datasource/test/local/person.spec.ts b/shared-libs/cht-datasource/test/local/person.spec.ts index 183e5ed3b2e..adca2b1f5d0 100644 --- a/shared-libs/cht-datasource/test/local/person.spec.ts +++ b/shared-libs/cht-datasource/test/local/person.spec.ts @@ -146,7 +146,7 @@ describe('local person', () => { const result = await Person.v1.getWithLineage(localContext)(identifier); expect(result).to.equal(copiedPerson); - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; expect(isPerson.calledOnceWithExactly(settings, person)).to.be.true; expect(warn.notCalled).to.be.true; expect(debug.notCalled).to.be.true; @@ -167,7 +167,7 @@ describe('local person', () => { const result = await Person.v1.getWithLineage(localContext)(identifier); expect(result).to.be.null; - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; expect(isPerson.notCalled).to.be.true; expect(warn.calledOnceWithExactly(`No person found for identifier [${identifier.uuid}].`)).to.be.true; expect(debug.notCalled).to.be.true; @@ -191,7 +191,7 @@ describe('local person', () => { const result = await Person.v1.getWithLineage(localContext)(identifier); expect(result).to.be.null; - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; expect(isPerson.calledOnceWithExactly(settings, person)).to.be.true; expect(warn.calledOnceWithExactly(`Document [${identifier.uuid}] is not a valid person.`)).to.be.true; expect(debug.notCalled).to.be.true; @@ -212,7 +212,7 @@ describe('local person', () => { const result = await Person.v1.getWithLineage(localContext)(identifier); expect(result).to.equal(person); - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; expect(isPerson.calledOnceWithExactly(settings, person)).to.be.true; expect(warn.notCalled).to.be.true; expect(debug.calledOnceWithExactly(`No lineage places found for person [${identifier.uuid}].`)).to.be.true; @@ -229,6 +229,8 @@ describe('local person', () => { const limit = 3; const skip = 0; const personIdentifier = 'person'; + const personTypeQualifier = {contactType: personIdentifier} as const; + const invalidPersonTypeQualifier = { contactType: 'invalid' } as const; const personType = [{person: true, id: personIdentifier}] as Record[]; let getPersonTypes: SinonStub; let queryDocsByKeyInner: SinonStub; @@ -239,40 +241,44 @@ describe('local person', () => { queryDocsByKeyOuter = sinon.stub(LocalDoc, 'queryDocsByKey').returns(queryDocsByKeyInner); getPersonTypes = sinon.stub(contactTypeUtils, 'getPersonTypes').returns(personType); settingsGetAll.returns(settings); - + isPerson.returns(true); }); + it('returns a page of people', async () => { const doc = { type: 'person'}; const docs = [doc, doc, doc]; queryDocsByKeyInner.resolves(docs); - const res = await Person.v1.getPage(localContext)(limit, skip); + const res = await Person.v1.getPage(localContext)(personTypeQualifier, limit, skip); - expect(res).to.equal(docs); - expect(settingsGetAll.calledOnce).to.be.true; + expect(res).to.deep.equal(docs); + expect(settingsGetAll.callCount === 4).to.be.true; expect(getPersonTypes.calledOnceWithExactly(settings)).to.be.true; expect( queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') ).to.be.true; - expect(queryDocsByKeyInner.calledOnceWithExactly(personIdentifier, limit, skip)).to.be.true; + expect(queryDocsByKeyInner.calledOnceWithExactly([personIdentifier], limit, skip)).to.be.true; }); - it('returns empty array if person identifier does not exist', () => { - getPersonTypes.returns({}); + it('throws an error if person identifier is invalid/does not exist', async () => { queryDocsByKeyInner.resolves([]); - expect(() => Person.v1.getPage(localContext)(limit, skip)).to.throw('Person type not found'); + await expect(Person.v1.getPage(localContext)(invalidPersonTypeQualifier, limit, skip)).to.be.rejectedWith( + `Invalid person type: ${invalidPersonTypeQualifier.contactType}` + ); expect(settingsGetAll.calledOnce).to.be.true; expect(getPersonTypes.calledOnceWithExactly(settings)).to.be.true; - expect(queryDocsByKeyOuter.notCalled).to.be.true; + expect(queryDocsByKeyOuter.calledOnceWithExactly( + localContext.medicDb, 'medic-client/contacts_by_type' + )).to.be.true; expect(queryDocsByKeyInner.notCalled).to.be.true; }); it('returns empty array if people does not exist', async () => { queryDocsByKeyInner.resolves([]); - const res = await Person.v1.getPage(localContext)(limit, skip); + const res = await Person.v1.getPage(localContext)(personTypeQualifier, limit, skip); expect(res).to.deep.equal([]); expect(settingsGetAll.calledOnce).to.be.true; @@ -280,7 +286,7 @@ describe('local person', () => { expect( queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') ).to.be.true; - expect(queryDocsByKeyInner.calledOnceWithExactly(personIdentifier, limit, skip)).to.be.true; + expect(queryDocsByKeyInner.calledOnceWithExactly([personIdentifier], limit, skip)).to.be.true; }); }); }); diff --git a/shared-libs/cht-datasource/test/local/place.spec.ts b/shared-libs/cht-datasource/test/local/place.spec.ts index 2b08b36a16d..da8c9c50c61 100644 --- a/shared-libs/cht-datasource/test/local/place.spec.ts +++ b/shared-libs/cht-datasource/test/local/place.spec.ts @@ -145,7 +145,7 @@ describe('local place', () => { const result = await Place.v1.getWithLineage(localContext)(identifier); expect(result).to.equal(copiedPlace); - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; expect(isPlace.calledOnceWithExactly(settings, place0)).to.be.true; expect(warn.notCalled).to.be.true; expect(debug.notCalled).to.be.true; @@ -166,7 +166,7 @@ describe('local place', () => { const result = await Place.v1.getWithLineage(localContext)(identifier); expect(result).to.be.null; - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; expect(isPlace.notCalled).to.be.true; expect(warn.calledOnceWithExactly(`No place found for identifier [${identifier.uuid}].`)).to.be.true; expect(debug.notCalled).to.be.true; @@ -189,7 +189,7 @@ describe('local place', () => { const result = await Place.v1.getWithLineage(localContext)(identifier); expect(result).to.be.null; - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; expect(isPlace.calledOnceWithExactly(settings, place0)).to.be.true; expect(warn.calledOnceWithExactly(`Document [${identifier.uuid}] is not a valid place.`)).to.be.true; expect(debug.notCalled).to.be.true; @@ -210,7 +210,7 @@ describe('local place', () => { const result = await Place.v1.getWithLineage(localContext)(identifier); expect(result).to.equal(place); - expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid, identifier.uuid)).to.be.true; + expect(getLineageDocsByIdInner.calledOnceWithExactly(identifier.uuid)).to.be.true; expect(isPlace.calledOnceWithExactly(settings, place)).to.be.true; expect(warn.notCalled).to.be.true; expect(debug.calledOnceWithExactly(`No lineage places found for place [${identifier.uuid}].`)).to.be.true; diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index c474e1096fc..78e3dec466b 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -12,11 +12,13 @@ describe('person', () => { let assertDataContext: SinonStub; let adapt: SinonStub; let isUuidQualifier: SinonStub; + let isContactTypeQualifier: SinonStub; beforeEach(() => { assertDataContext = sinon.stub(Context, 'assertDataContext'); adapt = sinon.stub(Context, 'adapt'); isUuidQualifier = sinon.stub(Qualifier, 'isUuidQualifier'); + isContactTypeQualifier = sinon.stub(Qualifier, 'isContactTypeQualifier'); }); afterEach(() => sinon.restore()); @@ -128,6 +130,8 @@ describe('person', () => { const people = [{ _id: 'person1' }, { _id: 'person2' }, { _id: 'person3' }] as Person.v1.Person[]; const limit = 3; const skip = 1; + const personTypeQualifier = {contactType: 'person'} as const; + const invalidQualifier = { contactType: 'invalid' } as const; let getPage: SinonStub; beforeEach(() => { @@ -136,9 +140,10 @@ describe('person', () => { }); it('retrieves people from the data context', async () => { + isContactTypeQualifier.returns(true); getPage.resolves(people); - const result = await Person.v1.getPage(dataContext)(limit, skip); + const result = await Person.v1.getPage(dataContext)(personTypeQualifier, limit, skip); expect(result).to.equal(people); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -146,6 +151,7 @@ describe('person', () => { }); it('throws an error if the data context is invalid', () => { + isContactTypeQualifier.returns(true); assertDataContext.throws(new Error(`Invalid data context [null].`)); expect(() => Person.v1.getPage(dataContext)).to.throw(`Invalid data context [null].`); @@ -154,6 +160,18 @@ describe('person', () => { expect(adapt.notCalled).to.be.true; expect(getPage.notCalled).to.be.true; }); + + it('throws an error if the qualifier is invalid', async () => { + isContactTypeQualifier.returns(false); + + await expect(Person.v1.getPage(dataContext)(invalidQualifier)) + .to.be.rejectedWith(`Invalid type [${JSON.stringify(invalidQualifier)}].`); + + expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; + expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; + expect(isContactTypeQualifier.calledOnceWithExactly(invalidQualifier)).to.be.true; + expect(getPage.notCalled).to.be.true; + }); }); }); }); diff --git a/shared-libs/cht-datasource/test/qualifier.spec.ts b/shared-libs/cht-datasource/test/qualifier.spec.ts index 0b9fcb3b5b3..60146d3eecb 100644 --- a/shared-libs/cht-datasource/test/qualifier.spec.ts +++ b/shared-libs/cht-datasource/test/qualifier.spec.ts @@ -1,4 +1,4 @@ -import { byUuid, isUuidQualifier } from '../src/qualifier'; +import {byContactType, byUuid, isContactTypeQualifier, isUuidQualifier} from '../src/qualifier'; import { expect } from 'chai'; describe('qualifier', () => { @@ -31,4 +31,36 @@ describe('qualifier', () => { }); }); }); + + describe('byContactType', () => { + it('builds a qualifier that identifies an entity by its contactType', () => { + expect(byContactType('person')).to.deep.equal({ contactType: 'person' }); + }); + + [ + null, + '', + { }, + ].forEach(contactType => { + it(`throws an error for ${JSON.stringify(contactType)}`, () => { + expect(() => byContactType(contactType as string)).to.throw( + `Invalid ContactType [${JSON.stringify(contactType)}].` + ); + }); + }); + }); + + describe('isContactTypeQualifier', () => { + [ + [ null, false ], + [ 'person', false ], + [ { contactType: { } }, false ], + [ { contactType: 'person' }, true ], + [ { contactType: 'person', other: 'other' }, true ] + ].forEach(([ contactType, expected ]) => { + it(`evaluates ${JSON.stringify(contactType)}`, () => { + expect(isContactTypeQualifier(contactType)).to.equal(expected); + }); + }); + }); }); diff --git a/shared-libs/cht-datasource/test/remote/person.spec.ts b/shared-libs/cht-datasource/test/remote/person.spec.ts index 2f33c2516ff..1028b52e3d6 100644 --- a/shared-libs/cht-datasource/test/remote/person.spec.ts +++ b/shared-libs/cht-datasource/test/remote/person.spec.ts @@ -8,10 +8,14 @@ describe('remote person', () => { const remoteContext = {} as RemoteDataContext; let getResourceInner: SinonStub; let getResourceOuter: SinonStub; + let getResourcesInner: SinonStub; + let getResourcesOuter: SinonStub; beforeEach(() => { getResourceInner = sinon.stub(); getResourceOuter = sinon.stub(RemoteEnv, 'getResource').returns(getResourceInner); + getResourcesInner = sinon.stub(); + getResourcesOuter = sinon.stub(RemoteEnv, 'getResources').returns(getResourcesInner); }); afterEach(() => sinon.restore()); @@ -68,27 +72,32 @@ describe('remote person', () => { describe('getPage', () => { const limit = 3; const skip = 1; - const queryParam = {limit: limit.toString(), skip: skip.toString()}; + const personTypeQualifier = {contactType: 'person'}; + const queryParam = { + limit: limit.toString(), + skip: skip.toString(), + ...personTypeQualifier + }; it('returns people', async () => { const doc = [{ type: 'person' }, {type: 'person'}]; - getResourceInner.resolves(doc); + getResourcesInner.resolves(doc); - const result = await Person.v1.getPage(remoteContext)(limit, skip); + const result = await Person.v1.getPage(remoteContext)(personTypeQualifier, limit, skip); expect(result).to.equal(doc); - expect(getResourceOuter.calledOnceWithExactly(remoteContext, 'api/v1/person')).to.be.true; - expect(getResourceInner.calledOnceWithExactly('', queryParam)).to.be.true; + expect(getResourcesOuter.calledOnceWithExactly(remoteContext, 'api/v1/person')).to.be.true; + expect(getResourcesInner.calledOnceWithExactly(queryParam)).to.be.true; }); it('returns empty array if docs are not found', async () => { - getResourceInner.resolves([]); + getResourcesInner.resolves([]); - const result = await Person.v1.getPage(remoteContext)(limit, skip); + const result = await Person.v1.getPage(remoteContext)(personTypeQualifier, limit, skip); expect(result).to.deep.equal([]); - expect(getResourceOuter.calledOnceWithExactly(remoteContext, 'api/v1/person')).to.be.true; - expect(getResourceInner.calledOnceWithExactly('', queryParam)).to.be.true; + expect(getResourcesOuter.calledOnceWithExactly(remoteContext, 'api/v1/person')).to.be.true; + expect(getResourcesInner.calledOnceWithExactly(queryParam)).to.be.true; }); }); }); From 105d5db67dde85e1d0c503d52b1781f8c2c4b78d Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:37:05 +0545 Subject: [PATCH 04/56] Update shared-libs/cht-datasource/src/qualifier.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/qualifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-libs/cht-datasource/src/qualifier.ts b/shared-libs/cht-datasource/src/qualifier.ts index a7149344c2a..5ead8fccb15 100644 --- a/shared-libs/cht-datasource/src/qualifier.ts +++ b/shared-libs/cht-datasource/src/qualifier.ts @@ -29,7 +29,7 @@ export const isUuidQualifier = (identifier: unknown): identifier is UuidQualifie }; /** - * A qualifier that identifies an entity based on type +A qualifier that identifies contacts based on type. */ export type ContactTypeQualifier = Readonly<{ contactType: string }>; From 2b92b4c1aeec8e14f7b7e8d08ddbf8749e9ebf20 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:08:28 +0545 Subject: [PATCH 05/56] Update shared-libs/cht-datasource/test/local/person.spec.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/test/local/person.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-libs/cht-datasource/test/local/person.spec.ts b/shared-libs/cht-datasource/test/local/person.spec.ts index adca2b1f5d0..148a91b9cd5 100644 --- a/shared-libs/cht-datasource/test/local/person.spec.ts +++ b/shared-libs/cht-datasource/test/local/person.spec.ts @@ -252,7 +252,7 @@ describe('local person', () => { const res = await Person.v1.getPage(localContext)(personTypeQualifier, limit, skip); expect(res).to.deep.equal(docs); - expect(settingsGetAll.callCount === 4).to.be.true; + expect(settingsGetAll.callCount).to.equal(4); expect(getPersonTypes.calledOnceWithExactly(settings)).to.be.true; expect( queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') From ba2e27a53a407e3defa641eab7570f14bf246012 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:06:43 +0545 Subject: [PATCH 06/56] Update shared-libs/cht-datasource/src/person.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/person.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 0027b65d5d7..e699712f9b0 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -82,10 +82,10 @@ export namespace v1 { export const getWithLineage = getPerson(Local.Person.v1.getWithLineage, Remote.Person.v1.getWithLineage); /** - * Returns an array of people. + * Returns a function for retrieving a paged array of persons from the given data context. * @param context the current data context - * @returns an array of people - * @throws Error if the provided context or personType qualifier is invalid + * @returns a function for retrieving a paged array of persons + * @throws Error if a data context is not provided */ export const getPage = getPeople(Local.Person.v1.getPage, Remote.Person.v1.getPage); } From 15068fa20f63653a160371c457466d3f9be40eae Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:07:50 +0545 Subject: [PATCH 07/56] Update shared-libs/cht-datasource/src/remote/person.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/remote/person.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-libs/cht-datasource/src/remote/person.ts b/shared-libs/cht-datasource/src/remote/person.ts index 305178c4453..4ec6d79bbfe 100644 --- a/shared-libs/cht-datasource/src/remote/person.ts +++ b/shared-libs/cht-datasource/src/remote/person.ts @@ -27,7 +27,7 @@ export namespace v1 { personType: ContactTypeQualifier, limit: number, skip: number - ): Promise => getPeople(remoteContext)( + ): Promise => getPeople(remoteContext)( {'limit': limit.toString(), 'skip': skip.toString(), 'contactType': personType.contactType} ); } From a83a25909a5e20856927f14d9376899d879e626c Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:09:06 +0545 Subject: [PATCH 08/56] Update shared-libs/cht-datasource/src/person.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/person.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index e699712f9b0..780247fbc3e 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -32,9 +32,9 @@ export namespace v1 { } }; - const assertTypeQualifier: ( + const assertTypeQualifier: (qualifier: unknown) => asserts qualifier is ContactTypeQualifier = ( qualifier: unknown - ) => asserts qualifier is ContactTypeQualifier = (qualifier: unknown) => { + ) => { if (!isContactTypeQualifier(qualifier)) { throw new Error(`Invalid type [${JSON.stringify(qualifier)}].`); } From 29a3ad5eb055d41df8cd2bda13f588210977855c Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:09:28 +0545 Subject: [PATCH 09/56] Update shared-libs/cht-datasource/src/index.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/index.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index 56e3507868d..1a1d66e166b 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -86,13 +86,16 @@ export const getDatasource = (ctx: DataContext) => { */ getByUuidWithLineage: (uuid: string) => ctx.bind(Person.v1.getWithLineage)(Qualifier.byUuid(uuid)), - /** - * Returns a list of people. - * @param personType the string that represents the person type - * @param limit the total number of records to retrieve - * @param skip the total number of records to skip - * @returns array of `Person` - */ + /** + * Returns an array of persons for the provided page specifications. + * @param personType the type of persons to return + * @param limit the maximum number of persons to return. Default is 100. + * @param skip the number of persons to skip. Default is 0. + * @returns an array of persons for the provided page specifications + * @throws Error if no type is provided or if the type is not for a person + * @throws Error if the provided limit is `<= 0` + * @throws Error if the provided skip is `< 0` + */ getPage: (personType: string, limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)( Qualifier.byContactType(personType), limit, skip ), From 8fbc245fea851643d58cbe5135f7b63f51407335 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Tue, 23 Jul 2024 16:37:26 +0545 Subject: [PATCH 10/56] feat(#9237): Address PR comments --- .../cht-datasource/src/local/libs/lineage.ts | 6 +--- .../cht-datasource/src/local/person.ts | 16 +++++----- shared-libs/cht-datasource/src/person.ts | 11 +++++++ shared-libs/cht-datasource/test/index.spec.ts | 2 +- .../test/local/libs/doc.spec.ts | 30 +++++++++++++----- .../test/local/libs/lineage.spec.ts | 4 +-- .../cht-datasource/test/local/person.spec.ts | 29 +++++++++++++++-- .../cht-datasource/test/person.spec.ts | 31 +++++++++++++++++++ 8 files changed, 104 insertions(+), 25 deletions(-) diff --git a/shared-libs/cht-datasource/src/local/libs/lineage.ts b/shared-libs/cht-datasource/src/local/libs/lineage.ts index 431408442e9..15b7c97cbaa 100644 --- a/shared-libs/cht-datasource/src/local/libs/lineage.ts +++ b/shared-libs/cht-datasource/src/local/libs/lineage.ts @@ -18,11 +18,7 @@ import logger from '@medic/logger'; * 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[]> => { +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, {}]); }; diff --git a/shared-libs/cht-datasource/src/local/person.ts b/shared-libs/cht-datasource/src/local/person.ts index 1e4e7cb6be5..52e8eb207c3 100644 --- a/shared-libs/cht-datasource/src/local/person.ts +++ b/shared-libs/cht-datasource/src/local/person.ts @@ -10,7 +10,7 @@ import { getLineageDocsById, getPrimaryContactIds, hydrateLineage, hydratePrimar /** @internal */ export namespace v1 { - const isPerson = (settings: SettingsService, uuid: string, doc: Nullable): doc is Person.v1.Person => { + const isPerson = (settings: SettingsService, doc: Nullable, uuid = ''): doc is Person.v1.Person => { if (!doc) { logger.warn(`No person found for identifier [${uuid}].`); return false; @@ -28,7 +28,7 @@ export namespace v1 { const getMedicDocById = getDocById(medicDb); return async (identifier: UuidQualifier): Promise> => { const doc = await getMedicDocById(identifier.uuid); - if (!isPerson(settings, identifier.uuid, doc)) { + if (!isPerson(settings, doc, identifier.uuid)) { return null; } return doc; @@ -41,7 +41,7 @@ export namespace v1 { const getMedicDocsById = getDocsByIds(medicDb); return async (identifier: UuidQualifier): Promise> => { const [person, ...lineagePlaces] = await getLineageDocs(identifier.uuid); - if (!isPerson(settings, identifier.uuid, person)) { + if (!isPerson(settings, person, identifier.uuid)) { return null; } // Intentionally not further validating lineage. For passivity, lineage problems should not block retrieval. @@ -61,19 +61,19 @@ export namespace v1 { /** @internal */ export const getPage = ({ medicDb, settings }: LocalDataContext) => { - const personTypes = contactTypeUtils.getPersonTypes(settings.getAll()); - const personTypesIds = personTypes.map(item => item.id); + return async (personType: ContactTypeQualifier, limit: number, skip: number): Promise => { + const personTypes = contactTypeUtils.getPersonTypes(settings.getAll()); + const personTypesIds = personTypes.map(item => item.id); - const getDocsByPage = queryDocsByKey(medicDb, 'medic-client/contacts_by_type'); + const getDocsByPage = queryDocsByKey(medicDb, 'medic-client/contacts_by_type'); - return async (personType: ContactTypeQualifier, limit: number, skip: number): Promise => { if (!personTypesIds.includes(personType.contactType)) { throw new Error(`Invalid person type: ${personType.contactType}`); } const docs = await getDocsByPage([personType.contactType], limit, skip); - return docs.filter((doc): doc is Person.v1.Person => isPerson(settings, doc?._id ?? '', doc)); + return docs.filter((doc): doc is Person.v1.Person => isPerson(settings, doc, doc?._id)); }; }; } diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 780247fbc3e..e50e8752b32 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -40,6 +40,15 @@ export namespace v1 { } }; + const assertLimitAndSkip = (limit: unknown, skip: unknown) => { + if (typeof limit !== 'number' || limit <= 0) { + throw new Error('limit must be a positive number'); + } + if (typeof skip !== 'number' || skip < 0) { + throw new Error('skip must be a non-negative number'); + } + }; + const getPerson = ( localFn: (c: LocalDataContext) => (qualifier: UuidQualifier) => Promise, remoteFn: (c: RemoteDataContext) => (qualifier: UuidQualifier) => Promise @@ -61,6 +70,8 @@ export namespace v1 { return async (personType: ContactTypeQualifier, limit = 100, skip = 0): Promise => { assertTypeQualifier(personType); + assertLimitAndSkip(limit, skip); + return fn(personType, limit, skip); }; }; diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index 2253c629455..d1bdfb8e133 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -127,7 +127,7 @@ describe('CHT Script API - getDatasource', () => { }); it('getPage', async () => { - const expectedPeople: Index.Nullable[] = []; + const expectedPeople: Person.v1.Person[] = []; const personGetPage = sinon.stub().resolves(expectedPeople); dataContextBind.returns(personGetPage); const personType = 'person'; 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 db6bf96a013..33c8aa7bdb0 100644 --- a/shared-libs/cht-datasource/test/local/libs/doc.spec.ts +++ b/shared-libs/cht-datasource/test/local/libs/doc.spec.ts @@ -163,14 +163,14 @@ describe('local doc lib', () => { }); isDoc.returns(true); - const result = await queryDocsByRange(db, 'medic-client/docs_by_id_lineage')(doc0._id, doc0._id); + const result = await queryDocsByRange(db, 'medic-client/docs_by_id_lineage')(doc0._id, doc1._id); expect(result).to.deep.equal([doc0, doc1, doc2]); expect(dbQuery.calledOnceWithExactly('medic-client/docs_by_id_lineage', { include_docs: true, startkey: doc0._id, - endkey: doc0._id + endkey: doc1._id })).to.be.true; expect(isDoc.args).to.deep.equal([[doc0], [doc1], [doc2]]); }); @@ -187,12 +187,12 @@ describe('local doc lib', () => { }); isDoc.returns(true); - const result = await queryDocsByRange(db, 'medic-client/docs_by_id_lineage')(doc0._id, doc0._id); + const result = await queryDocsByRange(db, 'medic-client/docs_by_id_lineage')(doc0._id, doc2._id); expect(result).to.deep.equal([doc0, null, doc2]); expect(dbQuery.calledOnceWithExactly('medic-client/docs_by_id_lineage', { startkey: doc0._id, - endkey: doc0._id, + endkey: doc2._id, include_docs: true })).to.be.true; expect(isDoc.args).to.deep.equal([[doc0], [null], [doc2]]); @@ -249,22 +249,38 @@ describe('local doc lib', () => { }); it('returns empty array if docs are not found', async () => { + dbQuery.resolves({ rows: [] }); + isDoc.returns(true); + + const result = await queryDocsByKey(db, 'medic-client/contacts_by_type')(contactType, limit, skip); + + expect(result).to.deep.equal([]); + expect(dbQuery.calledOnceWithExactly('medic-client/contacts_by_type', { + include_docs: true, key: contactType, limit, skip + })).to.be.true; + expect(isDoc.args).to.deep.equal([]); + }); + + it('returns null valued array if rows from database are not docs', async () => { + const doc0 = { _id: 'doc0' }; + dbQuery.resolves({ rows: [ + { doc: doc0 }, ] }); - isDoc.returns(true); + isDoc.returns(false); const result = await queryDocsByKey(db, 'medic-client/contacts_by_type')(contactType, limit, skip); - expect(result).to.deep.equal([]); + expect(result).to.deep.equal([null]); expect(dbQuery.calledOnceWithExactly('medic-client/contacts_by_type', { include_docs: true, key: contactType, limit, skip })).to.be.true; - expect(isDoc.args).to.deep.equal([]); + expect(isDoc.args).to.deep.equal([[doc0]]); }); }); }); 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 a70cfa95a42..a3c9c453062 100644 --- a/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts +++ b/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts @@ -16,12 +16,12 @@ describe('local lineage lib', () => { beforeEach(() => { debug = sinon.stub(logger, 'debug'); }); - + // afterEach(() => sinon.restore()); it('getLineageDocsById', async () => { const uuid = '123'; - const queryFn = sinon.stub().returns(Promise.resolve([])); + const queryFn = sinon.stub().returns([]); const queryDocsByRange = sinon .stub(LocalDoc, 'queryDocsByRange') .returns(queryFn); diff --git a/shared-libs/cht-datasource/test/local/person.spec.ts b/shared-libs/cht-datasource/test/local/person.spec.ts index 148a91b9cd5..c1d77d9ec54 100644 --- a/shared-libs/cht-datasource/test/local/person.spec.ts +++ b/shared-libs/cht-datasource/test/local/person.spec.ts @@ -258,11 +258,13 @@ describe('local person', () => { queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') ).to.be.true; expect(queryDocsByKeyInner.calledOnceWithExactly([personIdentifier], limit, skip)).to.be.true; + expect(isPerson.callCount).to.equal(3); + expect(isPerson.getCall(0).args).to.deep.equal([settings, doc]); + expect(isPerson.getCall(1).args).to.deep.equal([settings, doc]); + expect(isPerson.getCall(2).args).to.deep.equal([settings, doc]); }); it('throws an error if person identifier is invalid/does not exist', async () => { - queryDocsByKeyInner.resolves([]); - await expect(Person.v1.getPage(localContext)(invalidPersonTypeQualifier, limit, skip)).to.be.rejectedWith( `Invalid person type: ${invalidPersonTypeQualifier.contactType}` ); @@ -273,6 +275,7 @@ describe('local person', () => { localContext.medicDb, 'medic-client/contacts_by_type' )).to.be.true; expect(queryDocsByKeyInner.notCalled).to.be.true; + expect(isPerson.notCalled).to.be.true; }); it('returns empty array if people does not exist', async () => { @@ -287,6 +290,28 @@ describe('local person', () => { queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') ).to.be.true; expect(queryDocsByKeyInner.calledOnceWithExactly([personIdentifier], limit, skip)).to.be.true; + expect(isPerson.notCalled).to.be.true; + }); + + it('returns null valued array if rows returned from database are not docs', async () => { + const doc = { type: 'person'}; + const docs = [doc, doc, doc]; + queryDocsByKeyInner.resolves(docs); + isPerson.returns(false); + + const res = await Person.v1.getPage(localContext)(personTypeQualifier, limit, skip); + + expect(res).to.deep.equal([]); + expect(settingsGetAll.callCount).to.equal(4); + expect(getPersonTypes.calledOnceWithExactly(settings)).to.be.true; + expect( + queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') + ).to.be.true; + expect(queryDocsByKeyInner.calledOnceWithExactly([personIdentifier], limit, skip)).to.be.true; + expect(isPerson.callCount).to.equal(3); + expect(isPerson.getCall(0).args).to.deep.equal([settings, doc]); + expect(isPerson.getCall(1).args).to.deep.equal([settings, doc]); + expect(isPerson.getCall(2).args).to.deep.equal([settings, doc]); }); }); }); diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index 78e3dec466b..a9e74dccc9c 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -130,6 +130,8 @@ describe('person', () => { const people = [{ _id: 'person1' }, { _id: 'person2' }, { _id: 'person3' }] as Person.v1.Person[]; const limit = 3; const skip = 1; + const invalidLimit = -1; + const invalidSkip = -1; const personTypeQualifier = {contactType: 'person'} as const; const invalidQualifier = { contactType: 'invalid' } as const; let getPage: SinonStub; @@ -148,6 +150,8 @@ describe('person', () => { expect(result).to.equal(people); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; + expect(getPage.calledOnceWithExactly(personTypeQualifier, limit, skip)).to.be.true; + expect(isContactTypeQualifier.calledOnceWithExactly((personTypeQualifier))).to.be.true; }); it('throws an error if the data context is invalid', () => { @@ -159,6 +163,7 @@ describe('person', () => { expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(adapt.notCalled).to.be.true; expect(getPage.notCalled).to.be.true; + expect(isContactTypeQualifier.notCalled).to.be.true; }); it('throws an error if the qualifier is invalid', async () => { @@ -172,6 +177,32 @@ describe('person', () => { expect(isContactTypeQualifier.calledOnceWithExactly(invalidQualifier)).to.be.true; expect(getPage.notCalled).to.be.true; }); + + it('throws an error if limit is invalid', async () => { + isContactTypeQualifier.returns(true); + getPage.resolves(people); + + await expect(Person.v1.getPage(dataContext)(personTypeQualifier, invalidLimit, skip)) + .to.be.rejectedWith(`limit must be a positive number`); + + expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; + expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; + expect(isContactTypeQualifier.calledOnceWithExactly((personTypeQualifier))).to.be.true; + expect(getPage.notCalled).to.be.true; + }); + + it('throws an error if skip is invalid', async () => { + isContactTypeQualifier.returns(true); + getPage.resolves(people); + + await expect(Person.v1.getPage(dataContext)(personTypeQualifier, limit, invalidSkip)) + .to.be.rejectedWith(`skip must be a non-negative number`); + + expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; + expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; + expect(isContactTypeQualifier.calledOnceWithExactly((personTypeQualifier))).to.be.true; + expect(getPage.notCalled).to.be.true; + }); }); }); }); From a99de7277d31cd935f3d47bce99685f96eeb9b83 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Tue, 23 Jul 2024 22:06:26 +0545 Subject: [PATCH 11/56] feat(#9237): Add unit tests for getResources in remote mode --- shared-libs/cht-datasource/src/index.ts | 20 +++---- shared-libs/cht-datasource/src/person.ts | 4 +- .../src/remote/libs/data-context.ts | 2 +- .../test/remote/libs/data-context.spec.ts | 55 +++++++++++++++++++ 4 files changed, 68 insertions(+), 13 deletions(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index 1a1d66e166b..239e30dd163 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -86,16 +86,16 @@ export const getDatasource = (ctx: DataContext) => { */ getByUuidWithLineage: (uuid: string) => ctx.bind(Person.v1.getWithLineage)(Qualifier.byUuid(uuid)), - /** - * Returns an array of persons for the provided page specifications. - * @param personType the type of persons to return - * @param limit the maximum number of persons to return. Default is 100. - * @param skip the number of persons to skip. Default is 0. - * @returns an array of persons for the provided page specifications - * @throws Error if no type is provided or if the type is not for a person - * @throws Error if the provided limit is `<= 0` - * @throws Error if the provided skip is `< 0` - */ + /** + * Returns an array of people for the provided page specifications. + * @param personType the type of people to return + * @param limit the maximum number of people to return. Default is 100. + * @param skip the number of people to skip. Default is 0. + * @returns an array of people for the provided page specifications + * @throws Error if no type is provided or if the type is not for a person + * @throws Error if the provided limit is `<= 0` + * @throws Error if the provided skip is `< 0` + */ getPage: (personType: string, limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)( Qualifier.byContactType(personType), limit, skip ), diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index e50e8752b32..af0ed3eb289 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -93,9 +93,9 @@ export namespace v1 { export const getWithLineage = getPerson(Local.Person.v1.getWithLineage, Remote.Person.v1.getWithLineage); /** - * Returns a function for retrieving a paged array of persons from the given data context. + * Returns a function for retrieving a paged array of people from the given data context. * @param context the current data context - * @returns a function for retrieving a paged array of persons + * @returns a function for retrieving a paged array of people * @throws Error if a data context is not provided */ export const getPage = getPeople(Local.Person.v1.getPage, Remote.Person.v1.getPage); diff --git a/shared-libs/cht-datasource/src/remote/libs/data-context.ts b/shared-libs/cht-datasource/src/remote/libs/data-context.ts index 800140e2e70..016d9026399 100644 --- a/shared-libs/cht-datasource/src/remote/libs/data-context.ts +++ b/shared-libs/cht-datasource/src/remote/libs/data-context.ts @@ -65,7 +65,7 @@ export const getResources = (context: RemoteDataContext, path: string) => async ): Promise => { const params = new URLSearchParams(queryParams).toString(); try { - const response = await fetch(`${context.url}/${path}}?${params}`); + const response = await fetch(`${context.url}/${path}?${params}`); if (!response.ok) { throw new Error(response.statusText); } diff --git a/shared-libs/cht-datasource/test/remote/libs/data-context.spec.ts b/shared-libs/cht-datasource/test/remote/libs/data-context.spec.ts index 8516b547e2e..55c17fd9408 100644 --- a/shared-libs/cht-datasource/test/remote/libs/data-context.spec.ts +++ b/shared-libs/cht-datasource/test/remote/libs/data-context.spec.ts @@ -4,6 +4,7 @@ import sinon, { SinonStub } from 'sinon'; import { assertRemoteDataContext, getResource, + getResources, getRemoteDataContext, isRemoteDataContext, RemoteDataContext @@ -149,4 +150,58 @@ describe('remote context lib', () => { expect(fetchResponse.json.notCalled).to.be.true; }); }); + + describe('getResources', () => { + let params: Record; + let stringifiedParams: string; + + beforeEach(() => { + params = {abc: 'xyz'}; + stringifiedParams = new URLSearchParams(params).toString(); + }); + + it('fetches a resource with a path', async () => { + const path = 'path'; + const resource = { hello: 'world' }; + fetchResponse.json.resolves(resource); + + const response = await getResources(context, path)(params); + + expect(response).to.equal(resource); + expect(fetchStub.calledOnceWithExactly(`${context.url}/${path}?${stringifiedParams}`)).to.be.true; + expect(fetchResponse.json.calledOnceWithExactly()).to.be.true; + }); + + it('throws an error if the resource fetch rejects', async () => { + const path = 'path'; + const expectedError = new Error('unexpected error'); + fetchStub.rejects(expectedError); + + await expect(getResources(context, path)(params)).to.be.rejectedWith(expectedError); + + expect(fetchStub.calledOnceWithExactly(`${context.url}/${path}?${stringifiedParams}`)).to.be.true; + expect(loggerError.calledOnceWithExactly( + `Failed to fetch resources from ${context.url}/${path} with params: ${stringifiedParams}`, + expectedError + )).to.be.true; + expect(fetchResponse.json.notCalled).to.be.true; + }); + + it('throws an error if the resource fetch resolves an error status', async () => { + const path = 'path'; + fetchResponse.ok = false; + fetchResponse.status = 501; + fetchResponse.statusText = 'Not Implemented'; + + await expect(getResources(context, path)(params)).to.be.rejectedWith(fetchResponse.statusText); + + expect(fetchStub.calledOnceWithExactly(`${context.url}/${path}?${stringifiedParams}`)).to.be.true; + expect(loggerError.calledOnce).to.be.true; + expect(loggerError.args[0]).to.deep.equal([ + `Failed to fetch resources from ${context.url}/${path} with params: ${stringifiedParams}`, + new Error(fetchResponse.statusText) + ]); + expect(fetchResponse.json.notCalled).to.be.true; + }); + }); }); From d87c258fb3247c4d39a8b3552e180c05a9bc69b9 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 24 Jul 2024 10:17:59 +0545 Subject: [PATCH 12/56] feat(#9237): Address PR comments --- shared-libs/cht-datasource/src/person.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index af0ed3eb289..dc12b0ece04 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -68,12 +68,23 @@ export namespace v1 { assertDataContext(context); const fn = adapt(context, localFn, remoteFn); - return async (personType: ContactTypeQualifier, limit = 100, skip = 0): Promise => { + /** + * Returns an array of people for the provided page specifications. + * @param personType the type of people to return + * @param limit the maximum number of people to return. Default is 100. + * @param skip the number of people to skip. Default is 0. + * @returns an array of people for the provided page specifications. + * @throws Error if `personType` qualifier is invalid + * @throws Error if the provided `limit` value is `<=0` + * @throws Error if the provided `skip` value is `<0` + */ + const curriedFn = async (personType: ContactTypeQualifier, limit = 100, skip = 0): Promise => { assertTypeQualifier(personType); assertLimitAndSkip(limit, skip); return fn(personType, limit, skip); }; + return curriedFn; }; /** From c34716aeb05c3588aecd402688fd0c026838f8ff Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 24 Jul 2024 21:42:45 +0545 Subject: [PATCH 13/56] Write getAll generator in src/person.ts --- shared-libs/cht-datasource/src/person.ts | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index dc12b0ece04..2e8b7dff312 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -87,6 +87,34 @@ export namespace v1 { return curriedFn; }; + // NOTE: there's probably a better name for this function + const getPeopleGenerator = () => (context: DataContext) => { + assertDataContext(context); + + return async (personType: ContactTypeQualifier) => { + assertTypeQualifier(personType); + const limit = 100; + let skip = 0; + + return { + [Symbol.asyncIterator]: async function*() { + while (true) { + const docs = await context.bind(getPage)(personType, limit, skip); + + if (docs.length < 100) { + yield docs; + break; + } + + skip += limit; + + yield docs; + } + } + }; + }; + }; + /** * Returns a person for the given qualifier. * @param context the current data context @@ -110,4 +138,9 @@ export namespace v1 { * @throws Error if a data context is not provided */ export const getPage = getPeople(Local.Person.v1.getPage, Remote.Person.v1.getPage); + + /** + * TODO: Write docs + */ + export const getAll = getPeopleGenerator(); } From a9fb5eb7bed5185b8ea69b2f4c0dfe6228a7b14f Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 24 Jul 2024 18:12:39 +0545 Subject: [PATCH 14/56] (#feat): Minor fix --- shared-libs/cht-datasource/src/local/person.ts | 4 ++-- shared-libs/cht-datasource/test/local/person.spec.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/shared-libs/cht-datasource/src/local/person.ts b/shared-libs/cht-datasource/src/local/person.ts index 52e8eb207c3..9bc0bbb8b9b 100644 --- a/shared-libs/cht-datasource/src/local/person.ts +++ b/shared-libs/cht-datasource/src/local/person.ts @@ -65,12 +65,12 @@ export namespace v1 { const personTypes = contactTypeUtils.getPersonTypes(settings.getAll()); const personTypesIds = personTypes.map(item => item.id); - const getDocsByPage = queryDocsByKey(medicDb, 'medic-client/contacts_by_type'); - if (!personTypesIds.includes(personType.contactType)) { throw new Error(`Invalid person type: ${personType.contactType}`); } + const getDocsByPage = queryDocsByKey(medicDb, 'medic-client/contacts_by_type'); + const docs = await getDocsByPage([personType.contactType], limit, skip); return docs.filter((doc): doc is Person.v1.Person => isPerson(settings, doc, doc?._id)); diff --git a/shared-libs/cht-datasource/test/local/person.spec.ts b/shared-libs/cht-datasource/test/local/person.spec.ts index c1d77d9ec54..3c3a35edff9 100644 --- a/shared-libs/cht-datasource/test/local/person.spec.ts +++ b/shared-libs/cht-datasource/test/local/person.spec.ts @@ -271,9 +271,7 @@ describe('local person', () => { expect(settingsGetAll.calledOnce).to.be.true; expect(getPersonTypes.calledOnceWithExactly(settings)).to.be.true; - expect(queryDocsByKeyOuter.calledOnceWithExactly( - localContext.medicDb, 'medic-client/contacts_by_type' - )).to.be.true; + expect(queryDocsByKeyOuter.notCalled).to.be.true; expect(queryDocsByKeyInner.notCalled).to.be.true; expect(isPerson.notCalled).to.be.true; }); From 7b94447d26c5d9e1a05eab9a92481b3b6a2a8297 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Thu, 25 Jul 2024 13:24:58 +0545 Subject: [PATCH 15/56] Refactor the AsyncIterator --- shared-libs/cht-datasource/src/index.ts | 6 ++++++ shared-libs/cht-datasource/src/person.ts | 23 +++++++++-------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index 239e30dd163..b79624c318b 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -99,6 +99,12 @@ export const getDatasource = (ctx: DataContext) => { getPage: (personType: string, limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)( Qualifier.byContactType(personType), limit, skip ), + + /** + * TODO: write this + * @param personType the type of people to return + */ + getAll: (personType: string) => ctx.bind(Person.v1.getAll)(Qualifier.byContactType(personType)), } } }; diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 2e8b7dff312..88484362272 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -91,27 +91,22 @@ export namespace v1 { const getPeopleGenerator = () => (context: DataContext) => { assertDataContext(context); - return async (personType: ContactTypeQualifier) => { + return async function* (personType: ContactTypeQualifier){ assertTypeQualifier(personType); const limit = 100; let skip = 0; - return { - [Symbol.asyncIterator]: async function*() { - while (true) { - const docs = await context.bind(getPage)(personType, limit, skip); + while (true) { + const docs = await context.bind(getPage)(personType, limit, skip); - if (docs.length < 100) { - yield docs; - break; - } + yield docs; - skip += limit; - - yield docs; - } + if (docs.length < limit) { + break; } - }; + + skip += limit; + } }; }; From 4b534f3b5052fd0b90b7820d536fa53eb430e570 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Thu, 25 Jul 2024 13:48:30 +0545 Subject: [PATCH 16/56] Add documentation for new functions --- shared-libs/cht-datasource/src/index.ts | 4 +++- shared-libs/cht-datasource/src/person.ts | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index b79624c318b..377e39a6097 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -101,8 +101,10 @@ export const getDatasource = (ctx: DataContext) => { ), /** - * TODO: write this + * Returns a `AsyncGenerator` for fetching batches of people data. * @param personType the type of people to return + * @returns a `AsyncGenerator` for fetching batches of people data. + * @throws Error if no type is provided or if the type is not for a person */ getAll: (personType: string) => ctx.bind(Person.v1.getAll)(Qualifier.byContactType(personType)), } diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 88484362272..e15f2ca38e1 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -87,11 +87,16 @@ export namespace v1 { return curriedFn; }; - // NOTE: there's probably a better name for this function const getPeopleGenerator = () => (context: DataContext) => { assertDataContext(context); - return async function* (personType: ContactTypeQualifier){ + /** + * Creates an iterator-like object for fetching batches of people data. + * @param personType - The type of person to fetch + * @throws throws an error if the provided personType is invalid. + * @returns An iterator-like object with a next method for fetching data. + */ + const curriedFn = async function* (personType: ContactTypeQualifier): AsyncGenerator { assertTypeQualifier(personType); const limit = 100; let skip = 0; @@ -108,6 +113,7 @@ export namespace v1 { skip += limit; } }; + return curriedFn; }; /** @@ -135,7 +141,16 @@ export namespace v1 { export const getPage = getPeople(Local.Person.v1.getPage, Remote.Person.v1.getPage); /** - * TODO: Write docs + * Returns a people generator function for fetching batches of people data. + * @param context the current data context + * @returns a generator function that creates an iterator-like object for fetching people data. + * @example + * const getAllIterator = Person.v1.getAll(ctx)(Qualifier.byContactType('person')); + * for await (const page of getAllIterator) { + * for (const doc of page) { + * ... + * } + * } */ export const getAll = getPeopleGenerator(); } From eff3972dfdb391567879bcc2b74810f4b0a9484c Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Thu, 25 Jul 2024 16:56:15 +0545 Subject: [PATCH 17/56] Add unit tests --- shared-libs/cht-datasource/test/index.spec.ts | 21 ++++- .../cht-datasource/test/person.spec.ts | 85 +++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index d1bdfb8e133..dd67e1c1ae1 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -7,7 +7,6 @@ import * as Qualifier from '../src/qualifier'; import sinon, { SinonStub } from 'sinon'; import * as Context from '../src/libs/data-context'; import { DataContext } from '../src'; -import { Doc } from '../src/libs/doc'; describe('CHT Script API - getDatasource', () => { let dataContext: DataContext; @@ -93,7 +92,7 @@ describe('CHT Script API - getDatasource', () => { beforeEach(() => person = v1.person); it('contains expected keys', () => { - expect(person).to.have.all.keys(['getByUuid', 'getByUuidWithLineage', 'getPage']); + expect(person).to.have.all.keys(['getAll', 'getByUuid', 'getByUuidWithLineage', 'getPage']); }); it('getByUuid', async () => { @@ -143,6 +142,24 @@ describe('CHT Script API - getDatasource', () => { expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip)).to.be.true; expect(byContactType.calledOnceWithExactly(personType)).to.be.true; }); + + it('getAll', async () => { + const mockAsyncGenerator = async function* () { + yield []; + }; + const personGetAll = sinon.stub().resolves(mockAsyncGenerator); + dataContextBind.returns(personGetAll); + const personType = 'person'; + const personTypeQualifier = { contactType: personType }; + const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); + + const res = await person.getAll(personType); + + expect(res).to.deep.equal(mockAsyncGenerator); + expect(dataContextBind.calledOnceWithExactly(Person.v1.getAll)).to.be.true; + expect(personGetAll.calledOnceWithExactly(personTypeQualifier)).to.be.true; + expect(byContactType.calledOnceWithExactly(personType)).to.be.true; + }); }); }); }); diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index a9e74dccc9c..e09e194bd94 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -204,5 +204,90 @@ describe('person', () => { expect(getPage.notCalled).to.be.true; }); }); + + describe('getAll', () => { + const personType = 'person'; + const personTypeQualifier = {contactType: personType} as const; + const limit = 100; + const skip0 = 0; + const people = [{ _id: 'person1' }, { _id: 'person2' }, { _id: 'person3' }] as Person.v1.Person[]; + const person = { _id: 'person' } as Person.v1.Person; + + let personGetPage: sinon.SinonStub; + + beforeEach(() => { + personGetPage = sinon.stub(Person.v1, 'getPage'); + dataContext.bind = sinon.stub().returns(personGetPage); + }); + + it('should get people generator with correct parameters', async () => { + isContactTypeQualifier.returns(true); + personGetPage.returns(people); + + const generator = Person.v1.getAll(dataContext)(personTypeQualifier); + const res = await generator.next(); + await generator.next(); + + expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; + expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip0)).to.be.true; + expect(res.value).to.deep.equal(people); + expect(isContactTypeQualifier.calledOnceWithExactly(personTypeQualifier)).to.be.true; + }); + + it('should get people generator with multiple batches with correct parameters', async () => { + isContactTypeQualifier.returns(true); + const firstPage = Array.from({ length: 100 }, () => ({ ...person })); + const secondPage = Array.from({ length: 99 }, () => ({ ...person })); + personGetPage.onCall(0).returns(firstPage); + personGetPage.onCall(1).returns(secondPage); + + const generator = Person.v1.getAll(dataContext)(personTypeQualifier); + const page1 = await generator.next(); + const page2 = await generator.next(); + await generator.next(); + + expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; + expect(personGetPage.callCount).to.equal(2); + expect(page1.value).to.deep.equal(firstPage); + expect(page2.value).to.deep.equal(secondPage); + expect(isContactTypeQualifier.calledOnceWithExactly(personTypeQualifier)).to.be.true; + }); + + it('should handle empty result set', async () => { + isContactTypeQualifier.returns(true); + personGetPage.returns([]); + + const generator = Person.v1.getAll(dataContext)(personTypeQualifier); + const res = await generator.next(); + await generator.next(); + + expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; + expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip0)).to.be.true; + expect(res.value).to.deep.equal([]); + expect(isContactTypeQualifier.calledOnceWithExactly(personTypeQualifier)).to.be.true; + }); + + it('should throw an error for invalid datacontext', () => { + const errMsg = 'Invalid data context [null].'; + isContactTypeQualifier.returns(true); + assertDataContext.throws(new Error(errMsg)); + + expect(() => Person.v1.getAll(dataContext)).to.throw(errMsg); + expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; + expect(personGetPage.notCalled).to.be.true; + expect(isContactTypeQualifier.notCalled).to.be.true; + }); + + it('should throw an error for invalid personType', async () => { + isContactTypeQualifier.returns(false); + + const generator = Person.v1.getAll(dataContext)(personTypeQualifier); + await expect(generator.next()).to.be.rejectedWith(`Invalid type [${JSON.stringify(personTypeQualifier)}].`); + + expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; + expect(personGetPage.notCalled).to.be.true; + expect(isContactTypeQualifier.calledOnceWithExactly(personTypeQualifier)).to.be.true; + }); + }); }); }); From 9a94d36446fc894407382012bf146511cf31a1ef Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Thu, 25 Jul 2024 20:37:59 +0545 Subject: [PATCH 18/56] Add comment on unit tests for clarity --- shared-libs/cht-datasource/test/person.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index e09e194bd94..e01424fcc1e 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -226,7 +226,7 @@ describe('person', () => { const generator = Person.v1.getAll(dataContext)(personTypeQualifier); const res = await generator.next(); - await generator.next(); + await generator.next(); // the exit call expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip0)).to.be.true; @@ -244,7 +244,7 @@ describe('person', () => { const generator = Person.v1.getAll(dataContext)(personTypeQualifier); const page1 = await generator.next(); const page2 = await generator.next(); - await generator.next(); + await generator.next(); // the exit call expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(personGetPage.callCount).to.equal(2); @@ -259,7 +259,7 @@ describe('person', () => { const generator = Person.v1.getAll(dataContext)(personTypeQualifier); const res = await generator.next(); - await generator.next(); + await generator.next(); // the exit call expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip0)).to.be.true; From 141cfab9252b024119fba3d660963220e65e7c08 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Fri, 26 Jul 2024 18:41:39 +0545 Subject: [PATCH 19/56] Update shared-libs/cht-datasource/src/person.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/person.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index e15f2ca38e1..7cf77c18cfc 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -91,10 +91,10 @@ export namespace v1 { assertDataContext(context); /** - * Creates an iterator-like object for fetching batches of people data. - * @param personType - The type of person to fetch - * @throws throws an error if the provided personType is invalid. - * @returns An iterator-like object with a next method for fetching data. + * Returns a generator that yeilds the persons with the given type. + * @param personType The type of person to fetch + * @returns a generator that yeilds the persons with the given type + * @throws Error if no person type is provided or if the type is not for a person */ const curriedFn = async function* (personType: ContactTypeQualifier): AsyncGenerator { assertTypeQualifier(personType); From 5daa8cfe276db1049187a6e34d50e2ea66a34dde Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Fri, 26 Jul 2024 18:42:32 +0545 Subject: [PATCH 20/56] Update shared-libs/cht-datasource/src/person.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/person.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 7cf77c18cfc..b5f3f724889 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -141,16 +141,10 @@ export namespace v1 { export const getPage = getPeople(Local.Person.v1.getPage, Remote.Person.v1.getPage); /** - * Returns a people generator function for fetching batches of people data. + * Returns a function for retrieving people from the given data context. * @param context the current data context - * @returns a generator function that creates an iterator-like object for fetching people data. - * @example - * const getAllIterator = Person.v1.getAll(ctx)(Qualifier.byContactType('person')); - * for await (const page of getAllIterator) { - * for (const doc of page) { - * ... - * } - * } + * @returns a function for retrieving people + * @throws Error if a data context is not provided */ export const getAll = getPeopleGenerator(); } From 901535345e4b9eae6c897482485594b5d73cd55d Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Mon, 29 Jul 2024 16:03:51 +0545 Subject: [PATCH 21/56] Address PR comments --- .../cht-datasource/src/libs/data-context.ts | 23 ++++++++ shared-libs/cht-datasource/src/person.ts | 57 ++++++++---------- .../test/libs/data-context.spec.ts | 59 ++++++++++++++++++- .../cht-datasource/test/person.spec.ts | 56 ++++++++---------- 4 files changed, 130 insertions(+), 65 deletions(-) diff --git a/shared-libs/cht-datasource/src/libs/data-context.ts b/shared-libs/cht-datasource/src/libs/data-context.ts index d58c017c780..d11820b2010 100644 --- a/shared-libs/cht-datasource/src/libs/data-context.ts +++ b/shared-libs/cht-datasource/src/libs/data-context.ts @@ -39,3 +39,26 @@ export const adapt = ( assertRemoteDataContext(context); return remote(context); }; + +/** @internal */ +export const getDocumentStream = async function* ( + fetchFunction: (...args: Args) => Promise, + fetchFunctionArgs: { + limit: number; + skip: number; + } & Record +): AsyncGenerator { + const { limit } = fetchFunctionArgs; + let { skip } = fetchFunctionArgs; + const hasMoreResults = () => skip && skip % limit === 0; + + do { + const docs = await fetchFunction(...(Object.values(fetchFunctionArgs) as Args)); + + for (const doc of docs) { + yield doc; + } + + skip += docs.length; + } while (hasMoreResults()); +}; diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index b5f3f724889..93e6b330141 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -1,5 +1,5 @@ -import { isContactTypeQualifier, isUuidQualifier, ContactTypeQualifier, UuidQualifier } from './qualifier'; -import { adapt, assertDataContext, DataContext } from './libs/data-context'; +import { ContactTypeQualifier, isContactTypeQualifier, isUuidQualifier, UuidQualifier } from './qualifier'; +import { adapt, assertDataContext, DataContext, getDocumentStream } from './libs/data-context'; import { Contact, NormalizedParent } from './libs/contact'; import * as Remote from './remote'; import * as Local from './local'; @@ -87,35 +87,6 @@ export namespace v1 { return curriedFn; }; - const getPeopleGenerator = () => (context: DataContext) => { - assertDataContext(context); - - /** - * Returns a generator that yeilds the persons with the given type. - * @param personType The type of person to fetch - * @returns a generator that yeilds the persons with the given type - * @throws Error if no person type is provided or if the type is not for a person - */ - const curriedFn = async function* (personType: ContactTypeQualifier): AsyncGenerator { - assertTypeQualifier(personType); - const limit = 100; - let skip = 0; - - while (true) { - const docs = await context.bind(getPage)(personType, limit, skip); - - yield docs; - - if (docs.length < limit) { - break; - } - - skip += limit; - } - }; - return curriedFn; - }; - /** * Returns a person for the given qualifier. * @param context the current data context @@ -141,10 +112,28 @@ export namespace v1 { export const getPage = getPeople(Local.Person.v1.getPage, Remote.Person.v1.getPage); /** - * Returns a function for retrieving people from the given data context. + * Returns a generator function for retrieving people from the given data context. * @param context the current data context - * @returns a function for retrieving people + * @returns a generator function for retrieving people * @throws Error if a data context is not provided */ - export const getAll = getPeopleGenerator(); + export const getAll = ( + context: DataContext + ): (personType: ContactTypeQualifier) => AsyncGenerator => { + assertDataContext(context); + + /** + * Returns a generator that yields the persons with the given type. + * @param personType The type of person to fetch + * @returns a generator that yields the persons with the given type + * @throws Error if no person type is provided or if the type is not for a person + */ + return async function* (personType: ContactTypeQualifier): AsyncGenerator { + assertTypeQualifier(personType); + const getPage = context.bind(v1.getPage); + const limit = 100; + const skip = 0; + yield* getDocumentStream(getPage, { personType, limit, skip }) as AsyncGenerator; + }; + }; } diff --git a/shared-libs/cht-datasource/test/libs/data-context.spec.ts b/shared-libs/cht-datasource/test/libs/data-context.spec.ts index 82ba05ba0da..14b9375985b 100644 --- a/shared-libs/cht-datasource/test/libs/data-context.spec.ts +++ b/shared-libs/cht-datasource/test/libs/data-context.spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { adapt, assertDataContext } from '../../src/libs/data-context'; +import { adapt, assertDataContext, getDocumentStream } from '../../src/libs/data-context'; import * as LocalContext from '../../src/local/libs/data-context'; import * as RemoteContext from '../../src/remote/libs/data-context'; import sinon, { SinonStub } from 'sinon'; @@ -118,4 +118,61 @@ describe('context lib', () => { expect(remote.notCalled).to.be.true; }); }); + + describe('getDocumentStream', () => { + let fetchFunctionStub: SinonStub; + + beforeEach(() => { + fetchFunctionStub = sinon.stub(); + }); + + it('yields document one by one', async () => { + const mockDocs = [{ id: 1 }, { id: 2 }, { id: 3 }]; + const args = { limit: 4, skip: 0, extraArgs: 'value'}; + fetchFunctionStub.resolves(mockDocs); + + const generator = getDocumentStream(fetchFunctionStub, args); + + const results = []; + + for await (const doc of generator) { + results.push(doc); + } + + expect(results).to.deep.equal(mockDocs); + expect(fetchFunctionStub.calledOnceWithExactly(...Object.values(args))).to.be.true; + }); + + it('should handle multiple pages', async () => { + const mockDocs1 = [{ id: 1 }, { id: 2 }]; + const mockDocs2 = [{ id: 3 }]; + + fetchFunctionStub.onFirstCall().resolves(mockDocs1); + fetchFunctionStub.onSecondCall().resolves(mockDocs2); + + const generator = getDocumentStream(fetchFunctionStub, { limit: 2, skip: 0}); + + const results = []; + for await (const doc of generator) { + results.push(doc); + } + + expect(results).to.deep.equal([...mockDocs1, ...mockDocs2]); + expect(fetchFunctionStub.callCount).to.equal(2); + expect(fetchFunctionStub.firstCall.args).to.deep.equal([2, 0]); + expect(fetchFunctionStub.secondCall.args).to.deep.equal([2, 0]); + }); + + it('should handle empty result', async () => { + fetchFunctionStub.resolves([]); + + const generator = getDocumentStream(fetchFunctionStub, { limit: 10, skip: 0 }); + + const result = await generator.next(); + + expect(result.done).to.be.true; + expect(result.value).to.be.equal(undefined); + expect(fetchFunctionStub.calledOnce).to.be.true; + }); + }); }); diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index e01424fcc1e..a7dc144ae11 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -209,61 +209,57 @@ describe('person', () => { const personType = 'person'; const personTypeQualifier = {contactType: personType} as const; const limit = 100; - const skip0 = 0; - const people = [{ _id: 'person1' }, { _id: 'person2' }, { _id: 'person3' }] as Person.v1.Person[]; - const person = { _id: 'person' } as Person.v1.Person; + const skip = 0; + const firstPerson = { _id: 'person1' } as Person.v1.Person; + const secondPerson = { _id: 'person2' } as Person.v1.Person; + const thirdPerson = { _id: 'person3' } as Person.v1.Person; + const people = [firstPerson, secondPerson, thirdPerson]; + const mockGenerator = async function* () { + for (const person of people) { + yield person; + } + }; let personGetPage: sinon.SinonStub; + let getDocumentStream: sinon.SinonStub; beforeEach(() => { personGetPage = sinon.stub(Person.v1, 'getPage'); dataContext.bind = sinon.stub().returns(personGetPage); + getDocumentStream = sinon.stub(Context, 'getDocumentStream'); }); it('should get people generator with correct parameters', async () => { isContactTypeQualifier.returns(true); - personGetPage.returns(people); + getDocumentStream.returns(mockGenerator()); const generator = Person.v1.getAll(dataContext)(personTypeQualifier); - const res = await generator.next(); - await generator.next(); // the exit call + const res = []; - expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; - expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip0)).to.be.true; - expect(res.value).to.deep.equal(people); - expect(isContactTypeQualifier.calledOnceWithExactly(personTypeQualifier)).to.be.true; - }); - - it('should get people generator with multiple batches with correct parameters', async () => { - isContactTypeQualifier.returns(true); - const firstPage = Array.from({ length: 100 }, () => ({ ...person })); - const secondPage = Array.from({ length: 99 }, () => ({ ...person })); - personGetPage.onCall(0).returns(firstPage); - personGetPage.onCall(1).returns(secondPage); - - const generator = Person.v1.getAll(dataContext)(personTypeQualifier); - const page1 = await generator.next(); - const page2 = await generator.next(); - await generator.next(); // the exit call + for await (const person of generator) { + res.push(person); + } expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; - expect(personGetPage.callCount).to.equal(2); - expect(page1.value).to.deep.equal(firstPage); - expect(page2.value).to.deep.equal(secondPage); + expect(getDocumentStream.calledOnceWithExactly(personGetPage, { + personType: personTypeQualifier, limit, skip + })).to.be.true; + expect(res).to.be.deep.equal(people); expect(isContactTypeQualifier.calledOnceWithExactly(personTypeQualifier)).to.be.true; }); it('should handle empty result set', async () => { isContactTypeQualifier.returns(true); - personGetPage.returns([]); + getDocumentStream.returns([]); const generator = Person.v1.getAll(dataContext)(personTypeQualifier); const res = await generator.next(); - await generator.next(); // the exit call expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; - expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip0)).to.be.true; - expect(res.value).to.deep.equal([]); + expect(getDocumentStream.calledOnceWithExactly(personGetPage, { + personType: personTypeQualifier, limit, skip + })).to.be.true; + expect(res.value).to.equal(undefined); expect(isContactTypeQualifier.calledOnceWithExactly(personTypeQualifier)).to.be.true; }); From 8be7b70a66713de15f809d5856426a4a23adcbe0 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 31 Jul 2024 11:49:20 +0545 Subject: [PATCH 22/56] Avoid using spread operator in getDocumentStream function --- shared-libs/cht-datasource/src/index.ts | 2 +- .../cht-datasource/src/libs/data-context.ts | 20 +++++++++++-------- shared-libs/cht-datasource/src/person.ts | 8 ++++++-- shared-libs/cht-datasource/test/index.spec.ts | 2 +- .../test/libs/data-context.spec.ts | 6 +++--- .../cht-datasource/test/person.spec.ts | 13 +++++++----- 6 files changed, 31 insertions(+), 20 deletions(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index 377e39a6097..c7907aca5be 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -97,7 +97,7 @@ export const getDatasource = (ctx: DataContext) => { * @throws Error if the provided skip is `< 0` */ getPage: (personType: string, limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)( - Qualifier.byContactType(personType), limit, skip + { personType: Qualifier.byContactType(personType), limit, skip } ), /** diff --git a/shared-libs/cht-datasource/src/libs/data-context.ts b/shared-libs/cht-datasource/src/libs/data-context.ts index d11820b2010..4a11e2b9cf6 100644 --- a/shared-libs/cht-datasource/src/libs/data-context.ts +++ b/shared-libs/cht-datasource/src/libs/data-context.ts @@ -40,20 +40,24 @@ export const adapt = ( return remote(context); }; +interface PaginationArgs { + limit: number; + skip: number; +} + +type FetchFunctionArgs> = PaginationArgs & TFields; + /** @internal */ -export const getDocumentStream = async function* ( - fetchFunction: (...args: Args) => Promise, - fetchFunctionArgs: { - limit: number; - skip: number; - } & Record -): AsyncGenerator { +export const getDocumentStream = async function* >( + fetchFunction: (args: FetchFunctionArgs) => Promise, + fetchFunctionArgs: FetchFunctionArgs +): AsyncGenerator { const { limit } = fetchFunctionArgs; let { skip } = fetchFunctionArgs; const hasMoreResults = () => skip && skip % limit === 0; do { - const docs = await fetchFunction(...(Object.values(fetchFunctionArgs) as Args)); + const docs = await fetchFunction(fetchFunctionArgs); for (const doc of docs) { yield doc; diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 93e6b330141..7cfb336dbd6 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -78,7 +78,11 @@ export namespace v1 { * @throws Error if the provided `limit` value is `<=0` * @throws Error if the provided `skip` value is `<0` */ - const curriedFn = async (personType: ContactTypeQualifier, limit = 100, skip = 0): Promise => { + const curriedFn = async ({personType, limit = 100, skip = 0}: { + personType: ContactTypeQualifier; + limit?: number; + skip?: number; + }): Promise => { assertTypeQualifier(personType); assertLimitAndSkip(limit, skip); @@ -133,7 +137,7 @@ export namespace v1 { const getPage = context.bind(v1.getPage); const limit = 100; const skip = 0; - yield* getDocumentStream(getPage, { personType, limit, skip }) as AsyncGenerator; + yield* getDocumentStream(getPage, { personType, limit, skip }); }; }; } diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index dd67e1c1ae1..d6bd7582540 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -139,7 +139,7 @@ describe('CHT Script API - getDatasource', () => { expect(returnedPeople).to.equal(expectedPeople); expect(dataContextBind.calledOnceWithExactly(Person.v1.getPage)).to.be.true; - expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip)).to.be.true; + expect(personGetPage.calledOnceWithExactly({ personType: personTypeQualifier, limit, skip })).to.be.true; expect(byContactType.calledOnceWithExactly(personType)).to.be.true; }); diff --git a/shared-libs/cht-datasource/test/libs/data-context.spec.ts b/shared-libs/cht-datasource/test/libs/data-context.spec.ts index 14b9375985b..82d57682e60 100644 --- a/shared-libs/cht-datasource/test/libs/data-context.spec.ts +++ b/shared-libs/cht-datasource/test/libs/data-context.spec.ts @@ -140,7 +140,7 @@ describe('context lib', () => { } expect(results).to.deep.equal(mockDocs); - expect(fetchFunctionStub.calledOnceWithExactly(...Object.values(args))).to.be.true; + expect(fetchFunctionStub.calledOnceWithExactly(args)).to.be.true; }); it('should handle multiple pages', async () => { @@ -159,8 +159,8 @@ describe('context lib', () => { expect(results).to.deep.equal([...mockDocs1, ...mockDocs2]); expect(fetchFunctionStub.callCount).to.equal(2); - expect(fetchFunctionStub.firstCall.args).to.deep.equal([2, 0]); - expect(fetchFunctionStub.secondCall.args).to.deep.equal([2, 0]); + expect(fetchFunctionStub.firstCall.args).to.deep.equal([{ limit: 2, skip: 0 }]); + expect(fetchFunctionStub.secondCall.args).to.deep.equal([{ limit: 2, skip: 0 }]); }); it('should handle empty result', async () => { diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index a7dc144ae11..d6013c5f290 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -145,7 +145,7 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); - const result = await Person.v1.getPage(dataContext)(personTypeQualifier, limit, skip); + const result = await Person.v1.getPage(dataContext)({ personType: personTypeQualifier, limit, skip }); expect(result).to.equal(people); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -169,7 +169,7 @@ describe('person', () => { it('throws an error if the qualifier is invalid', async () => { isContactTypeQualifier.returns(false); - await expect(Person.v1.getPage(dataContext)(invalidQualifier)) + await expect(Person.v1.getPage(dataContext)({personType: invalidQualifier})) .to.be.rejectedWith(`Invalid type [${JSON.stringify(invalidQualifier)}].`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -182,7 +182,7 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); - await expect(Person.v1.getPage(dataContext)(personTypeQualifier, invalidLimit, skip)) + await expect(Person.v1.getPage(dataContext)({ personType: personTypeQualifier, limit: invalidLimit, skip })) .to.be.rejectedWith(`limit must be a positive number`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -195,8 +195,11 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); - await expect(Person.v1.getPage(dataContext)(personTypeQualifier, limit, invalidSkip)) - .to.be.rejectedWith(`skip must be a non-negative number`); + await expect(Person.v1.getPage(dataContext)({ + personType: personTypeQualifier, + limit: limit, + skip: invalidSkip + })).to.be.rejectedWith(`skip must be a non-negative number`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; From 54da9fb73c48c87ed0bd46454bcd2ab4c4438826 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 31 Jul 2024 13:15:33 +0545 Subject: [PATCH 23/56] Fix documentation of previously changed functions --- shared-libs/cht-datasource/src/person.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 7cfb336dbd6..db366c33107 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -70,9 +70,10 @@ export namespace v1 { /** * Returns an array of people for the provided page specifications. - * @param personType the type of people to return - * @param limit the maximum number of people to return. Default is 100. - * @param skip the number of people to skip. Default is 0. + * @param params the function parameters + * @param params.personType the type of people to return + * @param params.limit the maximum number of people to return. Default is 100. + * @param params.skip the number of people to skip. Default is 0. * @returns an array of people for the provided page specifications. * @throws Error if `personType` qualifier is invalid * @throws Error if the provided `limit` value is `<=0` @@ -126,12 +127,6 @@ export namespace v1 { ): (personType: ContactTypeQualifier) => AsyncGenerator => { assertDataContext(context); - /** - * Returns a generator that yields the persons with the given type. - * @param personType The type of person to fetch - * @returns a generator that yields the persons with the given type - * @throws Error if no person type is provided or if the type is not for a person - */ return async function* (personType: ContactTypeQualifier): AsyncGenerator { assertTypeQualifier(personType); const getPage = context.bind(v1.getPage); From 6064c9953a7c28c731d5e373c769cb05e5b612bd Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:19:35 +0545 Subject: [PATCH 24/56] Update shared-libs/cht-datasource/src/person.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/person.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index db366c33107..2dfe4222f19 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -75,7 +75,7 @@ export namespace v1 { * @param params.limit the maximum number of people to return. Default is 100. * @param params.skip the number of people to skip. Default is 0. * @returns an array of people for the provided page specifications. - * @throws Error if `personType` qualifier is invalid + * @throws Error if no type is provided or if the type is not for a person * @throws Error if the provided `limit` value is `<=0` * @throws Error if the provided `skip` value is `<0` */ From 9fb6c7b1293e273256a22462c871fae6ea9e6e4b Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:20:35 +0545 Subject: [PATCH 25/56] Update shared-libs/cht-datasource/test/local/libs/lineage.spec.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/test/local/libs/lineage.spec.ts | 1 - 1 file changed, 1 deletion(-) 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 a3c9c453062..4f39d989071 100644 --- a/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts +++ b/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts @@ -16,7 +16,6 @@ describe('local lineage lib', () => { beforeEach(() => { debug = sinon.stub(logger, 'debug'); }); - // afterEach(() => sinon.restore()); it('getLineageDocsById', async () => { From 2364d82303064eb7e143483a018b2027d3bd6ecb Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:21:23 +0545 Subject: [PATCH 26/56] Update shared-libs/cht-datasource/test/local/libs/lineage.spec.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/test/local/libs/lineage.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4f39d989071..60539b9cb93 100644 --- a/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts +++ b/shared-libs/cht-datasource/test/local/libs/lineage.spec.ts @@ -20,7 +20,7 @@ describe('local lineage lib', () => { it('getLineageDocsById', async () => { const uuid = '123'; - const queryFn = sinon.stub().returns([]); + const queryFn = sinon.stub().resolves([]); const queryDocsByRange = sinon .stub(LocalDoc, 'queryDocsByRange') .returns(queryFn); From 1b87d4af29783ce9633f233f8e8bf1ec023bd5d2 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:21:37 +0545 Subject: [PATCH 27/56] Update shared-libs/cht-datasource/test/local/person.spec.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/test/local/person.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-libs/cht-datasource/test/local/person.spec.ts b/shared-libs/cht-datasource/test/local/person.spec.ts index 3c3a35edff9..e2022fd0fea 100644 --- a/shared-libs/cht-datasource/test/local/person.spec.ts +++ b/shared-libs/cht-datasource/test/local/person.spec.ts @@ -291,7 +291,7 @@ describe('local person', () => { expect(isPerson.notCalled).to.be.true; }); - it('returns null valued array if rows returned from database are not docs', async () => { + it('returns empty array if rows returned from database are not persons', async () => { const doc = { type: 'person'}; const docs = [doc, doc, doc]; queryDocsByKeyInner.resolves(docs); From 9a828a628cc27d3dbe7adca873449b803bc3243a Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Fri, 26 Jul 2024 16:02:04 +0545 Subject: [PATCH 28/56] feat(#9237): Address PR comments --- shared-libs/cht-datasource/src/index.ts | 2 +- shared-libs/cht-datasource/src/person.ts | 70 +++++++++---------- shared-libs/cht-datasource/test/index.spec.ts | 4 +- 3 files changed, 36 insertions(+), 40 deletions(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index c7907aca5be..defe74d3823 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -96,7 +96,7 @@ export const getDatasource = (ctx: DataContext) => { * @throws Error if the provided limit is `<= 0` * @throws Error if the provided skip is `< 0` */ - getPage: (personType: string, limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)( + getPageByType: (personType: string, limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)( { personType: Qualifier.byContactType(personType), limit, skip } ), diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 2dfe4222f19..ea86c0319fe 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -40,12 +40,15 @@ export namespace v1 { } }; - const assertLimitAndSkip = (limit: unknown, skip: unknown) => { - if (typeof limit !== 'number' || limit <= 0) { - throw new Error('limit must be a positive number'); + const assertLimit = (limit: unknown) => { + if (typeof limit !== 'number' || !Number.isInteger(limit) || limit <= 0) { + throw new Error(`The limit must be a positive number: [${String(limit)}]`); } - if (typeof skip !== 'number' || skip < 0) { - throw new Error('skip must be a non-negative number'); + }; + + const assertSkip = (skip: unknown) => { + if (typeof skip !== 'number' || !Number.isInteger(skip) || skip < 0) { + throw new Error(`The skip must be a non-negative number: [${String(skip)}]`); } }; @@ -61,37 +64,6 @@ export namespace v1 { }; }; - const getPeople = ( - localFn: (c: LocalDataContext) => (personType: ContactTypeQualifier, limit: number, skip: number) => Promise, - remoteFn: (c: RemoteDataContext) => (personType: ContactTypeQualifier, limit: number, skip: number) => Promise - ) => (context: DataContext) => { - assertDataContext(context); - const fn = adapt(context, localFn, remoteFn); - - /** - * Returns an array of people for the provided page specifications. - * @param params the function parameters - * @param params.personType the type of people to return - * @param params.limit the maximum number of people to return. Default is 100. - * @param params.skip the number of people to skip. Default is 0. - * @returns an array of people for the provided page specifications. - * @throws Error if no type is provided or if the type is not for a person - * @throws Error if the provided `limit` value is `<=0` - * @throws Error if the provided `skip` value is `<0` - */ - const curriedFn = async ({personType, limit = 100, skip = 0}: { - personType: ContactTypeQualifier; - limit?: number; - skip?: number; - }): Promise => { - assertTypeQualifier(personType); - assertLimitAndSkip(limit, skip); - - return fn(personType, limit, skip); - }; - return curriedFn; - }; - /** * Returns a person for the given qualifier. * @param context the current data context @@ -114,7 +86,31 @@ export namespace v1 { * @returns a function for retrieving a paged array of people * @throws Error if a data context is not provided */ - export const getPage = getPeople(Local.Person.v1.getPage, Remote.Person.v1.getPage); + export const getPage = ( + context: DataContext + ): (personType: ContactTypeQualifier, limit: number, skip: number) => Promise => { + assertDataContext(context); + const fn = adapt(context, Local.Person.v1.getPage, Remote.Person.v1.getPage); + + /** + * Returns an array of people for the provided page specifications. + * @param personType the type of people to return + * @param limit the maximum number of people to return. Default is 100. + * @param skip the number of people to skip. Default is 0. + * @returns an array of people for the provided page specifications. + * @throws Error if no type is provided or if the type is not for a person + * @throws Error if the provided `limit` value is `<=0` + * @throws Error if the provided `skip` value is `<0` + */ + const curriedFn = async (personType: ContactTypeQualifier, limit = 100, skip = 0): Promise => { + assertTypeQualifier(personType); + assertLimit(limit); + assertSkip(skip); + + return fn(personType, limit, skip); + }; + return curriedFn; + }; /** * Returns a generator function for retrieving people from the given data context. diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index d6bd7582540..1984be59f4b 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -92,7 +92,7 @@ describe('CHT Script API - getDatasource', () => { beforeEach(() => person = v1.person); it('contains expected keys', () => { - expect(person).to.have.all.keys(['getAll', 'getByUuid', 'getByUuidWithLineage', 'getPage']); + expect(person).to.have.all.keys(['getAll', 'getByUuid', 'getByUuidWithLineage', 'getPageByType']); }); it('getByUuid', async () => { @@ -135,7 +135,7 @@ describe('CHT Script API - getDatasource', () => { const personTypeQualifier = { contactType: personType }; const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); - const returnedPeople = await person.getPage(personType, limit, skip); + const returnedPeople = await person.getPageByType(personType, limit, skip); expect(returnedPeople).to.equal(expectedPeople); expect(dataContextBind.calledOnceWithExactly(Person.v1.getPage)).to.be.true; From cd81f2df4fd87b612aa6a334b0591e6e6cb78382 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 24 Jul 2024 21:42:45 +0545 Subject: [PATCH 29/56] Write getAll generator in src/person.ts --- shared-libs/cht-datasource/src/person.ts | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index ea86c0319fe..e546a583c4e 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -64,6 +64,34 @@ export namespace v1 { }; }; + // NOTE: there's probably a better name for this function + const getPeopleGenerator = () => (context: DataContext) => { + assertDataContext(context); + + return async (personType: ContactTypeQualifier) => { + assertTypeQualifier(personType); + const limit = 100; + let skip = 0; + + return { + [Symbol.asyncIterator]: async function*() { + while (true) { + const docs = await context.bind(getPage)(personType, limit, skip); + + if (docs.length < 100) { + yield docs; + break; + } + + skip += limit; + + yield docs; + } + } + }; + }; + }; + /** * Returns a person for the given qualifier. * @param context the current data context From cd344d5c512a23b676afb116b5f23c359de3d8f9 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Thu, 25 Jul 2024 13:24:58 +0545 Subject: [PATCH 30/56] Refactor the AsyncIterator --- shared-libs/cht-datasource/src/person.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index e546a583c4e..6bc1883951e 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -68,27 +68,22 @@ export namespace v1 { const getPeopleGenerator = () => (context: DataContext) => { assertDataContext(context); - return async (personType: ContactTypeQualifier) => { + return async function* (personType: ContactTypeQualifier){ assertTypeQualifier(personType); const limit = 100; let skip = 0; - return { - [Symbol.asyncIterator]: async function*() { - while (true) { - const docs = await context.bind(getPage)(personType, limit, skip); + while (true) { + const docs = await context.bind(getPage)(personType, limit, skip); - if (docs.length < 100) { - yield docs; - break; - } + yield docs; - skip += limit; - - yield docs; - } + if (docs.length < limit) { + break; } - }; + + skip += limit; + } }; }; From 5d7fc9830e49f00ffe20a31bd4e4a8ceece81fc0 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Thu, 25 Jul 2024 13:48:30 +0545 Subject: [PATCH 31/56] Add documentation for new functions --- shared-libs/cht-datasource/src/person.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 6bc1883951e..16570b38d20 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -68,7 +68,13 @@ export namespace v1 { const getPeopleGenerator = () => (context: DataContext) => { assertDataContext(context); - return async function* (personType: ContactTypeQualifier){ + /** + * Creates an iterator-like object for fetching batches of people data. + * @param personType - The type of person to fetch + * @throws throws an error if the provided personType is invalid. + * @returns An iterator-like object with a next method for fetching data. + */ + const curriedFn = async function* (personType: ContactTypeQualifier): AsyncGenerator { assertTypeQualifier(personType); const limit = 100; let skip = 0; @@ -85,6 +91,7 @@ export namespace v1 { skip += limit; } }; + return curriedFn; }; /** From 4279c60cb699ba899f64b6fc5926de4914298be7 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Thu, 25 Jul 2024 16:56:15 +0545 Subject: [PATCH 32/56] Add unit tests --- shared-libs/cht-datasource/test/index.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index 1984be59f4b..972484e8ccf 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -160,6 +160,24 @@ describe('CHT Script API - getDatasource', () => { expect(personGetAll.calledOnceWithExactly(personTypeQualifier)).to.be.true; expect(byContactType.calledOnceWithExactly(personType)).to.be.true; }); + + it('getAll', async () => { + const mockAsyncGenerator = async function* () { + yield []; + }; + const personGetAll = sinon.stub().resolves(mockAsyncGenerator); + dataContextBind.returns(personGetAll); + const personType = 'person'; + const personTypeQualifier = { contactType: personType }; + const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); + + const res = await person.getAll(personType); + + expect(res).to.deep.equal(mockAsyncGenerator); + expect(dataContextBind.calledOnceWithExactly(Person.v1.getAll)).to.be.true; + expect(personGetAll.calledOnceWithExactly(personTypeQualifier)).to.be.true; + expect(byContactType.calledOnceWithExactly(personType)).to.be.true; + }); }); }); }); From 190f69e22b056ffb6a77ea04f6cc2765b2362754 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Mon, 29 Jul 2024 16:03:51 +0545 Subject: [PATCH 33/56] Address PR comments --- shared-libs/cht-datasource/src/person.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 16570b38d20..d39ff305f8e 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -64,6 +64,7 @@ export namespace v1 { }; }; +<<<<<<< HEAD // NOTE: there's probably a better name for this function const getPeopleGenerator = () => (context: DataContext) => { assertDataContext(context); @@ -94,6 +95,8 @@ export namespace v1 { return curriedFn; }; +======= +>>>>>>> b02973b26 (Address PR comments) /** * Returns a person for the given qualifier. * @param context the current data context From cb34075483b5fb5a60cc923e1dc3fa6f118a1d91 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 31 Jul 2024 14:35:33 +0545 Subject: [PATCH 34/56] Fix conflict with target branch --- shared-libs/cht-datasource/src/person.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index d39ff305f8e..29cac45eb2e 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -26,6 +26,12 @@ export namespace v1 { readonly parent?: Place.v1.PlaceWithLineage | NormalizedParent, } + interface getPageParams { + personType: ContactTypeQualifier; + limit?: number; + skip?: number; + } + const assertPersonQualifier: (qualifier: unknown) => asserts qualifier is UuidQualifier = (qualifier: unknown) => { if (!isUuidQualifier(qualifier)) { throw new Error(`Invalid identifier [${JSON.stringify(qualifier)}].`); @@ -121,21 +127,22 @@ export namespace v1 { */ export const getPage = ( context: DataContext - ): (personType: ContactTypeQualifier, limit: number, skip: number) => Promise => { + ): ({ personType, limit, skip }: getPageParams) => Promise => { assertDataContext(context); const fn = adapt(context, Local.Person.v1.getPage, Remote.Person.v1.getPage); /** * Returns an array of people for the provided page specifications. - * @param personType the type of people to return - * @param limit the maximum number of people to return. Default is 100. - * @param skip the number of people to skip. Default is 0. + * @param params the function params + * @param params.personType the type of people to return + * @param params.limit the maximum number of people to return. Default is 100. + * @param params.skip the number of people to skip. Default is 0. * @returns an array of people for the provided page specifications. * @throws Error if no type is provided or if the type is not for a person * @throws Error if the provided `limit` value is `<=0` * @throws Error if the provided `skip` value is `<0` */ - const curriedFn = async (personType: ContactTypeQualifier, limit = 100, skip = 0): Promise => { + const curriedFn = async ({ personType, limit = 100, skip = 0}: getPageParams): Promise => { assertTypeQualifier(personType); assertLimit(limit); assertSkip(skip); From 154b295bd63e11042408e4e662d3cd4afba290fe Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 31 Jul 2024 15:04:10 +0545 Subject: [PATCH 35/56] Fix conflict with target branch --- shared-libs/cht-datasource/src/person.ts | 33 ------------------------ 1 file changed, 33 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 29cac45eb2e..eb0253c5ab8 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -70,39 +70,6 @@ export namespace v1 { }; }; -<<<<<<< HEAD - // NOTE: there's probably a better name for this function - const getPeopleGenerator = () => (context: DataContext) => { - assertDataContext(context); - - /** - * Creates an iterator-like object for fetching batches of people data. - * @param personType - The type of person to fetch - * @throws throws an error if the provided personType is invalid. - * @returns An iterator-like object with a next method for fetching data. - */ - const curriedFn = async function* (personType: ContactTypeQualifier): AsyncGenerator { - assertTypeQualifier(personType); - const limit = 100; - let skip = 0; - - while (true) { - const docs = await context.bind(getPage)(personType, limit, skip); - - yield docs; - - if (docs.length < limit) { - break; - } - - skip += limit; - } - }; - return curriedFn; - }; - -======= ->>>>>>> b02973b26 (Address PR comments) /** * Returns a person for the given qualifier. * @param context the current data context From c233d0b8623e4dc810304fe163f1e713e5c98514 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Wed, 31 Jul 2024 21:17:49 +0545 Subject: [PATCH 36/56] Update shared-libs/cht-datasource/src/person.ts Co-authored-by: Mokhtar --- shared-libs/cht-datasource/src/person.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index eb0253c5ab8..41969dac70e 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -26,7 +26,7 @@ export namespace v1 { readonly parent?: Place.v1.PlaceWithLineage | NormalizedParent, } - interface getPageParams { + interface GetPageParams { personType: ContactTypeQualifier; limit?: number; skip?: number; From a9cf536d636dde062c7b2331d313e28db48f793c Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 31 Jul 2024 21:31:43 +0545 Subject: [PATCH 37/56] Address PR comments --- shared-libs/cht-datasource/src/person.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 41969dac70e..e31577a31d7 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -94,7 +94,7 @@ export namespace v1 { */ export const getPage = ( context: DataContext - ): ({ personType, limit, skip }: getPageParams) => Promise => { + ): ({ personType, limit, skip }: GetPageParams) => Promise => { assertDataContext(context); const fn = adapt(context, Local.Person.v1.getPage, Remote.Person.v1.getPage); @@ -109,7 +109,7 @@ export namespace v1 { * @throws Error if the provided `limit` value is `<=0` * @throws Error if the provided `skip` value is `<0` */ - const curriedFn = async ({ personType, limit = 100, skip = 0}: getPageParams): Promise => { + const curriedFn = async ({ personType, limit = 100, skip = 0}: GetPageParams): Promise => { assertTypeQualifier(personType); assertLimit(limit); assertSkip(skip); From 318c1a46b4c4c051bf78ad29d0092e1116d46703 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Tue, 6 Aug 2024 14:55:52 +0545 Subject: [PATCH 38/56] Update shared-libs/cht-datasource/src/index.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index defe74d3823..75f80a65ce4 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -106,7 +106,7 @@ export const getDatasource = (ctx: DataContext) => { * @returns a `AsyncGenerator` for fetching batches of people data. * @throws Error if no type is provided or if the type is not for a person */ - getAll: (personType: string) => ctx.bind(Person.v1.getAll)(Qualifier.byContactType(personType)), + getByType: (personType: string) => ctx.bind(Person.v1.getAll)(Qualifier.byContactType(personType)), } } }; From bc92ff3fdc4e494e3d0f2a7944113a9c8fdad25b Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Tue, 6 Aug 2024 14:56:21 +0545 Subject: [PATCH 39/56] Update shared-libs/cht-datasource/src/index.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index 75f80a65ce4..f75abee0852 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -101,9 +101,9 @@ export const getDatasource = (ctx: DataContext) => { ), /** - * Returns a `AsyncGenerator` for fetching batches of people data. + * Returns a generator for fetching all people with the given type. * @param personType the type of people to return - * @returns a `AsyncGenerator` for fetching batches of people data. + * @returns a generator for fetching all people with the given type * @throws Error if no type is provided or if the type is not for a person */ getByType: (personType: string) => ctx.bind(Person.v1.getAll)(Qualifier.byContactType(personType)), From 0e6e9eab1fcb64c8bfd92ba1253f1653c6996905 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:07:22 +0545 Subject: [PATCH 40/56] Update shared-libs/cht-datasource/src/person.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/person.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index e31577a31d7..cd255fb1e56 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -120,9 +120,9 @@ export namespace v1 { }; /** - * Returns a generator function for retrieving people from the given data context. + * Returns a function for getting a generator that fetches people from the given data context. * @param context the current data context - * @returns a generator function for retrieving people + * @returns a function for getting a generator that fetches people * @throws Error if a data context is not provided */ export const getAll = ( From d14ce9e2e6bec42700577ca36a1eda45817e92a4 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Tue, 6 Aug 2024 18:46:18 +0545 Subject: [PATCH 41/56] Remove limit and skip as arguments for getDocumentStream function --- shared-libs/cht-datasource/src/index.ts | 3 ++- .../cht-datasource/src/libs/data-context.ts | 23 +++++++--------- shared-libs/cht-datasource/src/person.ts | 27 +++++++++++-------- shared-libs/cht-datasource/test/index.spec.ts | 9 ++++--- .../test/libs/data-context.spec.ts | 20 ++++++++------ .../cht-datasource/test/person.spec.ts | 25 ++++++----------- 6 files changed, 53 insertions(+), 54 deletions(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index f75abee0852..b893d2c742f 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -95,9 +95,10 @@ export const getDatasource = (ctx: DataContext) => { * @throws Error if no type is provided or if the type is not for a person * @throws Error if the provided limit is `<= 0` * @throws Error if the provided skip is `< 0` + * @see {@link getByType} which provides the same data, but without having to manually account for paging */ getPageByType: (personType: string, limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)( - { personType: Qualifier.byContactType(personType), limit, skip } + Qualifier.byContactType(personType), limit, skip ), /** diff --git a/shared-libs/cht-datasource/src/libs/data-context.ts b/shared-libs/cht-datasource/src/libs/data-context.ts index 4a11e2b9cf6..24f23590991 100644 --- a/shared-libs/cht-datasource/src/libs/data-context.ts +++ b/shared-libs/cht-datasource/src/libs/data-context.ts @@ -40,24 +40,21 @@ export const adapt = ( return remote(context); }; -interface PaginationArgs { - limit: number; - skip: number; -} - -type FetchFunctionArgs> = PaginationArgs & TFields; - /** @internal */ -export const getDocumentStream = async function* >( - fetchFunction: (args: FetchFunctionArgs) => Promise, - fetchFunctionArgs: FetchFunctionArgs +export const getDocumentStream = async function* ( + fetchFunction: (args: S, l: number, s: number) => Promise, + fetchFunctionArgs: S ): AsyncGenerator { - const { limit } = fetchFunctionArgs; - let { skip } = fetchFunctionArgs; + const limit = 100; + let skip = 0; + // TODO: found a bug in this function where if limit is 1 + // then it will always return true but right now since limit + // is hardcoded to 100, it won't but if limit is made to be + // dynamic then, change this implementation const hasMoreResults = () => skip && skip % limit === 0; do { - const docs = await fetchFunction(fetchFunctionArgs); + const docs = await fetchFunction(fetchFunctionArgs, limit, skip); for (const doc of docs) { yield doc; diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index cd255fb1e56..2f8de475e2c 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -91,25 +91,25 @@ export namespace v1 { * @param context the current data context * @returns a function for retrieving a paged array of people * @throws Error if a data context is not provided + * @see {@link getAll} which provides the same data, but without having to manually account for paging */ export const getPage = ( context: DataContext - ): ({ personType, limit, skip }: GetPageParams) => Promise => { + ): typeof curriedFn => { assertDataContext(context); const fn = adapt(context, Local.Person.v1.getPage, Remote.Person.v1.getPage); /** * Returns an array of people for the provided page specifications. - * @param params the function params - * @param params.personType the type of people to return - * @param params.limit the maximum number of people to return. Default is 100. - * @param params.skip the number of people to skip. Default is 0. + * @param personType the type of people to return + * @param limit the maximum number of people to return. Default is 100. + * @param skip the number of people to skip. Default is 0. * @returns an array of people for the provided page specifications. * @throws Error if no type is provided or if the type is not for a person * @throws Error if the provided `limit` value is `<=0` * @throws Error if the provided `skip` value is `<0` */ - const curriedFn = async ({ personType, limit = 100, skip = 0}: GetPageParams): Promise => { + const curriedFn = async (personType: ContactTypeQualifier, limit = 100, skip = 0): Promise => { assertTypeQualifier(personType); assertLimit(limit); assertSkip(skip); @@ -127,15 +127,20 @@ export namespace v1 { */ export const getAll = ( context: DataContext - ): (personType: ContactTypeQualifier) => AsyncGenerator => { + ): typeof curriedGen => { assertDataContext(context); - return async function* (personType: ContactTypeQualifier): AsyncGenerator { + /** + * Returns a generator for fetching all people with the given type + * @param personType the type of people to return + * @returns a generator for fetching all people with the given type + * @throws Error if no type is provided or if the type is not for a person + */ + const curriedGen = async function* (personType: ContactTypeQualifier): AsyncGenerator { assertTypeQualifier(personType); const getPage = context.bind(v1.getPage); - const limit = 100; - const skip = 0; - yield* getDocumentStream(getPage, { personType, limit, skip }); + yield* getDocumentStream(getPage, personType); }; + return curriedGen; }; } diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index 972484e8ccf..34dbd44a02b 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -92,7 +92,7 @@ describe('CHT Script API - getDatasource', () => { beforeEach(() => person = v1.person); it('contains expected keys', () => { - expect(person).to.have.all.keys(['getAll', 'getByUuid', 'getByUuidWithLineage', 'getPageByType']); + expect(person).to.have.all.keys(['getByType', 'getByUuid', 'getByUuidWithLineage', 'getPageByType']); }); it('getByUuid', async () => { @@ -139,11 +139,12 @@ describe('CHT Script API - getDatasource', () => { expect(returnedPeople).to.equal(expectedPeople); expect(dataContextBind.calledOnceWithExactly(Person.v1.getPage)).to.be.true; - expect(personGetPage.calledOnceWithExactly({ personType: personTypeQualifier, limit, skip })).to.be.true; + expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip)).to.be.true; expect(byContactType.calledOnceWithExactly(personType)).to.be.true; }); it('getAll', async () => { + // eslint-disable-next-line @typescript-eslint/require-await const mockAsyncGenerator = async function* () { yield []; }; @@ -153,7 +154,7 @@ describe('CHT Script API - getDatasource', () => { const personTypeQualifier = { contactType: personType }; const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); - const res = await person.getAll(personType); + const res = await person.getByType(personType); expect(res).to.deep.equal(mockAsyncGenerator); expect(dataContextBind.calledOnceWithExactly(Person.v1.getAll)).to.be.true; @@ -171,7 +172,7 @@ describe('CHT Script API - getDatasource', () => { const personTypeQualifier = { contactType: personType }; const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); - const res = await person.getAll(personType); + const res = await person.getByType(personType); expect(res).to.deep.equal(mockAsyncGenerator); expect(dataContextBind.calledOnceWithExactly(Person.v1.getAll)).to.be.true; diff --git a/shared-libs/cht-datasource/test/libs/data-context.spec.ts b/shared-libs/cht-datasource/test/libs/data-context.spec.ts index 82d57682e60..72e107d559a 100644 --- a/shared-libs/cht-datasource/test/libs/data-context.spec.ts +++ b/shared-libs/cht-datasource/test/libs/data-context.spec.ts @@ -121,6 +121,8 @@ describe('context lib', () => { describe('getDocumentStream', () => { let fetchFunctionStub: SinonStub; + const limit = 100; + const skip = 0; beforeEach(() => { fetchFunctionStub = sinon.stub(); @@ -128,10 +130,10 @@ describe('context lib', () => { it('yields document one by one', async () => { const mockDocs = [{ id: 1 }, { id: 2 }, { id: 3 }]; - const args = { limit: 4, skip: 0, extraArgs: 'value'}; + const extraArg = 'value'; fetchFunctionStub.resolves(mockDocs); - const generator = getDocumentStream(fetchFunctionStub, args); + const generator = getDocumentStream(fetchFunctionStub, extraArg); const results = []; @@ -140,17 +142,19 @@ describe('context lib', () => { } expect(results).to.deep.equal(mockDocs); - expect(fetchFunctionStub.calledOnceWithExactly(args)).to.be.true; + expect(fetchFunctionStub.calledOnceWithExactly(extraArg, limit, skip)).to.be.true; }); it('should handle multiple pages', async () => { - const mockDocs1 = [{ id: 1 }, { id: 2 }]; - const mockDocs2 = [{ id: 3 }]; + const mockDoc = { id: 1 }; + const mockDocs1 = Array.from({ length: 100 }, () => ({ ...mockDoc })); + const mockDocs2 = [{ id: 101 }]; + const extraArg = 'value'; fetchFunctionStub.onFirstCall().resolves(mockDocs1); fetchFunctionStub.onSecondCall().resolves(mockDocs2); - const generator = getDocumentStream(fetchFunctionStub, { limit: 2, skip: 0}); + const generator = getDocumentStream(fetchFunctionStub, extraArg); const results = []; for await (const doc of generator) { @@ -159,8 +163,8 @@ describe('context lib', () => { expect(results).to.deep.equal([...mockDocs1, ...mockDocs2]); expect(fetchFunctionStub.callCount).to.equal(2); - expect(fetchFunctionStub.firstCall.args).to.deep.equal([{ limit: 2, skip: 0 }]); - expect(fetchFunctionStub.secondCall.args).to.deep.equal([{ limit: 2, skip: 0 }]); + expect(fetchFunctionStub.firstCall.args).to.deep.equal([extraArg, limit, skip]); + expect(fetchFunctionStub.secondCall.args).to.deep.equal([extraArg, limit, skip + limit]); }); it('should handle empty result', async () => { diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index d6013c5f290..36fd865db5c 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -145,7 +145,7 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); - const result = await Person.v1.getPage(dataContext)({ personType: personTypeQualifier, limit, skip }); + const result = await Person.v1.getPage(dataContext)(personTypeQualifier, limit, skip); expect(result).to.equal(people); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -169,7 +169,7 @@ describe('person', () => { it('throws an error if the qualifier is invalid', async () => { isContactTypeQualifier.returns(false); - await expect(Person.v1.getPage(dataContext)({personType: invalidQualifier})) + await expect(Person.v1.getPage(dataContext)(invalidQualifier, limit, skip)) .to.be.rejectedWith(`Invalid type [${JSON.stringify(invalidQualifier)}].`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -182,7 +182,7 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); - await expect(Person.v1.getPage(dataContext)({ personType: personTypeQualifier, limit: invalidLimit, skip })) + await expect(Person.v1.getPage(dataContext)(personTypeQualifier, invalidLimit, skip )) .to.be.rejectedWith(`limit must be a positive number`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -195,11 +195,8 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); - await expect(Person.v1.getPage(dataContext)({ - personType: personTypeQualifier, - limit: limit, - skip: invalidSkip - })).to.be.rejectedWith(`skip must be a non-negative number`); + await expect(Person.v1.getPage(dataContext)(personTypeQualifier, limit, invalidSkip)) + .to.be.rejectedWith(`skip must be a non-negative number`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; @@ -211,13 +208,11 @@ describe('person', () => { describe('getAll', () => { const personType = 'person'; const personTypeQualifier = {contactType: personType} as const; - const limit = 100; - const skip = 0; const firstPerson = { _id: 'person1' } as Person.v1.Person; const secondPerson = { _id: 'person2' } as Person.v1.Person; const thirdPerson = { _id: 'person3' } as Person.v1.Person; const people = [firstPerson, secondPerson, thirdPerson]; - const mockGenerator = async function* () { + const mockGenerator = function* () { for (const person of people) { yield person; } @@ -244,9 +239,7 @@ describe('person', () => { } expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; - expect(getDocumentStream.calledOnceWithExactly(personGetPage, { - personType: personTypeQualifier, limit, skip - })).to.be.true; + expect(getDocumentStream.calledOnceWithExactly(personGetPage, personTypeQualifier)).to.be.true; expect(res).to.be.deep.equal(people); expect(isContactTypeQualifier.calledOnceWithExactly(personTypeQualifier)).to.be.true; }); @@ -259,9 +252,7 @@ describe('person', () => { const res = await generator.next(); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; - expect(getDocumentStream.calledOnceWithExactly(personGetPage, { - personType: personTypeQualifier, limit, skip - })).to.be.true; + expect(getDocumentStream.calledOnceWithExactly(personGetPage, personTypeQualifier)).to.be.true; expect(res.value).to.equal(undefined); expect(isContactTypeQualifier.calledOnceWithExactly(personTypeQualifier)).to.be.true; }); From 36a5a872b9ea7a58ea7b04da8fc5e997ac92dfec Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 7 Aug 2024 13:19:51 +0545 Subject: [PATCH 42/56] Remove duplicate test case added during conflict resolution --- shared-libs/cht-datasource/test/index.spec.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index 34dbd44a02b..46488c58bd0 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -154,24 +154,7 @@ describe('CHT Script API - getDatasource', () => { const personTypeQualifier = { contactType: personType }; const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); - const res = await person.getByType(personType); - - expect(res).to.deep.equal(mockAsyncGenerator); - expect(dataContextBind.calledOnceWithExactly(Person.v1.getAll)).to.be.true; - expect(personGetAll.calledOnceWithExactly(personTypeQualifier)).to.be.true; - expect(byContactType.calledOnceWithExactly(personType)).to.be.true; - }); - - it('getAll', async () => { - const mockAsyncGenerator = async function* () { - yield []; - }; - const personGetAll = sinon.stub().resolves(mockAsyncGenerator); - dataContextBind.returns(personGetAll); - const personType = 'person'; - const personTypeQualifier = { contactType: personType }; - const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); - + // eslint-disable-next-line @typescript-eslint/await-thenable const res = await person.getByType(personType); expect(res).to.deep.equal(mockAsyncGenerator); From d87d2936ed416217b5918a78cc0f1b7a6c6c8bc6 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 7 Aug 2024 13:28:56 +0545 Subject: [PATCH 43/56] Cleanup after refactoring --- shared-libs/cht-datasource/src/person.ts | 6 ------ shared-libs/cht-datasource/test/index.spec.ts | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 2f8de475e2c..bbbab8ed7a0 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -26,12 +26,6 @@ export namespace v1 { readonly parent?: Place.v1.PlaceWithLineage | NormalizedParent, } - interface GetPageParams { - personType: ContactTypeQualifier; - limit?: number; - skip?: number; - } - const assertPersonQualifier: (qualifier: unknown) => asserts qualifier is UuidQualifier = (qualifier: unknown) => { if (!isUuidQualifier(qualifier)) { throw new Error(`Invalid identifier [${JSON.stringify(qualifier)}].`); diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index 46488c58bd0..ffb50aaab6e 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -125,7 +125,7 @@ describe('CHT Script API - getDatasource', () => { expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; }); - it('getPage', async () => { + it('getPageByType', async () => { const expectedPeople: Person.v1.Person[] = []; const personGetPage = sinon.stub().resolves(expectedPeople); dataContextBind.returns(personGetPage); @@ -143,7 +143,7 @@ describe('CHT Script API - getDatasource', () => { expect(byContactType.calledOnceWithExactly(personType)).to.be.true; }); - it('getAll', async () => { + it('getByType', async () => { // eslint-disable-next-line @typescript-eslint/require-await const mockAsyncGenerator = async function* () { yield []; From 2867a808c1d28a5e25d1b12420c47f8fbe3f5375 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Tue, 6 Aug 2024 13:30:48 +0545 Subject: [PATCH 44/56] Implement cursor based pagination --- shared-libs/cht-datasource/src/libs/core.ts | 6 +++ .../cht-datasource/src/local/person.ts | 46 +++++++++++++++++-- shared-libs/cht-datasource/src/person.ts | 7 ++- .../cht-datasource/src/remote/person.ts | 4 +- 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/shared-libs/cht-datasource/src/libs/core.ts b/shared-libs/cht-datasource/src/libs/core.ts index f2475cc84f8..ebdedc95b46 100644 --- a/shared-libs/cht-datasource/src/libs/core.ts +++ b/shared-libs/cht-datasource/src/libs/core.ts @@ -112,3 +112,9 @@ export const findById = (values: T[], id: string): Nulla export abstract class AbstractDataContext implements DataContext { readonly bind = (fn: (ctx: DataContext) => T): T => fn(this); } + +/** @internal */ +export interface Page { + readonly data: T[]; + readonly cursor: string; +} diff --git a/shared-libs/cht-datasource/src/local/person.ts b/shared-libs/cht-datasource/src/local/person.ts index 9bc0bbb8b9b..8452d512243 100644 --- a/shared-libs/cht-datasource/src/local/person.ts +++ b/shared-libs/cht-datasource/src/local/person.ts @@ -1,6 +1,6 @@ import { Doc } from '../libs/doc'; import contactTypeUtils from '@medic/contact-types-utils'; -import { deepCopy, isNonEmptyArray, Nullable } from '../libs/core'; +import { deepCopy, isNonEmptyArray, Nullable, Page } from '../libs/core'; import { ContactTypeQualifier, UuidQualifier } from '../qualifier'; import * as Person from '../person'; import { getDocById, getDocsByIds, queryDocsByKey } from './libs/doc'; @@ -61,9 +61,13 @@ export namespace v1 { /** @internal */ export const getPage = ({ medicDb, settings }: LocalDataContext) => { - return async (personType: ContactTypeQualifier, limit: number, skip: number): Promise => { + return async ( + personType: ContactTypeQualifier, + limit: number, + skip: number + ): Promise> => { const personTypes = contactTypeUtils.getPersonTypes(settings.getAll()); - const personTypesIds = personTypes.map(item => item.id); + const personTypesIds = personTypes.map((item) => item.id); if (!personTypesIds.includes(personType.contactType)) { throw new Error(`Invalid person type: ${personType.contactType}`); @@ -71,9 +75,41 @@ export namespace v1 { const getDocsByPage = queryDocsByKey(medicDb, 'medic-client/contacts_by_type'); - const docs = await getDocsByPage([personType.contactType], limit, skip); + const fetchAndFilter = async ( + currentLimit: number, + currentSkip: number, + personDocs: Person.v1.Person[], + totalDocsFetched = 0, + ): Promise> => { + const docs = await getDocsByPage([personType.contactType], currentLimit, currentSkip); + if (docs.length === 0) { + return { data: personDocs, cursor: '-1' }; + } - return docs.filter((doc): doc is Person.v1.Person => isPerson(settings, doc, doc?._id)); + const tempFilteredDocs = docs.filter((doc): doc is Person.v1.Person => isPerson(settings, doc, doc?._id)); + + personDocs.push(...tempFilteredDocs); + totalDocsFetched += docs.length; + + if (personDocs.length >= limit) { + let cursor: number; + if (docs.length < currentLimit) { + cursor = -1; + } else { + cursor = skip + totalDocsFetched - (personDocs.length - limit); + } + return { data: personDocs.slice(0, limit), cursor: cursor.toString() }; + } + + return fetchAndFilter( + (currentLimit - tempFilteredDocs.length) * 2, + currentSkip + currentLimit, + personDocs, + totalDocsFetched + ); + }; + + return fetchAndFilter(limit, skip, []); }; }; } diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index bbbab8ed7a0..bca674c5007 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -6,6 +6,7 @@ import * as Local from './local'; import * as Place from './place'; import { LocalDataContext } from './local/libs/data-context'; import { RemoteDataContext } from './remote/libs/data-context'; +import { Page } from './libs/core'; /** */ export namespace v1 { @@ -103,7 +104,11 @@ export namespace v1 { * @throws Error if the provided `limit` value is `<=0` * @throws Error if the provided `skip` value is `<0` */ - const curriedFn = async (personType: ContactTypeQualifier, limit = 100, skip = 0): Promise => { + const curriedFn = async ( + personType: ContactTypeQualifier, + limit = 100, + skip = 0 + ): Promise> => { assertTypeQualifier(personType); assertLimit(limit); assertSkip(skip); diff --git a/shared-libs/cht-datasource/src/remote/person.ts b/shared-libs/cht-datasource/src/remote/person.ts index 4ec6d79bbfe..0604c0c8b4a 100644 --- a/shared-libs/cht-datasource/src/remote/person.ts +++ b/shared-libs/cht-datasource/src/remote/person.ts @@ -1,4 +1,4 @@ -import { Nullable } from '../libs/core'; +import { Nullable, Page } from '../libs/core'; import { ContactTypeQualifier, UuidQualifier } from '../qualifier'; import * as Person from '../person'; import { getResource, getResources, RemoteDataContext } from './libs/data-context'; @@ -27,7 +27,7 @@ export namespace v1 { personType: ContactTypeQualifier, limit: number, skip: number - ): Promise => getPeople(remoteContext)( + ): Promise> => getPeople(remoteContext)( {'limit': limit.toString(), 'skip': skip.toString(), 'contactType': personType.contactType} ); } From 84c30c9c928d94fe06abef9b590f3ba22d91114d Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Tue, 6 Aug 2024 14:54:34 +0545 Subject: [PATCH 45/56] Fix unit tests according to implementation of cursor pagination --- shared-libs/cht-datasource/test/index.spec.ts | 3 +- .../cht-datasource/test/local/person.spec.ts | 32 ++++++++++++++----- .../cht-datasource/test/person.spec.ts | 6 ++-- .../cht-datasource/test/remote/person.spec.ts | 5 +-- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index ffb50aaab6e..1ee95acaec5 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -7,6 +7,7 @@ import * as Qualifier from '../src/qualifier'; import sinon, { SinonStub } from 'sinon'; import * as Context from '../src/libs/data-context'; import { DataContext } from '../src'; +import { Page } from '../src/libs/core'; describe('CHT Script API - getDatasource', () => { let dataContext: DataContext; @@ -126,7 +127,7 @@ describe('CHT Script API - getDatasource', () => { }); it('getPageByType', async () => { - const expectedPeople: Person.v1.Person[] = []; + const expectedPeople: Page = {data: [], cursor: '-1'}; const personGetPage = sinon.stub().resolves(expectedPeople); dataContextBind.returns(personGetPage); const personType = 'person'; diff --git a/shared-libs/cht-datasource/test/local/person.spec.ts b/shared-libs/cht-datasource/test/local/person.spec.ts index e2022fd0fea..58a7e6ad418 100644 --- a/shared-libs/cht-datasource/test/local/person.spec.ts +++ b/shared-libs/cht-datasource/test/local/person.spec.ts @@ -248,10 +248,14 @@ describe('local person', () => { const doc = { type: 'person'}; const docs = [doc, doc, doc]; queryDocsByKeyInner.resolves(docs); + const expectedResult = { + cursor: '3', + data: docs + }; const res = await Person.v1.getPage(localContext)(personTypeQualifier, limit, skip); - expect(res).to.deep.equal(docs); + expect(res).to.deep.equal(expectedResult); expect(settingsGetAll.callCount).to.equal(4); expect(getPersonTypes.calledOnceWithExactly(settings)).to.be.true; expect( @@ -278,10 +282,14 @@ describe('local person', () => { it('returns empty array if people does not exist', async () => { queryDocsByKeyInner.resolves([]); + const expectedResult = { + data: [], + cursor: '-1' + }; const res = await Person.v1.getPage(localContext)(personTypeQualifier, limit, skip); - expect(res).to.deep.equal([]); + expect(res).to.deep.equal(expectedResult); expect(settingsGetAll.calledOnce).to.be.true; expect(getPersonTypes.calledOnceWithExactly(settings)).to.be.true; expect( @@ -291,22 +299,30 @@ describe('local person', () => { expect(isPerson.notCalled).to.be.true; }); - it('returns empty array if rows returned from database are not persons', async () => { + it('returns page of people by refetching the database if the previous lot consisted on non-persons', async () => { const doc = { type: 'person'}; const docs = [doc, doc, doc]; queryDocsByKeyInner.resolves(docs); - isPerson.returns(false); + isPerson.onFirstCall().returns(false); + isPerson.onSecondCall().returns(false); + isPerson.returns(true); + const expectedResult = { + cursor: '-1', + data: docs + }; const res = await Person.v1.getPage(localContext)(personTypeQualifier, limit, skip); - expect(res).to.deep.equal([]); - expect(settingsGetAll.callCount).to.equal(4); + expect(res).to.deep.equal(expectedResult); + expect(settingsGetAll.callCount).to.equal(7); expect(getPersonTypes.calledOnceWithExactly(settings)).to.be.true; expect( queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') ).to.be.true; - expect(queryDocsByKeyInner.calledOnceWithExactly([personIdentifier], limit, skip)).to.be.true; - expect(isPerson.callCount).to.equal(3); + expect(queryDocsByKeyInner.callCount).to.be.equal(2); + expect(queryDocsByKeyInner.firstCall.args).to.deep.equal([[personIdentifier], limit, skip]); + expect(queryDocsByKeyInner.secondCall.args).to.deep.equal([[personIdentifier], 4, 3]); + expect(isPerson.callCount).to.equal(6); expect(isPerson.getCall(0).args).to.deep.equal([settings, doc]); expect(isPerson.getCall(1).args).to.deep.equal([settings, doc]); expect(isPerson.getCall(2).args).to.deep.equal([settings, doc]); diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index 36fd865db5c..e6b5b5922a5 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -128,6 +128,8 @@ describe('person', () => { describe('getPage', () => { const people = [{ _id: 'person1' }, { _id: 'person2' }, { _id: 'person3' }] as Person.v1.Person[]; + const cursor = '-1'; + const pageData = { data: people, cursor }; const limit = 3; const skip = 1; const invalidLimit = -1; @@ -143,11 +145,11 @@ describe('person', () => { it('retrieves people from the data context', async () => { isContactTypeQualifier.returns(true); - getPage.resolves(people); + getPage.resolves(pageData); const result = await Person.v1.getPage(dataContext)(personTypeQualifier, limit, skip); - expect(result).to.equal(people); + expect(result).to.equal(pageData); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; expect(getPage.calledOnceWithExactly(personTypeQualifier, limit, skip)).to.be.true; diff --git a/shared-libs/cht-datasource/test/remote/person.spec.ts b/shared-libs/cht-datasource/test/remote/person.spec.ts index 1028b52e3d6..a2634acaf3e 100644 --- a/shared-libs/cht-datasource/test/remote/person.spec.ts +++ b/shared-libs/cht-datasource/test/remote/person.spec.ts @@ -81,11 +81,12 @@ describe('remote person', () => { it('returns people', async () => { const doc = [{ type: 'person' }, {type: 'person'}]; - getResourcesInner.resolves(doc); + const expectedResponse = { data: doc, cursor: '-1' }; + getResourcesInner.resolves(expectedResponse); const result = await Person.v1.getPage(remoteContext)(personTypeQualifier, limit, skip); - expect(result).to.equal(doc); + expect(result).to.equal(expectedResponse); expect(getResourcesOuter.calledOnceWithExactly(remoteContext, 'api/v1/person')).to.be.true; expect(getResourcesInner.calledOnceWithExactly(queryParam)).to.be.true; }); From d45f0d95fdd4a1d91840b175b1e8994a094ac7f5 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Thu, 25 Jul 2024 16:56:15 +0545 Subject: [PATCH 46/56] Add unit tests --- shared-libs/cht-datasource/test/index.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index 1ee95acaec5..06bba8c44b6 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -7,7 +7,10 @@ import * as Qualifier from '../src/qualifier'; import sinon, { SinonStub } from 'sinon'; import * as Context from '../src/libs/data-context'; import { DataContext } from '../src'; +<<<<<<< HEAD import { Page } from '../src/libs/core'; +======= +>>>>>>> eff3972df (Add unit tests) describe('CHT Script API - getDatasource', () => { let dataContext: DataContext; From 563ac8186fc1fe541d3e9fc3b1bf8dbf6178fb34 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Mon, 29 Jul 2024 16:03:51 +0545 Subject: [PATCH 47/56] Address PR comments --- shared-libs/cht-datasource/test/person.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index e6b5b5922a5..190571e7728 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -210,11 +210,13 @@ describe('person', () => { describe('getAll', () => { const personType = 'person'; const personTypeQualifier = {contactType: personType} as const; + const limit = 100; + const skip = 0; const firstPerson = { _id: 'person1' } as Person.v1.Person; const secondPerson = { _id: 'person2' } as Person.v1.Person; const thirdPerson = { _id: 'person3' } as Person.v1.Person; const people = [firstPerson, secondPerson, thirdPerson]; - const mockGenerator = function* () { + const mockGenerator = async function* () { for (const person of people) { yield person; } From fd72695cf848183498ff59817a696d187ff565d8 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 31 Jul 2024 11:49:20 +0545 Subject: [PATCH 48/56] Avoid using spread operator in getDocumentStream function --- shared-libs/cht-datasource/src/libs/data-context.ts | 7 +++++++ shared-libs/cht-datasource/test/index.spec.ts | 2 +- shared-libs/cht-datasource/test/person.spec.ts | 13 ++++++++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/shared-libs/cht-datasource/src/libs/data-context.ts b/shared-libs/cht-datasource/src/libs/data-context.ts index 24f23590991..da00da0f662 100644 --- a/shared-libs/cht-datasource/src/libs/data-context.ts +++ b/shared-libs/cht-datasource/src/libs/data-context.ts @@ -40,6 +40,13 @@ export const adapt = ( return remote(context); }; +interface PaginationArgs { + limit: number; + skip: number; +} + +type FetchFunctionArgs> = PaginationArgs & TFields; + /** @internal */ export const getDocumentStream = async function* ( fetchFunction: (args: S, l: number, s: number) => Promise, diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index 06bba8c44b6..e6c56ec8774 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -143,7 +143,7 @@ describe('CHT Script API - getDatasource', () => { expect(returnedPeople).to.equal(expectedPeople); expect(dataContextBind.calledOnceWithExactly(Person.v1.getPage)).to.be.true; - expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip)).to.be.true; + expect(personGetPage.calledOnceWithExactly({ personType: personTypeQualifier, limit, skip })).to.be.true; expect(byContactType.calledOnceWithExactly(personType)).to.be.true; }); diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index 190571e7728..eb2701a31ab 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -147,7 +147,7 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(pageData); - const result = await Person.v1.getPage(dataContext)(personTypeQualifier, limit, skip); + const result = await Person.v1.getPage(dataContext)({ personType: personTypeQualifier, limit, skip }); expect(result).to.equal(pageData); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -184,7 +184,11 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); +<<<<<<< HEAD await expect(Person.v1.getPage(dataContext)(personTypeQualifier, invalidLimit, skip )) +======= + await expect(Person.v1.getPage(dataContext)({ personType: personTypeQualifier, limit: invalidLimit, skip })) +>>>>>>> b988b5782 (Avoid using spread operator in getDocumentStream function) .to.be.rejectedWith(`limit must be a positive number`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -197,8 +201,11 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); - await expect(Person.v1.getPage(dataContext)(personTypeQualifier, limit, invalidSkip)) - .to.be.rejectedWith(`skip must be a non-negative number`); + await expect(Person.v1.getPage(dataContext)({ + personType: personTypeQualifier, + limit: limit, + skip: invalidSkip + })).to.be.rejectedWith(`skip must be a non-negative number`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; From bab3c1e7afee59cae5096176b8139f8ea529f4af Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 31 Jul 2024 14:35:33 +0545 Subject: [PATCH 49/56] Fix conflict with target branch --- shared-libs/cht-datasource/src/person.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index bca674c5007..12a852344e6 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -27,6 +27,12 @@ export namespace v1 { readonly parent?: Place.v1.PlaceWithLineage | NormalizedParent, } + interface getPageParams { + personType: ContactTypeQualifier; + limit?: number; + skip?: number; + } + const assertPersonQualifier: (qualifier: unknown) => asserts qualifier is UuidQualifier = (qualifier: unknown) => { if (!isUuidQualifier(qualifier)) { throw new Error(`Invalid identifier [${JSON.stringify(qualifier)}].`); @@ -96,9 +102,10 @@ export namespace v1 { /** * Returns an array of people for the provided page specifications. - * @param personType the type of people to return - * @param limit the maximum number of people to return. Default is 100. - * @param skip the number of people to skip. Default is 0. + * @param params the function params + * @param params.personType the type of people to return + * @param params.limit the maximum number of people to return. Default is 100. + * @param params.skip the number of people to skip. Default is 0. * @returns an array of people for the provided page specifications. * @throws Error if no type is provided or if the type is not for a person * @throws Error if the provided `limit` value is `<=0` From 0d305ff2f376aa9153e16ba06b5c1f473214764d Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Tue, 6 Aug 2024 18:46:18 +0545 Subject: [PATCH 50/56] Remove limit and skip as arguments for getDocumentStream function --- .../cht-datasource/src/libs/data-context.ts | 7 ------- shared-libs/cht-datasource/src/person.ts | 7 +++---- shared-libs/cht-datasource/test/index.spec.ts | 2 +- shared-libs/cht-datasource/test/person.spec.ts | 17 ++++++++--------- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/shared-libs/cht-datasource/src/libs/data-context.ts b/shared-libs/cht-datasource/src/libs/data-context.ts index da00da0f662..24f23590991 100644 --- a/shared-libs/cht-datasource/src/libs/data-context.ts +++ b/shared-libs/cht-datasource/src/libs/data-context.ts @@ -40,13 +40,6 @@ export const adapt = ( return remote(context); }; -interface PaginationArgs { - limit: number; - skip: number; -} - -type FetchFunctionArgs> = PaginationArgs & TFields; - /** @internal */ export const getDocumentStream = async function* ( fetchFunction: (args: S, l: number, s: number) => Promise, diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 12a852344e6..2d531e7abda 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -102,10 +102,9 @@ export namespace v1 { /** * Returns an array of people for the provided page specifications. - * @param params the function params - * @param params.personType the type of people to return - * @param params.limit the maximum number of people to return. Default is 100. - * @param params.skip the number of people to skip. Default is 0. + * @param personType the type of people to return + * @param limit the maximum number of people to return. Default is 100. + * @param skip the number of people to skip. Default is 0. * @returns an array of people for the provided page specifications. * @throws Error if no type is provided or if the type is not for a person * @throws Error if the provided `limit` value is `<=0` diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index e6c56ec8774..06bba8c44b6 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -143,7 +143,7 @@ describe('CHT Script API - getDatasource', () => { expect(returnedPeople).to.equal(expectedPeople); expect(dataContextBind.calledOnceWithExactly(Person.v1.getPage)).to.be.true; - expect(personGetPage.calledOnceWithExactly({ personType: personTypeQualifier, limit, skip })).to.be.true; + expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip)).to.be.true; expect(byContactType.calledOnceWithExactly(personType)).to.be.true; }); diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index eb2701a31ab..6a106516db6 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -147,7 +147,7 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(pageData); - const result = await Person.v1.getPage(dataContext)({ personType: personTypeQualifier, limit, skip }); + const result = await Person.v1.getPage(dataContext)(personTypeQualifier, limit, skip); expect(result).to.equal(pageData); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -184,11 +184,15 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); +<<<<<<< HEAD <<<<<<< HEAD await expect(Person.v1.getPage(dataContext)(personTypeQualifier, invalidLimit, skip )) ======= await expect(Person.v1.getPage(dataContext)({ personType: personTypeQualifier, limit: invalidLimit, skip })) >>>>>>> b988b5782 (Avoid using spread operator in getDocumentStream function) +======= + await expect(Person.v1.getPage(dataContext)(personTypeQualifier, invalidLimit, skip )) +>>>>>>> 90cc4cae2 (Remove limit and skip as arguments for getDocumentStream function) .to.be.rejectedWith(`limit must be a positive number`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -201,11 +205,8 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); - await expect(Person.v1.getPage(dataContext)({ - personType: personTypeQualifier, - limit: limit, - skip: invalidSkip - })).to.be.rejectedWith(`skip must be a non-negative number`); + await expect(Person.v1.getPage(dataContext)(personTypeQualifier, limit, invalidSkip)) + .to.be.rejectedWith(`skip must be a non-negative number`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; @@ -217,13 +218,11 @@ describe('person', () => { describe('getAll', () => { const personType = 'person'; const personTypeQualifier = {contactType: personType} as const; - const limit = 100; - const skip = 0; const firstPerson = { _id: 'person1' } as Person.v1.Person; const secondPerson = { _id: 'person2' } as Person.v1.Person; const thirdPerson = { _id: 'person3' } as Person.v1.Person; const people = [firstPerson, secondPerson, thirdPerson]; - const mockGenerator = async function* () { + const mockGenerator = function* () { for (const person of people) { yield person; } From 15b6e3941930060c3650b3cdbbeb829d56596237 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 7 Aug 2024 13:28:56 +0545 Subject: [PATCH 51/56] Cleanup after refactoring --- shared-libs/cht-datasource/src/person.ts | 6 ------ shared-libs/cht-datasource/test/index.spec.ts | 4 ---- 2 files changed, 10 deletions(-) diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 2d531e7abda..bca674c5007 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -27,12 +27,6 @@ export namespace v1 { readonly parent?: Place.v1.PlaceWithLineage | NormalizedParent, } - interface getPageParams { - personType: ContactTypeQualifier; - limit?: number; - skip?: number; - } - const assertPersonQualifier: (qualifier: unknown) => asserts qualifier is UuidQualifier = (qualifier: unknown) => { if (!isUuidQualifier(qualifier)) { throw new Error(`Invalid identifier [${JSON.stringify(qualifier)}].`); diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index 06bba8c44b6..d077d087c48 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -7,10 +7,6 @@ import * as Qualifier from '../src/qualifier'; import sinon, { SinonStub } from 'sinon'; import * as Context from '../src/libs/data-context'; import { DataContext } from '../src'; -<<<<<<< HEAD -import { Page } from '../src/libs/core'; -======= ->>>>>>> eff3972df (Add unit tests) describe('CHT Script API - getDatasource', () => { let dataContext: DataContext; From 1d62657b49f87b1c0b0375133f07250b7cceca18 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 7 Aug 2024 15:51:53 +0545 Subject: [PATCH 52/56] Refactor getDocumentStream w.r.t. the cursor based pagination in getPageByType --- .../cht-datasource/src/libs/data-context.ts | 14 +++++--------- .../cht-datasource/test/libs/data-context.spec.ts | 11 +++++++---- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/shared-libs/cht-datasource/src/libs/data-context.ts b/shared-libs/cht-datasource/src/libs/data-context.ts index 24f23590991..72d523ed8da 100644 --- a/shared-libs/cht-datasource/src/libs/data-context.ts +++ b/shared-libs/cht-datasource/src/libs/data-context.ts @@ -1,4 +1,4 @@ -import { hasField, isRecord } from './core'; +import { hasField, isRecord, Page } from './core'; import { isLocalDataContext, LocalDataContext } from '../local/libs/data-context'; import { assertRemoteDataContext, isRemoteDataContext, RemoteDataContext } from '../remote/libs/data-context'; @@ -42,24 +42,20 @@ export const adapt = ( /** @internal */ export const getDocumentStream = async function* ( - fetchFunction: (args: S, l: number, s: number) => Promise, + fetchFunction: (args: S, l: number, s: number) => Promise>, fetchFunctionArgs: S ): AsyncGenerator { const limit = 100; let skip = 0; - // TODO: found a bug in this function where if limit is 1 - // then it will always return true but right now since limit - // is hardcoded to 100, it won't but if limit is made to be - // dynamic then, change this implementation - const hasMoreResults = () => skip && skip % limit === 0; + const hasMoreResults = () => skip !== -1; do { const docs = await fetchFunction(fetchFunctionArgs, limit, skip); - for (const doc of docs) { + for (const doc of docs.data) { yield doc; } - skip += docs.length; + skip = Number(docs.cursor); } while (hasMoreResults()); }; diff --git a/shared-libs/cht-datasource/test/libs/data-context.spec.ts b/shared-libs/cht-datasource/test/libs/data-context.spec.ts index 72e107d559a..b2107106357 100644 --- a/shared-libs/cht-datasource/test/libs/data-context.spec.ts +++ b/shared-libs/cht-datasource/test/libs/data-context.spec.ts @@ -130,8 +130,9 @@ describe('context lib', () => { it('yields document one by one', async () => { const mockDocs = [{ id: 1 }, { id: 2 }, { id: 3 }]; + const mockPage = { data: mockDocs, cursor: '-1' }; const extraArg = 'value'; - fetchFunctionStub.resolves(mockDocs); + fetchFunctionStub.resolves(mockPage); const generator = getDocumentStream(fetchFunctionStub, extraArg); @@ -148,11 +149,13 @@ describe('context lib', () => { it('should handle multiple pages', async () => { const mockDoc = { id: 1 }; const mockDocs1 = Array.from({ length: 100 }, () => ({ ...mockDoc })); + const mockPage1 = { data: mockDocs1, cursor: '100' }; const mockDocs2 = [{ id: 101 }]; + const mockPage2 = { data: mockDocs2, cursor: '-1' }; const extraArg = 'value'; - fetchFunctionStub.onFirstCall().resolves(mockDocs1); - fetchFunctionStub.onSecondCall().resolves(mockDocs2); + fetchFunctionStub.onFirstCall().resolves(mockPage1); + fetchFunctionStub.onSecondCall().resolves(mockPage2); const generator = getDocumentStream(fetchFunctionStub, extraArg); @@ -168,7 +171,7 @@ describe('context lib', () => { }); it('should handle empty result', async () => { - fetchFunctionStub.resolves([]); + fetchFunctionStub.resolves({ data: [], cursor: '-1' }); const generator = getDocumentStream(fetchFunctionStub, { limit: 10, skip: 0 }); From 1057fe48fdedff6564dcd6020d3e2e99560da568 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Wed, 7 Aug 2024 16:09:56 +0545 Subject: [PATCH 53/56] Fix merge conflicts with target branch --- shared-libs/cht-datasource/test/index.spec.ts | 1 + shared-libs/cht-datasource/test/person.spec.ts | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index d077d087c48..1ee95acaec5 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -7,6 +7,7 @@ import * as Qualifier from '../src/qualifier'; import sinon, { SinonStub } from 'sinon'; import * as Context from '../src/libs/data-context'; import { DataContext } from '../src'; +import { Page } from '../src/libs/core'; describe('CHT Script API - getDatasource', () => { let dataContext: DataContext; diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index 6a106516db6..e6b5b5922a5 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -184,15 +184,7 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); -<<<<<<< HEAD -<<<<<<< HEAD await expect(Person.v1.getPage(dataContext)(personTypeQualifier, invalidLimit, skip )) -======= - await expect(Person.v1.getPage(dataContext)({ personType: personTypeQualifier, limit: invalidLimit, skip })) ->>>>>>> b988b5782 (Avoid using spread operator in getDocumentStream function) -======= - await expect(Person.v1.getPage(dataContext)(personTypeQualifier, invalidLimit, skip )) ->>>>>>> 90cc4cae2 (Remove limit and skip as arguments for getDocumentStream function) .to.be.rejectedWith(`limit must be a positive number`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; From 88f39d7e9de42a21b30545cac7c4134c08991d64 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya <30311933+sugat009@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:36:41 +0545 Subject: [PATCH 54/56] Update shared-libs/cht-datasource/src/libs/core.ts Co-authored-by: Joshua Kuestersteffen --- shared-libs/cht-datasource/src/libs/core.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shared-libs/cht-datasource/src/libs/core.ts b/shared-libs/cht-datasource/src/libs/core.ts index ebdedc95b46..9ff684027bd 100644 --- a/shared-libs/cht-datasource/src/libs/core.ts +++ b/shared-libs/cht-datasource/src/libs/core.ts @@ -113,7 +113,12 @@ export abstract class AbstractDataContext implements DataContext { readonly bind = (fn: (ctx: DataContext) => T): T => fn(this); } -/** @internal */ +/** + * Represents a page of results. The `data` array contains the results for this page. The `cursor` field contains a + * key that can be used to fetch the next page of results. If no `cursor` value is returned, there are no additional + * results available. (Note that no assumptions should be made about the _contents_ of the cursor string.) + * @typeParam T the type of the data in the page + */ export interface Page { readonly data: T[]; readonly cursor: string; From 60604a585e25f0bba349e2f66ae15e299abe624c Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Thu, 8 Aug 2024 16:32:45 +0545 Subject: [PATCH 55/56] Change skip to cursor in all places and refactor fetchAndFilter function in local/person.ts --- shared-libs/cht-datasource/src/index.ts | 8 ++-- .../cht-datasource/src/libs/data-context.ts | 10 ++-- .../cht-datasource/src/local/person.ts | 46 ++++++++++--------- shared-libs/cht-datasource/src/person.ts | 18 ++++---- .../cht-datasource/src/remote/person.ts | 4 +- shared-libs/cht-datasource/test/index.spec.ts | 6 +-- .../test/libs/data-context.spec.ts | 8 ++-- .../cht-datasource/test/local/person.spec.ts | 16 +++---- .../cht-datasource/test/person.spec.ts | 29 ++++++------ .../cht-datasource/test/remote/person.spec.ts | 8 ++-- 10 files changed, 78 insertions(+), 75 deletions(-) diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index b893d2c742f..a88f3f34d64 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -89,16 +89,16 @@ export const getDatasource = (ctx: DataContext) => { /** * Returns an array of people for the provided page specifications. * @param personType the type of people to return + * @param cursor a value representing the index of the first person to return * @param limit the maximum number of people to return. Default is 100. - * @param skip the number of people to skip. Default is 0. * @returns an array of people for the provided page specifications * @throws Error if no type is provided or if the type is not for a person * @throws Error if the provided limit is `<= 0` - * @throws Error if the provided skip is `< 0` + * @throws Error if the provided cursor is `< 0` * @see {@link getByType} which provides the same data, but without having to manually account for paging */ - getPageByType: (personType: string, limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)( - Qualifier.byContactType(personType), limit, skip + getPageByType: (personType: string, cursor = '0', limit = 100) => ctx.bind(Person.v1.getPage)( + Qualifier.byContactType(personType), cursor, limit ), /** diff --git a/shared-libs/cht-datasource/src/libs/data-context.ts b/shared-libs/cht-datasource/src/libs/data-context.ts index 72d523ed8da..d2b9c225db4 100644 --- a/shared-libs/cht-datasource/src/libs/data-context.ts +++ b/shared-libs/cht-datasource/src/libs/data-context.ts @@ -42,20 +42,20 @@ export const adapt = ( /** @internal */ export const getDocumentStream = async function* ( - fetchFunction: (args: S, l: number, s: number) => Promise>, + fetchFunction: (args: S, s: string, l: number) => Promise>, fetchFunctionArgs: S ): AsyncGenerator { const limit = 100; - let skip = 0; - const hasMoreResults = () => skip !== -1; + let cursor = '0'; + const hasMoreResults = () => cursor !== '-1'; do { - const docs = await fetchFunction(fetchFunctionArgs, limit, skip); + const docs = await fetchFunction(fetchFunctionArgs, cursor, limit); for (const doc of docs.data) { yield doc; } - skip = Number(docs.cursor); + cursor = docs.cursor; } while (hasMoreResults()); }; diff --git a/shared-libs/cht-datasource/src/local/person.ts b/shared-libs/cht-datasource/src/local/person.ts index 8452d512243..265a5bca446 100644 --- a/shared-libs/cht-datasource/src/local/person.ts +++ b/shared-libs/cht-datasource/src/local/person.ts @@ -63,8 +63,8 @@ export namespace v1 { export const getPage = ({ medicDb, settings }: LocalDataContext) => { return async ( personType: ContactTypeQualifier, + cursor: string, limit: number, - skip: number ): Promise> => { const personTypes = contactTypeUtils.getPersonTypes(settings.getAll()); const personTypesIds = personTypes.map((item) => item.id); @@ -73,43 +73,45 @@ export namespace v1 { throw new Error(`Invalid person type: ${personType.contactType}`); } + // Adding a number skip variable here so as not to confuse ourselves + const skip = Number(cursor) || 0; const getDocsByPage = queryDocsByKey(medicDb, 'medic-client/contacts_by_type'); const fetchAndFilter = async ( currentLimit: number, currentSkip: number, - personDocs: Person.v1.Person[], - totalDocsFetched = 0, + currentPersonDocs: Person.v1.Person[] = [], ): Promise> => { const docs = await getDocsByPage([personType.contactType], currentLimit, currentSkip); - if (docs.length === 0) { - return { data: personDocs, cursor: '-1' }; - } + const noMoreResults = docs.length < currentLimit; + const newPersonDocs = docs.filter((doc): doc is Person.v1.Person => isPerson(settings, doc, doc?._id)); + const overFetchCount = currentPersonDocs.length + newPersonDocs.length - limit || 0; + const totalPeople = [...currentPersonDocs, ...newPersonDocs].slice(0, limit); - const tempFilteredDocs = docs.filter((doc): doc is Person.v1.Person => isPerson(settings, doc, doc?._id)); + if (noMoreResults) { + return { data: totalPeople, cursor: '-1' }; + } - personDocs.push(...tempFilteredDocs); - totalDocsFetched += docs.length; + if (totalPeople.length === limit) { + const nextSkip = currentSkip + currentLimit - overFetchCount; - if (personDocs.length >= limit) { - let cursor: number; - if (docs.length < currentLimit) { - cursor = -1; - } else { - cursor = skip + totalDocsFetched - (personDocs.length - limit); - } - return { data: personDocs.slice(0, limit), cursor: cursor.toString() }; + return { data: totalPeople, cursor: nextSkip.toString() }; } + // Re-fetch twice as many docs as we need to limit number of recursions + const missingCount = currentLimit - newPersonDocs.length; + logger.debug(`Found [${missingCount.toString()}] invalid persons. Re-fetching additional records.`); + const nextLimit = missingCount * 2; + const nextSkip = currentSkip + currentLimit; + return fetchAndFilter( - (currentLimit - tempFilteredDocs.length) * 2, - currentSkip + currentLimit, - personDocs, - totalDocsFetched + nextLimit, + nextSkip, + totalPeople, ); }; - return fetchAndFilter(limit, skip, []); + return fetchAndFilter(limit, skip); }; }; } diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index bca674c5007..6963b7f5211 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -47,9 +47,9 @@ export namespace v1 { } }; - const assertSkip = (skip: unknown) => { - if (typeof skip !== 'number' || !Number.isInteger(skip) || skip < 0) { - throw new Error(`The skip must be a non-negative number: [${String(skip)}]`); + const assertCursor = (cursor: unknown) => { + if (typeof cursor !== 'string' || Number(cursor) < 0) { + throw new Error(`The cursor must be a stringified non-negative number: [${String(cursor)}]`); } }; @@ -97,8 +97,8 @@ export namespace v1 { /** * Returns an array of people for the provided page specifications. * @param personType the type of people to return + * @param cursor the number of people to skip. Default is 0. * @param limit the maximum number of people to return. Default is 100. - * @param skip the number of people to skip. Default is 0. * @returns an array of people for the provided page specifications. * @throws Error if no type is provided or if the type is not for a person * @throws Error if the provided `limit` value is `<=0` @@ -106,14 +106,14 @@ export namespace v1 { */ const curriedFn = async ( personType: ContactTypeQualifier, + cursor = '0', limit = 100, - skip = 0 ): Promise> => { assertTypeQualifier(personType); assertLimit(limit); - assertSkip(skip); + assertCursor(cursor); - return fn(personType, limit, skip); + return fn(personType, cursor, limit); }; return curriedFn; }; @@ -135,10 +135,10 @@ export namespace v1 { * @returns a generator for fetching all people with the given type * @throws Error if no type is provided or if the type is not for a person */ - const curriedGen = async function* (personType: ContactTypeQualifier): AsyncGenerator { + const curriedGen = (personType: ContactTypeQualifier): AsyncGenerator => { assertTypeQualifier(personType); const getPage = context.bind(v1.getPage); - yield* getDocumentStream(getPage, personType); + return getDocumentStream(getPage, personType); }; return curriedGen; }; diff --git a/shared-libs/cht-datasource/src/remote/person.ts b/shared-libs/cht-datasource/src/remote/person.ts index 0604c0c8b4a..120d357644b 100644 --- a/shared-libs/cht-datasource/src/remote/person.ts +++ b/shared-libs/cht-datasource/src/remote/person.ts @@ -25,9 +25,9 @@ export namespace v1 { /** @internal */ export const getPage = (remoteContext: RemoteDataContext) => ( personType: ContactTypeQualifier, + cursor: string, limit: number, - skip: number ): Promise> => getPeople(remoteContext)( - {'limit': limit.toString(), 'skip': skip.toString(), 'contactType': personType.contactType} + {'limit': limit.toString(), 'contactType': personType.contactType, cursor} ); } diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index 1ee95acaec5..0f7a728abcb 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -132,15 +132,15 @@ describe('CHT Script API - getDatasource', () => { dataContextBind.returns(personGetPage); const personType = 'person'; const limit = 2; - const skip = 1; + const cursor = '1'; const personTypeQualifier = { contactType: personType }; const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); - const returnedPeople = await person.getPageByType(personType, limit, skip); + const returnedPeople = await person.getPageByType(personType, cursor, limit); expect(returnedPeople).to.equal(expectedPeople); expect(dataContextBind.calledOnceWithExactly(Person.v1.getPage)).to.be.true; - expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip)).to.be.true; + expect(personGetPage.calledOnceWithExactly(personTypeQualifier, cursor, limit)).to.be.true; expect(byContactType.calledOnceWithExactly(personType)).to.be.true; }); diff --git a/shared-libs/cht-datasource/test/libs/data-context.spec.ts b/shared-libs/cht-datasource/test/libs/data-context.spec.ts index b2107106357..1164e7bca8a 100644 --- a/shared-libs/cht-datasource/test/libs/data-context.spec.ts +++ b/shared-libs/cht-datasource/test/libs/data-context.spec.ts @@ -122,7 +122,7 @@ describe('context lib', () => { describe('getDocumentStream', () => { let fetchFunctionStub: SinonStub; const limit = 100; - const skip = 0; + const cursor = '0'; beforeEach(() => { fetchFunctionStub = sinon.stub(); @@ -143,7 +143,7 @@ describe('context lib', () => { } expect(results).to.deep.equal(mockDocs); - expect(fetchFunctionStub.calledOnceWithExactly(extraArg, limit, skip)).to.be.true; + expect(fetchFunctionStub.calledOnceWithExactly(extraArg, cursor, limit)).to.be.true; }); it('should handle multiple pages', async () => { @@ -166,8 +166,8 @@ describe('context lib', () => { expect(results).to.deep.equal([...mockDocs1, ...mockDocs2]); expect(fetchFunctionStub.callCount).to.equal(2); - expect(fetchFunctionStub.firstCall.args).to.deep.equal([extraArg, limit, skip]); - expect(fetchFunctionStub.secondCall.args).to.deep.equal([extraArg, limit, skip + limit]); + expect(fetchFunctionStub.firstCall.args).to.deep.equal([extraArg, cursor, limit]); + expect(fetchFunctionStub.secondCall.args).to.deep.equal([extraArg, (Number(cursor) + limit).toString(), limit]); }); it('should handle empty result', async () => { diff --git a/shared-libs/cht-datasource/test/local/person.spec.ts b/shared-libs/cht-datasource/test/local/person.spec.ts index 58a7e6ad418..89f4065b1db 100644 --- a/shared-libs/cht-datasource/test/local/person.spec.ts +++ b/shared-libs/cht-datasource/test/local/person.spec.ts @@ -227,7 +227,7 @@ describe('local person', () => { describe('getPage', () => { const limit = 3; - const skip = 0; + const cursor = '0'; const personIdentifier = 'person'; const personTypeQualifier = {contactType: personIdentifier} as const; const invalidPersonTypeQualifier = { contactType: 'invalid' } as const; @@ -253,7 +253,7 @@ describe('local person', () => { data: docs }; - const res = await Person.v1.getPage(localContext)(personTypeQualifier, limit, skip); + const res = await Person.v1.getPage(localContext)(personTypeQualifier, cursor, limit); expect(res).to.deep.equal(expectedResult); expect(settingsGetAll.callCount).to.equal(4); @@ -261,7 +261,7 @@ describe('local person', () => { expect( queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') ).to.be.true; - expect(queryDocsByKeyInner.calledOnceWithExactly([personIdentifier], limit, skip)).to.be.true; + expect(queryDocsByKeyInner.calledOnceWithExactly([personIdentifier], limit, Number(cursor))).to.be.true; expect(isPerson.callCount).to.equal(3); expect(isPerson.getCall(0).args).to.deep.equal([settings, doc]); expect(isPerson.getCall(1).args).to.deep.equal([settings, doc]); @@ -269,7 +269,7 @@ describe('local person', () => { }); it('throws an error if person identifier is invalid/does not exist', async () => { - await expect(Person.v1.getPage(localContext)(invalidPersonTypeQualifier, limit, skip)).to.be.rejectedWith( + await expect(Person.v1.getPage(localContext)(invalidPersonTypeQualifier, cursor, limit)).to.be.rejectedWith( `Invalid person type: ${invalidPersonTypeQualifier.contactType}` ); @@ -287,7 +287,7 @@ describe('local person', () => { cursor: '-1' }; - const res = await Person.v1.getPage(localContext)(personTypeQualifier, limit, skip); + const res = await Person.v1.getPage(localContext)(personTypeQualifier, cursor, limit); expect(res).to.deep.equal(expectedResult); expect(settingsGetAll.calledOnce).to.be.true; @@ -295,7 +295,7 @@ describe('local person', () => { expect( queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') ).to.be.true; - expect(queryDocsByKeyInner.calledOnceWithExactly([personIdentifier], limit, skip)).to.be.true; + expect(queryDocsByKeyInner.calledOnceWithExactly([personIdentifier], limit, Number(cursor))).to.be.true; expect(isPerson.notCalled).to.be.true; }); @@ -311,7 +311,7 @@ describe('local person', () => { data: docs }; - const res = await Person.v1.getPage(localContext)(personTypeQualifier, limit, skip); + const res = await Person.v1.getPage(localContext)(personTypeQualifier, cursor, limit); expect(res).to.deep.equal(expectedResult); expect(settingsGetAll.callCount).to.equal(7); @@ -320,7 +320,7 @@ describe('local person', () => { queryDocsByKeyOuter.calledOnceWithExactly(localContext.medicDb, 'medic-client/contacts_by_type') ).to.be.true; expect(queryDocsByKeyInner.callCount).to.be.equal(2); - expect(queryDocsByKeyInner.firstCall.args).to.deep.equal([[personIdentifier], limit, skip]); + expect(queryDocsByKeyInner.firstCall.args).to.deep.equal([[personIdentifier], limit, Number(cursor)]); expect(queryDocsByKeyInner.secondCall.args).to.deep.equal([[personIdentifier], 4, 3]); expect(isPerson.callCount).to.equal(6); expect(isPerson.getCall(0).args).to.deep.equal([settings, doc]); diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index e6b5b5922a5..d9313840216 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -128,12 +128,11 @@ describe('person', () => { describe('getPage', () => { const people = [{ _id: 'person1' }, { _id: 'person2' }, { _id: 'person3' }] as Person.v1.Person[]; - const cursor = '-1'; + const cursor = '1'; const pageData = { data: people, cursor }; const limit = 3; - const skip = 1; const invalidLimit = -1; - const invalidSkip = -1; + const invalidCursor = '-1'; const personTypeQualifier = {contactType: 'person'} as const; const invalidQualifier = { contactType: 'invalid' } as const; let getPage: SinonStub; @@ -147,12 +146,12 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(pageData); - const result = await Person.v1.getPage(dataContext)(personTypeQualifier, limit, skip); + const result = await Person.v1.getPage(dataContext)(personTypeQualifier, cursor, limit); expect(result).to.equal(pageData); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; - expect(getPage.calledOnceWithExactly(personTypeQualifier, limit, skip)).to.be.true; + expect(getPage.calledOnceWithExactly(personTypeQualifier, cursor, limit)).to.be.true; expect(isContactTypeQualifier.calledOnceWithExactly((personTypeQualifier))).to.be.true; }); @@ -171,7 +170,7 @@ describe('person', () => { it('throws an error if the qualifier is invalid', async () => { isContactTypeQualifier.returns(false); - await expect(Person.v1.getPage(dataContext)(invalidQualifier, limit, skip)) + await expect(Person.v1.getPage(dataContext)(invalidQualifier, cursor, limit)) .to.be.rejectedWith(`Invalid type [${JSON.stringify(invalidQualifier)}].`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -184,7 +183,7 @@ describe('person', () => { isContactTypeQualifier.returns(true); getPage.resolves(people); - await expect(Person.v1.getPage(dataContext)(personTypeQualifier, invalidLimit, skip )) + await expect(Person.v1.getPage(dataContext)(personTypeQualifier, cursor, invalidLimit)) .to.be.rejectedWith(`limit must be a positive number`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; @@ -193,12 +192,12 @@ describe('person', () => { expect(getPage.notCalled).to.be.true; }); - it('throws an error if skip is invalid', async () => { + it('throws an error if cursor is invalid', async () => { isContactTypeQualifier.returns(true); getPage.resolves(people); - await expect(Person.v1.getPage(dataContext)(personTypeQualifier, limit, invalidSkip)) - .to.be.rejectedWith(`skip must be a non-negative number`); + await expect(Person.v1.getPage(dataContext)(personTypeQualifier, invalidCursor, limit)) + .to.be.rejectedWith(`The cursor must be a stringified non-negative number: [${String(invalidCursor)}]`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(adapt.calledOnceWithExactly(dataContext, Local.Person.v1.getPage, Remote.Person.v1.getPage)).to.be.true; @@ -219,6 +218,9 @@ describe('person', () => { yield person; } }; + const emptyMockGenerator = function* () { + // empty + }; let personGetPage: sinon.SinonStub; let getDocumentStream: sinon.SinonStub; @@ -248,7 +250,7 @@ describe('person', () => { it('should handle empty result set', async () => { isContactTypeQualifier.returns(true); - getDocumentStream.returns([]); + getDocumentStream.returns(emptyMockGenerator()); const generator = Person.v1.getAll(dataContext)(personTypeQualifier); const res = await generator.next(); @@ -273,9 +275,8 @@ describe('person', () => { it('should throw an error for invalid personType', async () => { isContactTypeQualifier.returns(false); - const generator = Person.v1.getAll(dataContext)(personTypeQualifier); - await expect(generator.next()).to.be.rejectedWith(`Invalid type [${JSON.stringify(personTypeQualifier)}].`); - + expect(() => Person.v1.getAll(dataContext)(personTypeQualifier)) + .to.throw(`Invalid type [${JSON.stringify(personTypeQualifier)}]`); expect(assertDataContext.calledOnceWithExactly(dataContext)).to.be.true; expect(personGetPage.notCalled).to.be.true; expect(isContactTypeQualifier.calledOnceWithExactly(personTypeQualifier)).to.be.true; diff --git a/shared-libs/cht-datasource/test/remote/person.spec.ts b/shared-libs/cht-datasource/test/remote/person.spec.ts index a2634acaf3e..d578f27402a 100644 --- a/shared-libs/cht-datasource/test/remote/person.spec.ts +++ b/shared-libs/cht-datasource/test/remote/person.spec.ts @@ -71,11 +71,11 @@ describe('remote person', () => { describe('getPage', () => { const limit = 3; - const skip = 1; + const cursor = '1'; const personTypeQualifier = {contactType: 'person'}; const queryParam = { limit: limit.toString(), - skip: skip.toString(), + cursor, ...personTypeQualifier }; @@ -84,7 +84,7 @@ describe('remote person', () => { const expectedResponse = { data: doc, cursor: '-1' }; getResourcesInner.resolves(expectedResponse); - const result = await Person.v1.getPage(remoteContext)(personTypeQualifier, limit, skip); + const result = await Person.v1.getPage(remoteContext)(personTypeQualifier, cursor, limit); expect(result).to.equal(expectedResponse); expect(getResourcesOuter.calledOnceWithExactly(remoteContext, 'api/v1/person')).to.be.true; @@ -94,7 +94,7 @@ describe('remote person', () => { it('returns empty array if docs are not found', async () => { getResourcesInner.resolves([]); - const result = await Person.v1.getPage(remoteContext)(personTypeQualifier, limit, skip); + const result = await Person.v1.getPage(remoteContext)(personTypeQualifier, cursor, limit); expect(result).to.deep.equal([]); expect(getResourcesOuter.calledOnceWithExactly(remoteContext, 'api/v1/person')).to.be.true; From 3eeb04937b9853beb9698d726b124784b0b5f984 Mon Sep 17 00:00:00 2001 From: Sugat Bajracharya Date: Thu, 8 Aug 2024 16:51:08 +0545 Subject: [PATCH 56/56] Fix eslint issues --- shared-libs/cht-datasource/.eslintrc.js | 3 ++- shared-libs/cht-datasource/test/person.spec.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/shared-libs/cht-datasource/.eslintrc.js b/shared-libs/cht-datasource/.eslintrc.js index 061b1e55a75..6fcd829d220 100644 --- a/shared-libs/cht-datasource/.eslintrc.js +++ b/shared-libs/cht-datasource/.eslintrc.js @@ -43,7 +43,8 @@ module.exports = { MethodDefinition: true, }, publicOnly: true, - }] + }], + ['jsdoc/check-tag-names']: ['error', { definedTags: ['typeParam'] }], } } ] diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index d9313840216..acca2a0658f 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -272,7 +272,7 @@ describe('person', () => { expect(isContactTypeQualifier.notCalled).to.be.true; }); - it('should throw an error for invalid personType', async () => { + it('should throw an error for invalid personType', () => { isContactTypeQualifier.returns(false); expect(() => Person.v1.getAll(dataContext)(personTypeQualifier))