diff --git a/api/src/controllers/contact.js b/api/src/controllers/contact.js new file mode 100644 index 00000000000..bee429db3cd --- /dev/null +++ b/api/src/controllers/contact.js @@ -0,0 +1,33 @@ +const auth = require('../auth'); +const { Contact, Qualifier} = require('@medic/cht-datasource'); +const ctx = require('../services/data-context'); +const serverUtils = require('../server-utils'); + +const getContact = ({ with_lineage }) => ctx.bind( + with_lineage === 'true' + ? Contact.v1.getWithLineage + : Contact.v1.get +); + +const checkUserPermissions = async (req) => { + const userCtx = await auth.getUserCtx(req); + if (!auth.isOnlineOnly(userCtx) || !auth.hasAllPermissions(userCtx, 'can_view_contacts')) { + return Promise.reject({ code: 403, message: 'Insufficient privileges' }); + } +}; + +module.exports = { + v1: { + get: serverUtils.doOrError(async (req, res) => { + await checkUserPermissions(req); + const { uuid } = req.params; + const contact = await getContact(req.query)(Qualifier.byUuid(uuid)); + + if (!contact) { + return serverUtils.error({ status: 404, message: 'Contact not found' }, req, res); + } + + return res.json(contact); + }) + } +}; diff --git a/api/src/routing.js b/api/src/routing.js index f027f1ddb03..613d286e45e 100644 --- a/api/src/routing.js +++ b/api/src/routing.js @@ -37,6 +37,7 @@ const exportData = require('./controllers/export-data'); const records = require('./controllers/records'); const forms = require('./controllers/forms'); const users = require('./controllers/users'); +const contact = require('./controllers/contact'); const person = require('./controllers/person'); const place = require('./controllers/place'); const { people, places } = require('@medic/contacts')(config, db, dataContext); @@ -485,6 +486,8 @@ app.postJson('/api/v1/people', function(req, res) { app.get('/api/v1/person', person.v1.getAll); app.get('/api/v1/person/:uuid', person.v1.get); +app.get('/api/v1/contact/:uuid', contact.v1.get); + app.postJson('/api/v1/bulk-delete', bulkDocs.bulkDelete); // offline users are not allowed to hydrate documents via the hydrate API diff --git a/shared-libs/cht-datasource/src/contact.ts b/shared-libs/cht-datasource/src/contact.ts new file mode 100644 index 00000000000..c3cdbca645b --- /dev/null +++ b/shared-libs/cht-datasource/src/contact.ts @@ -0,0 +1,125 @@ +import { Doc } from './libs/doc'; +import { DataObject, Identifiable, isDataObject, isIdentifiable } from './libs/core'; +import { isUuidQualifier, UuidQualifier } from './qualifier'; +import { adapt, assertDataContext, DataContext } from './libs/data-context'; +import { LocalDataContext } from './local/libs/data-context'; +import { RemoteDataContext } from './remote/libs/data-context'; +import { InvalidArgumentError } from './libs/error'; +import * as Local from './local'; +import * as Remote from './remote'; + +/** */ +export namespace v1 { + /** @internal */ + export interface NormalizedParent extends DataObject, Identifiable { + readonly parent?: NormalizedParent; + } + + /** @internal */ + export const isNormalizedParent = (value: unknown): value is NormalizedParent => { + return isDataObject(value) && isIdentifiable(value) && (!value.parent || isNormalizedParent(value.parent)); + }; + + /** + * Immutable data about a Contact. + */ + export interface Contact extends Doc, NormalizedParent { + readonly contact_type?: string; + readonly name?: string; + readonly reported_date?: Date; + readonly type: string; + } + + /** + * Immutable data about a contact, including the full records of the parent's lineage. + */ + export interface ContactWithLineage extends Contact { + readonly parent?: ContactWithLineage | NormalizedParent; + } + + const assertContactQualifier: (qualifier: unknown) => asserts qualifier is UuidQualifier = (qualifier: unknown) => { + if (!isUuidQualifier(qualifier)) { + throw new InvalidArgumentError(`Invalid identifier [${JSON.stringify(qualifier)}].`); + } + }; + + const getContact = ( + localFn: (c: LocalDataContext) => (qualifier: UuidQualifier) => Promise, + remoteFn: (c: RemoteDataContext) => (qualifier: UuidQualifier) => Promise + ) => (context: DataContext) => { + assertDataContext(context); + const fn = adapt(context, localFn, remoteFn); + return async (qualifier: UuidQualifier): Promise => { + assertContactQualifier(qualifier); + return fn(qualifier); + }; + }; + + /** + * Returns a function for retrieving a contact from the given data context. + * @param context the current data context + * @returns a function for retrieving a contact + * @throws Error if a data context is not provided + */ + /** + * Returns a contact for the given qualifier. + * @param qualifier identifier for the contact to retrieve + * @returns the contact or `null` if no contact is found for the qualifier + * @throws Error if the qualifier is invalid + */ + export const get = getContact(Local.Contact.v1.get, Remote.Contact.v1.get); + + /** + * Returns a function for retrieving a contact from the given data context with the contact's parent lineage. + * @param context the current data context + * @returns a function for retrieving a contact with the contact's parent lineage + * @throws Error if a data context is not provided + */ + /** + * Returns a contact for the given qualifier with the contact's parent lineage. + * @param qualifier identifier for the contact to retrieve + * @returns the contact or `null` if no contact is found for the qualifier + * @throws Error if the qualifier is invalid + */ + export const getWithLineage = getContact(Local.Contact.v1.getWithLineage, Remote.Contact.v1.getWithLineage); + + // New REST api: /api/v1/contact/id + /** + * Returns a function for retrieving a paged array of contact identifiers from the given data context. + * @param context the current data context + * @returns a function for retrieving a paged array of contact identifiers + * @throws Error if a data context is not provided + * @see {@link getIdsAll} which provides the same data, but without having to manually account for paging + */ + /** + * Returns an array of contact identifiers for the provided page specifications. + * @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 contact identifiers for the provided specification + * @throws Error if no qualifier is provided or if the qualifier is invalid + * @throws Error if the provided `limit` value is `<=0` + * @throws Error if the provided cursor is not a valid page token or `null` + */ + // const getIdsPage = (context: DataContext) => ( + // qualifier: ContactTypeQualifier | FreetextQualifier, + // cursor: Nullable, limit: number + // ) => Promise>; + + /** + * Returns a function for getting a generator that fetches contact identifiers from the given data context. + * @param context the current data context + * @returns a function for getting a generator that fetches contact identifiers + * @throws Error if a data context is not provided + */ + /** + * Returns a generator for fetching all contact identifiers that match the given qualifier + * @param qualifier the limiter defining which identifiers to return + * @returns a generator for fetching all contact identifiers that match the given qualifier + * @throws Error if no qualifier is provided or if the qualifier is invalid + */ + // const getIdsAll = (context: DataContext) => ( + // qualifier: ContactTypeQualifier | FreetextQualifier + // ) => AsyncGenerator; +} diff --git a/shared-libs/cht-datasource/src/index.ts b/shared-libs/cht-datasource/src/index.ts index 394b23e9bae..46a51f9bfc7 100644 --- a/shared-libs/cht-datasource/src/index.ts +++ b/shared-libs/cht-datasource/src/index.ts @@ -29,6 +29,7 @@ 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'; @@ -38,6 +39,7 @@ export { DataContext } from './libs/data-context'; export { getLocalDataContext } from './local'; export { getRemoteDataContext } from './remote'; export { InvalidArgumentError } from './libs/error'; +export * as Contact from './contact'; export * as Person from './person'; export * as Place from './place'; export * as Qualifier from './qualifier'; @@ -54,6 +56,13 @@ export const getDatasource = (ctx: DataContext) => { v1: { hasPermissions, hasAnyPermission, + contact: { + /** TODO */ + getByUuid: (uuid: string) => ctx.bind(Contact.v1.get)(Qualifier.byUuid(uuid)), + + /** TODO */ + getByUuidWithLineage: (uuid: string) => ctx.bind(Contact.v1.getWithLineage)(Qualifier.byUuid(uuid)), + }, place: { /** * Returns a place by its UUID. @@ -143,7 +152,7 @@ export const getDatasource = (ctx: DataContext) => { * @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)), - } - } + }, + }, }; }; diff --git a/shared-libs/cht-datasource/src/libs/contact.ts b/shared-libs/cht-datasource/src/libs/contact.ts deleted file mode 100644 index 63c0f505b93..00000000000 --- a/shared-libs/cht-datasource/src/libs/contact.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Doc } from './doc'; -import { DataObject, Identifiable, isDataObject, isIdentifiable } from './core'; - -/** @internal */ -export interface NormalizedParent extends DataObject, Identifiable { - readonly parent?: NormalizedParent; -} - -/** @internal */ -export const isNormalizedParent = (value: unknown): value is NormalizedParent => { - return isDataObject(value) - && isIdentifiable(value) - && (!value.parent || isNormalizedParent(value.parent)); -}; - -/** @internal */ -export interface Contact extends Doc, NormalizedParent { - readonly contact_type?: string; - readonly name?: string; - readonly reported_date?: Date; - readonly type: string; -} diff --git a/shared-libs/cht-datasource/src/libs/data-context.ts b/shared-libs/cht-datasource/src/libs/data-context.ts index d58c017c780..51fa0d65293 100644 --- a/shared-libs/cht-datasource/src/libs/data-context.ts +++ b/shared-libs/cht-datasource/src/libs/data-context.ts @@ -13,7 +13,7 @@ export interface DataContext { * @param fn the function to execute * @returns the result of the function */ - bind: (fn: (ctx: DataContext) => T) => T + bind: (fn: (ctx: DataContext) => T) => T; } const isDataContext = (context: unknown): context is DataContext => { diff --git a/shared-libs/cht-datasource/src/local/contact.ts b/shared-libs/cht-datasource/src/local/contact.ts new file mode 100644 index 00000000000..f3e384c091f --- /dev/null +++ b/shared-libs/cht-datasource/src/local/contact.ts @@ -0,0 +1,67 @@ +import { LocalDataContext, SettingsService } from './libs/data-context'; +import { getDocById, getDocsByIds } from './libs/doc'; +import { UuidQualifier } from '../qualifier'; +import * as Contact from '../contact'; +import { deepCopy, isNonEmptyArray, Nullable } from '../libs/core'; +import { Doc } from '../libs/doc'; +import logger from '@medic/logger'; +import contactTypeUtils from '@medic/contact-types-utils'; +import { getLineageDocsById, getPrimaryContactIds, hydrateLineage, hydratePrimaryContact } from './libs/lineage'; + +/** @internal */ +export namespace v1 { + const isContact = + (settings: SettingsService) => (doc: Nullable, uuid?: string): doc is Contact.v1.Contact => { + if (!doc) { + if (uuid) { + logger.warn(`No contact found for identifier [${uuid}].`); + } + return false; + } + + const contactTypes = contactTypeUtils.getContactTypes(settings.getAll()); + const contactTypesIds = contactTypes.map((item) => item.id); + if (!contactTypesIds.includes(doc.type)) { + logger.warn(`Document [${doc._id}] is not a valid contact.`); + return false; + } + return true; + }; + + /** @internal */ + export const get = ({ medicDb, settings }: LocalDataContext) => { + const getMedicDocById = getDocById(medicDb); + return async (identifier: UuidQualifier): Promise> => { + const doc = await getMedicDocById(identifier.uuid); + if (!isContact(settings)(doc, identifier.uuid)) { + return null; + } + + return doc; + }; + }; + + /** @internal */ + export const getWithLineage = ({ medicDb, settings }: LocalDataContext) => { + const getLineageDocs = getLineageDocsById(medicDb); + const getMedicDocsById = getDocsByIds(medicDb); + return async (identifier: UuidQualifier): Promise> => { + const [contact, ...lineageContacts] = await getLineageDocs(identifier.uuid); + if (!isContact(settings)(contact, identifier.uuid)) { + return null; + } + + if (!isNonEmptyArray(lineageContacts)) { + logger.debug(`No lineage places found for person [${identifier.uuid}].`); + return contact; + } + + const contactUuids = getPrimaryContactIds(lineageContacts) + .filter((uuid) => uuid !== identifier.uuid); + const contacts = [contact, ...(await getMedicDocsById(contactUuids))]; + const lineageContactsWithContact = lineageContacts.map(hydratePrimaryContact(contacts)); + const contactWithLineage = hydrateLineage(contact, lineageContactsWithContact); + return deepCopy(contactWithLineage); + }; + }; +} diff --git a/shared-libs/cht-datasource/src/local/index.ts b/shared-libs/cht-datasource/src/local/index.ts index f6fc2ffe2f6..81a9e76eb71 100644 --- a/shared-libs/cht-datasource/src/local/index.ts +++ b/shared-libs/cht-datasource/src/local/index.ts @@ -1,3 +1,4 @@ +export * as Contact from './contact'; export * as Person from './person'; export * as Place from './place'; export { getLocalDataContext } from './libs/data-context'; diff --git a/shared-libs/cht-datasource/src/local/libs/lineage.ts b/shared-libs/cht-datasource/src/local/libs/lineage.ts index 15b7c97cbaa..d53868d3a3f 100644 --- a/shared-libs/cht-datasource/src/local/libs/lineage.ts +++ b/shared-libs/cht-datasource/src/local/libs/lineage.ts @@ -1,4 +1,4 @@ -import { Contact, NormalizedParent } from '../../libs/contact'; +import * as Contact from '../../contact'; import { DataObject, findById, @@ -47,7 +47,7 @@ export const hydratePrimaryContact = (contacts: Doc[]) => (place: Nullable) }; }; -const getParentUuid = (index: number, contact?: NormalizedParent): Nullable => { +const getParentUuid = (index: number, contact?: Contact.v1.NormalizedParent): Nullable => { if (!contact) { return null; } @@ -71,9 +71,9 @@ const mergeLineage = (lineage: DataObject[], parent: DataObject): DataObject => /** @internal */ export const hydrateLineage = ( - contact: Contact, + contact: Contact.v1.Contact, lineage: Nullable[] -): Contact => { +): Contact.v1.Contact => { const fullLineage = lineage .map((place, index) => { if (place) { @@ -87,5 +87,5 @@ export const hydrateLineage = ( return { _id: parentId }; }); const hierarchy: NonEmptyArray = [contact, ...fullLineage]; - return mergeLineage(hierarchy.slice(0, -1), getLastElement(hierarchy)) as Contact; + return mergeLineage(hierarchy.slice(0, -1), getLastElement(hierarchy)) as Contact.v1.Contact; }; diff --git a/shared-libs/cht-datasource/src/local/person.ts b/shared-libs/cht-datasource/src/local/person.ts index ecf4f86061f..d356a8eeea5 100644 --- a/shared-libs/cht-datasource/src/local/person.ts +++ b/shared-libs/cht-datasource/src/local/person.ts @@ -3,11 +3,11 @@ import contactTypeUtils from '@medic/contact-types-utils'; import { deepCopy, isNonEmptyArray, Nullable, Page } from '../libs/core'; import { ContactTypeQualifier, UuidQualifier } from '../qualifier'; import * as Person from '../person'; -import {fetchAndFilter, getDocById, getDocsByIds, queryDocsByKey} from './libs/doc'; +import { fetchAndFilter, getDocById, getDocsByIds, queryDocsByKey } from './libs/doc'; import { LocalDataContext, SettingsService } from './libs/data-context'; import logger from '@medic/logger'; import { getLineageDocsById, getPrimaryContactIds, hydrateLineage, hydratePrimaryContact } from './libs/lineage'; -import {InvalidArgumentError} from '../libs/error'; +import { InvalidArgumentError } from '../libs/error'; /** @internal */ export namespace v1 { diff --git a/shared-libs/cht-datasource/src/local/place.ts b/shared-libs/cht-datasource/src/local/place.ts index 0c2663a04c7..2deebb0f58d 100644 --- a/shared-libs/cht-datasource/src/local/place.ts +++ b/shared-libs/cht-datasource/src/local/place.ts @@ -3,9 +3,9 @@ import contactTypeUtils from '@medic/contact-types-utils'; import { deepCopy, isNonEmptyArray, NonEmptyArray, Nullable, Page } from '../libs/core'; import { ContactTypeQualifier, UuidQualifier } from '../qualifier'; import * as Place from '../place'; -import {fetchAndFilter, getDocById, getDocsByIds, queryDocsByKey} from './libs/doc'; +import { fetchAndFilter, getDocById, getDocsByIds, queryDocsByKey } from './libs/doc'; import { LocalDataContext, SettingsService } from './libs/data-context'; -import { Contact } from '../libs/contact'; +import * as Contact from '../contact'; import logger from '@medic/logger'; import { getLineageDocsById, getPrimaryContactIds, hydrateLineage, hydratePrimaryContact } from './libs/lineage'; import { InvalidArgumentError } from '../libs/error'; @@ -56,7 +56,7 @@ export namespace v1 { const contactUuids = getPrimaryContactIds(places); const contacts = await getMedicDocsById(contactUuids); const [placeWithContact, ...linagePlacesWithContact] = places.map(hydratePrimaryContact(contacts)); - const placeWithLineage = hydrateLineage(placeWithContact as Contact, linagePlacesWithContact); + const placeWithLineage = hydrateLineage(placeWithContact as Contact.v1.Contact, linagePlacesWithContact); return deepCopy(placeWithLineage); }; }; diff --git a/shared-libs/cht-datasource/src/person.ts b/shared-libs/cht-datasource/src/person.ts index 8d20f85c887..789b7704c44 100644 --- a/shared-libs/cht-datasource/src/person.ts +++ b/shared-libs/cht-datasource/src/person.ts @@ -1,6 +1,6 @@ import { ContactTypeQualifier, isUuidQualifier, UuidQualifier } from './qualifier'; import { adapt, assertDataContext, DataContext } from './libs/data-context'; -import { Contact, NormalizedParent } from './libs/contact'; +import * as Contact from './contact'; import * as Remote from './remote'; import * as Local from './local'; import * as Place from './place'; @@ -14,7 +14,7 @@ export namespace v1 { /** * Immutable data about a person contact. */ - export interface Person extends Contact { + export interface Person extends Contact.v1.Contact { readonly date_of_birth?: Date; readonly phone?: string; readonly patient_id?: string; @@ -25,7 +25,7 @@ export namespace v1 { * Immutable data about a person contact, including the full records of the parent place lineage. */ export interface PersonWithLineage extends Person { - readonly parent?: Place.v1.PlaceWithLineage | NormalizedParent, + readonly parent?: Place.v1.PlaceWithLineage | Contact.v1.NormalizedParent; } const assertPersonQualifier: (qualifier: unknown) => asserts qualifier is UuidQualifier = (qualifier: unknown) => { @@ -34,10 +34,11 @@ export namespace v1 { } }; - const getPerson = ( - localFn: (c: LocalDataContext) => (qualifier: UuidQualifier) => Promise, - remoteFn: (c: RemoteDataContext) => (qualifier: UuidQualifier) => Promise - ) => (context: DataContext) => { + const getPerson = + ( + localFn: (c: LocalDataContext) => (qualifier: UuidQualifier) => Promise, + remoteFn: (c: RemoteDataContext) => (qualifier: UuidQualifier) => Promise + ) => (context: DataContext) => { assertDataContext(context); const fn = adapt(context, localFn, remoteFn); return async (qualifier: UuidQualifier): Promise => { @@ -69,9 +70,7 @@ export namespace v1 { * @throws Error if a data context is not provided * @see {@link getAll} which provides the same data, but without having to manually account for paging */ - export const getPage = ( - context: DataContext - ): typeof curriedFn => { + export const getPage = (context: DataContext): typeof curriedFn => { assertDataContext(context); const fn = adapt(context, Local.Person.v1.getPage, Remote.Person.v1.getPage); @@ -89,7 +88,7 @@ export namespace v1 { const curriedFn = async ( personType: ContactTypeQualifier, cursor: Nullable = null, - limit = 100, + limit = 100 ): Promise> => { assertTypeQualifier(personType); assertCursor(cursor); @@ -106,9 +105,7 @@ export namespace v1 { * @returns a function for getting a generator that fetches people * @throws Error if a data context is not provided */ - export const getAll = ( - context: DataContext - ): typeof curriedGen => { + export const getAll = (context: DataContext): typeof curriedGen => { assertDataContext(context); const getPage = context.bind(v1.getPage); diff --git a/shared-libs/cht-datasource/src/place.ts b/shared-libs/cht-datasource/src/place.ts index 84c78d98de4..8f55ee6e4cc 100644 --- a/shared-libs/cht-datasource/src/place.ts +++ b/shared-libs/cht-datasource/src/place.ts @@ -1,22 +1,21 @@ -import { Contact, NormalizedParent } from './libs/contact'; +import * as Contact from './contact'; import * as Person from './person'; -import { LocalDataContext} from './local/libs/data-context'; -import {ContactTypeQualifier, isUuidQualifier, UuidQualifier} from './qualifier'; +import { LocalDataContext } from './local/libs/data-context'; +import { ContactTypeQualifier, isUuidQualifier, UuidQualifier } from './qualifier'; import { RemoteDataContext } from './remote/libs/data-context'; import { adapt, assertDataContext, DataContext } from './libs/data-context'; import * as Local from './local'; import * as Remote from './remote'; -import {assertCursor, assertLimit, assertTypeQualifier, getPagedGenerator, Nullable, Page} from './libs/core'; +import { assertCursor, assertLimit, assertTypeQualifier, getPagedGenerator, Nullable, Page } from './libs/core'; /** */ export namespace v1 { - /** * Immutable data about a place contact. */ - export interface Place extends Contact { - readonly contact?: NormalizedParent, - readonly place_id?: string, + export interface Place extends Contact.v1.Contact { + readonly contact?: Contact.v1.NormalizedParent; + readonly place_id?: string; } /** @@ -24,8 +23,8 @@ export namespace v1 { * contact for the place. */ export interface PlaceWithLineage extends Place { - readonly contact?: Person.v1.PersonWithLineage | NormalizedParent, - readonly parent?: PlaceWithLineage | NormalizedParent, + readonly contact?: Person.v1.PersonWithLineage | Contact.v1.NormalizedParent; + readonly parent?: PlaceWithLineage | Contact.v1.NormalizedParent; } const assertPlaceQualifier: (qualifier: unknown) => asserts qualifier is UuidQualifier = (qualifier: unknown) => { @@ -34,10 +33,11 @@ export namespace v1 { } }; - const getPlace = ( - localFn: (c: LocalDataContext) => (qualifier: UuidQualifier) => Promise, - remoteFn: (c: RemoteDataContext) => (qualifier: UuidQualifier) => Promise - ) => (context: DataContext) => { + const getPlace = + ( + localFn: (c: LocalDataContext) => (qualifier: UuidQualifier) => Promise, + remoteFn: (c: RemoteDataContext) => (qualifier: UuidQualifier) => Promise + ) => (context: DataContext) => { assertDataContext(context); const fn = adapt(context, localFn, remoteFn); return async (qualifier: UuidQualifier): Promise => { @@ -69,9 +69,7 @@ export namespace v1 { * @throws Error if a data context is not provided * @see {@link getAll} which provides the same data, but without having to manually account for paging */ - export const getPage = ( - context: DataContext - ): typeof curriedFn => { + export const getPage = (context: DataContext): typeof curriedFn => { assertDataContext(context); const fn = adapt(context, Local.Place.v1.getPage, Remote.Place.v1.getPage); @@ -106,9 +104,7 @@ export namespace v1 { * @returns a function for getting a generator that fetches places * @throws Error if a data context is not provided */ - export const getAll = ( - context: DataContext - ): typeof curriedGen => { + export const getAll = (context: DataContext): typeof curriedGen => { assertDataContext(context); const getPage = context.bind(v1.getPage); diff --git a/shared-libs/cht-datasource/src/qualifier.ts b/shared-libs/cht-datasource/src/qualifier.ts index 67c6ac67b1a..0e76dbcd637 100644 --- a/shared-libs/cht-datasource/src/qualifier.ts +++ b/shared-libs/cht-datasource/src/qualifier.ts @@ -56,3 +56,31 @@ export const byContactType = (contactType: string): ContactTypeQualifier => { export const isContactTypeQualifier = (contactType: unknown): contactType is ContactTypeQualifier => { return isRecord(contactType) && hasField(contactType, { name: 'contactType', type: 'string' }); }; + +/** + * A qualifier that identifies entities based on a freetext search string. + */ +export type FreetextQualifier = Readonly<{ freetext: string }>; + +/** + * Builds a qualifier for finding entities by the given freetext string. + * @param freetext the text to search with + * @returns the qualifier + * @throws Error if the search string is not provided or is empty + */ +export const byFreetext = (freetext: string): FreetextQualifier => { + if (!isString(freetext) || freetext.length === 0) { + throw new InvalidArgumentError(`Invalid freetext [${JSON.stringify(freetext)}].`); + } + + return { freetext }; +}; + +/** + * Returns `true` if the given qualifier is a {@link FreetextQualifier} otherwise `false`. + * @param qualifier the qualifier to check + * @returns `true` if the given type is a {@link FreetextQualifier}, otherwise `false`. + */ +export const isFreetextQualifier = (qualifier: unknown): qualifier is FreetextQualifier => { + return isRecord(qualifier); +}; diff --git a/shared-libs/cht-datasource/src/remote/contact.ts b/shared-libs/cht-datasource/src/remote/contact.ts new file mode 100644 index 00000000000..4a245ebea1b --- /dev/null +++ b/shared-libs/cht-datasource/src/remote/contact.ts @@ -0,0 +1,23 @@ +import { getResource, RemoteDataContext } from './libs/data-context'; +import { UuidQualifier } from '../qualifier'; +import { Nullable } from '../libs/core'; +import * as Contact from '../contact'; + +/** @internal */ +export namespace v1 { + const getContact = (remoteContext: RemoteDataContext) => getResource(remoteContext, 'api/v1/contact'); + + /** @internal */ + export const get = + (remoteContext: RemoteDataContext) => ( + identifier: UuidQualifier + ): Promise> => getContact(remoteContext)(identifier.uuid); + + /** @internal */ + export const getWithLineage = (remoteContext: RemoteDataContext) => ( + identifier: UuidQualifier + ): Promise> => getContact(remoteContext)( + identifier.uuid, + { with_lineage: 'true' } + ); +} diff --git a/shared-libs/cht-datasource/src/remote/index.ts b/shared-libs/cht-datasource/src/remote/index.ts index 6db881ec27e..e1a562530cf 100644 --- a/shared-libs/cht-datasource/src/remote/index.ts +++ b/shared-libs/cht-datasource/src/remote/index.ts @@ -1,3 +1,4 @@ +export * as Contact from './contact'; export * as Person from './person'; export * as Place from './place'; export { getRemoteDataContext } from './libs/data-context'; diff --git a/shared-libs/cht-datasource/src/remote/place.ts b/shared-libs/cht-datasource/src/remote/place.ts index 351ab5dfab0..710abfdfb99 100644 --- a/shared-libs/cht-datasource/src/remote/place.ts +++ b/shared-libs/cht-datasource/src/remote/place.ts @@ -1,5 +1,5 @@ import { Nullable, Page } from '../libs/core'; -import {ContactTypeQualifier, UuidQualifier} from '../qualifier'; +import { ContactTypeQualifier, UuidQualifier } from '../qualifier'; import * as Place from '../place'; import { getResource, getResources, RemoteDataContext } from './libs/data-context'; diff --git a/shared-libs/cht-datasource/test/libs/contact.spec.ts b/shared-libs/cht-datasource/test/libs/contact.spec.ts index dadb7d71ca0..e8978b3c1d7 100644 --- a/shared-libs/cht-datasource/test/libs/contact.spec.ts +++ b/shared-libs/cht-datasource/test/libs/contact.spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { isNormalizedParent } from '../../src/libs/contact'; +import * as Contact from '../../src/contact'; import sinon, { SinonStub } from 'sinon'; import * as Core from '../../src/libs/core'; @@ -24,7 +24,7 @@ describe('contact lib', () => { ] as [unknown, boolean, boolean][]).forEach(([value, dataObj, expected]) => { it(`evaluates ${JSON.stringify(value)}`, () => { isDataObject.returns(dataObj); - expect(isNormalizedParent(value)).to.equal(expected); + expect(Contact.v1.isNormalizedParent(value)).to.equal(expected); }); }); });