Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#9237): add functionality of getting people with pagination in cht-datasource #9266

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
08e2b27
feat(#9237): Add functionality of getting people with pagination in c…
sugat009 Jul 15, 2024
ab92f0b
Update shared-libs/cht-datasource/src/local/libs/doc.ts
sugat009 Jul 17, 2024
de63197
feat(#9237): Address PR comments
sugat009 Jul 19, 2024
105d5db
Update shared-libs/cht-datasource/src/qualifier.ts
sugat009 Jul 23, 2024
2b92b4c
Update shared-libs/cht-datasource/test/local/person.spec.ts
sugat009 Jul 23, 2024
ba2e27a
Update shared-libs/cht-datasource/src/person.ts
sugat009 Jul 23, 2024
15068fa
Update shared-libs/cht-datasource/src/remote/person.ts
sugat009 Jul 23, 2024
a83a259
Update shared-libs/cht-datasource/src/person.ts
sugat009 Jul 23, 2024
29a3ad5
Update shared-libs/cht-datasource/src/index.ts
sugat009 Jul 23, 2024
8fbc245
feat(#9237): Address PR comments
sugat009 Jul 23, 2024
a99de72
feat(#9237): Add unit tests for getResources in remote mode
sugat009 Jul 23, 2024
d87c258
feat(#9237): Address PR comments
sugat009 Jul 24, 2024
eceeb75
(#feat): Minor fix
sugat009 Jul 24, 2024
1aab0e3
Update shared-libs/cht-datasource/src/person.ts
sugat009 Jul 26, 2024
54d04cb
Update shared-libs/cht-datasource/test/local/libs/lineage.spec.ts
sugat009 Jul 26, 2024
dcc3758
Update shared-libs/cht-datasource/test/local/libs/lineage.spec.ts
sugat009 Jul 26, 2024
e800b1a
Update shared-libs/cht-datasource/test/local/person.spec.ts
sugat009 Jul 26, 2024
34b466c
feat(#9237): Address PR comments
sugat009 Jul 26, 2024
4facbad
Implement cursor based pagination
sugat009 Aug 6, 2024
33d13fd
Fix unit tests according to implementation of cursor pagination
sugat009 Aug 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions shared-libs/cht-datasource/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ 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 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`
*/
sugat009 marked this conversation as resolved.
Show resolved Hide resolved
getPage: (personType: string, limit = 100, skip = 0) => ctx.bind(Person.v1.getPage)(
sugat009 marked this conversation as resolved.
Show resolved Hide resolved
Qualifier.byContactType(personType), limit, skip
),
}
}
};
Expand Down
26 changes: 18 additions & 8 deletions shared-libs/cht-datasource/src/local/libs/doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,25 @@ export const getDocsByIds = (db: PouchDB.Database<Doc>) => async (uuids: string[
.filter((doc): doc is Doc => isDoc(doc));
};

const queryDocs = (db: PouchDB.Database<Doc>, view: string, options: PouchDB.Query.Options<Doc, unknown>) => db
.query(view, {...options})
.then(({ rows }) => rows.map(({ doc }) => isDoc(doc) ? doc : null));

/** @internal */
export const queryDocsByKey = (
export const queryDocsByRange = (
db: PouchDB.Database<Doc>,
view: string
) => async (key: string): Promise<Nullable<Doc>[]> => 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<Nullable<Doc>[]> => queryDocs(db, view, { include_docs: true, startkey, endkey});

/** @internal */
export const queryDocsByKey = (
db: PouchDB.Database<Doc>,
view: string
) => async (
key: unknown,
limit: number,
skip: number
): Promise<Nullable<Doc>[]> => queryDocs(db, view, { include_docs: true, key, limit, skip });
9 changes: 7 additions & 2 deletions shared-libs/cht-datasource/src/local/libs/lineage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -20,7 +20,12 @@ import logger from '@medic/logger';
*/
export const getLineageDocsById = (
medicDb: PouchDB.Database<Doc>
): (id: string) => Promise<Nullable<Doc>[]> => queryDocsByKey(medicDb, 'medic-client/docs_by_id_lineage');
): (
id: string
) => Promise<Nullable<Doc>[]> => {
sugat009 marked this conversation as resolved.
Show resolved Hide resolved
const fn = queryDocsByRange(medicDb, 'medic-client/docs_by_id_lineage');
return (id: string) => fn([id], [id, {}]);
};

/** @internal */
export const getPrimaryContactIds = (places: NonEmptyArray<Nullable<Doc>>): string[] => places
Expand Down
22 changes: 20 additions & 2 deletions shared-libs/cht-datasource/src/local/person.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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 } 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';
Expand Down Expand Up @@ -58,4 +58,22 @@ export namespace v1 {
return deepCopy(personWithLineage);
};
};

/** @internal */
export const getPage = ({ medicDb, settings }: LocalDataContext) => {
const personTypes = contactTypeUtils.getPersonTypes(settings.getAll());
const personTypesIds = personTypes.map(item => item.id);
sugat009 marked this conversation as resolved.
Show resolved Hide resolved

const getDocsByPage = queryDocsByKey(medicDb, 'medic-client/contacts_by_type');

return async (personType: ContactTypeQualifier, limit: number, skip: number): Promise<Person.v1.Person[]> => {
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));
sugat009 marked this conversation as resolved.
Show resolved Hide resolved
};
};
}
31 changes: 30 additions & 1 deletion shared-libs/cht-datasource/src/person.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -32,6 +32,14 @@ export namespace v1 {
}
};

const assertTypeQualifier: (
qualifier: unknown
) => asserts qualifier is ContactTypeQualifier = (qualifier: unknown) => {
if (!isContactTypeQualifier(qualifier)) {
sugat009 marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(`Invalid type [${JSON.stringify(qualifier)}].`);
}
};

const getPerson = <T>(
localFn: (c: LocalDataContext) => (qualifier: UuidQualifier) => Promise<T>,
remoteFn: (c: RemoteDataContext) => (qualifier: UuidQualifier) => Promise<T>
Expand All @@ -44,6 +52,19 @@ export namespace v1 {
};
};

const getPeople = <T>(
localFn: (c: LocalDataContext) => (personType: ContactTypeQualifier, limit: number, skip: number) => Promise<T>,
sugat009 marked this conversation as resolved.
Show resolved Hide resolved
remoteFn: (c: RemoteDataContext) => (personType: ContactTypeQualifier, limit: number, skip: number) => Promise<T>
) => (context: DataContext) => {
assertDataContext(context);
const fn = adapt(context, localFn, remoteFn);

return async (personType: ContactTypeQualifier, limit = 100, skip = 0): Promise<T> => {
assertTypeQualifier(personType);
sugat009 marked this conversation as resolved.
Show resolved Hide resolved
return fn(personType, limit, skip);
};
};
jkuester marked this conversation as resolved.
Show resolved Hide resolved

/**
* Returns a person for the given qualifier.
* @param context the current data context
Expand All @@ -59,4 +80,12 @@ 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
* @throws Error if the provided context or personType qualifier is invalid
*/
export const getPage = getPeople(Local.Person.v1.getPage, Remote.Person.v1.getPage);
sugat009 marked this conversation as resolved.
Show resolved Hide resolved
}
28 changes: 28 additions & 0 deletions shared-libs/cht-datasource/src/qualifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
sugat009 marked this conversation as resolved.
Show resolved Hide resolved
*/
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' });
};
18 changes: 18 additions & 0 deletions shared-libs/cht-datasource/src/remote/libs/data-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,21 @@ export const getResource = (context: RemoteDataContext, path: string) => async <
throw error;
}
};

/** @internal */
export const getResources = (context: RemoteDataContext, path: string) => async <T>(
queryParams?: Record<string, string>,
): Promise<T> => {
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;
}
};
15 changes: 13 additions & 2 deletions shared-libs/cht-datasource/src/remote/person.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,4 +21,13 @@ export namespace v1 {
identifier.uuid,
{ with_lineage: 'true' }
);

/** @internal */
export const getPage = (remoteContext: RemoteDataContext) => (
personType: ContactTypeQualifier,
limit: number,
skip: number
): Promise<null> => getPeople(remoteContext)(
sugat009 marked this conversation as resolved.
Show resolved Hide resolved
{'limit': limit.toString(), 'skip': skip.toString(), 'contactType': personType.contactType}
);
}
21 changes: 20 additions & 1 deletion shared-libs/cht-datasource/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -124,6 +125,24 @@ 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<Doc>[] = [];
sugat009 marked this conversation as resolved.
Show resolved Hide resolved
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(personType, limit, skip);

expect(returnedPeople).to.equal(expectedPeople);
expect(dataContextBind.calledOnceWithExactly(Person.v1.getPage)).to.be.true;
expect(personGetPage.calledOnceWithExactly(personTypeQualifier, limit, skip)).to.be.true;
expect(byContactType.calledOnceWithExactly(personType)).to.be.true;
});
});
});
});
Loading
Loading