import { Operation } from 'fast-json-patch'; import { Bundle, BundleEntry, FhirResource } from 'fhir/r4b'; import { addParamsToSearch, fetcher, InternalClientRequest, ZapEHRClientRequest } from '../../client/client'; import { servers } from '../../client/openapi/fhir-api'; import { getConfig } from '../../config'; import { ZapEHRFHIRError, ZapEHRSdkError } from '../../errors'; import { BatchInput, BatchInputRequest } from './types'; function getServer(): string { return getConfig().ZAPEHR_FHIR_API_URL ?? servers[0]; } /** * Optional parameter that can be passed to the client methods. It allows * overriding the access token or project ID, and setting various headers, * such as 'Content-Type'. Also support enabling optimistic locking. */ export interface ZapEHRFHIRUpdateClientRequest extends ZapEHRClientRequest { /** * Enable optimistic locking for the request. If set to a version ID, the request will * include the 'If-Match' header with that value in the FHIR optimistic-locking format. * If the resource has been updated since the version provided, the request * will fail with a 412 Precondition Failed error. */ optimisticLockingVersionId?: string; } type FhirData = T | T[] | Bundle; type FhirFetcherResponse = any> = T; function fhirFetcher(path: string, method: string) { return async (params: any, request?: InternalClientRequest): Promise> => { try { // must await here to catch return await fetcher(getServer, path, method)(params, request); } catch (err: unknown) { // FHIR API error messages are JSON strings const fullError = err as { message: string | Record; code: number }; if (typeof fullError.message === 'string') { throw new ZapEHRSdkError({ message: fullError.message, code: fullError.code, }); } throw new ZapEHRFHIRError({ error: fullError.message, code: fullError.code, }); } }; } /** * @deprecated Use `search` instead * * Performs a FHIR search and returns the results as an array of FHIR resources * * @param options FHIR resource type and FHIR search parameters * @param request optional ZapEHRClientRequest object * @returns Array of FHIR resources */ export async function list( { resourceType, params }: Parameters[0], request?: ZapEHRClientRequest ): Promise> { const listResp = await listAsBundle({ resourceType, params }, request); const resources: T[] = listResp.entry ? listResp.entry.map((entry) => entry.resource as T) : []; return resources; } /** * @deprecated Use `search` instead * * Performs a FHIR search and returns the results as a Bundle resource * * @param options FHIR resource type and FHIR search parameters * @param request optional ZapEHRClientRequest object * @returns FHIR Bundle resource */ export async function listAsBundle( { resourceType, params }: { resourceType: string; params?: { name: string; value: string | number }[] }, request?: ZapEHRClientRequest ): Promise>> { return search({ resourceType, params }, request); } /** * Performs a FHIR search and returns the results as a Bundle resource * * @param options FHIR resource type and FHIR search parameters * @param request optional ZapEHRClientRequest object * @returns FHIR Bundle resource */ export async function search( { resourceType, params }: { resourceType: string; params?: { name: string; value: string | number }[] }, request?: ZapEHRClientRequest ): Promise>> { let paramMap: Record = {}; if (params) { paramMap = Object.entries(params).reduce((acc, [_, param]) => { if (!acc[param.name]) { acc[param.name] = []; } acc[param.name].push(param.value); return acc; }, {} as Record); } return fhirFetcher>(`/${resourceType}/_search`, 'POST')(paramMap, { ...request, contentType: 'application/x-www-form-urlencoded', }); } export async function create( params: T, request?: ZapEHRClientRequest ): Promise> { const { resourceType } = params; return fhirFetcher(`/${resourceType}`, 'POST')(params as unknown as Record, request); } export async function get( { resourceType, id }: { resourceType: string; id: string }, request?: ZapEHRClientRequest ): Promise> { return fhirFetcher(`/${resourceType}/${id}`, 'GET')({}, request); } export async function update( params: T, request?: ZapEHRFHIRUpdateClientRequest ): Promise> { const { id, resourceType } = params; return fhirFetcher(`/${resourceType}/${id}`, 'PUT')(params as unknown as Record, { ...request, ifMatch: request?.optimisticLockingVersionId ? `W/"${request.optimisticLockingVersionId}"` : undefined, }); } export async function patch( { resourceType, id, operations }: { resourceType: string; id: string; operations: Operation[] }, request?: ZapEHRFHIRUpdateClientRequest ): Promise> { return fhirFetcher(`/${resourceType}/${id}`, 'PATCH')(operations, { ...request, contentType: 'application/json-patch+json', ifMatch: request?.optimisticLockingVersionId ? `W/"${request.optimisticLockingVersionId}"` : undefined, }); } async function del( { resourceType, id }: { resourceType: string; id: string }, request?: ZapEHRClientRequest ): Promise> { return fhirFetcher(`/${resourceType}/${id}`, 'DELETE')({}, request); } export { del as delete }; export async function history( { resourceType, id }: { resourceType: string; id: string }, request?: ZapEHRClientRequest ): Promise>>; export async function history( { resourceType, id, versionId }: { resourceType: string; id: string; versionId: string }, request?: ZapEHRClientRequest ): Promise>; export async function history( { resourceType, id, versionId }: { resourceType: string; id: string; versionId?: string }, request?: ZapEHRClientRequest ): Promise> | FhirFetcherResponse> { return fhirFetcher(`/${resourceType}/${id}/_history${versionId ? `/${versionId}` : ''}`, 'GET')({}, request); } function batchInputRequestToBundleEntryItem(request: BatchInputRequest): BundleEntry { const { method, url } = request; const baseRequest = { request: { method, url, }, }; if (url.split('?').length > 1) { const [resource, query] = url.split('?'); const params = query .split('&') .map((param) => { const [name, value] = param.split('='); return { name, value }; }) .reduce((acc, { name, value }) => { acc[name] = value; return acc; }, {} as Record); const search = new URLSearchParams(); addParamsToSearch(params, search); baseRequest.request.url = `${resource}?${search.toString()}`; } if (['GET', 'DELETE', 'HEAD'].includes(method)) { return baseRequest; } if (method === 'PUT' || method === 'PATCH') { const { resource } = request; return { ...baseRequest, resource, }; } if (method === 'POST') { const { resource, fullUrl } = request; return { ...baseRequest, resource, fullUrl, }; } throw new Error('Unrecognized method'); } function bundleRequest(type: 'batch' | 'transaction') { return async function ( input: BatchInput, request?: ZapEHRClientRequest ): Promise>> { return fhirFetcher('/', 'POST')( { resourceType: 'Bundle', type, entry: input.requests.map(batchInputRequestToBundleEntryItem), }, request ); }; } export const batch = bundleRequest('batch'); export const transaction = bundleRequest('transaction');