@twentysixlabs/smarterfhir
Version:
Library for simplifying all things FHIR and SMART
359 lines (330 loc) • 14.3 kB
text/typescript
import * as R4 from "fhir/r4"
import SubClient, { FhirClientTypes } from "../FhirClient"
import { EMR } from "../Launcher/SmartLaunchHandler"
import {
isResourceMissingContext,
isResourceMissingEncounter,
isResourceMissingSubject,
} from "../Resource/resourceUtils"
import { Transformer } from "../Resource/transformer"
import {
Author,
FhirClientResourceWithRequiredType,
GenericContext,
GenericEncounterReference,
GenericSubject,
ObjectWithID,
R4ResourceWithRequiredType,
UserReadResult,
} from "../types"
/**
* The EMR_ENDPOINTS type represents an object with two properties, "token" and "r4", both of which are URLs.
* @property {URL} token - A URL that represents the endpoint for accessing the token service in an EMR (Electronic Medical Record) system.
* @property {URL} r4 - The "r4" property in the EMR_ENDPOINTS type represents a URL that is used to access the R4 version of an EMR (Electronic Medical Record)
* endpoint.
*/
export type EMR_ENDPOINTS = {
token: URL,
r4: URL,
auth: URL
}
/**
Represents the BaseClient abstract class.
*/
export default abstract class BaseClient {
readonly fhirClientDefault
readonly defaultCreateHeaders: HeadersInit = {}
abstract readonly EMR_TYPE: EMR
static readonly AUTHORIZE_ENDPOINT: string | undefined;
static readonly TOKEN_ENDPOINT: string | undefined;
static readonly R4_ENDPOINT: string | undefined;
public abstract getEndpoints(): EMR_ENDPOINTS
public getEMRType(): EMR {
return this.EMR_TYPE
}
/**
* The function constructs and returns an object containing three endpoints (token, r4, and auth) based on the provided tokenEP, r4EP, and authorizeEP values.
* @param {string | undefined} tokenEP - The `tokenEP` parameter is a string that represents the token endpoint. This endpoint is used to obtain an access token
* for authentication and authorization purposes.
* @param {string | undefined} r4EP - The `r4EP` parameter is the endpoint URL for the R4 API. It is used to make requests to the R4 API.
* @param {string | undefined} authorizeEP - The `authorizeEP` parameter is the endpoint URL for the authorization server. It is used for initiating the
* authorization process and obtaining an authorization code or access token.
* @returns An object with three properties: "token", "r4", and "auth". Each property is assigned a new URL object based on the corresponding input parameters.
*/
protected static constructEndpoints(tokenEP: string | undefined, r4EP: string | undefined, authorizeEP: string | undefined) {
if (tokenEP == undefined)
throw Error('Token Endpoint not defined')
if (r4EP === undefined)
throw Error('R4 Endpoint not defined')
if (authorizeEP === undefined)
throw Error('Auth Endpoint not defined')
return {
token: new URL(tokenEP),
r4: new URL(r4EP),
auth: new URL(authorizeEP)
}
}
/**
* Fetch options for create operation headers.
* @private
* @readonly
* @type {FhirClientTypes.FetchOptions}
*/
protected createHeaders(
additionalCreateHeaders: HeadersInit
): FhirClientTypes.FetchOptions {
return {
headers: {
...this.defaultCreateHeaders,
...additionalCreateHeaders,
},
}
}
/**
* Creates an instance of BaseClient.
* @param {SubClient} fhirClientDefault - The default FHIR client to use.
*/
constructor(fhirClientDefault: SubClient) {
this.fhirClientDefault = fhirClientDefault
}
/**
* Gets the ID from an object with ID.
* @private
* @param {T} objectWithId - The object with ID.
* @returns {Promise<string>} - A promise resolving to the ID.
* @throws {Error} - Throws an error if the ID is not found.
*/
private async getIDfromObject<T extends ObjectWithID>(objectWithId: T) {
const id = await objectWithId.id
if (!id) {
console.error(objectWithId)
throw new Error(`id not found`)
}
return id
}
/**
* Creates a patient subject.
* @private
* @returns {Promise<GenericSubject>} - A promise resolving to the patient subject.
*/
private async createPatientSubject(patientIdParam?: string): Promise<GenericSubject> {
const patientID = patientIdParam == undefined ? await this.getIDfromObject(
this.fhirClientDefault.patient
) : patientIdParam
return {
subject: {
reference: `Patient/${patientID}`,
},
}
}
/**
* Creates a reference to an encounter in a FHIR system. The function takes two optional parameters: `encounterIdParam` which is the ID of the encounter, and `encounterType` which specifies the type of reference to be created ('GenericEncounterReference' or 'R4.Reference').
* @template T
* @param [encounterIdParam]
* @param [encounterType]
* @returns encounter reference
*/
private async createEncounterReference<T extends 'GenericEncounterReference' | 'R4.Reference'>(encounterIdParam?: string, encounterType?: T): Promise<T extends 'GenericEncounterReference' ? GenericEncounterReference : R4.Reference>
private async createEncounterReference(encounterIdParam?: string, encounterType?: 'GenericEncounterReference' | 'R4.Reference') {
const encounterID = encounterIdParam == undefined ? await this.getIDfromObject(
this.fhirClientDefault.encounter
) : encounterIdParam
const reference: R4.Reference = {
reference: `Encounter/${encounterID}`,
}
return encounterType == 'GenericEncounterReference' ? { encounter: reference } : reference
}
/**
* The function creates an array of encounter references asynchronously.
* @returns An array containing the result of the `createReferenceToEncounter` function, which is awaited.
*/
private async createEncounterReferenceArray(encounterIdParam?: string): Promise<R4.Reference[]> {
return [await this.createEncounterReference(encounterIdParam, 'R4.Reference')]
}
/**
* The function "createPeriod" creates a period object with the same start and end date.
* @param {string} start - The start parameter is a string that represents the start date or time of a period.
* @returns An object of type R4.Period is being returned.
*/
private createPeriod(start: string): R4.Period {
return {
start: start,
end: start,
}
}
/**
* The createContext function creates a context object with an encounter reference array and a period.
* @returns The function `createContext` is returning an object with a property `context` which contains the values of `encounter` and `period`.
*/
private async createContext(encounterIdParam?: string): Promise<GenericContext> {
const encounter = await this.createEncounterReferenceArray(encounterIdParam)
const currentDateString = new Date().toISOString()
const period: R4.Period = this.createPeriod(currentDateString)
return {
context: {
encounter: encounter,
period: period,
},
}
}
/**
* The function creates an array of author references using the user ID obtained from the FHIR client.
* @returns an object with an "author" property, which is an array containing an object with a "reference" property. The value of the "reference" property is a
* string in the format "Practitioner/{userID}".
*/
async createReferenceArrayAuthor(): Promise<Author> {
const userID = await this.getIDfromObject(this.fhirClientDefault.user)
return {
author: [
{
reference: `Practitioner/${userID}`,
},
],
}
}
/**
* Hydrates a resource with subject and encounter context.
* @param {T} fhirClientResource - The resource to hydrate.
* @returns {Promise<T>} - A promise resolving to the hydrated resource.
*/
async hydrateResource<T extends FhirClientResourceWithRequiredType, U extends R4ResourceWithRequiredType>(
fhirClientResource: T,
r4Resource: U,
patientId?: string,
encounterId?: string
): Promise<T> {
const subject = async (): Promise<GenericSubject | undefined> => {
if (isResourceMissingSubject(r4Resource)) return await this.createPatientSubject(patientId)
}
const encounter = async (): Promise<GenericEncounterReference | undefined> => {
if (isResourceMissingEncounter(r4Resource)) return await this.createEncounterReference(encounterId, 'GenericEncounterReference')
}
const context = async (): Promise<GenericContext | undefined> => {
if (isResourceMissingContext(r4Resource)) return await this.createContext(encounterId)
}
return {
...fhirClientResource,
...await subject() ?? {},
...await encounter() ?? {},
...await context() ?? {},
}
}
/**
* The function creates a resource of type T, transforms it to a FhirClientType, hydrates it, sends a create request to the FhirClientDefault, transforms the
* result back to type T, and returns it.
* @param {T} r4Resource - The `resource` parameter is the FHIR resource object that you want to create. It should be an object that conforms to the R4 (Release 4)
* FHIR specification and has a required `resourceType` property.
* @param [additionalHeaders] - The `additionalHeaders` parameter is an optional object that represents additional headers to be included in the HTTP request when
* creating a resource. It is of type `FhirClientTypes.FetchOptions`.
* @returns a Promise of type T, which is the same type as the input resource.
*/
async create<T extends R4ResourceWithRequiredType>(
r4Resource: T,
patientId?: string,
encounterId?: string,
additionalHeaders?: FhirClientTypes.FetchOptions
): Promise<T> {
const transformedResource = Transformer.toFhirClientType(r4Resource)
const hydratedResource = await this.hydrateResource(transformedResource, r4Resource, patientId, encounterId)
const resultResource: FhirClientResourceWithRequiredType =
await this.createHydratedResource(hydratedResource, additionalHeaders)
const resultAsR4 = Transformer.toR4FhirType<typeof resultResource, T>(
resultResource
)
return resultAsR4 as T
}
async createHydratedResource(hydratedResource: Omit<Partial<FhirClientTypes.FHIR.Resource>, "resourceType"> & Required<Pick<Partial<FhirClientTypes.FHIR.Resource>, "resourceType">> & { context?: GenericContext; subject?: R4.Reference | undefined }, additionalHeaders?: FhirClientTypes.FetchOptions | undefined): Promise<FhirClientResourceWithRequiredType> {
return await this.fhirClientDefault
.create(hydratedResource, {
...(additionalHeaders ? additionalHeaders : {}),
})
.then((resource) => {
if (!(resource as R4ResourceWithRequiredType).resourceType) {
return resource.body as FhirClientResourceWithRequiredType
}
if (!(resource as R4ResourceWithRequiredType).resourceType) {
console.log(resource)
throw new Error(`Resource ${resource}, must have a resource type.`)
}
return resource as FhirClientResourceWithRequiredType
})
.catch((reason) => {
throw new Error("It failed with:" + reason)
})
}
/**
* The function `requestResource` asynchronously requests a resource using a specified resource ID and optional request options.
* @param {string} resourceID - The resourceID parameter is a string that represents the ID of the resource you want to request. It could be the URL or identifier
* of the resource you want to retrieve.
* @param {RequestInit} [requestOptions] - The `requestOptions` parameter is an optional object that contains additional options for the HTTP request. It can
* include properties such as headers, method, body, etc.
* @returns a resource of type R4.Resource.
*/
async requestResource(resourceID: string, requestOptions?: RequestInit) {
const resultResource = await this.fhirClientDefault.request({
url: resourceID,
...(requestOptions ? { headers: requestOptions.headers } : {}),
})
return resultResource as R4.Resource
}
/**
* The function `getUserRead` is a private asynchronous function that retrieves user data using the `read` method of the `fhirClientDefault` object and returns a
* `UserReadResult` promise.
* @returns a Promise that resolves to a UserReadResult object.
*/
private async getUserRead(): Promise<UserReadResult> {
const user = await this.fhirClientDefault.user.read()
return user
}
/**
* The function `getPractitionerRead` retrieves a user and checks if they are a practitioner, then converts the user to an R4 Practitioner if they are, otherwise
* it throws an error.
* @param {UserReadResult} user - The `user` parameter is of type `UserReadResult`, which is a result object returned from the `getUserRead()` function. It
* represents a user resource in the FHIR format.
* @returns a Promise that resolves to a FHIR R4 Practitioner resource.
*/
async getPractitionerRead(): Promise<R4.Practitioner> {
function isPractitioner(
user: UserReadResult
): user is FhirClientTypes.FHIR.Practitioner {
return user.resourceType == "Practitioner"
}
const user = await this.getUserRead()
if (isPractitioner(user)) {
const userInR4 = Transformer.toR4FhirType<typeof user, R4.Practitioner>(
user
)
return userInR4
}
throw new Error("User is Not a Practitioner")
}
/**
* The function `getPatientRead` retrieves a patient record from a FHIR server and transforms it into an R4.Patient object.
* @returns a Promise that resolves to an instance of the R4.Patient type.
*/
async getPatientRead(): Promise<R4.Patient> {
const patient = await this.fhirClientDefault.patient.read()
const patientInR4 = Transformer.toR4FhirType<typeof patient, R4.Patient>(
patient
)
return patientInR4
}
/* The `getEncounterRead` function is an asynchronous function that retrieves an encounter record from a FHIR server and transforms it into an R4.Encounter object. */
async getEncounterRead(): Promise<R4.Encounter> {
const encounter = await this.fhirClientDefault.encounter.read()
const encounterInR4 = Transformer.toR4FhirType<
typeof encounter,
R4.Encounter
>(encounter)
return encounterInR4
}
/**
* The function creates a patient resource and returns it as a R4.Patient object.
* @param {R4.Patient} patient - The `patient` parameter is the FHIR patient resource object that you want to create. It should be an object that conforms to the R4 (Release 4)
* FHIR specification and has a required `resourceType` property.
* @returns a Promise of type R4.Patient
*/
async createPatient(patient: R4.Patient): Promise<R4.Patient> {
return this.create(patient)
}
}