@fhirfly-io/terminology 0.1.3 → 0.2.0

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/dist/index.js CHANGED
@@ -101,12 +101,71 @@ var TimeoutError = class _TimeoutError extends FhirflyError {
101
101
  };
102
102
 
103
103
  // src/http.ts
104
+ var TokenManager = class {
105
+ credentials;
106
+ accessToken = null;
107
+ expiresAt = 0;
108
+ refreshPromise = null;
109
+ constructor(credentials) {
110
+ this.credentials = credentials;
111
+ }
112
+ /**
113
+ * Get a valid access token, refreshing if needed.
114
+ * Deduplicates concurrent refresh calls.
115
+ */
116
+ async getToken() {
117
+ if (this.accessToken && Date.now() < this.expiresAt) {
118
+ return this.accessToken;
119
+ }
120
+ if (this.refreshPromise) {
121
+ return this.refreshPromise;
122
+ }
123
+ this.refreshPromise = this.fetchToken();
124
+ try {
125
+ return await this.refreshPromise;
126
+ } finally {
127
+ this.refreshPromise = null;
128
+ }
129
+ }
130
+ /**
131
+ * Invalidate the cached token (e.g., after a 401 response).
132
+ */
133
+ invalidate() {
134
+ this.accessToken = null;
135
+ this.expiresAt = 0;
136
+ }
137
+ async fetchToken() {
138
+ const body = new URLSearchParams({
139
+ grant_type: "client_credentials",
140
+ client_id: this.credentials.clientId,
141
+ client_secret: this.credentials.clientSecret
142
+ });
143
+ if (this.credentials.scopes?.length) {
144
+ body.set("scope", this.credentials.scopes.join(" "));
145
+ }
146
+ const response = await fetch(this.credentials.tokenUrl, {
147
+ method: "POST",
148
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
149
+ body: body.toString()
150
+ });
151
+ if (!response.ok) {
152
+ const text = await response.text().catch(() => "");
153
+ throw new AuthenticationError(
154
+ `OAuth2 token exchange failed (${response.status}): ${text}`
155
+ );
156
+ }
157
+ const data = await response.json();
158
+ this.accessToken = data.access_token;
159
+ this.expiresAt = Date.now() + (data.expires_in - 60) * 1e3;
160
+ return this.accessToken;
161
+ }
162
+ };
104
163
  var HttpClient = class {
105
164
  config;
106
165
  constructor(config) {
107
166
  this.config = {
108
167
  baseUrl: config.baseUrl,
109
- apiKey: config.apiKey,
168
+ auth: config.auth,
110
169
  timeout: config.timeout ?? 3e4,
111
170
  maxRetries: config.maxRetries ?? 3,
112
171
  retryDelay: config.retryDelay ?? 1e3,
@@ -128,6 +187,26 @@ var HttpClient = class {
128
187
  const queryString = params.toString();
129
188
  return queryString ? `?${queryString}` : "";
130
189
  }
190
+ /**
191
+ * Build query string from search params object.
192
+ */
193
+ buildSearchQueryString(params) {
194
+ const searchParams = new URLSearchParams();
195
+ for (const [key, value] of Object.entries(params)) {
196
+ if (value === void 0 || value === null) continue;
197
+ if (typeof value === "boolean") {
198
+ searchParams.set(key, value.toString());
199
+ } else if (typeof value === "number") {
200
+ searchParams.set(key, value.toString());
201
+ } else if (typeof value === "string" && value.length > 0) {
202
+ searchParams.set(key, value);
203
+ } else if (Array.isArray(value) && value.length > 0) {
204
+ searchParams.set(key, value.join(","));
205
+ }
206
+ }
207
+ const queryString = searchParams.toString();
208
+ return queryString ? `?${queryString}` : "";
209
+ }
131
210
  /**
132
211
  * Parse error response from API.
133
212
  */
@@ -180,10 +259,20 @@ var HttpClient = class {
180
259
  sleep(ms) {
181
260
  return new Promise((resolve) => setTimeout(resolve, ms));
182
261
  }
262
+ /**
263
+ * Get auth headers for the current request.
264
+ */
265
+ async getAuthHeaders() {
266
+ if (this.config.auth.type === "api-key") {
267
+ return { "x-api-key": this.config.auth.apiKey };
268
+ }
269
+ const token = await this.config.auth.tokenManager.getToken();
270
+ return { "Authorization": `Bearer ${token}` };
271
+ }
183
272
  /**
184
273
  * Make an HTTP request with retries.
185
274
  */
186
- async request(method, endpoint, body) {
275
+ async request(method, endpoint, body, isRetryAfter401 = false) {
187
276
  const url = `${this.config.baseUrl}${endpoint}`;
188
277
  let lastError;
189
278
  for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
@@ -193,10 +282,11 @@ var HttpClient = class {
193
282
  () => controller.abort(),
194
283
  this.config.timeout
195
284
  );
285
+ const authHeaders = await this.getAuthHeaders();
196
286
  const response = await fetch(url, {
197
287
  method,
198
288
  headers: {
199
- "Authorization": `Bearer ${this.config.apiKey}`,
289
+ ...authHeaders,
200
290
  "Content-Type": "application/json",
201
291
  "User-Agent": this.config.userAgent,
202
292
  "Accept": "application/json"
@@ -206,6 +296,10 @@ var HttpClient = class {
206
296
  });
207
297
  clearTimeout(timeoutId);
208
298
  if (!response.ok) {
299
+ if (response.status === 401 && this.config.auth.type === "oauth" && !isRetryAfter401) {
300
+ this.config.auth.tokenManager.invalidate();
301
+ return this.request(method, endpoint, body, true);
302
+ }
209
303
  if (response.status < 500 && response.status !== 429) {
210
304
  await this.parseErrorResponse(response, endpoint);
211
305
  }
@@ -266,6 +360,14 @@ var HttpClient = class {
266
360
  const response = await this.request("POST", `${endpoint}${queryString}`, body);
267
361
  return response.data;
268
362
  }
363
+ /**
364
+ * Make a GET request with search parameters.
365
+ */
366
+ async search(endpoint, params) {
367
+ const queryString = this.buildSearchQueryString(params);
368
+ const response = await this.request("GET", `${endpoint}${queryString}`);
369
+ return response.data;
370
+ }
269
371
  };
270
372
 
271
373
  // src/endpoints/ndc.ts
@@ -320,6 +422,38 @@ var NdcEndpoint = class {
320
422
  options
321
423
  );
322
424
  }
425
+ /**
426
+ * Search for NDC products.
427
+ *
428
+ * @param params - Search parameters (q, name, brand, ingredient, etc.)
429
+ * @param options - Pagination and response shape options
430
+ * @returns Search results with facets
431
+ *
432
+ * @example
433
+ * ```ts
434
+ * // Search by drug name
435
+ * const results = await client.ndc.search({ q: "advil" });
436
+ *
437
+ * // Search with filters
438
+ * const results = await client.ndc.search({
439
+ * ingredient: "ibuprofen",
440
+ * dosage_form: "TABLET",
441
+ * product_type: "otc"
442
+ * });
443
+ *
444
+ * console.log(`Found ${results.total} products`);
445
+ * for (const item of results.items) {
446
+ * console.log(item.product_name);
447
+ * }
448
+ * ```
449
+ */
450
+ async search(params, options) {
451
+ return this.http.search("/v1/ndc/search", {
452
+ ...params,
453
+ ...options,
454
+ include: options?.include?.join(",")
455
+ });
456
+ }
323
457
  };
324
458
 
325
459
  // src/endpoints/npi.ts
@@ -365,6 +499,35 @@ var NpiEndpoint = class {
365
499
  options
366
500
  );
367
501
  }
502
+ /**
503
+ * Search for healthcare providers.
504
+ *
505
+ * @param params - Search parameters (q, name, specialty, state, etc.)
506
+ * @param options - Pagination and response shape options
507
+ * @returns Search results with facets
508
+ *
509
+ * @example
510
+ * ```ts
511
+ * // Search by name
512
+ * const results = await client.npi.search({ q: "smith" });
513
+ *
514
+ * // Search with filters
515
+ * const results = await client.npi.search({
516
+ * specialty: "cardiology",
517
+ * state: "CA",
518
+ * entity_type: "individual"
519
+ * });
520
+ *
521
+ * console.log(`Found ${results.total} providers`);
522
+ * ```
523
+ */
524
+ async search(params, options) {
525
+ return this.http.search("/v1/npi/search", {
526
+ ...params,
527
+ ...options,
528
+ include: options?.include?.join(",")
529
+ });
530
+ }
368
531
  };
369
532
 
370
533
  // src/endpoints/rxnorm.ts
@@ -402,6 +565,32 @@ var RxNormEndpoint = class {
402
565
  options
403
566
  );
404
567
  }
568
+ /**
569
+ * Search for drugs in RxNorm.
570
+ *
571
+ * @param params - Search parameters (q, name, ingredient, brand, etc.)
572
+ * @param options - Pagination and response shape options
573
+ * @returns Search results with facets
574
+ *
575
+ * @example
576
+ * ```ts
577
+ * // Search by drug name
578
+ * const results = await client.rxnorm.search({ q: "lipitor" });
579
+ *
580
+ * // Search prescribable drugs by ingredient
581
+ * const results = await client.rxnorm.search({
582
+ * ingredient: "metformin",
583
+ * is_prescribable: true
584
+ * });
585
+ * ```
586
+ */
587
+ async search(params, options) {
588
+ return this.http.search("/v1/rxnorm/search", {
589
+ ...params,
590
+ ...options,
591
+ include: options?.include?.join(",")
592
+ });
593
+ }
405
594
  };
406
595
 
407
596
  // src/endpoints/loinc.ts
@@ -439,6 +628,33 @@ var LoincEndpoint = class {
439
628
  options
440
629
  );
441
630
  }
631
+ /**
632
+ * Search for LOINC codes.
633
+ *
634
+ * @param params - Search parameters (q, component, class, system, etc.)
635
+ * @param options - Pagination and response shape options
636
+ * @returns Search results with facets
637
+ *
638
+ * @example
639
+ * ```ts
640
+ * // Search by term
641
+ * const results = await client.loinc.search({ q: "glucose" });
642
+ *
643
+ * // Search blood chemistry tests
644
+ * const results = await client.loinc.search({
645
+ * class: "CHEM",
646
+ * system: "Bld",
647
+ * scale: "Qn"
648
+ * });
649
+ * ```
650
+ */
651
+ async search(params, options) {
652
+ return this.http.search("/v1/loinc/search", {
653
+ ...params,
654
+ ...options,
655
+ include: options?.include?.join(",")
656
+ });
657
+ }
442
658
  };
443
659
 
444
660
  // src/endpoints/icd10.ts
@@ -506,6 +722,36 @@ var Icd10Endpoint = class {
506
722
  options
507
723
  );
508
724
  }
725
+ /**
726
+ * Search for ICD-10 codes (both CM and PCS).
727
+ *
728
+ * @param params - Search parameters (q, code_system, chapter, billable, etc.)
729
+ * @param options - Pagination and response shape options
730
+ * @returns Search results with facets
731
+ *
732
+ * @example
733
+ * ```ts
734
+ * // Search diagnosis codes
735
+ * const results = await client.icd10.search({
736
+ * q: "diabetes",
737
+ * code_system: "CM",
738
+ * billable: true
739
+ * });
740
+ *
741
+ * // Search procedure codes
742
+ * const results = await client.icd10.search({
743
+ * q: "bypass",
744
+ * code_system: "PCS"
745
+ * });
746
+ * ```
747
+ */
748
+ async search(params, options) {
749
+ return this.http.search("/v1/icd10/search", {
750
+ ...params,
751
+ ...options,
752
+ include: options?.include?.join(",")
753
+ });
754
+ }
509
755
  };
510
756
 
511
757
  // src/endpoints/cvx.ts
@@ -543,6 +789,32 @@ var CvxEndpoint = class {
543
789
  options
544
790
  );
545
791
  }
792
+ /**
793
+ * Search for vaccine codes.
794
+ *
795
+ * @param params - Search parameters (q, status, vaccine_type, is_covid_vaccine)
796
+ * @param options - Pagination and response shape options
797
+ * @returns Search results with facets
798
+ *
799
+ * @example
800
+ * ```ts
801
+ * // Search for flu vaccines
802
+ * const results = await client.cvx.search({ q: "influenza" });
803
+ *
804
+ * // Find all COVID-19 vaccines
805
+ * const results = await client.cvx.search({
806
+ * is_covid_vaccine: true,
807
+ * status: "active"
808
+ * });
809
+ * ```
810
+ */
811
+ async search(params, options) {
812
+ return this.http.search("/v1/cvx/search", {
813
+ ...params,
814
+ ...options,
815
+ include: options?.include?.join(",")
816
+ });
817
+ }
546
818
  };
547
819
 
548
820
  // src/endpoints/mvx.ts
@@ -580,6 +852,29 @@ var MvxEndpoint = class {
580
852
  options
581
853
  );
582
854
  }
855
+ /**
856
+ * Search for vaccine manufacturers.
857
+ *
858
+ * @param params - Search parameters (q, status)
859
+ * @param options - Pagination and response shape options
860
+ * @returns Search results with facets
861
+ *
862
+ * @example
863
+ * ```ts
864
+ * // Search by name
865
+ * const results = await client.mvx.search({ q: "pfizer" });
866
+ *
867
+ * // List all active manufacturers
868
+ * const results = await client.mvx.search({ status: "active" });
869
+ * ```
870
+ */
871
+ async search(params, options) {
872
+ return this.http.search("/v1/mvx/search", {
873
+ ...params,
874
+ ...options,
875
+ include: options?.include?.join(",")
876
+ });
877
+ }
583
878
  };
584
879
 
585
880
  // src/endpoints/fda-labels.ts
@@ -633,6 +928,38 @@ var FdaLabelsEndpoint = class {
633
928
  options
634
929
  );
635
930
  }
931
+ /**
932
+ * Search for FDA drug labels.
933
+ *
934
+ * @param params - Search parameters (q, name, brand, substance, manufacturer, etc.)
935
+ * @param options - Pagination and response shape options
936
+ * @returns Search results with facets
937
+ *
938
+ * @example
939
+ * ```ts
940
+ * // Search by drug name
941
+ * const results = await client.fdaLabels.search({ q: "advil" });
942
+ *
943
+ * // Search OTC pain relievers
944
+ * const results = await client.fdaLabels.search({
945
+ * substance: "acetaminophen",
946
+ * product_type: "otc"
947
+ * });
948
+ *
949
+ * // Search by manufacturer
950
+ * const results = await client.fdaLabels.search({
951
+ * manufacturer: "pfizer",
952
+ * product_type: "rx"
953
+ * });
954
+ * ```
955
+ */
956
+ async search(params, options) {
957
+ return this.http.search("/v1/fda-label/search", {
958
+ ...params,
959
+ ...options,
960
+ include: options?.include?.join(",")
961
+ });
962
+ }
636
963
  };
637
964
 
638
965
  // src/client.ts
@@ -673,22 +1000,39 @@ var Fhirfly = class {
673
1000
  /**
674
1001
  * Create a new FHIRfly client.
675
1002
  *
676
- * @param config - Client configuration
677
- * @throws {Error} If apiKey is not provided
1003
+ * @param config - Client configuration (API key or OAuth2 client credentials)
1004
+ * @throws {Error} If neither apiKey nor clientId+clientSecret is provided
678
1005
  */
679
1006
  constructor(config) {
680
- if (!config.apiKey) {
1007
+ const baseUrl = config.baseUrl ?? "https://api.fhirfly.io";
1008
+ let httpConfig;
1009
+ if ("apiKey" in config && config.apiKey) {
1010
+ httpConfig = {
1011
+ baseUrl,
1012
+ auth: { type: "api-key", apiKey: config.apiKey },
1013
+ timeout: config.timeout,
1014
+ maxRetries: config.maxRetries,
1015
+ retryDelay: config.retryDelay
1016
+ };
1017
+ } else if ("clientId" in config && config.clientId && config.clientSecret) {
1018
+ const tokenManager = new TokenManager({
1019
+ clientId: config.clientId,
1020
+ clientSecret: config.clientSecret,
1021
+ tokenUrl: config.tokenUrl ?? `${baseUrl}/oauth2/token`,
1022
+ scopes: config.scopes
1023
+ });
1024
+ httpConfig = {
1025
+ baseUrl,
1026
+ auth: { type: "oauth", tokenManager },
1027
+ timeout: config.timeout,
1028
+ maxRetries: config.maxRetries,
1029
+ retryDelay: config.retryDelay
1030
+ };
1031
+ } else {
681
1032
  throw new Error(
682
- "FHIRfly API key is required. Get one at https://fhirfly.io/dashboard"
1033
+ "FHIRfly requires either an apiKey or clientId+clientSecret. Get credentials at https://fhirfly.io/dashboard"
683
1034
  );
684
1035
  }
685
- const httpConfig = {
686
- baseUrl: config.baseUrl ?? "https://api.fhirfly.io",
687
- apiKey: config.apiKey,
688
- timeout: config.timeout,
689
- maxRetries: config.maxRetries,
690
- retryDelay: config.retryDelay
691
- };
692
1036
  this.http = new HttpClient(httpConfig);
693
1037
  this.ndc = new NdcEndpoint(this.http);
694
1038
  this.npi = new NpiEndpoint(this.http);
@@ -701,6 +1045,6 @@ var Fhirfly = class {
701
1045
  }
702
1046
  };
703
1047
 
704
- export { ApiError, AuthenticationError, Fhirfly, FhirflyError, NetworkError, NotFoundError, QuotaExceededError, RateLimitError, ServerError, TimeoutError, ValidationError };
1048
+ export { ApiError, AuthenticationError, Fhirfly, FhirflyError, NetworkError, NotFoundError, QuotaExceededError, RateLimitError, ServerError, TimeoutError, TokenManager, ValidationError };
705
1049
  //# sourceMappingURL=index.js.map
706
1050
  //# sourceMappingURL=index.js.map