-
Notifications
You must be signed in to change notification settings - Fork 68
RELEASE healthie v0 #3771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RELEASE healthie v0 #3771
Conversation
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
… 82-healthie Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
… 82-healthie Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
… 82-healthie Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
… 82-healthie Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-82 Ref: #1040 Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
feat(healthie): integration v0
WalkthroughThis change introduces comprehensive support for the "Healthie" EHR integration across the codebase. It adds new modules for Healthie-specific API interaction, patient and appointment processing, webhook subscription and handling, and JWT token management. The infrastructure is extended to support new SQS queues, Lambda functions, and secret management for Healthie. New route handlers and middleware are implemented for both internal and external API endpoints, including webhook and dashboard routes. Shared interfaces, schemas, and configuration utilities are updated to include Healthie as a recognized EHR source. Additionally, error handling, batching, and retry logic are refined in several core modules, and documentation comments are updated for clarity and accuracy. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant API
participant HealthieApi
participant SQS
participant Lambda
participant MetriportDB
%% Patient sync via dashboard or webhook
Client->>API: Request patient sync (with Healthie IDs)
API->>HealthieApi: Fetch patient data
HealthieApi-->>API: Patient data
API->>MetriportDB: Find or create patient
MetriportDB-->>API: Patient ID
API->>HealthieApi: Update patient quick notes with Metriport link
API-->>Client: Respond with patient ID
%% Patient linking via SQS/Lambda
API->>SQS: Enqueue link patient request
SQS->>Lambda: Trigger Lambda with request
Lambda->>API: Link patient (calls API endpoint)
API->>HealthieApi: Update patient quick notes
Lambda-->>SQS: Complete
%% Webhook subscription flow
API->>HealthieApi: Subscribe to webhook resource (e.g., appointment.created)
HealthieApi-->>API: Subscription confirmation (with secret)
API->>MetriportDB: Save subscription and secret
%% Webhook event handling
HealthieApi->>API: Send webhook event (appointment.created)
API->>MetriportDB: Validate mapping and settings
API->>HealthieApi: Update patient quick notes (if enabled)
API->>MetriportDB: Trigger patient sync (if enabled)
API-->>HealthieApi: Respond 200 OK
Possibly related PRs
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 21
🧹 Nitpick comments (22)
packages/shared/src/interface/external/ehr/elation/index.ts (1)
1-7
: Consider ordering exports alphabetically for readability. Alphabetical order helps maintainability as the number of exports grows. For example:-export * from "./appointment"; -export * from "./patient"; -export * from "./jwt-token"; -export * from "./problem"; -export * from "./subscription"; -export * from "./cx-mapping"; -export * from "./event"; +export * from "./appointment"; +export * from "./cx-mapping"; +export * from "./event"; +export * from "./jwt-token"; +export * from "./patient"; +export * from "./problem"; +export * from "./subscription";packages/core/src/command/write-to-storage/s3/write-to-s3-cloud.ts (1)
34-34
: Avoid shadowing imported names. Thefor
loop iterator namedchunk
shadows the importedchunk
function fromlodash
. Consider renaming the variable tobatch
,chunkBatch
, or similar for clarity:- for (const chunk of chunks) { + for (const batch of chunks) { await Promise.all( batch.map(params => { // ... }) ); - } + }packages/api/src/routes/ehr/athenahealth/patient.ts (1)
20-64
: Consider consolidating 8000 duplicate endpoint logicBoth GET and POST endpoints implement identical logic to synchronize AthenaHealth patient data. While not directly related to your current changes, consider refactoring to eliminate this duplication in the future.
You could extract the common logic to a shared function:
+const handlePatientSync = async (req: Request, res: Response) => { + const cxId = getCxIdOrFail(req); + const athenaPatientId = getFrom("params").orFail("id", req); + const athenaPracticeId = getFromQueryOrFail("practiceId", req); + const athenaDepartmentId = getFromQueryOrFail("departmentId", req); + const patientId = await syncAthenaPatientIntoMetriport({ + cxId, + athenaPracticeId, + athenaPatientId, + athenaDepartmentId, + }); + return res.status(httpStatus.OK).json(patientId); +}; router.get( "/:id", handleParams, requestLogger, - asyncHandler(async (req: Request, res: Response) => { - const cxId = getCxIdOrFail(req); - const athenaPatientId = getFrom("params").orFail("id", req); - const athenaPracticeId = getFromQueryOrFail("practiceId", req); - const athenaDepartmentId = getFromQueryOrFail("departmentId", req); - const patientId = await syncAthenaPatientIntoMetriport({ - cxId, - athenaPracticeId, - athenaPatientId, - athenaDepartmentId, - }); - return res.status(httpStatus.OK).json(patientId); - }) + asyncHandler(handlePatientSync) );packages/api/src/routes/ehr/elation/patient.ts (1)
21-62
: Consider consolidating duplicate endpoint logicBoth GET and POST endpoints implement identical logic to synchronize Elation patient data. While not directly related to your current changes, consider refactoring to eliminate this duplication in the future.
You could extract the common logic to a shared function:
+const handlePatientSync = async (req: Request, res: Response) => { + const cxId = getCxIdOrFail(req); + const elationPatientId = getFrom("params").orFail("id", req); + const elationPracticeId = getFromQueryOrFail("practiceId", req); + const patientId = await syncElationPatientIntoMetriport({ + cxId, + elationPracticeId, + elationPatientId, + }); + return res.status(httpStatus.OK).json(patientId); +}; router.get( "/:id", handleParams, processEhrPatientId(tokenEhrPatientIdQueryParam, "params"), requestLogger, - asyncHandler(async (req: Request, res: Response) => { - const cxId = getCxIdOrFail(req); - const elationPatientId = getFrom("params").orFail("id", req); - const elationPracticeId = getFromQueryOrFail("practiceId", req); - const patientId = await syncElationPatientIntoMetriport({ - cxId, - elationPracticeId, - elationPatientId, - }); - return res.status(httpStatus.OK).json(patientId); - }) + asyncHandler(handlePatientSync) );packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (1)
1-9
: Consider adding return type documentationWhile the interface is well-defined, consider adding documentation comments explaining the purpose of the interface and what the
processLinkPatient
method is expected to accomplish when it resolves successfully.+/** + * Handler for linking patients with Healthie EHR system + */ export interface HealthieLinkPatientHandler { + /** + * Process a patient linking request + * @param request Patient linking request data + * @returns Promise that resolves when linking is complete or enqueued + */ processLinkPatient(request: ProcessLinkPatientRequest): Promise<void>; }packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts (1)
1-13
: Implementation looks good but consider adding error handling.The factory pattern implementation is clean and follows the coding guidelines nicely. It properly abstracts environment-specific implementations and returns the appropriate handler.
Consider adding error handling for the case where
healthieLinkPatientQueueUrl
might not be available in non-dev environments:export function buildHealthieLinkPatientHandler(): HealthieLinkPatientHandler { if (Config.isDev()) { const waitTimeAtTheEndInMillis = 0; return new HealthieLinkPatientLocal(waitTimeAtTheEndInMillis); } const healthieLinkPatientQueueUrl = Config.getHealthieLinkPatientQueueUrl(); + if (!healthieLinkPatientQueueUrl) { + throw new Error("Healthie link patient queue URL is not configured"); + } return new HealthieLinkPatientCloud(healthieLinkPatientQueueUrl); }packages/api/src/routes/internal/jwt-token/healthie.ts (2)
15-29
: Enhance endpoint documentation with more descriptive JSDoc.The current JSDoc only includes the route path without describing the endpoint's purpose and response format.
/** * GET /internal/token/healthie + * + * Verifies the status of a Healthie JWT token. + * @param req.headers.authorization The Bearer token to verify. + * @returns {Object} Token status information with HTTP 200. */
36-52
: Enhance endpoint documentation and add token validation.Similar to the GET endpoint, the documentation is minimal. Additionally, there's no explicit check that the token exists before using it.
/** * POST /internal/token/healthie + * + * Saves a Healthie JWT token. + * @param req.headers.authorization The Bearer token to save. + * @param req.body.exp The token expiration timestamp. + * @param req.body.data The token data conforming to healthieDashJwtTokenDataSchema. + * @returns HTTP 200 if successful. */ router.post( "/", requestLogger, asyncHandler(async (req: Request, res: Response) => { const token = getAuthorizationToken(req); + if (!token) { + return res.status(httpStatus.UNAUTHORIZED).json({ error: "Missing authorization token" }); + } const data = createJwtSchema.parse(req.body); await saveJwtToken({ token, source: healthieDashSource, ...data, }); return res.sendStatus(httpStatus.OK); }) );packages/api/src/routes/ehr/healthie/patient.ts (1)
13-20
: Improve JSDoc documentation clarityThe JSDoc comments for both routes say "Tries to retrieve the matching Metriport patient", but from the implementation it appears to be doing more than just retrieval - it's synchronizing the patient data and potentially creating a new patient if one doesn't exist.
Update the JSDoc comments to more accurately reflect what the endpoints do:
/** * GET /ehr/healthie/patient/:id * - * Tries to retrieve the matching Metriport patient + * Synchronizes a Healthie patient with Metriport and returns the Metriport patient ID. + * If the patient doesn't exist in Metriport, it will be created. * @param req.params.id The ID of Healthie Patient. * @param req.query.practiceId The ID of Healthie Practice. * @returns Metriport Patient if found. */And similarly for the POST endpoint.
Also applies to: 39-46
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
4-19
: Add JSDoc comments to explain the schema propertiesThe schema lacks documentation explaining the purpose of each field and how they affect the system behavior. This makes it difficult for other developers to understand the configuration options.
Add JSDoc comments to explain each property:
+/** + * Schema for webhook configuration + */ const webhookSchema = z.object({ url: z.string(), secretKey: z.string(), }); +/** + * Schema for Healthie secondary mappings that control webhook subscriptions + * and patient processing behavior. + */ export const healthieSecondaryMappingsSchema = z.object({ + /** + * Mapping of subscription resources to webhook configurations + */ webhooks: z.record(z.enum(subscriptionResources), webhookSchema).optional(), + /** + * When true, disables patient linking from patient webhooks + */ webhookPatientPatientLinkingDisabled: z.boolean().optional(), // Add comments for each fieldpackages/api/src/external/ehr/shared.ts (1)
159-177
: New utility function for time range calculation with offset.The function
getLookForwardTimeRangeWithOffset
calculates a time range with an offset, including proper validation that the start range is not after the end range. This shows good defensive programming.A subtle improvement could be adding a check for negative durations, though the current implementation will work correctly with the provided inputs.
Consider adding validation for negative durations:
export function getLookForwardTimeRangeWithOffset({ lookForward, offset, }: { lookForward: Duration; offset: Duration; }): { startRange: Date; endRange: Date; } { + if (lookForward.asMilliseconds() <= 0) { + throw new BadRequestError("lookForward must be positive"); + } const currentDatetime = buildDayjs(); const startRange = buildDayjs(currentDatetime).add(offset).toDate(); const endRange = buildDayjs(currentDatetime).add(lookForward).toDate(); if (startRange > endRange) throw new BadRequestError("Start range is greater than end range"); return { startRange, endRange, }; }packages/api/src/routes/internal/ehr/healthie/patient.ts (2)
24-30
: Prefer 202 (Accepted) for fire-and-forget jobsThe handlers immediately spawn background work and return.
Returning HTTP 200 may suggest that the work already completed successfully.
Use 202 to communicate that the request has been accepted but processing is still in progress.- return res.sendStatus(httpStatus.OK); + return res.sendStatus(httpStatus.ACCEPTED);Also applies to: 41-46
63-67
:triggerDq
may beundefined
– clarify default
getFromQueryAsBoolean
returnsboolean | undefined
, butsyncHealthiePatientIntoMetriport
expectstriggerDq?: boolean
.
If the absence of the flag should be treated asfalse
, consider forcing the default here to
avoid propagatingundefined
.- const triggerDq = getFromQueryAsBoolean("triggerDq", req); + const triggerDq = getFromQueryAsBoolean("triggerDq", req) ?? false;packages/api/src/external/ehr/healthie/command/subscribe-to-webhook.ts (1)
60-73
: Minor: subscribe to webhooks in parallelThe two awaits run sequentially; subscribing in parallel reduces latency and halves the chances of
partial failure.- await subscribeToWebhook({ ... }); - await subscribeToWebhook({ ... }); + await Promise.all([ + subscribeToWebhook({ cxId, healthiePracticeId: externalId, resource: "appointment.created" }), + subscribeToWebhook({ cxId, healthiePracticeId: externalId, resource: "patient.created" }), + ]);packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts (1)
94-105
: Shared mutable array across workers
allAppointments.push(...appointments)
is safe in Node’s single-threaded model but can lead to
hard-to-reproduce race conditions if this code is ever executed in a multi-threaded environment
(worker threads). Returning the slice and concatenating afterexecuteAsynchronously
would be
safer and side-effect-free.packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)
19-20
: Validate URLs instead of bare strings
url
represents an endpoint but is accepted as any string. Adding.url()
gives quick feedback if a malformed URL is supplied:- url: z.string().nullable(), + url: z.string().url().nullable(),packages/api/src/routes/ehr/healthie/auth/middleware.ts (3)
41-44
: Avoid mutatingreq.query
directlyThe middleware rewrites
req.query
in-place, which can surprise downstream handlers that have already captured a reference. Prefer assigning tores.locals
or adding a new property (e.g.req.healthie
) to carry canonical values.- req.query = { - ...req.query, - practiceId: secretKeyInfo.practiceId, - }; + res.locals.practiceId = secretKeyInfo.practiceId;
47-50
: Swallowed original errorWrapping every caught error in a new
ForbiddenError()
discards the cause, violating the guideline “Pass the original error as the new one’scause
”.- } catch (error) { - throw new ForbiddenError(); + } catch (error) { + throw new ForbiddenError(undefined, { cause: error }); }
68-79
:updateTokenExpiration
failure is reported as 403If DB latency or transient error occurs, clients receive 403 even though their credentials were valid. Consider propagating a 5xx instead, or at minimum log the internal error:
- } catch (error) { - throw new ForbiddenError(); + } catch (error) { + out().error("Unable to shorten Healthie token expiration", error); + throw new MetriportError("Internal error", { cause: error }); }packages/shared/src/interface/external/ehr/healthie/appointment.ts (2)
3-8
: Consider validatingcursor
as an opaque cursor or ISO dateRight now
cursor
is a free-form string. If the API uses an ISO timestamp or numeric offset, capturing that format in zod (e.g.z.string().datetime()
orz.string().regex(...)
) prevents subtle pagination bugs.
10-13 6D47 code>:
AppointmentWithAttendee
can be simplifiedZod already supports
.refine
on arrays. Instead of re-declaring the intersection type, you can enforce “at least one attendee” directly in the schema:export const appointmentWithAttendeeSchema = appointmentSchema.extend({ attendees: z.array(z.object({ id: z.string() })).min(1), }); export type AppointmentWithAttendee = z.infer<typeof appointmentWithAttendeeSchema>;Reduces duplication and keeps validation logic co-located with the schema.
packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)
90-102
: Minor perf – reuse the already-createdHealthieApi
instead of instantiating twice
healthieApi
is created on L73 yetupdateHealthiePatientQuickNotes
may create another client whenhealthieApi
is undefined.
Forwarding the same instance avoids an extra network round-trip to fetch the secret key.- updateHealthiePatientQuickNotes({ - cxId, - healthiePracticeId, - healthiePatientId, - healthieApi, - }), + updateHealthiePatientQuickNotes({ + cxId, + healthiePracticeId, + healthiePatientId, + healthieApi, // already created – pass it through + }),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (66)
packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts
(2 hunks)packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts
(2 hunks)packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts
(2 hunks)packages/api/src/external/ehr/elation/command/sync-patient.ts
(2 hunks)packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts
(1 hunks)packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts
(1 hunks)packages/api/src/external/ehr/healthie/command/subscribe-to-webhook.ts
(1 hunks)packages/api/src/external/ehr/healthie/command/sync-patient.ts
(1 hunks)packages/api/src/external/ehr/healthie/shared.ts
(1 hunks)packages/api/src/external/ehr/shared.ts
(7 hunks)packages/api/src/routes/ehr/athenahealth/patient.ts
(2 hunks)packages/api/src/routes/ehr/elation/auth/middleware.ts
(1 hunks)packages/api/src/routes/ehr/elation/patient.ts
(2 hunks)packages/api/src/routes/ehr/healthie/appointment-webhook.ts
(1 hunks)packages/api/src/routes/ehr/healthie/auth/middleware.ts
(1 hunks)packages/api/src/routes/ehr/healthie/patient-webhook.ts
(1 hunks)packages/api/src/routes/ehr/healthie/patient.ts
(1 hunks)packages/api/src/routes/ehr/healthie/routes/dash.ts
(1 hunks)packages/api/src/routes/ehr/healthie/routes/webhook.ts
(1 hunks)packages/api/src/routes/ehr/index.ts
(1 hunks)packages/api/src/routes/internal/ehr/athenahealth/patient.ts
(1 hunks)packages/api/src/routes/internal/ehr/elation/patient.ts
(2 hunks)packages/api/src/routes/internal/ehr/healthie/index.ts
(1 hunks)packages/api/src/routes/internal/ehr/healthie/patient.ts
(1 hunks)packages/api/src/routes/internal/ehr/healthie/secret-key.ts
(1 hunks)packages/api/src/routes/internal/ehr/index.ts
(1 hunks)packages/api/src/routes/internal/index.ts
(2 hunks)packages/api/src/routes/internal/jwt-token/healthie.ts
(1 hunks)packages/api/src/routes/internal/jwt-token/index.ts
(1 hunks)packages/api/src/shared/config.ts
(1 hunks)packages/core/src/command/write-to-storage/s3/write-to-s3-cloud.ts
(2 hunks)packages/core/src/external/ehr/api/link-patient.ts
(3 hunks)packages/core/src/external/ehr/athenahealth/index.ts
(4 hunks)packages/core/src/external/ehr/bundle/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-cloud.ts
(2 hunks)packages/core/src/external/ehr/bundle/create-resource-diff-bundles/steps/start/ehr-start-resource-diff-bundles-local.ts
(1 hunks)packages/core/src/external/ehr/elation/index.ts
(4 hunks)packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts
(2 hunks)packages/core/src/external/ehr/healthie/index.ts
(1 hunks)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts
(1 hunks)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts
(1 hunks)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts
(1 hunks)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts
(1 hunks)packages/core/src/external/ehr/shared.ts
(4 hunks)packages/core/src/util/config.ts
(1 hunks)packages/core/src/util/sqs.ts
(1 hunks)packages/core/src/util/webhook.ts
(2 hunks)packages/infra/config/env-config.ts
(1 hunks)packages/infra/lib/api-stack.ts
(3 hunks)packages/infra/lib/api-stack/api-service.ts
(5 hunks)packages/infra/lib/ehr-nested-stack.ts
(10 hunks)packages/infra/lib/secrets-stack.ts
(1 hunks)packages/infra/lib/shared/secrets.ts
(1 hunks)packages/lambdas/src/elation-link-patient.ts
(2 hunks)packages/lambdas/src/healthie-link-patient.ts
(1 hunks)packages/lambdas/src/shared/ehr.ts
(2 hunks)packages/shared/src/common/retry.ts
(1 hunks)packages/shared/src/domain/secrets.ts
(1 hunks)packages/shared/src/interface/external/ehr/elation/index.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/appointment.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/event.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/index.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/jwt-token.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/patient.ts
(1 hunks)packages/shared/src/interface/external/ehr/healthie/subscription.ts
(1 hunks)packages/shared/src/interface/external/ehr/source.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
to other types- Type predicates: only applicable to narrow down the type, not to force a complete type conversion
- Prefer deconstructing parameters for functions instead of multiple parameters that might be of
the same type- Don’t use
null
inside the app, only on code interacting with external interfaces/services,
like DB and HTTP; convert toundefined
before sending inwards into the code- Use
async/await
instead of.then()
- Use the strict equality operator
===
, don’t use abstract equality operator==
- When calling a Promise-returning function asynchronously (i.e., not awaiting), use
.catch()
to
handle errors (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
- Avoid creating arrow functions
- Use truthy syntax instead of
in
- i.e.,if (data.link)
notif ('link' in data)
- Error handling
- Pass the original error as the new one’s
cause
so the stack trace is persisted- Error messages should have a static message - add dynamic data to MetriportError's
additionalInfo
prop- Avoid sending multiple events to Sentry for a single error
- Global constants and variables
- Move literals to constants declared after imports when possible (avoid magic numbers)
- Avoid shared, global objects
- Avoid using
console.log
andconsole.error
in packages other than utils, infra and shared,
and try to useout().log
instead- Avoid multi-line logs
- don't send objects as a second parameter to
console.log()
orout().log()
- don't create multi-line strings when using
JSON.stringify()
- Use
eslint
to enforce code style- Use
prettier
to format code- max column length is 100 chars
- multi-line comments use
/** */
- scripts: top-level comments go after the import
packages/api/src/routes/internal/ehr/healthie/index.ts
packages/api/src/external/ehr/elation/command/sync-patient.ts
packages/api/src/routes/ehr/healthie/routes/webhook.ts
packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts
packages/api/src/routes/ehr/index.ts
packages/api/src/routes/ehr/elation/auth/middleware.ts
packages/shared/src/interface/external/ehr/source.ts
packages/lambdas/src/elation-link-patient.ts
packages/shared/src/interface/external/ehr/elation/index.ts
packages/api/src/routes/ehr/elation/patient.ts
packages/infra/lib/secrets-stack.ts
packages/infra/lib/shared/secrets.ts
packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts
packages/shared/src/common/retry.ts
packages/shared/src/domain/secrets.ts
packages/infra/lib/api-stack.ts
packages/core/src/external/ehr/elation/index.ts
packages/core/src/external/ehr/bundle/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-cloud.ts
packages/infra/config/env-config.ts
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts
packages/core/src/command/write-to-storage/s3/write-to-s3-cloud.ts
packages/shared/src/interface/external/ehr/healthie/index.ts
packages/api/src/routes/internal/ehr/index.ts
packages/api/src/routes/internal/jwt-token/index.ts
packages/api/src/routes/ehr/athenahealth/patient.ts
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts
packages/core/src/util/config.ts
packages/core/src/external/ehr/bundle/create-resource-diff-bundles/steps/start/ehr-start-resource-diff-bundles-local.ts
packages/infra/lib/api-stack/api-service.ts
packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts
packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts
packages/api/src/routes/internal/ehr/healthie/secret-key.ts
packages/api/src/routes/internal/ehr/athenahealth/patient.ts
packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts
packages/api/src/routes/internal/ehr/elation/patient.ts
packages/api/src/routes/internal/jwt-token/healthie.ts
packages/api/src/routes/ehr/healthie/patient.ts
packages/core/src/util/sqs.ts
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts
packages/lambdas/src/healthie-link-patient.ts
packages/api/src/routes/internal/ehr/healthie/patient.ts
packages/api/src/routes/ehr/healthie/routes/dash.ts
packages/core/src/external/ehr/athenahealth/index.ts
packages/shared/src/interface/external/ehr/healthie/jwt-token.ts
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts
packages/api/src/routes/internal/index.ts
packages/api/src/routes/ehr/healthie/patient-webhook.ts
packages/api/src/external/ehr/shared.ts
packages/lambdas/src/shared/ehr.ts
packages/core/src/external/ehr/api/link-patient.ts
packages/core/src/external/ehr/shared.ts
packages/infra/lib/ehr-nested-stack.ts
packages/api/src/shared/config.ts
packages/api/src/external/ehr/healthie/command/sync-patient.ts
packages/core/src/util/webhook.ts
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts
packages/api/src/external/ehr/healthie/command/subscribe-to-webhook.ts
packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts
packages/api/src/external/ehr/healthie/shared.ts
packages/api/src/routes/ehr/healthie/appointment-webhook.ts
packages/shared/src/interface/external/ehr/healthie/event.ts
packages/shared/src/interface/external/ehr/healthie/patient.ts
packages/shared/src/interface/external/ehr/healthie/subscription.ts
packages/shared/src/interface/external/ehr/healthie/appointment.ts
packages/api/src/routes/ehr/healthie/auth/middleware.ts
packages/core/src/external/ehr/healthie/index.ts
🧠 Learnings (2)
packages/infra/lib/ehr-nested-stack.ts (1)
Learnt from: thomasyopes
PR: metriport/metriport#3427
File: packages/core/src/external/ehr/api/sync-patient.ts:16-55
Timestamp: 2025-03-11T20:42:46.516Z
Learning: In the patient synchronization architecture, the flow follows this pattern: (1) `ehr-sync-patient-cloud.ts` sends messages to an SQS queue, (2) the `ehr-sync-patient` Lambda consumes these messages, and (3) the Lambda uses the `syncPatient` function to make the API calls to process the patient data.
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (1)
Learnt from: thomasyopes
PR: metriport/metriport#3427
File: packages/core/src/external/ehr/api/sync-patient.ts:16-55
Timestamp: 2025-03-11T20:42:46.516Z
Learning: In the patient synchronization architecture, the flow follows this pattern: (1) `ehr-sync-patient-cloud.ts` sends messages to an SQS queue, (2) the `ehr-sync-patient` Lambda consumes these messages, and (3) the Lambda uses the `syncPatient` function to make the API calls to process the patient data.
🧬 Code Graph Analysis (23)
packages/lambdas/src/elation-link-patient.ts (1)
packages/lambdas/src/shared/ehr.ts (1)
parseLinkPatient
(58-78)
packages/infra/lib/shared/secrets.ts (1)
packages/infra/config/example.ts (1)
config
(7-198)
packages/core/src/external/ehr/bundle/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-cloud.ts (1)
packages/core/src/util/sqs.ts (1)
SQS_MESSAGE_BATCH_SIZE_FIFO
(2-2)
packages/core/src/command/write-to-storage/s3/write-to-s3-cloud.ts (1)
packages/core/src/util/sqs.ts (1)
SQS_MESSAGE_BATCH_SIZE_STANDARD
(3-3)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts (4)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (1)
HealthieLinkPatientHandler
(7-9)packages/core/src/util/config.ts (1)
Config
(9-202)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (1)
HealthieLinkPatientLocal
(6-22)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (1)
HealthieLinkPatientCloud
(6-26)
packages/core/src/util/config.ts (1)
packages/api/src/shared/config.ts (1)
getEnvVarOrFail
(14-14)
packages/infra/lib/api-stack/api-service.ts (1)
packages/infra/lib/shared/sqs.ts (1)
provideAccessToQueue
(219-233)
packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts (1)
packages/api/src/external/ehr/healthie/shared.ts (1)
createHealthieClient
(100-109)
packages/api/src/routes/ehr/healthie/patient.ts (2)
packages/api/src/routes/ehr/healthie/auth/middleware.ts (1)
tokenEhrPatientIdQueryParam
(20-20)packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)
syncHealthiePatientIntoMetriport
(47-104)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (3)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (2)
HealthieLinkPatientHandler
(7-9)ProcessLinkPatientRequest
(1-5)packages/core/src/external/ehr/api/link-patient.ts (1)
linkPatient
(17-47)packages/shared/src/index.ts (1)
sleep
(13-13)
packages/lambdas/src/healthie-link-patient.ts (5)
packages/lambdas/src/shared/sqs.ts (1)
getSingleMessageOrFail
(62-83)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (1)
HealthieLinkPatientLocal
(6-22)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (1)
ProcessLinkPatientRequest
(1-5)packages/shared/src/index.ts (1)
MetriportError
(40-40)packages/lambdas/src/shared/ehr.ts (1)
parseLinkPatient
(58-78)
packages/core/src/external/ehr/athenahealth/index.ts (1)
packages/shared/src/index.ts (2)
BadRequestError
(39-39)NotFoundError
(41-41)
packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)
subscriptionResources
(3-3)
packages/api/src/routes/ehr/healthie/patient-webhook.ts (4)
packages/api/src/routes/helpers/request-logger.ts (1)
requestLogger
(10-63)packages/shared/src/interface/external/ehr/healthie/event.ts (1)
healthiePatientCreatedEventSchema
(10-14)packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
healthieSecondaryMappingsSchema
(9-19)packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)
updateHealthiePatientQuickNotes
(187-200)
packages/api/src/external/ehr/shared.ts (4)
packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (2)
healthieDashSource
(4-4)HealthieDashJwtTokenData
(10-10)packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (2)
HealthieSecondaryMappings
(20-20)healthieSecondaryMappingsSchema
(9-19)packages/shared/src/common/date.ts (1)
buildDayjs
(70-72)packages/shared/src/index.ts (1)
BadRequestError
(39-39)
packages/core/src/external/ehr/api/link-patient.ts (1)
packages/core/src/external/ehr/api/api-shared.ts (1)
ApiBaseParams
(3-9)
packages/infra/lib/ehr-nested-stack.ts (3)
packages/infra/lib/shared/settings.ts (1)
QueueAndLambdaSettings
(5-33)packages/infra/lib/shared/lambda-layers.ts (1)
LambdaLayers
(16-16)packages/infra/lib/shared/sqs.ts (1)
createQueue
(50-103)
packages/core/src/util/webhook.ts (1)
packages/shared/src/index.ts (1)
MetriportError
(40-40)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (2)
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (2)
HealthieLinkPatientHandler
(7-9)ProcessLinkPatientRequest
(1-5)packages/core/src/util/config.ts (1)
Config
(9-202)
packages/api/src/external/ehr/healthie/command/subscribe-to-webhook.ts (4)
packages/shared/src/interface/external/ehr/healthie/subscription.ts (2)
SubscriptionResource
(4-4)SubscriptionWithSignatureSecret
(26-26)packages/shared/src/index.ts (1)
MetriportError
(40-40)packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (1)
healthieSecondaryMappingsSchema
(9-19)packages/api/src/external/ehr/healthie/shared.ts (1)
createHealthieClient
(100-109)
packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts (8)
packages/api/src/external/ehr/healthie/shared.ts (2)
LookupMode
(153-153)createHealthieClient
(100-109)packages/shared/src/index.ts (4)
MetriportError
(40-40)BadRequestError
(39-39)NotFoundError
(41-41)errorToString
(42-42)packages/shared/src/interface/external/ehr/healthie/cx-mapping.ts (2)
healthieSecondaryMappingsSchema
(9-19)HealthieSecondaryMappings
(20-20)packages/shared/src/interface/external/ehr/healthie/appointment.ts (2)
Appointment
(9-9)AppointmentWithAttendee
(11-13)packages/api/src/external/ehr/shared.ts (7)
Appointment
(127-131)parallelPractices
(53-53)delayBetweenPracticeBatches
(51-51)parallelPatients
(54-54)delayBetweenPatientBatches
(52-52)getLookForwardTimeRange
(146-157)getLookForwardTimeRangeWithOffset
(159-177)packages/core/src/util/concurrency.ts (1)
executeAsynchronously
(83-130)packages/api/src/external/ehr/healthie/command/sync-patient.ts (2)
UpdateHealthiePatientQuickNotesParams
(180-185)SyncHealthiePatientIntoMetriportParams
(39-45)packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-factory.ts (1)
buildHealthieLinkPatientHandler
(6-13)
packages/shared/src/interface/external/ehr/healthie/appointment.ts (1)
packages/api/src/external/ehr/shared.ts (1)
Appointment
(127-131)
packages/api/src/routes/ehr/healthie/auth/middleware.ts (7)
packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)
isSubscriptionResource
(5-7)packages/api/src/external/ehr/healthie/shared.ts (1)
getHealthieSecretKeyInfo
(111-138)packages/core/src/util/webhook.ts (1)
verifySignature
(76-95)packages/api/src/shared/config.ts (1)
Config
(16-366)packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (1)
healthieDashSource
(4-4)packages/shared/src/common/date.ts (1)
buildDayjs
(70-72)packages/api/src/external/ehr/healthie/command/sync-patient.ts (1)
shortDurationTokenDuration
(37-37)
🪛 Biome (1.9.4)
packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts
[error] 60-60: Avoid the use of spread (...
) syntax on accumulators.
Spread syntax should be avoided on accumulators (like those in .reduce
) because it causes a time complexity of O(n^2)
.
Consider methods such as .splice or .push instead.
(lint/performance/noAccumulatingSpread)
⏰ Context from checks skipped due to timeout of 90000ms (9)
- GitHub Check: check-pr / lint-build-test
- GitHub Check: check-pr / lint-build-test
- GitHub Check: check-pr / lint-build-test
- GitHub Check: check-pr / lint-build-test
- GitHub Check: check-pr / lint-build-test
- GitHub Check: Analyze (javascript)
- GitHub Check: api / deploy
- GitHub Check: infra-api-lambdas / deploy
- GitHub Check: mllp-server / deploy
🔇 Additional comments (116)
packages/shared/src/interface/external/ehr/elation/index.ts (1)
6-7
: Re-export new modules correctly integrated. The additions forcx-mapping
andevent
follow the existing barrel pattern and expose the new Elation EHR interface components.packages/api/src/routes/ehr/elation/auth/middleware.ts (1)
1-1
: The import reorder is purely organizational without any behavioral impact. No comment needed.packages/shared/src/interface/external/ehr/source.ts (1)
5-5
: Approve new EHR source entry. Addinghealthie
toEhrSources
enables the new Healthie integration; it's correctly added as a string literal alongside existing sources.packages/lambdas/src/elation-link-patient.ts (2)
7-7
: Adopt shared parsing utility. Replacing the Elation-specific parser with the genericparseLinkPatient
centralizes validation logic and reduces duplication. Ensure the import path"./shared/ehr"
is accurate and doesn't introduce cyclic dependencies.
48-50
: Validate type compatibility between generic parser and handler.parseLinkPatient
returns a union (ElationProcessLinkPatientRequest | HealthieProcessLinkPatientRequest
), butparseBody
is typed asProcessLinkPatientRequest
(Elation-specific). Confirm these types are structurally identical to avoid type mismatches at compile time.packages/core/src/command/write-to-storage/s3/write-to-s3-cloud.ts (1)
8-8
: Use standard batch size constant. Switching toSQS_MESSAGE_BATCH_SIZE_STANDARD
aligns this writer with the updated SQS utilities and ensures consistent batch sizes for standard queues.packages/infra/lib/secrets-stack.ts (1)
65-65
: Healthie secrets integration looks good!This addition properly extends the
ehrSecrets
object to include Healthie secrets from the configuration, following the established pattern for handling EHR integrations.packages/infra/lib/shared/secrets.ts (1)
34-36
: Well-implemented Healthie secrets configurationThe addition correctly follows the established pattern for other EHR integrations, with proper optional chaining and conditional inclusion of Healthie secrets.
packages/api/src/routes/internal/ehr/healthie/index.ts (1)
1-10
: Router implementation follows best practicesThe router is correctly implemented using express-promise-router and follows the established pattern for organizing internal EHR routes. The modular approach with separate sub-routes for patient and secretKey functionalities maintains good separation of concerns.
packages/shared/src/domain/secrets.ts (1)
4-4
: API key map schema looks goodThe new schema for API key mapping follows the existing pattern while providing a dedicated schema for what appears to be Healthie-specific API key validation.
Though structurally identical to
cxClientKeyAndSecretMapSecretSchema
, I assume this represents a semantically different entity specifically for Healthie API keys.packages/core/src/util/config.ts (1)
180-182
: Correctly implemented Healthie queue URL access method.The addition of this method follows the established pattern in the Config class, placing it logically after other EHR link patient queue methods and using the existing getEnvVarOrFail utility.
packages/api/src/routes/internal/jwt-token/index.ts (2)
5-5
: LGTM: Correct import for Healthie JWT token handling.The import statement follows the established pattern used for other EHR systems in this file.
13-13
: LGTM: Properly configured Healthie JWT token route.The route configuration correctly follows the same pattern as other EHR systems, mapping the "/healthie" path to the imported healthie router.
packages/shared/src/common/retry.ts (2)
32-32
: LGTM: Updated documentation for maxAttempts default.The comment correctly reflects the new default value of 5 maximum retry attempts.
15-15
:❓ Verification inconclusive
Reduced default retry attempts to improve handling of non-retriable errors.
Reducing the default maxAttempts from 10 to 5 is a reasonable optimization that complements the changes to prevent retries on HTTP 400 and 404 errors, avoiding unnecessary delays for client errors that are unlikely to resolve with retries.
🏁 Script executed:
#!/bin/bash # Check if there are any dependencies on the previous default value of 10 retries rg -n "maxAttempts\s*[:=]\s*(?!5)\d+" --type tsLength of output: 303
The previous regex used look-around, which ripgrep doesn’t support by default. Let’s explicitly search for any occurrences of the old hard-coded value (10) or any other uses of
maxAttempts
:
🏁 Script executed:
#!/bin/bash # Find any remaining occurrences of the old default (10) rg -n "maxAttempts\s*[:=]\s*10" --type ts # List every usage of maxAttempts to ensure nothing else relies on the old default rg -n "maxAttempts" --type tsLength of output: 6321
Reduced default retry attempts to improve handling of non-retriable errors – verified no residual hard-coded 10s
A repo-wide search found zero occurrences of
maxAttempts: 10
; almost every retry call explicitly overrides the default (e.g.maxAttempts: 6
,NETWORK_RETRY_MAX_ATTEMPTS
, etc.). Reducing the shared default from 10 to 5 therefore won’t impact those.Please double-check any calls to the shared retry utility that rely on its implicit default to confirm that a 5-attempt fallback is acceptable.
packages/core/src/external/ehr/bundle/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-cloud.ts (2)
8-8
: LGTM: Updated SQS batch size import for FIFO queues.The import correctly uses the FIFO-specific constant, which aligns with the queue type used in this file.
37-37
: LGTM: Updated batch size constant usage for FIFO queue compatibility.The code now properly uses SQS_MESSAGE_BATCH_SIZE_FIFO for chunking messages, which is appropriate given the FIFO queue configuration (as evidenced by the fifo: true parameter in sendMessageToQueue).
packages/core/src/external/ehr/bundle/create-resource-diff-bundles/steps/start/ehr-start-resource-diff-bundles-local.ts (1)
99-99
: Great addition of the early return guard clause.This optimization prevents unnecessary job total updates and handler invocations when there are no resources to process, making the function more efficient. The guard clause follows the functional programming principle of early returns.
packages/api/src/routes/internal/ehr/index.ts (1)
5-5
: Good addition of Healthie EHR routes.The changes correctly follow the established pattern for adding new EHR integration routes, maintaining consistent structure with the existing routes for Athenahealth, Elation, and Canvas.
Also applies to: 12-12
packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts (1)
2-3
: Good refactoring to use a generalized linkPatient function.The change appropriately:
- Updates the import path to use a more generic link-patient module
- Adds the explicit EHR source parameter to the function call
- Uses the EhrSources enum for type safety
This refactoring improves modularity and code reuse across different EHR integrations.
Also applies to: 15-15
packages/api/src/external/ehr/elation/command/sync-patient.ts (1)
12-12
: Good implementation of dayjs duration constants.The changes properly:
- Import and extend dayjs with the duration plugin
- Define named constants for token durations instead of using magic numbers
- Follow the coding guideline to "prefer
dayjs.duration(...)
to create duration consts"These constants improve code readability and maintainability, especially for token expiration handling.
Also applies to: 28-31
packages/api/src/routes/ehr/healthie/routes/webhook.ts (1)
1-10
: Well-structured webhook router implementation.The router is cleanly organized, separating appointment and patient webhook handlers into distinct routes. Using express-promise-router is a good choice for handling asynchronous operations in the route handlers.
packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts (2)
5-6
: Good addition of error classes to imports.Adding
BadRequestError
andNotFoundError
imports prepares for the improved error handling in the function below.
177-177
: Improved error handling for expected error cases.This change enhances error handling by specifically checking for
BadRequestError
andNotFoundError
instances, returning an empty result instead of logging the error. This reduces noise in logs and handles expected error cases more gracefully.packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts (2)
5-5
: Good addition of error classes to imports.Adding
BadRequestError
andNotFoundError
imports prepares for the improved error handling in the function below.
113-113
: Improved error handling for expected error cases.This change enhances error handling by specifically checking for
BadRequestError
andNotFoundError
instances, returning an empty result instead of logging the error. This reduces noise in logs and handles expected error cases more gracefully.packages/core/src/external/ehr/elation/index.ts (4)
100-100
: Fixed header capitalization to follow HTTP standards.Corrected the header key from lowercase "content-type" to the standard capitalized format "Content-Type" when fetching the two-legged auth token.
131-131
: Fixed header capitalization to follow HTTP standards.Corrected the header key from lowercase "content-type" to the standard capitalized format "Content-Type" when initializing the axios instance.
176-177
: Fixed header capitalization to follow HTTP standards.Corrected the header key from lowercase "content-type" to the standard capitalized format "Content-Type" when updating patient metadata.
229-230
: Fixed header capitalization to follow HTTP standards.Corrected the header key from lowercase "content-type" to the standard capitalized format "Content-Type" when creating a problem.
packages/infra/config/env-config.ts (1)
260-265
: Good addition of Healthie configuration.The structure of the Healthie configuration follows the established pattern for other EHR integrations, with environment and secrets properties.
packages/api/src/shared/config.ts (1)
356-361
: Methods follow consistent pattern for EHR config access.These new utility methods align well with existing EHR configuration methods and provide clean access to Healthie-specific environment variables.
packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts (2)
6-6
: Good import addition for better error handling.The added error types align with the improvements to the error handling downstream.
163-163
: Improved error handling for expected failures.This change correctly adds special handling for BadRequestError and NotFoundError, preventing unnecessary error logging for expected cases. This matches similar improvements in other EHR integration modules.
packages/shared/src/interface/external/ehr/healthie/index.ts (1)
1-6
: Clean barrel file for Healthie EHR interfaces.This index file follows best practices for module organization, making imports cleaner in consuming files by providing a single entry point for all Healthie-related interfaces.
packages/api/src/routes/ehr/athenahealth/patient.ts (2)
16-17
: Documentation improvement for API query parametersThe added documentation for
practiceId
anddepartmentId
query parameters improves API clarity by accurately reflecting what the implementation already requires.
44-45
: Documentation improvement for API query parametersThe added documentation for
practiceId
anddepartmentId
query parameters improves API clarity by accurately reflecting what the implementation already requires.packages/core/src/util/sqs.ts (1)
1-3
: Improved SQS constants for different queue typesThese changes make good improvements:
- Reducing
MAX_SQS_MESSAGE_SIZE
provides more buffer against AWS limits- Adding queue-specific batch size constants (
SQS_MESSAGE_BATCH_SIZE_FIFO
andSQS_MESSAGE_BATCH_SIZE_STANDARD
) provides better configuration for different queue typesThe differentiation allows for optimized batch processing based on queue characteristics.
packages/api/src/routes/ehr/elation/patient.ts (2)
18-18
: Documentation improvement for API query parameterThe added documentation for
practiceId
query parameter improves API clarity by accurately reflecting what the implementation already requires.
44-44
: Documentation improvement for API query parameterThe added documentation for
practiceId
query parameter improves API clarity by accurately reflecting what the implementation already requires.packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient.ts (2)
1-5
: Well-defined type for patient linking requestThe
ProcessLinkPatientRequest
type is clear and provides all necessary fields for patient linking operations.
7-9
: Clean interface design for patient linking handlerThe
HealthieLinkPatientHandler
interface follows good design principles:
- Clear single responsibility (processing patient links)
- Simple async method signature with well-defined request type
- Follows dependency inversion principle, allowing for multiple implementations
This enables both local and cloud implementations as mentioned in the summary, providing flexibility based on the runtime environment.
packages/api/src/routes/ehr/index.ts (3)
17-22
: LGTM! New Healthie integration follows consistent middleware pattern.The imports follow the established pattern used for other EHR providers, maintaining consistency in how authentication middleware and route handlers are organized.
29-29
: LGTM! Dashboard route follows consistent structure.The Healthie dashboard route is correctly configured with the appropriate middleware chain (processCxIdHealthieDash → checkMAPIAccess → healthieDash) matching the pattern used for other EHR integrations.
33-33
: LGTM! Webhook route follows consistent structure.The Healthie webhook route is correctly configured with the appropriate middleware chain (processCxIdHealthieWebhooks → checkMAPIAccess → healthieWebhooks) matching the pattern used for other EHR integrations.
packages/core/src/external/ehr/athenahealth/index.ts (4)
178-178
: Header capitalization fix improves standards compliance.Properly capitalizing HTTP headers as "Content-Type" rather than "content-type" follows standard conventions, though HTTP headers are technically case-insensitive.
211-211
: Consistent header capitalization improvement.This change maintains consistency with the previous header capitalization fix, ensuring "Content-Type" is properly capitalized throughout the codebase.
568-568
: Enhanced error handling to prevent unnecessary error accumulation.The early return for BadRequestError and NotFoundError instances prevents unnecessary error logging and accumulation in the createVitals method, improving error management for expected error conditions.
651-651
: Improved error handling consistency across methods.The early return for BadRequestError and NotFoundError instances in the searchForMedication method follows the same pattern established in the createVitals method, ensuring consistent error handling throughout the class.
packages/infra/lib/api-stack.ts (3)
416-417
: LGTM! Healthie infrastructure integration follows established patterns.The additions of
healthieLinkPatientQueue
andhealthieLinkPatientLambda
to the destructured output from EhrNestedStack follow the same pattern used for Elation, maintaining consistency in how EHR integrations are structured.
545-545
: LGTM! Healthie queue correctly integrated with API service.The Healthie queue is properly included in the API service creation, ensuring the service has access to the queue for patient linking operations.
640-640
: LGTM! Healthie lambda configured with API URL.The Healthie lambda is correctly added to the list of lambdas that receive the API_URL environment variable, enabling it to communicate with the API service.
packages/api/src/routes/internal/index.ts (2)
35-36
: LGTM! Clear alias naming for webhook subscription functions.Renaming the Elation function to
subscribeToElationWebhooks
and importing the Healthie function assubscribeToHealthieWebhooks
provides clear differentiation between the two providers.
3 F438 31-334
: LGTM! Conditional webhook subscription based on EHR source.The conditional logic to call either
subscribeToElationWebhooks
orsubscribeToHealthieWebhooks
based on the EHR source ensures the appropriate webhook subscription process is triggered for each provider.packages/api/src/routes/internal/ehr/athenahealth/patient.ts (1)
67-76
: Documentation improvement looks good.The updated JSDoc comment accurately describes all the query parameters used by the endpoint handler, making the API documentation more complete and helpful.
packages/api/src/routes/internal/ehr/elation/patient.ts (2)
33-41
: Documentation improvement looks good.The updated JSDoc comment accurately describes all the query parameters used by the endpoint handler, making the API documentation more complete and helpful.
61-68
: Documentation improvement looks good.The updated JSDoc comment accurately describes all the query parameters used by the endpoint handler, making the API documentation more complete and helpful.
packages/api/src/routes/internal/jwt-token/healthie.ts (1)
31-34
: Schema validation looks good.The Zod schema correctly validates the JWT token payload structure against the expected format.
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-local.ts (1)
1-22
: Implementation looks good and follows established patterns.The
HealthieLinkPatientLocal
class correctly implements theHealthieLinkPatientHandler
interface and follows the functional programming style guidelines. The implementation is clean, focused, and properly leverages the existinglinkPatient
API with the Healthie EHR source.The optional sleep timer is a good addition for ensuring synchronization when needed, especially for systems that might need time to process the link before subsequent operations.
packages/core/src/external/ehr/api/link-patient.ts (5)
3-7
: Good refactoring to support dynamic EHR sources.The imports and type definition changes properly set up the file to work with multiple EHR sources instead of hardcoding to a specific one.
12-13
: JSDoc updated correctly to reflect the new parameter.Documentation has been properly updated to include the EHR source parameter.
18-19
: Function signature updated to include the EHR source parameter.The updated function signature correctly includes the EHR source, allowing the function to be used with different EHR providers.
30-30
: Dynamic URL construction using the EHR source parameter.The URL construction now properly uses the provided EHR source parameter instead of hardcoding the EHR provider.
39-44
: Error handling enhanced with proper context.Error handling has been improved by:
- Including the EHR source in the error metadata
- Generalizing the error context to be EHR-agnostic
This will make debugging easier and error tracking more consistent across different EHR integrations.
packages/infra/lib/api-stack/api-service.ts (5)
112-113
: Added Healthie link patient queue parameter.Parameter added correctly to support the new Healthie integration.
154-155
: Updated function interface to include the Healthie queue.The interface definition has been properly updated to include the new parameter.
289-290
: Added Healthie queue URL to container environment variables.The environment variable is correctly added to provide the Healthie link patient queue URL to the container.
339-340
: Added Healthie environment configuration.The Healthie environment variable is properly added alongside existing EHR environment variables.
452-456
: Granted send permission to the Healthie link patient queue.Properly configured queue access permissions for the API service to send messages to the Healthie link patient queue.
packages/api/src/routes/ehr/healthie/routes/dash.ts (5)
1-14
: Imports follow best practices and naming conventions.All imports follow the project's naming conventions and are organized logically. The imports provide the necessary components for building the routes.
15-18
: Router initialization and configuration looks good.The router is properly initialized using
express-promise-router
, and the document skip paths are defined correctly for the Healthie ID check.
19-27
: Patient routes properly configured with appropriate middleware.The patient routes are correctly set up with the necessary middleware chain:
- Parameter handling
- Patient route processing
- EHR patient ID extraction
- Patient authorization
- Medical patient handler
This follows the established pattern for EHR integrations in the codebase.
28-34
: Document and settings routes properly configured.The document route middleware chain is set up correctly with:
- Document route processing
- EHR patient ID extraction with appropriate skip paths
- Medical document handler
The settings route is also properly configured.
36-36
: Router exported correctly.The router is properly exported as the default export.
packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts (1)
17-24
:⚠️ Potential issuePossible runtime crash when
attendees
is empty
healthieAppointment.attendees[0].id
assumes at least one attendee. If the appointment has no attendees the call will throwTypeError: Cannot read properties of undefined (reading 'id')
.-const healthiePatientId = healthieAppointment.attendees[0].id; -return healthiePatientId; +const firstAttendee = healthieAppointment.attendees?.[0]; +return firstAttendee?.id;Consider also validating the shape of
healthieAppointment
with a type-guard or zod schema to avoid silent API changes.⛔ Skipped due to learnings
Learnt from: thomasyopes PR: metriport/metriport#3754 File: packages/api/src/external/ehr/healthie/command/get-patient-from-appointment.ts:22-24 Timestamp: 2025-04-30T20:59:34.235Z Learning: The Healthie API's `getAppointment` method already validates that appointments have attendees and throws an error if none are found. The return type `AppointmentWithAttendee` reflects this guarantee, making additional validation unnecessary when accessing `attendees[0]`.
packages/lambdas/src/healthie-link-patient.ts (1)
42-51
: 🛠️ Refactor suggestionAdd proper error handling for JSON parsing
The
JSON.parse
operation can throw an exception if the input is not valid JSON, but this exception is not explicitly caught, which could cause the Lambda to fail with an unhelpful error message.Add try/catch for the JSON parsing operation:
function parseBody(body?: unknown): ProcessLinkPatientRequest { if (!body) throw new MetriportError(`Missing message body`); const bodyString = typeof body === "string" ? (body as string) : undefined; if (!bodyString) throw new MetriportError(`Invalid body`); - const bodyAsJson = JSON.parse(bodyString); - - return parseLinkPatient(bodyAsJson); + try { + const bodyAsJson = JSON.parse(bodyString); + return parseLinkPatient(bodyAsJson); + } catch (error) { + throw new MetriportError(`Invalid JSON in message body`, error); + } }⛔ Skipped due to learnings
Learnt from: thomasyopes PR: metriport/metriport#3608 File: packages/lambdas/src/ehr-compute-resource-diff-bundles.ts:58-62 Timestamp: 2025-04-23T19:00:49.707Z Learning: For Lambda functions in the metriport codebase, the team prefers to let the default Lambda error handling catch JSON parsing errors rather than adding explicit try/catch blocks.
packages/core/src/external/ehr/healthie/link-patient/healthie-link-patient-cloud.ts (1)
1-26
: Well-implemented cloud handler for Healthie patient linkingThis class follows the established pattern for EHR integrations, where cloud handlers send messages to SQS queues that are then processed by Lambda functions. The implementation demonstrates good practices:
- Follows the single responsibility principle by focusing only on queuing messages
- Properly handles optional dependencies with sensible defaults
- Ensures idempotent operation through the use of deduplication IDs
- Maintains FIFO ordering with message group IDs based on customer ID
- Uses dependency injection for better testability
The implementation aligns with the architecture pattern mentioned in the retrieved learning about patient synchronization flow.
packages/core/src/external/ehr/shared.ts (4)
103-105
: Good improvement: Case-insensitive content-type header checkThe updated check handles both "content-type" and "Content-Type" variants, making the code more robust against inconsistent header casing from different APIs or clients.
117-137
: Excellent enhancement to retry logicThe implementation of
executeWithRetries
with a custom retry strategy improves the resilience of the system by:
- Centralizing retry logic
- Avoiding unnecessary retries for client errors (400, 404) that are unlikely to succeed upon retry
- Properly filtering which errors should trigger retries
This is a valuable improvement that will reduce unnecessary API calls and help the system fail faster when appropriate.
140-142
: Improved error handling with better type checkingUsing
isAxiosError
type guard and the existingerrorToString
function improves type safety and maintains consistent error handling patterns across the codebase.
226-228
: Good utility function for error classificationThe
isNotRetriableAxiosError
function provides a clear, single source of truth for determining which errors should not be retried.packages/shared/src/interface/external/ehr/healthie/event.ts (1)
1-15
: Well-structured webhook event schemas with strong type safetyThe implementation of the Healthie webhook event schemas follows best practices:
- Uses Zod for runtime validation alongside TypeScript types
- Coerces
resource_id
to string type to handle potential numeric values from the API- Uses literal types for
event_type
to ensure exact matching- Maintains consistent structure between different event types
- Properly exports both schemas and their inferred TypeScript types
For future expansion, if many similar event types are added, consider creating a generic event schema factory to reduce duplication.
packages/shared/src/interface/external/ehr/healthie/jwt-token.ts (1)
1-10
: Clean implementation of JWT token schema for HealthieThe schema is well-structured and follows best practices:
- Uses Zod for runtime validation alongside TypeScript types
- Uses a constant for the source value to maintain consistency
- Properly narrows the source type with
z.literal
- Follows the established pattern for JWT token schemas in the codebase
The use of template literal with
${healthieDashSource}
in the literal type is a good approach to ensure type consistency, though a small comment might help clarify this pattern for future developers.packages/api/src/routes/ehr/healthie/patient-webhook.ts (6)
1-14
: Imports look correct and follow project conventions.The imports are organized logically, with external dependencies first, followed by internal modules. The code follows the project's import organization pattern.
17-22
: Documentation explains endpoint purpose clearly.The JSDoc comment clearly explains the purpose of this endpoint and its expected HTTP response.
23-40
: Proper error handling and parameter validation.The route handler correctly:
- Extracts and validates the cxId and practiceId
- Parses the event body against a schema
- Retrieves and validates the CX mapping
- Throws a meaningful error if secondary mappings are missing
This follows best practices for request validation and error handling.
41-44
: Good feature flag handling for patient linking.The code respects the
webhookPatientPatientLinkingDisabled
configuration flag, returning early if patient linking is disabled. This allows for flexible deployment configurations without code changes.
45-49
: Patient quick notes update follows standard pattern.The call to
updateHealthiePatientQuickNotes
follows the expected pattern for Healthie patient linking.
50-59
: Conditional patient processing based on feature flag.The code uses the
webhookPatientPatientProcessingEnabled
flag to conditionally trigger patient synchronization, which aligns with the configuration-driven approach throughout the codebase.packages/api/src/external/ehr/shared.ts (7)
4-4
: Explicit imports improve code readability.Good practice to explicitly import all required types and errors. The addition of
BadRequestError
indicates it will be used for validation in the new functionality.
34-41
: Healthie imports follow established pattern.The imports for Healthie-specific secondary mappings and JWT token data follow the same pattern used for other EHR sources, maintaining consistency in the codebase.
63-66
: New type for API key based authentication.The introduction of
EhrEnvAndApiKey
suggests Healthie uses a different authentication method (API key instead of client credentials). This pattern appropriately extends the existing authentication models to accommodate the new integration.
74-79
: Extended EHR dash JWT token sources consistently.The update to include
healthieDashSource
in theehrDashJwtTokenSources
array follows the established pattern for other EHR sources.
85-89
: EHR dashboard JWT token data type extension includes Healthie.The union type
EhrDashJwtTokenData
is properly extended to include the new Healthie dashboard JWT token data type.
114-117
: Extended secondary mappings type to include Healthie.The union type
EhrCxMappingSecondaryMappings
is properly extended to include Healthie secondary mappings.
118-125
: Schema map updated correctly for Healthie.The
ehrCxMappingSecondaryMappingsSchemaMap
is updated to include the Healthie schema, maintaining consistency with the other EHR sources.packages/infra/lib/ehr-nested-stack.ts (10)
18-18
: Wait time constant follows established pattern.The addition of the
waitTimeHealthieLinkPatient
constant with a comment explaining the rate limit follows the same pattern as other wait time constants.
23-26
: Settings function signature updated to include Healthie.The
settings
function return type is correctly extended to include the newhealthieLinkPatient
property.
36-37
: Memory allocation reduced for Lambda functions.Memory allocation for
syncPatient
andelationLinkPatient
Lambdas was reduced from 1024 MB to 512 MB. This is likely an optimization based on observed resource usage patterns.Also applies to: 57-58
40-41
: Increased alarm age timeout for older messages.The alarm max age for oldest messages in the queues increased from 2 to 6 hours. This suggests that messages may legitimately stay in the queue longer than previously anticipated, which is a reasonable adjustment based on operational experience.
Also applies to: 61-62
73-93
: Healthie link patient settings follow established pattern.The
healthieLinkPatient
settings configuration follows the same pattern as the existingelationLinkPatient
configuration, with appropriate timeout, memory, and queue settings.
94-96
: Increased Lambda timeouts for resource-intensive operations.Lambda timeouts were increased for several operations, likely based on observed runtime requirements:
startResourceDiffBundles
: From 5 to 10 minutescomputeResourceDiffBundles
: From 5 to 10 minutesrefreshEhrBundles
: From 5 to 10 minutesThis adjustment shows good operational adaptation to real-world execution times.
Also applies to: 120-120, 144-144
168-168
: Settings return value updated to include Healthie.The
healthieLinkPatient
property is correctly added to the return object of thesettings
function.
188-189
: Class properties added for Healthie resources.The
EhrNestedStack
class is updated with properties for the Healthie link patient Lambda and queue.
237-246
: Constructor initializes Healthie resources.The constructor initializes the Healthie link patient resources using the new
setupHealthieLinkPatient
method and assigns them to the class properties.
380-427
: Setup method for Healthie follows established pattern.The
setupHealthieLinkPatient
method follows the same pattern as the existingsetupElationLinkPatient
method, creating a queue and Lambda with appropriate settings, and adding an SQS event source.This consistent approach to infrastructure setup is excellent for maintainability.
packages/api/src/routes/ehr/healthie/appointment-webhook.ts (7)
1-15
: Imports are properly organized.The imports follow the project's conventions with external dependencies first, followed by internal modules.
18-23
: Documentation explains endpoint purpose clearly.The JSDoc comment clearly explains the purpose of the endpoint and its expected HTTP response.
24-41
: Proper error handling and parameter validation.The route handler correctly:
- Extracts and validates the cxId and practiceId
- Parses the event body against a schema
- Retrieves and validates the CX mapping
- Throws a meaningful error if secondary mappings are missing
This follows best practices for request validation and error handling.
42-45
: Feature flag handling for appointment-based patient linking.The code respects the
webhookAppointmentPatientLinkingDisabled
configuration flag, returning early if appointment-based patient linking is disabled. This allows for flexible deployment configurations.
46-51
: Patient retrieval with early exit for missing patient.The code retrieves the patient ID associated with the appointment and exits early if no patient is found. This graceful handling prevents errors when processing appointments without associated patients.
52-56
: Patient quick notes update follows standard pattern.The call to
updateHealthiePatientQuickNotes
follows the expected pattern for Healthie patient linking.
57-68
: Conditional patient processing based on feature flag.The code uses the
webhookAppointmentPatientProcessingDisabled
flag to conditionally trigger patient synchronization, which aligns with the configuration-driven approach throughout the codebase.packages/api/src/external/ehr/healthie/shared.ts (1)
42-53
:patient.locations
can be undefined
flatMap
will throw iflocations
isundefined
. Consider defaulting to[]
or failing early
with a clear error.- const addresses = patient.locations.flatMap(address => { + const locations = patient.locations ?? []; + const addresses = locations.flatMap(address => {packages/shared/src/interface/external/ehr/healthie/subscription.ts (1)
24-26
: Consider movingsignature_secret
outside of the public-facing typeExposing the
signature_secret
on every read response can leak sensitive data. If Healthie never returns this value after creation, keep it only onsubscriptionWithSignatureSecretGraphqlSchema
and drop it from the plainsubscriptionWithSignatureSecretSchema
. Otherwise, ensure downstream logging redacts it.packages/api/src/routes/ehr/healthie/auth/middleware.ts (1)
33-35
: Potential mismatch inpath
when not running in DEV
verifySignature
is givenpath: `${req.baseUrl}${Config.isDev() ? req.path : ""}`,which means that in production the path omits
req.path
. If the signature is generated on Healthie’s side using the full request path, verification will always fail. Please double-check the contract or derivepath
fromreq.originalUrl
for consistency in all envs.
packages/core/src/util/webhook.ts
Outdated
const signatureHeader = requestParams.headers["signature"]; | ||
if (!signatureHeader) throw new MetriportError("Signature is required"); | ||
const actualSignature = signatureHeader.split("=")[1]; | ||
if (!actualSignature) throw new MetriportError("Signature is required"); | ||
return computedSignature === actualSignature; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Header parsing fragile when multiple signature schemes are present
signature: v1=abcd1234
is currently split on the first "="
, but Healthie (and some proxies) may concatenate multiple schemes, e.g.
Signature: v1=abcd1234, v0=ef5678
.
Splitting on "="
and taking [1]
yields "abcd1234, v0"
which will never match. Consider using a regex:
const match = signatureHeader.match(/v1=([0-9a-fA-F]+)/);
if (!match) throw new MetriportError("v1 signature not found");
const actualSignature = match[1];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Healthie does not concatenate schemas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thomasyopes I understand. Thank you for clarifying that Healthie does not use concatenated signature schemes in their headers. This confirms that the current implementation that splits on "=" is appropriate for this specific integration.
✏️ Learnings added
Learnt from: thomasyopes
PR: metriport/metriport#3771
File: packages/core/src/util/webhook.ts:90-94
Timestamp: 2025-05-01T16:10:09.466Z
Learning: Healthie does not concatenate signature schemas in their webhook headers, so splitting on "=" is appropriate for parsing their signature headers.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This being Healthie-specific should go on it's own folder. Also applicable to other functions on this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
packages/core/src/util/webhook.ts
Outdated
// From Healthie docs https://docs.gethealthie.com/guides/webhooks/ | ||
async function getSigningKey(secretKey: string): Promise<crypto.webcrypto.CryptoKey> { | ||
const encoder = new TextEncoder(); | ||
const keyData = encoder.encode(secretKey); | ||
return await crypto.subtle.importKey( | ||
"raw", | ||
keyData, | ||
{ name: "HMAC", hash: { name: "SHA-256" } }, | ||
false, | ||
["sign"] | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Potential Node-version incompatibility with crypto.webcrypto
and TextEncoder
crypto.webcrypto.subtle
and the WHATWG TextEncoder
/ TextDecoder
APIs are only un-flagged in Node ≥ 18. If any of our Lambda runtimes or CI images are still on Node 16 the import of the raw key will throw at runtime, breaking every Healthie webhook.
Two easy fall-backs:
-import { webcrypto } from "crypto";
+import crypto from "crypto";
-// use webcrypto if present, else fall back
-const subtle = crypto.webcrypto?.subtle ?? undefined;
+const subtle: crypto.webcrypto.SubtleCrypto | undefined =
+ // Prefer native WebCrypto when available (Node ≥ 18)
+ (crypto.webcrypto as any)?.subtle ??
+ // Fallback to `crypto.createHmac`
+ undefined;
and, inside getSigningKey
/ generateSignature
, switch to crypto.createHmac
when subtle
is undefined.
Failing fast with a descriptive MetriportError
would also help debugging when the runtime is too old.
Committable suggestion skipped: line range outside the PR's diff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is copy and pasted from Healthie's documentation. We will not edit it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thomasyopes I understand and respect your decision to maintain the code exactly as provided in Healthie's documentation. This approach ensures compliance with their implementation requirements.
✏️ Learnings added
Learnt from: thomasyopes
PR: metriport/metriport#3771
File: packages/core/src/util/webhook.ts:34-45
Timestamp: 2025-05-01T16:10:45.255Z
Learning: The webhook signature verification code in packages/core/src/util/webhook.ts is copied directly from Healthie's documentation and should not be modified to maintain exact compliance with their implementation.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is specific to Healthie and we're just copy/pasting w/o deeper review I think it should go to it's own folder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (1)
packages/core/src/external/ehr/healthie/index.ts (1)
347-352
: GraphQLquery webhooks()
non-standard but accepted – duplicate prior feedback
The empty parentheses are unconventional GraphQL but the Healthie API reportedly allows it. No action needed unless the vendor API changes.
🧹 Nitpick comments (3)
packages/core/src/external/ehr/webhook.ts (2)
6-6
: Typo in comment – “dosc” ➜ “docs”
Minor spelling fix keeps comments tidy and searchable.-// Interpreted from Elation dosc https://docs.elationhealth.com/reference/webhooks +// Interpreted from Elation docs https://docs.elationhealth.com/reference/webhooks
65-66
: Node-only Blob requires experimental fetch on ≤ v18 – use Buffer for portability
new Blob()
is only guaranteed from Node 20+. UsingBuffer.byteLength()
avoids a hard runtime dependency on the newer fetch implementation.-const contentLength = new Blob([JSON.stringify(body)]).size; +const contentLength = Buffer.byteLength(JSON.stringify(body));packages/core/src/external/ehr/healthie/index.ts (1)
54-55
: RedundantaxiosInstance
creation
The instance created here is immediately replaced insideinitialize()
, so this line is dead code and may confuse future readers.- this.axiosInstance = axios.create({}); + // axiosInstance is created in initialize()
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
📒 Files selected for processing (4)
packages/api/src/routes/ehr/elation/auth/middleware.ts
(2 hunks)packages/api/src/routes/ehr/healthie/auth/middleware.ts
(1 hunks)packages/core/src/external/ehr/healthie/index.ts
(1 hunks)packages/core/src/external/ehr/webhook.ts
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- packages/api/src/routes/ehr/elation/auth/middleware.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/api/src/routes/ehr/healthie/auth/middleware.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
to other types- Type predicates: only applicable to narrow down the type, not to force a complete type conversion
- Prefer deconstructing parameters for functions instead of multiple parameters that might be of
the same type- Don’t use
null
inside the app, only on code interacting with external interfaces/services,
like DB and HTTP; convert toundefined
before sending inwards into the code- Use
async/await
instead of.then()
- Use the strict equality operator
===
, don’t use abstract equality operator==
- When calling a Promise-returning function asynchronously (i.e., not awaiting), use
.catch()
to
handle errors (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
- Avoid creating arrow functions
- Use truthy syntax instead of
in
- i.e.,if (data.link)
notif ('link' in data)
- Error handling
- Pass the original error as the new one’s
cause
so the stack trace is persisted- Error messages should have a static message - add dynamic data to MetriportError's
additionalInfo
prop- Avoid sending multiple events to Sentry for a single error
- Global constants and variables
- Move literals to constants declared after imports when possible (avoid magic numbers)
- Avoid shared, global objects
- Avoid using
console.log
andconsole.error
in packages other than utils, infra and shared,
and try to useout().log
instead- Avoid multi-line logs
- don't send objects as a second parameter to
console.log()
orout().log()
- don't create multi-line strings when using
JSON.stringify()
- Use
eslint
to enforce code style- Use
prettier
to format code- max column length is 100 chars
- multi-line comments use
/** */
- scripts: top-level comments go after the import
packages/core/src/external/ehr/webhook.ts
packages/core/src/external/ehr/healthie/index.ts
🧠 Learnings (2)
packages/core/src/external/ehr/webhook.ts (1)
Learnt from: thomasyopes
PR: metriport/metriport#3771
File: packages/core/src/util/webhook.ts:34-45
Timestamp: 2025-05-01T16:10:45.255Z
Learning: The webhook signature verification code in packages/core/src/util/webhook.ts is copied directly from Healthie's documentation and should not be modified to maintain exact compliance with their implementation.
packages/core/src/external/ehr/healthie/index.ts (1)
Learnt from: thomasyopes
PR: metriport/metriport#3771
File: packages/core/src/external/ehr/healthie/index.ts:348-363
Timestamp: 2025-05-01T16:17:39.110Z
Learning: The Healthie GraphQL API accepts operations with empty parentheses (e.g., `query webhooks()`) even though this isn't standard GraphQL syntax.
🧬 Code Graph Analysis (1)
packages/core/src/external/ehr/webhook.ts (1)
packages/shared/src/index.ts (1)
MetriportError
(40-40)
const computedSignature = await generateSignature(key, dataToSign); | ||
const signatureHeader = requestParams.headers["signature"]; | ||
if (!signatureHeader) throw new MetriportError("Signature is required"); | ||
const actualSignature = signatureHeader.split("=")[1]; | ||
if (!actualSignature) throw new MetriportError("Signature is required"); | ||
return computedSignature === actualSignature; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use constant-time compare for HMAC signatures
A direct ===
leaks timing information and makes the endpoint theoretically vulnerable to a timing-attack. Node provides crypto.timingSafeEqual
.
- return computedSignature === actualSignature;
+ // constant-time comparison
+ const a = Buffer.from(computedSignature, "hex");
+ const b = Buffer.from(actualSignature, "hex");
+ // Length mismatch → early return to avoid throwing inside timingSafeEqual
+ if (a.length !== b.length) return false;
+ return crypto.timingSafeEqual(a, b);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const computedSignature = await generateSignature(key, dataToSign); | |
const signatureHeader = requestParams.headers["signature"]; | |
if (!signatureHeader) throw new MetriportError("Signature is required"); | |
const actualSignature = |
|
if (!actualSignature) throw new MetriportError("Signature is required"); | |
return computedSignature === actualSignature; | |
const computedSignature = await generateSignature(key, dataToSign); | |
const signatureHeader = requestParams.headers["signature"]; | |
if (!signatureHeader) throw new MetriportError("Signature is required"); | |
const actualSignature = signatureHeader.split("=")[1]; | |
if (!actualSignature) throw new MetriportError("Signature is required"); | |
// constant-time comparison | |
const a = Buffer.from(computedSignature, "hex"); | |
const b = Buffer.from(actualSignature, "hex"); | |
// Length mismatch → early return to avoid throwing inside timingSafeEqual | |
if (a.length !== b.length) return false; | |
return crypto.timingSafeEqual(a, b); |
function createPublicKey(key: string) { | ||
return ` | ||
-----BEGIN PUBLIC KEY----- | ||
${ed25519Prefix}${key} | ||
-----END PUBLIC KEY----- | ||
`; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
PEM string contains leading whitespace that can break key parsing
The template literal keeps the indentation before every line, producing spaces in the PEM headers/footers. Some crypto libraries (including OpenSSL) refuse to parse such keys.
-function createPublicKey(key: string) {
- return `
------BEGIN PUBLIC KEY-----
-${ed25519Prefix}${key}
------END PUBLIC KEY-----
- `;
+function createPublicKey(key: string) {
+ // trim() removes the leading new-line and all indentation
+ return `
+-----BEGIN PUBLIC KEY-----
+${ed25519Prefix}${key}
+-----END PUBLIC KEY-----
+`.trim();
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
function createPublicKey(key: string) { | |
return ` | |
-----BEGIN PUBLIC KEY----- | |
${ed25519Prefix}${key} | |
-----END PUBLIC KEY----- | |
`; | |
} | |
function createPublicKey(key: string) { | |
// trim() removes the leading new-line and all indentation | |
return ` | |
-----BEGIN PUBLIC KEY----- | |
${ed25519Prefix}${key} | |
-----END PUBLIC KEY----- | |
`.trim(); | |
} |
const lastAppointment = appointments[appointments.length - 1]; | ||
if (!lastAppointment) return acc; | ||
const nextCursor = lastAppointment.cursor; | ||
return paginateAppointments(api, nextCursor, acc); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible infinite recursion when cursor
is unchanged or missing
If every page returns a non-empty cursor
, paginateAppointments
keeps calling itself even after the last page (where after
yields zero results). This can cause an unbounded loop and eventually a stack overflow.
- const lastAppointment = appointments[appointments.length - 1];
- if (!lastAppointment) return acc;
- const nextCursor = lastAppointment.cursor;
- return paginateAppointments(api, nextCursor, acc);
+ const lastAppointment = appointments[appointments.length - 1];
+ if (!lastAppointment || !lastAppointment.cursor || lastAppointment.cursor === cursor) {
+ return acc; // reached the end
+ }
+ return paginateAppointments(api, lastAppointment.cursor, acc);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const lastAppointment = appointments[appointments.length - 1]; | |
if (!lastAppointment) return acc; | |
const nextCursor = lastAppointment.cursor; | |
return paginateAppointments(api, nextCursor, acc); | |
} | |
const lastAppointment = appointments[appointments.length - 1]; | |
if (!lastAppointment || !lastAppointment.cursor || lastAppointment.cursor === cursor) { | |
return acc; // reached the end | |
} | |
return paginateAppointments(api, lastAppointment.cursor, acc); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The last call with have no appointments and return on if (!lastAppointment) return acc;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thomasyopes You're absolutely right. When the pagination reaches the end, the API will return an empty appointments array, which would make lastAppointment
undefined and trigger the existing if (!lastAppointment) return acc;
condition. Thank you for explaining the expected API behavior - the termination condition is already properly handled in the current implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approving considering we can discuss/address outstanding comments on follow-up PRs.
Issues:
Dependencies
Description
Testing
Check each PR.
Release Plan
master
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Chores