@oystehr/sdk
Version:
Oystehr SDK
305 lines (276 loc) • 9.22 kB
text/typescript
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();
}