@ovok/core 0.4.3 → 0.4.5
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 +221 -4
- package/dist/client/auth/errors.d.ts +3 -0
- package/dist/client/auth/errors.js +11 -0
- package/dist/client/auth/methods/index.d.ts +1 -0
- package/dist/client/auth/methods/index.js +1 -0
- package/dist/client/auth/methods/registerPractitioner.d.ts +4 -0
- package/dist/client/auth/methods/registerPractitioner.js +15 -0
- package/dist/client/auth/types/RegisterPractitionerBody.d.ts +2 -0
- package/dist/client/auth/types/RegisterPractitionerBody.js +2 -0
- package/dist/client/email/index.d.ts +2 -0
- package/dist/client/email/index.js +18 -0
- package/dist/client/email/methods.d.ts +3 -0
- package/dist/client/email/methods.js +6 -0
- package/dist/client/email/types.d.ts +21 -0
- package/dist/client/email/types.js +2 -0
- package/dist/client/errors/ovok-error.d.ts +6 -0
- package/dist/client/errors/ovok-error.js +158 -26
- package/dist/client/errors/rate-limit-error.js +4 -0
- package/dist/client/invites/index.d.ts +3 -0
- package/dist/client/invites/index.js +19 -0
- package/dist/client/invites/methods.d.ts +8 -0
- package/dist/client/invites/methods.js +30 -0
- package/dist/client/invites/parse-accept-invite-link.d.ts +3 -0
- package/dist/client/invites/parse-accept-invite-link.js +23 -0
- package/dist/client/invites/types.d.ts +19 -0
- package/dist/client/invites/types.js +8 -0
- package/dist/client/ovok-client.d.ts +3 -1
- package/dist/client/ovok-client.js +4 -0
- package/dist/client/patient-sharing/methods.d.ts +2 -2
- package/dist/client/patient-sharing/methods.js +7 -1
- package/dist/client/patient-sharing/types.d.ts +30 -3
- package/dist/client/platform/methods.d.ts +2 -1
- package/dist/client/platform/methods.js +4 -0
- package/dist/client/platform/types.d.ts +10 -1
- package/dist/client/platform/types.js +15 -2
- package/dist/client/signals/errors.js +3 -104
- package/dist/client/signals/methods.d.ts +17 -4
- package/dist/client/signals/methods.js +47 -24
- package/dist/client/signals/types.d.ts +74 -1
- package/dist/conformance/api-requirements.js +13 -0
- package/dist/conformance/capability-requirements.js +1 -1
- package/dist/hooks/email-hooks.d.ts +3 -0
- package/dist/hooks/email-hooks.js +46 -0
- package/dist/hooks/index.d.ts +3 -1
- package/dist/hooks/index.js +9 -2
- package/dist/hooks/invites-hooks.d.ts +5 -0
- package/dist/hooks/invites-hooks.js +11 -0
- package/dist/hooks/patient-sharing-hooks.d.ts +2 -2
- package/dist/hooks/platform-hooks.d.ts +5 -1
- package/dist/hooks/platform-hooks.js +3 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/types/ValidationError.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,6 +75,57 @@ const features = await client.getProjectFeatures();
|
|
|
75
75
|
const locales = await client.getLocales();
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
The `values` property contains the six project text settings: `PATIENT_APP_URL`,
|
|
79
|
+
`PRACTITIONER_APP_URL`, `PATIENT_SIGNUP_URL`, `CLINICIAN_DASHBOARD_URL`,
|
|
80
|
+
`DEFAULT_PRACTITIONER_ACCESS_POLICY`, and `CLINICIAN_INVITE_ACCESS_POLICY`. Unset
|
|
81
|
+
values are `null`. Set or clear one with `updateProjectTextSetting(key, value)`;
|
|
82
|
+
pass `null` to clear it. `useUpdateProjectTextSetting()` exposes the same operation
|
|
83
|
+
as a mutation. The `useProjectSettings()` request hook returns the current values.
|
|
84
|
+
Mobile clients should configure `PATIENT_APP_URL` so password-reset and invite
|
|
85
|
+
emails can link back to the patient app. Email links for each audience use that
|
|
86
|
+
audience's configured app URL; they do not use a mobile request's `Origin` header.
|
|
87
|
+
|
|
88
|
+
Any project practitioner can read settings; only a project admin can update text
|
|
89
|
+
values. URL values are trimmed, limited to 2048 characters, and must use HTTPS
|
|
90
|
+
(HTTP is allowed for localhost), an allowed origin, and no credentials, query,
|
|
91
|
+
or fragment. Empty strings are invalid; pass `null` to clear a value. Access
|
|
92
|
+
policies must be `AccessPolicy/<id>` references in the project. The older
|
|
93
|
+
`PATIENT_SIGNUP_URL` and `CLINICIAN_DASHBOARD_URL` settings remain available as
|
|
94
|
+
fallbacks; `PATIENT_SIGNUP_URL` can also be a full continuation link. For an
|
|
95
|
+
audience's base URL, Core checks its project app URL, the parent project's app
|
|
96
|
+
URL, the older setting, the request origin for self-service email only, then
|
|
97
|
+
the configured environment fallback. It never uses a request origin to build a
|
|
98
|
+
link for the other audience. An older Core server may omit `values` and return
|
|
99
|
+
404 for the write route, so only rely on these methods after the matching Core
|
|
100
|
+
release is deployed.
|
|
101
|
+
The server can return `INVALID_SETTING_VALUE` (422), `APP_URL_NOT_ALLOWED`
|
|
102
|
+
(400), or `UNKNOWN_SETTING` (400; the response lists accepted keys). A code-less
|
|
103
|
+
409 means settings changed and the screen should reload before retrying. A 403
|
|
104
|
+
means the caller is not a project admin. `readOvokError(error)` reads the status,
|
|
105
|
+
code, user-facing message, and validation field errors from a Medplum error.
|
|
106
|
+
`IOT_ENABLED` is not a supported project setting.
|
|
107
|
+
|
|
108
|
+
The email-template catalog is available to authenticated project practitioners:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const { data: templateCatalog, loading, error, reload } = useEmailTemplates();
|
|
112
|
+
const templates = templateCatalog?.templates;
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The equivalent client call is `client.getEmailTemplates()`, which reads
|
|
116
|
+
`GET /v1/email/templates`. Each template includes its name, audience, parameter
|
|
117
|
+
schema, full example, and provider syntax for SendGrid, Brevo, Resend, and SMTP.
|
|
118
|
+
Use this catalog to explain template variables to administrators; it does not
|
|
119
|
+
edit the project's email ConceptMap. Core validates the parameters when an email
|
|
120
|
+
is sent. A send with invalid parameters is refused; SMTP uses strict Handlebars,
|
|
121
|
+
Resend renders nulls as empty strings and lists as newline-delimited values.
|
|
122
|
+
Every template also includes nullable `baseUrl`, `projectName`, and `tenantCode`
|
|
123
|
+
params. The list can grow, so treat template names as strings. The older
|
|
124
|
+
unversioned `/email/templates` route has a different response shape and is not
|
|
125
|
+
used. An older server may return 404 until the matching Core release is
|
|
126
|
+
deployed. The request requires a project practitioner (401 without a session,
|
|
127
|
+
403 for a non-practitioner).
|
|
128
|
+
|
|
78
129
|
All methods preserve the backend HTTP verbs and return the backend response shapes. Billing is
|
|
79
130
|
not included until Ovok publishes a supported billing contract.
|
|
80
131
|
|
|
@@ -182,16 +233,59 @@ non-empty `reason`; the backend decides whether the caller has the required capa
|
|
|
182
233
|
`shared` settings tenant is read-only. Signal errors are exposed as `SignalsError` values and can
|
|
183
234
|
be checked with `isSignalsError`.
|
|
184
235
|
|
|
185
|
-
Use `enableSignalsForPatient` to enroll an existing patient. It is idempotent
|
|
186
|
-
`
|
|
187
|
-
|
|
188
|
-
|
|
236
|
+
Use `enableSignalsForPatient` to enroll an existing patient. It is idempotent, always returns
|
|
237
|
+
`enabled: true` on success, and requires the caller to have `signals_config:update` and update
|
|
238
|
+
access to that patient. A 409 conflict and 503 failures (including `signals_mint_failed`) are
|
|
239
|
+
retryable; use a bounded retry with backoff and do not retry other statuses automatically. The
|
|
240
|
+
SDK maps backend responses to `SignalsError`; read the `code`, `status`, `fieldErrors`, and
|
|
241
|
+
`message` properties rather than trying to infer a code from a reason phrase. React applications
|
|
242
|
+
can call `useEnableSignalsForPatient().mutate(patientId)`; the hook exposes the standard mutation
|
|
243
|
+
`loading`, `error`, and `data` state.
|
|
189
244
|
|
|
190
245
|
`useSignalsAlerts` accumulates pages through `loadMore`. Check `hasMore` rather than the current
|
|
191
246
|
item count: access filtering can produce a short or empty page with another cursor. Query changes
|
|
192
247
|
and `reload` start again at the first page. The hook exposes `reload` so a screen can refresh its
|
|
193
248
|
alert list after `useAcknowledgeSignalsAlert` succeeds.
|
|
194
249
|
|
|
250
|
+
### Raw Signals alerts
|
|
251
|
+
|
|
252
|
+
`listSignalsAlerts` reads episodes and needs the project's `episodicAlerts` (409
|
|
253
|
+
`signals_episodes_disabled` otherwise). To work with individual alerts instead, whether or not
|
|
254
|
+
`episodicAlerts` is on, use the raw alert methods:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
const open = await client.listSignalsRawAlerts({
|
|
258
|
+
acknowledged: false,
|
|
259
|
+
codes: ["8867-4", "9279-1"],
|
|
260
|
+
since: "2026-10-01T00:00:00Z",
|
|
261
|
+
limit: 50,
|
|
262
|
+
});
|
|
263
|
+
const alert = await client.getSignalsRawAlert(open.items[0].id);
|
|
264
|
+
const acked = await client.acknowledgeSignalsRawAlert(alert.id);
|
|
265
|
+
// acked.acknowledgedAt and acked.acknowledgedActor record when and by whom.
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Each `SignalsRawAlert` carries Signals' own `id`, `createdAt` and `resolvedAt`. `status` is Signals'
|
|
269
|
+
state of the reading: `ACTIVE` while it breaches, `RESOLVED` once it recovered or the alert was
|
|
270
|
+
acknowledged. An alert stays open for the practitioner until it is acknowledged, so list the
|
|
271
|
+
worklist with `acknowledged: false` rather than `status: "ACTIVE"`. Acknowledging resolves the
|
|
272
|
+
alert, and the next breaching reading opens a new one. It is idempotent: an acknowledged alert keeps
|
|
273
|
+
its first `acknowledgedAt` and `acknowledgedActor`.
|
|
274
|
+
|
|
275
|
+
Filters: `status`, `acknowledged`, `patientId` (a Medplum Patient id), `codes` (LOINC codes), and
|
|
276
|
+
`since` (inclusive) and `until` (exclusive) on `createdAt`, as date-times with `Z` or an offset.
|
|
277
|
+
Pages hold at most 50 alerts. As with episodes, a page can be short or empty and still have a
|
|
278
|
+
`nextCursor`, because alerts of residents the caller can't read are left out; page until
|
|
279
|
+
`nextCursor` is null, and pass it back unchanged: it is sealed to the project.
|
|
280
|
+
|
|
281
|
+
Alerts raised before a project starts acknowledging are unacknowledged too, so a new worklist
|
|
282
|
+
should bound its history with `since`. Each row also carries the breaching reading (`valueNumber`
|
|
283
|
+
or `valueString`), the rule's `message` and the alert's `reason`. Acknowledging a raw alert does not
|
|
284
|
+
acknowledge an episode: with `episodicAlerts` on, keep a screen to one of the two. Every request is scoped to the
|
|
285
|
+
caller's project and the residents their access policy allows: an alert outside that scope answers
|
|
286
|
+
404, and acknowledging needs `signals_alerts:update` and update access to the resident (403 when the
|
|
287
|
+
caller can only read them).
|
|
288
|
+
|
|
195
289
|
## CareHub APIs
|
|
196
290
|
|
|
197
291
|
CareHub methods cover the verified `/v1/slim/*` and `/v2/slim/*` application routes for
|
|
@@ -352,6 +446,129 @@ await client.register({ ...registerBody, continueAsPatientToken });
|
|
|
352
446
|
The token is optional and is sent only when provided. An invalid or expired token is rejected
|
|
353
447
|
with HTTP 400 and the message “The continue-as-patient link is invalid or has expired.”
|
|
354
448
|
|
|
449
|
+
### Practitioner self-registration
|
|
450
|
+
|
|
451
|
+
`registerPractitioner()` posts a new practitioner account to
|
|
452
|
+
`POST /auth/tenant/Practitioner/register` and returns an `AuthResponse`. It checks
|
|
453
|
+
`passwordConfirm` locally and never sends that confirmation field. Email is
|
|
454
|
+
lowercased by Core and password must be 1–128 characters. The route is limited to
|
|
455
|
+
five requests per minute, and validation failures return 422 field errors. The project
|
|
456
|
+
must enable `PRACTITIONER_REGISTRATION_ENABLED` and configure a default
|
|
457
|
+
practitioner access policy (prefer `DEFAULT_PRACTITIONER_ACCESS_POLICY`, falling
|
|
458
|
+
back to `CLINICIAN_INVITE_ACCESS_POLICY`). Registration does not verify the
|
|
459
|
+
email address and does not send an email; do not treat the address as verified.
|
|
460
|
+
Core deliberately holds registration failures for at least a second, so a brief
|
|
461
|
+
spinner while the request is pending is expected.
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
const result = await client.registerPractitioner(registerBody);
|
|
465
|
+
await client.setActiveLogin(result);
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Both patient and practitioner registration intentionally return the same
|
|
469
|
+
`400 Registration failed.` when an email already belongs to an account. Show a
|
|
470
|
+
neutral form-level message with sign-in and password-reset links. Do not identify
|
|
471
|
+
the email as taken or retry automatically. `isRegistrationFailed(error)` matches
|
|
472
|
+
this exact response. The practitioner route can also return 403 when registration
|
|
473
|
+
is disabled, 409 `practitioner_registration_not_configured` when no default
|
|
474
|
+
policy is configured, 404 for an unknown tenant, 422 for invalid fields, and 429
|
|
475
|
+
when its five-per-minute limit is reached. `readOvokError(error)` exposes the
|
|
476
|
+
status/code/message and field errors for these responses.
|
|
477
|
+
|
|
478
|
+
## Patient sharing and invitations
|
|
479
|
+
|
|
480
|
+
Patient sharing settings are read and written through `getPatientSharingSettings()`
|
|
481
|
+
and `updatePatientSharingSettings()`. The update always includes `enabled`; when
|
|
482
|
+
changing only practitioner sharing, send the current `enabled` value too.
|
|
483
|
+
`practitionerSharingEnabled` is a separate, off-by-default setting, not a
|
|
484
|
+
`ProjectSettingKey`. It controls new practitioner invitations and acceptances;
|
|
485
|
+
turning it off does not revoke existing shares. A project admin can toggle it
|
|
486
|
+
through `PUT /v1/patient-sharing/settings`; concurrent settings changes return
|
|
487
|
+
409 and should be reloaded, and non-admins receive 403. Do not enable it for a project
|
|
488
|
+
whose practitioners use the Carehub dashboard. Map the email templates
|
|
489
|
+
`PATIENT_INVITE`, `PATIENT_ASSIGNMENT_REQUEST`,
|
|
490
|
+
`PRACTITIONER_INVITED_BY_PATIENT`, and `PRACTITIONER_PATIENT_SHARED` before
|
|
491
|
+
turning the feature on; an unmapped email can still produce the uniform 202
|
|
492
|
+
response without sending a message. `useUpdatePatientSharingSettings()` is the
|
|
493
|
+
corresponding mutation hook.
|
|
494
|
+
|
|
495
|
+
`listPatientSharing()` returns `outgoing`, `incoming`, and `practitioners`;
|
|
496
|
+
the latter lists active shares of a patient's whole record with practitioners.
|
|
497
|
+
For compatibility with older servers, the SDK defaults a missing `practitioners`
|
|
498
|
+
array to `[]`. Listing is patient-only (403 for another profile).
|
|
499
|
+
`revokePatientSharing(consentId)` uses the existing DELETE route
|
|
500
|
+
to end a share immediately or decline a pending invite. It returns 204; 404
|
|
501
|
+
(`Share not found.`) means the caller is not a party, and a code-less 409
|
|
502
|
+
(`This share kept changing; try again.`) can be retried after reloading. A
|
|
503
|
+
practitioner's whole-record share has no measurement-code list.
|
|
504
|
+
|
|
505
|
+
Invitations use four client methods, all backed by `/v1/invites/*`:
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
await client.invitePractitioner({ email, firstName, lastName }); // patient invites practitioner
|
|
509
|
+
await client.invitePatient({ email, firstName, lastName }); // practitioner invites patient
|
|
510
|
+
await client.acceptPractitionerInvite({ inviteId, code });
|
|
511
|
+
await client.acceptPatientInvite({ inviteId, code });
|
|
512
|
+
|
|
513
|
+
// Or dispatch acceptance using the signed-in profile:
|
|
514
|
+
await client.acceptInvite({ inviteId, code });
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
`useInvitePractitioner()`, `useInvitePatient()`, and `useAcceptInvite()` provide
|
|
518
|
+
React mutation states. The invite methods always return `{ status: "sent" }`
|
|
519
|
+
(HTTP 202), whether or not an email is ultimately sent. Show only “If that
|
|
520
|
+
address can be invited, an email is on its way”; the response must not reveal
|
|
521
|
+
whether the address has an account. Both names are trimmed by Core, and email
|
|
522
|
+
addresses are normalized there. A new address receives an account and a
|
|
523
|
+
set-password link; an existing account receives an accept link. If sending the
|
|
524
|
+
email fails, Core rolls back the invite. Email is limited to 254 characters;
|
|
525
|
+
the trimmed first and last names must each be 1–200 characters.
|
|
526
|
+
|
|
527
|
+
The project must enable `PRACTITIONER_SHARING_ENABLED` and the invitee's
|
|
528
|
+
`PATIENT_INVITATION_ENABLED` or `PRACTITIONER_INVITATION_ENABLED` setting. An
|
|
529
|
+
invite can also fail with 401 (no session), 403 (feature/role gate), 422 (invalid
|
|
530
|
+
body), 409 `invitation_not_configured` (missing default policy),
|
|
531
|
+
`app_url_not_configured` (missing audience app URL), or
|
|
532
|
+
`practitioner_without_access_policy` (a full-access practitioner cannot be
|
|
533
|
+
narrowed to a share policy). A code-less 409 means another invite for the same
|
|
534
|
+
address or practitioner is still being written; offer retry. A 429 means the
|
|
535
|
+
inviter has 20 pending invites. Use
|
|
536
|
+
`readOvokError(error)` and `OVOK_INVITE_ERROR_CODES` to branch on stable codes.
|
|
537
|
+
|
|
538
|
+
### Accept-invite page
|
|
539
|
+
|
|
540
|
+
Every app that can receive invitations needs a page at
|
|
541
|
+
`<app URL>/accept-invite?invite=<uuid>&code=<secret>`. Practitioner links use
|
|
542
|
+
`PRACTITIONER_APP_URL`; patient links use `PATIENT_APP_URL`. The secret is valid
|
|
543
|
+
for seven days. `parseAcceptInviteLink(url)` returns a body only when the UUID and
|
|
544
|
+
16–128 character code are present and valid; otherwise show “This link is
|
|
545
|
+
incomplete” and make no request. Once parsed, remove the code from the address
|
|
546
|
+
bar/history and do not send it to logs or analytics.
|
|
547
|
+
|
|
548
|
+
If the visitor is signed out, send them through sign-in and restore the link on
|
|
549
|
+
return. There is no preview endpoint. Once signed in, call `acceptInvite()`; it
|
|
550
|
+
chooses the route from the `Patient` or `Practitioner` profile and rejects other
|
|
551
|
+
profile types. Never accept on page load. For a patient accepting, explain that
|
|
552
|
+
their whole record will be shared and wait for an explicit Accept action. A
|
|
553
|
+
patient can decline with `revokePatientSharing(inviteId)`; practitioners have no
|
|
554
|
+
decline route and can ignore the link. After a successful acceptance, show who
|
|
555
|
+
shares with whom (use a generic name when either is null), provide a way into the
|
|
556
|
+
app, and refresh `/auth/me` because practitioner access may have changed.
|
|
557
|
+
|
|
558
|
+
Render accept failures without attempting to distinguish private cases:
|
|
559
|
+
|
|
560
|
+
- 404 `Invite not found.` means an unknown id, bad code, or invite for another
|
|
561
|
+
account. Say the invite is not for the signed-in account and offer sign-out.
|
|
562
|
+
- 409 `This invite was already used.` means accepted or cancelled; the other
|
|
563
|
+
code-less 409 (`This invite kept changing; try again.`) can be retried.
|
|
564
|
+
- 410 means expired; ask the sender for a new invite.
|
|
565
|
+
- 409 `The patient who shared their record is no longer a member of this
|
|
566
|
+
project.`, `The practitioner who invited you is no longer a member of this
|
|
567
|
+
project.`, or `More than one account in this project acts as that practitioner.`
|
|
568
|
+
cannot be accepted; ask the sender to resolve it.
|
|
569
|
+
- 401 requires sign-in; 403 means the wrong account type or a disabled gate;
|
|
570
|
+
422 means the link values are invalid.
|
|
571
|
+
|
|
355
572
|
## Large measurements
|
|
356
573
|
|
|
357
574
|
`saveMeasurement` and `saveObservations` use conditional creates for idempotent saves. Medplum
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REGISTRATION_FAILED_MESSAGE = void 0;
|
|
4
|
+
exports.isRegistrationFailed = isRegistrationFailed;
|
|
5
|
+
const ovok_error_1 = require("../errors/ovok-error");
|
|
6
|
+
exports.REGISTRATION_FAILED_MESSAGE = "Registration failed.";
|
|
7
|
+
/** Returns true when signup was refused because the address already has an account. */
|
|
8
|
+
function isRegistrationFailed(error) {
|
|
9
|
+
const details = (0, ovok_error_1.readOvokError)(error);
|
|
10
|
+
return details.status === 400 && details.message === exports.REGISTRATION_FAILED_MESSAGE;
|
|
11
|
+
}
|
|
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./login"), exports);
|
|
18
18
|
__exportStar(require("./register"), exports);
|
|
19
|
+
__exportStar(require("./registerPractitioner"), exports);
|
|
19
20
|
__exportStar(require("./refetchProfile"), exports);
|
|
20
21
|
__exportStar(require("./changePassword"), exports);
|
|
21
22
|
__exportStar(require("./requestDeleteUser"), exports);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { OvokClient } from "../../ovok-client";
|
|
2
|
+
import { AuthResponse } from "../types/AuthResponse";
|
|
3
|
+
import { RegisterPractitionerBody } from "../types/RegisterPractitionerBody";
|
|
4
|
+
export declare function registerPractitioner(this: OvokClient, body: RegisterPractitionerBody): Promise<AuthResponse>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerPractitioner = registerPractitioner;
|
|
4
|
+
async function registerPractitioner(body) {
|
|
5
|
+
if (body.password !== body.passwordConfirm) {
|
|
6
|
+
throw new Error("Password and password confirm do not match");
|
|
7
|
+
}
|
|
8
|
+
return this.post("auth/tenant/Practitioner/register", {
|
|
9
|
+
email: body.email,
|
|
10
|
+
password: body.password,
|
|
11
|
+
name: body.name,
|
|
12
|
+
surname: body.surname,
|
|
13
|
+
tenantCode: body.tenantCode,
|
|
14
|
+
}, "application/json");
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./methods"), exports);
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type OvokEmailAudience = "patient" | "practitioner";
|
|
2
|
+
export type OvokEmailTemplateParam = {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
nullable: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type OvokEmailTemplate = {
|
|
8
|
+
name: string;
|
|
9
|
+
audience: OvokEmailAudience;
|
|
10
|
+
params: OvokEmailTemplateParam[];
|
|
11
|
+
example: Record<string, unknown>;
|
|
12
|
+
syntax: {
|
|
13
|
+
sendgrid: string;
|
|
14
|
+
brevo: string;
|
|
15
|
+
resend: string;
|
|
16
|
+
smtp: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type OvokEmailTemplates = {
|
|
20
|
+
templates: OvokEmailTemplate[];
|
|
21
|
+
};
|
|
@@ -3,5 +3,11 @@ export type ParsedOvokError = {
|
|
|
3
3
|
message: string;
|
|
4
4
|
fieldErrors: ValidationError[];
|
|
5
5
|
};
|
|
6
|
+
export type OvokErrorDetails = ParsedOvokError & {
|
|
7
|
+
status: number | undefined;
|
|
8
|
+
code: string | undefined;
|
|
9
|
+
};
|
|
10
|
+
/** Reads the error details Medplum preserves from Ovok's non-FHIR responses. */
|
|
11
|
+
export declare const readOvokError: (error: unknown) => OvokErrorDetails;
|
|
6
12
|
/** Normalizes Ovok's Nest and FHIR error envelopes without exposing transport details. */
|
|
7
13
|
export declare const parseOvokError: (error: unknown) => ParsedOvokError;
|
|
@@ -1,34 +1,166 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseOvokError = void 0;
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
exports.parseOvokError = exports.readOvokError = void 0;
|
|
4
|
+
const STATUS_BY_BARE_CODE = {
|
|
5
|
+
INVALID_SETTING_VALUE: 422,
|
|
6
|
+
APP_URL_NOT_ALLOWED: 400,
|
|
7
|
+
UNKNOWN_SETTING: 400,
|
|
8
|
+
app_url_not_configured: 409,
|
|
9
|
+
};
|
|
10
|
+
const HTTP_REASON_PHRASES = new Set([
|
|
11
|
+
"bad request",
|
|
12
|
+
"unauthorized",
|
|
13
|
+
"forbidden",
|
|
14
|
+
"not found",
|
|
15
|
+
"conflict",
|
|
16
|
+
"too many requests",
|
|
17
|
+
"internal server error",
|
|
18
|
+
"service unavailable",
|
|
19
|
+
]);
|
|
9
20
|
const asRecord = (value) => value && typeof value === "object" ? value : undefined;
|
|
10
|
-
/**
|
|
11
|
-
const
|
|
12
|
-
var _a, _b;
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
: error
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
21
|
+
/** Reads the error details Medplum preserves from Ovok's non-FHIR responses. */
|
|
22
|
+
const readOvokError = (error) => {
|
|
23
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
24
|
+
const root = asRecord(error);
|
|
25
|
+
const response = asRecord(root === null || root === void 0 ? void 0 : root.response);
|
|
26
|
+
const messagePayload = error instanceof Error
|
|
27
|
+
? parseJson(error.message)
|
|
28
|
+
: parseJson(error);
|
|
29
|
+
const payload = (_f = (_e = (_d = (_c = (_b = (_a = asRecord(messagePayload)) !== null && _a !== void 0 ? _a : asRecord(root === null || root === void 0 ? void 0 : root.body)) !== null && _b !== void 0 ? _b : asRecord(root === null || root === void 0 ? void 0 : root.data)) !== null && _c !== void 0 ? _c : asRecord(response === null || response === void 0 ? void 0 : response.data)) !== null && _d !== void 0 ? _d : asRecord(response === null || response === void 0 ? void 0 : response.body)) !== null && _e !== void 0 ? _e : asRecord(root === null || root === void 0 ? void 0 : root.outcome)) !== null && _f !== void 0 ? _f : root;
|
|
30
|
+
const nestedError = asRecord(payload === null || payload === void 0 ? void 0 : payload.error);
|
|
31
|
+
const bareMessage = typeof error === "string"
|
|
32
|
+
? error
|
|
33
|
+
: error instanceof Error
|
|
34
|
+
? error.message
|
|
35
|
+
: undefined;
|
|
36
|
+
const bareCode = bareMessage && /^[A-Za-z][A-Za-z0-9_]*$/.test(bareMessage)
|
|
37
|
+
&& !HTTP_REASON_PHRASES.has(bareMessage.toLowerCase())
|
|
38
|
+
? bareMessage
|
|
39
|
+
: undefined;
|
|
40
|
+
const code = (_k = (_j = (_h = (_g = stringValue(payload === null || payload === void 0 ? void 0 : payload.code)) !== null && _g !== void 0 ? _g : stringValue(nestedError === null || nestedError === void 0 ? void 0 : nestedError.code)) !== null && _h !== void 0 ? _h : codeValue(payload === null || payload === void 0 ? void 0 : payload.error)) !== null && _j !== void 0 ? _j : bareCode) !== null && _k !== void 0 ? _k : codeFromOutcome((_l = payload === null || payload === void 0 ? void 0 : payload.outcome) !== null && _l !== void 0 ? _l : payload);
|
|
41
|
+
const status = (_t = (_r = (_q = (_p = (_o = (_m = numberValue(root === null || root === void 0 ? void 0 : root.status)) !== null && _m !== void 0 ? _m : numberValue(root === null || root === void 0 ? void 0 : root.statusCode)) !== null && _o !== void 0 ? _o : numberValue(response === null || response === void 0 ? void 0 : response.status)) !== null && _p !== void 0 ? _p : numberValue(payload === null || payload === void 0 ? void 0 : payload.status)) !== null && _q !== void 0 ? _q : numberValue(payload === null || payload === void 0 ? void 0 : payload.statusCode)) !== null && _r !== void 0 ? _r : statusFromOutcome((_s = payload === null || payload === void 0 ? void 0 : payload.outcome) !== null && _s !== void 0 ? _s : payload)) !== null && _t !== void 0 ? _t : (code ? STATUS_BY_BARE_CODE[code] : undefined);
|
|
42
|
+
const fieldErrors = readFieldErrors(payload);
|
|
27
43
|
return {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
status,
|
|
45
|
+
code,
|
|
46
|
+
message: readMessage(payload, error),
|
|
31
47
|
fieldErrors,
|
|
32
48
|
};
|
|
33
49
|
};
|
|
50
|
+
exports.readOvokError = readOvokError;
|
|
51
|
+
/** Normalizes Ovok's Nest and FHIR error envelopes without exposing transport details. */
|
|
52
|
+
const parseOvokError = (error) => {
|
|
53
|
+
const { message, fieldErrors } = (0, exports.readOvokError)(error);
|
|
54
|
+
return { message, fieldErrors };
|
|
55
|
+
};
|
|
34
56
|
exports.parseOvokError = parseOvokError;
|
|
57
|
+
function readMessage(payload, error) {
|
|
58
|
+
var _a, _b, _c;
|
|
59
|
+
const nestedMessage = stringValue((_a = asRecord(payload === null || payload === void 0 ? void 0 : payload.error)) === null || _a === void 0 ? void 0 : _a.message);
|
|
60
|
+
const message = (_c = (_b = stringValue(payload === null || payload === void 0 ? void 0 : payload.message)) !== null && _b !== void 0 ? _b : nestedMessage) !== null && _c !== void 0 ? _c : payload === null || payload === void 0 ? void 0 : payload.message;
|
|
61
|
+
if (typeof message === "string" && message.length > 0) {
|
|
62
|
+
return message;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(message)) {
|
|
65
|
+
const messages = message
|
|
66
|
+
.map((entry) => { var _a; return stringValue((_a = asRecord(entry)) === null || _a === void 0 ? void 0 : _a.message); })
|
|
67
|
+
.filter((entry) => entry !== undefined);
|
|
68
|
+
if (messages.length > 0) {
|
|
69
|
+
return messages.join("; ");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (error instanceof Error) {
|
|
73
|
+
return error.message;
|
|
74
|
+
}
|
|
75
|
+
return typeof error === "string" ? error : String(error);
|
|
76
|
+
}
|
|
77
|
+
function readFieldErrors(payload) {
|
|
78
|
+
const messageErrors = Array.isArray(payload === null || payload === void 0 ? void 0 : payload.message) ? payload.message : [];
|
|
79
|
+
const nested = asRecord(payload === null || payload === void 0 ? void 0 : payload.error);
|
|
80
|
+
const rawErrors = Array.isArray(nested === null || nested === void 0 ? void 0 : nested.errors)
|
|
81
|
+
? nested.errors
|
|
82
|
+
: Array.isArray(payload === null || payload === void 0 ? void 0 : payload.errors)
|
|
83
|
+
? payload.errors
|
|
84
|
+
: messageErrors;
|
|
85
|
+
return rawErrors.flatMap((value) => {
|
|
86
|
+
const record = asRecord(value);
|
|
87
|
+
if (!record || typeof record.message !== "string") {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const path = Array.isArray(record.path)
|
|
91
|
+
? record.path.map((segment) => String(segment))
|
|
92
|
+
: [];
|
|
93
|
+
return [{
|
|
94
|
+
...(stringValue(record.code) ? { code: stringValue(record.code) } : {}),
|
|
95
|
+
message: record.message,
|
|
96
|
+
path,
|
|
97
|
+
...(stringValue(record.validation)
|
|
98
|
+
? { validation: stringValue(record.validation) }
|
|
99
|
+
: {}),
|
|
100
|
+
}];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function codeValue(value) {
|
|
104
|
+
return typeof value === "string" && /^[a-z][a-z0-9_]+$/.test(value)
|
|
105
|
+
? value
|
|
106
|
+
: undefined;
|
|
107
|
+
}
|
|
108
|
+
function codeFromOutcome(value) {
|
|
109
|
+
var _a, _b;
|
|
110
|
+
const outcome = asRecord(value);
|
|
111
|
+
const issues = Array.isArray(outcome === null || outcome === void 0 ? void 0 : outcome.issue) ? outcome.issue : [];
|
|
112
|
+
for (const issue of issues) {
|
|
113
|
+
const record = asRecord(issue);
|
|
114
|
+
const details = asRecord(record === null || record === void 0 ? void 0 : record.details);
|
|
115
|
+
const coding = Array.isArray(details === null || details === void 0 ? void 0 : details.coding) ? details.coding : [];
|
|
116
|
+
for (const entry of coding) {
|
|
117
|
+
const code = codeValue((_a = asRecord(entry)) === null || _a === void 0 ? void 0 : _a.code);
|
|
118
|
+
if (code === null || code === void 0 ? void 0 : code.startsWith("signals_")) {
|
|
119
|
+
return code;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const diagnostics = parseJson(record === null || record === void 0 ? void 0 : record.diagnostics);
|
|
123
|
+
const diagnostic = asRecord(diagnostics);
|
|
124
|
+
const code = (_b = codeValue(diagnostic === null || diagnostic === void 0 ? void 0 : diagnostic.error)) !== null && _b !== void 0 ? _b : codeValue(diagnostic === null || diagnostic === void 0 ? void 0 : diagnostic.code);
|
|
125
|
+
if (code) {
|
|
126
|
+
return code;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
function statusFromOutcome(value) {
|
|
132
|
+
var _a, _b;
|
|
133
|
+
const outcome = asRecord(value);
|
|
134
|
+
const issues = Array.isArray(outcome === null || outcome === void 0 ? void 0 : outcome.issue) ? outcome.issue : [];
|
|
135
|
+
const statusByIssue = {
|
|
136
|
+
conflict: 409,
|
|
137
|
+
forbidden: 403,
|
|
138
|
+
"not-found": 404,
|
|
139
|
+
"too-many-requests": 429,
|
|
140
|
+
throttled: 429,
|
|
141
|
+
};
|
|
142
|
+
for (const issue of issues) {
|
|
143
|
+
const status = statusByIssue[String((_b = (_a = asRecord(issue)) === null || _a === void 0 ? void 0 : _a.code) !== null && _b !== void 0 ? _b : "")];
|
|
144
|
+
if (status) {
|
|
145
|
+
return status;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
function numberValue(value) {
|
|
151
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
152
|
+
}
|
|
153
|
+
function stringValue(value) {
|
|
154
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
155
|
+
}
|
|
156
|
+
function parseJson(value) {
|
|
157
|
+
if (typeof value !== "string") {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(value);
|
|
162
|
+
}
|
|
163
|
+
catch (_a) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -4,6 +4,7 @@ exports.RateLimitError = void 0;
|
|
|
4
4
|
exports.isRateLimitError = isRateLimitError;
|
|
5
5
|
exports.mapRateLimitError = mapRateLimitError;
|
|
6
6
|
const core_1 = require("@medplum/core");
|
|
7
|
+
const ovok_error_1 = require("./ovok-error");
|
|
7
8
|
const RATE_LIMIT_RESET_EXTENSION_URL = "https://medplum.com/fhir/StructureDefinition/rate-limit-reset";
|
|
8
9
|
/** A typed error returned when the server asks the client to slow down. */
|
|
9
10
|
class RateLimitError extends Error {
|
|
@@ -34,6 +35,9 @@ function mapRateLimitError(error) {
|
|
|
34
35
|
if (error instanceof core_1.OperationOutcomeError && isRateLimitOutcome(error.outcome)) {
|
|
35
36
|
return new RateLimitError(rateLimitResetFromOutcome(error.outcome), { cause: error });
|
|
36
37
|
}
|
|
38
|
+
if ((0, ovok_error_1.readOvokError)(error).status === 429) {
|
|
39
|
+
return new RateLimitError(undefined, { cause: error });
|
|
40
|
+
}
|
|
37
41
|
return error;
|
|
38
42
|
}
|
|
39
43
|
function rateLimitResetFromOutcome(outcome) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./methods"), exports);
|
|
18
|
+
__exportStar(require("./parse-accept-invite-link"), exports);
|
|
19
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { OvokClient } from "../ovok-client";
|
|
2
|
+
import { PatientSharingPractitionerShare } from "../patient-sharing/types";
|
|
3
|
+
import { AcceptInviteBody, InviteBody, InviteSent } from "./types";
|
|
4
|
+
export declare function invitePractitioner(this: OvokClient, body: InviteBody): Promise<InviteSent>;
|
|
5
|
+
export declare function invitePatient(this: OvokClient, body: InviteBody): Promise<InviteSent>;
|
|
6
|
+
export declare function acceptPractitionerInvite(this: OvokClient, body: AcceptInviteBody): Promise<PatientSharingPractitionerShare>;
|
|
7
|
+
export declare function acceptPatientInvite(this: OvokClient, body: AcceptInviteBody): Promise<PatientSharingPractitionerShare>;
|
|
8
|
+
export declare function acceptInvite(this: OvokClient, body: AcceptInviteBody): Promise<PatientSharingPractitionerShare>;
|