8000 RELEASE healthie v0 by thomasyopes · Pull Request #3771 · metriport/metriport · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

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

Merged
merged 39 commits into from
May 1, 2025
Merged

RELEASE healthie v0 #3771

merged 39 commits into from
May 1, 2025

Conversation

thomasyopes
Copy link
Contributor
@thomasyopes thomasyopes commented May 1, 2025

Issues:

Dependencies

Description

Testing

Check each PR.

Release Plan

  • ⚠️ Points to master
  • Upstream dependencies are met/released
  • Merge this

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced full integration with the Healthie EHR platform, including patient and appointment data synchronization, webhook event processing, and secure JWT-based dashboard access.
    • Added API endpoints for Healthie patient management, appointment processing, and webhook handling.
    • Enabled background processing of Healthie appointments and patient linking with robust error handling and concurrency controls.
    • Implemented infrastructure support for Healthie, including new SQS queues, Lambda functions, and environment configuration.
  • Bug Fixes

    • Improved error handling for appointment retrieval in various EHR integrations to gracefully handle specific error types without unnecessary logging.
  • Documentation

    • Updated API documentation to accurately reflect query parameters for patient-related endpoints across multiple EHR integrations.
  • Chores

    • Extended infrastructure and configuration to support Healthie integration and secrets management.
    • Standardized retry logic defaults and SQS batch size constants for improved reliability and scalability.

8000
Thomas Yopes added 30 commits April 29, 2025 11:38
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>
Thomas Yopes and others added 6 commits May 1, 2025 06:08
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
Copy link
coderabbitai bot commented May 1, 2025

Walkthrough

This 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

Files / Paths (Grouped) Change Summary
packages/api/src/external/ehr/healthie/command/, packages/api/src/external/ehr/healthie/shared.ts New modules for Healthie: patient/appointment processing, webhook subscription, patient syncing, and shared utilities.
packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.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 Improved error handling: BadRequestError and NotFoundError return empty object without logging.
packages/api/src/external/ehr/elation/command/sync-patient.ts Added dayjs duration plugin and constants for token durations.
packages/api/src/routes/ehr/healthie/, packages/api/src/routes/ehr/healthie/auth/middleware.ts New Express routers and middleware for Healthie patient, appointment, and webhook endpoints.
packages/api/src/routes/ehr/athenahealth/patient.ts, packages/api/src/routes/ehr/elation/patient.ts, packages/api/src/routes/internal/ehr/athenahealth/patient.ts, packages/api/src/routes/internal/ehr/elation/patient.ts Updated JSDoc comments for route parameter documentation.
packages/api/src/routes/ehr/healthie/routes/, packages/api/src/routes/ehr/index.ts Route aggregation and registration for Healthie dashboard and webhook endpoints.
packages/api/src/routes/internal/ehr/healthie/, packages/api/src/routes/internal/ehr/index.ts New internal API routes for Healthie patient, secret key management, and route registration.
packages/api/src/routes/internal/jwt-token/healthie.ts, packages/api/src/routes/internal/jwt-token/index.ts New internal JWT token handlers and route registration for Healthie.
packages/core/src/external/ehr/healthie/, packages/core/src/util/config.ts New Healthie API client, patient linking handler (cloud/local), handler factory, and config for SQS queue URL.
packages/core/src/util/sqs.ts Adjusted SQS constants: batch sizes split for FIFO/standard, max message size reduced.
packages/core/src/util/webhook.ts Added HMAC-SHA256 webhook signature verification for Healthie.
packages/core/sr 8000 c/external/ehr/shared.ts Improved request retry logic; retries not performed for 400/404 errors, header checks case-insensitive.
packages/core/src/command/write-to-storage/s3/write-to-s3-cloud.ts, packages/core/src/external/ehr/bundle/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-cloud.ts Updated SQS batch size constants for chunking.
packages/core/src/external/ehr/bundle/create-resource-diff-bundles/steps/start/ehr-start-resource-diff-bundles-local.ts Added guard clause to skip processing if no resources.
packages/core/src/external/ehr/api/link-patient.ts, packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts, packages/lambdas/src/shared/ehr.ts Generalized patient linking logic to support multiple EHRs, including Healthie.
packages/lambdas/src/healthie-link-patient.ts New Lambda handler for Healthie patient linking from SQS.
packages/infra/config/env-config.ts, packages/infra/lib/api-stack/api-service.ts, packages/infra/lib/ehr-nested-stack.ts, packages/infra/lib/api-stack.ts, packages/infra/lib/secrets-stack.ts, packages/infra/lib/shared/secrets.ts Infrastructure updates: new Healthie queue, Lambda, environment variables, and secret management.
packages/shared/src/interface/external/ehr/healthie/ (all files), packages/shared/src/interface/external/ehr/source.ts New shared schemas, types, and source enum value for Healthie.
packages/shared/src/domain/secrets.ts New Zod schema for API key map secrets.
packages/shared/src/common/retry.ts Default retry attempts reduced from 10 to 5.
packages/api/src/routes/ehr/elation/auth/middleware.ts Reordered import statements and replaced webhook signature verification call.
packages/core/src/external/ehr/athenahealth/index.ts, packages/core/src/external/ehr/elation/index.ts Fixed HTTP header capitalization; improved error handling for specific error types.
packages/lambdas/src/elation-link-patient.ts Updated import and function usage to generalized link patient parsing.
packages/shared/src/interface/external/ehr/elation/index.ts Added exports for cx-mapping and event modules.
packages/core/src/external/ehr/webhook.ts Added separate webhook signature verification functions for Elation (Ed25519) and Healthie (HMAC-SHA256).

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
Loading

Possibly related PRs

  • feat(healthie): integration v0 #3754: Implements the initial Healthie integration, including foundational modules for patient processing, API client, webhooks, and route handlers; highly related as it provides the base Healthie integration which this PR builds upon or complements.

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
@coderabbitai coderabbitai bot left a 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. The for loop iterator named chunk shadows the imported chunk function from lodash. Consider renaming the variable to batch, 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 logic

Both 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 logic

Both 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 documentation

While 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 clarity

The 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 properties

The 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 field
packages/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 jobs

The 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 be undefined – clarify default

getFromQueryAsBoolean returns boolean | undefined, but syncHealthiePatientIntoMetriport
expects triggerDq?: boolean.
If the absence of the flag should be treated as false, consider forcing the default here to
avoid propagating undefined.

-    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 parallel

The 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 after executeAsynchronously 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 mutating req.query directly

The middleware rewrites req.query in-place, which can surprise downstream handlers that have already captured a reference. Prefer assigning to res.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 error

Wrapping every caught error in a new ForbiddenError() discards the cause, violating the guideline “Pass the original error as the new one’s cause”.

-  } catch (error) {
-    throw new ForbiddenError();
+  } catch (error) {
+    throw new ForbiddenError(undefined, { cause: error });
   }

68-79: updateTokenExpiration failure is reported as 403

If 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 validating cursor as an opaque cursor or ISO date

Right 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() or z.string().regex(...)) prevents subtle pagination bugs.


10-13: AppointmentWithAttendee can be simplified

Zod 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-created HealthieApi instead of instantiating twice

healthieApi is created on L73 yet updateHealthiePatientQuickNotes may create another client when healthieApi 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

📥 Commits

Reviewing files that changed from the base of the PR and between e3a6eed and ee751be.

📒 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, prefer isDisabled
    • For numeric values, if the type doesn’t convey the unit, add the unit to the name
  • Typescript
    • Use types
    • Prefer const instead of let
    • Avoid any and casting from any 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 to undefined 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 (see processAsyncError and emptyFunction depending on the case)
    • Date and Time
      • Always use buildDayjs() to create dayjs instances
      • Prefer dayjs.duration(...) to create duration consts and keep them as duration
  • 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) not if ('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 and console.error in packages other than utils, infra and shared,
    and try to use out().log instead
  • Avoid multi-line logs
    • don't send objects as a second parameter to console.log() or out().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 for cx-mapping and event 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. Adding healthie to EhrSources 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 generic parseLinkPatient 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), but parseBody is typed as ProcessLinkPatientRequest (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 to SQS_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 configuration

The 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 practices

The 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 good

The 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 ts

Length 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 ts

Length 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:

  1. Updates the import path to use a more generic link-patient module
  2. Adds the explicit EHR source parameter to the function call
  3. 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:

  1. Import and extend dayjs with the duration plugin
  2. Define named constants for token durations instead of using magic numbers
  3. 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 and NotFoundError 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 and NotFoundError 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 and NotFoundError 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 and NotFoundError 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 parameters

The added documentation for practiceId and departmentId query parameters improves API clarity by accurately reflecting what the implementation already requires.


44-45: Documentation improvement for API query parameters

The added documentation for practiceId and departmentId 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 types

These changes make good improvements:

  1. Reducing MAX_SQS_MESSAGE_SIZE provides more buffer against AWS limits
  2. Adding queue-specific batch size constants (SQS_MESSAGE_BATCH_SIZE_FIFO and SQS_MESSAGE_BATCH_SIZE_STANDARD) provides better configuration for different queue types

The 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 parameter

The added documentation for practiceId query parameter improves API clarity by accurately reflecting what the implementation already requires.


44-44: Documentation improvement for API query parameter

The 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 request

The ProcessLinkPatientRequest type is clear and provides all necessary fields for patient linking operations.


7-9: Clean interface design for patient linking handler

The HealthieLinkPatientHandler interface follows good design principles:

  1. Clear single responsibility (processing patient links)
  2. Simple async method signature with well-defined request type
  3. 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 and healthieLinkPatientLambda 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 as subscribeToHealthieWebhooks 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 or subscribeToHealthieWebhooks 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 the HealthieLinkPatientHandler interface and follows the functional programming style guidelines. The implementation is clean, focused, and properly leverages the existing linkPatient 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:

  1. Including the EHR source in the error metadata
  2. 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:

  1. Parameter handling
  2. Patient route processing
  3. EHR patient ID extraction
  4. Patient authorization
  5. 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:

  1. Document route processing
  2. EHR patient ID extraction with appropriate skip paths
  3. 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 issue

Possible runtime crash when attendees is empty

healthieAppointment.attendees[0].id assumes at least one attendee. If the appointment has no attendees the call will throw TypeError: 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 suggestion

Add 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 linking

This 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 check

The 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 logic

The implementation of executeWithRetries with a custom retry strategy improves the resilience of the system by:

  1. Centralizing retry logic
  2. Avoiding unnecessary retries for client errors (400, 404) that are unlikely to succeed upon retry
  3. 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 checking

Using isAxiosError type guard and the existing errorToString function improves type safety and maintains consistent error handling patterns across the codebase.


226-228: Good utility function for error classification

The 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 safety

The 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 Healthie

The 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:

  1. Extracts and validates the cxId and practiceId
  2. Parses the event body against a schema
  3. Retrieves and validates the CX mapping
  4. 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 the ehrDashJwtTokenSources 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 new healthieLinkPatient property.


36-37: Memory allocation reduced for Lambda functions.

Memory allocation for syncPatient and elationLinkPatient 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 existing elationLinkPatient 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 minutes
  • computeResourceDiffBundles: From 5 to 10 minutes
  • refreshEhrBundles: From 5 to 10 minutes

This 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 the settings 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 existing setupElationLinkPatient 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:

  1. Extracts and validates the cxId and practiceId
  2. Parses the event body against a schema
  3. Retrieves and validates the CX mapping
  4. 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 if locations is undefined. 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 moving signature_secret outside of the public-facing type

Exposing the signature_secret on every read response can leak sensitive data. If Healthie never returns this value after creation, keep it only on subscriptionWithSignatureSecretGraphqlSchema and drop it from the plain subscriptionWithSignatureSecretSchema. Otherwise, ensure downstream logging redacts it.

packages/api/src/routes/ehr/healthie/auth/middleware.ts (1)

33-35: Potential mismatch in path when not running in DEV

verifySignature is given

path: `${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 derive path from req.originalUrl for consistency in all envs.

Comment on lines 90 to 94
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;
Copy link
@coderabbitai coderabbitai bot May 1, 2025

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];

Copy link
Contributor Author

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.

Copy link

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.

Copy link
Member

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.

Copy link

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!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 34 to 45
// 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"]
);
}
Copy link
@coderabbitai coderabbitai bot May 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

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.

Copy link
Contributor Author

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.

Copy link

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.

Copy link
Member

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.

Copy link

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!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Thomas Yopes and others added 3 commits May 1, 2025 09:22
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): change last page determination
Copy link
@coderabbitai coderabbitai bot left a 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: GraphQL query 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+. Using Buffer.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: Redundant axiosInstance creation
The instance created here is immediately replaced inside initialize(), 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

📥 Commits

Reviewing files that changed from the base of the PR and between ee751be and 26b95a7.

📒 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, prefer isDisabled
    • For numeric values, if the type doesn’t convey the unit, add the unit to the name
  • Typescript
    • Use types
    • Prefer const instead of let
    • Avoid any and casting from any 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 to undefined 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 (see processAsyncError and emptyFunction depending on the case)
    • Date and Time
      • Always use buildDayjs() to create dayjs instances
      • Prefer dayjs.duration(...) to create duration consts and keep them as duration
  • 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) not if ('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 and console.error in packages other than utils, infra and shared,
    and try to use out().log instead
  • Avoid multi-line logs
    • don't send objects as a second parameter to console.log() or out().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)

Comment on lines +90 to +95
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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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;
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);

Comment on lines +23 to +29
function createPublicKey(key: string) {
return `
-----BEGIN PUBLIC KEY-----
${ed25519Prefix}${key}
-----END PUBLIC KEY-----
`;
}
Copy link

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.

Suggested change
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();
}

Comment on lines +289 to +293
const lastAppointment = appointments[appointments.length - 1];
if (!lastAppointment) return acc;
const nextCursor = lastAppointment.cursor;
return paginateAppointments(api, nextCursor, acc);
}
Copy link
@coderabbitai coderabbitai bot May 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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);

Copy link
Contributor Author

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;

Copy link

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.

Copy link
Member
@leite08 leite08 left a 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.

@thomasyopes thomasyopes added this pull request to the merge queue May 1, 2025
Merged via the queue into master with commit 9ad1828 May 1, 2025
71 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
0