From 9ae0648d7669b085e14907d15ab77db5e00eee45 Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Fri, 22 May 2026 20:54:36 -0500 Subject: [PATCH 1/3] Update cht-datasource to properly include documentation for the Datasource interface in the docs site --- eslint.config.js | 2 +- shared-libs/cht-datasource/README.md | 37 +- shared-libs/cht-datasource/package.json | 2 +- shared-libs/cht-datasource/src/contact.ts | 138 ++++++ shared-libs/cht-datasource/src/index.ts | 540 +++------------------- shared-libs/cht-datasource/src/person.ts | 94 +++- shared-libs/cht-datasource/src/place.ts | 99 +++- shared-libs/cht-datasource/src/report.ts | 92 +++- shared-libs/cht-datasource/src/target.ts | 91 +++- 9 files changed, 615 insertions(+), 480 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 69af88c225e..9ea4dbc48db 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -612,7 +612,7 @@ module.exports = defineConfig([ }], ['jsdoc/check-tag-names']: ['error', { - definedTags: ['typeParam'], + definedTags: ['typeParam', 'packageDocumentation'], }], }, }, diff --git a/shared-libs/cht-datasource/README.md b/shared-libs/cht-datasource/README.md index bc35948cf51..7204a976712 100644 --- a/shared-libs/cht-datasource/README.md +++ b/shared-libs/cht-datasource/README.md @@ -2,7 +2,42 @@ The CHT Datasource library is intended to be agnostic and simple. It provides a versioned API from feature modules. -See the TSDoc in [the code](./src/index.ts) for more information about using the API. +## Usage + +To get started, get a `DataContext`: + +```ts +import { getRemoteDataContext, getLocalDataContext } from '@medic/cht-datasource'; + +const dataContext = isOnlineOnly + ? getRemoteDataContext(...) + : getLocalDataContext(...); +``` + +Then, use the context to perform data operations. There are two different usage modes available for performing the +same operations. + +### Declarative usage mode +```ts +import { Person, Qualifier } from '@medic/cht-datasource'; + +const getPerson = Person.v1.get(dataContext); +// Or +const getPerson = dataContext.bind(Person.v1.get); + +const myUuid = 'my-uuid'; +const myPerson = await getPerson(Qualifier.byUuid(uuid)); +``` + +### Imperative usage mode + +```ts +import { getDatasource } from '@medic/cht-datasource'; + +const datasource = getDatasource(dataContext); +const myUuid = 'my-uuid'; +const myPerson = await datasource.v1.person.getByUuid(myUuid); +``` ## Development diff --git a/shared-libs/cht-datasource/package.json b/shared-libs/cht-datasource/package.json index 9ca66d7f5ba..8f32cf07fe0 100644 --- a/shared-libs/cht-datasource/package.json +++ b/shared-libs/cht-datasource/package.json @@ -11,7 +11,7 @@ "build": "tsc -p tsconfig.build.json", "build-watch": "tsc --watch -p tsconfig.build.json", "test": "nyc --nycrcPath='../nyc.config.js' mocha \"test/**/*\"", - "gen-docs": "typedoc ./src/index.ts" + "gen-docs": "typedoc ./src/index.ts --readme none --excludeInternal" }, "author": "", "license": "Apache-2.0", diff --git a/shared-libs/cht-datasource/src/contact.ts b/shared-libs/cht-datasource/src/contact.ts index 43231b1114e..67d3f9d6b89 100644 --- a/shared-libs/cht-datasource/src/contact.ts +++ b/shared-libs/cht-datasource/src/contact.ts @@ -4,6 +4,10 @@ import { Page, } from './libs/core'; import { + and, + byContactType, + byFreetext, + byUuid, ContactTypeQualifier, FreetextQualifier, UuidQualifier @@ -139,4 +143,138 @@ export namespace v1 { }; return curriedGen; }; + + /** + * Operations for working with contacts. + */ + export interface Datasource { + /** + * Returns a contact by their UUID. + * @param uuid the UUID of the contact to retrieve + * @returns the contact or `null` if no contact is found for the UUID + * @throws InvalidArgumentError if no UUID is provided + */ + getByUuid: (uuid: string) => Promise>; + + /** + * Returns a contact by their UUID along with the contact's parent lineage. + * @param uuid the UUID of the contact to retrieve + * @returns the contact or `null` if no contact is found for the UUID + * @throws InvalidArgumentError if no UUID is provided + */ + getByUuidWithLineage: (uuid: string) => Promise>; + + /** + * Returns an array of contact identifiers for the provided page specifications, freetext and type. + * @param freetext the search keyword(s) + * @param type the type of contact to search for + * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be + * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. + * @param limit the maximum number of identifiers to return. Default is 10000. + * @returns a page of contact identifiers for the provided specifications + * @throws InvalidArgumentError if either `freetext` or `type` is not provided + * @throws InvalidArgumentError if the `freetext` is empty or if the `type is invalid for a contact + * @throws InvalidArgumentError if the provided limit is `<= 0` + * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` + */ + getUuidsPageByTypeFreetext: ( + freetext: string, + type: string, + cursor?: Nullable, + limit?: number | `${number}` + ) => Promise>; + + /** + * Returns an array of contact identifiers for the provided page specifications and type. + * @param type the type of contact to search for + * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be + * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. + * @param limit the maximum number of identifiers to return. Default is 10000. + * @returns a page of contact identifiers for the provided specifications + * @throws InvalidArgumentError if `type` is not provided + * @throws InvalidArgumentError if the `type is invalid for a contact + * @throws InvalidArgumentError if the provided limit is `<= 0` + * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` + */ + getUuidsPageByType: ( + type: string, + cursor?: Nullable, + limit?: number | `${number}` + ) => Promise>; + + /** + * Returns an array of contact identifiers for the provided page specifications and freetext. + * @param freetext the search keyword(s) + * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be + * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. + * @param limit the maximum number of identifiers to return. Default is 10000. + * @returns a page of contact identifiers for the provided specifications + * @throws InvalidArgumentError if `freetext` is not provided + * @throws InvalidArgumentError if the `freetext` is less than 3 characters long or if it contains white-space + * @throws InvalidArgumentError if the provided limit is `<= 0` + * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` + */ + getUuidsPageByFreetext: ( + freetext: string, + cursor?: Nullable, + limit?: number | `${number}` + ) => Promise>; + + /** + * Returns a generator for fetching all the contact identifiers for given `freetext` and `type`. + * @param freetext the search keyword(s) + * @param type the type of contact identifiers to return + * @returns a generator for fetching all the contact identifiers matching the given `freetext` and `type`. + * @throws InvalidArgumentError if either `freetext` or `type` is not provided + * @throws InvalidArgumentError if the `freetext` is empty or if the `type is invalid for a contact + */ + getUuidsByTypeFreetext: (freetext: string, type: string) => AsyncGenerator; + + /** + * Returns a generator for fetching all the contact identifiers for given `type`. + * @param type the type of contact identifiers to return + * @returns a generator for fetching all the contact identifiers matching the given `type`. + * @throws InvalidArgumentError if `type` is not provided + * @throws InvalidArgumentError if the `type is invalid for a contact + */ + getUuidsByType: (type: string) => AsyncGenerator; + + /** + * Returns a generator for fetching all the contact identifiers for given `freetext`. + * @param freetext the search keyword(s) + * @returns a generator for fetching all the contact identifiers matching the given `freetext`. + * @throws InvalidArgumentError if `freetext`is not provided + * @throws InvalidArgumentError if the `freetext` is empty or invalid + */ + getUuidsByFreetext: (freetext: string) => AsyncGenerator; + } + + /** @internal */ + export const getDatasource = (ctx: DataContext): Datasource => { + return { + getByUuid: (uuid) => ctx.bind(v1.get)(byUuid(uuid)), + getByUuidWithLineage: (uuid) => ctx.bind(v1.getWithLineage)(byUuid(uuid)), + getUuidsPageByTypeFreetext: ( + freetext, + type, + cursor = null, + limit = DEFAULT_IDS_PAGE_LIMIT + ) => ctx.bind(v1.getUuidsPage)(and(byFreetext(freetext), byContactType(type)), cursor, limit), + getUuidsPageByType: ( + type, + cursor = null, + limit = DEFAULT_IDS_PAGE_LIMIT + ) => ctx.bind(v1.getUuidsPage)(byContactType(type), cursor, limit), + getUuidsPageByFreetext: ( + freetext, + cursor = null, + limit = DEFAULT_IDS_PAGE_LIMIT + ) => ctx.bind(v1.getUuidsPage)(byFreetext(freetext), cursor, limit), + getUuidsByTypeFreetext: (freetext, type) => ctx.bind(v1.getUuids)( + and(byFreetext(freetext), byContactType(type)) + ), + getUuidsByType: (type) => ctx.bind(v1.getUuids)(byContactType(type)), + getUuidsByFreetext: (freetext) => ctx.bind(v1.getUuids)(byFreetext(freetext)), + }; + }; } diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index a020911a09b..65607ceadc0 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -1,45 +1,21 @@ /** - * CHT datasource. + * The API for interacting with the CHT data model. See the {@link Datasource} interface for details about the + * available functionality. These APIs are guaranteed to be stable. Non-passive changes will only be released + * in major versions of the CHT platform. * - * This module provides a simple API for interacting with CHT data. To get started, obtain a {@link DataContext}. Then - * use the context to perform data operations. There are two different usage modes available for performing the same - * operations. - * @example Get Data Context: - * import { getRemoteDataContext, getLocalDataContext } from '@medic/cht-datasource'; - * - * const dataContext = isOnlineOnly - * ? getRemoteDataContext(...) - * : getLocalDataContext(...); - * @example Declarative usage mode: - * import { Person, Qualifier } from '@medic/cht-datasource'; - * - * const getPerson = Person.v1.get(dataContext); - * // Or - * const getPerson = dataContext.bind(Person.v1.get); - * - * const myUuid = 'my-uuid'; - * const myPerson = await getPerson(Qualifier.byUuid(uuid)); - * @example Imperative usage mode: - * import { getDatasource } from '@medic/cht-datasource'; - * - * const datasource = getDatasource(dataContext); - * const myUuid = 'my-uuid'; - * const myPerson = await datasource.v1.person.getByUuid(myUuid); + * Note that most data model functions (aside from `v1.hasPermissions` and `v1.hasAnyPermission`) are _asynchronous_ + * and should NOT be used in a _synchronous_ context (such as Tasks, Targets, Contact Summary, and Purge configuration). + * @packageDocumentation */ import { hasAnyPermission, hasPermissions } from './auth'; -import { Nullable } from './libs/core'; import { assertDataContext, DataContext } from './libs/data-context'; import * as Contact from './contact'; import * as Person from './person'; import * as Place from './place'; -import * as Qualifier from './qualifier'; -import { and, byContactId, byContactIds, byReportingPeriod } from './qualifier'; import * as Report from './report'; import * as Target from './target'; -import * as Input from './input'; -import { DEFAULT_DOCS_PAGE_LIMIT, DEFAULT_IDS_PAGE_LIMIT, } from './libs/constants'; -export { Nullable, NonEmptyArray } from './libs/core'; +export { Nullable, NonEmptyArray, Page } from './libs/core'; export { DataContext } from './libs/data-context'; export { getLocalDataContext } from './local'; export { getRemoteDataContext } from './remote'; @@ -52,463 +28,81 @@ export * as Input from './input'; export * as Report from './report'; export * as Target from './target'; +/** + * The CHT datasource API. + */ +export interface Datasource { + v1: { + /** + * Verify if the user's role has the permission(s). + * @param permissions permission(s) to verify + * @param userRoles array of user roles + * @param chtPermissionsSettings Deprecated. Optional override for the permissions config. Omit this to use + * the current settings for the data context. + */ + hasPermissions: ( + permissions: string | string[], + userRoles: string[], + chtPermissionsSettings?: Record + ) => boolean; + + /** + * Verify if the user's role has all the permissions of any of the provided groups. + * @param permissionsGroupList array of groups of permissions + * @param userRoles array of user roles + * @param chtPermissionsSettings Deprecated. Optional override for the permissions config. Omit this to use + * the current settings for the data context. + */ + hasAnyPermission: ( + permissionsGroupList: string[][], + userRoles: string[], + chtPermissionsSettings?: Record + ) => boolean; + + /** Operations for working with contacts. */ + contact: Contact.v1.Datasource; + + /** Operations for working with places. */ + place: Place.v1.Datasource; + + /** Operations for working with people. */ + person: Person.v1.Datasource; + + /** Operations for working with reports. */ + report: Report.v1.Datasource; + + /** Operations for working with targets. */ + target: Target.v1.Datasource; + }; +} + /** * Returns the source for CHT data. * @param ctx the current data context * @returns the CHT datasource API * @throws Error if the provided context is invalid */ -export const getDatasource = (ctx: DataContext) => { +export const getDatasource = (ctx: DataContext): Datasource => { assertDataContext(ctx); return { v1: { - /** - * Verify if the user's role has the permission(s). - * @param permissions permission(s) to verify - * @param userRoles array of user roles - * @param chtPermissionsSettings Deprecated. Optional override for the permissions config. Omit this to use - * the current settings for the data context. - */ hasPermissions: ( - permissions: string | string[], - userRoles: string[], - chtPermissionsSettings?: Record + permissions, + userRoles, + chtPermissionsSettings ) => ctx.bind(hasPermissions)(permissions, userRoles, chtPermissionsSettings), - /** - * Verify if the user's role has all the permissions of any of the provided groups. - * @param permissionsGroupList array of groups of permissions - * @param userRoles array of user roles - * @param chtPermissionsSettings Deprecated. Optional override for the permissions config. Omit this to use - * the current settings for the data context. - */ hasAnyPermission: ( - permissionsGroupList: string[][], - userRoles: string[], - chtPermissionsSettings?: Record + permissionsGroupList, + userRoles, + chtPermissionsSettings ) => ctx.bind(hasAnyPermission)(permissionsGroupList, userRoles, chtPermissionsSettings), - contact: { - /** - * Returns a contact by their UUID. - * @param uuid the UUID of the contact to retrieve - * @returns the contact or `null` if no contact is found for the UUID - * @throws InvalidArgumentError if no UUID is provided - */ - getByUuid: (uuid: string) => ctx.bind(Contact.v1.get)(Qualifier.byUuid(uuid)), - - /** - * Returns a contact by their UUID along with the contact's parent lineage. - * @param uuid the UUID of the contact to retrieve - * @returns the contact or `null` if no contact is found for the UUID - * @throws InvalidArgumentError if no UUID is provided - */ - getByUuidWithLineage: (uuid: string) => ctx.bind(Contact.v1.getWithLineage)(Qualifier.byUuid(uuid)), - - /** - * Returns an array of contact identifiers for the provided page specifications, - * freetext and type - * @param freetext the search keyword(s) - * @param type the type of contact to search for - * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be - * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. - * @param limit the maximum number of identifiers to return. Default is 10000. - * @returns a page of contact identifiers for the provided specifications - * @throws InvalidArgumentError if either `freetext` or `type` is not provided - * @throws InvalidArgumentError if the `freetext` is empty or if the `type is invalid for a contact - * @throws InvalidArgumentError if the provided limit is `<= 0` - * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` - */ - getUuidsPageByTypeFreetext: ( - freetext: string, - type: string, - cursor: Nullable = null, - limit: number | `${number}` = DEFAULT_IDS_PAGE_LIMIT - ) => ctx.bind(Contact.v1.getUuidsPage)( - Qualifier.and(Qualifier.byFreetext(freetext), Qualifier.byContactType(type)), cursor, limit - ), - - /** - * Returns an array of contact identifiers for the provided page specifications and type. - * @param type the type of contact to search for - * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be - * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. - * @param limit the maximum number of identifiers to return. Default is 10000. - * @returns a page of contact identifiers for the provided specifications - * @throws InvalidArgumentError if `type` is not provided - * @throws InvalidArgumentError if the `type is invalid for a contact - * @throws InvalidArgumentError if the provided limit is `<= 0` - * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` - */ - getUuidsPageByType: ( - type: string, - cursor: Nullable = null, - limit: number | `${number}` = DEFAULT_IDS_PAGE_LIMIT - ) => ctx.bind(Contact.v1.getUuidsPage)( - Qualifier.byContactType(type), cursor, limit - ), - - /** - * Returns an array of contact identifiers for the provided page specifications and freetext - * @param freetext the search keyword(s) - * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be - * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. - * @param limit the maximum number of identifiers to return. Default is 10000. - * @returns a page of contact identifiers for the provided specifications - * @throws InvalidArgumentError if `freetext` is not provided - * @throws InvalidArgumentError if the `freetext` is less than 3 characters long or if it contains white-space - * @throws InvalidArgumentError if the provided limit is `<= 0` - * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` - */ - getUuidsPageByFreetext: ( - freetext: string, - cursor: Nullable = null, - limit: number | `${number}` = DEFAULT_IDS_PAGE_LIMIT - ) => ctx.bind(Contact.v1.getUuidsPage)( - Qualifier.byFreetext(freetext), cursor, limit - ), - - /** - * Returns a generator for fetching all the contact identifiers for given - * `freetext` and `type`. - * @param freetext the search keyword(s) - * @param type the type of contact identifiers to return - * @returns a generator for fetching all the contact identifiers matching the given `freetext` and `type`. - * @throws InvalidArgumentError if either `freetext` or `type` is not provided - * @throws InvalidArgumentError if the `freetext` is empty or if the `type is invalid for a contact - */ - getUuidsByTypeFreetext: ( - freetext: string, - type: string - ) => ctx.bind(Contact.v1.getUuids)( - Qualifier.and(Qualifier.byFreetext(freetext), Qualifier.byContactType(type)) - ), - - /** - * Returns a generator for fetching all the contact identifiers for given `type`. - * @param type the type of contact identifiers to return - * @returns a generator for fetching all the contact identifiers matching the given `type`. - * @throws InvalidArgumentError if `type` is not provided - * @throws InvalidArgumentError if the `type is invalid for a contact - */ - getUuidsByType: ( - type: string - ) => ctx.bind(Contact.v1.getUuids)( - Qualifier.byContactType(type) - ), - - /** - * Returns a generator for fetching all the contact identifiers for given - * `freetext`. - * @param freetext the search keyword(s) - * @returns a generator for fetching all the contact identifiers matching the given `freetext`. - * @throws InvalidArgumentError if `freetext`is not provided - * @throws InvalidArgumentError if the `freetext` is empty or invalid - */ - getUuidsByFreetext: ( - freetext: string, - ) => ctx.bind(Contact.v1.getUuids)( - Qualifier.byFreetext(freetext) - ), - }, - place: { - /** - * Returns a place by its UUID. - * @param uuid the UUID of the place to retrieve - * @returns the place or `null` if no place is found for the UUID - * @throws InvalidArgumentError if no UUID is provided - */ - getByUuid: (uuid: string) => ctx.bind(Place.v1.get)(Qualifier.byUuid(uuid)), - - /** - * Returns a place by its UUID along with the place's parent lineage. - * @param uuid the UUID of the place to retrieve - * @returns the place or `null` if no place is found for the UUID - * @throws InvalidArgumentError if no UUID is provided - */ - getByUuidWithLineage: (uuid: string) => ctx.bind(Place.v1.getWithLineage)(Qualifier.byUuid(uuid)), - - /** - * Returns an array of places for the provided page specifications. - * @param placeType the type of place to return - * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be - * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. - * @param limit the maximum number of place to return. Default is 100. - * @returns a page of places for the provided specifications - * @throws InvalidArgumentError if no type is provided or if the type is not a supported place contact type - * @throws InvalidArgumentError if the provided limit is `<= 0` - * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` - * @see {@link getByType} which provides the same data, but without having to manually account for paging - */ - getPageByType: ( - placeType: string, - cursor: Nullable = null, - limit: number | `${number}` = DEFAULT_DOCS_PAGE_LIMIT - ) => ctx.bind(Place.v1.getPage)( - Qualifier.byContactType(placeType), cursor, limit - ), - - /** - * Returns a generator for fetching all places with the given type. - * @param placeType the type of place to return - * @returns a generator for fetching all places with the given type - * @throws InvalidArgumentError if no type if provided or if the type is not a supported place contact type - */ - getByType: (placeType: string) => ctx.bind(Place.v1.getAll)(Qualifier.byContactType(placeType)), - - /** - * Creates a new place record. - * @param input input fields for creating a place - * @returns the created place record - * @throws InvalidArgumentError if `type` is not provided or is not a supported place contact type - * @throws InvalidArgumentError if `name` is not provided - * @throws InvalidArgumentError if `parent` is not provided for types requiring a parent or is not the - * identifier of a valid contact. The parent contact's type must be one of the supported parent contact - * types for the new place. - * @throws InvalidArgumentError if the provided `reported_date` is not in a valid format. Valid formats are - * 'YYYY-MM-DDTHH:mm:ssZ', 'YYYY-MM-DDTHH:mm:ss.SSSZ', or . - * @throws InvalidArgumentError if the provided `contact` is not the identifier of a valid person contact - */ - create: (input: Input.v1.PlaceInput) => ctx.bind(Place.v1.create)(input), - - /** - * Updates an existing place to have the provided data. - * @param updated the updated place data. The complete data for the place must be provided. Existing fields not - * included in the updated data will be removed from the place. If the provided parent/contact lineage is - * hydrated (e.g. for a {@link PlaceWithLineage}), the lineage will be properly dehydrated before being stored. - * @returns the updated place with the new `_rev` value - * @throws InvalidArgumentError if `_id` is not provided - * @throws ResourceNotFoundError if `_id does not identify an existing place contact - * @throws InvalidArgumentError if `_rev` is not provided or does not match the place's current `_rev` value - * @throws InvalidArgumentError if `name` is not provided - * @throws InvalidArgumentError if the provided `contact` is not the identifier of a valid person contact - * @throws InvalidArgumentError if any of the following read-only properties are changed: `reported_date`, - * `parent`, `type`, `contact_type` - */ - update: ( - updated: Input.v1.UpdatePlaceInput - ) => ctx.bind(Place.v1.update)(updated) - }, - person: { - /** - * Returns a person by their UUID. - * @param uuid the UUID of the person to retrieve - * @returns the person or `null` if no person is found for the UUID - * @throws InvalidArgumentError if no UUID is provided - */ - getByUuid: (uuid: string) => ctx.bind(Person.v1.get)(Qualifier.byUuid(uuid)), - - /** - * Returns a person by their UUID along with the person's parent lineage. - * @param uuid the UUID of the person to retrieve - * @returns the person or `null` if no person is found for the UUID - * @throws InvalidArgumentError if no UUID is provided - */ - getByUuidWithLineage: (uuid: string) => ctx.bind(Person.v1.getWithLineage)(Qualifier.byUuid(uuid)), - - /** - * Returns an array of people for the provided page specifications. - * @param personType the type of people to return - * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be - * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. - * @param limit the maximum number of people to return. Default is 100. - * @returns a page of people for the provided specifications - * @throws InvalidArgumentError if no type is provided or if the type is not for a person - * @throws InvalidArgumentError if the provided limit is `<= 0` - * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` - * @see {@link getByType} which provides the same data, but without having to manually account for paging - */ - getPageByType: ( - personType: string, - cursor: Nullable = null, - limit: number | `${number}` = DEFAULT_DOCS_PAGE_LIMIT - ) => ctx.bind(Person.v1.getPage)( - Qualifier.byContactType(personType), cursor, limit - ), - - /** - * 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 InvalidArgumentError 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)), - - /** - * Creates a new person record. - * @param input input fields for creating a person - * @returns the created person record - * @throws InvalidArgumentError if `type` is not provided or is not a supported person contact type - * @throws InvalidArgumentError if `name` is not provided - * @throws InvalidArgumentError if `parent` is not provided or is not the identifier of a valid contact. The - * parent contact's type must be one of the supported parent contact types for the new person. - * @throws InvalidArgumentError if the provided `reported_date` is not in a valid format. Valid formats are - * 'YYYY-MM-DDTHH:mm:ssZ', 'YYYY-MM-DDTHH:mm:ss.SSSZ', or . - */ - create: (input: Input.v1.PersonInput) => ctx.bind(Person.v1.create)(input), - - /** - * Updates an existing person to have the provided data. - * @param updated the updated person data. The complete data for the person must be provided. Existing fields - * not included in the updated data will be removed from the person. If the provided parent lineage is - * hydrated (e.g. for a {@link PersonWithLineage}), the lineage will be properly dehydrated before being stored. - * @returns the updated person with the new `_rev` value - * @throws InvalidArgumentError if `_id` is not provided - * @throws ResourceNotFoundError if `_id does not identify an existing person contact - * @throws InvalidArgumentError if `_rev` is not provided or does not match the person's current `_rev` value - * @throws InvalidArgumentError if `name` is not provided - * @throws InvalidArgumentError if any of the following read-only properties are changed: `reported_date`, - * `parent`, `type`, `contact_type` - */ - update: ( - updated: T - ) => ctx.bind(Person.v1.update)(updated) - }, - report: { - /** - * Returns a report by their UUID. - * @param uuid the UUID of the report to retrieve - * @returns the report or `null` if no report is found for the UUID - * @throws InvalidArgumentError if no UUID is provided - */ - getByUuid: (uuid: string) => ctx.bind(Report.v1.get)(Qualifier.byUuid(uuid)), - - /** - * Returns a report by UUID along with the report's lineage information. - * @param uuid the UUID of the report to retrieve - * @returns the report or `null` if no report is found for the UUID - * @throws InvalidArgumentError if no UUID is provided - */ - getByUuidWithLineage: (uuid: string) => ctx.bind(Report.v1.getWithLineage)(Qualifier.byUuid(uuid)), - - /** - * Returns a paged array of report identifiers from the given data context. - * @param qualifier the limiter defining which identifiers to return - * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be - * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. - * @param limit the maximum number of identifiers to return. Default is 10000. - * @returns a page of report identifiers for the provided specification - * @throws InvalidArgumentError if no qualifier is provided or if the qualifier is invalid - * @throws InvalidArgumentError if the provided `limit` value is `<=0` - * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` - */ - getUuidsPageByFreetext: ( - qualifier: string, - cursor: Nullable = null, - limit: number | `${number}` = DEFAULT_IDS_PAGE_LIMIT - ) => ctx.bind(Report.v1.getUuidsPage)( - Qualifier.byFreetext(qualifier), cursor, limit - ), - - /** - * Returns a generator for fetching all the contact identifiers for given qualifier - * @param qualifier the limiter defining which identifiers to return - * @returns a generator for fetching all report identifiers that match the given qualifier - * @throws InvalidArgumentError if no qualifier is provided or if the qualifier is invalid - */ - getUuidsByFreetext: ( - qualifier: string, - ) => ctx.bind(Report.v1.getUuids)(Qualifier.byFreetext(qualifier)), - - /** - * Creates a new report record. - * @param input input fields for creating a report - * @returns the created report record - * @throws InvalidArgumentError if `form` is not provided or is not a supported form id - * @throws InvalidArgumentError if `contact` is not provided or is not the identifier of a valid contact - * @throws InvalidArgumentError if the provided `reported_date` is not in a valid format. Valid formats are - * 'YYYY-MM-DDTHH:mm:ssZ', 'YYYY-MM-DDTHH:mm:ss.SSSZ', or . - */ - create: (input: Input.v1.ReportInput) => ctx.bind(Report.v1.create)(input), - - /** - * Updates an existing report to have the provided data. - * @param updated the updated report data. The complete data for the report must be provided. Existing fields - * not included in the updated data will be removed from the report. If the provided parent/patient/place - * lineage is hydrated (e.g. for a {@link ReportWithLineage}), the lineage will be properly dehydrated before - * being stored. - * @returns the updated report with the new `_rev` value - * @throws InvalidArgumentError if `_id` is not provided - * @throws ResourceNotFoundError if `_id does not identify an existing report - * @throws InvalidArgumentError if `_rev` is not provided or does not match the report's current `_rev` value - * @throws InvalidArgumentError if `form` is not provided or is not a supported form id - * @throws InvalidArgumentError if `contact` is not provided or is not a valid contact - * @throws InvalidArgumentError if any of the following read-only properties are changed: `reported_date`, - * `type` - */ - update: ( - updated: T - ) => ctx.bind(Report.v1.update)(updated) - }, - target: { - /** - * Returns a target by identifier. - * @param id the identifier of the target to retrieve - * @returns the target or `null` if no target is found for the identifier - * @throws InvalidArgumentError if no identifier is provided - */ - getById: (id: string) => ctx.bind(Target.v1.get)(Qualifier.byId(id)), - - /** - * Returns the target for the given username and reporting period. - * @param reportingPeriod the reporting period for the target - * @param contactId the contact identifier of the user for the target - * @param username the username (without the "org.couchdb.user:" prefix) of the user for the target - * @returns the target or `null` if no target is found - * @throws InvalidArgumentError if no reporting period, contact identifier, and/or username is provided - */ - getByReportingPeriodContactIdUsername: ( - reportingPeriod: string, - contactId: string, - username: string - ) => ctx.bind(Target.v1.get)(and( - Qualifier.byReportingPeriod(reportingPeriod), - Qualifier.byContactId(contactId), - Qualifier.byUsername(username) - )), - - /** - * Returns an array of targets for the provided page specifications. - * @param reportingPeriod the reporting period for the targets - * @param contactIds the contact identifiers for the targets - * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be - * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. - * @param limit the maximum number of targets to return. Default is 100. - * @returns a page of targets for the provided specifications - * @throws InvalidArgumentError if no reporting period is provided - * @throws InvalidArgumentError if no contact identifiers are provided - * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` - * @throws InvalidArgumentError if the provided limit is `<= 0` - * @see {@link getByReportingPeriodContactIds} which provides the same data, but without having to manually - * account for paging - */ - getPageByReportingPeriodContactIds: ( - reportingPeriod: string, - contactIds: string | [string, ...string[]], - cursor: Nullable = null, - limit: number | `${number}` = DEFAULT_DOCS_PAGE_LIMIT - ) => ctx.bind(Target.v1.getPage)( - and( - byReportingPeriod(reportingPeriod), - Array.isArray(contactIds) ? byContactIds(contactIds) : byContactId(contactIds) - ), cursor, limit - ), - /** - * Returns a generator for fetching all targets in the reporting period for the given contact identifiers. - * @param reportingPeriod the reporting period for the targets - * @param contactIds the contact identifiers for the targets - * @returns a generator for fetching all targets with the given type - * @throws InvalidArgumentError if no reporting period is provided - * @throws InvalidArgumentError if no contact identifiers are provided - */ - getByReportingPeriodContactIds: ( - reportingPeriod: string, - contactIds: string | [string, ...string[]] - ) => ctx.bind(Target.v1.getAll)(and( - byReportingPeriod(reportingPeriod), - Array.isArray(contactIds) ? byContactIds(contactIds) : byContactId(contactIds) - )) - } + contact: Contact.v1.getDatasource(ctx), + place: Place.v1.getDatasource(ctx), + person: Person.v1.getDatasource(ctx), + report: Report.v1.getDatasource(ctx), + target: Target.v1.getDatasource(ctx), } }; }; diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 8b7c924e854..af171564f96 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -1,4 +1,4 @@ -import { ContactTypeQualifier, UuidQualifier } from './qualifier'; +import { byContactType, byUuid, ContactTypeQualifier, UuidQualifier } from './qualifier'; import { adapt, assertDataContext, DataContext } from './libs/data-context'; import * as Contact from './contact'; import * as Remote from './remote'; @@ -188,4 +188,96 @@ export namespace v1 { }; return curriedFn; }; + + /** + * Operations for working with people. + */ + export interface Datasource { + /** + * Returns a person by their UUID. + * @param uuid the UUID of the person to retrieve + * @returns the person or `null` if no person is found for the UUID + * @throws InvalidArgumentError if no UUID is provided + */ + getByUuid: (uuid: string) => Promise>; + + /** + * Returns a person by their UUID along with the person's parent lineage. + * @param uuid the UUID of the person to retrieve + * @returns the person or `null` if no person is found for the UUID + * @throws InvalidArgumentError if no UUID is provided + */ + getByUuidWithLineage: (uuid: string) => Promise>; + + /** + * Returns an array of people for the provided page specifications. + * @param personType the type of people to return + * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be + * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. + * @param limit the maximum number of people to return. Default is 100. + * @returns a page of people for the provided specifications + * @throws InvalidArgumentError if no type is provided or if the type is not for a person + * @throws InvalidArgumentError if the provided limit is `<= 0` + * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` + * @see {@link getByType} which provides the same data, but without having to manually account for paging + */ + getPageByType: ( + personType: string, + cursor?: Nullable, + limit?: number | `${number}` + ) => Promise>; + + /** + * 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 InvalidArgumentError if no type is provided or if the type is not for a person + */ + getByType: (personType: string) => AsyncGenerator; + + /** + * Creates a new person record. + * @param input input fields for creating a person + * @returns the created person record + * @throws InvalidArgumentError if `type` is not provided or is not a supported person contact type + * @throws InvalidArgumentError if `name` is not provided + * @throws InvalidArgumentError if `parent` is not provided or is not the identifier of a valid contact. The + * parent contact's type must be one of the supported parent contact types for the new person. + * @throws InvalidArgumentError if the provided `reported_date` is not in a valid format. Valid formats are + * 'YYYY-MM-DDTHH:mm:ssZ', 'YYYY-MM-DDTHH:mm:ss.SSSZ', or . + */ + create: (input: Input.v1.PersonInput) => Promise; + + /** + * Updates an existing person to have the provided data. + * @param updated the updated person data. The complete data for the person must be provided. Existing fields + * not included in the updated data will be removed from the person. If the provided parent lineage is + * hydrated (e.g. for a {@link v1.PersonWithLineage}), the lineage will be properly dehydrated before being + * stored. + * @returns the updated person with the new `_rev` value + * @throws InvalidArgumentError if `_id` is not provided + * @throws ResourceNotFoundError if `_id does not identify an existing person contact + * @throws InvalidArgumentError if `_rev` is not provided or does not match the person's current `_rev` value + * @throws InvalidArgumentError if `name` is not provided + * @throws InvalidArgumentError if any of the following read-only properties are changed: `reported_date`, + * `parent`, `type`, `contact_type` + */ + update: (updated: T) => Promise; + } + + /** @internal */ + export const getDatasource = (ctx: DataContext): Datasource => { + return { + getByUuid: (uuid) => ctx.bind(v1.get)(byUuid(uuid)), + getByUuidWithLineage: (uuid) => ctx.bind(v1.getWithLineage)(byUuid(uuid)), + getPageByType: ( + personType, + cursor = null, + limit = DEFAULT_DOCS_PAGE_LIMIT + ) => ctx.bind(v1.getPage)(byContactType(personType), cursor, limit), + getByType: (personType) => ctx.bind(v1.getAll)(byContactType(personType)), + create: (input) => ctx.bind(v1.create)(input), + update: (updated) => ctx.bind(v1.update)(updated), + }; + }; } diff --git a/shared-libs/cht-datasource/src/place.ts b/shared-libs/cht-datasource/src/place.ts index 2662036aa1e..694fff8b869 100644 --- a/shared-libs/cht-datasource/src/place.ts +++ b/shared-libs/cht-datasource/src/place.ts @@ -1,7 +1,7 @@ import * as Contact from './contact'; import * as Person from './person'; import { LocalDataContext } from './local/libs/data-context'; -import { ContactTypeQualifier, UuidQualifier } from './qualifier'; +import { byContactType, byUuid, ContactTypeQualifier, UuidQualifier } from './qualifier'; import { RemoteDataContext } from './remote/libs/data-context'; import { adapt, assertDataContext, DataContext } from './libs/data-context'; import * as Local from './local'; @@ -185,4 +185,101 @@ export namespace v1 { }; return curriedFn; }; + + /** + * Operations for working with places. + */ + export interface Datasource { + /** + * Returns a place by its UUID. + * @param uuid the UUID of the place to retrieve + * @returns the place or `null` if no place is found for the UUID + * @throws InvalidArgumentError if no UUID is provided + */ + getByUuid: (uuid: string) => Promise>; + + /** + * Returns a place by its UUID along with the place's parent lineage. + * @param uuid the UUID of the place to retrieve + * @returns the place or `null` if no place is found for the UUID + * @throws InvalidArgumentError if no UUID is provided + */ + getByUuidWithLineage: (uuid: string) => Promise>; + + /** + * Returns an array of places for the provided page specifications. + * @param placeType the type of place to return + * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be + * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. + * @param limit the maximum number of place to return. Default is 100. + * @returns a page of places for the provided specifications + * @throws InvalidArgumentError if no type is provided or if the type is not a supported place contact type + * @throws InvalidArgumentError if the provided limit is `<= 0` + * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` + * @see {@link getByType} which provides the same data, but without having to manually account for paging + */ + getPageByType: ( + placeType: string, + cursor?: Nullable, + limit?: number | `${number}` + ) => Promise>; + + /** + * Returns a generator for fetching all places with the given type. + * @param placeType the type of place to return + * @returns a generator for fetching all places with the given type + * @throws InvalidArgumentError if no type if provided or if the type is not a supported place contact type + */ + getByType: (placeType: string) => AsyncGenerator; + + /** + * Creates a new place record. + * @param input input fields for creating a place + * @returns the created place record + * @throws InvalidArgumentError if `type` is not provided or is not a supported place contact type + * @throws InvalidArgumentError if `name` is not provided + * @throws InvalidArgumentError if `parent` is not provided for types requiring a parent or is not the + * identifier of a valid contact. The parent contact's type must be one of the supported parent contact + * types for the new place. + * @throws InvalidArgumentError if the provided `reported_date` is not in a valid format. Valid formats are + * 'YYYY-MM-DDTHH:mm:ssZ', 'YYYY-MM-DDTHH:mm:ss.SSSZ', or . + * @throws InvalidArgumentError if the provided `contact` is not the identifier of a valid person contact + */ + create: (input: Input.v1.PlaceInput) => Promise; + + /** + * Updates an existing place to have the provided data. + * @param updated the updated place data. The complete data for the place must be provided. Existing fields not + * included in the updated data will be removed from the place. If the provided parent/contact lineage is + * hydrated (e.g. for a {@link v1.PlaceWithLineage}), the lineage will be properly dehydrated before being + * stored. + * @returns the updated place with the new `_rev` value + * @throws InvalidArgumentError if `_id` is not provided + * @throws ResourceNotFoundError if `_id does not identify an existing place contact + * @throws InvalidArgumentError if `_rev` is not provided or does not match the place's current `_rev` value + * @throws InvalidArgumentError if `name` is not provided + * @throws InvalidArgumentError if the provided `contact` is not the identifier of a valid person contact + * @throws InvalidArgumentError if any of the following read-only properties are changed: `reported_date`, + * `parent`, `type`, `contact_type` + */ + update: ( + updated: Input.v1.UpdatePlaceInput + ) => Promise; + } + + /** @internal */ + export const getDatasource = (ctx: DataContext): Datasource => { + return { + getByUuid: (uuid) => ctx.bind(v1.get)(byUuid(uuid)), + getByUuidWithLineage: (uuid) => ctx.bind(v1.getWithLineage)(byUuid(uuid)), + getPageByType: ( + placeType, + cursor = null, + limit = DEFAULT_DOCS_PAGE_LIMIT + ) => ctx.bind(v1.getPage)(byContactType(placeType), cursor, limit), + getByType: (placeType) => ctx.bind(v1.getAll)(byContactType(placeType)), + create: (input) => ctx.bind(v1.create)(input), + update: (updated) => ctx.bind(v1.update)(updated), + }; + }; } diff --git a/shared-libs/cht-datasource/src/report.ts b/shared-libs/cht-datasource/src/report.ts index 55466e367c6..a3d7dd3431f 100644 --- a/shared-libs/cht-datasource/src/report.ts +++ b/shared-libs/cht-datasource/src/report.ts @@ -2,7 +2,7 @@ import { DataObject, getPagedGenerator, isIdentifiable, isRecord, NormalizedPare import { adapt, assertDataContext, DataContext } from './libs/data-context'; import { Doc } from './libs/doc'; import * as Local from './local'; -import { FreetextQualifier, UuidQualifier } from './qualifier'; +import { byFreetext, byUuid, FreetextQualifier, UuidQualifier } from './qualifier'; import * as Remote from './remote'; import { DEFAULT_IDS_PAGE_LIMIT } from './libs/constants'; import { assertCursor, assertFreetextQualifier, assertLimit, assertUuidQualifier } from './libs/parameter-validators'; @@ -205,4 +205,94 @@ export namespace v1 { }; return curriedFnWithLineage; }; + + /** + * Operations for working with reports. + */ + export interface Datasource { + /** + * Returns a report by their UUID. + * @param uuid the UUID of the report to retrieve + * @returns the report or `null` if no report is found for the UUID + * @throws InvalidArgumentError if no UUID is provided + */ + getByUuid: (uuid: string) => Promise>; + + /** + * Returns a report by UUID along with the report's lineage information. + * @param uuid the UUID of the report to retrieve + * @returns the report or `null` if no report is found for the UUID + * @throws InvalidArgumentError if no UUID is provided + */ + getByUuidWithLineage: (uuid: string) => Promise>; + + /** + * Returns a paged array of report identifiers from the given data context. + * @param qualifier the limiter defining which identifiers to return + * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be + * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. + * @param limit the maximum number of identifiers to return. Default is 10000. + * @returns a page of report identifiers for the provided specification + * @throws InvalidArgumentError if no qualifier is provided or if the qualifier is invalid + * @throws InvalidArgumentError if the provided `limit` value is `<=0` + * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` + */ + getUuidsPageByFreetext: ( + qualifier: string, + cursor?: Nullable, + limit?: number | `${number}` + ) => Promise>; + + /** + * Returns a generator for fetching all the contact identifiers for given qualifier. + * @param qualifier the limiter defining which identifiers to return + * @returns a generator for fetching all report identifiers that match the given qualifier + * @throws InvalidArgumentError if no qualifier is provided or if the qualifier is invalid + */ + getUuidsByFreetext: (qualifier: string) => AsyncGenerator; + + /** + * Creates a new report record. + * @param input input fields for creating a report + * @returns the created report record + * @throws InvalidArgumentError if `form` is not provided or is not a supported form id + * @throws InvalidArgumentError if `contact` is not provided or is not the identifier of a valid contact + * @throws InvalidArgumentError if the provided `reported_date` is not in a valid format. Valid formats are + * 'YYYY-MM-DDTHH:mm:ssZ', 'YYYY-MM-DDTHH:mm:ss.SSSZ', or . + */ + create: (input: Input.v1.ReportInput) => Promise; + + /** + * Updates an existing report to have the provided data. + * @param updated the updated report data. The complete data for the report must be provided. Existing fields + * not included in the updated data will be removed from the report. If the provided parent/patient/place + * lineage is hydrated (e.g. for a {@link v1.ReportWithLineage}), the lineage will be properly dehydrated + * before being stored. + * @returns the updated report with the new `_rev` value + * @throws InvalidArgumentError if `_id` is not provided + * @throws ResourceNotFoundError if `_id does not identify an existing report + * @throws InvalidArgumentError if `_rev` is not provided or does not match the report's current `_rev` value + * @throws InvalidArgumentError if `form` is not provided or is not a supported form id + * @throws InvalidArgumentError if `contact` is not provided or is not a valid contact + * @throws InvalidArgumentError if any of the following read-only properties are changed: `reported_date`, + * `type` + */ + update: (updated: T) => Promise; + } + + /** @internal */ + export const getDatasource = (ctx: DataContext): Datasource => { + return { + getByUuid: (uuid) => ctx.bind(v1.get)(byUuid(uuid)), + getByUuidWithLineage: (uuid) => ctx.bind(v1.getWithLineage)(byUuid(uuid)), + getUuidsPageByFreetext: ( + qualifier, + cursor = null, + limit = DEFAULT_IDS_PAGE_LIMIT + ) => ctx.bind(v1.getUuidsPage)(byFreetext(qualifier), cursor, limit), + getUuidsByFreetext: (qualifier) => ctx.bind(v1.getUuids)(byFreetext(qualifier)), + create: (input) => ctx.bind(v1.create)(input), + update: (updated) => ctx.bind(v1.update)(updated), + }; + }; } diff --git a/shared-libs/cht-datasource/src/target.ts b/shared-libs/cht-datasource/src/target.ts index 4026194bf79..48e1bc556f4 100644 --- a/shared-libs/cht-datasource/src/target.ts +++ b/shared-libs/cht-datasource/src/target.ts @@ -9,7 +9,7 @@ import { isIdQualifier, ReportingPeriodQualifier, UsernameQualifier, - IdQualifier + IdQualifier, and, byReportingPeriod, byContactIds, byContactId, byUsername } from './qualifier'; import * as Local from './local'; import * as Remote from './remote'; @@ -161,4 +161,93 @@ export namespace v1 { }; return curriedGen; }; + + /** + * Operations for working with targets. + */ + export interface Datasource { + /** + * Returns a target by identifier. + * @param id the identifier of the target to retrieve + * @returns the target or `null` if no target is found for the identifier + * @throws InvalidArgumentError if no identifier is provided + */ + getById: (id: string) => Promise>; + + /** + * Returns the target for the given username and reporting period. + * @param reportingPeriod the reporting period for the target + * @param contactId the contact identifier of the user for the target + * @param username the username (without the "org.couchdb.user:" prefix) of the user for the target + * @returns the target or `null` if no target is found + * @throws InvalidArgumentError if no reporting period, contact identifier, and/or username is provided + */ + getByReportingPeriodContactIdUsername: ( + reportingPeriod: string, + contactId: string, + username: string + ) => Promise>; + + /** + * Returns an array of targets for the provided page specifications. + * @param reportingPeriod the reporting period for the targets + * @param contactIds the contact identifiers for the targets + * @param cursor the token identifying which page to retrieve. A `null` value indicates the first page should be + * returned. Subsequent pages can be retrieved by providing the cursor returned with the previous page. + * @param limit the maximum number of targets to return. Default is 100. + * @returns a page of targets for the provided specifications + * @throws InvalidArgumentError if no reporting period is provided + * @throws InvalidArgumentError if no contact identifiers are provided + * @throws InvalidArgumentError if the provided cursor is not a valid page token or `null` + * @throws InvalidArgumentError if the provided limit is `<= 0` + * @see {@link getByReportingPeriodContactIds} which provides the same data, but without having to manually + * account for paging + */ + getPageByReportingPeriodContactIds: ( + reportingPeriod: string, + contactIds: string | [string, ...string[]], + cursor?: Nullable, + limit?: number | `${number}` + ) => Promise>; + + /** + * Returns a generator for fetching all targets in the reporting period for the given contact identifiers. + * @param reportingPeriod the reporting period for the targets + * @param contactIds the contact identifiers for the targets + * @returns a generator for fetching all targets with the given type + * @throws InvalidArgumentError if no reporting period is provided + * @throws InvalidArgumentError if no contact identifiers are provided + */ + getByReportingPeriodContactIds: ( + reportingPeriod: string, + contactIds: string | [string, ...string[]] + ) => AsyncGenerator; + } + + /** @internal */ + export const getDatasource = (ctx: DataContext): Datasource => { + return { + getById: (id) => ctx.bind(v1.get)(byId(id)), + getByReportingPeriodContactIdUsername: (reportingPeriod, contactId, username) => ctx.bind(v1.get)(and( + byReportingPeriod(reportingPeriod), + byContactId(contactId), + byUsername(username) + )), + getPageByReportingPeriodContactIds: ( + reportingPeriod, + contactIds, + cursor = null, + limit = DEFAULT_DOCS_PAGE_LIMIT + ) => ctx.bind(v1.getPage)( + and( + byReportingPeriod(reportingPeriod), + Array.isArray(contactIds) ? byContactIds(contactIds) : byContactId(contactIds) + ), cursor, limit + ), + getByReportingPeriodContactIds: (reportingPeriod, contactIds) => ctx.bind(v1.getAll)(and( + byReportingPeriod(reportingPeriod), + Array.isArray(contactIds) ? byContactIds(contactIds) : byContactId(contactIds) + )), + }; + }; } From b3934cc32141b260769fd0d6ae73d4d5ed94fedb Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Fri, 22 May 2026 21:18:22 -0500 Subject: [PATCH 2/3] Move unit tests for sub-datasource objects --- .../cht-datasource/test/contact.spec.ts | 232 +++++- shared-libs/cht-datasource/test/index.spec.ts | 774 ------------------ .../cht-datasource/test/person.spec.ts | 140 +++- shared-libs/cht-datasource/test/place.spec.ts | 129 ++- .../cht-datasource/test/report.spec.ts | 144 +++- .../cht-datasource/test/target.spec.ts | 149 +++- 6 files changed, 786 insertions(+), 782 deletions(-) diff --git a/shared-libs/cht-datasource/test/contact.spec.ts b/shared-libs/cht-datasource/test/contact.spec.ts index 42c8cfdbb02..0df190f445f 100644 --- a/shared-libs/cht-datasource/test/contact.spec.ts +++ b/shared-libs/cht-datasource/test/contact.spec.ts @@ -1,4 +1,4 @@ -import { DataContext } from '../src'; +import { DataContext, Page } from '../src'; import sinon, { SinonStub } from 'sinon'; import * as Context from '../src/libs/data-context'; import * as Local from '../src/local'; @@ -7,9 +7,11 @@ import * as Qualifier from '../src/qualifier'; import * as Contact from '../src/contact'; import { expect } from 'chai'; import * as Core from '../src/libs/core'; +import { fakeGenerator } from './utils'; describe('contact', () => { - const dataContext = { } as DataContext; + const dataContext = { bind: () => null } as DataContext; + let dataContextBind: SinonStub; let assertDataContext: SinonStub; let adapt: SinonStub; let isUuidQualifier: SinonStub; @@ -17,6 +19,7 @@ describe('contact', () => { let isFreetextQualifier: SinonStub; beforeEach(() => { + dataContextBind = sinon.stub(dataContext, 'bind'); assertDataContext = sinon.stub(Context, 'assertDataContext'); adapt = sinon.stub(Context, 'adapt'); isUuidQualifier = sinon.stub(Qualifier, 'isUuidQualifier'); @@ -415,5 +418,230 @@ describe('contact', () => { expect(isFreetextQualifier.calledOnceWithExactly(invalidQualifier)).to.be.true; }); }); + + describe('getDatasource', () => { + let contact: Contact.v1.Datasource; + + beforeEach(() => contact = Contact.v1.getDatasource(dataContext)); + + it('contains expected keys', () => { + expect(contact).to.have.all.keys( + [ + 'getByUuid', + 'getByUuidWithLineage', + 'getUuidsByTypeFreetext', + 'getUuidsPageByTypeFreetext', + 'getUuidsPageByFreetext', + 'getUuidsByFreetext', + 'getUuidsPageByType', + 'getUuidsByType', + ] + ); + }); + + it('getByUuid', async () => { + const expectedContact = {}; + const contactGet = sinon.stub().resolves(expectedContact); + dataContextBind.returns(contactGet); + const qualifier = { uuid: 'my-contact-uuid' }; + const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); + + const returnedContact = await contact.getByUuid(qualifier.uuid); + + expect(returnedContact).to.equal(expectedContact); + expect(dataContextBind.calledOnceWithExactly(Contact.v1.get)).to.be.true; + expect(contactGet.calledOnceWithExactly(qualifier)).to.be.true; + expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; + }); + + it('getByUuidWithLineage', async () => { + const expectedContact = {}; + const contactGet = sinon.stub().resolves(expectedContact); + dataContextBind.returns(contactGet); + const qualifier = { uuid: 'my-contact-uuid' }; + const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); + + const returnedContact = await contact.getByUuidWithLineage(qualifier.uuid); + + expect(returnedContact).to.equal(expectedContact); + expect(dataContextBind.calledOnceWithExactly(Contact.v1.getWithLineage)).to.be.true; + expect(contactGet.calledOnceWithExactly(qualifier)).to.be.true; + expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; + }); + + it('getUuidsPageByTypeFreetext uses default cursor and limit', async () => { + const expectedContactIds: Page = {data: [], cursor: null}; + const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); + dataContextBind.returns(contactGetIdsPage); + const freetext = 'abc'; + const contactType = 'person'; + const qualifier = { contactType, freetext }; + sinon.stub(Qualifier, 'and').returns(qualifier); + sinon.stub(Qualifier, 'byFreetext').returns({ freetext }); + sinon.stub(Qualifier, 'byContactType').returns({ contactType }); + + const returnedContactIds = await contact.getUuidsPageByTypeFreetext(freetext, contactType); + + expect(returnedContactIds).to.equal(expectedContactIds); + expect(contactGetIdsPage.calledOnceWithExactly(qualifier, null, 10000)).to.be.true; + }); + + it('getUuidsPageByTypeFreetext', async () => { + const expectedContactIds: Page = { data: [], cursor: null }; + const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); + dataContextBind.returns(contactGetIdsPage); + const freetext = 'abc'; + const contactType = 'person'; + const limit = 2; + const cursor = '1'; + const contactTypeQualifier = { contactType }; + const freetextQualifier = { freetext }; + const qualifier = { contactType, freetext }; + const andQualifier = sinon.stub(Qualifier, 'and').returns(qualifier); + const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(freetextQualifier); + const byContactType = sinon.stub(Qualifier, 'byContactType').returns(contactTypeQualifier); + + const returnedContactIds = await contact.getUuidsPageByTypeFreetext(freetext, contactType, cursor, limit); + + expect(returnedContactIds).to.equal(expectedContactIds); + expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuidsPage)).to.be.true; + expect( + contactGetIdsPage.calledOnceWithExactly(qualifier, cursor, limit) + ).to.be.true; + expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; + expect(byContactType.calledOnceWithExactly(contactType)).to.be.true; + expect(andQualifier.calledOnceWithExactly(freetextQualifier, contactTypeQualifier)).to.be.true; + }); + + it('getUuidsPageByType', async () => { + const expectedContactIds: Page = { data: [], cursor: null }; + const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); + dataContextBind.returns(contactGetIdsPage); + const contactType = 'person'; + const limit = 2; + const cursor = '1'; + const contactTypeQualifier = { contactType }; + const byContactType = sinon.stub(Qualifier, 'byContactType').returns(contactTypeQualifier); + const byFreetext = sinon.stub(Qualifier, 'byFreetext'); + + const returnedContactIds = await contact.getUuidsPageByType(contactType, cursor, limit); + + expect(returnedContactIds).to.equal(expectedContactIds); + expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuidsPage)).to.be.true; + expect( + contactGetIdsPage.calledOnceWithExactly(contactTypeQualifier, cursor, limit) + ).to.be.true; + expect(byContactType.calledOnceWithExactly(contactType)).to.be.true; + expect(byFreetext.notCalled).to.be.true; + }); + + it('getUuidsPageByType uses default cursor and limit', async () => { + const expectedContactIds: Page = {data: [], cursor: null}; + const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); + dataContextBind.returns(contactGetIdsPage); + const contactType = 'person'; + const contactTypeQualifier = { contactType }; + sinon.stub(Qualifier, 'byContactType').returns(contactTypeQualifier); + + const returnedContactIds = await contact.getUuidsPageByType(contactType); + + expect(returnedContactIds).to.equal(expectedContactIds); + expect(contactGetIdsPage.calledOnceWithExactly(contactTypeQualifier, null, 10000)).to.be.true; + }); + + it('getUuidsPageByFreetext', async () => { + const expectedContactIds: Page = { data: [], cursor: null }; + const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); + dataContextBind.returns(contactGetIdsPage); + const freetext = 'abc'; + const limit = 2; + const cursor = '1'; + const freetextQualifier = { freetext }; + const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(freetextQualifier); + const byContactType = sinon.stub(Qualifier, 'byContactType'); + + const returnedContactIds = await contact.getUuidsPageByFreetext(freetext, cursor, limit); + + expect(returnedContactIds).to.equal(expectedContactIds); + expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuidsPage)).to.be.true; + expect( + contactGetIdsPage.calledOnceWithExactly(freetextQualifier, cursor, limit) + ).to.be.true; + expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; + expect(byContactType.notCalled).to.be.true; + }); + + it('getUuidsPageByFreetext uses default cursor and limit', async () => { + const expectedContactIds: Page = {data: [], cursor: null}; + const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); + dataContextBind.returns(contactGetIdsPage); + const freetext = 'abc'; + const freetextQualifier = { freetext }; + sinon.stub(Qualifier, 'byFreetext').returns(freetextQualifier); + + const returnedContactIds = await contact.getUuidsPageByFreetext(freetext); + + expect(returnedContactIds).to.equal(expectedContactIds); + expect(contactGetIdsPage.calledOnceWithExactly(freetextQualifier, null, 10000)).to.be.true; + }); + + it('getUuidsByTypeFreetext', () => { + const mockAsyncGenerator = fakeGenerator(); + + const contactGetIds = sinon.stub().returns(mockAsyncGenerator); + dataContextBind.returns(contactGetIds); + const freetext = 'abc'; + const contactType = 'person'; + const contactTypeQualifier = { contactType }; + const freetextQualifier = { freetext }; + const qualifier = { contactType, freetext }; + const andQualifier = sinon.stub(Qualifier, 'and').returns(qualifier); + const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(freetextQualifier); + const byContactType = sinon.stub(Qualifier, 'byContactType').returns(contactTypeQualifier); + + const res = contact.getUuidsByTypeFreetext(freetext, contactType); + + expect(res).to.deep.equal(mockAsyncGenerator); + expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuids)).to.be.true; + expect(contactGetIds.calledOnceWithExactly(qualifier)).to.be.true; + expect(andQualifier.calledOnceWithExactly(freetextQualifier, contactTypeQualifier)).to.be.true; + expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; + expect(byContactType.calledOnceWithExactly(contactType)).to.be.true; + }); + + it('getUuidsByType', () => { + const mockAsyncGenerator = fakeGenerator(); + + const contactGetIds = sinon.stub().returns(mockAsyncGenerator); + dataContextBind.returns(contactGetIds); + const contactType = 'person'; + const contactTypeQualifier = { contactType }; + const byContactType = sinon.stub(Qualifier, 'byContactType').returns(contactTypeQualifier); + + const res = contact.getUuidsByType(contactType); + + expect(res).to.deep.equal(mockAsyncGenerator); + expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuids)).to.be.true; + expect(contactGetIds.calledOnceWithExactly(contactTypeQualifier)).to.be.true; + expect(byContactType.calledOnceWithExactly(contactType)).to.be.true; + }); + + it('getUuidsByFreetext', () => { + const mockAsyncGenerator = fakeGenerator(); + + const contactGetIds = sinon.stub().returns(mockAsyncGenerator); + dataContextBind.returns(contactGetIds); + const freetext = 'abc'; + const freetextQualifier = { freetext }; + const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(freetextQualifier); + + const res = contact.getUuidsByFreetext(freetext); + + expect(res).to.deep.equal(mockAsyncGenerator); + expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuids)).to.be.true; + expect(contactGetIds.calledOnceWithExactly(freetextQualifier)).to.be.true; + expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; + }); + }); }); }); diff --git a/shared-libs/cht-datasource/test/index.spec.ts b/shared-libs/cht-datasource/test/index.spec.ts index 426fddebc20..f2e86ec46d3 100644 --- a/shared-libs/cht-datasource/test/index.spec.ts +++ b/shared-libs/cht-datasource/test/index.spec.ts @@ -2,17 +2,8 @@ import { expect } from 'chai'; import * as Index from '../src'; import { DataContext } from '../src'; import { hasAnyPermission, hasPermissions } from '../src/auth'; -import * as Contact from '../src/contact'; -import * as Person from '../src/person'; -import * as Place from '../src/place'; -import * as Qualifier from '../src/qualifier'; -import * as Report from '../src/report'; -import * as Target from '../src/target'; import sinon, { SinonStub } from 'sinon'; import * as Context from '../src/libs/data-context'; -import { Page } from '../src/libs/core'; -import { fakeGenerator } from './utils'; -import { DOC_TYPES } from '@medic/constants'; describe('CHT Script API - getDatasource', () => { let dataContext: DataContext; @@ -68,770 +59,5 @@ describe('CHT Script API - getDatasource', () => { expect(dataContextBind.calledOnceWithExactly(hasAnyPermission)).to.be.true; expect(bound.calledOnceWithExactly([['can_edit']], ['chw'], undefined)).to.be.true; }); - - describe('place', () => { - let place: typeof v1.place; - - beforeEach(() => place = v1.place); - - it('contains expected keys', () => { - expect(place).to.have.all.keys(['getByType', 'getByUuid', 'getByUuidWithLineage', 'getPageByType', - 'create', 'update']); - }); - - it('getByUuid', async () => { - const expectedPlace = {}; - const placeGet = sinon.stub().resolves(expectedPlace); - dataContextBind.returns(placeGet); - const qualifier = { uuid: 'my-places-uuid' }; - const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); - - const returnedPlace = await place.getByUuid(qualifier.uuid); - - expect(returnedPlace).to.equal(expectedPlace); - expect(dataContextBind.calledOnceWithExactly(Place.v1.get)).to.be.true; - expect(placeGet.calledOnceWithExactly(qualifier)).to.be.true; - expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; - }); - - it('getByUuidWithLineage', async () => { - const expectedPlace = {}; - const placeGet = sinon.stub().resolves(expectedPlace); - dataContextBind.returns(placeGet); - const qualifier = { uuid: 'my-places-uuid' }; - const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); - - const returnedPlace = await place.getByUuidWithLineage(qualifier.uuid); - - expect(returnedPlace).to.equal(expectedPlace); - expect(dataContextBind.calledOnceWithExactly(Place.v1.getWithLineage)).to.be.true; - expect(placeGet.calledOnceWithExactly(qualifier)).to.be.true; - expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; - }); - - it('getPageByType', async () => { - const expectedPlaces: Page = { data: [], cursor: null }; - const placeGetPage = sinon.stub().resolves(expectedPlaces); - dataContextBind.returns(placeGetPage); - const placeType = 'place'; - const limit = 2; - const cursor = '1'; - const placeTypeQualifier = { contactType: placeType }; - const byContactType = sinon.stub(Qualifier, 'byContactType').returns(placeTypeQualifier); - - const returnedPlaces = await place.getPageByType(placeType, cursor, limit); - - expect(returnedPlaces).to.equal(expectedPlaces); - expect(dataContextBind.calledOnceWithExactly(Place.v1.getPage)).to.be.true; - expect(placeGetPage.calledOnceWithExactly(placeTypeQualifier, cursor, limit)).to.be.true; - expect(byContactType.calledOnceWithExactly(placeType)).to.be.true; - }); - - it('getPageByType uses default cursor and limit', async () => { - const expectedPlaces: Page = {data: [], cursor: null}; - const placeGetPage = sinon.stub().resolves(expectedPlaces); - dataContextBind.returns(placeGetPage); - const placeType = 'place'; - const placeTypeQualifier = { contactType: placeType }; - sinon.stub(Qualifier, 'byContactType').returns(placeTypeQualifier); - - const returnedPlaces = await place.getPageByType(placeType); - - expect(returnedPlaces).to.equal(expectedPlaces); - expect(placeGetPage.calledOnceWithExactly(placeTypeQualifier, null, 100)).to.be.true; - }); - - it('getByType', () => { - const mockAsyncGenerator = fakeGenerator(); - - const placeGetAll = sinon.stub().returns(mockAsyncGenerator); - dataContextBind.returns(placeGetAll); - const placeType = 'place'; - const placeTypeQualifier = { contactType: placeType }; - const byContactType = sinon.stub(Qualifier, 'byContactType').returns(placeTypeQualifier); - - const res = place.getByType(placeType); - - expect(res).to.deep.equal(mockAsyncGenerator); - expect(dataContextBind.calledOnceWithExactly(Place.v1.getAll)).to.be.true; - expect(placeGetAll.calledOnceWithExactly(placeTypeQualifier)).to.be.true; - expect(byContactType.calledOnceWithExactly(placeType)).to.be.true; - }); - - it('create', async () => { - const placeInput = { name: 'p1', type: 'place' }; - const expectedPlace = { - ...placeInput, - reported_date: 12312312 - }; - const placeCreate = sinon.stub().resolves(expectedPlace); - dataContextBind.returns(placeCreate); - - const returnedPlace = await place.create(placeInput); - - expect(returnedPlace).to.equal(expectedPlace); - expect(dataContextBind.calledOnceWithExactly(Place.v1.create)).to.be.true; - expect(placeCreate.calledOnceWithExactly(placeInput)).to.be.true; - }); - - it('update', async () => { - const placeInput = { name: 'p1', type: 'place', _id: '123', _rev: '1-abc' }; - const expectedPlace = { - ...placeInput, - reported_date: 12312312 - }; - const placeUpdate = sinon.stub().resolves(expectedPlace); - dataContextBind.returns(placeUpdate); - - const returnedPlace = await place.update(placeInput); - - expect(returnedPlace).to.equal(expectedPlace); - expect(dataContextBind.calledOnceWithExactly(Place.v1.update)).to.be.true; - expect(placeUpdate.calledOnceWithExactly(placeInput)).to.be.true; - }); - }); - - describe('person', () => { - let person: typeof v1.person; - - beforeEach(() => person = v1.person); - - it('contains expected keys', () => { - expect(person).to.have.all.keys([ - 'getByType', - 'getByUuid', - 'getByUuidWithLineage', - 'getPageByType', - 'create', - 'update' - ]); - }); - - it('getByUuid', async () => { - const expectedPerson = {}; - const personGet = sinon.stub().resolves(expectedPerson); - dataContextBind.returns(personGet); - const qualifier = { uuid: 'my-persons-uuid' }; - const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); - - const returnedPerson = await person.getByUuid(qualifier.uuid); - - expect(returnedPerson).to.equal(expectedPerson); - expect(dataContextBind.calledOnceWithExactly(Person.v1.get)).to.be.true; - expect(personGet.calledOnceWithExactly(qualifier)).to.be.true; - expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; - }); - - it('create', async () => { - const personInput = { name: 'apoorva', type: 'person', parent: 'p1' }; - const expectedPerson = { - ...personInput, - reported_date: 12312312 - }; - const personCreate = sinon.stub().resolves(expectedPerson); - dataContextBind.returns(personCreate); - - const returnedPerson = await person.create(personInput); - - expect(returnedPerson).to.equal(expectedPerson); - expect(dataContextBind.calledOnceWithExactly(Person.v1.create)).to.be.true; - expect(personCreate.calledOnceWithExactly(personInput)).to.be.true; - }); - - it('update', async () => { - const personInput = { - name: 'apoorva', - type: 'person', - parent: { _id: 'p1' }, - _id: '123', - _rev: '1-abc', - reported_date: 12312312 - }; - const expectedPerson = { - ...personInput, - _rev: '2-def', - }; - const personUpdate = sinon.stub().resolves(expectedPerson); - dataContextBind.returns(personUpdate); - - const returnedPlace = await person.update(personInput); - - expect(returnedPlace).to.equal(expectedPerson); - expect(dataContextBind.calledOnceWithExactly(Person.v1.update)).to.be.true; - expect(personUpdate.calledOnceWithExactly(personInput)).to.be.true; - }); - - it('getByUuidWithLineage', async () => { - const expectedPerson = {}; - const personGet = sinon.stub().resolves(expectedPerson); - dataContextBind.returns(personGet); - const qualifier = { uuid: 'my-persons-uuid' }; - const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); - - const returnedPerson = await person.getByUuidWithLineage(qualifier.uuid); - - expect(returnedPerson).to.equal(expectedPerson); - expect(dataContextBind.calledOnceWithExactly(Person.v1.getWithLineage)).to.be.true; - expect(personGet.calledOnceWithExactly(qualifier)).to.be.true; - expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; - }); - - it('getPageByType', async () => { - const expectedPeople: Page = { data: [], cursor: null }; - const personGetPage = sinon.stub().resolves(expectedPeople); - dataContextBind.returns(personGetPage); - const personType = 'person'; - const limit = 2; - const cursor = '1'; - const personTypeQualifier = { contactType: personType }; - const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); - - 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, cursor, limit)).to.be.true; - expect(byContactType.calledOnceWithExactly(personType)).to.be.true; - }); - - it('getPageByType uses default cursor and limit', async () => { - const expectedPeople: Page = {data: [], cursor: null}; - const personGetPage = sinon.stub().resolves(expectedPeople); - dataContextBind.returns(personGetPage); - const personType = 'person'; - const personTypeQualifier = { contactType: personType }; - sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); - - const returnedPeople = await person.getPageByType(personType); - - expect(returnedPeople).to.equal(expectedPeople); - expect(personGetPage.calledOnceWithExactly(personTypeQualifier, null, 100)).to.be.true; - }); - - it('getByType', () => { - const mockAsyncGenerator = fakeGenerator(); - - const personGetAll = sinon.stub().returns(mockAsyncGenerator); - dataContextBind.returns(personGetAll); - const personType = 'person'; - const personTypeQualifier = { contactType: personType }; - const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); - - const res = 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; - }); - }); - - describe('contact', () => { - let contact: typeof v1.contact; - - beforeEach(() => contact = v1.contact); - - it('contains expected keys', () => { - expect(contact).to.have.all.keys( - [ - 'getByUuid', - 'getByUuidWithLineage', - 'getUuidsByTypeFreetext', - 'getUuidsPageByTypeFreetext', - 'getUuidsPageByFreetext', - 'getUuidsByFreetext', - 'getUuidsPageByType', - 'getUuidsByType', - ] - ); - }); - - it('getByUuid', async () => { - const expectedContact = {}; - const contactGet = sinon.stub().resolves(expectedContact); - dataContextBind.returns(contactGet); - const qualifier = { uuid: 'my-contact-uuid' }; - const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); - - const returnedContact = await contact.getByUuid(qualifier.uuid); - - expect(returnedContact).to.equal(expectedContact); - expect(dataContextBind.calledOnceWithExactly(Contact.v1.get)).to.be.true; - expect(contactGet.calledOnceWithExactly(qualifier)).to.be.true; - expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; - }); - - it('getByUuidWithLineage', async () => { - const expectedContact = {}; - const contactGet = sinon.stub().resolves(expectedContact); - dataContextBind.returns(contactGet); - const qualifier = { uuid: 'my-contact-uuid' }; - const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); - - const returnedContact = await contact.getByUuidWithLineage(qualifier.uuid); - - expect(returnedContact).to.equal(expectedContact); - expect(dataContextBind.calledOnceWithExactly(Contact.v1.getWithLineage)).to.be.true; - expect(contactGet.calledOnceWithExactly(qualifier)).to.be.true; - expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; - }); - - it('getUuidsPageByTypeFreetext uses default cursor and limit', async () => { - const expectedContactIds: Page = {data: [], cursor: null}; - const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); - dataContextBind.returns(contactGetIdsPage); - const freetext = 'abc'; - const contactType = 'person'; - const qualifier = { contactType, freetext }; - sinon.stub(Qualifier, 'and').returns(qualifier); - sinon.stub(Qualifier, 'byFreetext').returns({ freetext }); - sinon.stub(Qualifier, 'byContactType').returns({ contactType }); - - const returnedContactIds = await contact.getUuidsPageByTypeFreetext(freetext, contactType); - - expect(returnedContactIds).to.equal(expectedContactIds); - expect(contactGetIdsPage.calledOnceWithExactly(qualifier, null, 10000)).to.be.true; - }); - - it('getUuidsPageByTypeFreetext', async () => { - const expectedContactIds: Page = { data: [], cursor: null }; - const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); - dataContextBind.returns(contactGetIdsPage); - const freetext = 'abc'; - const contactType = 'person'; - const limit = 2; - const cursor = '1'; - const contactTypeQualifier = { contactType }; - const freetextQualifier = { freetext }; - const qualifier = { contactType, freetext }; - const andQualifier = sinon.stub(Qualifier, 'and').returns(qualifier); - const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(freetextQualifier); - const byContactType = sinon.stub(Qualifier, 'byContactType').returns(contactTypeQualifier); - - const returnedContactIds = await contact.getUuidsPageByTypeFreetext(freetext, contactType, cursor, limit); - - expect(returnedContactIds).to.equal(expectedContactIds); - expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuidsPage)).to.be.true; - expect( - contactGetIdsPage.calledOnceWithExactly(qualifier, cursor, limit) - ).to.be.true; - expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; - expect(byContactType.calledOnceWithExactly(contactType)).to.be.true; - expect(andQualifier.calledOnceWithExactly(freetextQualifier, contactTypeQualifier)).to.be.true; - }); - - it('getUuidsPageByType', async () => { - const expectedContactIds: Page = { data: [], cursor: null }; - const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); - dataContextBind.returns(contactGetIdsPage); - const contactType = 'person'; - const limit = 2; - const cursor = '1'; - const contactTypeQualifier = { contactType }; - const byContactType = sinon.stub(Qualifier, 'byContactType').returns(contactTypeQualifier); - const byFreetext = sinon.stub(Qualifier, 'byFreetext'); - - const returnedContactIds = await contact.getUuidsPageByType(contactType, cursor, limit); - - expect(returnedContactIds).to.equal(expectedContactIds); - expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuidsPage)).to.be.true; - expect( - contactGetIdsPage.calledOnceWithExactly(contactTypeQualifier, cursor, limit) - ).to.be.true; - expect(byContactType.calledOnceWithExactly(contactType)).to.be.true; - expect(byFreetext.notCalled).to.be.true; - }); - - it('getUuidsPageByType uses default cursor and limit', async () => { - const expectedContactIds: Page = {data: [], cursor: null}; - const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); - dataContextBind.returns(contactGetIdsPage); - const contactType = 'person'; - const contactTypeQualifier = { contactType }; - sinon.stub(Qualifier, 'byContactType').returns(contactTypeQualifier); - - const returnedContactIds = await contact.getUuidsPageByType(contactType); - - expect(returnedContactIds).to.equal(expectedContactIds); - expect(contactGetIdsPage.calledOnceWithExactly(contactTypeQualifier, null, 10000)).to.be.true; - }); - - it('getUuidsPageByFreetext', async () => { - const expectedContactIds: Page = { data: [], cursor: null }; - const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); - dataContextBind.returns(contactGetIdsPage); - const freetext = 'abc'; - const limit = 2; - const cursor = '1'; - const freetextQualifier = { freetext }; - const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(freetextQualifier); - const byContactType = sinon.stub(Qualifier, 'byContactType'); - - const returnedContactIds = await contact.getUuidsPageByFreetext(freetext, cursor, limit); - - expect(returnedContactIds).to.equal(expectedContactIds); - expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuidsPage)).to.be.true; - expect( - contactGetIdsPage.calledOnceWithExactly(freetextQualifier, cursor, limit) - ).to.be.true; - expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; - expect(byContactType.notCalled).to.be.true; - }); - - it('getUuidsPageByFreetext uses default cursor and limit', async () => { - const expectedContactIds: Page = {data: [], cursor: null}; - const contactGetIdsPage = sinon.stub().resolves(expectedContactIds); - dataContextBind.returns(contactGetIdsPage); - const freetext = 'abc'; - const freetextQualifier = { freetext }; - sinon.stub(Qualifier, 'byFreetext').returns(freetextQualifier); - - const returnedContactIds = await contact.getUuidsPageByFreetext(freetext); - - expect(returnedContactIds).to.equal(expectedContactIds); - expect(contactGetIdsPage.calledOnceWithExactly(freetextQualifier, null, 10000)).to.be.true; - }); - - it('getUuidsByTypeFreetext', () => { - const mockAsyncGenerator = fakeGenerator(); - - const contactGetIds = sinon.stub().returns(mockAsyncGenerator); - dataContextBind.returns(contactGetIds); - const freetext = 'abc'; - const contactType = 'person'; - const contactTypeQualifier = { contactType }; - const freetextQualifier = { freetext }; - const qualifier = { contactType, freetext }; - const andQualifier = sinon.stub(Qualifier, 'and').returns(qualifier); - const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(freetextQualifier); - const byContactType = sinon.stub(Qualifier, 'byContactType').returns(contactTypeQualifier); - - const res = contact.getUuidsByTypeFreetext(freetext, contactType); - - expect(res).to.deep.equal(mockAsyncGenerator); - expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuids)).to.be.true; - expect(contactGetIds.calledOnceWithExactly(qualifier)).to.be.true; - expect(andQualifier.calledOnceWithExactly(freetextQualifier, contactTypeQualifier)).to.be.true; - expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; - expect(byContactType.calledOnceWithExactly(contactType)).to.be.true; - }); - - it('getUuidsByType', () => { - const mockAsyncGenerator = fakeGenerator(); - - const contactGetIds = sinon.stub().returns(mockAsyncGenerator); - dataContextBind.returns(contactGetIds); - const contactType = 'person'; - const contactTypeQualifier = { contactType }; - const byContactType = sinon.stub(Qualifier, 'byContactType').returns(contactTypeQualifier); - - const res = contact.getUuidsByType(contactType); - - expect(res).to.deep.equal(mockAsyncGenerator); - expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuids)).to.be.true; - expect(contactGetIds.calledOnceWithExactly(contactTypeQualifier)).to.be.true; - expect(byContactType.calledOnceWithExactly(contactType)).to.be.true; - }); - - it('getUuidsByFreetext', () => { - const mockAsyncGenerator = fakeGenerator(); - - const contactGetIds = sinon.stub().returns(mockAsyncGenerator); - dataContextBind.returns(contactGetIds); - const freetext = 'abc'; - const freetextQualifier = { freetext }; - const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(freetextQualifier); - - const res = contact.getUuidsByFreetext(freetext); - - expect(res).to.deep.equal(mockAsyncGenerator); - expect(dataContextBind.calledOnceWithExactly(Contact.v1.getUuids)).to.be.true; - expect(contactGetIds.calledOnceWithExactly(freetextQualifier)).to.be.true; - expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; - }); - }); - - describe('report', () => { - let report: typeof v1.report; - - beforeEach(() => report = v1.report); - - it('contains expected keys', () => { - expect(report).to.have.all.keys([ - 'getUuidsByFreetext', - 'getUuidsPageByFreetext', - 'getByUuid', - 'create', - 'update', - 'getByUuidWithLineage', - ]); - }); - - it('getByUuid', async () => { - const expectedReport = {}; - const reportGet = sinon.stub().resolves(expectedReport); - dataContextBind.returns(reportGet); - const qualifier = { uuid: 'my-report-uuid' }; - const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); - - const returnedReport = await report.getByUuid(qualifier.uuid); - - expect(returnedReport).to.equal(expectedReport); - expect(dataContextBind.calledOnceWithExactly(Report.v1.get)).to.be.true; - expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; - expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; - }); - - it('getByUuidWithLineage', async () => { - const expectedReport = {}; - const reportGet = sinon.stub().resolves(expectedReport); - dataContextBind.returns(reportGet); - const qualifier = { uuid: 'my-report-uuid' }; - const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); - - const returnedReport = await report.getByUuidWithLineage(qualifier.uuid); - - expect(returnedReport).to.equal(expectedReport); - expect(dataContextBind.calledOnceWithExactly(Report.v1.getWithLineage)).to.be.true; - expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; - expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; - }); - - it('getUuidsPageByFreetext', async () => { - const expectedReportIds: Page = { data: [], cursor: null }; - const reportGetIdsPage = sinon.stub().resolves(expectedReportIds); - dataContextBind.returns(reportGetIdsPage); - const freetext = 'abc'; - const limit = 2; - const cursor = '1'; - const qualifier = { freetext }; - const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(qualifier); - - const returnedContactIds = await report.getUuidsPageByFreetext(freetext, cursor, limit); - - expect(returnedContactIds).to.equal(expectedReportIds); - expect(dataContextBind.calledOnceWithExactly(Report.v1.getUuidsPage)).to.be.true; - expect( - reportGetIdsPage.calledOnceWithExactly(qualifier, cursor, limit) - ).to.be.true; - expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; - }); - - it('getUuidsPageByFreetext uses default cursor and limit', async () => { - const expectedReportIds: Page = {data: [], cursor: null}; - const reportGetIdsPage = sinon.stub().resolves(expectedReportIds); - dataContextBind.returns(reportGetIdsPage); - const freetext = 'abc'; - const qualifier = { freetext }; - sinon.stub(Qualifier, 'byFreetext').returns(qualifier); - - const returnedContactIds = await report.getUuidsPageByFreetext(freetext); - - expect(returnedContactIds).to.equal(expectedReportIds); - expect(reportGetIdsPage.calledOnceWithExactly(qualifier, null, 10000)).to.be.true; - }); - - it('getUuidsByFreetext', () => { - const mockAsyncGenerator = fakeGenerator(); - - const contactGetIds = sinon.stub().returns(mockAsyncGenerator); - dataContextBind.returns(contactGetIds); - const freetext = 'abc'; - const qualifier = { freetext }; - const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(qualifier); - - const res = report.getUuidsByFreetext(freetext); - - expect(res).to.deep.equal(mockAsyncGenerator); - expect(dataContextBind.calledOnceWithExactly(Report.v1.getUuids)).to.be.true; - expect(contactGetIds.calledOnceWithExactly(qualifier)).to.be.true; - expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; - }); - - it('create', async () => { - const reportInput = { form: 'apoorva', type: 'report', contact: 'c1' }; - const expectedReport = { - ...reportInput, - reported_date: 12312312 - }; - const reportCreate = sinon.stub().resolves(expectedReport); - dataContextBind.returns(reportCreate); - - const returnedReport = await report.create(reportInput); - - expect(returnedReport).to.equal(expectedReport); - expect(dataContextBind.calledOnceWithExactly(Report.v1.create)).to.be.true; - expect(reportCreate.calledOnceWithExactly(reportInput)).to.be.true; - }); - - it('update', async () => { - const reportInput = { - form: 'apoorva', - type: DOC_TYPES.DATA_RECORD, - contact: { _id: 'c1' }, - _id: '123', - _rev: '1-abc', - reported_date: 12312312, - fields: {} - }; - const expectedReport = { - ...reportInput, - _rev: '2-def', - }; - const reportUpdate = sinon.stub().resolves(expectedReport); - dataContextBind.returns(reportUpdate); - - const returnedPlace = await report.update(reportInput); - - expect(returnedPlace).to.equal(expectedReport); - expect(dataContextBind.calledOnceWithExactly(Report.v1.update)).to.be.true; - expect(reportUpdate.calledOnceWithExactly(reportInput)).to.be.true; - }); - }); - - describe('target', () => { - let target: typeof v1.target; - - beforeEach(() => target = v1.target); - - it('contains expected keys', () => { - expect(target).to.have.all.keys([ - 'getById', 'getByReportingPeriodContactIdUsername', - 'getPageByReportingPeriodContactIds', 'getByReportingPeriodContactIds' - ]); - }); - - it('getById', async () => { - const expectedTarget = {}; - const reportGet = sinon.stub().resolves(expectedTarget); - dataContextBind.returns(reportGet); - const qualifier = Qualifier.byId('my-target-uuid'); - - const returnedTarget = await target.getById(qualifier.id); - - expect(returnedTarget).to.equal(expectedTarget); - expect(dataContextBind.calledOnceWithExactly(Target.v1.get)).to.be.true; - expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; - }); - - it('getByReportingPeriodContactIdUsername', async () => { - const expectedTarget = {}; - const reportGet = sinon.stub().resolves(expectedTarget); - dataContextBind.returns(reportGet); - const qualifier = Qualifier.and( - Qualifier.byReportingPeriod('2020-01' ), - Qualifier.byContactId('my-contact-uuid'), - Qualifier.byUsername('my-username') - ); - - const returnedTarget = await target.getByReportingPeriodContactIdUsername( - qualifier.reportingPeriod, - qualifier.contactId, - qualifier.username - ); - - expect(returnedTarget).to.equal(expectedTarget); - expect(dataContextBind.calledOnceWithExactly(Target.v1.get)).to.be.true; - expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; - }); - - it('getPageByReportingPeriodContactIds uses default cursor and limit', async () => { - const expectedTarget = {}; - const reportGet = sinon.stub().resolves(expectedTarget); - dataContextBind.returns(reportGet); - const qualifier = Qualifier.and( - Qualifier.byReportingPeriod('2020-01'), - Qualifier.byContactIds(['my-first-contact-uuid', 'my-second-contact-uuid']) - ); - - const returnedTarget = await target.getPageByReportingPeriodContactIds( - qualifier.reportingPeriod, - qualifier.contactIds - ); - - expect(returnedTarget).to.equal(expectedTarget); - expect(reportGet.calledOnceWithExactly(qualifier, null, 100)).to.be.true; - }); - - it('getPageByReportingPeriodContactIds - multiple contact Ids', async () => { - const expectedTarget = {}; - const reportGet = sinon.stub().resolves(expectedTarget); - dataContextBind.returns(reportGet); - const qualifier = Qualifier.and( - Qualifier.byReportingPeriod('2020-01'), - Qualifier.byContactIds(['my-first-contact-uuid', 'my-second-contact-uuid']) - ); - - const returnedTarget = await target.getPageByReportingPeriodContactIds( - qualifier.reportingPeriod, - qualifier.contactIds, - '1', - 10 - ); - - expect(returnedTarget).to.equal(expectedTarget); - expect(dataContextBind.calledOnceWithExactly(Target.v1.getPage)).to.be.true; - expect(reportGet.calledOnceWithExactly(qualifier, '1', 10)).to.be.true; - }); - - it('getPageByReportingPeriodContactIds - since contact Id', async () => { - const expectedTarget = {}; - const reportGet = sinon.stub().resolves(expectedTarget); - dataContextBind.returns(reportGet); - const qualifier = Qualifier.and( - Qualifier.byReportingPeriod('2020-01'), - Qualifier.byContactId('my-first-contact-uuid') - ); - - const returnedTarget = await target.getPageByReportingPeriodContactIds( - qualifier.reportingPeriod, - qualifier.contactId, - '1', - 10 - ); - - expect(returnedTarget).to.equal(expectedTarget); - expect(dataContextBind.calledOnceWithExactly(Target.v1.getPage)).to.be.true; - expect(reportGet.calledOnceWithExactly(qualifier, '1', 10)).to.be.true; - }); - - it('getByReportingPeriodContactIds multiple contact Ids', () => { - const mockAsyncGenerator = fakeGenerator(); - const reportGet = sinon.stub().returns(mockAsyncGenerator); - dataContextBind.returns(reportGet); - const qualifier = Qualifier.and( - Qualifier.byReportingPeriod('2020-01'), - Qualifier.byContactIds(['my-first-contact-uuid', 'my-second-contact-uuid']) - ); - - const returnedTarget = target.getByReportingPeriodContactIds( - qualifier.reportingPeriod, - qualifier.contactIds - ); - - expect(returnedTarget).to.deep.equal(mockAsyncGenerator); - expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; - expect(dataContextBind.calledOnceWithExactly(Target.v1.getAll)).to.be.true; - }); - - it('getByReportingPeriodContactIds since contact Id', () => { - const mockAsyncGenerator = fakeGenerator(); - const reportGet = sinon.stub().returns(mockAsyncGenerator); - dataContextBind.returns(reportGet); - const qualifier = Qualifier.and( - Qualifier.byReportingPeriod('2020-01'), - Qualifier.byContactId('my-first-contact-uuid') - ); - - const returnedTarget = target.getByReportingPeriodContactIds( - qualifier.reportingPeriod, - qualifier.contactId - ); - - expect(returnedTarget).to.deep.equal(mockAsyncGenerator); - expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; - expect(dataContextBind.calledOnceWithExactly(Target.v1.getAll)).to.be.true; - }); - }); }); }); diff --git a/shared-libs/cht-datasource/test/person.spec.ts b/shared-libs/cht-datasource/test/person.spec.ts index e59bbfd6dfc..12bf3caa0f4 100644 --- a/shared-libs/cht-datasource/test/person.spec.ts +++ b/shared-libs/cht-datasource/test/person.spec.ts @@ -9,15 +9,18 @@ import sinon, { SinonStub } from 'sinon'; import { expect } from 'chai'; import { DataContext } from '../src'; import { fakeGenerator } from './utils'; +import { Page } from '../src'; describe('person', () => { - const dataContext = {} as DataContext; + const dataContext = { bind: () => null } as DataContext; + let dataContextBind: SinonStub; let assertDataContext: SinonStub; let adapt: SinonStub; let isUuidQualifier: SinonStub; let isContactTypeQualifier: SinonStub; beforeEach(() => { + dataContextBind = sinon.stub(dataContext, 'bind'); assertDataContext = sinon.stub(Context, 'assertDataContext'); adapt = sinon.stub(Context, 'adapt'); isUuidQualifier = sinon.stub(Qualifier, 'isUuidQualifier'); @@ -402,5 +405,140 @@ describe('person', () => { expect(updatePersonDoc.notCalled).to.be.true; }); }); + + describe('getDatasource', () => { + let person: Person.v1.Datasource; + + beforeEach(() => person = Person.v1.getDatasource(dataContext)); + + it('contains expected keys', () => { + expect(person).to.have.all.keys([ + 'getByType', + 'getByUuid', + 'getByUuidWithLineage', + 'getPageByType', + 'create', + 'update' + ]); + }); + + it('getByUuid', async () => { + const expectedPerson = {}; + const personGet = sinon.stub().resolves(expectedPerson); + dataContextBind.returns(personGet); + const qualifier = { uuid: 'my-persons-uuid' }; + const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); + + const returnedPerson = await person.getByUuid(qualifier.uuid); + + expect(returnedPerson).to.equal(expectedPerson); + expect(dataContextBind.calledOnceWithExactly(Person.v1.get)).to.be.true; + expect(personGet.calledOnceWithExactly(qualifier)).to.be.true; + expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; + }); + + it('create', async () => { + const personInput = { name: 'apoorva', type: 'person', parent: 'p1' }; + const expectedPerson = { + ...personInput, + reported_date: 12312312 + }; + const personCreate = sinon.stub().resolves(expectedPerson); + dataContextBind.returns(personCreate); + + const returnedPerson = await person.create(personInput); + + expect(returnedPerson).to.equal(expectedPerson); + expect(dataContextBind.calledOnceWithExactly(Person.v1.create)).to.be.true; + expect(personCreate.calledOnceWithExactly(personInput)).to.be.true; + }); + + it('update', async () => { + const personInput = { + name: 'apoorva', + type: 'person', + parent: { _id: 'p1' }, + _id: '123', + _rev: '1-abc', + reported_date: 12312312 + }; + const expectedPerson = { + ...personInput, + _rev: '2-def', + }; + const personUpdate = sinon.stub().resolves(expectedPerson); + dataContextBind.returns(personUpdate); + + const returnedPlace = await person.update(personInput); + + expect(returnedPlace).to.equal(expectedPerson); + expect(dataContextBind.calledOnceWithExactly(Person.v1.update)).to.be.true; + expect(personUpdate.calledOnceWithExactly(personInput)).to.be.true; + }); + + it('getByUuidWithLineage', async () => { + const expectedPerson = {}; + const personGet = sinon.stub().resolves(expectedPerson); + dataContextBind.returns(personGet); + const qualifier = { uuid: 'my-persons-uuid' }; + const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); + + const returnedPerson = await person.getByUuidWithLineage(qualifier.uuid); + + expect(returnedPerson).to.equal(expectedPerson); + expect(dataContextBind.calledOnceWithExactly(Person.v1.getWithLineage)).to.be.true; + expect(personGet.calledOnceWithExactly(qualifier)).to.be.true; + expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; + }); + + it('getPageByType', async () => { + const expectedPeople: Page = { data: [], cursor: null }; + const personGetPage = sinon.stub().resolves(expectedPeople); + dataContextBind.returns(personGetPage); + const personType = 'person'; + const limit = 2; + const cursor = '1'; + const personTypeQualifier = { contactType: personType }; + const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); + + 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, cursor, limit)).to.be.true; + expect(byContactType.calledOnceWithExactly(personType)).to.be.true; + }); + + it('getPageByType uses default cursor and limit', async () => { + const expectedPeople: Page = {data: [], cursor: null}; + const personGetPage = sinon.stub().resolves(expectedPeople); + dataContextBind.returns(personGetPage); + const personType = 'person'; + const personTypeQualifier = { contactType: personType }; + sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); + + const returnedPeople = await person.getPageByType(personType); + + expect(returnedPeople).to.equal(expectedPeople); + expect(personGetPage.calledOnceWithExactly(personTypeQualifier, null, 100)).to.be.true; + }); + + it('getByType', () => { + const mockAsyncGenerator = fakeGenerator(); + + const personGetAll = sinon.stub().returns(mockAsyncGenerator); + dataContextBind.returns(personGetAll); + const personType = 'person'; + const personTypeQualifier = { contactType: personType }; + const byContactType = sinon.stub(Qualifier, 'byContactType').returns(personTypeQualifier); + + const res = 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; + }); + }); }); }); diff --git a/shared-libs/cht-datasource/test/place.spec.ts b/shared-libs/cht-datasource/test/place.spec.ts index d115e28583b..15e87777eb8 100644 --- a/shared-libs/cht-datasource/test/place.spec.ts +++ b/shared-libs/cht-datasource/test/place.spec.ts @@ -6,18 +6,20 @@ import * as Input from '../src/input'; import * as Context from '../src/libs/data-context'; import sinon, { SinonStub } from 'sinon'; import { expect } from 'chai'; -import { DataContext } from '../src'; +import { DataContext, Page } from '../src'; import * as Core from '../src/libs/core'; import { fakeGenerator } from './utils'; describe('place', () => { - const dataContext = { } as DataContext; + const dataContext = { bind: () => null } as DataContext; + let dataContextBind: SinonStub; let assertDataContext: SinonStub; let adapt: SinonStub; let isUuidQualifier: SinonStub; let isContactTypeQualifier: SinonStub; beforeEach(() => { + dataContextBind = sinon.stub(dataContext, 'bind'); assertDataContext = sinon.stub(Context, 'assertDataContext'); adapt = sinon.stub(Context, 'adapt'); isUuidQualifier = sinon.stub(Qualifier, 'isUuidQualifier'); @@ -402,5 +404,128 @@ describe('place', () => { expect(updatePlaceDoc.notCalled).to.be.true; }); }); + + describe('getDatasource', () => { + let place: Place.v1.Datasource; + + beforeEach(() => place = Place.v1.getDatasource(dataContext)); + + it('contains expected keys', () => { + expect(place).to.have.all.keys([ + 'getByType', 'getByUuid', 'getByUuidWithLineage', 'getPageByType', 'create', 'update' + ]); + }); + + it('getByUuid', async () => { + const expectedPlace = {}; + const placeGet = sinon.stub().resolves(expectedPlace); + dataContextBind.returns(placeGet); + const qualifier = { uuid: 'my-places-uuid' }; + const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); + + const returnedPlace = await place.getByUuid(qualifier.uuid); + + expect(returnedPlace).to.equal(expectedPlace); + expect(dataContextBind.calledOnceWithExactly(Place.v1.get)).to.be.true; + expect(placeGet.calledOnceWithExactly(qualifier)).to.be.true; + expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; + }); + + it('getByUuidWithLineage', async () => { + const expectedPlace = {}; + const placeGet = sinon.stub().resolves(expectedPlace); + dataContextBind.returns(placeGet); + const qualifier = { uuid: 'my-places-uuid' }; + const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); + + const returnedPlace = await place.getByUuidWithLineage(qualifier.uuid); + + expect(returnedPlace).to.equal(expectedPlace); + expect(dataContextBind.calledOnceWithExactly(Place.v1.getWithLineage)).to.be.true; + expect(placeGet.calledOnceWithExactly(qualifier)).to.be.true; + expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; + }); + + it('getPageByType', async () => { + const expectedPlaces: Page = { data: [], cursor: null }; + const placeGetPage = sinon.stub().resolves(expectedPlaces); + dataContextBind.returns(placeGetPage); + const placeType = 'place'; + const limit = 2; + const cursor = '1'; + const placeTypeQualifier = { contactType: placeType }; + const byContactType = sinon.stub(Qualifier, 'byContactType').returns(placeTypeQualifier); + + const returnedPlaces = await place.getPageByType(placeType, cursor, limit); + + expect(returnedPlaces).to.equal(expectedPlaces); + expect(dataContextBind.calledOnceWithExactly(Place.v1.getPage)).to.be.true; + expect(placeGetPage.calledOnceWithExactly(placeTypeQualifier, cursor, limit)).to.be.true; + expect(byContactType.calledOnceWithExactly(placeType)).to.be.true; + }); + + it('getPageByType uses default cursor and limit', async () => { + const expectedPlaces: Page = {data: [], cursor: null}; + const placeGetPage = sinon.stub().resolves(expectedPlaces); + dataContextBind.returns(placeGetPage); + const placeType = 'place'; + const placeTypeQualifier = { contactType: placeType }; + sinon.stub(Qualifier, 'byContactType').returns(placeTypeQualifier); + + const returnedPlaces = await place.getPageByType(placeType); + + expect(returnedPlaces).to.equal(expectedPlaces); + expect(placeGetPage.calledOnceWithExactly(placeTypeQualifier, null, 100)).to.be.true; + }); + + it('getByType', () => { + const mockAsyncGenerator = fakeGenerator(); + + const placeGetAll = sinon.stub().returns(mockAsyncGenerator); + dataContextBind.returns(placeGetAll); + const placeType = 'place'; + const placeTypeQualifier = { contactType: placeType }; + const byContactType = sinon.stub(Qualifier, 'byContactType').returns(placeTypeQualifier); + + const res = place.getByType(placeType); + + expect(res).to.deep.equal(mockAsyncGenerator); + expect(dataContextBind.calledOnceWithExactly(Place.v1.getAll)).to.be.true; + expect(placeGetAll.calledOnceWithExactly(placeTypeQualifier)).to.be.true; + expect(byContactType.calledOnceWithExactly(placeType)).to.be.true; + }); + + it('create', async () => { + const placeInput = { name: 'p1', type: 'place' }; + const expectedPlace = { + ...placeInput, + reported_date: 12312312 + }; + const placeCreate = sinon.stub().resolves(expectedPlace); + dataContextBind.returns(placeCreate); + + const returnedPlace = await place.create(placeInput); + + expect(returnedPlace).to.equal(expectedPlace); + expect(dataContextBind.calledOnceWithExactly(Place.v1.create)).to.be.true; + expect(placeCreate.calledOnceWithExactly(placeInput)).to.be.true; + }); + + it('update', async () => { + const placeInput = { name: 'p1', type: 'place', _id: '123', _rev: '1-abc' }; + const expectedPlace = { + ...placeInput, + reported_date: 12312312 + }; + const placeUpdate = sinon.stub().resolves(expectedPlace); + dataContextBind.returns(placeUpdate); + + const returnedPlace = await place.update(placeInput); + + expect(returnedPlace).to.equal(expectedPlace); + expect(dataContextBind.calledOnceWithExactly(Place.v1.update)).to.be.true; + expect(placeUpdate.calledOnceWithExactly(placeInput)).to.be.true; + }); + }); }); }); diff --git a/shared-libs/cht-datasource/test/report.spec.ts b/shared-libs/cht-datasource/test/report.spec.ts index 3a1cc9d41be..cf9240cc002 100644 --- a/shared-libs/cht-datasource/test/report.spec.ts +++ b/shared-libs/cht-datasource/test/report.spec.ts @@ -1,4 +1,4 @@ -import { DataContext } from '../src'; +import { DataContext, Page } from '../src'; import sinon, { SinonStub } from 'sinon'; import * as Context from '../src/libs/data-context'; import * as Qualifier from '../src/qualifier'; @@ -12,13 +12,15 @@ import * as Input from '../src/input'; import { DOC_TYPES, CONTACT_TYPES } from '@medic/constants'; describe('report', () => { - const dataContext = { } as DataContext; + const dataContext = { bind: () => null } as DataContext; + let dataContextBind: SinonStub; let assertDataContext: SinonStub; let adapt: SinonStub; let isUuidQualifier: SinonStub; let isFreetextQualifier: SinonStub; beforeEach(() => { + dataContextBind = sinon.stub(dataContext, 'bind'); assertDataContext = sinon.stub(Context, 'assertDataContext'); adapt = sinon.stub(Context, 'adapt'); isUuidQualifier = sinon.stub(Qualifier, 'isUuidQualifier'); @@ -420,5 +422,143 @@ describe('report', () => { expect(updateReportDoc.calledOnceWithExactly(input)).to.be.true; }); }); + + describe('getDatasource', () => { + let report: Report.v1.Datasource; + + beforeEach(() => report = Report.v1.getDatasource(dataContext)); + + it('contains expected keys', () => { + expect(report).to.have.all.keys([ + 'getUuidsByFreetext', + 'getUuidsPageByFreetext', + 'getByUuid', + 'create', + 'update', + 'getByUuidWithLineage', + ]); + }); + + it('getByUuid', async () => { + const expectedReport = {}; + const reportGet = sinon.stub().resolves(expectedReport); + dataContextBind.returns(reportGet); + const qualifier = { uuid: 'my-report-uuid' }; + const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); + + const returnedReport = await report.getByUuid(qualifier.uuid); + + expect(returnedReport).to.equal(expectedReport); + expect(dataContextBind.calledOnceWithExactly(Report.v1.get)).to.be.true; + expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; + expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; + }); + + it('getByUuidWithLineage', async () => { + const expectedReport = {}; + const reportGet = sinon.stub().resolves(expectedReport); + dataContextBind.returns(reportGet); + const qualifier = { uuid: 'my-report-uuid' }; + const byUuid = sinon.stub(Qualifier, 'byUuid').returns(qualifier); + + const returnedReport = await report.getByUuidWithLineage(qualifier.uuid); + + expect(returnedReport).to.equal(expectedReport); + expect(dataContextBind.calledOnceWithExactly(Report.v1.getWithLineage)).to.be.true; + expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; + expect(byUuid.calledOnceWithExactly(qualifier.uuid)).to.be.true; + }); + + it('getUuidsPageByFreetext', async () => { + const expectedReportIds: Page = { data: [], cursor: null }; + const reportGetIdsPage = sinon.stub().resolves(expectedReportIds); + dataContextBind.returns(reportGetIdsPage); + const freetext = 'abc'; + const limit = 2; + const cursor = '1'; + const qualifier = { freetext }; + const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(qualifier); + + const returnedContactIds = await report.getUuidsPageByFreetext(freetext, cursor, limit); + + expect(returnedContactIds).to.equal(expectedReportIds); + expect(dataContextBind.calledOnceWithExactly(Report.v1.getUuidsPage)).to.be.true; + expect( + reportGetIdsPage.calledOnceWithExactly(qualifier, cursor, limit) + ).to.be.true; + expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; + }); + + it('getUuidsPageByFreetext uses default cursor and limit', async () => { + const expectedReportIds: Page = {data: [], cursor: null}; + const reportGetIdsPage = sinon.stub().resolves(expectedReportIds); + dataContextBind.returns(reportGetIdsPage); + const freetext = 'abc'; + const qualifier = { freetext }; + sinon.stub(Qualifier, 'byFreetext').returns(qualifier); + + const returnedContactIds = await report.getUuidsPageByFreetext(freetext); + + expect(returnedContactIds).to.equal(expectedReportIds); + expect(reportGetIdsPage.calledOnceWithExactly(qualifier, null, 10000)).to.be.true; + }); + + it('getUuidsByFreetext', () => { + const mockAsyncGenerator = fakeGenerator(); + + const contactGetIds = sinon.stub().returns(mockAsyncGenerator); + dataContextBind.returns(contactGetIds); + const freetext = 'abc'; + const qualifier = { freetext }; + const byFreetext = sinon.stub(Qualifier, 'byFreetext').returns(qualifier); + + const res = report.getUuidsByFreetext(freetext); + + expect(res).to.deep.equal(mockAsyncGenerator); + expect(dataContextBind.calledOnceWithExactly(Report.v1.getUuids)).to.be.true; + expect(contactGetIds.calledOnceWithExactly(qualifier)).to.be.true; + expect(byFreetext.calledOnceWithExactly(freetext)).to.be.true; + }); + + it('create', async () => { + const reportInput = { form: 'apoorva', type: 'report', contact: 'c1' }; + const expectedReport = { + ...reportInput, + reported_date: 12312312 + }; + const reportCreate = sinon.stub().resolves(expectedReport); + dataContextBind.returns(reportCreate); + + const returnedReport = await report.create(reportInput); + + expect(returnedReport).to.equal(expectedReport); + expect(dataContextBind.calledOnceWithExactly(Report.v1.create)).to.be.true; + expect(reportCreate.calledOnceWithExactly(reportInput)).to.be.true; + }); + + it('update', async () => { + const reportInput = { + form: 'apoorva', + type: DOC_TYPES.DATA_RECORD, + contact: { _id: 'c1' }, + _id: '123', + _rev: '1-abc', + reported_date: 12312312, + fields: {} + }; + const expectedReport = { + ...reportInput, + _rev: '2-def', + }; + const reportUpdate = sinon.stub().resolves(expectedReport); + dataContextBind.returns(reportUpdate); + + const returnedPlace = await report.update(reportInput); + + expect(returnedPlace).to.equal(expectedReport); + expect(dataContextBind.calledOnceWithExactly(Report.v1.update)).to.be.true; + expect(reportUpdate.calledOnceWithExactly(reportInput)).to.be.true; + }); + }); }); }); diff --git a/shared-libs/cht-datasource/test/target.spec.ts b/shared-libs/cht-datasource/test/target.spec.ts index 4648d26168e..c76efeb1712 100644 --- a/shared-libs/cht-datasource/test/target.spec.ts +++ b/shared-libs/cht-datasource/test/target.spec.ts @@ -12,11 +12,13 @@ import { fakeGenerator } from './utils'; const { PREFIXES } = require('@medic/constants'); describe('target', () => { - const dataContext = { } as DataContext; + const dataContext = { bind: () => null } as DataContext; + let dataContextBind: SinonStub; let assertDataContext: SinonStub; let adapt: SinonStub; beforeEach(() => { + dataContextBind = sinon.stub(dataContext, 'bind'); assertDataContext = sinon.stub(Context, 'assertDataContext'); adapt = sinon.stub(Context, 'adapt'); }); @@ -363,5 +365,150 @@ describe('target', () => { }); }); }); + + describe('getDatasource', () => { + let target: Target.v1.Datasource; + + beforeEach(() => target = Target.v1.getDatasource(dataContext)); + + it('contains expected keys', () => { + expect(target).to.have.all.keys([ + 'getById', 'getByReportingPeriodContactIdUsername', + 'getPageByReportingPeriodContactIds', 'getByReportingPeriodContactIds' + ]); + }); + + it('getById', async () => { + const expectedTarget = {}; + const reportGet = sinon.stub().resolves(expectedTarget); + dataContextBind.returns(reportGet); + const qualifier = Qualifier.byId('my-target-uuid'); + + const returnedTarget = await target.getById(qualifier.id); + + expect(returnedTarget).to.equal(expectedTarget); + expect(dataContextBind.calledOnceWithExactly(Target.v1.get)).to.be.true; + expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; + }); + + it('getByReportingPeriodContactIdUsername', async () => { + const expectedTarget = {}; + const reportGet = sinon.stub().resolves(expectedTarget); + dataContextBind.returns(reportGet); + const qualifier = Qualifier.and( + Qualifier.byReportingPeriod('2020-01' ), + Qualifier.byContactId('my-contact-uuid'), + Qualifier.byUsername('my-username') + ); + + const returnedTarget = await target.getByReportingPeriodContactIdUsername( + qualifier.reportingPeriod, + qualifier.contactId, + qualifier.username + ); + + expect(returnedTarget).to.equal(expectedTarget); + expect(dataContextBind.calledOnceWithExactly(Target.v1.get)).to.be.true; + expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; + }); + + it('getPageByReportingPeriodContactIds uses default cursor and limit', async () => { + const expectedTarget = {}; + const reportGet = sinon.stub().resolves(expectedTarget); + dataContextBind.returns(reportGet); + const qualifier = Qualifier.and( + Qualifier.byReportingPeriod('2020-01'), + Qualifier.byContactIds(['my-first-contact-uuid', 'my-second-contact-uuid']) + ); + + const returnedTarget = await target.getPageByReportingPeriodContactIds( + qualifier.reportingPeriod, + qualifier.contactIds + ); + + expect(returnedTarget).to.equal(expectedTarget); + expect(reportGet.calledOnceWithExactly(qualifier, null, 100)).to.be.true; + }); + + it('getPageByReportingPeriodContactIds - multiple contact Ids', async () => { + const expectedTarget = {}; + const reportGet = sinon.stub().resolves(expectedTarget); + dataContextBind.returns(reportGet); + const qualifier = Qualifier.and( + Qualifier.byReportingPeriod('2020-01'), + Qualifier.byContactIds(['my-first-contact-uuid', 'my-second-contact-uuid']) + ); + + const returnedTarget = await target.getPageByReportingPeriodContactIds( + qualifier.reportingPeriod, + qualifier.contactIds, + '1', + 10 + ); + + expect(returnedTarget).to.equal(expectedTarget); + expect(dataContextBind.calledOnceWithExactly(Target.v1.getPage)).to.be.true; + expect(reportGet.calledOnceWithExactly(qualifier, '1', 10)).to.be.true; + }); + + it('getPageByReportingPeriodContactIds - since contact Id', async () => { + const expectedTarget = {}; + const reportGet = sinon.stub().resolves(expectedTarget); + dataContextBind.returns(reportGet); + const qualifier = Qualifier.and( + Qualifier.byReportingPeriod('2020-01'), + Qualifier.byContactId('my-first-contact-uuid') + ); + + const returnedTarget = await target.getPageByReportingPeriodContactIds( + qualifier.reportingPeriod, + qualifier.contactId, + '1', + 10 + ); + + expect(returnedTarget).to.equal(expectedTarget); + expect(dataContextBind.calledOnceWithExactly(Target.v1.getPage)).to.be.true; + expect(reportGet.calledOnceWithExactly(qualifier, '1', 10)).to.be.true; + }); + + it('getByReportingPeriodContactIds multiple contact Ids', () => { + const mockAsyncGenerator = fakeGenerator(); + const reportGet = sinon.stub().returns(mockAsyncGenerator); + dataContextBind.returns(reportGet); + const qualifier = Qualifier.and( + Qualifier.byReportingPeriod('2020-01'), + Qualifier.byContactIds(['my-first-contact-uuid', 'my-second-contact-uuid']) + ); + + const returnedTarget = target.getByReportingPeriodContactIds( + qualifier.reportingPeriod, + qualifier.contactIds + ); + + expect(returnedTarget).to.deep.equal(mockAsyncGenerator); + expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; + expect(dataContextBind.calledOnceWithExactly(Target.v1.getAll)).to.be.true; + }); + + it('getByReportingPeriodContactIds since contact Id', () => { + const mockAsyncGenerator = fakeGenerator(); + const reportGet = sinon.stub().returns(mockAsyncGenerator); + dataContextBind.returns(reportGet); + const qualifier = Qualifier.and( + Qualifier.byReportingPeriod('2020-01'), + Qualifier.byContactId('my-first-contact-uuid') + ); + + const returnedTarget = target.getByReportingPeriodContactIds( + qualifier.reportingPeriod, + qualifier.contactId + ); + + expect(returnedTarget).to.deep.equal(mockAsyncGenerator); + expect(reportGet.calledOnceWithExactly(qualifier)).to.be.true; + expect(dataContextBind.calledOnceWithExactly(Target.v1.getAll)).to.be.true; + }); + }); }); }); From 97dcc400ea8933626e143e36dc3dc0389c6bfb5e Mon Sep 17 00:00:00 2001 From: Joshua Kuestersteffen Date: Fri, 22 May 2026 21:49:16 -0500 Subject: [PATCH 3/3] Clean up docs and error on warning --- scripts/build/generate-openapi.js | 6 +++++- shared-libs/cht-datasource/package.json | 2 +- shared-libs/cht-datasource/src/index.ts | 8 ++++++-- shared-libs/cht-datasource/src/libs/core.ts | 15 ++++++++++++--- .../cht-datasource/src/local/libs/data-context.ts | 2 +- shared-libs/cht-datasource/src/qualifier.ts | 7 ++++++- 6 files changed, 31 insertions(+), 9 deletions(-) diff --git a/scripts/build/generate-openapi.js b/scripts/build/generate-openapi.js index 09024d0d8d1..b31b6d20b5e 100644 --- a/scripts/build/generate-openapi.js +++ b/scripts/build/generate-openapi.js @@ -18,6 +18,10 @@ const TYPE_SOURCES = [ 'target.ts', 'input.ts', ].map(file => path.join(DATASOURCE_DIR, file)); +const SCHEMAS_TO_SKIP = { + '*': undefined, + 'v1.Datasource': undefined, // Imperative interface not referenced for REST apis +}; const TSJ_OPTIONS = { tsconfig: TSCONFIG, @@ -136,7 +140,7 @@ const generateTsSchemas = () => { .map(path => ({ ...TSJ_OPTIONS, path })) .map(opts => tsj.createGenerator(opts)) .map(generator => generator.createSchema()) - .map(({ definitions }) => ({ ...definitions, '*': undefined })) + .map(({ definitions }) => ({ ...definitions, ...SCHEMAS_TO_SKIP })) .forEach((definitions) => Object.assign(schemas, definitions)); return transformSchema(schemas); }; diff --git a/shared-libs/cht-datasource/package.json b/shared-libs/cht-datasource/package.json index 8f32cf07fe0..11cae47c888 100644 --- a/shared-libs/cht-datasource/package.json +++ b/shared-libs/cht-datasource/package.json @@ -11,7 +11,7 @@ "build": "tsc -p tsconfig.build.json", "build-watch": "tsc --watch -p tsconfig.build.json", "test": "nyc --nycrcPath='../nyc.config.js' mocha \"test/**/*\"", - "gen-docs": "typedoc ./src/index.ts --readme none --excludeInternal" + "gen-docs": "typedoc ./src/index.ts --readme none --excludeInternal --treatWarningsAsErrors" }, "author": "", "license": "Apache-2.0", diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index 65607ceadc0..85f1d29aa51 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -15,11 +15,15 @@ import * as Place from './place'; import * as Report from './report'; import * as Target from './target'; -export { Nullable, NonEmptyArray, Page } from './libs/core'; -export { DataContext } from './libs/data-context'; +export { + Nullable, NonEmptyArray, Page, DataObject, DataValue, DataArray, DataPrimitive, NormalizedParent +} from './libs/core'; +export { Doc } from './libs/doc'; +export { DataContext, SettingsService } from './libs/data-context'; export { getLocalDataContext } from './local'; export { getRemoteDataContext } from './remote'; export { InvalidArgumentError, ResourceNotFoundError } from './libs/error'; +export { SourceDatabases } from './local/libs/data-context'; export * as Contact from './contact'; export * as Person from './person'; export * as Place from './place'; diff --git a/shared-libs/cht-datasource/src/libs/core.ts b/shared-libs/cht-datasource/src/libs/core.ts index dd22604b310..188fd72692c 100644 --- a/shared-libs/cht-datasource/src/libs/core.ts +++ b/shared-libs/cht-datasource/src/libs/core.ts @@ -34,8 +34,14 @@ export const isNonEmptyArray = (value: T[]): value is NonEmptyArray => !!v /** @internal */ export const getLastElement = (array: NonEmptyArray): T => array[array.length - 1]; -type DataValue = DataPrimitive | DataArray | DataObject; -type DataPrimitive = string | number | boolean | Date | null | undefined; +/** + * A serializable value. + */ +export type DataValue = DataPrimitive | DataArray | DataObject; +/** + * A privative value. + */ +export type DataPrimitive = string | number | boolean | Date | null | undefined; const isDataPrimitive = (value: unknown): value is DataPrimitive => { return value === null @@ -46,7 +52,10 @@ const isDataPrimitive = (value: unknown): value is DataPrimitive => { || value instanceof Date; }; -type DataArray = readonly DataValue[]; +/** + * A serializable array. + */ +export type DataArray = readonly DataValue[]; const isDataArray = (value: unknown): value is DataArray => { return Array.isArray(value) && value.every(v => isDataPrimitive(v) || isDataArray(v) || isDataObject(v)); diff --git a/shared-libs/cht-datasource/src/local/libs/data-context.ts b/shared-libs/cht-datasource/src/local/libs/data-context.ts index 391107102ed..b4be849e46d 100644 --- a/shared-libs/cht-datasource/src/local/libs/data-context.ts +++ b/shared-libs/cht-datasource/src/local/libs/data-context.ts @@ -5,7 +5,7 @@ import { assertSettingsService, DataContext, SettingsService } from '../../libs/ export type { SettingsService } from '../../libs/data-context'; /** - * {@link PouchDB.Database}s to be used as the local data source. + * `PouchDB.Database`s to be used as the local data source. */ export type SourceDatabases = Readonly<{ medic: PouchDB.Database }>; diff --git a/shared-libs/cht-datasource/src/qualifier.ts b/shared-libs/cht-datasource/src/qualifier.ts index e8b2f433264..161c82e8105 100644 --- a/shared-libs/cht-datasource/src/qualifier.ts +++ b/shared-libs/cht-datasource/src/qualifier.ts @@ -278,7 +278,12 @@ export const byContactIds = (contactIds: [string, ...string[]]): ContactIdsQuali }; // https://stackoverflow.com/a/50375286 -type UnionToIntersection = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +/** + * The intersection of the specified types. + */ +export type UnionToIntersection = ( + U extends unknown ? (k: U) => void : never +) extends (k: infer I) => void ? I : never; /** * Combines multiple qualifiers into a single object.