@oystehr/sdk 4.3.8 → 4.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -41
- package/dist/cjs/client/client.cjs +137 -5
- package/dist/cjs/client/client.cjs.map +1 -1
- package/dist/cjs/client/client.d.ts +28 -3
- package/dist/cjs/index.min.cjs +1 -1
- package/dist/cjs/index.min.cjs.map +1 -1
- package/dist/cjs/resources/classes/fhir-ext.cjs +108 -17
- package/dist/cjs/resources/classes/fhir-ext.cjs.map +1 -1
- package/dist/cjs/resources/classes/fhir-ext.d.ts +67 -12
- package/dist/cjs/resources/classes/fhir.cjs +3 -0
- package/dist/cjs/resources/classes/fhir.cjs.map +1 -1
- package/dist/cjs/resources/classes/fhir.d.ts +3 -0
- package/dist/cjs/resources/types/fhir.d.ts +67 -0
- package/dist/esm/client/client.d.ts +28 -3
- package/dist/esm/client/client.js +137 -5
- package/dist/esm/client/client.js.map +1 -1
- package/dist/esm/index.min.js +1 -1
- package/dist/esm/index.min.js.map +1 -1
- package/dist/esm/resources/classes/fhir-ext.d.ts +67 -12
- package/dist/esm/resources/classes/fhir-ext.js +107 -19
- package/dist/esm/resources/classes/fhir-ext.js.map +1 -1
- package/dist/esm/resources/classes/fhir.d.ts +3 -0
- package/dist/esm/resources/classes/fhir.js +4 -1
- package/dist/esm/resources/classes/fhir.js.map +1 -1
- package/dist/esm/resources/types/fhir.d.ts +67 -0
- package/package.json +1 -1
- package/src/client/client.ts +214 -7
- package/src/resources/classes/fhir-ext.ts +278 -38
- package/src/resources/classes/fhir.ts +3 -0
- package/src/resources/types/fhir.ts +88 -0
|
@@ -117,4 +117,71 @@ export type BatchInputRequest<F extends FhirResource> = BatchInputGetRequest | B
|
|
|
117
117
|
export interface BatchInput<F extends FhirResource> {
|
|
118
118
|
requests: BatchInputRequest<F>[];
|
|
119
119
|
}
|
|
120
|
+
export interface FhirAsyncJobHandle {
|
|
121
|
+
jobId: string;
|
|
122
|
+
contentLocation: string;
|
|
123
|
+
mode: FhirAsyncResponseMode;
|
|
124
|
+
}
|
|
125
|
+
export type FhirResponseMode = 'sync' | 'async-bundle' | 'async-bulk';
|
|
126
|
+
export type FhirAsyncResponseMode = 'bundle' | 'bulk';
|
|
127
|
+
export interface FhirAsyncCompletionBundleEntry<T extends FhirResource> {
|
|
128
|
+
response?: {
|
|
129
|
+
status?: string;
|
|
130
|
+
outcome?: OperationOutcome;
|
|
131
|
+
};
|
|
132
|
+
resource?: T | OperationOutcome;
|
|
133
|
+
}
|
|
134
|
+
export type FhirAsyncCompletionBundle<T extends FhirResource> = EntrylessFhirBundle<T | OperationOutcome> & {
|
|
135
|
+
resourceType: 'Bundle';
|
|
136
|
+
type: 'batch-response';
|
|
137
|
+
entry?: Array<FhirAsyncCompletionBundleEntry<T>>;
|
|
138
|
+
};
|
|
139
|
+
export interface FhirAsyncJobInProgress {
|
|
140
|
+
status: 202;
|
|
141
|
+
xProgress?: string;
|
|
142
|
+
retryAfter?: string;
|
|
143
|
+
}
|
|
144
|
+
export interface FhirAsyncJobCompletedBundle<T extends FhirResource = FhirResource> {
|
|
145
|
+
status: 200;
|
|
146
|
+
mode: 'bundle';
|
|
147
|
+
bundle: FhirAsyncCompletionBundle<T>;
|
|
148
|
+
interactionStatus?: string;
|
|
149
|
+
resource?: T | OperationOutcome;
|
|
150
|
+
outcome?: OperationOutcome;
|
|
151
|
+
}
|
|
152
|
+
export interface FhirAsyncBulkOutputFile {
|
|
153
|
+
type: string;
|
|
154
|
+
url: string;
|
|
155
|
+
}
|
|
156
|
+
export interface FhirAsyncBulkManifest {
|
|
157
|
+
transactionTime: string;
|
|
158
|
+
request: string;
|
|
159
|
+
requiresAccessToken: boolean;
|
|
160
|
+
output: FhirAsyncBulkOutputFile[];
|
|
161
|
+
error: FhirAsyncBulkOutputFile[];
|
|
162
|
+
deleted?: FhirAsyncBulkOutputFile[];
|
|
163
|
+
extension?: Record<string, unknown>;
|
|
164
|
+
}
|
|
165
|
+
export interface FhirAsyncJobCompletedBulk {
|
|
166
|
+
status: 200;
|
|
167
|
+
mode: 'bulk';
|
|
168
|
+
manifest: FhirAsyncBulkManifest;
|
|
169
|
+
}
|
|
170
|
+
export interface FhirAsyncJobExpired {
|
|
171
|
+
status: 410;
|
|
172
|
+
}
|
|
173
|
+
export interface FhirAsyncJobNotFound {
|
|
174
|
+
status: 404;
|
|
175
|
+
}
|
|
176
|
+
export interface FhirAsyncJobUnexpected {
|
|
177
|
+
status: Exclude<number, 200 | 202 | 404 | 410>;
|
|
178
|
+
body: unknown;
|
|
179
|
+
}
|
|
180
|
+
export type FhirAsyncJobStatus<T extends FhirResource = FhirResource> = FhirAsyncJobInProgress | FhirAsyncJobCompletedBundle<T> | FhirAsyncJobCompletedBulk | FhirAsyncJobExpired | FhirAsyncJobNotFound | FhirAsyncJobUnexpected;
|
|
181
|
+
export interface FhirAsyncWaitOptions {
|
|
182
|
+
/** Poll interval in milliseconds. Defaults to 1000 (1 second). */
|
|
183
|
+
pollIntervalMs: number;
|
|
184
|
+
/** Maximum wait time in milliseconds before timing out. Defaults to 900000 (15 minutes). */
|
|
185
|
+
timeoutMs: number;
|
|
186
|
+
}
|
|
120
187
|
export {};
|
package/package.json
CHANGED
package/src/client/client.ts
CHANGED
|
@@ -2,7 +2,16 @@ import { v4 as uuidv4, validate as uuidValidate } from 'uuid';
|
|
|
2
2
|
import { OystehrConfig } from '../config';
|
|
3
3
|
import { OystehrFHIRError, OystehrSdkError } from '../errors';
|
|
4
4
|
import { Logger } from '../logger';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
FhirAsyncBulkManifest,
|
|
7
|
+
FhirAsyncCompletionBundle,
|
|
8
|
+
FhirAsyncJobHandle,
|
|
9
|
+
FhirAsyncJobStatus,
|
|
10
|
+
FhirBundle,
|
|
11
|
+
FhirResource,
|
|
12
|
+
FhirResponseMode,
|
|
13
|
+
OperationOutcome,
|
|
14
|
+
} from '../resources/types';
|
|
6
15
|
|
|
7
16
|
type HttpMethod = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace';
|
|
8
17
|
export const defaultProjectApiUrl = 'https://project-api.zapehr.com/v1';
|
|
@@ -41,10 +50,19 @@ export interface OystehrClientRequest {
|
|
|
41
50
|
* Unique identifier for this request.
|
|
42
51
|
*/
|
|
43
52
|
requestId?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Optional execution mode for FHIR requests that support async behavior.
|
|
55
|
+
* Defaults to `sync` when omitted.
|
|
56
|
+
*/
|
|
57
|
+
mode?: FhirResponseMode;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
interface InternalClientRequest extends OystehrClientRequest {
|
|
47
61
|
ifMatch?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Internal-only: returns raw response metadata ({ status, headers, body }) instead of throwing on non-2xx statuses.
|
|
64
|
+
*/
|
|
65
|
+
rawResponse?: boolean;
|
|
48
66
|
}
|
|
49
67
|
|
|
50
68
|
type FhirData<T extends FhirResource> = T | T[] | FhirBundle<T>;
|
|
@@ -75,14 +93,22 @@ export class SDKResource {
|
|
|
75
93
|
};
|
|
76
94
|
}
|
|
77
95
|
|
|
78
|
-
protected fhirRequest<T
|
|
79
|
-
|
|
96
|
+
protected fhirRequest<T = FhirResource>(
|
|
97
|
+
path: string,
|
|
98
|
+
method: string
|
|
99
|
+
): {
|
|
100
|
+
(params: any, request: InternalClientRequest & { rawResponse: true }, requestMode?: FhirResponseMode): Promise<
|
|
101
|
+
RawFetcherResponse<T>
|
|
102
|
+
>;
|
|
103
|
+
(params: any, request?: InternalClientRequest, requestMode?: FhirResponseMode): Promise<T>;
|
|
104
|
+
} {
|
|
105
|
+
return async (params: any, request?: InternalClientRequest, requestMode?: FhirResponseMode): Promise<any> => {
|
|
80
106
|
try {
|
|
81
107
|
const baseUrlThunk = (): string => this.config.services?.fhirApiUrl ?? defaultFhirApiUrl;
|
|
82
108
|
const configThunk = (): OystehrConfig => this.config;
|
|
83
109
|
const loggerThunk = (): Logger => this.logger;
|
|
84
110
|
// must await here to catch
|
|
85
|
-
return await fetcher(baseUrlThunk, configThunk, loggerThunk, path, method)(params, request);
|
|
111
|
+
return await fetcher(baseUrlThunk, configThunk, loggerThunk, path, method)(params, request, requestMode);
|
|
86
112
|
} catch (err: unknown) {
|
|
87
113
|
// FHIR API error messages are JSON strings
|
|
88
114
|
const fullError = err as { message: string | Record<string, any>; code: number; cause?: unknown };
|
|
@@ -100,13 +126,170 @@ export class SDKResource {
|
|
|
100
126
|
}
|
|
101
127
|
};
|
|
102
128
|
}
|
|
129
|
+
|
|
130
|
+
protected async startAsyncJob(
|
|
131
|
+
path: string,
|
|
132
|
+
method: string,
|
|
133
|
+
params: Record<string, unknown>,
|
|
134
|
+
requestMode: FhirResponseMode,
|
|
135
|
+
request?: InternalClientRequest
|
|
136
|
+
): Promise<FhirAsyncJobHandle> {
|
|
137
|
+
const mode = requestMode === 'async-bulk' ? 'bulk' : 'bundle';
|
|
138
|
+
const asyncPath = requestMode === 'async-bulk' ? this.appendBulkOutputFormat(path) : path;
|
|
139
|
+
|
|
140
|
+
const raw = await this.fhirRequest(asyncPath, method)(
|
|
141
|
+
params,
|
|
142
|
+
{
|
|
143
|
+
...request,
|
|
144
|
+
rawResponse: true,
|
|
145
|
+
},
|
|
146
|
+
requestMode
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (raw.status !== 202) {
|
|
150
|
+
throw new OystehrSdkError({
|
|
151
|
+
message: `Expected start async job to return 202 Accepted, received ${raw.status}`,
|
|
152
|
+
code: raw.status,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const contentLocation = this.readHeader(raw.headers, 'content-location');
|
|
157
|
+
if (!contentLocation) {
|
|
158
|
+
throw new OystehrSdkError({
|
|
159
|
+
message: 'Start Async job response missing Content-Location header',
|
|
160
|
+
code: 500,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const jobId = this.parseAsyncJobId(contentLocation);
|
|
165
|
+
if (!jobId) {
|
|
166
|
+
throw new OystehrSdkError({
|
|
167
|
+
message: `Could not parse async job id from Content-Location: ${contentLocation}`,
|
|
168
|
+
code: 500,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { jobId, contentLocation, mode };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
protected async fetchAsyncJobStatus<T extends FhirResource>(
|
|
176
|
+
jobId: string,
|
|
177
|
+
request?: OystehrClientRequest
|
|
178
|
+
): Promise<FhirAsyncJobStatus<T>> {
|
|
179
|
+
const raw = await this.fhirRequest(`/async-job/${jobId}`, 'GET')(
|
|
180
|
+
{},
|
|
181
|
+
{
|
|
182
|
+
...request,
|
|
183
|
+
rawResponse: true,
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (raw.status === 202) {
|
|
188
|
+
return {
|
|
189
|
+
status: 202,
|
|
190
|
+
xProgress: this.readHeader(raw.headers, 'x-progress'),
|
|
191
|
+
retryAfter: this.readHeader(raw.headers, 'retry-after'),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (raw.status === 410) {
|
|
196
|
+
return { status: 410 };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (raw.status === 404) {
|
|
200
|
+
return { status: 404 };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (raw.status === 200) {
|
|
204
|
+
if (this.isBulkManifest(raw.body)) {
|
|
205
|
+
return {
|
|
206
|
+
status: 200,
|
|
207
|
+
mode: 'bulk',
|
|
208
|
+
manifest: raw.body,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const bundle = raw.body as FhirAsyncCompletionBundle<T>;
|
|
213
|
+
if (bundle?.resourceType === 'Bundle' && bundle?.type === 'batch-response') {
|
|
214
|
+
const entry0 = bundle.entry?.[0];
|
|
215
|
+
const interactionStatus = entry0?.response?.status;
|
|
216
|
+
const resource = entry0?.resource;
|
|
217
|
+
const outcome = entry0?.response?.outcome;
|
|
218
|
+
return {
|
|
219
|
+
status: 200,
|
|
220
|
+
mode: 'bundle',
|
|
221
|
+
bundle,
|
|
222
|
+
interactionStatus,
|
|
223
|
+
resource,
|
|
224
|
+
outcome,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
status: 200,
|
|
230
|
+
body: raw.body,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
status: raw.status,
|
|
236
|
+
body: raw.body,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private readHeader(headers: Record<string, string>, name: string): string | undefined {
|
|
241
|
+
const direct = headers[name];
|
|
242
|
+
if (direct != null) {
|
|
243
|
+
return direct;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const lower = name.toLowerCase();
|
|
247
|
+
const key = Object.keys(headers).find((h) => h.toLowerCase() === lower);
|
|
248
|
+
return key ? headers[key] : undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private parseAsyncJobId(contentLocation: string): string | undefined {
|
|
252
|
+
const segments = contentLocation.split('/').filter(Boolean);
|
|
253
|
+
const asyncJobIndex = segments.lastIndexOf('async-job');
|
|
254
|
+
if (asyncJobIndex < 0 || asyncJobIndex + 1 >= segments.length) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return segments[asyncJobIndex + 1];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private appendBulkOutputFormat(path: string): string {
|
|
262
|
+
const separator = path.includes('?') ? '&' : '?';
|
|
263
|
+
return `${path}${separator}_outputFormat=${encodeURIComponent('application/fhir+ndjson')}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private isBulkManifest(body: unknown): body is FhirAsyncBulkManifest {
|
|
267
|
+
if (body == null || typeof body !== 'object') {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const maybe = body as Record<string, unknown>;
|
|
272
|
+
return (
|
|
273
|
+
typeof maybe.transactionTime === 'string' &&
|
|
274
|
+
typeof maybe.request === 'string' &&
|
|
275
|
+
typeof maybe.requiresAccessToken === 'boolean' &&
|
|
276
|
+
Array.isArray(maybe.output) &&
|
|
277
|
+
Array.isArray(maybe.error)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
103
280
|
}
|
|
104
281
|
|
|
105
282
|
export type FetcherError = { message: string; code: number };
|
|
106
283
|
export type FetcherResponse = any;
|
|
284
|
+
export type RawFetcherResponse<T = unknown> = {
|
|
285
|
+
status: number;
|
|
286
|
+
headers: Record<string, string>;
|
|
287
|
+
body: T | null;
|
|
288
|
+
};
|
|
107
289
|
export type FetcherFunction = (
|
|
108
290
|
params?: Record<string, any> | [any] | InternalClientRequest,
|
|
109
|
-
request?: InternalClientRequest
|
|
291
|
+
request?: InternalClientRequest,
|
|
292
|
+
requestMode?: FhirResponseMode
|
|
110
293
|
) => Promise<FetcherResponse>;
|
|
111
294
|
|
|
112
295
|
function isInternalClientRequest(request: Record<string, any>): request is InternalClientRequest {
|
|
@@ -115,10 +298,19 @@ function isInternalClientRequest(request: Record<string, any>): request is Inter
|
|
|
115
298
|
('projectId' in request && uuidValidate(request.projectId)) ||
|
|
116
299
|
('contentType' in request && request.contentType?.split('/').length === 2) ||
|
|
117
300
|
'requestId' in request ||
|
|
118
|
-
('ifMatch' in request && request.ifMatch.startsWith('W/"'))
|
|
301
|
+
('ifMatch' in request && request.ifMatch.startsWith('W/"')) ||
|
|
302
|
+
'mode' in request ||
|
|
303
|
+
'rawResponse' in request
|
|
119
304
|
);
|
|
120
305
|
}
|
|
121
306
|
|
|
307
|
+
function getPreferHeaderFromMode(mode: FhirResponseMode | undefined): string | undefined {
|
|
308
|
+
if (mode === 'async-bundle' || mode === 'async-bulk') {
|
|
309
|
+
return 'respond-async';
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
122
314
|
/**
|
|
123
315
|
* Parse XML response in format <response><status>...</status><output>...</output></response>
|
|
124
316
|
*/
|
|
@@ -151,7 +343,8 @@ function fetcher(
|
|
|
151
343
|
): FetcherFunction {
|
|
152
344
|
return async (
|
|
153
345
|
params?: Record<string, unknown> | [any] | InternalClientRequest,
|
|
154
|
-
request?: InternalClientRequest
|
|
346
|
+
request?: InternalClientRequest,
|
|
347
|
+
requestMode?: FhirResponseMode
|
|
155
348
|
): Promise<FetcherResponse> => {
|
|
156
349
|
// this function supports multiple signatures. fetcher(baseUrl, path, method)(params, request) or fetcher(baseUrl, path, method)(request)
|
|
157
350
|
// or fetcher(baseUrl, path, method)(params) or fetcher(baseUrl, path, method)(). the types for this are handled by Client<Path, Methods>
|
|
@@ -214,6 +407,8 @@ function fetcher(
|
|
|
214
407
|
requestId: requestCtx?.requestId,
|
|
215
408
|
});
|
|
216
409
|
|
|
410
|
+
const preferHeader = getPreferHeaderFromMode(requestMode);
|
|
411
|
+
|
|
217
412
|
const headers: Record<string, string> = Object.assign(
|
|
218
413
|
projectId
|
|
219
414
|
? {
|
|
@@ -224,6 +419,7 @@ function fetcher(
|
|
|
224
419
|
{
|
|
225
420
|
'content-type': requestCtx?.contentType ?? 'application/json',
|
|
226
421
|
},
|
|
422
|
+
preferHeader ? { Prefer: preferHeader } : {},
|
|
227
423
|
accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
|
|
228
424
|
requestCtx?.ifMatch ? { 'If-Match': requestCtx.ifMatch } : {},
|
|
229
425
|
{ 'x-oystehr-request-id': requestCtx?.requestId }
|
|
@@ -301,6 +497,17 @@ function fetcher(
|
|
|
301
497
|
url,
|
|
302
498
|
requestId: requestCtx?.requestId,
|
|
303
499
|
});
|
|
500
|
+
if (requestCtx?.rawResponse) {
|
|
501
|
+
const headersRecord: Record<string, string> = {};
|
|
502
|
+
response.headers.forEach((value, key) => {
|
|
503
|
+
headersRecord[key] = value;
|
|
504
|
+
});
|
|
505
|
+
return {
|
|
506
|
+
status: response.status,
|
|
507
|
+
headers: headersRecord,
|
|
508
|
+
body: responseJson ?? responseBody,
|
|
509
|
+
} as RawFetcherResponse;
|
|
510
|
+
}
|
|
304
511
|
const isError = !response.ok || response.status >= 400;
|
|
305
512
|
if (isError) {
|
|
306
513
|
const errObj = {
|