fhir-kit-client
Version:
466 lines • 20.1 kB
JavaScript
import { HttpClient } from './http-client.js';
import { Pagination } from './pagination.js';
import { ReferenceResolver } from './reference-resolver.js';
import { authFromCapability, authFromWellKnown } from './smart.js';
import { createQueryString, validResourceType } from './utils.js';
/**
* FHIR REST client. Provides typed methods for all FHIR interactions.
*
* @example
* ```ts
* import { Client } from 'fhir-kit-client';
*
* const client = new Client({ baseUrl: 'https://r4.smarthealthit.org' });
* const patient = await client.read({ resourceType: 'Patient', id: '123' });
* ```
*/
export class Client {
/** Underlying HTTP client — exposed for advanced use cases. */
httpClient;
/** Pagination helper. */
pagination;
resolver;
/**
* @param config - Client configuration. `baseUrl` is required.
* @throws If `baseUrl` is blank or not a string.
*/
constructor({ baseUrl, customHeaders, requestOptions, requestSigner, bearerToken } = {}) {
this.httpClient = new HttpClient({ baseUrl, customHeaders, requestOptions, requestSigner });
if (bearerToken) {
this.httpClient.bearerToken = bearerToken;
}
this.resolver = new ReferenceResolver(this);
this.pagination = new Pagination(this.httpClient);
}
/**
* Return the raw `Request` and `Response` objects attached to a FHIR
* response value by the HTTP layer.
*
* @param requestResponse - A FHIR resource returned by any client method.
* @returns `{ request, response }` — either may be `undefined` for synthetic responses.
*/
static httpFor(requestResponse) {
return {
request: HttpClient.requestFor(requestResponse),
response: HttpClient.responseFor(requestResponse),
};
}
/** FHIR server base URL. */
get baseUrl() {
return this.httpClient.baseUrl;
}
set baseUrl(url) {
this.httpClient.baseUrl = url;
}
/** Custom headers sent with every request. */
get customHeaders() {
return this.httpClient.customHeaders;
}
set customHeaders(headers) {
this.httpClient.customHeaders = headers;
}
/** Set the OAuth2 bearer token used for Authorization headers. */
set bearerToken(token) {
this.httpClient.bearerToken = token;
}
/**
* Resolve a FHIR reference to a resource.
*
* - **Absolute URL** (`http://...`): fetches directly or creates a new client for a foreign base URL.
* - **Relative URL** (`Patient/123`): fetches from this client's `baseUrl`.
* - **Contained reference** (`#id`): resolved from `context.contained[]`.
* - **Bundle-scoped reference**: resolved from `context.entry[].fullUrl`.
*
* @param params.reference - The reference string to resolve.
* @param params.context - Optional Bundle or resource that may contain the referenced resource.
* @param params.options - Per-request options.
* @returns The resolved FHIR resource.
*/
resolve({ reference, context, options, }) {
return this.resolver.resolve({ reference, context, options });
}
/**
* Fetch SMART OAuth 2.0 authorization metadata from the server.
*
* Races three sources simultaneously and resolves with the first successful
* response: `.well-known/smart-configuration`, `metadata` (CapabilityStatement),
* and `.well-known/openid-configuration`. Rejects only if all three fail.
*
* @param params.options - Per-request options.
* @returns SMART auth metadata containing `authorizeUrl`, `tokenUrl`, etc.
*/
async smartAuthMetadata({ options = {} } = {}) {
const fetchOptions = {
...options,
headers: { accept: 'application/fhir+json,application/json', ...options.headers },
};
const base = this.baseUrl.replace(/\/*$/, '/');
const controllers = Array.from({ length: 3 }, () => new AbortController());
const abortOthers = (winner) => controllers.forEach((c, i) => {
if (i !== winner)
c.abort();
});
return Promise.any([
this.httpClient
.request('GET', `${base}.well-known/smart-configuration`, { ...fetchOptions, signal: controllers[0].signal })
.then((r) => {
abortOthers(0);
return authFromWellKnown(r);
}),
this.capabilityStatement({ ...fetchOptions, signal: controllers[1].signal }).then((r) => {
abortOthers(1);
return authFromCapability(r);
}),
this.httpClient
.request('GET', `${base}.well-known/openid-configuration`, { ...fetchOptions, signal: controllers[2].signal })
.then((r) => {
abortOthers(2);
return authFromWellKnown(r);
}),
]);
}
/**
* Fetch the server's CapabilityStatement.
*
* @param options - Per-request options.
* @returns The CapabilityStatement resource.
*/
capabilityStatement(options) {
return this.httpClient.get('metadata', options);
}
/**
* Execute a raw request against the FHIR server. Useful for endpoints not
* covered by higher-level methods.
*
* @param requestUrl - Path or absolute URL to request.
* @param params.method - HTTP method (default: `'GET'`).
* @param params.options - Per-request options.
* @param params.body - Request body (will be JSON-serialized if an object).
* @returns The parsed FHIR response.
*
* @example
* ```ts
* client.request('Patient/123')
* client.request('Patient/123', { method: 'DELETE' })
* client.request('Patient', { method: 'POST', body: newPatient })
* ```
*/
request(requestUrl, { method = 'GET', options = {}, body } = {}) {
return this.httpClient.request(method, requestUrl, options, body);
}
/**
* Read a resource by type and id (FHIR `read` interaction).
*
* @param params.resourceType - FHIR resource type (e.g. `'Patient'`).
* @param params.id - Resource id.
* @param params.options - Per-request options.
* @returns The requested resource.
* @throws If `resourceType` is invalid or the server returns an error.
*/
read({ resourceType, id, options }) {
if (!validResourceType(resourceType))
throw new Error(`Invalid resourceType: ${resourceType}`);
return this.httpClient.get(`${resourceType}/${id}`, options);
}
/**
* Read a specific version of a resource (FHIR `vread` interaction).
*
* @param params.resourceType - FHIR resource type.
* @param params.id - Resource id.
* @param params.version - Version id.
* @param params.options - Per-request options.
* @returns The specified version of the resource.
* @throws If `resourceType` is invalid or the server returns an error.
*/
vread({ resourceType, id, version, options }) {
if (!validResourceType(resourceType))
throw new Error(`Invalid resourceType: ${resourceType}`);
return this.httpClient.get(`${resourceType}/${id}/_history/${version}`, options);
}
/**
* Create a new resource (FHIR `create` interaction).
*
* @param params.resourceType - FHIR resource type.
* @param params.body - The resource to create.
* @param params.options - Per-request options.
* @returns The created resource (with server-assigned id).
* @throws If `resourceType` is invalid or the server returns an error.
*/
create({ resourceType, body, options }) {
if (!validResourceType(resourceType))
throw new Error(`Invalid resourceType: ${resourceType}`);
return this.httpClient.post(resourceType, body, options);
}
/**
* Delete a resource by type and id (FHIR `delete` interaction).
*
* @param params.resourceType - FHIR resource type.
* @param params.id - Resource id.
* @param params.options - Per-request options.
* @returns The server's response (often an OperationOutcome).
* @throws If `resourceType` is invalid or the server returns an error.
*/
delete({ resourceType, id, options }) {
if (!validResourceType(resourceType))
throw new Error(`Invalid resourceType: ${resourceType}`);
return this.httpClient.delete(`${resourceType}/${id}`, options);
}
/**
* Update a resource (FHIR `update` interaction).
*
* Supports conditional update via `searchParams` (sets the `If-Match`
* criteria). Provide either `id` or `searchParams`, not both.
*
* @param params.resourceType - FHIR resource type.
* @param params.id - Resource id (mutually exclusive with `searchParams`).
* @param params.searchParams - Conditional update criteria (mutually exclusive with `id`).
* @param params.body - The updated resource.
* @param params.options - Per-request options.
* @returns The updated resource.
* @throws If `resourceType` is invalid, both or neither of `id`/`searchParams` are provided,
* or the server returns an error.
*/
update({ resourceType, id, searchParams, body, options }) {
if (!validResourceType(resourceType))
throw new Error(`Invalid resourceType: ${resourceType}`);
if (id && searchParams)
throw new Error('Cannot specify both id and searchParams for update');
if (!id && !searchParams)
throw new Error('update requires either id or searchParams');
if (searchParams) {
const query = createQueryString(searchParams);
return this.httpClient.put(`${resourceType}?${query}`, body, options);
}
return this.httpClient.put(`${resourceType}/${id}`, body, options);
}
/**
* Patch a resource using JSON Patch (RFC 6902) (FHIR `patch` interaction).
*
* Sends `Content-Type: application/json-patch+json`.
*
* @param params.resourceType - FHIR resource type.
* @param params.id - Resource id.
* @param params.jsonPatch - Array of RFC 6902 patch operations.
* @param params.options - Per-request options.
* @returns The patched resource.
* @throws If `resourceType` is invalid or the server returns an error.
*/
patch({ resourceType, id, jsonPatch, options = {} }) {
if (!validResourceType(resourceType))
throw new Error(`Invalid resourceType: ${resourceType}`);
const headers = { ...options.headers, 'Content-Type': 'application/json-patch+json' };
return this.httpClient.patch(`${resourceType}/${id}`, jsonPatch, { ...options, headers });
}
/**
* Submit a batch Bundle (FHIR `batch` interaction).
*
* The `body.type` field must be `'batch'`. Each entry is processed
* independently; partial failures are reported per-entry in the response.
*
* @param params.body - A FHIR Bundle resource with `type: 'batch'`.
* @param params.options - Per-request options.
* @returns The batch response Bundle.
*/
batch({ body, options }) {
return this.httpClient.post('/', body, options);
}
/**
* Submit a transaction Bundle (FHIR `transaction` interaction).
*
* The `body.type` field must be `'transaction'`. All entries succeed or
* fail together (atomic).
*
* @param params.body - A FHIR Bundle resource with `type: 'transaction'`.
* @param params.options - Per-request options.
* @returns The transaction response Bundle.
*/
transaction({ body, options }) {
return this.httpClient.post('/', body, options);
}
/**
* Invoke a FHIR operation (`$name`) at the system, type, or instance level.
*
* @param params.name - Operation name with or without leading `$`.
* @param params.resourceType - Scopes the operation to a resource type (type/instance level).
* @param params.id - Scopes the operation to a specific instance (instance level).
* @param params.method - `'GET'` or `'POST'` (default: `'POST'`).
* @param params.input - Parameters resource (POST) or query parameters (GET).
* @param params.options - Per-request options.
* @returns The operation output resource.
*/
operation({ name, resourceType, id, method = 'POST', input, options = {} }) {
const urlParts = ['/'];
if (resourceType) {
if (!validResourceType(resourceType))
throw new Error(`Invalid resourceType: ${resourceType}`);
urlParts.push(`${resourceType}/`);
}
if (id)
urlParts.push(`${id}/`);
urlParts.push(name.startsWith('$') ? name : `$${name}`);
const url = urlParts.join('');
if (method === 'POST') {
return this.httpClient.post(url, input, options);
}
const getUrl = input ? `${url}?${createQueryString(input)}` : url;
return this.httpClient.get(getUrl, options);
}
/**
* Return the next page of a Bundle search result.
*
* @param params.bundle - A Bundle with a `link` array containing a `next` relation.
* @param params.options - Per-request options.
* @returns The next page Bundle, or `undefined` if there is no next page.
*/
nextPage({ bundle, options }) {
return this.pagination.nextPage(bundle, options);
}
/**
* Return the previous page of a Bundle search result.
*
* @param params.bundle - A Bundle with a `link` array containing a `previous` relation.
* @param params.options - Per-request options.
* @returns The previous page Bundle, or `undefined` if there is no previous page.
*/
prevPage({ bundle, options }) {
return this.pagination.prevPage(bundle, options);
}
/**
* Search for resources. Dispatches to the appropriate search variant based on parameters:
*
* - `compartment + resourceType` → compartment search
* - `resourceType` only → type-level search
* - `searchParams` only → system-wide search
*
* @param params.resourceType - FHIR resource type.
* @param params.compartment - Compartment `{ resourceType, id }`.
* @param params.searchParams - Search query parameters.
* @param params.options - Per-request options. Set `postSearch: true` to use POST.
* @returns A search result Bundle.
* @throws If `resourceType` is invalid or insufficient parameters are provided.
*/
search({ resourceType, compartment, searchParams, options } = {}) {
if (resourceType && !validResourceType(resourceType)) {
throw new Error(`Invalid resourceType: ${resourceType}`);
}
if (compartment && resourceType) {
return this.compartmentSearch({ resourceType, compartment, searchParams, options });
}
if (resourceType) {
return this.resourceSearch({ resourceType, searchParams, options });
}
if (searchParams && Object.keys(searchParams).length > 0) {
return this.systemSearch({ searchParams, options });
}
throw new Error('search requires either searchParams or a resourceType');
}
/**
* Search within a specific resource type (FHIR type-level search).
*
* @param params.resourceType - FHIR resource type.
* @param params.searchParams - Query parameters.
* @param params.options - Per-request options. Set `postSearch: true` to POST to `_search`.
* @returns A search result Bundle.
*/
resourceSearch({ resourceType, searchParams, options = {} }) {
if (!validResourceType(resourceType))
throw new Error(`Invalid resourceType: ${resourceType}`);
const searchPath = options.postSearch ? `${resourceType}/_search` : resourceType;
return this.baseSearch({ searchPath, searchParams, options });
}
/**
* System-wide search across all resource types.
*
* @param params.searchParams - Query parameters.
* @param params.options - Per-request options. Set `postSearch: true` to POST to `/_search`.
* @returns A search result Bundle.
*/
systemSearch({ searchParams, options = {} } = {}) {
const searchPath = options.postSearch ? '/_search' : '/';
return this.baseSearch({ searchPath, searchParams, options });
}
/**
* Search within a FHIR compartment.
*
* @param params.resourceType - Resource type to search within the compartment.
* @param params.compartment - Compartment `{ resourceType, id }`.
* @param params.searchParams - Query parameters.
* @param params.options - Per-request options.
* @returns A search result Bundle.
*/
compartmentSearch({ resourceType, compartment, searchParams, options = {}, }) {
if (!validResourceType(resourceType))
throw new Error(`Invalid resourceType: ${resourceType}`);
if (!validResourceType(compartment.resourceType)) {
throw new Error(`Invalid compartment resourceType: ${compartment.resourceType}`);
}
let searchPath = `/${compartment.resourceType}/${compartment.id}/${resourceType}`;
if (options.postSearch)
searchPath += '/_search';
return this.baseSearch({ searchPath, searchParams, options });
}
baseSearch({ searchPath, searchParams, options = {}, }) {
if (options.postSearch) {
const query = createQueryString(searchParams) ?? '';
const headers = { 'Content-Type': 'application/x-www-form-urlencoded', ...options.headers };
return this.httpClient.post(searchPath, query, { ...options, headers });
}
const query = createQueryString(searchParams);
const url = query ? `${searchPath}?${query}` : searchPath;
return this.httpClient.get(url, options);
}
/**
* Retrieve history. Dispatches to instance-, type-, or system-level history
* based on the parameters provided.
*
* @param params.resourceType - Scopes to type or instance history.
* @param params.id - Combined with `resourceType` for instance history.
* @param params.options - Per-request options.
* @returns A history Bundle.
*/
history({ resourceType, id, options } = {}) {
if (resourceType && !validResourceType(resourceType)) {
throw new Error(`Invalid resourceType: ${resourceType}`);
}
if (id && resourceType)
return this.resourceHistory({ resourceType, id, options });
if (resourceType)
return this.typeHistory({ resourceType, options });
return this.systemHistory({ options });
}
/**
* Instance-level history for a specific resource.
*
* @param params.resourceType - FHIR resource type.
* @param params.id - Resource id.
* @param params.options - Per-request options.
* @returns A history Bundle.
*/
resourceHistory({ resourceType, id, options, }) {
if (!validResourceType(resourceType))
throw new Error(`Invalid resourceType: ${resourceType}`);
return this.httpClient.get(`${resourceType}/${id}/_history`, options);
}
/**
* Type-level history for all instances of a resource type.
*
* @param params.resourceType - FHIR resource type.
* @param params.options - Per-request options.
* @returns A history Bundle.
*/
typeHistory({ resourceType, options }) {
if (!validResourceType(resourceType))
throw new Error(`Invalid resourceType: ${resourceType}`);
return this.httpClient.get(`${resourceType}/_history`, options);
}
/**
* System-level history across all resource types.
*
* @param params.options - Per-request options.
* @returns A history Bundle.
*/
systemHistory({ options } = {}) {
return this.httpClient.get('/_history', options);
}
}
//# sourceMappingURL=client.js.map