UNPKG

@oystehr/sdk

Version:

Oystehr SDK

305 lines (276 loc) 9.22 kB
import { Address as AddressR4B, HumanName as HumanNameR4B } from 'fhir/r4b'; import { Address as AddressR5, HumanName as HumanNameR5 } from 'fhir/r5'; import { BatchInput, BatchInputRequest, Binary, Bundle, BundleEntry, FhirBundle, FhirCreateParams, FhirDeleteParams, FhirGetParams, FhirHistoryGetParams, FhirHistorySearchParams, FhirPatchParams, FhirResource, FhirSearchParams, FhirUpdateParams, } from '../..'; import { addParamsToSearch, FhirFetcherResponse, OystehrClientRequest, SDKResource } from '../../client/client'; /** * 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 OystehrFHIRUpdateClientRequest extends OystehrClientRequest { /** * 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; } /** * Performs a FHIR search and returns the results as a Bundle resource * * @param options FHIR resource type and FHIR search parameters * @param request optional OystehrClientRequest object * @returns FHIR Bundle resource */ export async function search<T extends FhirResource>( this: SDKResource, params: FhirSearchParams<T>, request?: OystehrClientRequest ): Promise<FhirFetcherResponse<Bundle<T>>> { const { resourceType, params: searchParams } = params; let paramMap: Record<string, (string | number)[]> | undefined; if (searchParams) { paramMap = Object.entries(searchParams).reduce((acc, [_, param]) => { if (!acc[param.name]) { acc[param.name] = []; } acc[param.name].push(param.value); return acc; }, {} as Record<string, (string | number)[]>); } const requestBundle = await this.fhirRequest<FhirBundle<T>>(`/${resourceType}/_search`, 'POST')(paramMap, { ...request, contentType: 'application/x-www-form-urlencoded', }); const bundle: Bundle<T> = { ...requestBundle, entry: requestBundle.entry as Array<BundleEntry<T>> | undefined, unbundle: function (this: { entry?: Array<BundleEntry<T>> | undefined }) { return this.entry?.map((entry) => entry.resource).filter((entry): entry is T => entry !== undefined) ?? []; }, }; return bundle; } export async function create<T extends FhirResource>( this: SDKResource, params: FhirCreateParams<T>, request?: OystehrClientRequest ): Promise<FhirFetcherResponse<T>> { const { resourceType } = params; return this.fhirRequest(`/${resourceType}`, 'POST')(params as unknown as Record<string, unknown>, request); } export async function get<T extends FhirResource>( this: SDKResource, { resourceType, id }: FhirGetParams<T>, request?: OystehrClientRequest ): Promise<FhirFetcherResponse<T>> { return this.fhirRequest<T>(`/${resourceType}/${id}`, 'GET')({}, request); } export async function update<T extends FhirResource>( this: SDKResource, params: FhirUpdateParams<T>, request?: OystehrFHIRUpdateClientRequest ): Promise<FhirFetcherResponse<T>> { const { id, resourceType } = params; return this.fhirRequest(`/${resourceType}/${id}`, 'PUT')(params as unknown as Record<string, unknown>, { ...request, ifMatch: request?.optimisticLockingVersionId ? `W/"${request.optimisticLockingVersionId}"` : undefined, }); } export async function patch<T extends FhirResource>( this: SDKResource, { resourceType, id, operations }: FhirPatchParams<T>, request?: OystehrFHIRUpdateClientRequest ): Promise<FhirFetcherResponse<T>> { return this.fhirRequest(`/${resourceType}/${id}`, 'PATCH')(operations, { ...request, contentType: 'application/json-patch+json', ifMatch: request?.optimisticLockingVersionId ? `W/"${request.optimisticLockingVersionId}"` : undefined, }); } async function del<T extends FhirResource>( this: SDKResource, { resourceType, id }: FhirDeleteParams<T>, request?: OystehrClientRequest ): Promise<FhirFetcherResponse<T>> { return this.fhirRequest(`/${resourceType}/${id}`, 'DELETE')({}, request); } export { del as delete }; export async function history<T extends FhirResource>( this: SDKResource, { resourceType, id }: FhirHistorySearchParams<T>, request?: OystehrClientRequest ): Promise<FhirFetcherResponse<Bundle<T>>>; export async function history<T extends FhirResource>( this: SDKResource, { resourceType, id, versionId }: FhirHistoryGetParams<T>, request?: OystehrClientRequest ): Promise<FhirFetcherResponse<T>>; export async function history<T extends FhirResource>( this: SDKResource, { resourceType, id, versionId }: { resourceType: string; id: string; versionId?: string }, request?: OystehrClientRequest ): Promise<FhirFetcherResponse<Bundle<T>> | FhirFetcherResponse<T>> { return this.fhirRequest(`/${resourceType}/${id}/_history${versionId ? `/${versionId}` : ''}`, 'GET')({}, request); } function batchInputRequestToBundleEntryItem<T extends FhirResource>( request: BatchInputRequest<T> ): BundleEntry<T | Binary<T>> { const { method, url } = request; const baseRequest = { request: { method, url, }, }; // Escape query string parameters in entry.request.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 }) => { if (!name) { return acc; } if (!acc[name]) { acc[name] = []; } acc[name].push(value); return acc; }, {} as Record<string, string[]>); const search = new URLSearchParams(); addParamsToSearch(params, search); baseRequest.request.url = `${resource}?${search.toString()}`; } // GET, DELETE, and HEAD require no further parameters if (['GET', 'DELETE', 'HEAD'].includes(method)) { return baseRequest as BundleEntry<T>; } // PUT updates require a full resource if (method === 'PUT') { const { resource } = request; return { ...baseRequest, resource: resource as T, } as BundleEntry<T>; } // PATCH can be Binary resource or JSON patch if (method === 'PATCH') { if ('resource' in request) { return { ...baseRequest, resource: request.resource, } as BundleEntry<Binary<T>>; } return { ...baseRequest, resource: { resourceType: 'Binary', contentType: 'application/json-patch+json', data: Buffer.from(JSON.stringify(request.operations), 'utf8').toString('base64'), }, } as BundleEntry<Binary<T>>; } // POST creates require a full resource if (method === 'POST') { const { resource, fullUrl } = request; return { ...baseRequest, resource: resource as T, fullUrl, } as BundleEntry<T>; } throw new Error('Unrecognized method'); } function bundleRequest(type: 'batch' | 'transaction') { return async function <BundleContentType extends FhirResource>( this: SDKResource, input: BatchInput<BundleContentType>, request?: OystehrClientRequest ): Promise<FhirFetcherResponse<Bundle<BundleContentType>>> { return this.fhirRequest('/', 'POST')( { resourceType: 'Bundle', type, entry: input.requests.map(batchInputRequestToBundleEntryItem), }, request ); }; } export const batch = bundleRequest('batch'); export const transaction = bundleRequest('transaction'); export function formatAddress( address: AddressR4B | AddressR5, options?: { all?: boolean; use?: boolean; lineSeparator?: string } ): string { const builder = []; if (address.line) { builder.push(...address.line); } if (address.city || address.state || address.postalCode) { const cityStateZip = []; if (address.city) { cityStateZip.push(address.city); } if (address.state) { cityStateZip.push(address.state); } if (address.postalCode) { cityStateZip.push(address.postalCode); } builder.push(cityStateZip.join(', ')); } if (address.use && (options?.all || options?.use)) { builder.push('[' + address.use + ']'); } return builder.join(options?.lineSeparator || ', ').trim(); } export function formatHumanName( name: HumanNameR4B | HumanNameR5, options?: { all?: boolean; prefix?: boolean; suffix?: boolean; use?: boolean; } ): string { const builder = []; if (name.prefix && options?.prefix !== false) { builder.push(...name.prefix); } if (name.given) { builder.push(...name.given); } if (name.family) { builder.push(name.family); } if (name.suffix && options?.suffix !== false) { builder.push(...name.suffix); } if (name.use && (options?.all || options?.use)) { builder.push('[' + name.use + ']'); } return builder.join(' ').trim(); }