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