8000 feat(ehr): moving get-appointments for Canvas to lambda by thomasyopes · Pull Request #3873 · metriport/metriport · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat(ehr): moving get-appointments for Canvas to lambda #3873

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

Open
wants to merge 43 commits into
base: develop
Choose a base branch
from

Conversation

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

Ref: ENG-292

Issues:

Dependencies

Description

ENTRYPONT: Canvas process-patients-from-appointments.ts

  • adding new get-appointments lambda (currently Canvas get appointments only) and command lookup map
  • adding new get-token-info internal endpoint for getting token info for a client in a lambda
  • adding new get-secrets internal endpoint per practice for getting secrets in the event the token is expired and needs to be refreshed
  • introduced new command folder
  • wired Canavs get-appointments API command to use new lambda connector
  • added infra for get-appointments lambda connector
  • added lambda code for get-appointments lambda connector + adding permissions to write to the EHR responses bucket
  • converted async s3 writes in the shared request handler to synchronous now that it'll be running on a lambda sometimes

Testing

  • Local
    • canvas get-appointments runs correctly on "local" implementation with no token info (will force the client to fetch secrets and create a NEW token from handler)
    • canvas get-appointments runs correctly on "local" implementation with token id (will have the client fetch the token info)
  • Staging
    • canvas get-appointments runs correctly on "local" implementation with token id (will have the client fetch the token info)
  • Sandbox
    • N/A
  • Production
    • canvas get-appointments runs correctly on "local" implementation with token id (will have the client fetch the token info)

Release Plan

  • Merge this

Summary by CodeRabbit

  • New Features

    • Introduced a new internal API endpoint to retrieve secrets for EHR practices.
    • Added a Lambda function and supporting infrastructure for fetching EHR appointments.
    • Implemented a generic handler for retrieving appointments from EHR systems, with support for Canvas EHR.
    • Added JWT token retrieval endpoint for internal use.
    • Added explicit type alias for AWS Lambda client.
  • Improvements

    • Enhanced appointment time range calculations for more accurate results.
    • Refined client creation for EHR integrations to include token and environment metadata.
    • Updated internal APIs and schemas for better structure and extensibility.
    • Improved error handling and logging for file uploads and external API calls.
    • Added environment variable support for EHR appointments Lambda in API service.
    • Added factory for appointment handlers selecting between direct and Lambda invocation based on environment.
    • Consolidated appointment fetching logic to use handlers with token and environment metadata.
    • Made environment retrieval functions publicly accessible for multiple EHR integrations.
  • Bug Fixes

    • Corrected appointment lookup window for Healthie integration.
  • Chores

    • Updated import paths and refactored code organization for clarity and maintainability.
    • Removed unused test data generators.

Ref: ENG-292

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Copy link
linear bot commented May 19, 2025

Copy link
coderabbitai bot commented May 19, 2025
## Walkthrough

This update introduces a new, modular architecture for EHR appointment retrieval and secrets management, adds Lambda-based appointment fetching, and unifies EHR client creation for multiple vendors. It includes new route handlers for internal secrets and token retrieval, refactors import paths, and removes obsolete Canvas test data generators. Several new types, schemas, and error handling improvements are also added.

## Changes

| File(s) / Path(s)                                                                                   | Change Summary |
|-----------------------------------------------------------------------------------------------------|---------------|
| api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts<br>api/src/external/ehr/elation/command/process-patients-from-appointments.ts<br>api/src/routes/ehr/canvas/patient-webhook.ts<br>api/src/routes/ehr/elation/appointment-webhook.ts<br>api/src/routes/ehr/elation/patient-webhook.ts<br>api/src/routes/ehr/healthie/appointment-webhook.ts<br>api/src/routes/ehr/healthie/patient-webhook.ts<br>lambdas/src/ehr-sync-patient.ts<br>lambdas/src/elation-link-patient.ts<br>lambdas/src/healthie-link-patient.ts<br>lambdas/src/shared/ehr.ts | Refactored import paths for handler factories and request types to new `command` subdirectories. |
| api/src/external/ehr/athenahealth/shared.ts<br>api/src/external/ehr/canvas/shared.ts<br>api/src/external/ehr/elation/shared.ts<br>api/src/external/ehr/healthie/shared.ts | Unified EHR client creation: split into `{createXClientWithTokenIdAndEnvironment, createXClient}` pairs; made environment getter functions exported. |
| api/src/external/ehr/canvas/command/process-patients-from-appointments.ts | Refactored to use new appointment handler abstraction and updated client creation. |
| api/src/external/ehr/healthie/command/process-patients-from-appointments.ts | Updated appointment window calculation logic and refactored import paths. |
| api/src/external/ehr/shared/command/secrets/get-secrets.ts | Added new `getSecrets` function to unify EHR secrets retrieval. |
| api/src/external/ehr/shared/utils/appointment.ts | Fixed time range calculation in `getLookForwardTimeRangeWithOffset`. |
| api/src/external/ehr/shared/utils/client.ts | Made `EhrEnv` exported; `createEhrClient` now returns client, tokenId, and environment. |
| api/src/routes/ehr/util.ts | Added braces to conditional for clarity; no logic change. |
| api/src/routes/internal/ehr/index.ts | Added new `/practice` route for internal EHR endpoints. |
| api/src/routes/internal/ehr/practice.ts | New router: GET `/internal/ehr/:id/secrets` for EHR/practice secrets retrieval. |
| api/src/routes/internal/jwt-token/index.ts | Added new `/token` route for JWT token operations. |
| api/src/routes/internal/jwt-token/token.ts | New router: GET `/internal/token/:id` for JWT token retrieval by ID. |
| core/src/external/aws/lambda.ts | Added `LambdaClient` type alias; made return type explicit for `makeLambdaClient`. |
| core/src/external/ehr/api/get-client-key-and-secret.ts | Added `getSecrets` function for EHR API secrets retrieval. |
| core/src/external/ehr/api/get-token-info.ts | Added `getTokenInfo` function for JWT token info retrieval. |
| core/src/external/ehr/canvas/command/get-appointments.ts | Added `getAppointments` function for Canvas appointment retrieval. |
| core/src/external/ehr/canvas/data.ts | Deleted: removed test/demo FHIR bundle and resource generators. |
| core/src/external/ehr/canvas/shared.ts | Added `createCanvasClient` for Canvas API client instantiation with token info. |
| core/src/external/ehr/command/get-appointments/ehr-get-appointments-cloud.ts | New class: Lambda-based appointment retrieval handler. |
| core/src/external/ehr/command/get-appointments/ehr-get-appointments-direct.ts | New class: Direct appointment retrieval handler and handler map. |
| core/src/external/ehr/command/get-appointments/ehr-get-appointments-factory.ts | New factory function to select appointment handler based on environment. |
| core/src/external/ehr/command/get-appointments/ehr-get-appointments.ts | New types/interfaces for appointment retrieval and handler abstraction. |
| core/src/external/ehr/command/sync-patient/ehr-sync-patient-cloud.ts<br>core/src/external/ehr/command/sync-patient/ehr-sync-patient-factory.ts<br>core/src/external/ehr/command/sync-patient/ehr-sync-patient-local.ts<br>core/src/external/ehr/elation/command/link-patient/elation-link-patient-cloud.ts<br>core/src/external/ehr/elation/command/link-patient/elation-link-patient-factory.ts<br>core/src/external/ehr/elation/command/link-patient/elation-link-patient-local.ts<br>core/src/external/ehr/healthie/command/link-patient/healthie-link-patient-cloud.ts<br>core/src/external/ehr/healthie/command/link-patient/healthie-link-patient-factory.ts<br>core/src/external/ehr/healthie/command/link-patient/healthie-link-patient-local.ts | Refactored import paths for configuration and utility modules. |
| core/src/external/ehr/job/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-cloud.ts<br>core/src/external/ehr/job/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-factory.ts<br>core/src/external/ehr/job/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-local.ts<br>core/src/external/ehr/job/create-resource-diff-bundles/steps/refresh/ehr-refresh-ehr-bundles-cloud.ts<br>core/src/external/ehr/job/create-resource-diff-bundles/steps/refresh/ehr-refresh-ehr-bundles-factory.ts<br>core/src/external/ehr/job/create-resource-diff-bundles/steps/refresh/ehr-refresh-ehr-bundles-local.ts | Refactored import paths for job/bundle modules. |
| api/src/external/ehr/shared/job/bundle/create-resource-diff-bundles/start-job.ts | Refactored import path for refresh bundles handler. |
| lambdas/src/ehr-compute-resource-diff-bundles.ts<br>lambdas/src/ehr-refresh-ehr-bundles.ts | Refactored import paths for job/bundle modules. |
| core/src/external/ehr/shared.ts | Improved S3 error handling; added Zod schemas/types for OAuth and API key secrets. |
| core/src/util/config.ts | Added `getEhrGetAppointmentsLambdaName` method to `Config`. |
| infra/lib/api-stack.ts | Added `ehrGetAppointmentsLambda` to API stack and environment. |
| infra/lib/api-stack/api-service.ts | Added `ehrGetAppointmentsLambda` parameter, environment variable, and permissions. |
| infra/lib/ehr-nested-stack.ts | Added support for `getAppointments` Lambda, new settings, and permissions. |
| lambdas/src/ehr-get-appointments.ts | New Lambda handler for appointment retrieval via SQS. |
| lambdas/src/shared/ehr.ts | Added Zod schema for appointment retrieval requests. |
| shared/src/domain/jwt-token.ts | Added optional `id` to `JwtTokenInfo` type. |

## Sequence Diagram(s)

### EHR Appointment Retrieval (New Lambda-Based Flow)

```mermaid
sequenceDiagram
    participant API_Service
    participant Lambda (ehr-get-appointments)
    participant EHR_AppointmentsHandler
    participant EHR_System

    API_Service->>Lambda: SQS message (appointment request)
    Lambda->>Lambda: Parse & validate message
    Lambda->>EHR_AppointmentsHandler: getAppointments(params)
    EHR_AppointmentsHandler->>EHR_System: Fetch appointments (API call)
    EHR_System-->>EHR_AppointmentsHandler: Appointment data
    EHR_AppointmentsHandler-->>Lambda: Appointment data
    Lambda-->>API_Service: Return appointment data

EHR Secrets Retrieval (Internal Route)

sequenceDiagram
    participant Internal_Client
    participant API_Service
    participant getSecrets Handler
    participant EHR_SecretsProvider

    Internal_Client->>API_Service: GET /internal/ehr/:id/secrets?cxId=...&practiceId=...
    API_Service->>getSecrets Handler: getSecrets({ehr, cxId, practiceId})
    getSecrets Handler->>EHR_SecretsProvider: Fetch secrets (by EHR type)
    EHR_SecretsProvider-->>getSecrets Handler: Secrets (OAuth or API key)
    getSecrets Handler-->>API_Service: Secrets
    API_Service-->>Internal_Client: JSON response
Loading

Possibly related PRs

  • metriport/metriport#3874: Introduces a new EClinicalWorks EHR integration with client and JWT token handling, related by EHR client and token management patterns.
  • metriport/metriport#3788: Refactors and centralizes EHR shared logic, resource diff jobs, and token management, directly related to this PR's architectural changes.
  • metriport/metriport#3870: Refactors and removes Canvas-specific resource diff bundle job code, related through shared EHR resource diff and Canvas integration changes.


<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> npm error code ERR_SSL_WRONG_VERSION_NUMBER
> npm error errno ERR_SSL_WRONG_VERSION_NUMBER
> npm error request to https://10.0.0.28:4873/punycode/-/punycode-2.3.1.tgz failed, reason: C0BC7E43907F0000:error:0A00010B:SSL routines:ssl3_get_record:wrong version number:../deps/openssl/openssl/ssl/record/ssl3_record.c:354:
> npm error
> npm error A complete log of this run can be found in: /.npm/_logs/2025-05-30T12_15_54_809Z-debug-0.log
> 
> 
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e00066e9d8aa86135531d3fe9051452aed432142 and eac887f9eefc236dead9f34a05884fe8df147fc0.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/core/src/external/ehr/api/get-client-key-and-secret.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/core/src/external/ehr/api/get-client-key-and-secret.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

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

</details>

</details>
<!-- internal state start -->


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

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=metriport/metriport&utm_content=3873):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `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 explain this code block.`
  -	`@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 explain its main purpose.`
  - `@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](https://www.coderabbit.ai/contact-us/support) 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 docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this 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](https://docs.coderabbit.ai/guides/configure-coderabbit) 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](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Thomas Yopes added 6 commits May 20, 2025 10:27
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
@@ -220,7 +219,7 @@ async function getAppointmentsFromApi({
}
if (lookupMode === LookupModes.Appointments48hr) {
const { startRange, endRange } = getLookForwardTimeRangeWithOffset({
lookForward: appointmentsLookForward48hr,
lookForward: appointmentsLookForward,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

offset is now applied to both dates so it's now just a 1day look-forward + 1day offset

Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Thomas Yopes added 4 commits May 20, 2025 12:04
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
@thomasyopes thomasyopes changed the title feat(ehr): moving appointments + resource fetching to lambda feat(ehr): moving appointments for Canvas May 20, 2025
@thomasyopes thomasyopes marked this pull request as ready for review May 20, 2025 19:31
@thomasyopes thomasyopes changed the title feat(ehr): moving appointments for Canvas feat(ehr): moving get-appointments for Canvas to lambda May 20, 2025
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: 11

♻️ Duplicate comments (1)
packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts (1)

222-224: LGTM! Standardized appointment lookup duration.

The change to use appointmentsLookForward for the 48hr lookup mode (with an offset) standardizes the approach to time range calculations across different modes.

🧹 Nitpick comments (6)
packages/core/src/external/ehr/lambdas/elation/link-patient/elation-link-patient-local.ts (1)

3-3: Consistent import path update for linkPatient.
The import has been updated to reflect the new lambdas directory structure. Ensure this relative path remains valid and consider using a path alias (e.g., @metriport/api) to prevent brittle deep-relative imports when refactoring folder layouts.

packages/api/src/routes/internal/ehr/practice.ts (1)

13-15: Incorrect HTTP method in documentation

The JSDoc comment specifies a POST endpoint, but the implementation uses a GET endpoint.

-/**
- * POST /internal/ehr/:ehrId/practice/:id/secrets
- *
+/**
+ * GET /internal/ehr/:ehrId/practice/:id/secrets
+ *
packages/lambdas/src/ehr-get-appointments.ts (2)

18-19: Address the TODO comment regarding Sentry wrapper

The TODO comment indicates that the Sentry wrapping should be moved to the capture.wrapHandler() function.

Consider replacing the direct Sentry.AWSLambda.wrapHandler with your own utility:

-// TODO move to capture.wrapHandler()
-export const handler = Sentry.AWSLambda.wrapHandler(async (event: SQSEvent) => {
+export const handler = capture.wrapHandler(async (event: SQSEvent) => {

27-28: Consider simplifying with destructuring assignment

You could destructure properties directly from parseBody() to avoid the intermediate variable.

-  const parsedBody = parseBody(message.body);
-  const { ehr, cxId, practiceId } = parsedBody;
+  const { ehr, cxId, practiceId, ...rest } = parseBody(message.body);
packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-cloud.ts (1)

8-46: Fix typo in filename path.

The file is located in a directory with a misspelled name: appoinment/get-appoinemtns/ should be appointment/get-appointments/.

packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments.ts (1)

21-25: Fix typo in enum value.

The enum value contains a typo: "canvasGetAppoinemtns" should be "canvasGetAppointments".

export enum AppointmentMethods {
-  canvasGetAppointments = "canvasGetAppoinemtns",
+  canvasGetAppointments = "canvasGetAppointments",
}

Also note that the folder path in the file location contains the same typo ("get-appoinemtns" instead of "get-appointments").

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 741f3bc and 358826d.

📒 Files selected for processing (56)
  • packages/api/src/command/jwt-token.ts (1 hunks)
  • packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts (1 hunks)
  • packages/api/src/external/ehr/athenahealth/shared.ts (3 hunks)
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts (3 hunks)
  • packages/api/src/external/ehr/canvas/shared.ts (2 hunks)
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts (1 hunks)
  • packages/api/src/external/ehr/elation/shared.ts (3 hunks)
  • packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts (2 hunks)
  • packages/api/src/external/ehr/healthie/shared.ts (1 hunks)
  • packages/api/src/external/ehr/shared/command/secrets/get-secrets.ts (1 hunks)
  • packages/api/src/external/ehr/shared/utils/appointment.ts (1 hunks)
  • packages/api/src/external/ehr/shared/utils/client.ts (6 hunks)
  • packages/api/src/external/ehr/shared/utils/secrets.ts (1 hunks)
  • packages/api/src/routes/ehr/canvas/patient-webhook.ts (1 hunks)
  • packages/api/src/routes/ehr/elation/appointment-webhook.ts (1 hunks)
  • packages/api/src/routes/ehr/elation/patient-webhook.ts (1 hunks)
  • packages/api/src/routes/ehr/healthie/appointment-webhook.ts (1 hunks)
  • packages/api/src/routes/ehr/healthie/patient-webhook.ts (1 hunks)
  • packages/api/src/routes/ehr/util.ts (1 hunks)
  • packages/api/src/routes/internal/ehr/index.ts (2 hunks)
  • packages/api/src/routes/internal/ehr/practice.ts (1 hunks)
  • packages/api/src/routes/internal/jwt-token/index.ts (2 hunks)
  • packages/api/src/routes/internal/jwt-token/token.ts (1 hunks)
  • packages/core/src/external/aws/lambda.ts (1 hunks)
  • packages/core/src/external/ehr/api/get-client-key-and-secret.ts (1 hunks)
  • packages/core/src/external/ehr/api/get-token-info.ts (1 hunks)
  • packages/core/src/external/ehr/athenahealth/index.ts (5 hunks)
  • packages/core/src/external/ehr/canvas/command/get-appointments.ts (1 hunks)
  • packages/core/src/external/ehr/canvas/data.ts (0 hunks)
  • packages/core/src/external/ehr/canvas/index.ts (4 hunks)
  • packages/core/src/external/ehr/elation/index.ts (3 hunks)
  • packages/core/src/external/ehr/healthie/index.ts (3 hunks)
  • packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-factory.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-local.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/elation/link-patient/elation-link-patient-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/elation/link-patient/elation-link-patient-factory.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/elation/link-patient/elation-link-patient-local.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/healthie/link-patient/healthie-link-patient-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/healthie/link-patient/healthie-link-patient-factory.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/healthie/link-patient/healthie-link-patient-local.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/sync-patient/ehr-sync-patient-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/sync-patient/ehr-sync-patient-factory.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/sync-patient/ehr-sync-patient-local.ts (1 hunks)
  • packages/core/src/external/ehr/shared.ts (1 hunks)
  • packages/core/src/util/config.ts (1 hunks)
  • packages/infra/lib/api-stack.ts (3 hunks)
  • packages/infra/lib/api-stack/api-service.ts (4 hunks)
  • packages/infra/lib/ehr-nested-stack.ts (5 hunks)
  • packages/lambdas/src/ehr-get-appointments.ts (1 hunks)
  • packages/lambdas/src/ehr-sync-patient.ts (1 hunks)
  • packages/lambdas/src/elation-link-patient.ts (1 hunks)
  • packages/lambdas/src/healthie-link-patient.ts (1 hunks)
  • packages/lambdas/src/shared/ehr.ts (2 hunks)
  • packages/shared/src/domain/jwt-token.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • packages/core/src/external/ehr/canvas/data.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/lambdas/sync-patient/ehr-sync-patient-factory.ts
  • packages/api/src/routes/ehr/util.ts
  • packages/lambdas/src/elation-link-patient.ts
  • packages/lambdas/src/ehr-sync-patient.ts
  • packages/api/src/external/ehr/shared/utils/appointment.ts
  • packages/core/src/external/ehr/lambdas/sync-patient/ehr-sync-patient-cloud.ts
  • packages/core/src/external/ehr/lambdas/elation/link-patient/elation-link-patient-factory.ts
  • packages/core/src/external/ehr/lambdas/healthie/link-patient/healthie-link-patient-local.ts
  • packages/api/src/routes/ehr/canvas/patient-webhook.ts
  • packages/api/src/routes/ehr/elation/patient-webhook.ts
  • packages/api/src/routes/ehr/elation/appointment-webhook.ts
  • packages/api/src/routes/internal/jwt-token/index.ts
  • packages/core/src/external/ehr/lambdas/sync-patient/ehr-sync-patient-local.ts
  • packages/core/src/external/ehr/lambdas/healthie/link-patient/healthie-link-patient-cloud.ts
  • packages/api/src/routes/ehr/healthie/patient-webhook.ts
  • packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts
  • packages/lambdas/src/shared/ehr.ts
  • packages/api/src/routes/ehr/healthie/appointment-webhook.ts
  • packages/core/src/external/ehr/lambdas/elation/link-patient/elation-link-patient-cloud.ts
  • packages/shared/src/domain/jwt-token.ts
  • packages/infra/lib/api-stack.ts
  • packages/api/src/routes/internal/jwt-token/token.ts
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts
  • packages/lambdas/src/healthie-link-patient.ts
  • packages/core/src/external/ehr/api/get-token-info.ts
  • packages/api/src/routes/internal/ehr/index.ts
  • packages/core/src/external/aws/lambda.ts
  • packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-factory.ts
  • packages/core/src/util/config.ts
  • packages/api/src/external/ehr/healthie/shared.ts
  • packages/lambdas/src/ehr-get-appointments.ts
  • packages/infra/lib/api-stack/api-service.ts
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts
  • packages/api/src/command/jwt-token.ts
  • packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts
  • packages/api/src/routes/internal/ehr/practice.ts
  • packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments.ts
  • packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-local.ts
  • packages/core/src/external/ehr/canvas/command/get-appointments.ts
  • packages/api/src/external/ehr/shared/utils/secrets.ts
  • packages/api/src/external/ehr/shared/command/secrets/get-secrets.ts
  • packages/core/src/external/ehr/api/get-client-key-and-secret.ts
  • packages/core/src/external/ehr/lambdas/elation/link-patient/elation-link-patient-local.ts
  • packages/core/src/external/ehr/elation/index.ts
  • packages/core/src/external/ehr/healthie/index.ts
  • packages/core/src/external/ehr/athenahealth/index.ts
  • packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-cloud.ts
  • packages/core/src/external/ehr/lambdas/healthie/link-patient/healthie-link-patient-factory.ts
  • packages/api/src/external/ehr/elation/shared.ts
  • packages/api/src/external/ehr/canvas/shared.ts
  • packages/core/src/external/ehr/canvas/index.ts
  • packages/api/src/external/ehr/shared/utils/client.ts
  • packages/infra/lib/ehr-nested-stack.ts
  • packages/core/src/external/ehr/shared.ts
  • packages/api/src/external/ehr/athenahealth/shared.ts
🧠 Learnings (3)
packages/lambdas/src/ehr-sync-patient.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/lambdas/sync-patient/ehr-sync-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.
packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.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 (20)
packages/api/src/routes/ehr/util.ts (1)
packages/shared/src/index.ts (1)
  • BadRequestError (39-39)
packages/api/src/external/ehr/shared/utils/appointment.ts (1)
packages/shared/src/common/date.ts (1)
  • buildDayjs (70-72)
packages/api/src/routes/internal/jwt-token/token.ts (1)
packages/api/src/command/jwt-token.ts (1)
  • getJwtTokenByIdOrFail (66-70)
packages/core/src/external/ehr/api/get-token-info.ts (5)
packages/shared/src/domain/jwt-token.ts (1)
  • JwtTokenInfo (1-5)
packages/core/src/util/config.ts (1)
  • Config (9-225)
packages/shared/src/net/retry.ts (1)
  • executeWithNetworkRetries (108-135)
packages/core/src/external/ehr/api/api-shared.ts (1)
  • validateAndLogResponse (13-20)
packages/shared/src/index.ts (1)
  • errorToString (42-42)
packages/api/src/routes/internal/ehr/index.ts (1)
packages/api/src/routes/internal/ehr/middleware.ts (1)
  • processEhrId (6-11)
packages/api/src/external/ehr/healthie/shared.ts (3)
packages/api/src/shared/config.ts (1)
  • A3E2 getHealthieEnv (344-346)
packages/api/src/external/ehr/shared/utils/client.ts (2)
  • EhrPerPracticeParams (30-30)
  • EhrEnvAndApiKey (19-22)
packages/core/src/external/ehr/healthie/index.ts (1)
  • HealthieEnv (47-47)
packages/lambdas/src/ehr-get-appointments.ts (5)
packages/lambdas/src/shared/capture.ts (1)
  • capture (18-103)
packages/lambdas/src/shared/sqs.ts (1)
  • getSingleMessageOrFail (62-83)
packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-local.ts (1)
  • EhrGetAppointmentsLocal (7-12)
packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments.ts (1)
  • GetAppointmentsRequest (6-15)
packages/lambdas/src/shared/ehr.ts (1)
  • ehrGetAppointmentsSchema (92-101)
packages/api/src/command/jwt-token.ts (2)
packages/api/src/domain/jwt-token.ts (1)
  • JwtToken (30-30)
packages/shared/src/index.ts (1)
  • NotFoundError (41-41)
packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments.ts (2)
packages/shared/src/interface/external/ehr/source.ts (1)
  • EhrSource (8-8)
packages/shared/src/index.ts (1)
  • BadRequestError (39-39)
packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-local.ts (1)
packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments.ts (3)
  • EhrGetAppointmentsHandler (17-19)
  • GetAppointmentsRequest (6-15)
  • getEhrGetAppointmentsHandler (47-56)
packages/core/src/external/ehr/canvas/command/get-appointments.ts (6)
packages/core/src/external/ehr/canvas/index.ts (1)
  • getAppointments (860-915)
packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments.ts (1)
  • GetAppointmentsClientRequest (21-21)
packages/shared/src/index.ts (1)
  • BadRequestError (39-39)
packages/core/src/external/ehr/api/get-token-info.ts (1)
  • getTokenInfo (17-39)
packages/core/src/external/ehr/api/get-client-key-and-secret.ts (1)
  • getSecrets (17-44)
packages/core/src/external/ehr/shared.ts (2)
  • GetSecretsOauthResult (43-43)
  • getSecretsOauthSchema (39-42)
packages/api/src/external/ehr/shared/utils/secrets.ts (7)
packages/api/src/external/ehr/shared/utils/client.ts (4)
  • EhrEnv (12-12)
  • EhrPerPracticeParams (30-30)
  • EhrEnvAndClientCredentials (13-17)
  • EhrEnvAndApiKey (19-22)
packages/shared/src/interface/external/ehr/source.ts (1)
  • EhrSource (8-8)
packages/api/src/external/ehr/canvas/shared.ts (1)
  • getCanvasEnv (11-37)
packages/api/src/external/ehr/athenahealth/shared.ts (1)
  • getAthenaEnv (15-29)
packages/api/src/external/ehr/elation/shared.ts (1)
  • getElationEnv (86-106)
packages/api/src/external/ehr/healthie/shared.ts (1)
  • getHealthieEnv (84-101)
packages/shared/src/index.ts (1)
  • BadRequestError (39-39)
packages/api/src/external/ehr/shared/command/secrets/get-secrets.ts (4)
packages/core/src/external/ehr/api/get-client-key-and-secret.ts (1)
  • getSecrets (17-44)
packages/api/src/external/ehr/shared/utils/client.ts (4)
  • EhrPerPracticeParams (30-30)
  • EhrEnvAndClientCredentials (13-17)
  • EhrEnv (12-12)
  • EhrEnvAndApiKey (19-22)
packages/shared/src/interface/external/ehr/source.ts (1)
  • EhrSource (8-8)
packages/api/src/external/ehr/shared/utils/secrets.ts (1)
  • getSecretsFunction (28-34)
packages/core/src/external/ehr/api/get-client-key-and-secret.ts (4)
packages/core/src/external/ehr/api/api-shared.ts (2)
  • ApiBaseParams (5-11)
  • validateAndLogResponse (13-20)
packages/core/src/util/config.ts (1)
  • Config (9-225)
packages/shared/src/net/retry.ts (1)
  • executeWithNetworkRetries (108-135)
packages/shared/src/index.ts (1)
  • errorToString (42-42)
packages/core/src/external/ehr/elation/index.ts (1)
packages/core/src/external/ehr/shared.ts (2)
  • GetSecretsOauthFunction (50-50)
  • processOauthSecrets (53-76)
packages/core/src/external/ehr/athenahealth/index.ts (1)
packages/core/src/external/ehr/shared.ts (2)
  • GetSecretsOauthFunction (50-50)
  • processOauthSecrets (53-76)
packages/api/src/external/ehr/elation/shared.ts (2)
packages/api/src/external/ehr/shared/utils/client.ts (1)
  • EhrPerPracticeParams (30-30)
packages/core/src/external/ehr/elation/index.ts (1)
  • ElationEnv (56-56)
packages/api/src/external/ehr/shared/utils/client.ts (2)
packages/core/src/external/ehr/healthie/index.ts (1)
  • HealthieEnv (47-47)
packages/api/src/command/jwt-token.ts (1)
  • findOrCreateJwtToken (11-21)
packages/core/src/external/ehr/shared.ts (3)
packages/shared/src/interface/external/ehr/source.ts (1)
  • EhrSource (8-8)
packages/core/src/external/ehr/api/get-client-key-and-secret.ts (1)
  • getSecrets (17-44)
packages/shared/src/domain/jwt-token.ts (1)
  • JwtTokenInfo (1-5)
packages/api/src/external/ehr/athenahealth/shared.ts (2)
packages/api/src/external/ehr/shared/utils/client.ts (2)
  • EhrEnvAndClientCredentials (13-17)
  • EhrPerPracticeParams (30-30)
packages/core/src/external/ehr/athenahealth/index.ts (1)
  • AthenaEnv (95-95)
⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: check-branch
  • 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)
🔇 Additional comments (95)
packages/core/src/external/ehr/lambdas/sync-patient/ehr-sync-patient-factory.ts (1)

1-1: Import path update looks good

The Config import path has been properly updated to reflect the new directory structure.

packages/core/src/external/ehr/lambdas/elation/link-patient/elation-link-patient-factory.ts (1)

1-1: Import path update looks good

The Config import path has been correctly updated to reflect the new directory structure.

packages/core/src/external/ehr/lambdas/sync-patient/ehr-sync-patient-cloud.ts (1)

3-4: Import paths correctly updated

Both import paths have been properly adjusted to reflect the new directory structure.

packages/api/src/routes/ehr/elation/appointment-webhook.ts (1)

1-1: Align import to new Lambda factory location.
The buildEhrSyncPatientHandler path correctly points to the lambdas subdirectory. Confirm that the factory export signature hasn't changed and that error handling remains consistent when invoking the new handler.

packages/api/src/routes/ehr/canvas/patient-webhook.ts (1)

1-1:

✅ Verification successful

Update sync handler import for Canvas.
Importing buildEhrSyncPatientHandler from the lambdas directory aligns with the Canvas refactor. Verify that this handler logic supports appointment retrieval for Canvas (e.g., buildEhrGetAppointmentsHandler elsewhere) and that no residual direct client calls remain.

Run this to locate the new appointments handler and ensure it's wired correctly:


🏁 Script executed:

#!/bin/bash
rg -g "*.ts" "buildEhrGetAppointmentsHandler" -L

Length of output: 815


Import Update Verified

  • Confirmed that buildEhrSyncPatientHandler is correctly imported from
    @metriport/core/external/ehr/lambdas/sync-patient/ehr-sync-patient-factory.
  • Located buildEhrGetAppointmentsHandler in
    packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-factory.ts
    and saw it wired in
    packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts.
  • No direct EHR client calls remain in
    packages/api/src/routes/ehr/canvas/patient-webhook.ts.

Everything aligns with the Canvas refactor. Approved.

packages/api/src/routes/ehr/healthie/patient-webhook.ts (1)

1-1: Consistent import path refactoring for Healthie sync handler.
The import now targets the lambdas-based sync factory. Please ensure that CI’s type-checker catches any mismatches if the factory’s API changed, and update documentation/comments if the handler signature was modified.

packages/lambdas/src/ehr-sync-patient.ts (1)

1-2:

✅ Verification successful

Adopt new lambdas module paths for local sync handler.
The ProcessSyncPatientRequest and EhrSyncPatientLocal imports have been updated to the lambdas directory. Verify that ehr-sync-patient-local.ts still exports the expected classes and run a scan to catch any lingering legacy imports:


🏁 Script executed:

#!/bin/bash
rg -g "*.ts" "@metriport/core/external/ehr/sync-patient/" --files

Length of output: 148


I suspect the initial ripgrep invocation targeted a non‐existent directory. Let’s instead scan all .ts files for the legacy import string:


🏁 Script executed:

#!/bin/bash
# Find any remaining imports from the old path
rg '@metriport/core/external/ehr/sync-patient/' -g '*.ts'

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Locate the new local sync handler file and inspect its exports
fd -e ts 'ehr-sync-patient-local.ts' --exec sed -n '1,50p' {}

Length of output: 769


🏁 Script executed:

#!/bin/bash
# Inspect ehr-sync-patient.ts in both the lambdas package (under review) and the external core package
for file in $(fd -e ts 'ehr-sync-patient.ts'); do
  echo "=== $file ==="
  sed -n '1,50p' "$file"
done

Length of output: 2985


Approved: Lambdas imports updated and no legacy references found
Verified that ehr-sync-patient-local.ts still exports EhrSyncPatientLocal and ProcessSyncPatientRequest, and a repo-wide search returned no remaining imports from the old @metriport/core/external/ehr/sync-patient/ path. All looks correct.

packages/api/src/routes/ehr/elation/patient-webhook.ts (1)

1-1: Consistent import path update for lambda-based handler
The import path has been correctly updated to reference the new lambdas directory for the EHR sync-patient factory. This aligns with the overall refactor to centralize Lambda handlers.

packages/lambdas/src/elation-link-patient.ts (1)

1-2: Align imports with new lambda module structure
The ProcessLinkPatientRequest and ElationLinkPatientLocal imports now point to the lambdas subdirectory, matching the reorganization across EHR handlers.

packages/api/src/routes/ehr/util.ts (1)

15-17: Add explicit braces for clarity
Wrapping the throw statement in {} improves readability and aligns with the project’s style of always using braces around conditional blocks, even single-line ones.

packages/core/src/external/ehr/lambdas/sync-patient/ehr-sync-patient-local.ts (1)

2-2: Update relative import to reflect directory restructure
The import for syncPatient now uses the correct relative path after moving handlers under lambdas. This keeps module resolution accurate with the new folder layout.

packages/core/src/external/ehr/lambdas/elation/link-patient/elation-link-patient-cloud.ts (1)

3-4: Correct deep relative imports for AWS utilities
The SQSClient and Config imports have been updated to the deeper relative paths under external/aws and util. This matches the refactored directory structure for cloud handlers.

packages/api/src/routes/ehr/healthie/appointment-webhook.ts (1)

1-1: Import path updated to new lambdas directory
The import for buildEhrSyncPatientHandler has been adjusted to point at @metriport/core/external/ehr/lambdas/sync-patient/.... This aligns with the reorganized folder structure—please verify that all similar EHR route handlers use the updated path to prevent resolution errors.

packages/core/src/external/ehr/lambdas/healthie/link-patient/healthie-link-patient-local.ts (1)

3-3: Corrected relative import for linkPatient
The linkPatient import path has been updated to match the relocated API module under the lambdas directory. Confirm that this reference resolves correctly and remove any stale imports if present.

packages/core/src/external/ehr/lambdas/healthie/link-patient/healthie-link-patient-factory.ts (1)

1-1: Adjusted Config import path
The Config import now traverses one additional directory level to accommodate the new lambdas layout. Ensure consistency with other factory modules and that util/config remains reachable from this location.

packages/api/src/external/ehr/athenahealth/command/process-patients-from-appointments.ts (1)

2-2: Updated sync-patient factory import
The import for buildEhrSyncPatientHandler now points to the lambdas/sync-patient path under @metriport/core. This reflects the new architecture—please double-check that all AthenaHealth and other EHR modules have received the same update.

packages/shared/src/domain/jwt-token.ts (1)

4-4: Extend JwtTokenInfo type with optional id
Introducing an optional id property enables identifying JWT tokens by their unique identifier, supporting the new getJwtTokenById API and related command functions. This change is backward-compatible and aligns with the enhanced token-management flow.

packages/lambdas/src/healthie-link-patient.ts (1)

1-2: Import paths updated correctly to match relocated lambda code structure.

The import paths now properly point to the new location under the lambdas subdirectory, which aligns with the PR objective of relocating linking and syncing lambda code into a dedicated lambdas folder.

packages/core/src/external/ehr/lambdas/healthie/link-patient/healthie-link-patient-cloud.ts (1)

3-4: Import paths correctly adjusted for new file location.

The imports have been updated with proper relative paths after moving the file into the new lambdas directory structure. This maintains the correct module resolution and adheres to the project's modular architecture.

packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts (1)

1-2: Import paths properly updated to use handlers from the new lambdas structure.

The import paths now correctly reference the handlers from the reorganized lambdas subdirectory, maintaining consistent implementation with the code refactoring across the codebase.

packages/core/src/util/config.ts (1)

215-217: New config method added for EHR appointments lambda.

The addition of getEhrGetAppointmentsLambdaName() properly supports the new lambda connector for retrieving EHR appointments, following the established pattern for config methods in this class.

packages/api/src/routes/internal/jwt-token/index.ts (2)

6-6: LGTM!

The token import and subsequent route registration in line 17 add a shared JWT token functionality to the router.


16-17: Good implementation of shared token routes

Adding the token routes at the root path is a clean way to organize shared JWT token functionality that applies across EHR types.

packages/api/src/routes/internal/ehr/index.ts (2)

8-8: LGTM!

The practice import follows the established pattern for EHR route modules.


17-17: Good implementation of practice routes

This route registration correctly uses the processEhrId middleware to validate and extract the EHR ID parameter before passing control to the practice router, following the same pattern as the existing patient routes.

packages/infra/lib/api-stack.ts (3)

424-424: LGTM!

Properly destructuring the get appointments Lambda from the EHR nested stack.


558-558: Good integration with API service

The appointments Lambda is correctly passed to the API service creation function.


663-663: Consistent environment variable configuration

This correctly adds the appointments Lambda to the list of Lambdas that need the API_URL environment variable.

packages/api/src/routes/internal/jwt-token/token.ts (1)

18-26: LGTM! Route handler implementation is clean and follows best practices.

The route handler correctly extracts the token ID from parameters with validation, retrieves the token using the appropriate command function, and returns it with the correct HTTP status code.

packages/api/src/external/ehr/healthie/shared.ts (1)

84-87: LGTM! Exporting this function supports the new Lambda architecture.

The change to export getHealthieEnv aligns with the PR objective of supporting Lambda-based appointment handling. The function provides the necessary environment and API key information for the EHR client.

packages/api/src/command/jwt-token.ts (2)

52-61: LGTM! Well-implemented token retrieval by ID function.

The getJwtTokenById function follows the established pattern in the file with proper documentation and error handling. The implementation is clean and focused.


63-70: LGTM! Fail-safe wrapper follows established pattern.

The getJwtTokenByIdOrFail function properly implements the "OrFail" pattern used throughout the codebase, with appropriate error handling and documentation.

packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts (1)

2-3: LGTM! Updated imports align with the new Lambda folder structure.

The import paths now correctly point to the new dedicated lambdas directories, supporting the PR's objective of better code organization.

packages/infra/lib/api-stack/api-service.ts (1)

115-115: Good addition of the EHR appointments Lambda infrastructure!

The changes properly integrate the new ehrGetAppointmentsLambda into the API service:

  1. Added as a parameter to the createAPIService function
  2. Correctly placed in the expected order in the parameter list
  3. Lambda name included as an environment variable for the Fargate service
  4. Task role granted permission to invoke the Lambda

This follows the established pattern for other Lambda integrations in this file.

Also applies to: 161-161, 305-305, 450-450

packages/core/src/external/ehr/api/get-token-info.ts (1)

1-39: Well-implemented token retrieval with proper error handling.

This function follows best practices:

  • Uses network retries for resilience
  • Properly transforms the API response
  • Includes contextual error information
  • Uses appropriate logging

The implementation aligns with the functional programming style preferred in the codebase.

packages/lambdas/src/shared/ehr.ts (2)

1-4: Good update to Lambda import paths.

The import paths have been properly updated to reference the new location of the lambdas code, which aligns with the PR objective of relocating the linking and syncing lambda code to a dedicated lambdas folder.


92-101: Well-structured Zod schema for appointment requests.

The schema properly validates:

  • EHR source from an enumeration
  • Required string fields (environment, method, tokenId, cxId, practiceId)
  • Optional date fields with type coercion

This provides strong type safety for the appointment Lambda handler.

packages/api/src/external/ehr/shared/command/secrets/get-secrets.ts (1)

1-26: Well-implemented secrets retrieval function

This function follows good practices by providing a clear interface for retrieving EHR secrets while delegating the implementation details to source-specific functions. The return type union correctly handles both OAuth client credentials and API key scenarios.

packages/api/src/routes/internal/ehr/practice.ts (1)

23-38: LGTM - Secure implementation of secrets retrieval endpoint

The endpoint appropriately validates all parameters before proceeding, using proper error handling for invalid inputs. The response is correctly formatted with appropriate HTTP status codes.

packages/lambdas/src/ehr-get-appointments.ts (2)

12-13: Good practice placing Sentry initialization early

Initializing capture as early as possible in the file ensures all errors are properly tracked.


41-50: Good input validation with descriptive error messages

The parseBody function properly validates the message structure and content with clear error messages at each step.

packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-local.ts (1)

7-12: Clean implementation of handler interface

The class follows a good pattern by implementing the interface and delegating to the appropriate handler based on EHR source and method. The parameter spreading keeps the code clean and maintainable.

packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts (2)

99-102: LGTM: Good use of destructuring for client creation

The updated client creation method now returns tokenId and environment alongside the client, which allows for more flexible use of these values.


108-118: LGTM: Good refactoring to use handler-based approach

The appointment fetching logic now uses a handler built by buildEhrGetAppointmentsHandler() instead of directly calling a method on the client. This improves modularity and testability by decoupling the appointment fetching logic from the client implementation.

packages/core/src/external/ehr/api/get-client-key-and-secret.ts (2)

8-8: LGTM: Good type definition

Well-defined type that properly omits fields not needed for fetching secrets.


17-44: LGTM: Well-implemented function for retrieving secrets

This function follows good practices:

  • Uses network retries for resilience
  • Proper error handling with context
  • Strong typing with Zod schema validation
  • Clear logging

The implementation is secure and robust.

packages/core/src/external/ehr/canvas/index.ts (2)

109-109: LGTM: Good addition of dynamic secrets retrieval

Adding a callback function for dynamic secrets retrieval allows for more flexible credential management.


133-142: LGTM: Improved OAuth secrets handling

The fetchTwoLeggedAuthToken method now uses processOauthSecrets to dynamically retrieve OAuth client credentials. This approach is more secure and flexible than hardcoding credentials or storing them directly in configuration.

packages/core/src/external/ehr/canvas/command/get-appointments.ts (1)

13-26: LGTM: Good parameter validation

The function properly validates required parameters and provides detailed error context.

packages/api/src/external/ehr/shared/utils/secrets.ts (4)

1-12: Well-structured imports.

The imports are nicely organized, bringing in necessary error handling, EHR source definitions, and environment-specific secrets retrieval functions for different EHR systems. The local import of client types is appropriately placed last.


14-19: Good type definitions for different secrets functions.

The type definitions create a clear contract for functions that retrieve different types of secrets:

  • OauthSecretsFunction for OAuth client credentials
  • ApiKeySecretsFunction for API key-based authentication
  • SecretsFunction as a union type, providing a unified interface

This design enables flexibility while maintaining type safety.


21-26: Good use of a lookup table for secrets functions.

The record maps each EHR source to its corresponding secret retrieval function, making it easy to extend with additional EHR sources in the future.


28-34: Well-implemented utility function with proper error handling.

The getSecretsFunction implementation appropriately checks for the existence of a secrets function and throws a meaningful error when none is found. This ensures that callers receive clear feedback when trying to fetch secrets for an unsupported EHR.

packages/core/src/external/ehr/elation/index.ts (4)

40-44: Good import additions for secret management.

Adding GetSecretsOauthFunction and processOauthSecrets imports supports the dynamic secret retrieval mechanism being implemented.


73-73: Well-typed new class property for secret retrieval.

The getSecrets property is correctly typed as an optional function, allowing the API to work both with static configuration and dynamic secret retrieval.


81-82: Good initialization of the getSecrets property.

Initializing this.getS F438 ecrets from the config ensures the class can access the dynamic secret retrieval function provided during instantiation.


94-125: Improved token retrieval with dynamic secrets management.

The refactored fetchTwoLeggedAuthToken method now uses processOauthSecrets to obtain credentials, making the authentication process more flexible. This change:

  1. Supports retrieving credentials at runtime
  2. Maintains backward compatibility with static credentials
  3. Properly handles the case when neither is available

The code correctly constructs a partial secrets object with existing credentials (if any) and passes it along with the getSecrets function to the processor.

packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments-cloud.ts (3)

1-9: Good imports with proper error handling utilities.

The imports bring in network retry functionality, configuration management, and Lambda client utilities needed for cloud-based appointment retrieval.


11-20: Well-designed class with proper dependency injection.

The EhrGetAppointmentsCloud class:

  1. Implements the required handler interface
  2. Accepts the Lambda function name as a required parameter
  3. Supports optional region and Lambda client parameters
  4. Initializes the Lambda client appropriately when not provided

This design follows good dependency injection principles, making the class more testable.


22-45: Robust implementation of the getAppointments method.

The getAppointments method:

  1. Logs operation details
  2. Properly serializes the request parameters
  3. Uses network retries for resilience
  4. Handles error cases in the Lambda response
  5. Returns an empty array as a fallback
  6. Correctly parses and returns the result

The implementation is resilient to various failure modes while maintaining type safety.

packages/core/src/external/ehr/athenahealth/index.ts (4)

75-79: Good import additions for secret management.

Adding GetSecretsOauthFunction and processOauthSecrets imports prepares the class for dynamic secret retrieval.


151-151: Well-typed new class property for secret retrieval.

The getSecrets property is correctly typed as an optional function, allowing the API to work with both static and dynamic credential retrieval.


160-161: Good initialization of the getSecrets property.

Initializing this.getSecrets from the config ensures the class can access the dynamic secret retrieval function provided during instantiation.


173-207: Improved token retrieval with dynamic secrets management.

The refactored fetchTwoLeggedAuthToken method now uses processOauthSecrets to obtain credentials dynamically. This change:

  1. Supports retrieving credentials at runtime
  2. Maintains backward compatibility with static credentials
  3. Properly handles credential absence
  4. Uses the retrieved credentials correctly in the auth header

This implementation ensures secure and flexible credential management for AthenaHealth API authentication.

packages/api/src/external/ehr/elation/shared.ts (3)

86-106: Export of environment retrieval function is a good enhancement

This is a positive change that makes the getElationEnv function accessible to other modules, which is necessary for the new Lambda-based appointment retrieval architecture being implemented in this PR.


124-133: Function return type enhancement improves flexibility

Good refactoring to return additional metadata (tokenId and environment) alongside the client. This enriched return value supports the new appointment fetching Lambda functionality while maintaining the core functionality.


135-140: Backward compatibility wrapper preserves API contract

The new function preserves the original interface by delegating to the enriched function and returning only the client. This follows good API design principles by minimizing impact on existing code while enabling new functionality.

packages/api/src/external/ehr/canvas/shared.ts (3)

3-3: Improved token source management through centralization

Importing canvasClientSource from a shared module rather than defining constants locally improves maintainability by centralizing token source definitions.


39-48: Enhanced client creation function with metadata

The enriched return type now includes tokenId and environment alongside the Canvas client, enabling more flexible API interactions for the new appointment lambda architecture.


50-55: Backward compatibility is correctly maintained

This function preserves the original interface by extracting just the client part from the enriched function, minimizing impact on existing code while supporting new appointment fetching functionality.

packages/api/src/external/ehr/athenahealth/shared.ts (3)

15-29: Export of environment getter improves module accessibility

Making getAthenaEnv exported allows other modules to access environment configuration, aligning with similar changes in other EHR modules and supporting the new architecture.


31-40: Enhanced client creation with metadata supports Lambda architecture

The function now returns additional context (tokenId and environment) alongside the client, following the same pattern used in other EHR integrations to support the new appointment fetching infrastructure.


42-47: Backward compatibility function maintains API contract

This wrapper function maintains the original API contract by delegating to the new function and returning only the client, which is a good practice for maintaining compatibility with existing code.

packages/infra/lib/ehr-nested-stack.ts (5)

22-22: Well-defined type for Lambda settings

Good refactoring to create a specific type for Lambda settings using TypeScript's Pick utility type, making the code more maintainable and type-safe.


32-40: Appropriate Lambda configuration for appointment retrieval

The configuration for the new appointment retrieval Lambda function is well-defined with a 12-minute timeout and 4GB memory allocation, which should be sufficient for the expected workload.


171-171: Clean addition of Lambda property to stack class

Adding a readonly property for the new Lambda function to the EhrNestedStack class follows good object-oriented design principles by clearly exposing the resource to clients of the class.


189-195: Proper Lambda initialization in constructor

The initialization call in the constructor follows the established pattern in this class, maintaining consistency with the rest of the codebase.


266-292: Well-structured Lambda setup method

The implementation follows the same pattern as other Lambda setup methods in the class, with appropriate configuration for environment variables, layers, VPC settings, and alarms. Unlike other Lambdas in this stack, this one doesn't create an SQS queue or event source, which is appropriate for its intended use case.

packages/api/src/external/ehr/shared/utils/client.ts (6)

4-4: Import from Healthie module looks good.

This import is necessary to add support for the Healthie EHR environment type.


12-12: EHR environment type correctly extended.

Adding HealthieEnv to the EhrEnv union type properly extends the system to support the new Healthie EHR source.


24-24: Type rename improves clarity.

Renaming from EhrClient to EhrOauthClient more accurately reflects that these are OAuth-based clients, which is appropriate since the Healthie client uses API key authentication instead of OAuth.


46-47: Token ID added to support Lambda functions.

Adding the token ID to the returned information provides necessary context for the new Lambda-based architecture where tokens need to be referenced by ID.


69-69: Enhanced return type provides better context.

The expanded return type including tokenId and environment provides richer information to consumers of this function, which is needed for the Lambda-based appointment retrieval system.


82-88: Token persistence and return structure look good.

Using findOrCreateJwtToken appropriately persists the token information. The return structure provides all necessary data for downstream consumers.

Note: The environment.environment access pattern suggests a nested environment property. While this works, it could be simplified in a future refactoring if appropriate.

packages/core/src/external/ehr/lambdas/appoinment/get-appoinemtns/ehr-get-appointments.ts (3)

1-19: Well-structured imports and type definitions.

The imports and type definitions provide a clear foundation for the appointment retrieval system. The GetAppointmentsRequest includes all necessary fields, and the EhrGetAppointmentsHandler interface defines a clean contract with generic typing.


27-45: Extensible mapping structure for EHR appointment handlers.

The map structure provides a clear framework for associating EHR sources and methods with their handler implementations. This architecture allows for easy addition of new sources and methods in the future.


47-56: Well-implemented handler retrieval function.

The getEhrGetAppointmentsHandler function provides a type-safe way to retrieve the appropriate handler for a given EHR source and method. The error handling is appropriate, providing context about the requested handler in the error message.

packages/core/src/external/ehr/healthie/index.ts (4)

27-33: Good utility imports to support dynamic secret retrieval.

The updated imports bring in necessary utilities for implementing the dynamic secret retrieval pattern, consistent with other EHR implementations.


37-41: Interface properly updated for dynamic secrets.

The HealthieApiConfig interface is correctly extended to support optional API key with dynamic retrieval via a getSecrets function. This aligns with the PR objective of retrieving secrets per practice to support Lambda operations.


56-56: Class correctly stores secret retrieval function.

Adding the getSecrets field and initializing it in the constructor provides the necessary state for dynamic secret retrieval.

Also applies to: 64-64


74-86: Robust initialization with dynamic secret retrieval.

The updated initialize method correctly implements the dynamic secret retrieval pattern:

  1. First checks if API key is directly provided
  2. If not, verifies a getSecrets function exists
  3. Throws a clear error if neither is available
  4. Otherwise, uses getSecrets to dynamically retrieve the API key

This implementation is robust and provides clear error messaging.

packages/core/src/external/ehr/shared.ts (4)

39-48: Well-defined schemas for secret validation.

Using Zod schemas for OAuth and API key secrets provides runtime validation and type safety, ensuring that secret data structures are consistent and complete throughout the system.


50-51: Clear function types for secret retrieval.

These function types establish a consistent contract for secret retrieval functions, making the system more maintainable and extensible.


53-76: Robust OAuth secret processing function.

The processOauthSecrets function implements a robust pattern for handling OAuth credentials:

  1. Use provided credentials if available
  2. Fall back to dynamic retrieval if a getSecrets function exists
  3. Provide clear error messages when requirements aren't met

This centralizes the logic and reduces duplication across different EHR client implementations.


81-83: Interface properly updated for dynamic secret retrieval.

Making clientKey and clientSecret optional and adding the getSecrets function to ApiConfig reflects the new pattern for credential management, supporting both direct provision and dynamic retrieval.

Thomas Yopes added 2 commits May 20, 2025 12:49
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
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

🧹 Nitpick comments (3)
packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments-local.ts (1)

7-12: Implementation looks good, but consider optimization for future scalability

The EhrGetAppointmentsLocal class follows a good implementation pattern for the interface. It correctly destructures the request and delegates to the appropriate handler.

For future scalability, consider adding error logging specifically for this local implementation. When more EHR sources are added, having detailed logs here will help with troubleshooting:

  async getAppointments<T>({ ehr, method, ...params }: GetAppointmentsRequest): Promise<T[]> {
+   const { log } = out(`EhrGetAppointmentsLocal - ehr ${ehr}, method ${method}`);
    const handler = getEhrGetAppointmentsHandler<T>(ehr, method);
+   log(`Handler found, executing with params`);
    return await handler({ ...params });
  }
packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments.ts (2)

27-36: Consider a more maintainable type definition approach

The current GetAppointmentsMap type definition will require manual updates each time a new EHR source is added.

Consider using a more dynamic type generation approach:

- export type GetAppointmentsMap = {
-   [EhrSources.athena]: undefined;
-   [EhrSources.elation]: undefined;
-   [EhrSources.healthie]: undefined;
-   [EhrSources.canvas]: {
-     [AppointmentMethods.canvasGetAppointments]: (
-       params: GetAppointmentsClientRequest
-     ) => Promise<SlimBookedAppointment[]>;
-   };
- };
+ export type EhrMethodHandler = (params: GetAppointmentsClientRequest) => Promise<unknown[]>;
+ 
+ export type EhrMethodsMap = {
+   [method in AppointmentMethods]?: EhrMethodHandler;
+ };
+ 
+ export type GetAppointmentsMap = {
+   [source in EhrSource]?: EhrMethodsMap;
+ };

This approach will be more scalable as you add more EHR sources and methods in the future, without requiring repetitive type definitions.


47-56: Handler lookup function is well-implemented with proper error handling

The getEhrGetAppointmentsHandler function correctly handles the lookup and error case.

Consider adding a type guard to improve the explicitness of the type casting:

export function getEhrGetAppointmentsHandler<T>(
  ehr: EhrSource,
  method: AppointmentMethods
): (params: GetAppointmentsClientRequest) => Promise<T[]> {
  const handler = ehrGetAppointmentsMap[ehr]?.[method];
  if (!handler) {
    throw new BadRequestError(`No get appointments handler found`, undefined, { ehr, method });
  }
-  return handler as (params: GetAppointmentsClientRequest) => Promise<T[]>;
+  return ((params: GetAppointmentsClientRequest) => {
+    return handler(params) as Promise<T[]>;
+  });
}

This makes the type conversion more explicit and controlled, though the current implementation is also valid.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between 358826d and 83d31e8.

📒 Files selected for processing (10)
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts (3 hunks)
  • packages/api/src/routes/internal/jwt-token/token.ts (1 hunks)
  • packages/core/src/external/aws/lambda.ts (1 hunks)
  • packages/core/src/external/ehr/canvas/command/get-appointments.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments-factory.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments-local.ts (1 hunks)
  • packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments.ts (1 hunks)
  • packages/lambdas/src/ehr-get-appointments.ts (1 hunks)
  • packages/lambdas/src/shared/ehr.ts (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments-factory.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts
  • packages/core/src/external/aws/lambda.ts
  • packages/api/src/routes/internal/jwt-token/token.ts
  • packages/lambdas/src/shared/ehr.ts
  • packages/core/src/external/ehr/canvas/command/get-appointments.ts
  • packages/lambdas/src/ehr-get-appointments.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/lambdas/appointment/get-appointments/ehr-get-appointments-cloud.ts
  • packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments-local.ts
  • packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments.ts
🧬 Code Graph Analysis (2)
packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments-cloud.ts (3)
packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments.ts (2)
  • EhrGetAppointmentsHandler (17-19)
  • GetAppointmentsRequest (6-15)
packages/core/src/external/aws/lambda.ts (3)
  • LambdaClient (7-7)
  • makeLambdaClient (13-19)
  • defaultLambdaInvocationResponseHandler (26-35)
packages/shared/src/net/retry.ts (1)
  • executeWithNetworkRetries (108-135)
packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments.ts (3)
packages/shared/src/interface/external/ehr/source.ts (1)
  • EhrSource (8-8)
packages/lambdas/src/ehr-get-appointments.ts (1)
  • handler (19-39)
packages/shared/src/index.ts (1)
  • BadRequestError (39-39)
🔇 Additional comments (2)
packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments-cloud.ts (1)

11-20: Constructor implementation follows best practices

The constructor correctly initializes the Lambda client with either the provided client or creates a new one with appropriate region configuration.

packages/core/src/external/ehr/lambdas/appointment/get-appointments/ehr-get-appointments.ts (1)

6-21: Type definitions look good with clear interface contract

The request type definitions and handler interface are well-structured with appropriate typing.

Thomas Yopes added 2 commits May 20, 2025 13:06
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
@thomasyopes
Copy link
Contributor Author
thomasyopes commented May 28, 2025
  • PR description: update test case for staging and prod
  • core's ./ehr/lambdas folder: why call it lambda? can we call it "command" or something like that? It's not lambda code and it doesn't only run on lambdas... even on the cloud, it could run in other containers/tech.

Done for core. For testing, I can't run the "create token on the fly" code as it's setup right now to always generate a token on the API first before calling the new lambda. This will start getting used once we refresh the bundles in "real-time" from the refresh lambda (downstream PR).

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

🧹 Nitpick comments (2)
packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments-cloud.ts (1)

41-42: Consider type safety for JSON parsing.

The JSON.parse operation could throw if the Lambda returns invalid JSON. Consider adding error handling or type validation.

-      if (!result) return [];
-      return JSON.parse(result);
+      if (!result) return [];
+      try {
+        return JSON.parse(result);
+      } catch (error) {
+        throw new Error(`Failed to parse Lambda response`, { cause: error });
+      }
packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments-direct.ts (1)

57-57: Verify type casting safety.

The type assertion as (params: GetAppointmentsClientRequest) => Promise<T[]> assumes the handler matches the expected signature. While this should be safe due to the type-safe map design, consider if additional runtime validation would be beneficial.

The current implementation should be safe due to the strongly-typed map, but if you want extra safety:

-  return handler as (params: GetAppointmentsClientRequest) => Promise<T[]>;
+  // Type assertion is safe here due to strongly-typed map design
+  return handler as (params: GetAppointmentsClientRequest) => Promise<T[]>;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0d1262 and e429565.

📒 Files selected for processing (22)
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts (3 hunks)
  • packages/api/src/external/ehr/shared/utils/client.ts (4 hunks)
  • packages/api/src/external/ehr/shared/utils/secrets.ts (1 hunks)
  • packages/api/src/routes/internal/jwt-token/index.ts (1 hunks)
  • packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments-direct.ts (1 hunks)
  • packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments-factory.ts (1 hunks)
  • packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments.ts (1 hunks)
  • packages/core/src/external/ehr/command/elation/link-patient/elation-link-patient-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/command/elation/link-patient/elation-link-patient-factory.ts (1 hunks)
  • packages/core/src/external/ehr/command/elation/link-patient/elation-link-patient-local.ts (1 hunks)
  • packages/core/src/external/ehr/command/healthie/link-patient/healthie-link-patient-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/command/healthie/link-patient/healthie-link-patient-factory.ts (1 hunks)
  • packages/core/src/external/ehr/command/healthie/link-patient/healthie-link-patient-local.ts (1 hunks)
  • packages/core/src/external/ehr/command/sync-patient/ehr-sync-patient-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/command/sync-patient/ehr-sync-patient-factory.ts (1 hunks)
  • packages/core/src/external/ehr/command/sync-patient/ehr-sync-patient-local.ts (1 hunks)
  • packages/lambdas/src/ehr-get-appointments.ts (1 hunks)
  • packages/lambdas/src/ehr-sync-patient.ts (1 hunks)
  • packages/lambdas/src/elation-link-patient.ts (1 hunks)
  • packages/lambdas/src/healthie-link-patient.ts (1 hunks)
  • packages/lambdas/src/shared/ehr.ts (2 hunks)
✅ Files skipped from review due to trivial changes (10)
  • packages/core/src/external/ehr/command/sync-patient/ehr-sync-patient-local.ts
  • packages/core/src/external/ehr/command/healthie/link-patient/healthie-link-patient-cloud.ts
  • packages/core/src/external/ehr/command/sync-patient/ehr-sync-patient-cloud.ts
  • packages/core/src/external/ehr/command/healthie/link-patient/healthie-link-patient-local.ts
  • packages/core/src/external/ehr/command/healthie/link-patient/healthie-link-patient-factory.ts
  • packages/core/src/external/ehr/command/elation/link-patient/elation-link-patient-factory.ts
  • packages/core/src/external/ehr/command/elation/link-patient/elation-link-patient-local.ts
  • packages/core/src/external/ehr/command/sync-patient/ehr-sync-patient-factory.ts
  • packages/core/src/external/ehr/command/elation/link-patient/elation-link-patient-cloud.ts
  • packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/lambdas/src/healthie-link-patient.ts
  • packages/api/src/routes/internal/jwt-token/index.ts
  • packages/lambdas/src/elation-link-patient.ts
  • packages/lambdas/src/ehr-sync-patient.ts
  • packages/lambdas/src/ehr-get-appointments.ts
  • packages/lambdas/src/shared/ehr.ts
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts
  • packages/api/src/external/ehr/shared/utils/client.ts
  • packages/api/src/external/ehr/shared/utils/secrets.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/command/appointment/get-appointments/ehr-get-appointments-factory.ts
  • packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments-cloud.ts
  • packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments-direct.ts
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • 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)
🔇 Additional comments (7)
packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments-factory.ts (2)

6-12: LGTM! Clean factory pattern implementation.

The factory function correctly implements environment-based handler selection with clear separation between development and production environments. The implementation follows the coding guidelines with proper camelCase naming and functional programming principles.


10-11:

✅ Verification successful

Verify Config.getEhrGetAppointmentsLambdaName() error handling.

Ensure that Config.getEhrGetAppointmentsLambdaName() properly handles cases where the Lambda name is not configured, as this could cause runtime failures in production.


🏁 Script executed:

#!/bin/bash
# Description: Check how Config.getEhrGetAppointmentsLambdaName() handles missing configuration
# Expected: Should find error handling or default value logic

ast-grep --pattern 'getEhrGetAppointmentsLambdaName() {
  $$$
}'

Length of output: 66


🏁 Script executed:

#!/bin/bash
# Locate the definition and usage of getEhrGetAppointmentsLambdaName in the codebase
rg -n "getEhrGetAppointmentsLambdaName" -C3 .

Length of output: 1443


🏁 Script executed:

#!/bin/bash
# Inspect the implementation of getEnvVarOrFail to confirm its error handling behavior
rg -n "getEnvVarOrFail" -C5 packages/core/src/util/config.ts

Length of output: 6893


🏁 Script executed:

#!/bin/bash
# Inspect getEnvVarOrFail implementation for error handling behavior
rg -n "getEnvVarOrFail" -C5 packages/core/src/util/env-var.ts

Length of output: 877


No action needed: Config.getEhrGetAppointmentsLambdaName already fails fast on missing config
The factory method delegates to getEnvVarOrFail("EHR_GET_APPOINTMENTS_LAMBDA_NAME"), which throws a clear error if the env var is undefined or empty. This ensures the Lambda name is always present at runtime.

packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments-cloud.ts (2)

14-20: Well-designed constructor with dependency injection.

The constructor properly handles optional parameters with sensible defaults. The dependency injection pattern allows for testing while maintaining clean defaults for production use.


22-44: Robust Lambda invocation with proper error handling.

The implementation correctly uses:

  • Network retries for resilience
  • Synchronous Lambda invocation
  • Proper logging with customer context
  • Default response handler for error processing
  • Graceful handling of empty results

The async/await pattern and error handling follow the coding guidelines.

packages/core/src/external/ehr/command/appointment/get-appointments/ehr-get-appointments-direct.ts (3)

8-14: Clean implementation with proper validation.

The method correctly:

  • Validates appointment methods using type guards
  • Delegates to appropriate handlers
  • Uses destructuring for clean parameter handling
  • Follows functional programming principles

19-25: Well-designed enum and validation pattern.

The enum follows PascalCase naming for the enum itself and camelCase for values. The type guard function properly validates method strings against the enum values.


27-47: Excellent type-safe handler mapping design.

The handler map provides:

  • Complete coverage of all EHR sources
  • Type safety with specific method signatures
  • Clear indication of unsupported sources with undefined
  • Proper typing for Canvas implementation

This design makes it easy to add new handlers while maintaining type safety.

Thomas Yopes added 2 commits May 28, 2025 11:21
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
< 10000 input type="hidden" name="disable_live_updates" value="false" autocomplete="off" data-targets="batch-deferred-content.inputs" />
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
@thomasyopes thomasyopes requested a review from leite08 May 28, 2025 18:28
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Thomas Yopes added 4 commits May 29, 2025 05:28
… 199-elation-contribution-1.5

Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
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

🔭 Outside diff range comments (1)
packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-local.ts (1)

88-90: 🛠️ Refactor suggestion

Fix multi-line logging to comply with coding guidelines.

The current logging approach violates the coding guideline that states "Avoid multi-line logs". The log message spans multiple lines which should be consolidated.

Apply this diff to fix the logging:

-        out(
-          `computeResourceDiffBundles - metriportPatientId ${metriportPatientId} ehrPatientId ${ehrPatientId} resourceType ${resourceType}`
-        ).log(`Error creating metriport and ehr bundles. Cause: ${errorToString(error)}`);
+        out().log(
+          `computeResourceDiffBundles - metriportPatientId ${metriportPatientId} ehrPatientId ${ehrPatientId} resourceType ${resourceType} - Error creating metriport and ehr bundles. Cause: ${errorToString(error)}`
+        );
🧹 Nitpick comments (1)
packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-direct.ts (1)

12-18: Consider type safety improvement for handler result casting.

The type casting as T[] on line 17 assumes the handler always returns compatible types. While this may work with current handlers, it could become unsafe if handlers return incompatible appointment types in the future.

Consider adding runtime validation or using more specific typing to ensure type safety:

 async getAppointments<T extends Appointment>({
   method,
   ...params
 }: GetAppointmentsRequest): Promise<T[]> {
   const handler = getEhrGetAppointmentsHandler(method);
-  return (await handler({ ...params })) as T[];
+  const result = await handler({ ...params });
+  // Consider adding runtime validation here if needed
+  return result as T[];
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae3ef70 and 9df376e.

📒 Files selected for processing (26)
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts (3 hunks)
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts (1 hunks)
  • packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts (3 hunks)
  • packages/api/src/external/ehr/healthie/shared.ts (2 hunks)
  • packages/core/src/external/ehr/canvas/command/get-appointments.ts (1 hunks)
  • packages/core/src/external/ehr/canvas/shared.ts (1 hunks)
  • packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-direct.ts (1 hunks)
  • packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-factory.ts (1 hunks)
  • packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments.ts (1 hunks)
  • packages/core/src/external/ehr/elation/command/link-patient/elation-link-patient-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/elation/command/link-patient/elation-link-patient-factory.ts (1 hunks)
  • packages/core/src/external/ehr/elation/command/link-patient/elation-link-patient-local.ts (1 hunks)
  • packages/core/src/external/ehr/healthie/command/link-patient/healthie-link-patient-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/healthie/command/link-patient/healthie-link-patient-factory.ts (1 hunks)
  • packages/core/src/external/ehr/healthie/command/link-patient/healthie-link-patient-local.ts (1 hunks)
  • packages/core/src/external/ehr/healthie/index.ts (1 hunks)
  • packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-factory.ts (1 hunks)
  • packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-local.ts (1 hunks)
  • packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/refresh/ehr-refresh-ehr-bundles-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/refresh/ehr-refresh-ehr-bundles-factory.ts (1 hunks)
  • packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/refresh/ehr-refresh-ehr-bundles-local.ts (1 hunks)
  • packages/core/src/external/ehr/shared.ts (3 hunks)
  • packages/lambdas/src/ehr-get-appointments.ts (1 hunks)
  • packages/lambdas/src/shared/ehr.ts (2 hunks)
✅ Files skipped from review due to trivial changes (13)
  • packages/core/src/external/ehr/elation/command/link-patient/elation-link-patient-factory.ts
  • packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/refresh/ehr-refresh-ehr-bundles-cloud.ts
  • packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/refresh/ehr-refresh-ehr-bundles-factory.ts
  • packages/core/src/external/ehr/healthie/command/link-patient/healthie-link-patient-local.ts
  • packages/core/src/external/ehr/healthie/command/link-patient/healthie-link-patient-factory.ts
  • packages/core/src/external/ehr/healthie/command/link-patient/healthie-link-patient-cloud.ts
  • packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-factory.ts
  • packages/core/src/external/ehr/elation/command/link-patient/elation-link-patient-cloud.ts
  • packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-cloud.ts
  • packages/core/src/external/ehr/elation/command/link-patient/elation-link-patient-local.ts
  • packages/core/src/external/ehr/job/create-resource-diff-bundles/steps/refresh/ehr-refresh-ehr-bundles-local.ts
  • packages/api/src/external/ehr/elation/command/process-patients-from-appointments.ts
  • packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/api/src/external/ehr/healthie/command/process-patients-from-appointments.ts
  • packages/core/src/external/ehr/healthie/index.ts
  • packages/api/src/external/ehr/canvas/command/process-patients-from-appointments.ts
  • packages/core/src/external/ehr/canvas/command/get-appointments.ts
  • packages/lambdas/src/ehr-get-appointments.ts
  • packages/core/src/external/ehr/shared.ts
  • packages/lambdas/src/shared/ehr.ts
  • packages/api/src/external/ehr/healthie/shared.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/job/create-resource-diff-bundles/steps/compute/ehr-compute-resource-diff-bundles-local.ts
  • packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-factory.ts
  • packages/core/src/external/ehr/canvas/shared.ts
  • packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-cloud.ts
  • packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-direct.ts
🧬 Code Graph Analysis (3)
packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-factory.ts (3)
packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments.ts (1)
  • EhrGetAppointmentsHandler (15-17)
packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-direct.ts (1)
  • EhrGetAppointmentsDirect (11-19)
packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-cloud.ts (1)
  • EhrGetAppointmentsCloud (11-45)
packages/core/src/external/ehr/canvas/shared.ts (2)
packages/core/src/external/ehr/shared.ts (1)
  • getSecretsOauthSchema (38-42)
packages/core/src/external/ehr/api/get-token-info.ts (1)
  • getTokenInfo (17-39)
packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-direct.ts (3)
packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments.ts (4)
  • EhrGetAppointmentsHandler (15-17)
  • Appointment (19-19)
  • GetAppointmentsRequest (3-11)
  • GetAppointmentsClientRequest (13-13)
packages/lambdas/src/ehr-get-appointments.ts (1)
  • handler (18-38)
packages/shared/src/index.ts (1)
  • BadRequestError (40-40)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: check-pr / lint-build-test
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
packages/core/src/external/ehr/canvas/shared.ts (1)

7-30: LGTM! Clean implementation with good parameter destructuring.

The function correctly implements the Canvas client creation pattern:

  • Proper parameter destructuring following coding guidelines
  • Appropriate conditional token info fetching
  • Clean integration with the secrets and token info APIs

The error handling is appropriately delegated to the underlying getSecrets and getTokenInfo functions, which will bubble up any failures with proper context.

packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-cloud.ts (1)

11-20: LGTM! Good use of dependency injection pattern.

The constructor properly implements dependency injection for the Lambda client with sensible defaults:

  • Uses provided client or creates one with appropriate region configuration
  • Follows the coding guidelines for constructor parameter handling
  • Maintains good separation of concerns
packages/core/src/external/ehr/command/get-appointments/ehr-get-appointments-direct.ts (1)

25-35: LGTM! Excellent extensible mapping pattern with proper error handling.

The implementation provides:

  • Clean mapping from appointment methods to handlers
  • Proper error handling with contextual information
  • Extensible design for adding new appointment methods
  • Good use of BadRequestError with method context for debugging

This pattern makes it easy to add support for additional EHR systems in the future.

Thomas Yopes added 4 commits May 29, 2025 09:21
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
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: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 46d60a2 and da5f774.

📒 Files selected for processing (4)
  • packages/lambdas/src/ehr-compute-resource-diff-bundles.ts (1 hunks)
  • packages/lambdas/src/ehr-refresh-ehr-bundles.ts (1 hunks)
  • packages/lambdas/src/elation-link-patient.ts (1 hunks)
  • packages/lambdas/src/healthie-link-patient.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/lambdas/src/ehr-refresh-ehr-bundles.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/lambdas/src/elation-link-patient.ts
  • packages/lambdas/src/healthie-link-patient.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/lambdas/src/ehr-compute-resource-diff-bundles.ts
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • 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)

Thomas Yopes added 10 commits May 29, 2025 09:49
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
… 199-elation-contribution-1.5

Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
… 199-elation-contribution-1.5

Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: ENG-199

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Comment on lines +33 to +44
const result = await this.lambdaClient
.invoke({
FunctionName: this.ehrGetAppointmentsLambdaName,
InvocationType: "RequestResponse",
Payload: payload,
})
.promise()
.then(
defaultLambdaInvocationResponseHandler({
lambdaName: this.ehrGetAppointmentsLambdaName,
})
);
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't mix promise chaining with await-based code. I added an item in the code standards about this (should have it there a long time ago! 😄)

Comment on lines +25 to +27
const ehrGetAppointmentsMap: AppointmentMethodsMap = {
[AppointmentMethods.canvasGetAppointments]: getAppointmentsCanvas,
};
Copy link
Member

Choose a reason for hiding this comment

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

This is not ideal, as we're adding EHR-specific information in the global EHR code.

Not a blocker for now, but I'd create an issue to map this and add a TODO here.

Example of solution: could have a "setup" step/function so that each EHR implementation is required to pass certain functions to that code, and that effectively initializes the EHR global code so it can direct the execution flow to said functions based on the context/parameters of the execution.

Comment on lines +21 to +23
export enum AppointmentMethods {
canvasGetAppointments = "canvasGetAppointments",
}
Copy link
Member

Choose a reason for hiding this comment

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

I thought I added a comment about this on the previous PR. The problem with this type of code is that there's no compilation-time assessment that we're providing the expected parameters.

My suggestion is to do something like this:

image image image

Copy link
Member

Choose a reason for hiding this comment

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

Can't we move this to core and have the respective env functions there as well? None require DB access, so that's one less hit to the API and we keep the EHR code mode concentrated in core.

syncPatient: QueueAndLambdaSettings;
elationLinkPatient: QueueAndLambdaSettings;
healthieLinkPatient: QueueAndLambdaSettings;
computeResourceDiffBundles: QueueAndLambdaSettings;
refreshEhrBundles: QueueAndLambdaSettings;
} {
const getAppointmentsLambdaTimeout = Duration.minutes(12);
Copy link
Member

Choose a reason for hiding this comment

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

OOC, why 12 and not 15min? Super close to the limit, might as well give it more time so it doesn't fail after 12 min?

name: "EhrGetAppointments",
entry: "ehr-get-appointments",
lambda: {
memory: 4096,
Copy link
Member

Choose a reason for hiding this comment

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

Needs that much RAM? No worries if it does, just wondering about the thought process, as this impact cost.

Copy link
Member

Choose a reason for hiding this comment

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

I thought we had an example, but it's just a lambda's CSS. Could try w/ branch to staging, this would set an important improvement for code organization.

I thought I tried this but I don't remember now. This seem promising.

// Automatically set by AWS
const lambdaName = getEnvOrFail("AWS_LAMBDA_FUNCTION_NAME");

// TODO move to capture.wrapHandler()
Copy link
Member

Choose a reason for hiding this comment

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

remove left-over

const message = getSingleMessageOrFail(event.Records, lambdaName);
if (!message) return;

console.log(`Running with unparsed body: ${message.body}`);
Copy link
Member

Choose a reason for hiding this comment

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

need to log the body?

const { method, cxId, practiceId } = parsedBody;

const log = prefixedLog(`method ${method}, cxId ${cxId}, practiceId ${practiceId}`);
log(`Parsed: ${JSON.stringify(parsedBody)}`);
Copy link
Member

Choose a reason for hiding this comment

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

ditto

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