@oystehr/sdk 4.3.2 → 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.
Files changed (86) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/client/client.cjs +87 -13
  3. package/dist/cjs/client/client.cjs.map +1 -1
  4. package/dist/cjs/client/client.d.ts +3 -0
  5. package/dist/cjs/config.d.ts +19 -0
  6. package/dist/cjs/index.min.cjs +1 -1
  7. package/dist/cjs/index.min.cjs.map +1 -1
  8. package/dist/cjs/logger.cjs +36 -0
  9. package/dist/cjs/logger.cjs.map +1 -0
  10. package/dist/cjs/logger.d.ts +11 -0
  11. package/dist/cjs/resources/classes/erx.cjs +8 -0
  12. package/dist/cjs/resources/classes/erx.cjs.map +1 -1
  13. package/dist/cjs/resources/classes/erx.d.ts +7 -1
  14. package/dist/cjs/resources/classes/fhir-ext.cjs +290 -19
  15. package/dist/cjs/resources/classes/fhir-ext.cjs.map +1 -1
  16. package/dist/cjs/resources/classes/index.cjs +9 -2
  17. package/dist/cjs/resources/classes/index.cjs.map +1 -1
  18. package/dist/cjs/resources/classes/index.d.ts +1 -1
  19. package/dist/cjs/resources/classes/rcm.cjs +18 -0
  20. package/dist/cjs/resources/classes/rcm.cjs.map +1 -1
  21. package/dist/cjs/resources/classes/rcm.d.ts +22 -1
  22. package/dist/cjs/resources/classes/user.cjs +9 -0
  23. package/dist/cjs/resources/classes/user.cjs.map +1 -1
  24. package/dist/cjs/resources/classes/user.d.ts +8 -1
  25. package/dist/cjs/resources/types/ErxGetMedicationHistoryParams.d.ts +3 -0
  26. package/dist/cjs/resources/types/ErxGetMedicationHistoryResponse.d.ts +43 -0
  27. package/dist/cjs/resources/types/ErxGetMedicationResponse.d.ts +1 -1
  28. package/dist/cjs/resources/types/RcmGetPayerParams.d.ts +3 -0
  29. package/dist/cjs/resources/types/RcmGetPayerResponse.d.ts +5 -0
  30. package/dist/cjs/resources/types/RcmListPayersParams.d.ts +9 -0
  31. package/dist/cjs/resources/types/RcmListPayersResponse.d.ts +13 -0
  32. package/dist/cjs/resources/types/UserChangePasswordParams.d.ts +7 -0
  33. package/dist/cjs/resources/types/fhir.d.ts +13 -3
  34. package/dist/cjs/resources/types/index.d.ts +13 -6
  35. package/dist/esm/client/client.d.ts +3 -0
  36. package/dist/esm/client/client.js +88 -15
  37. package/dist/esm/client/client.js.map +1 -1
  38. package/dist/esm/config.d.ts +19 -0
  39. package/dist/esm/index.min.js +1 -1
  40. package/dist/esm/index.min.js.map +1 -1
  41. package/dist/esm/logger.d.ts +11 -0
  42. package/dist/esm/logger.js +34 -0
  43. package/dist/esm/logger.js.map +1 -0
  44. package/dist/esm/resources/classes/erx.d.ts +7 -1
  45. package/dist/esm/resources/classes/erx.js +8 -0
  46. package/dist/esm/resources/classes/erx.js.map +1 -1
  47. package/dist/esm/resources/classes/fhir-ext.js +290 -19
  48. package/dist/esm/resources/classes/fhir-ext.js.map +1 -1
  49. package/dist/esm/resources/classes/index.d.ts +1 -1
  50. package/dist/esm/resources/classes/index.js +9 -2
  51. package/dist/esm/resources/classes/index.js.map +1 -1
  52. package/dist/esm/resources/classes/rcm.d.ts +22 -1
  53. package/dist/esm/resources/classes/rcm.js +18 -0
  54. package/dist/esm/resources/classes/rcm.js.map +1 -1
  55. package/dist/esm/resources/classes/user.d.ts +8 -1
  56. package/dist/esm/resources/classes/user.js +9 -0
  57. package/dist/esm/resources/classes/user.js.map +1 -1
  58. package/dist/esm/resources/types/ErxGetMedicationHistoryParams.d.ts +3 -0
  59. package/dist/esm/resources/types/ErxGetMedicationHistoryResponse.d.ts +43 -0
  60. package/dist/esm/resources/types/ErxGetMedicationResponse.d.ts +1 -1
  61. package/dist/esm/resources/types/RcmGetPayerParams.d.ts +3 -0
  62. package/dist/esm/resources/types/RcmGetPayerResponse.d.ts +5 -0
  63. package/dist/esm/resources/types/RcmListPayersParams.d.ts +9 -0
  64. package/dist/esm/resources/types/RcmListPayersResponse.d.ts +13 -0
  65. package/dist/esm/resources/types/UserChangePasswordParams.d.ts +7 -0
  66. package/dist/esm/resources/types/fhir.d.ts +13 -3
  67. package/dist/esm/resources/types/index.d.ts +13 -6
  68. package/package.json +1 -1
  69. package/src/client/client.ts +95 -15
  70. package/src/config.ts +20 -0
  71. package/src/logger.ts +36 -0
  72. package/src/resources/classes/erx.ts +17 -0
  73. package/src/resources/classes/fhir-ext.ts +322 -19
  74. package/src/resources/classes/index.ts +9 -2
  75. package/src/resources/classes/rcm.ts +39 -0
  76. package/src/resources/classes/user.ts +10 -0
  77. package/src/resources/types/ErxGetMedicationHistoryParams.ts +5 -0
  78. package/src/resources/types/ErxGetMedicationHistoryResponse.ts +45 -0
  79. package/src/resources/types/ErxGetMedicationResponse.ts +1 -1
  80. package/src/resources/types/RcmGetPayerParams.ts +5 -0
  81. package/src/resources/types/RcmGetPayerResponse.ts +7 -0
  82. package/src/resources/types/RcmListPayersParams.ts +11 -0
  83. package/src/resources/types/RcmListPayersResponse.ts +15 -0
  84. package/src/resources/types/UserChangePasswordParams.ts +9 -0
  85. package/src/resources/types/fhir.ts +16 -0
  86. package/src/resources/types/index.ts +13 -6
@@ -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, params: searchParams } = params;
276
+ const { resourceType } = params;
277
+ const taggedParams = applyTagSearchParams(this.config, params.params);
68
278
  let paramMap: Record<string, (string | number)[]> | undefined;
69
- if (searchParams) {
70
- paramMap = Object.entries(searchParams).reduce((acc, [_, param]) => {
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 { resourceType } = params;
100
- return this.fhirRequest(`/${resourceType}`, 'POST')(params as unknown as Record<string, unknown>, request);
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
- return this.fhirRequest<FhirResourceReturnValue<T>>(`/${resourceType}/${id}`, 'GET')({}, request);
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 { id, resourceType } = params;
117
- return this.fhirRequest(`/${resourceType}/${id}`, 'PUT')(params as unknown as Record<string, unknown>, {
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
- return this.fhirRequest(`/${resourceType}/${id}`, 'PATCH')(operations, {
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, url } = request;
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 { resource } = request;
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: request.resource,
506
+ resource: patchResource,
244
507
  } as BundleEntry<Binary<T>>;
245
508
  }
246
- const data = stringToBase64(JSON.stringify(request.operations));
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 { resource, fullUrl } = request;
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: resp.entry as Array<BundleEntry<BundleContentType>> | undefined,
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';
@@ -45,12 +46,18 @@ export class Oystehr {
45
46
  readonly fax: Fax;
46
47
  readonly lab: Lab;
47
48
  readonly erx: Erx;
48
- readonly rcm: Rcm;
49
49
  readonly terminology: Terminology;
50
50
  readonly zambda: Zambda;
51
51
  readonly zambdaLogStream: ZambdaLogStream;
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;
@@ -73,10 +80,10 @@ export class Oystehr {
73
80
  this.fax = new Fax(config);
74
81
  this.lab = new Lab(config);
75
82
  this.erx = new Erx(config);
76
- this.rcm = new Rcm(config);
77
83
  this.terminology = new Terminology(config);
78
84
  this.zambda = new Zambda(config);
79
85
  this.zambdaLogStream = new ZambdaLogStream(config);
86
+ this.rcm = new Rcm(config);
80
87
  this.fhir = new Fhir(config);
81
88
  }
82
89
  }
@@ -4,6 +4,10 @@ import {
4
4
  OystehrClientRequest,
5
5
  RcmEligibilityCheckParams,
6
6
  RcmEligibilityCheckResponse,
7
+ RcmGetPayerParams,
8
+ RcmGetPayerResponse,
9
+ RcmListPayersParams,
10
+ RcmListPayersResponse,
7
11
  RcmSetClaimStatusParams,
8
12
  RcmSetClaimStatusResponse,
9
13
  RcmSubmitClaimParams,
@@ -58,4 +62,39 @@ export class Rcm extends SDKResource {
58
62
  setClaimStatus(params: RcmSetClaimStatusParams, request?: OystehrClientRequest): Promise<RcmSetClaimStatusResponse> {
59
63
  return this.request('/claim/{claimId}/status', 'post', this.#baseUrlThunk.bind(this))(params, request);
60
64
  }
65
+ /**
66
+ * Search for payers by name or identifier.
67
+ *
68
+ * Access Policy Action: `RCM:SearchPayer`
69
+ * Access Policy Resource: `RCM:Payer`
70
+ */
71
+ listPayers(params: RcmListPayersParams, request?: OystehrClientRequest): Promise<RcmListPayersResponse>;
72
+ /**
73
+ * Search for payers by name or identifier.
74
+ *
75
+ * Access Policy Action: `RCM:SearchPayer`
76
+ * Access Policy Resource: `RCM:Payer`
77
+ */
78
+ listPayers(request?: OystehrClientRequest): Promise<RcmListPayersResponse>;
79
+ /**
80
+ * Search for payers by name or identifier.
81
+ *
82
+ * Access Policy Action: `RCM:SearchPayer`
83
+ * Access Policy Resource: `RCM:Payer`
84
+ */
85
+ listPayers(
86
+ params?: RcmListPayersParams | OystehrClientRequest,
87
+ request?: OystehrClientRequest
88
+ ): Promise<RcmListPayersResponse> {
89
+ return this.request('/payer', 'get', this.#baseUrlThunk.bind(this))(params, request);
90
+ }
91
+ /**
92
+ * Retrieve a payer by its ID.
93
+ *
94
+ * Access Policy Action: `RCM:GetPayer`
95
+ * Access Policy Resource: `RCM:Payer`
96
+ */
97
+ getPayer(params: RcmGetPayerParams, request?: OystehrClientRequest): Promise<RcmGetPayerResponse> {
98
+ return this.request('/payer/{id}', 'get', this.#baseUrlThunk.bind(this))(params, request);
99
+ }
61
100
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {
4
4
  OystehrClientRequest,
5
+ UserChangePasswordParams,
5
6
  UserDeleteParams,
6
7
  UserGetParams,
7
8
  UserGetResponse,
@@ -79,6 +80,15 @@ export class User extends SDKResource {
79
80
  resetPasswordLink(params: UserResetPasswordLinkParams, request?: OystehrClientRequest): Promise<void> {
80
81
  return this.request('/user/{id}/reset-password-link', 'post', this.#baseUrlThunk.bind(this))(params, request);
81
82
  }
83
+ /**
84
+ * Automatically changes the password for the User with the provided ID. [Users](https://docs.oystehr.com/oystehr/services/app/users/) are the people who log into the [Applications](https://docs.oystehr.com/oystehr/services/app/applications/) you configure for securing the apps you build on top of Oystehr.
85
+ *
86
+ * Access Policy Action: `App:ChangeUserPassword`
87
+ * Access Policy Resource: `App:User`
88
+ */
89
+ changePassword(params: UserChangePasswordParams, request?: OystehrClientRequest): Promise<void> {
90
+ return this.request('/user/{id}/change-password', 'post', this.#baseUrlThunk.bind(this))(params, request);
91
+ }
82
92
  /**
83
93
  * Invite a User to the Project. [Users](https://docs.oystehr.com/oystehr/services/app/users/) are the people who log into the [Applications](https://docs.oystehr.com/oystehr/services/app/applications/) you configure for securing the apps you build on top of Oystehr.
84
94
  *
@@ -0,0 +1,5 @@
1
+ // AUTOGENERATED -- DO NOT EDIT
2
+
3
+ export interface ErxGetMedicationHistoryParams {
4
+ patientId: string;
5
+ }
@@ -0,0 +1,45 @@
1
+ // AUTOGENERATED -- DO NOT EDIT
2
+
3
+ /**
4
+ * Success
5
+ */
6
+ export type ErxGetMedicationHistoryResponse = {
7
+ /**
8
+ * Unique identifier for the medication history item.
9
+ */
10
+ id: number;
11
+ /**
12
+ * The Medi-Span identifier for the medication.
13
+ */
14
+ medicationId: number;
15
+ /**
16
+ * National Drug Code (NDC) identifier of medication.
17
+ */
18
+ ndc: string | null;
19
+ /**
20
+ * RxNorm Concept Unique (RxCUI) identifier of medication.
21
+ */
22
+ rxcui: number | null;
23
+ /**
24
+ * Human readable name of medication.
25
+ */
26
+ name: string;
27
+ route: string | null;
28
+ doseForm: string | null;
29
+ strength: string | null;
30
+ dispenseUnit: string | null;
31
+ isBrandName: boolean;
32
+ genericName: string | null;
33
+ isOtc: boolean;
34
+ refills: string;
35
+ daysSupply: number | null;
36
+ quantity: number;
37
+ classification: string | null;
38
+ schedule: number | null;
39
+ directions: string | null;
40
+ substitutionsAllowed: boolean;
41
+ writtenDate: string;
42
+ effectiveDate: string | null;
43
+ lastFillDate: string | null;
44
+ expirationDate: string | null;
45
+ }[];
@@ -9,7 +9,7 @@ export interface ErxGetMedicationResponse {
9
9
  */
10
10
  id: number;
11
11
  /**
12
- * RxNorm Concept Unique (RxCUI) identifier of medication.
12
+ * National Drug Code (NDC) identifier of medication.
13
13
  */
14
14
  ndc?: string | null;
15
15
  /**
@@ -0,0 +1,5 @@
1
+ // AUTOGENERATED -- DO NOT EDIT
2
+
3
+ export interface RcmGetPayerParams {
4
+ id: string;
5
+ }
@@ -0,0 +1,7 @@
1
+ // AUTOGENERATED -- DO NOT EDIT
2
+
3
+ import { Organization } from 'fhir/r4b';
4
+ /**
5
+ * A valid FHIR Organization object corresponding to the payer.
6
+ */
7
+ export type RcmGetPayerResponse = Organization;
@@ -0,0 +1,11 @@
1
+ // AUTOGENERATED -- DO NOT EDIT
2
+
3
+ export interface RcmListPayersParams {
4
+ name?: string;
5
+ identifier?: string;
6
+ includeInactive?: boolean;
7
+ cursor?: string;
8
+ limit?: number;
9
+ sort?: 'id' | 'name';
10
+ sortOrder?: 'asc' | 'desc';
11
+ }
@@ -0,0 +1,15 @@
1
+ // AUTOGENERATED -- DO NOT EDIT
2
+
3
+ import { Organization } from 'fhir/r4b';
4
+ /**
5
+ * List of payers matching the search criteria.
6
+ */
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
+ }