@haau3/sdk 0.1.2 → 0.2.1

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.mjs CHANGED
@@ -1,3 +1,40 @@
1
+ //#region src/api-promise.ts
2
+ /**
3
+ * What every typed method returns: a promise of the answer, as before, that can also give the
4
+ * request id and timing that came with it.
5
+ *
6
+ * ```ts
7
+ * const plan = await haau3.readInsurancePlan(id);
8
+ * const { data, requestId, durationMs } = await haau3.readInsurancePlan(id).withResponse();
9
+ * ```
10
+ *
11
+ * The request is sent once, when the method is called, whichever way it is awaited.
12
+ */
13
+ var APIPromise = class extends Promise {
14
+ #response;
15
+ constructor(response) {
16
+ super((resolve) => resolve(void 0));
17
+ this.#response = response;
18
+ }
19
+ /** The answer, its request id, its timing, its status and its headers. */
20
+ withResponse() {
21
+ return this.#response;
22
+ }
23
+ then(onfulfilled, onrejected) {
24
+ return this.#response.then((response) => response.data).then(onfulfilled, onrejected);
25
+ }
26
+ catch(onrejected) {
27
+ return this.then(void 0, onrejected);
28
+ }
29
+ finally(onfinally) {
30
+ return this.then().finally(onfinally);
31
+ }
32
+ /** `Promise.prototype.then` and friends build plain promises from an APIPromise. */
33
+ static get [Symbol.species]() {
34
+ return Promise;
35
+ }
36
+ };
37
+ //#endregion
1
38
  //#region src/errors.ts
2
39
  /** What every error this package throws has. */
3
40
  var Haau3Error = class extends Error {
@@ -28,9 +65,15 @@ function attachBody(error, body) {
28
65
  var AuthError = class extends Haau3Error {
29
66
  name = "AuthError";
30
67
  status;
31
- constructor(message, status, requestId, body) {
68
+ /** How many attempts were made. */
69
+ attempts;
70
+ /** Milliseconds from the first attempt to the answer. */
71
+ durationMs;
72
+ constructor(message, status, requestId, body, timing) {
32
73
  super(message, requestId);
33
74
  this.status = status;
75
+ this.attempts = timing?.attempts ?? 1;
76
+ this.durationMs = timing?.durationMs;
34
77
  attachBody(this, body);
35
78
  }
36
79
  };
@@ -42,9 +85,15 @@ var AuthError = class extends Haau3Error {
42
85
  var RefusedError = class extends Haau3Error {
43
86
  name = "RefusedError";
44
87
  status;
45
- constructor(message, status, requestId, body) {
88
+ /** How many attempts were made. */
89
+ attempts;
90
+ /** Milliseconds from the first attempt to the answer. */
91
+ durationMs;
92
+ constructor(message, status, requestId, body, timing) {
46
93
  super(message, requestId);
47
94
  this.status = status;
95
+ this.attempts = timing?.attempts ?? 1;
96
+ this.durationMs = timing?.durationMs;
48
97
  attachBody(this, body);
49
98
  }
50
99
  };
@@ -61,19 +110,22 @@ var NoAnswerError = class extends Haau3Error {
61
110
  retryAfter;
62
111
  /** How many attempts were made. */
63
112
  attempts;
113
+ /** Milliseconds from the first attempt to the last failure, waits between attempts included. */
114
+ durationMs;
64
115
  constructor(message, details) {
65
116
  super(message, details.requestId, details.cause === void 0 ? void 0 : { cause: details.cause });
66
117
  this.reason = details.reason;
67
118
  this.status = details.status;
68
119
  this.retryAfter = details.retryAfter;
69
120
  this.attempts = details.attempts;
121
+ this.durationMs = details.durationMs;
70
122
  attachBody(this, details.body);
71
123
  }
72
124
  };
73
125
  //#endregion
74
126
  //#region src/version.ts
75
127
  /** This package's version, sent in the `User-Agent` header. A test holds it to package.json. */
76
- const VERSION = "0.1.2";
128
+ const VERSION = "0.2.1";
77
129
  //#endregion
78
130
  //#region src/transport.ts
79
131
  const DEFAULT_BASE_URL = "https://api.haau3.com";
@@ -90,6 +142,18 @@ function inBrowser() {
90
142
  const g = globalThis;
91
143
  return typeof g.window !== "undefined" && typeof g.document !== "undefined" && !(g.navigator?.userAgent ?? "").includes("jsdom");
92
144
  }
145
+ /**
146
+ * Whether this runtime's fetch takes a `cache` field. Node's does. Some edge runtimes have refused
147
+ * the field outright, so the no-store default is sent only where it is taken.
148
+ */
149
+ function cacheFieldTaken() {
150
+ try {
151
+ new Request("https://api.haau3.com/", { cache: "no-store" });
152
+ return true;
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
93
157
  function envKey() {
94
158
  return globalThis.process?.env?.HAAU3_API_KEY || void 0;
95
159
  }
@@ -137,6 +201,13 @@ function readErrorBody(body) {
137
201
  value: body
138
202
  };
139
203
  }
204
+ /** A resource type with its article: "a Bundle", "an InsurancePlan". */
205
+ const withArticle = (type) => `${/^[AEIOU]/.test(type) ? "an" : "a"} ${type}`;
206
+ /** How an answer is named in a message: its resource type when it has a plausible one, never a value. */
207
+ function describeAnswer(body) {
208
+ const type = body && typeof body === "object" ? body.resourceType : void 0;
209
+ return typeof type === "string" && /^[A-Z][A-Za-z]{0,63}$/.test(type) ? withArticle(type) : "something that is not a FHIR resource";
210
+ }
140
211
  function noAnswerReason(status) {
141
212
  if (status === 429) return "rate-limited";
142
213
  if (status === 502 || status === 503) return "unavailable";
@@ -147,6 +218,8 @@ function noAnswerReason(status) {
147
218
  var Transport = class {
148
219
  #apiKey;
149
220
  #fetch;
221
+ #fetchOptions;
222
+ #noStoreTaken;
150
223
  /** Where requests go. */
151
224
  baseURL;
152
225
  /** Milliseconds one attempt may take. */
@@ -161,6 +234,8 @@ var Transport = class {
161
234
  if (!fetchImpl) throw new Haau3Error("No fetch: pass one as the fetch option. Node 22 and newer have one.");
162
235
  this.#apiKey = apiKey;
163
236
  this.#fetch = fetchImpl;
237
+ this.#fetchOptions = options.fetchOptions ?? {};
238
+ this.#noStoreTaken = cacheFieldTaken();
164
239
  this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
165
240
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
166
241
  this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
@@ -181,50 +256,84 @@ var Transport = class {
181
256
  durationMs: sent.durationMs
182
257
  };
183
258
  }
184
- /** Runs one operation of the spec and returns its answer, or throws one of the three errors. */
185
- async operation(call, options = {}) {
259
+ /**
260
+ * Runs one operation of the spec and returns its answer, or throws one of the three errors. The
261
+ * answer's `.withResponse()` gives its request id and timing too.
262
+ */
263
+ operation(call, options = {}) {
186
264
  let path = call.path;
187
265
  for (const [name, value] of Object.entries(call.pathParams ?? {})) path = path.replace(`{${name}}`, encodeURIComponent(value));
188
- return this.answer(call.method, this.#url(path, call.query), call.body, options, call.path);
266
+ let url;
267
+ try {
268
+ url = this.#url(path, call.query);
269
+ } catch (error) {
270
+ return new APIPromise(Promise.reject(error));
271
+ }
272
+ return new APIPromise(this.respond(call.method, url, call.body, options, call.path, call.expects));
273
+ }
274
+ /** Sends a request and returns its answer only. See {@link Transport.respond}. */
275
+ async answer(method, url, body, options, label, expects) {
276
+ return (await this.respond(method, url, body, options, label, expects)).data;
189
277
  }
190
278
  /**
191
279
  * Sends a request and reads its answer. `label` names the request in an error message: the
192
280
  * operation's path with its `{id}` left in, so no value a caller sent ends up in a message.
281
+ * `expects`, when set, are the FHIR resource types the answer may be; any other is not an answer.
193
282
  */
194
- async answer(method, url, body, options, label) {
195
- const { response, text, attempts } = await this.#send(method, url, body, options, label);
283
+ async respond(method, url, body, options, label, expects) {
284
+ const { response, text, attempts, durationMs } = await this.#send(method, url, body, options, label);
196
285
  const status = response.status;
197
286
  const requestId = response.headers.get("x-request-id") ?? void 0;
198
287
  const parsed = parseBody(text, response.headers.get("content-type"));
199
288
  const name = `${method} ${label}`;
289
+ const timing = {
290
+ attempts,
291
+ durationMs
292
+ };
200
293
  if (status >= 200 && status < 300) {
201
294
  if (parsed === void 0) throw new NoAnswerError(`${name} answered ${status} with a body that is not JSON.`, {
202
295
  reason: "unreadable",
203
- attempts,
204
296
  status,
205
- requestId
297
+ requestId,
298
+ ...timing
206
299
  });
207
- return parsed;
300
+ if (expects && !expects.includes(parsed?.resourceType)) {
301
+ const wanted = expects.map(withArticle).join(" or ");
302
+ throw new NoAnswerError(`${name} answered ${describeAnswer(parsed)} where ${wanted} was expected.`, {
303
+ reason: "unreadable",
304
+ status,
305
+ requestId,
306
+ ...timing
307
+ });
308
+ }
309
+ return {
310
+ data: parsed,
311
+ requestId,
312
+ durationMs,
313
+ attempts,
314
+ status,
315
+ headers: response.headers
316
+ };
208
317
  }
209
318
  const errorBody = readErrorBody(parsed);
210
- if (status === 401 || status === 403) throw new AuthError(`${name} was refused: the API key was not accepted (${status}).`, status, requestId, errorBody);
319
+ if (status === 401 || status === 403) throw new AuthError(`${name} was refused: the API key was not accepted (${status}).`, status, requestId, errorBody, timing);
211
320
  if (status === 429 || status >= 500) {
212
321
  const reason = noAnswerReason(status);
213
322
  throw new NoAnswerError(`${name} got no answer (${reason}, ${status}) after ${attempts} attempt${attempts === 1 ? "" : "s"}.`, {
214
323
  reason,
215
- attempts,
216
324
  status,
217
325
  retryAfter: retryAfterSeconds(response.headers),
218
326
  requestId,
219
- body: errorBody
327
+ body: errorBody,
328
+ ...timing
220
329
  });
221
330
  }
222
- if (status >= 400) throw new RefusedError(`${name} was refused (${status}).`, status, requestId, errorBody);
331
+ if (status >= 400) throw new RefusedError(`${name} was refused (${status}).`, status, requestId, errorBody, timing);
223
332
  throw new NoAnswerError(`${name} answered ${status}, which is not an answer.`, {
224
333
  reason: "unreadable",
225
- attempts,
226
334
  status,
227
- requestId
335
+ requestId,
336
+ ...timing
228
337
  });
229
338
  }
230
339
  /** A URL on this client's base, refusing any other host so the key is never sent to one. */
@@ -256,6 +365,11 @@ var Transport = class {
256
365
  for (const [name, value] of Object.entries(options.headers ?? {})) headers[name.toLowerCase()] = value;
257
366
  const timeout = options.timeout ?? this.timeout;
258
367
  const maxRetries = options.maxRetries ?? this.maxRetries;
368
+ const fetchOptions = {
369
+ ...this.#fetchOptions,
370
+ ...options.fetchOptions
371
+ };
372
+ if (this.#noStoreTaken && !("cache" in fetchOptions) && !("next" in fetchOptions)) fetchOptions.cache = "no-store";
259
373
  const started = Date.now();
260
374
  let attempt = 0;
261
375
  for (;;) {
@@ -267,6 +381,7 @@ var Transport = class {
267
381
  let text;
268
382
  try {
269
383
  response = await this.#fetch(url, {
384
+ ...fetchOptions,
270
385
  method,
271
386
  headers,
272
387
  body: payload,
@@ -284,6 +399,7 @@ var Transport = class {
284
399
  throw new NoAnswerError(`${method} ${label} got no answer (${reason}) after ${attempt} attempt${attempt === 1 ? "" : "s"}.`, {
285
400
  reason,
286
401
  attempts: attempt,
402
+ durationMs: Date.now() - started,
287
403
  cause: error
288
404
  });
289
405
  }
@@ -319,7 +435,8 @@ var Operations = class extends Transport {
319
435
  method: "GET",
320
436
  path: "/v1/fhir/r4/InsurancePlan/{id}/$in-network",
321
437
  pathParams: { id },
322
- query: params
438
+ query: params,
439
+ expects: ["Parameters"]
323
440
  }, options);
324
441
  }
325
442
  /**
@@ -333,7 +450,8 @@ var Operations = class extends Transport {
333
450
  return this.operation({
334
451
  method: "GET",
335
452
  path: "/v1/fhir/r4/CodeSystem/$lookup",
336
- query: params
453
+ query: params,
454
+ expects: ["Parameters"]
337
455
  }, options);
338
456
  }
339
457
  /**
@@ -347,7 +465,8 @@ var Operations = class extends Transport {
347
465
  return this.operation({
348
466
  method: "POST",
349
467
  path: "/v1/fhir/r4/CodeSystem/$lookup",
350
- body
468
+ body,
469
+ expects: ["Parameters"]
351
470
  }, options);
352
471
  }
353
472
  /**
@@ -361,7 +480,8 @@ var Operations = class extends Transport {
361
480
  return this.operation({
362
481
  method: "GET",
363
482
  path: "/v1/fhir/r4/CodeSystem/$validate-code",
364
- query: params
483
+ query: params,
484
+ expects: ["Parameters"]
365
485
  }, options);
366
486
  }
367
487
  /**
@@ -375,7 +495,8 @@ var Operations = class extends Transport {
375
495
  return this.operation({
376
496
  method: "POST",
377
497
  path: "/v1/fhir/r4/CodeSystem/$validate-code",
378
- body
498
+ body,
499
+ expects: ["Parameters"]
379
500
  }, options);
380
501
  }
381
502
  /**
@@ -389,7 +510,8 @@ var Operations = class extends Transport {
389
510
  return this.operation({
390
511
  method: "POST",
391
512
  path: "/v1/fhir/r4",
392
- body
513
+ body,
514
+ expects: ["Bundle"]
393
515
  }, options);
394
516
  }
395
517
  /**
@@ -403,7 +525,8 @@ var Operations = class extends Transport {
403
525
  return this.operation({
404
526
  method: "GET",
405
527
  path: "/v1/fhir/r4/metadata",
406
- query: params
528
+ query: params,
529
+ expects: ["CapabilityStatement", "TerminologyCapabilities"]
407
530
  }, options);
408
531
  }
409
532
  /**
@@ -417,7 +540,8 @@ var Operations = class extends Transport {
417
540
  return this.operation({
418
541
  method: "GET",
419
542
  path: "/v1/fhir/r4/InsurancePlan/$counties",
420
- query: params
543
+ query: params,
544
+ expects: ["Parameters"]
421
545
  }, options);
422
546
  }
423
547
  /**
@@ -472,7 +596,8 @@ var Operations = class extends Transport {
472
596
  return this.operation({
473
597
  method: "GET",
474
598
  path: "/v1/fhir/r4/Endpoint/{id}",
475
- pathParams: { id }
599
+ pathParams: { id },
600
+ expects: ["Endpoint"]
476
601
  }, options);
477
602
  }
478
603
  /**
@@ -486,7 +611,8 @@ var Operations = class extends Transport {
486
611
  return this.operation({
487
612
  method: "GET",
488
613
  path: "/v1/fhir/r4/HealthcareService/{id}",
489
- pathParams: { id }
614
+ pathParams: { id },
615
+ expects: ["HealthcareService"]
490
616
  }, options);
491
617
  }
492
618
  /**
@@ -500,7 +626,8 @@ var Operations = class extends Transport {
500
626
  return this.operation({
501
627
  method: "GET",
502
628
  path: "/v1/fhir/r4/InsurancePlan/{id}",
503
- pathParams: { id }
629
+ pathParams: { id },
630
+ expects: ["InsurancePlan"]
504
631
  }, options);
505
632
  }
506
633
  /**
@@ -514,7 +641,8 @@ var Operations = class extends Transport {
514
641
  return this.operation({
515
642
  method: "GET",
516
643
  path: "/v1/fhir/r4/Location/{id}",
517
- pathParams: { id }
644
+ pathParams: { id },
645
+ expects: ["Location"]
518
646
  }, options);
519
647
  }
520
648
  /**
@@ -528,7 +656,8 @@ var Operations = class extends Transport {
528
656
  return this.operation({
529
657
  method: "GET",
530
658
  path: "/v1/fhir/r4/Organization/{id}",
531
- pathParams: { id }
659
+ pathParams: { id },
660
+ expects: ["Organization"]
532
661
  }, options);
533
662
  }
534
663
  /**
@@ -542,7 +671,8 @@ var Operations = class extends Transport {
542
671
  return this.operation({
543
672
  method: "GET",
544
673
  path: "/v1/fhir/r4/OrganizationAffiliation/{id}",
545
- pathParams: { id }
674
+ pathParams: { id },
675
+ expects: ["OrganizationAffiliation"]
546
676
  }, options);
547
677
  }
548
678
  /**
@@ -556,7 +686,8 @@ var Operations = class extends Transport {
556
686
  return this.operation({
557
687
  method: "GET",
558
688
  path: "/v1/fhir/r4/Practitioner/{id}",
559
- pathParams: { id }
689
+ pathParams: { id },
690
+ expects: ["Practitioner"]
560
691
  }, options);
561
692
  }
562
693
  /**
@@ -570,7 +701,8 @@ var Operations = class extends Transport {
570
701
  return this.operation({
571
702
  method: "GET",
572
703
  path: "/v1/fhir/r4/PractitionerRole/{id}",
573
- pathParams: { id }
704
+ pathParams: { id },
705
+ expects: ["PractitionerRole"]
574
706
  }, options);
575
707
  }
576
708
  /**
@@ -586,7 +718,8 @@ var Operations = class extends Transport {
586
718
  return this.operation({
587
719
  method: "GET",
588
720
  path: "/v1/fhir/r4/Questionnaire/{id}",
589
- pathParams: { id }
721
+ pathParams: { id },
722
+ expects: ["Questionnaire"]
590
723
  }, options);
591
724
  }
592
725
  /**
@@ -600,7 +733,8 @@ var Operations = class extends Transport {
600
733
  return this.operation({
601
734
  method: "POST",
602
735
  path: "/v1/fhir/r4/$reconcile-medications",
603
- body
736
+ body,
737
+ expects: ["Bundle"]
604
738
  }, options);
605
739
  }
606
740
  /**
@@ -614,7 +748,8 @@ var Operations = class extends Transport {
614
748
  return this.operation({
615
749
  method: "GET",
616
750
  path: "/v1/fhir/r4/Endpoint",
617
- query: params
751
+ query: params,
752
+ expects: ["Bundle"]
618
753
  }, options);
619
754
  }
620
755
  /**
@@ -628,7 +763,8 @@ var Operations = class extends Transport {
628
763
  return this.operation({
629
764
  method: "GET",
630
765
  path: "/v1/fhir/r4/HealthcareService",
631
- query: params
766
+ query: params,
767
+ expects: ["Bundle"]
632
768
  }, options);
633
769
  }
634
770
  /**
@@ -648,7 +784,8 @@ var Operations = class extends Transport {
648
784
  return this.operation({
649
785
  method: "GET",
650
786
  path: "/v1/fhir/r4/InsurancePlan",
651
- query: params
787
+ query: params,
788
+ expects: ["Bundle"]
652
789
  }, options);
653
790
  }
654
791
  /**
@@ -662,7 +799,8 @@ var Operations = class extends Transport {
662
799
  return this.operation({
663
800
  method: "GET",
664
801
  path: "/v1/fhir/r4/Location",
665
- query: params
802
+ query: params,
803
+ expects: ["Bundle"]
666
804
  }, options);
667
805
  }
668
806
  /**
@@ -680,7 +818,8 @@ var Operations = class extends Transport {
680
818
  return this.operation({
681
819
  method: "GET",
682
820
  path: "/v1/fhir/r4/Organization",
683
- query: params
821
+ query: params,
822
+ expects: ["Bundle"]
684
823
  }, options);
685
824
  }
686
825
  /**
@@ -694,7 +833,8 @@ var Operations = class extends Transport {
694
833
  return this.operation({
695
834
  method: "GET",
696
835
  path: "/v1/fhir/r4/OrganizationAffiliation",
697
- query: params
836
+ query: params,
837
+ expects: ["Bundle"]
698
838
  }, options);
699
839
  }
700
840
  /**
@@ -712,7 +852,8 @@ var Operations = class extends Transport {
712
852
  return this.operation({
713
853
  method: "GET",
714
854
  path: "/v1/fhir/r4/Practitioner",
715
- query: params
855
+ query: params,
856
+ expects: ["Bundle"]
716
857
  }, options);
717
858
  }
718
859
  /**
@@ -730,7 +871,8 @@ var Operations = class extends Transport {
730
871
  return this.operation({
731
872
  method: "GET",
732
873
  path: "/v1/fhir/r4/PractitionerRole",
733
- query: params
874
+ query: params,
875
+ expects: ["Bundle"]
734
876
  }, options);
735
877
  }
736
878
  /**
@@ -766,7 +908,8 @@ var Operations = class extends Transport {
766
908
  return this.operation({
767
909
  method: "GET",
768
910
  path: "/v1/fhir/r4/Questionnaire",
769
- query: params
911
+ query: params,
912
+ expects: ["Bundle"]
770
913
  }, options);
771
914
  }
772
915
  /**
@@ -794,7 +937,8 @@ var Operations = class extends Transport {
794
937
  return this.operation({
795
938
  method: "GET",
796
939
  path: "/v1/fhir/r4/ValueSet/$expand",
797
- query: params
940
+ query: params,
941
+ expects: ["ValueSet"]
798
942
  }, options);
799
943
  }
800
944
  /**
@@ -808,7 +952,8 @@ var Operations = class extends Transport {
808
952
  return this.operation({
809
953
  method: "POST",
810
954
  path: "/v1/fhir/r4/ValueSet/$expand",
811
- body
955
+ body,
956
+ expects: ["ValueSet"]
812
957
  }, options);
813
958
  }
814
959
  /**
@@ -822,7 +967,8 @@ var Operations = class extends Transport {
822
967
  return this.operation({
823
968
  method: "GET",
824
969
  path: "/v1/fhir/r4/ValueSet/$validate-code",
825
- query: params
970
+ query: params,
971
+ expects: ["Parameters"]
826
972
  }, options);
827
973
  }
828
974
  /**
@@ -836,7 +982,8 @@ var Operations = class extends Transport {
836
982
  return this.operation({
837
983
  method: "POST",
838
984
  path: "/v1/fhir/r4/ValueSet/$validate-code",
839
- body
985
+ body,
986
+ expects: ["Parameters"]
840
987
  }, options);
841
988
  }
842
989
  };
@@ -878,9 +1025,9 @@ var Haau3 = class extends Operations {
878
1025
  const next = page.link?.find((link) => link.relation === "next")?.url;
879
1026
  if (!next) return;
880
1027
  const url = this.url(next);
881
- page = await this.answer("GET", url, void 0, options, url.pathname);
1028
+ page = await this.answer("GET", url, void 0, options, url.pathname, ["Bundle"]);
882
1029
  }
883
1030
  }
884
1031
  };
885
1032
  //#endregion
886
- export { AuthError, Haau3, Haau3Error, NoAnswerError, RefusedError, VERSION };
1033
+ export { APIPromise, AuthError, Haau3, Haau3Error, NoAnswerError, RefusedError, VERSION };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@haau3/sdk",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "TypeScript SDK for the haau3 platform API: scheduling, provider directory, terminology, questionnaires, plan benefits and medication lists, as typed calls.",
5
5
  "license": "MIT",
6
- "homepage": "https://docs.haau3.com",
6
+ "homepage": "https://platform.haau3.com",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/haau3/haau3-node.git"