@oystehr/sdk 4.3.3 → 4.3.4
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 +1 -1
- package/dist/cjs/config.d.ts +15 -0
- 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 +290 -19
- package/dist/cjs/resources/classes/fhir-ext.cjs.map +1 -1
- package/dist/cjs/resources/classes/index.cjs +7 -0
- package/dist/cjs/resources/classes/index.cjs.map +1 -1
- package/dist/cjs/resources/types/RcmListPayersParams.d.ts +4 -0
- package/dist/cjs/resources/types/RcmListPayersResponse.d.ts +9 -1
- package/dist/cjs/resources/types/fhir.d.ts +13 -3
- package/dist/esm/config.d.ts +15 -0
- package/dist/esm/index.min.js +1 -1
- package/dist/esm/index.min.js.map +1 -1
- package/dist/esm/resources/classes/fhir-ext.js +290 -19
- package/dist/esm/resources/classes/fhir-ext.js.map +1 -1
- package/dist/esm/resources/classes/index.js +7 -0
- package/dist/esm/resources/classes/index.js.map +1 -1
- package/dist/esm/resources/types/RcmListPayersParams.d.ts +4 -0
- package/dist/esm/resources/types/RcmListPayersResponse.d.ts +9 -1
- package/dist/esm/resources/types/fhir.d.ts +13 -3
- package/package.json +1 -1
- package/src/config.ts +16 -0
- package/src/resources/classes/fhir-ext.ts +322 -19
- package/src/resources/classes/index.ts +7 -0
- package/src/resources/types/RcmListPayersParams.ts +4 -0
- package/src/resources/types/RcmListPayersResponse.ts +9 -1
- package/src/resources/types/fhir.ts +16 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Operation } from 'fast-json-patch';
|
|
2
|
-
import type { Binary as BinaryR4B, Bundle as BundleR4B, BundleEntry as BundleEntryR4B, FhirResource as FhirResourceR4B, OperationOutcome as OperationOutcomeR4B } from 'fhir/r4b';
|
|
3
|
-
import type { Binary as BinaryR5, Bundle as BundleR5, BundleEntry as BundleEntryR5, FhirResource as FhirResourceR5, OperationOutcome as OperationOutcomeR5 } from 'fhir/r5';
|
|
2
|
+
import type { Binary as BinaryR4B, Bundle as BundleR4B, BundleEntry as BundleEntryR4B, Coding as CodingR4B, FhirResource as FhirResourceR4B, OperationOutcome as OperationOutcomeR4B, Resource as ResourceR4B } from 'fhir/r4b';
|
|
3
|
+
import type { Binary as BinaryR5, Bundle as BundleR5, BundleEntry as BundleEntryR5, Coding as CodingR5, FhirResource as FhirResourceR5, OperationOutcome as OperationOutcomeR5, Resource as ResourceR5 } from 'fhir/r5';
|
|
4
4
|
export type FhirResource = FhirResourceR4B | FhirResourceR5;
|
|
5
5
|
export type FhirBundle<F extends FhirResource> = F extends FhirResourceR4B ? BundleR4B<F> : BundleR5<F>;
|
|
6
6
|
export type EntrylessFhirBundle<F extends FhirResource> = F extends FhirResourceR4B ? Omit<BundleR4B<F>, 'entry'> : Omit<BundleR5<F>, 'entry'>;
|
|
@@ -28,6 +28,8 @@ export type TransactionBundle<F extends FhirResource> = EntrylessFhirBundle<F> &
|
|
|
28
28
|
export type BundleEntry<F extends FhirResource> = F extends FhirResourceR4B ? BundleEntryR4B<F> : BundleEntryR5<F>;
|
|
29
29
|
export type Binary<F extends FhirResource> = F extends FhirResourceR4B ? BinaryR4B : BinaryR5;
|
|
30
30
|
export type OperationOutcome = OperationOutcomeR4B | OperationOutcomeR5;
|
|
31
|
+
export type Coding = CodingR4B | CodingR5;
|
|
32
|
+
export type Resource = ResourceR4B | ResourceR5;
|
|
31
33
|
export type SearchParam = {
|
|
32
34
|
name: string;
|
|
33
35
|
value: string | number;
|
|
@@ -89,6 +91,14 @@ export interface BatchInputPostRequest<F extends FhirResource> extends BatchInpu
|
|
|
89
91
|
resource: F;
|
|
90
92
|
fullUrl?: string;
|
|
91
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* A batch POST to a `_search` endpoint (e.g. `Patient/_search`). Search parameters
|
|
96
|
+
* may be appended to the URL as query parameters. Unlike a create POST there is no
|
|
97
|
+
* resource body.
|
|
98
|
+
*/
|
|
99
|
+
export interface BatchInputSearchPostRequest extends BatchInputBaseRequest {
|
|
100
|
+
method: 'POST';
|
|
101
|
+
}
|
|
92
102
|
export interface BatchInputBinaryPatchRequest<F extends FhirResource> extends BatchInputBaseRequest {
|
|
93
103
|
method: 'PATCH';
|
|
94
104
|
ifMatch?: string;
|
|
@@ -100,7 +110,7 @@ export interface BatchInputJSONPatchRequest extends BatchInputBaseRequest {
|
|
|
100
110
|
operations: Operation[];
|
|
101
111
|
}
|
|
102
112
|
export type BatchInputPatchRequest<F extends FhirResource> = BatchInputBinaryPatchRequest<F> | BatchInputJSONPatchRequest;
|
|
103
|
-
export type BatchInputRequest<F extends FhirResource> = BatchInputGetRequest | BatchInputHeadRequest | BatchInputPutRequest<F> | BatchInputPatchRequest<F> | BatchInputPostRequest<F> | BatchInputDeleteRequest;
|
|
113
|
+
export type BatchInputRequest<F extends FhirResource> = BatchInputGetRequest | BatchInputHeadRequest | BatchInputPutRequest<F> | BatchInputPatchRequest<F> | BatchInputPostRequest<F> | BatchInputSearchPostRequest | BatchInputDeleteRequest;
|
|
104
114
|
export interface BatchInput<F extends FhirResource> {
|
|
105
115
|
requests: BatchInputRequest<F>[];
|
|
106
116
|
}
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Coding } from './resources/types/fhir';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Configuration for the Oystehr SDK client
|
|
3
5
|
*/
|
|
@@ -62,6 +64,20 @@ export interface OystehrConfig {
|
|
|
62
64
|
*/
|
|
63
65
|
retryOn?: number[];
|
|
64
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Optional workspace tag configuration. When set, all FHIR searches will be
|
|
69
|
+
* filtered to only return resources with this tag, and all FHIR mutations
|
|
70
|
+
* (create, update, patch) will have this tag injected automatically.
|
|
71
|
+
* Mutually exclusive with `ignoreTags`.
|
|
72
|
+
*/
|
|
73
|
+
workspaceTag?: Coding;
|
|
74
|
+
/**
|
|
75
|
+
* Optional list of tags to ignore. When set, all FHIR searches will be
|
|
76
|
+
* filtered to exclude resources matching any of these tags, and mutations
|
|
77
|
+
* will throw an error if the resource carries any of these tags.
|
|
78
|
+
* Mutually exclusive with `workspaceTag`.
|
|
79
|
+
*/
|
|
80
|
+
ignoreTags?: Coding[];
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
let globalConfig: OystehrConfig;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Operation } from 'fast-json-patch';
|
|
1
2
|
import { Address as AddressR4B, HumanName as HumanNameR4B } from 'fhir/r4b';
|
|
2
3
|
import { Address as AddressR5, HumanName as HumanNameR5 } from 'fhir/r5';
|
|
3
4
|
import {
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
Binary,
|
|
8
9
|
Bundle,
|
|
9
10
|
BundleEntry,
|
|
11
|
+
Coding,
|
|
10
12
|
FhirBundle,
|
|
11
13
|
FhirCreateParams,
|
|
12
14
|
FhirDeleteParams,
|
|
@@ -19,9 +21,13 @@ import {
|
|
|
19
21
|
FhirSearchParams,
|
|
20
22
|
FhirUpdateParams,
|
|
21
23
|
OperationOutcome,
|
|
24
|
+
Resource,
|
|
25
|
+
SearchParam,
|
|
22
26
|
TransactionBundle,
|
|
23
27
|
} from '../..';
|
|
24
28
|
import { addParamsToSearch, FhirFetcherResponse, OystehrClientRequest, SDKResource } from '../../client/client';
|
|
29
|
+
import { OystehrConfig } from '../../config';
|
|
30
|
+
import { OystehrFHIRError, OystehrSdkError } from '../../errors';
|
|
25
31
|
|
|
26
32
|
// Code adapted from https://github.com/sindresorhus/uint8array-extras
|
|
27
33
|
const MAX_BLOCK_SIZE = 65_535;
|
|
@@ -37,6 +43,209 @@ function stringToBase64(input: string): string {
|
|
|
37
43
|
return base64;
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
function base64ToString(input: string): string {
|
|
47
|
+
const binaryString = globalThis.atob(input);
|
|
48
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
49
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
50
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
51
|
+
}
|
|
52
|
+
return new globalThis.TextDecoder().decode(bytes);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Tag-mode helpers ────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/** Converts a Coding to the FHIR _tag parameter value format "system|code". */
|
|
58
|
+
function codingToTagValue(coding: Coding): string {
|
|
59
|
+
return coding.system ? `${coding.system}|${coding.code ?? ''}` : coding.code ?? '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Returns true if two Codings match on system (treating absent as empty string) and code. */
|
|
63
|
+
function codingMatches(a: Coding, b: Coding): boolean {
|
|
64
|
+
return (a.system ?? '') === (b.system ?? '') && (a.code ?? '') === (b.code ?? '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Appends _tag (tag-workspace mode) or _tag:not (ignore-tags mode) search
|
|
69
|
+
* params to an existing SearchParam array and returns the new array.
|
|
70
|
+
*/
|
|
71
|
+
function applyTagSearchParams(config: OystehrConfig, params: SearchParam[] | undefined): SearchParam[] {
|
|
72
|
+
const out: SearchParam[] = params ? [...params] : [];
|
|
73
|
+
if (config.workspaceTag) {
|
|
74
|
+
out.push({ name: '_tag', value: codingToTagValue(config.workspaceTag) });
|
|
75
|
+
} else if (config.ignoreTags?.length) {
|
|
76
|
+
for (const tag of config.ignoreTags) {
|
|
77
|
+
out.push({ name: '_tag:not', value: codingToTagValue(tag) });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* In ignore-tags mode: throws OystehrSdkError if the resource carries any ignored tag.
|
|
85
|
+
* In tag-workspace mode: injects the workspace tag into resource.meta.tag if not already present.
|
|
86
|
+
* Returns the (possibly cloned) resource.
|
|
87
|
+
*/
|
|
88
|
+
function applyTagToResource<T extends FhirResource>(config: OystehrConfig, resource: T): T {
|
|
89
|
+
const resourceLike = resource as unknown as Resource;
|
|
90
|
+
|
|
91
|
+
if (config.ignoreTags?.length) {
|
|
92
|
+
const resourceTags = (resourceLike.meta?.tag ?? []) as Coding[];
|
|
93
|
+
for (const ignored of config.ignoreTags) {
|
|
94
|
+
if (resourceTags.some((t) => codingMatches(t, ignored))) {
|
|
95
|
+
throw new OystehrSdkError({
|
|
96
|
+
message: `Resource has an ignored tag (system: "${ignored.system ?? ''}", code: "${
|
|
97
|
+
ignored.code ?? ''
|
|
98
|
+
}") and cannot be mutated in ignoreTags mode`,
|
|
99
|
+
code: 400,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return resource;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (config.workspaceTag) {
|
|
107
|
+
const tag = config.workspaceTag;
|
|
108
|
+
const existingTags = (resourceLike.meta?.tag ?? []) as Coding[];
|
|
109
|
+
if (!existingTags.some((t) => codingMatches(t, tag))) {
|
|
110
|
+
return {
|
|
111
|
+
...resource,
|
|
112
|
+
meta: {
|
|
113
|
+
...resourceLike.meta,
|
|
114
|
+
tag: [...existingTags, tag],
|
|
115
|
+
},
|
|
116
|
+
} as T;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return resource;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* In ignore-tags mode: throws OystehrSdkError if any operation value resembles an ignored tag.
|
|
125
|
+
* In tag-workspace mode: guards against removal or loss of the workspace tag:
|
|
126
|
+
* - add/replace of the tag array: ensures workspace tag is present in the value
|
|
127
|
+
* - add/replace of meta: ensures workspace tag is present in meta.tag
|
|
128
|
+
* - remove of /meta/tag or /meta: keeps the remove and appends an operation to restore the workspace tag
|
|
129
|
+
* - add of a single tag (path ends in /-): left unchanged (workspace tag already on the resource)
|
|
130
|
+
*/
|
|
131
|
+
function applyTagToPatchOperations(config: OystehrConfig, operations: Operation[]): Operation[] {
|
|
132
|
+
if (config.ignoreTags?.length) {
|
|
133
|
+
for (const op of operations) {
|
|
134
|
+
if (op.op === 'add' || op.op === 'replace' || op.op === 'test') {
|
|
135
|
+
const opValue: unknown = op.value;
|
|
136
|
+
if (opValue !== null && opValue !== undefined && typeof opValue === 'object') {
|
|
137
|
+
const v = opValue as Coding;
|
|
138
|
+
if (v.code !== undefined) {
|
|
139
|
+
for (const ignored of config.ignoreTags) {
|
|
140
|
+
if (codingMatches(v, ignored)) {
|
|
141
|
+
throw new OystehrSdkError({
|
|
142
|
+
message: `Patch operation contains an ignored tag (system: "${ignored.system ?? ''}", code: "${
|
|
143
|
+
ignored.code ?? ''
|
|
144
|
+
}") and cannot be applied in ignoreTags mode`,
|
|
145
|
+
code: 400,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return operations;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (config.workspaceTag) {
|
|
157
|
+
const tag = config.workspaceTag;
|
|
158
|
+
return operations.reduce<Operation[]>((result, op) => {
|
|
159
|
+
// remove /meta/tag — restore workspace tag afterwards
|
|
160
|
+
if (op.op === 'remove' && op.path === '/meta/tag') {
|
|
161
|
+
result.push(op);
|
|
162
|
+
result.push({ op: 'add' as const, path: '/meta/tag', value: [tag] });
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// remove /meta — restore workspace tag afterwards
|
|
167
|
+
if (op.op === 'remove' && op.path === '/meta') {
|
|
168
|
+
result.push(op);
|
|
169
|
+
result.push({ op: 'add' as const, path: '/meta', value: { tag: [tag] } });
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// add/replace the entire tag array — ensure workspace tag is present
|
|
174
|
+
if ((op.op === 'add' || op.op === 'replace') && op.path === '/meta/tag') {
|
|
175
|
+
const tags: Coding[] = Array.isArray(op.value) ? (op.value as Coding[]) : [];
|
|
176
|
+
if (!tags.some((t) => codingMatches(t, tag))) {
|
|
177
|
+
result.push({ ...op, value: [...tags, tag] });
|
|
178
|
+
} else {
|
|
179
|
+
result.push(op);
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// add/replace the entire meta object — ensure workspace tag is present in meta.tag
|
|
185
|
+
if ((op.op === 'add' || op.op === 'replace') && op.path === '/meta') {
|
|
186
|
+
const metaValue: { tag?: Coding[] } =
|
|
187
|
+
op.value != null && typeof op.value === 'object' ? (op.value as { tag?: Coding[] }) : {};
|
|
188
|
+
const tags = metaValue.tag ?? [];
|
|
189
|
+
if (!tags.some((t) => codingMatches(t, tag))) {
|
|
190
|
+
result.push({ ...op, value: { ...metaValue, tag: [...tags, tag] } });
|
|
191
|
+
} else {
|
|
192
|
+
result.push(op);
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// All other operations (including add /meta/tag/- for individual tags) pass through unchanged.
|
|
198
|
+
// Workspace tag is assumed already present on the resource from create/update.
|
|
199
|
+
result.push(op);
|
|
200
|
+
return result;
|
|
201
|
+
}, []);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return operations;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Throws OystehrFHIRError (404) if the retrieved resource violates the tag configuration:
|
|
209
|
+
* - workspaceTag mode: resource must carry the workspace tag
|
|
210
|
+
* - ignoreTags mode: resource must not carry any ignored tag
|
|
211
|
+
*/
|
|
212
|
+
function assertRetrievedResource(config: OystehrConfig, resource: FhirResource): void {
|
|
213
|
+
const resourceLike = resource as unknown as Resource;
|
|
214
|
+
const resourceTags = (resourceLike.meta?.tag ?? []) as Coding[];
|
|
215
|
+
|
|
216
|
+
if (config.workspaceTag) {
|
|
217
|
+
const tag = config.workspaceTag;
|
|
218
|
+
if (!resourceTags.some((t) => codingMatches(t, tag))) {
|
|
219
|
+
throw new OystehrFHIRError({
|
|
220
|
+
error: {
|
|
221
|
+
resourceType: 'OperationOutcome',
|
|
222
|
+
id: 'not-found',
|
|
223
|
+
issue: [{ severity: 'error', code: 'not-found', details: { text: 'Not found' } }],
|
|
224
|
+
},
|
|
225
|
+
code: 404,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (config.ignoreTags?.length) {
|
|
232
|
+
for (const ignored of config.ignoreTags) {
|
|
233
|
+
if (resourceTags.some((t) => codingMatches(t, ignored))) {
|
|
234
|
+
throw new OystehrFHIRError({
|
|
235
|
+
error: {
|
|
236
|
+
resourceType: 'OperationOutcome',
|
|
237
|
+
id: 'not-found',
|
|
238
|
+
issue: [{ severity: 'error', code: 'not-found', details: { text: 'Not found' } }],
|
|
239
|
+
},
|
|
240
|
+
code: 404,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
40
249
|
/**
|
|
41
250
|
* Optional parameter that can be passed to the client methods. It allows
|
|
42
251
|
* overriding the access token or project ID, and setting various headers,
|
|
@@ -64,10 +273,11 @@ export async function search<T extends FhirResource>(
|
|
|
64
273
|
params: FhirSearchParams<T>,
|
|
65
274
|
request?: OystehrClientRequest
|
|
66
275
|
): Promise<FhirFetcherResponse<Bundle<T>>> {
|
|
67
|
-
const { resourceType
|
|
276
|
+
const { resourceType } = params;
|
|
277
|
+
const taggedParams = applyTagSearchParams(this.config, params.params);
|
|
68
278
|
let paramMap: Record<string, (string | number)[]> | undefined;
|
|
69
|
-
if (
|
|
70
|
-
paramMap =
|
|
279
|
+
if (taggedParams.length) {
|
|
280
|
+
paramMap = taggedParams.reduce((acc, param) => {
|
|
71
281
|
if (!acc[param.name]) {
|
|
72
282
|
acc[param.name] = [];
|
|
73
283
|
}
|
|
@@ -96,8 +306,9 @@ export async function create<T extends FhirResource>(
|
|
|
96
306
|
params: FhirCreateParams<T>,
|
|
97
307
|
request?: OystehrClientRequest
|
|
98
308
|
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>>> {
|
|
99
|
-
const
|
|
100
|
-
|
|
309
|
+
const tagged = applyTagToResource(this.config, params);
|
|
310
|
+
const { resourceType } = tagged;
|
|
311
|
+
return this.fhirRequest(`/${resourceType}`, 'POST')(tagged as unknown as Record<string, unknown>, request);
|
|
101
312
|
}
|
|
102
313
|
|
|
103
314
|
export async function get<T extends FhirResource>(
|
|
@@ -105,7 +316,9 @@ export async function get<T extends FhirResource>(
|
|
|
105
316
|
{ resourceType, id }: FhirGetParams<T>,
|
|
106
317
|
request?: OystehrClientRequest
|
|
107
318
|
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>>> {
|
|
108
|
-
|
|
319
|
+
const result = await this.fhirRequest<FhirResourceReturnValue<T>>(`/${resourceType}/${id}`, 'GET')({}, request);
|
|
320
|
+
assertRetrievedResource(this.config, result);
|
|
321
|
+
return result;
|
|
109
322
|
}
|
|
110
323
|
|
|
111
324
|
export async function update<T extends FhirResource>(
|
|
@@ -113,8 +326,9 @@ export async function update<T extends FhirResource>(
|
|
|
113
326
|
params: FhirUpdateParams<T>,
|
|
114
327
|
request?: OystehrFHIRUpdateClientRequest
|
|
115
328
|
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>>> {
|
|
116
|
-
const
|
|
117
|
-
|
|
329
|
+
const tagged = applyTagToResource(this.config, params);
|
|
330
|
+
const { id, resourceType } = tagged;
|
|
331
|
+
return this.fhirRequest(`/${resourceType}/${id}`, 'PUT')(tagged as unknown as Record<string, unknown>, {
|
|
118
332
|
...request,
|
|
119
333
|
ifMatch: request?.optimisticLockingVersionId ? `W/"${request.optimisticLockingVersionId}"` : undefined,
|
|
120
334
|
});
|
|
@@ -125,7 +339,8 @@ export async function patch<T extends FhirResource>(
|
|
|
125
339
|
{ resourceType, id, operations }: FhirPatchParams<T>,
|
|
126
340
|
request?: OystehrFHIRUpdateClientRequest
|
|
127
341
|
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>>> {
|
|
128
|
-
|
|
342
|
+
const taggedOperations = applyTagToPatchOperations(this.config, operations);
|
|
343
|
+
return this.fhirRequest(`/${resourceType}/${id}`, 'PATCH')(taggedOperations, {
|
|
129
344
|
...request,
|
|
130
345
|
contentType: 'application/json-patch+json',
|
|
131
346
|
ifMatch: request?.optimisticLockingVersionId ? `W/"${request.optimisticLockingVersionId}"` : undefined,
|
|
@@ -180,10 +395,50 @@ export async function history<T extends FhirResource>(
|
|
|
180
395
|
return this.fhirRequest(`/${resourceType}/${id}/_history?_total=accurate`, 'GET')({}, request);
|
|
181
396
|
}
|
|
182
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Returns true when a batch GET/HEAD URL is a search (e.g. "Patient" or "Patient?name=foo")
|
|
400
|
+
* rather than a direct retrieval (e.g. "Patient/abc-123").
|
|
401
|
+
* A retrieval URL has a path segment after the resource type that is not a FHIR operation
|
|
402
|
+
* (operations start with "_", e.g. "_search" or "_history").
|
|
403
|
+
*/
|
|
404
|
+
function isBatchSearchUrl(url: string): boolean {
|
|
405
|
+
const path = url.split('?')[0];
|
|
406
|
+
const segments = path.split('/').filter(Boolean);
|
|
407
|
+
// Only one segment → bare resource type, always a search
|
|
408
|
+
if (segments.length <= 1) return true;
|
|
409
|
+
// Two or more segments: second segment is an ID if it doesn't start with '_'
|
|
410
|
+
return segments[1].startsWith('_');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Returns true when a batch POST URL targets a `_search` endpoint (e.g. "Patient/_search"). */
|
|
414
|
+
function isBatchSearchPostUrl(url: string): boolean {
|
|
415
|
+
return url.split('?')[0].endsWith('/_search');
|
|
416
|
+
}
|
|
417
|
+
|
|
183
418
|
function batchInputRequestToBundleEntryItem<T extends FhirResource>(
|
|
184
|
-
request: BatchInputRequest<T
|
|
419
|
+
request: BatchInputRequest<T>,
|
|
420
|
+
config: OystehrConfig
|
|
185
421
|
): BundleEntry<T | Binary<T>> {
|
|
186
|
-
const { method
|
|
422
|
+
const { method } = request;
|
|
423
|
+
let url = request.url;
|
|
424
|
+
|
|
425
|
+
// Inject tag search params into search request URLs before URL encoding.
|
|
426
|
+
// GET/HEAD: only for search URLs (not retrievals like Patient/<id>).
|
|
427
|
+
// POST: only for _search endpoints (not creates).
|
|
428
|
+
if (
|
|
429
|
+
((method === 'GET' || method === 'HEAD') && isBatchSearchUrl(url)) ||
|
|
430
|
+
(method === 'POST' && isBatchSearchPostUrl(url))
|
|
431
|
+
) {
|
|
432
|
+
if (config.workspaceTag) {
|
|
433
|
+
url += (url.includes('?') ? '&' : '?') + `_tag=${codingToTagValue(config.workspaceTag)}`;
|
|
434
|
+
}
|
|
435
|
+
if (config.ignoreTags?.length) {
|
|
436
|
+
for (const tag of config.ignoreTags) {
|
|
437
|
+
url += (url.includes('?') ? '&' : '?') + `_tag:not=${codingToTagValue(tag)}`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
187
442
|
const baseRequest = {
|
|
188
443
|
request: {
|
|
189
444
|
method,
|
|
@@ -222,7 +477,7 @@ function batchInputRequestToBundleEntryItem<T extends FhirResource>(
|
|
|
222
477
|
|
|
223
478
|
// PUT updates require a full resource
|
|
224
479
|
if (method === 'PUT') {
|
|
225
|
-
const
|
|
480
|
+
const resource = applyTagToResource(config, request.resource);
|
|
226
481
|
return {
|
|
227
482
|
request: {
|
|
228
483
|
...baseRequest.request,
|
|
@@ -235,15 +490,24 @@ function batchInputRequestToBundleEntryItem<T extends FhirResource>(
|
|
|
235
490
|
// PATCH can be Binary resource or JSON patch
|
|
236
491
|
if (method === 'PATCH') {
|
|
237
492
|
if ('resource' in request) {
|
|
493
|
+
// Binary patch — decode operations, apply tag transforms, re-encode
|
|
494
|
+
let patchResource = request.resource;
|
|
495
|
+
const binaryData = patchResource.data;
|
|
496
|
+
if (binaryData) {
|
|
497
|
+
const operations = JSON.parse(base64ToString(binaryData)) as Operation[];
|
|
498
|
+
const taggedOperations = applyTagToPatchOperations(config, operations);
|
|
499
|
+
patchResource = { ...patchResource, data: stringToBase64(JSON.stringify(taggedOperations)) } as Binary<T>;
|
|
500
|
+
}
|
|
238
501
|
return {
|
|
239
502
|
request: {
|
|
240
503
|
...baseRequest.request,
|
|
241
504
|
ifMatch: request.ifMatch,
|
|
242
505
|
},
|
|
243
|
-
resource:
|
|
506
|
+
resource: patchResource,
|
|
244
507
|
} as BundleEntry<Binary<T>>;
|
|
245
508
|
}
|
|
246
|
-
const
|
|
509
|
+
const operations = applyTagToPatchOperations(config, request.operations);
|
|
510
|
+
const data = stringToBase64(JSON.stringify(operations));
|
|
247
511
|
return {
|
|
248
512
|
...baseRequest,
|
|
249
513
|
resource: {
|
|
@@ -254,9 +518,15 @@ function batchInputRequestToBundleEntryItem<T extends FhirResource>(
|
|
|
254
518
|
} as BundleEntry<Binary<T>>;
|
|
255
519
|
}
|
|
256
520
|
|
|
521
|
+
// POST _search — no resource body; tag params were already injected into the URL above
|
|
522
|
+
if (method === 'POST' && isBatchSearchPostUrl(url)) {
|
|
523
|
+
return baseRequest as BundleEntry<T>;
|
|
524
|
+
}
|
|
525
|
+
|
|
257
526
|
// POST creates require a full resource
|
|
258
|
-
if (method === 'POST') {
|
|
259
|
-
const
|
|
527
|
+
if (method === 'POST' && 'resource' in request) {
|
|
528
|
+
const resource = applyTagToResource(config, request.resource);
|
|
529
|
+
const { fullUrl } = request;
|
|
260
530
|
return {
|
|
261
531
|
...baseRequest,
|
|
262
532
|
resource: resource as T,
|
|
@@ -284,13 +554,35 @@ export async function batch<BundleContentType extends FhirResource>(
|
|
|
284
554
|
{
|
|
285
555
|
resourceType: 'Bundle',
|
|
286
556
|
type: 'batch',
|
|
287
|
-
entry: input.requests.map(batchInputRequestToBundleEntryItem),
|
|
557
|
+
entry: input.requests.map((req) => batchInputRequestToBundleEntryItem(req, this.config)),
|
|
288
558
|
},
|
|
289
559
|
request
|
|
290
560
|
);
|
|
561
|
+
// Validate each GET/HEAD retrieval entry against the tag config.
|
|
562
|
+
// Violations are replaced with a synthetic 404 OperationOutcome entry; batch entries are independent.
|
|
563
|
+
const rawEntries = resp.entry as Array<BundleEntry<BundleContentType>> | undefined;
|
|
564
|
+
const processedEntries: Array<BundleEntry<BundleContentType>> | undefined =
|
|
565
|
+
this.config.workspaceTag || this.config.ignoreTags?.length
|
|
566
|
+
? rawEntries?.map((entry, i) => {
|
|
567
|
+
const req = input.requests[i];
|
|
568
|
+
if (!req || !entry?.resource) return entry;
|
|
569
|
+
if ((req.method === 'GET' || req.method === 'HEAD') && !isBatchSearchUrl(req.url)) {
|
|
570
|
+
try {
|
|
571
|
+
assertRetrievedResource(this.config, entry.resource);
|
|
572
|
+
} catch (err) {
|
|
573
|
+
if (!(err instanceof OystehrFHIRError)) throw err;
|
|
574
|
+
return {
|
|
575
|
+
request: entry.request,
|
|
576
|
+
response: { status: '404', outcome: err.cause },
|
|
577
|
+
} as unknown as BundleEntry<BundleContentType>;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return entry;
|
|
581
|
+
})
|
|
582
|
+
: rawEntries;
|
|
291
583
|
const bundle: BatchBundle<BundleContentType> = {
|
|
292
584
|
...resp,
|
|
293
|
-
entry:
|
|
585
|
+
entry: processedEntries,
|
|
294
586
|
unbundle: function (this: { entry?: Array<BundleEntry<BundleContentType>> | undefined }) {
|
|
295
587
|
return (
|
|
296
588
|
this.entry
|
|
@@ -317,10 +609,21 @@ export async function transaction<BundleContentType extends FhirResource>(
|
|
|
317
609
|
{
|
|
318
610
|
resourceType: 'Bundle',
|
|
319
611
|
type: 'transaction',
|
|
320
|
-
entry: input.requests.map(batchInputRequestToBundleEntryItem),
|
|
612
|
+
entry: input.requests.map((req) => batchInputRequestToBundleEntryItem(req, this.config)),
|
|
321
613
|
},
|
|
322
614
|
request
|
|
323
615
|
);
|
|
616
|
+
// Validate each GET/HEAD retrieval entry against the tag config.
|
|
617
|
+
// A violation throws OystehrFHIRError(404) — transactions are all-or-nothing.
|
|
618
|
+
if (this.config.workspaceTag || this.config.ignoreTags?.length) {
|
|
619
|
+
(resp.entry as Array<BundleEntry<BundleContentType>> | undefined)?.forEach((entry, i) => {
|
|
620
|
+
const req = input.requests[i];
|
|
621
|
+
if (!req || !entry?.resource) return;
|
|
622
|
+
if ((req.method === 'GET' || req.method === 'HEAD') && !isBatchSearchUrl(req.url)) {
|
|
623
|
+
assertRetrievedResource(this.config, entry.resource);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
324
627
|
const bundle: TransactionBundle<BundleContentType> = {
|
|
325
628
|
...resp,
|
|
326
629
|
entry: resp.entry as Array<BundleEntry<BundleContentType>> | undefined,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// AUTOGENERATED -- DO NOT EDIT
|
|
2
2
|
|
|
3
3
|
import { OystehrConfig } from '../../config';
|
|
4
|
+
import { OystehrSdkError } from '../../errors';
|
|
4
5
|
import { Application } from './application';
|
|
5
6
|
import { Charge } from './charge';
|
|
6
7
|
import { Conversation } from './conversation';
|
|
@@ -51,6 +52,12 @@ export class Oystehr {
|
|
|
51
52
|
readonly rcm: Rcm;
|
|
52
53
|
readonly fhir: Fhir;
|
|
53
54
|
constructor(config: OystehrConfig) {
|
|
55
|
+
if (config.workspaceTag && config.ignoreTags) {
|
|
56
|
+
throw new OystehrSdkError({
|
|
57
|
+
message: 'workspaceTag and ignoreTags are mutually exclusive and cannot both be set in config',
|
|
58
|
+
code: 400,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
54
61
|
this.config = config;
|
|
55
62
|
this.config.services ??= {};
|
|
56
63
|
this.config.services['projectApiUrl'] ??= config.projectApiUrl;
|
|
@@ -4,4 +4,12 @@ import { Organization } from 'fhir/r4b';
|
|
|
4
4
|
/**
|
|
5
5
|
* List of payers matching the search criteria.
|
|
6
6
|
*/
|
|
7
|
-
export
|
|
7
|
+
export interface RcmListPayersResponse {
|
|
8
|
+
data: Organization[];
|
|
9
|
+
metadata: {
|
|
10
|
+
/**
|
|
11
|
+
* Cursor to fetch the next page of results. Null if there are no further pages.
|
|
12
|
+
*/
|
|
13
|
+
nextCursor: string | null;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -3,15 +3,19 @@ import type {
|
|
|
3
3
|
Binary as BinaryR4B,
|
|
4
4
|
Bundle as BundleR4B,
|
|
5
5
|
BundleEntry as BundleEntryR4B,
|
|
6
|
+
Coding as CodingR4B,
|
|
6
7
|
FhirResource as FhirResourceR4B,
|
|
7
8
|
OperationOutcome as OperationOutcomeR4B,
|
|
9
|
+
Resource as ResourceR4B,
|
|
8
10
|
} from 'fhir/r4b';
|
|
9
11
|
import type {
|
|
10
12
|
Binary as BinaryR5,
|
|
11
13
|
Bundle as BundleR5,
|
|
12
14
|
BundleEntry as BundleEntryR5,
|
|
15
|
+
Coding as CodingR5,
|
|
13
16
|
FhirResource as FhirResourceR5,
|
|
14
17
|
OperationOutcome as OperationOutcomeR5,
|
|
18
|
+
Resource as ResourceR5,
|
|
15
19
|
} from 'fhir/r5';
|
|
16
20
|
|
|
17
21
|
export type FhirResource = FhirResourceR4B | FhirResourceR5;
|
|
@@ -46,6 +50,8 @@ export type TransactionBundle<F extends FhirResource> = EntrylessFhirBundle<F> &
|
|
|
46
50
|
export type BundleEntry<F extends FhirResource> = F extends FhirResourceR4B ? BundleEntryR4B<F> : BundleEntryR5<F>;
|
|
47
51
|
export type Binary<F extends FhirResource> = F extends FhirResourceR4B ? BinaryR4B : BinaryR5;
|
|
48
52
|
export type OperationOutcome = OperationOutcomeR4B | OperationOutcomeR5;
|
|
53
|
+
export type Coding = CodingR4B | CodingR5;
|
|
54
|
+
export type Resource = ResourceR4B | ResourceR5;
|
|
49
55
|
|
|
50
56
|
export type SearchParam = { name: string; value: string | number };
|
|
51
57
|
|
|
@@ -119,6 +125,15 @@ export interface BatchInputPostRequest<F extends FhirResource> extends BatchInpu
|
|
|
119
125
|
fullUrl?: string;
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
/**
|
|
129
|
+
* A batch POST to a `_search` endpoint (e.g. `Patient/_search`). Search parameters
|
|
130
|
+
* may be appended to the URL as query parameters. Unlike a create POST there is no
|
|
131
|
+
* resource body.
|
|
132
|
+
*/
|
|
133
|
+
export interface BatchInputSearchPostRequest extends BatchInputBaseRequest {
|
|
134
|
+
method: 'POST';
|
|
135
|
+
}
|
|
136
|
+
|
|
122
137
|
export interface BatchInputBinaryPatchRequest<F extends FhirResource> extends BatchInputBaseRequest {
|
|
123
138
|
method: 'PATCH';
|
|
124
139
|
ifMatch?: string;
|
|
@@ -141,6 +156,7 @@ export type BatchInputRequest<F extends FhirResource> =
|
|
|
141
156
|
| BatchInputPutRequest<F>
|
|
142
157
|
| BatchInputPatchRequest<F>
|
|
143
158
|
| BatchInputPostRequest<F>
|
|
159
|
+
| BatchInputSearchPostRequest
|
|
144
160
|
| BatchInputDeleteRequest;
|
|
145
161
|
|
|
146
162
|
export interface BatchInput<F extends FhirResource> {
|