Skip to content

Conversation

thomasyopes
Copy link
Contributor

@thomasyopes thomasyopes commented Apr 3, 2025

Ref: #2850

Dependencies

Description

  • adding new bundle layer for Canvas that introduce: EHR total and Metriport only bundle
  • adding refresh bundle workflow for refreshing the EHR total bundle
  • adding resource diff workflow for generating the Metriport only bundle
  • adding Canvas client methods for fetching FHIR resource from the EHR total bundle (or not) -- this method will update the existing EHR total bundle if it goes directly to Canvas
  • adding Canvas client methods for fetching the Metriport only bundle

Testing

  • Local
    • running resource diff works
    • running bundle refresh works
    • get endpoint works
  • Staging
    • running resource diff works
    • running bundle refresh works
    • get endpoint works
  • Sandbox
    • N/A
  • Production
    • running resource diff works
    • running bundle refresh works
    • get endpoint works

Release Plan

  • Merge this

Summary by CodeRabbit

  • New Features

    • Introduced APIs and backend infrastructure for generating, retrieving, and refreshing resource difference bundles between Metriport and Canvas EHR systems.
    • Added endpoints for asynchronous initiation and retrieval of resource diff bundles, supporting direction-based queries (Metriport-only or Canvas-only).
    • Enabled fetching, caching, and validation of FHIR resource bundles by resource type, with support for S3 storage and TTL-based cache management.
    • Provided utilities for deduplication and management of FHIR resources within bundles.
  • Infrastructure

    • Deployed new AWS Lambda functions, SQS queues, and S3 buckets to support resource diff and bundle refresh workflows.
    • Updated configuration and permissions to accommodate new storage and messaging resources.
  • Bug Fixes

    • Improved validation and error handling for resource type support and bundle operations.
  • Chores

    • Removed deprecated endpoints related to appointment-based patient processing for Athenahealth and Elation integrations.
  • Documentation

    • Enhanced API documentation for new and existing endpoints, including query parameters and expected behaviors.

Ref: #2850
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Copy link

coderabbitai bot commented Apr 3, 2025

Walkthrough

This update introduces a comprehensive infrastructure for managing, diffing, and refreshing FHIR resource bundles between Metriport and Canvas EHR systems. It adds new API endpoints and internal routes for initiating and retrieving resource diff bundles, as well as for refreshing and fetching EHR bundles by resource type. The core logic includes robust S3 key management, resource deduplication, and caching mechanisms, with new utility functions and type definitions for FHIR resources. Cloud infrastructure is expanded with new Lambda functions, SQS queues, and an S3 bucket for bundle storage, all integrated into the deployment stack and configuration. Error handling and logging are streamlined, and new factories and handlers are introduced for both local and cloud-based processing of resource diff and bundle refresh workflows.

Changes

File(s) / Path(s) Change Summary
packages/api/src/routes/ehr/canvas/patient.ts Added POST and GET routes for /ehr/canvas/patient/:id/resource-diff-bundle to initiate and retrieve resource diff bundles; updated documentation for query parameters.
packages/api/src/routes/internal/ehr/canvas/patient.ts Added endpoints for refreshing and fetching Canvas EHR bundles by resource type; updated parameter documentation.
packages/api/src/routes/internal/ehr/athenahealth/patient.ts,
packages/api/src/routes/internal/ehr/elation/patient.ts
Removed POST route handlers for appointment-based patient processing for Athenahealth and Elation.
packages/api/src/external/ehr/canvas/command/bundle/create-resource-diff-bundles.ts Added function to initiate resource diff bundle creation for Canvas patients.
packages/api/src/external/ehr/canvas/command/bundle/fetch-ehr-bundle.ts Added function to fetch Canvas patient bundles by resource type, supporting caching.
packages/api/src/external/ehr/canvas/command/bundle/fetch-resource-diff-bundle.ts Added function to fetch precomputed resource diff bundles for Canvas patients.
packages/api/src/external/ehr/canvas/command/bundle/refresh-ehr-bundles.ts Added function to trigger refresh of cached Canvas bundles for a patient.
packages/core/src/external/ehr/canvas/index.ts Added methods for fetching, validating, caching, and retrieving FHIR bundles by resource type; defined supported resource types and type guards; updated medication search return type.
packages/core/src/external/ehr/api/api-shared.ts Introduced ApiBaseParams type for EHR API parameter standardization.
packages/core/src/external/ehr/api/elation/link-patient.ts,
packages/core/src/external/ehr/api/sync-patient.ts
Updated parameter types for linking and syncing patients, using new type aliases.
packages/core/src/external/ehr/api/fetch-bundle.ts Added function to fetch EHR bundles from internal API endpoints by resource type.
packages/core/src/external/ehr/bundle/bundle-shared.ts Added S3 key creation utilities for EHR bundles; defined bundle types and supported resource queries.
packages/core/src/external/ehr/bundle/commands/create-or-replace-bundle.ts Added function to create or replace resource bundles in S3 with type validation.
packages/core/src/external/ehr/bundle/commands/fetch-bundle.ts Added functions to fetch EHR bundles from S3, with last-modified support and error handling.
packages/core/src/external/ehr/bundle/commands/update-bundle.ts Added function to update resource bundles in S3, merging new resources and ensuring uniqueness.
packages/core/src/external/ehr/bundle/create-resource-diff-bundles/create-resource-diff-bundle-shared.ts Added base request type for resource diff bundle creation.
packages/core/src/external/ehr/bundle/create-resource-diff-bundles/steps/compute/...
packages/core/src/external/ehr/bundle/create-resource-diff-bundles/steps/start/...
Added cloud and local handler classes and factories for starting and computing resource diff bundles, supporting both Metriport-only and EHR-only directions.
packages/core/src/external/ehr/bundle/create-resource-diff-bundles/utils.ts Added robust duplicate detection utility for FHIR resources using resource-type-specific deduplication.
packages/core/src/external/ehr/bundle/refresh-ehr-bundles/... Added cloud and local handler classes, factory, and types for refreshing EHR bundles by resource type.
packages/core/src/external/ehr/shared.ts Added S3 prefix utility, updated S3 key structure, and TTL-based bundle fetching function.
packages/core/src/fhir-deduplication/shared.ts Centralized artifact extension URL as a constant for maintainability.
packages/core/src/util/config.ts Added methods to retrieve new EHR bundle and queue environment variables.
packages/shared/src/interface/external/ehr/fhir-resource.ts Introduced Zod schemas, types, and utility functions for FHIR resources and bundles.
packages/shared/src/interface/external/ehr/index.ts Re-exported FHIR resource types and utilities.
packages/shared/src/interface/external/ehr/resource-diff.ts Added resource diff direction constants, type, and type guard.
packages/infra/lib/api-stack.ts Integrated new Lambda functions, SQS queues, and S3 bucket for EHR bundle processing into the API stack.
packages/infra/lib/api-stack/api-service.ts Passed new queue and bucket resources to the API service, updating environment variables and permissions.
packages/infra/lib/ehr-nested-stack.ts Added setup for new Lambdas, queues, and S3 bucket for resource diff and bundle refresh workflows; refactored resource creation.
packages/infra/config/env-config.ts,
packages/infra/config/example.ts
Added ehrBundleBucketName to environment configuration.
packages/core/src/external/ehr/elation/link-patient/elation-link-patient-cloud.ts,
packages/core/src/external/ehr/sync-patient/ehr-sync-patient-cloud.ts
Refactored to instantiate SQS clients in constructors and removed internal error handling/logging.
packages/core/src/external/ehr/elation/link-patient/elation-link-patient-local.ts,
packages/core/src/external/ehr/sync-patient/ehr-sync-patient-local.ts
Removed error handling and logging from local link/sync patient methods.
packages/core/src/command/write-to-storage/s3/write-to-s3-cloud.ts Simplified SQS client instantiation in the constructor.
packages/lambdas/src/ehr-compute-resource-diff-bundles.ts,
packages/lambdas/src/ehr-refresh-ehr-bundles.ts,
packages/lambdas/src/ehr-start-resource-diff-bundles.ts
Added Lambda handlers for computing, refreshing, and starting resource diff bundles, with SQS event processing and schema validation.
packages/lambdas/src/ehr-sync-patient.ts,
packages/lambdas/src/elation-link-patient.ts
Removed explicit error handling in Lambda handlers, relying on Sentry for exception capture.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API
    participant SQS
    participant Lambda
    participant S3

    Client->>API: POST /ehr/canvas/patient/:id/resource-diff-bundle?direction=metriport-only
    API->>SQS: Send start resource diff bundle message
    SQS->>Lambda: Trigger Lambda (start resource diff)
    Lambda->>Lambda: Start resource diff bundle logic
    Lambda->>SQS: Send compute resource diff bundle message
    SQS->>Lambda: Trigger Lambda (compute resource diff)
    Lambda->>S3: Write resource diff bundle to S3

    Client->>API: GET /ehr/canvas/patient/:id/resource-diff-bundle?direction=metriport-only
    API->>S3: Fetch resource diff bundle from S3
    S3-->>API: Return bundle (if exists)
    API-->>Client: Respond with bundle JSON
Loading
sequenceDiagram
    participant Client
    participant API
    participant SQS
    participant Lambda
    participant S3

    Client->>API: POST /internal/ehr/canvas/patient/refresh-ehr-bundles
    API->>SQS: Send refresh EHR bundles message
    SQS->>Lambda: Trigger Lambda (refresh EHR bundles)
    Lambda->>API: Fetch fresh bundles by resource type
    API->>S3: Write refreshed bundle to S3

    Client->>API: GET /internal/ehr/canvas/patient/ehr-bundle?resourceType=...
    API->>S3: Fetch bundle from S3 (optionally cached)
    S3-->>API: Return bundle (if exists)
    API-->>Client: Respond with bundle JSON
Loading
sequenceDiagram
    participant Lambda
    participant S3

    Lambda->>S3: Fetch existing bundle (by key)
    alt Bundle exists and fresh
        S3-->>Lambda: Return bundle
    else Bundle missing or stale
        S3-->>Lambda: Return undefined
    end
    Lambda->>S3: Write or update bundle (if needed)
Loading

<!-- walkthrough_end -->
<!-- internal state start -->


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

<!-- internal state 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=3608):

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

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

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

### Other keywords and placeholders

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

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](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 4 commits April 3, 2025 16:42
Ref: #2850
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
@thomasyopes thomasyopes marked this pull request as ready for review April 4, 2025 16:49
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: 4

🧹 Nitpick comments (32)
packages/shared/src/medical/fhir/resources.ts (1)

84-88: Consider refining the type predicate implementation.

The type predicate function implementation casts the input to the target type before checking if it exists in the array. This approach contradicts the coding guidelines which state that type predicates should "only [be] applicable to narrow down the type, not to force a complete type conversion."

A more appropriate implementation would be:

export function isResourceTypeForConsolidation(
  resourceType: string
): resourceType is ResourceTypeForConsolidation {
-  return resourceTypeForConsolidation.includes(resourceType as ResourceTypeForConsolidation);
+  return resourceTypeForConsolidation.some(type => type === resourceType);
}

This implementation avoids the type casting and performs a proper runtime check that aligns better with the purpose of type predicates.

packages/shared/src/interface/external/ehr/resource-diff.ts (1)

5-7: Type guard implementation is correct but could be improved

The type guard correctly checks if a string is a valid ResourceDiffDirection value, but the type assertion can be avoided.

export function isResourceDiffDirection(direction: string): direction is ResourceDiffDirection {
-  return Object.values(ResourceDiffDirection).includes(direction as ResourceDiffDirection);
+  return Object.values(ResourceDiffDirection).includes(direction as any);
}

Alternatively, for even better type safety:

export function isResourceDiffDirection(direction: string): direction is ResourceDiffDirection {
-  return Object.values(ResourceDiffDirection).includes(direction as ResourceDiffDirection);
+  return Object.values(ResourceDiffDirection).some(value => value === direction);
}
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-factory.ts (1)

6-13: Good use of factory pattern for environment-specific implementation.

The factory function correctly determines which implementation to return based on the environment, following the coding guidelines for keeping functions small and deterministic.

Consider adding a brief comment explaining why a waitTime of 0 is used for the local implementation.

 export function buildEhrComputeResourceDiffHandler(): EhrComputeResourceDiffHandler {
   if (Config.isDev()) {
+    // Local implementation with no delay for development environment
     const waitTimeAtTheEndInMillis = 0;
     return new EhrComputeResourceDiffLocal(waitTimeAtTheEndInMillis);
   }
   const ehrComputeResourceDiffQueueUrl = Config.getEhrComputeResourceDiffQueueurl("");
   return new EhrComputeResourceDiffCloud(ehrComputeResourceDiffQueueUrl);
 }
packages/core/src/external/ehr/api/save-resource-diff.ts (2)

35-41: Validate matchedResourceIds array before joining.

The code joins the matchedResourceIds array with commas without validation. Consider adding validation to ensure the array contains valid IDs before joining.

-  const queryParams = new URLSearchParams({
-    cxId,
-    patientId,
-    resourceId,
-    direction,
-    matchedResourceIds: matchedResourceIds.join(","),
-  });
+  const queryParams = new URLSearchParams({
+    cxId,
+    patientId,
+    resourceId,
+    direction,
+    matchedResourceIds: matchedResourceIds.filter(Boolean).join(","),
+  });

43-47: Consider returning the response data.

The function only verifies that response data exists but doesn't return it. If the caller never needs the response data, this is fine, but if they might need it in the future, consider modifying the return type and returning the response data.

-export async function saveResourceDiff({
+export async function saveResourceDiff<T = any>({
   ehr,
   cxId,
   patientId,
   resourceId,
   direction,
   matchedResourceIds,
 }: {
   ehr: EhrSource;
   cxId: string;
   patientId: string;
   resourceId: string;
   direction: ResourceDiffDirection;
   matchedResourceIds: string[];
-}): Promise<void> {
+}): Promise<T> {
   const { log, debug } = out(`Ehr saveResourceDiff - cxId ${cxId}`);
   const api = axios.create({ baseURL: Config.getApiurl("") });
   const queryParams = new URLSearchParams({
     cxId,
     patientId,
     resourceId,
     direction,
     matchedResourceIds: matchedResourceIds.join(","),
   });
   const saveResourceDiffUrl = `/internal/ehr/${ehr}/patient/save-resource-diff?${queryParams.toString()}`;
   try {
     const response = await api.post(saveResourceDiffUrl);
     if (!response.data) throw new Error(`No body returned from ${saveResourceDiffUrl}`);
     debug(`${saveResourceDiffUrl} resp: ${JSON.stringify(response.data)}`);
+    return response.data;
   } catch (error) {
packages/infra/lib/api-stack/api-service.ts (1)

80-156: Consider refactoring large function.

This function is over 500 lines long and handles many different concerns. Consider breaking it down into smaller, more focused functions for better maintainability.

For example, you could extract queue setup, environment variable setup, and access permissions into separate helper functions.

packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts (1)

68-68: Consider returning computation results.

The function returns void, which means callers can't know what differences were found. Consider returning the result of the computation.

If ehrResourceDiffHandler.computeResourceDiff() returns useful information, modify this function to return that data to the caller.

packages/api/src/models/resource-mapping-reversed.ts (1)

20-48: Add database constraints and indexes for better performance.

The model definition doesn't include any unique constraints or indexes, which could impact database integrity and query performance.

 static setup: ModelSetup = (sequelize: Sequelize) => {
   ResourceMappingReversedModel.init(
     {
       ...BaseModel.attributes(),
       cxId: {
         type: DataTypes.UUID,
+        allowNull: false,
       },
       patientId: {
         type: DataTypes.UUID,
+        allowNull: false,
       },
       patientMappingExternalId: {
         type: DataTypes.STRING,
+        allowNull: false,
       },
       resourceId: {
         type: DataTypes.STRING,
+        allowNull: false,
       },
       source: {
         type: DataTypes.STRING,
+        allowNull: false,
       },
       externalId: {
         type: DataTypes.STRING,
+        allowNull: false,
       },
     },
     {
       ...BaseModel.modelOptions(sequelize),
       tableName: ResourceMappingReversedModel.NAME,
+      indexes: [
+        {
+          unique: true,
+          fields: ["patient_id", "resource_id", "source"],
+          name: "resource_mapping_reversed_unique_idx",
+        },
+        {
+          fields: ["patient_mapping_external_id"],
+          name: "resource_mapping_reversed_patient_mapping_idx",
+        },
+      ],
     }
   );
 };
packages/core/src/external/ehr/api/compute-resource-diff.ts (1)

9-18: JSDoc parameters don't match function signature

The JSDoc comment includes parameters resourceId and matchedResourceIds which are not present in the actual function signature. Instead, the function accepts a resource parameter which isn't documented.

Update the JSDoc to match the actual function parameters:

/**
 * Sends a request to the API to sync a patient with Metriport.
 *
 * @param ehr - The EHR source.
 * @param cxId - The CX ID.
 * @param patientId - The patient ID.
- * @param resourceId - The resource ID.
+ * @param practiceId - The practice ID.
+ * @param resource - The FHIR resource.
 * @param direction - The direction of the resource diff.
- * @param matchedResourceIds - The matched resource IDs.
 */
packages/api/src/sequelize/migrations/2025-04-03_00_create-resource-mapping-reversed.ts (1)

16-17: Comment is misleading for sequential operations

The comment suggests using Promise.all for independent operations, but the migration operations are inherently sequential (constraints depend on table creation), so awaiting them one after another is correct.

Either remove this comment or clarify that in this specific case, sequential operations are required:

-// Use 'Promise.all' when changes are independent of each other
+// Operations must be sequential as later operations depend on the table being created first
packages/api/src/routes/ehr/canvas/patient.ts (3)

63-69: Enhance API documentation with more details

The JSDoc comment could be more descriptive about what the resource diff process does, the purpose of the direction parameter, and what clients should expect.

/**
 * POST /ehr/canvas/patient/:id/resource-diff
 *
- * Starts the resource diff process
+ * Starts the resource diff process to identify differences between resources in the 
+ * specified direction (EHR to Metriport or Metriport to EHR)
 * @param req.params.id The ID of Canvas Patient.
+ * @param req.query.practiceId The ID of the Canvas Practice.
+ * @param req.query.direction The direction of the diff (DIFF_EHR or DIFF_METRIPORT).
 * @returns 200 OK
 */

87-93: Enhance API documentation with response details

The JSDoc comment should include information about the response structure to improve API usability.

/**
 * GET /ehr/canvas/patient/:id/resource-diff
 *
- * Retrieves the resource diff
+ * Retrieves the resource differences between Canvas and Metriport
 * @param req.params.id The ID of Canvas Patient.
+ * @param req.query.direction The direction of the diff (DIFF_EHR or DIFF_METRIPORT).
- * @returns Resource diff
+ * @returns Array of ResourceMappingReversedMapped objects containing resource IDs and mapping information
 */

94-108: Consider adding query parameter validation for robustness

While the direction parameter is validated, there's no validation for the existence of other required parameters (unlike in the POST route where you validate practiceId).

router.get(
  "/:id/resource-diff",
  handleParams,
  requestLogger,
  asyncHandler(async (req: Request, res: Response) => {
    const cxId = getCxIdOrFail(req);
    const canvasPatientId = getFrom("params").orFail("id", req);
    const direction = getFromQueryOrFail("direction", req);
    if (!isResourceDiffDirection(direction)) {
      throw new BadRequestError("Invalid direction", undefined, { direction });
    }
+    // Optional: If practiceId is needed for context in getCanvasResourceDiffFromEhr
+    // const canvasPracticeId = getFromQueryOrFail("practiceId", req);
    const resourceIds = await getCanvasResourceDiffFromEhr({ cxId, canvasPatientId, direction });
    return res.status(httpStatus.OK).json(resourceIds);
  })
);
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-cloud.ts (1)

14-19: Consider simplifying the SQS client initialization logic.
You could unify the assignment into a single statement for brevity.

-    if (!sqsClient) {
-      this.sqsClient = new SQSClient({ region: region ?? Config.getAWSRegion() });
-    } else {
-      this.sqsClient = sqsClient;
-    }
+    this.sqsClient = sqsClient ?? new SQSClient({ region: region ?? Config.getAWSRegion() });
packages/lambdas/src/ehr-process-resource-diff.ts (2)

23-43: Consider processing all SQS messages in a single invocation.
Currently, only the first message in the event is handled. If multiple messages arrive, reviewing them all might be needed.

-  const message = getSingleMessageOrFail(event.Records, lambdaName);
-  if (!message) return;
-  // process the single message
+  for (const record of event.Records) {
+    if (!record.body) continue;
+    const parsedBody = parseBody(record.body);
+    // process each message
+  }

64-73: Handle JSON.parse errors gracefully.
Catching parsing errors can help generate a more direct failure message if the payload isn't valid JSON.

-function parseBody(body?: unknown): ProcessResourceDiffRequest {
-  if (!body) throw new MetriportError(`Missing message body`);
-  const bodyString = typeof body === "string" ? (body as string) : undefined;
-  if (!bodyString) throw new MetriportError(`Invalid body`);
-  const bodyAsJson = JSON.parse(bodyString);
-  return ehrResourceDiffSchema.parse(bodyAsJson) as ProcessResourceDiffRequest;
+function parseBody(body?: unknown): ProcessResourceDiffRequest {
+  if (!body) throw new MetriportError("Missing message body");
+  const bodyString = typeof body === "string" ? body : undefined;
+  if (!bodyString) throw new MetriportError("Invalid body");
+  try {
+    const bodyAsJson = JSON.parse(bodyString);
+    return ehrResourceDiffSchema.parse(bodyAsJson) as ProcessResourceDiffRequest;
+  } catch (error) {
+    throw new MetriportError("Failed to parse message body as JSON", error);
+  }
}
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-cloud.ts (1)

11-12: Constant definition is appropriately named.

MAX_SQS_MESSAGE_SIZE is descriptive. Consider moving it to a central constants file if this limit might be reused.

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

64-92: New /compute-resource-diff endpoint logic is well-structured.

  1. Validates direction via isResourceDiffDirection before proceeding.
  2. Uses fhirResourceSchema for request body validation, conforming to typed FHIR.
  3. Non-blocking computeCanvasResourceDiff call is wrapped in a catch, ensuring asynchronous errors are handled by processAsyncError.

Consider returning a JSON payload for more transparency (e.g., a short success message) instead of an empty 200.


94-122: New /save-resource-diff endpoint structure follows a similar pattern.

  1. Direction validation is consistent with the prior endpoint.
  2. The function saveCanvasResourceDiff is called asynchronously with error handling.
  3. Efficient parameter parsing from query is good; ensure callers always pass arrays for matchedResourceIds.

Same comment applies about possibly returning a JSON message or object confirming success.

packages/core/src/external/ehr/resource-diff/utils.ts (1)

1-19: Imports are extensive yet domain-focused.

They rely on multiple deduplication functions, indicating a modular approach. Keep evaluating if all resource-specific modules are truly needed to maintain minimal overhead.

packages/api/src/external/ehr/canvas/command/save-resource-diff.ts (1)

16-43: Consider adding validation for edge cases.

The function correctly handles the DIFF_EHR direction and provides an informative error for the DIFF_METRIPORT direction. However, there are a few considerations:

  1. There's no handling for a case where direction doesn't match either of the defined enum values.
  2. The matchedResourceIds.sort().join(",") approach assumes IDs won't contain commas, which could lead to parsing ambiguity.

For the sorting and joining of IDs, consider:

- externalId: matchedResourceIds.sort().join(","),
+ externalId: matchedResourceIds.sort().join("|"), // Or another delimiter less likely to appear in IDs

For handling unexpected directions:

  } else if (direction === ResourceDiffDirection.DIFF_METRIPORT) {
    throw new MetriportError("Cannot save resources in this direction", undefined, { direction });
+ } else {
+   throw new MetriportError("Invalid resource diff direction", undefined, { direction });
  }
packages/core/src/external/ehr/canvas/index.ts (1)

352-393: Robust implementation of FHIR resource retrieval.

The new method follows functional programming principles with good error handling, particularly for the NotFoundError case. A few recommendations:

  1. Consider strengthening validation by checking if bundle.entry exists before accessing it.
  2. The error message for invalid resource types could be more descriptive.
- throw new MetriportError(`Invalid resource type found`, undefined, {
+ throw new MetriportError(`Invalid resource type found when expecting ${resourceType}`, undefined, {
    invalidResourceType: invalidResource.resource.resourceType,
  });

Also, add a check for bundle.entry:

  const bundle = await this.makeRequest<FhirResourceBundle>({
    // ...
  });
+ if (!bundle.entry || !Array.isArray(bundle.entry)) {
+   return [];
+ }
  const invalidResource = bundle.entry.find(
packages/api/src/domain/resource-mapping-reversed.ts (2)

4-10: Type guard implementation could be improved.

The type guard function casts source to ResourceMappingReversedSource before checking if it's included, which could bypass TypeScript's type safety.

export function isResourceMappingReversedSource(
  source: string
): source is ResourceMappingReversedSource {
-  return resourceMappingSource.includes(source as ResourceMappingReversedSource);
+  return resourceMappingSource.includes(source as any);
}

12-19: Add documentation for domain type.

While the type is well-structured, it lacks documentation explaining what a "reversed" resource mapping represents. This would improve maintainability for new developers.

+/**
+ * Represents a mapping between a resource in the Metriport system and its corresponding 
+ * representation in an external system in a reversed direction (from external to Metriport).
+ */
export type ResourceMappingReversedPerSource = {
  externalId: string;
  cxId: string;
  patientId: string;
  patientMappingExternalId: string;
  resourceId: string;
  source: ResourceMappingReversedSource;
};
packages/api/src/external/ehr/canvas/command/start-resource-diff.ts (1)

14-38: Consider adding explicit error handling.

The function effectively orchestrates the resource diff process but relies on called functions for error handling. For improved robustness, consider adding specific error handling within this function.

export async function startCanvasResourceDiff({
  cxId,
  canvasPracticeId,
  canvasPatientId,
  direction,
}: StartResourceDiffParams): Promise<void> {
+  try {
    const existingPatient = await getPatientMappingOrFail({
      cxId,
      externalId: canvasPatientId,
      source: EhrSources.canvas,
    });
    const metriportPatient = await getPatientOrFail({
      cxId,
      id: existingPatient.patientId,
    });
    const ehrResourceDiffHandler = buildEhrStartResourceDiffHandler();
    await ehrResourceDiffHandler.startResourceDiff({
      ehr: EhrSources.canvas,
      cxId,
      practiceId: canvasPracticeId,
      metriportPatientId: metriportPatient.id,
      ehrPatientId: canvasPatientId,
      direction,
    });
+  } catch (error) {
+    throw new MetriportError("Failed to start Canvas resource diff", error as Error, {
+      cxId,
+      canvasPatientId,
+      direction
+    });
+  }
}
packages/lambdas/src/ehr-start-resource-diff.ts (2)

22-22: Consider avoiding arrow function for handler.

According to the coding guidelines, arrow functions should be avoided in modules. Converting this export to a traditional function expression or named function would align better with the project's guidelines.

-export const handler = Sentry.AWSLambda.wrapHandler(async (event: SQSEvent) => {
+export async function handler(event: SQSEvent) {

28-28: Use standardized logging interface.

The guidelines advise against using console.log outside of utils/infra/shared. Replace it with out().log, a custom logger, or prefixedLog for consistent logging, error tracking, and improved maintainability.

-    console.log(`Running with unparsed body: ${message.body}`);
+    log(`Running with unparsed body: ${message.body}`);
packages/shared/src/interface/external/ehr/fhir-resource.ts (1)

3-9: Consider stronger typing instead of z.any().

Although FHIR resources can vary widely, relying on z.any() bypasses type safety. Narrowing down the schema — or breaking it up if feasible — can yield earlier detection of incompatible resource fields during validation.

z.record(z.string(), z.any())
+// Potentially replace z.any() with respective FHIR field types or partials
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.ts (1)

47-49: Rename loop variable for clarity.

When iterating over each sub-chunk, consider using a more descriptive name than chunk, which is the same name as the Lodash function. A concise rename can improve readability.

-for (const chunk of chunks) {
-  await Promise.all(chunk.map(params => computeResourceDiff(params)));
+for (const diffBatch of chunks) {
+  await Promise.all(diffBatch.map(params => computeResourceDiff(params)));
packages/infra/lib/ehr-nested-stack.ts (1)

58-76: New “startResourceDiff” configuration looks consistent.

You've reused the same pattern of queue & lambda config as in other setups. This method ensures a clear separation of concerns. Consider factoring out repeating logic if more diffs are expected in the future.

packages/api/src/command/mapping/resource-reversed.ts (2)

16-44: Upsert-like logic in createOrUpdateResourceMappingReversed.

This function updates an existing record or creates a new one if it doesn’t exist. If multiple concurrent requests for the same resource arrive, consider an explicit transaction or an upsert method to avoid duplicates. Otherwise, it looks good.


133-143: deleteAllResourceMappingReverseds for bulk deletion.

Function is straightforward. Consider returning a count of deleted rows to confirm the bulk operation if needed, but it’s optional.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2b5c795 and e5f3dd8.

📒 Files selected for processing (34)
  • packages/api/src/command/mapping/resource-reversed.ts (1 hunks)
  • packages/api/src/command/medical/patient/consolidated-get.ts (1 hunks)
  • packages/api/src/domain/resource-mapping-reversed.ts (1 hunks)
  • packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts (1 hunks)
  • packages/api/src/external/ehr/canvas/command/get-resource-diff.ts (1 hunks)
  • packages/api/src/external/ehr/canvas/command/save-resource-diff.ts (1 hunks)
  • packages/api/src/external/ehr/canvas/command/start-resource-diff.ts (1 hunks)
  • packages/api/src/models/db.ts (3 hunks)
  • packages/api/src/models/resource-mapping-reversed.ts (1 hunks)
  • packages/api/src/routes/ehr/canvas/patient.ts (2 hunks)
  • packages/api/src/routes/internal/ehr/canvas/patient.ts (2 hunks)
  • packages/api/src/sequelize/migrations/2025-04-03_00_create-resource-mapping-reversed.ts (1 hunks)
  • packages/core/src/external/ehr/api/compute-resource-diff.ts (1 hunks)
  • packages/core/src/external/ehr/api/save-resource-diff.ts (1 hunks)
  • packages/core/src/external/ehr/canvas/index.ts (4 hunks)
  • packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-factory.ts (1 hunks)
  • packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-local.ts (1 hunks)
  • packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff.ts (1 hunks)
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-cloud.ts (1 hunks)
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-factory.ts (1 hunks)
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.ts (1 hunks)
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff.ts (1 hunks)
  • packages/core/src/external/ehr/resource-diff/utils.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 (6 hunks)
  • packages/lambdas/src/ehr-process-resource-diff.ts (1 hunks)
  • packages/lambdas/src/ehr-start-resource-diff.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/fhir-resource.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/index.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/resource-diff.ts (1 hunks)
  • packages/shared/src/medical/fhir/resources.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...

**/*.ts: - Use the Onion Pattern to organize a package's code in layers

  • Try to use immutable code and avoid sharing state across different functions, objects, and systems
  • Try to build code that's idempotent whenever possible
  • Prefer functional programming style functions: small, deterministic, 1 input, 1 output
  • Minimize coupling / dependencies
  • Avoid modifying objects received as parameter
  • Only add comments to code to explain why something was done, not how it works
  • Naming
    • classes, enums: PascalCase
    • constants, variables, functions: camelCase
    • file names: kebab-case
    • table and column names: snake_case
    • Use meaningful names, so whoever is reading the code understands what it means
    • Don’t use negative names, like notEnabled, prefer isDisabled
    • For numeric values, if the type doesn’t convey the unit, add the unit to the name
  • Typescript
    • Use types
    • Prefer const instead of let
    • Avoid any and casting from any to other types
    • Type predicates: only applicable to narrow down the type, not to force a complete type conversion
    • Prefer deconstructing parameters for functions instead of multiple parameters that might be of
      the same type
    • Don’t use null inside the app, only on code interacting with external interfaces/services,
      like DB and HTTP; convert to undefined before sending inwards into the code
    • Use async/await instead of .then()
    • Use the strict equality operator ===, don’t use abstract equality operator ==
    • When calling a Promise-returning function asynchronously (i.e., not awaiting), use .catch() to
      handle errors (see processAsyncError and emptyFunction depending on the case)
    • Date and Time
      • Always use buildDayjs() to create dayjs instances
      • Prefer dayjs.duration(...) to create duration consts and keep them as duration
  • Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
  • Avoid creating arrow functions
  • Use truthy syntax instead of in - i.e., if (data.link) not if ('link' in data)
  • Error handling
    • Pass the original error as the new one’s cause so the stack trace is persisted
    • Error messages should have a static message - add dynamic data to MetriportError's additionalInfo prop
    • Avoid sending multiple events to Sentry for a single error
  • Global constants and variables
    • Move literals to constants declared after imports when possible (avoid magic numbers)
    • Avoid shared, global objects
  • Avoid using console.log and console.error in packages other than utils, infra and shared,
    and try to use out().log instead
  • Avoid multi-line logs
    • don't send objects as a second parameter to console.log() or out().log()
    • don't create multi-line strings when using JSON.stringify()
  • Use eslint to enforce code style
  • Use prettier to format code
  • max column length is 100 chars
  • multi-line comments use /** */
  • scripts: top-level comments go after the import
  • packages/shared/src/interface/external/ehr/resource-diff.ts
  • packages/shared/src/interface/external/ehr/index.ts
  • packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-factory.ts
  • packages/shared/src/medical/fhir/resources.ts
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-factory.ts
  • packages/api/src/models/db.ts
  • packages/lambdas/src/ehr-process-resource-diff.ts
  • packages/api/src/models/resource-mapping-reversed.ts
  • packages/core/src/external/ehr/api/save-resource-diff.ts
  • packages/api/src/external/ehr/canvas/command/get-resource-diff.ts
  • packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-cloud.ts
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff.ts
  • packages/api/src/command/medical/patient/consolidated-get.ts
  • packages/infra/lib/api-stack.ts
  • packages/api/src/external/ehr/canvas/command/start-resource-diff.ts
  • packages/api/src/routes/ehr/canvas/patient.ts
  • packages/core/src/external/ehr/resource-diff/utils.ts
  • packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts
  • packages/api/src/domain/resource-mapping-reversed.ts
  • packages/core/src/util/config.ts
  • packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff.ts
  • packages/core/src/external/ehr/canvas/index.ts
  • packages/shared/src/interface/external/ehr/fhir-resource.ts
  • packages/api/src/sequelize/migrations/2025-04-03_00_create-resource-mapping-reversed.ts
  • packages/lambdas/src/ehr-start-resource-diff.ts
  • packages/api/src/routes/internal/ehr/canvas/patient.ts
  • packages/core/src/external/ehr/api/compute-resource-diff.ts
  • packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-local.ts
  • packages/infra/lib/api-stack/api-service.ts
  • packages/api/src/external/ehr/canvas/command/save-resource-diff.ts
  • packages/infra/lib/ehr-nested-stack.ts
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.ts
  • packages/api/src/command/mapping/resource-reversed.ts
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-cloud.ts
🧠 Learnings (1)
packages/api/src/sequelize/migrations/2025-04-03_00_create-resource-mapping-reversed.ts (1)
Learnt from: leite08
PR: metriport/metriport#3598
File: packages/api/src/sequelize/migrations/2025-04-03_00_drop-cq-directory-entry.ts:96-99
Timestamp: 2025-04-03T18:27:13.730Z
Learning: When reviewing database migration files, the `down` function should exactly replicate the original table schema (without necessarily adding "improvements" like default values) to ensure proper rollback functionality, even if that schema doesn't follow all best practices.
🧬 Code Definitions (18)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-factory.ts (4)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff.ts (1)
  • EhrComputeResourceDiffHandler (16-18)
packages/core/src/util/config.ts (1)
  • Config (9-180)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-local.ts (1)
  • EhrComputeResourceDiffLocal (9-36)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-cloud.ts (1)
  • EhrComputeResourceDiffCloud (13-51)
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-factory.ts (4)
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff.ts (1)
  • EhrStartResourceDiffHandler (13-15)
packages/core/src/util/config.ts (1)
  • Config (9-180)
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.ts (1)
  • EhrStartResourceDiffLocal (13-56)
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-cloud.ts (1)
  • EhrStartResourceDiffCloud (6-30)
packages/lambdas/src/ehr-process-resource-diff.ts (2)
packages/lambdas/src/ehr-start-resource-diff.ts (1)
  • handler (22-52)
packages/shared/src/interface/external/ehr/fhir-resource.ts (1)
  • fhirResourceSchema (3-9)
packages/api/src/models/resource-mapping-reversed.ts (1)
packages/api/src/domain/resource-mapping-reversed.ts (2)
  • ResourceMappingReversed (21-21)
  • ResourceMappingReversedSource (5-5)
packages/core/src/external/ehr/api/save-resource-diff.ts (1)
packages/core/src/util/config.ts (1)
  • Config (9-180)
packages/api/src/external/ehr/canvas/command/get-resource-diff.ts (1)
packages/api/src/command/mapping/resource-reversed.ts (2)
  • ResourceMappingReversedMapped (108-112)
  • getMappedResourceIdsByPatientMappingExternalId (114-131)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-cloud.ts (2)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff.ts (2)
  • EhrComputeResourceDiffHandler (16-18)
  • ComputeResourceDiffRequest (7-14)
packages/core/src/util/config.ts (1)
  • Config (9-180)
packages/api/src/external/ehr/canvas/command/start-resource-diff.ts (1)
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-factory.ts (1)
  • buildEhrStartResourceDiffHandler (6-13)
packages/api/src/routes/ehr/canvas/patient.ts (3)
packages/shared/src/interface/external/ehr/resource-diff.ts (1)
  • isResourceDiffDirection (5-7)
packages/api/src/external/ehr/canvas/command/start-resource-diff.ts (1)
  • startCanvasResourceDiff (14-38)
packages/api/src/external/ehr/canvas/command/get-resource-diff.ts (1)
  • getCanvasResourceDiffFromEhr (17-41)
packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts (5)
packages/shared/src/interface/external/ehr/fhir-resource.ts (2)
  • FhirResource (10-10)
  • fhirResourceSchema (3-9)
packages/shared/src/medical/fhir/resources.ts (1)
  • isResourceTypeForConsolidation (84-88)
packages/api/src/command/medical/patient/consolidated-get.ts (2)
  • GetConsolidatedPatientData (50-58)
  • getConsolidatedPatientData (411-432)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-factory.ts (1)
  • buildEhrComputeResourceDiffHandler (6-13)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff.ts (1)
  • ResourceWithId (5-5)
packages/core/src/util/config.ts (1)
packages/api/src/shared/config.ts (1)
  • getEnvVarOrFail (14-14)
packages/core/src/external/ehr/canvas/index.ts (1)
packages/shared/src/interface/external/ehr/fhir-resource.ts (3)
  • FhirResources (13-13)
  • FhirResourceBundle (24-24)
  • fhirResourceBundleSchema (20-23)
packages/lambdas/src/ehr-start-resource-diff.ts (2)
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.ts (1)
  • EhrStartResourceDiffLocal (13-56)
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff.ts (1)
  • StartResourceDiffRequest (4-11)
packages/api/src/routes/internal/ehr/canvas/patient.ts (4)
packages/shared/src/interface/external/ehr/resource-diff.ts (1)
  • isResourceDiffDirection (5-7)
packages/shared/src/interface/external/ehr/fhir-resource.ts (1)
  • fhirResourceSchema (3-9)
packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts (1)
  • computeCanvasResourceDiff (29-78)
packages/api/src/external/ehr/canvas/command/save-resource-diff.ts (1)
  • saveCanvasResourceDiff (16-44)
packages/core/src/external/ehr/api/compute-resource-diff.ts (5)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-cloud.ts (1)
  • computeResourceDiff (28-50)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-local.ts (1)
  • computeResourceDiff (12-35)
packages/core/src/external/ehr/resource-diff/utils.ts (1)
  • computeResourceDiff (39-174)
packages/shared/src/interface/external/ehr/fhir-resource.ts (1)
  • FhirResource (10-10)
packages/core/src/util/config.ts (1)
  • Config (9-180)
packages/api/src/external/ehr/canvas/command/save-resource-diff.ts (1)
packages/api/src/command/mapping/resource-reversed.ts (1)
  • createOrUpdateResourceMappingReversed (16-44)
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.ts (7)
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff.ts (2)
  • EhrStartResourceDiffHandler (13-15)
  • StartResourceDiffRequest (4-11)
packages/shared/src/interface/external/ehr/fhir-resource.ts (1)
  • fhirResourceSchema (3-9)
packages/core/src/external/ehr/canvas/index.ts (1)
  • supportedCanvasDiffResources (44-44)
packages/core/src/external/ehr/api/compute-resource-diff.ts (1)
  • computeResourceDiff (19-59)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-cloud.ts (1)
  • computeResourceDiff (28-50)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-local.ts (1)
  • computeResourceDiff (12-35)
packages/core/src/external/ehr/resource-diff/utils.ts (1)
  • computeResourceDiff (39-174)
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-cloud.ts (2)
packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff.ts (2)
  • EhrStartResourceDiffHandler (13-15)
  • StartResourceDiffRequest (4-11)
packages/core/src/util/config.ts (1)
  • Config (9-180)
🪛 GitHub Actions: PR Check - Lambdas
packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts

[error] 2-2: error TS2307: Cannot find module '@***/core/external/ehr/resource-diff/process/ehr-process-resource-diff' or its corresponding type declarations.

🪛 GitHub Actions: PR Check - Core
packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts

[error] 2-2: error TS2307: Cannot find module '@***/core/external/ehr/resource-diff/process/ehr-process-resource-diff' or its corresponding type declarations.

🪛 GitHub Actions: PR Check - API
packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts

[error] 2-2: error TS2307: Cannot find module '@***/core/external/ehr/resource-diff/process/ehr-process-resource-diff' or its corresponding type declarations.

🪛 GitHub Actions: PR Check - Infra
packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts

[error] 2-2: error TS2307: Cannot find module '@***/core/external/ehr/resource-diff/process/ehr-process-resource-diff' or its corresponding type declarations.

🪛 GitHub Actions: PR Check - Shared
packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts

[error] 2-2: error TS2307: Cannot find module '@***/core/external/ehr/resource-diff/process/ehr-process-resource-diff' or its corresponding type declarations.

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (62)
packages/shared/src/interface/external/ehr/index.ts (1)

3-4: LGTM: Clean addition of module exports

The addition of these exports follows the existing pattern and properly makes the resource diff and FHIR resource modules available to other parts of the application.

packages/api/src/models/db.ts (2)

40-40: LGTM: Import follows existing pattern

The import of ResourceMappingReversedModel is correctly placed with other model imports.


71-71: LGTM: Model setup added correctly

The ResourceMappingReversedModel.setup has been properly added to the models array, following the existing pattern for model initialization.

packages/shared/src/interface/external/ehr/resource-diff.ts (1)

1-4: LGTM: Well-defined enum with descriptive values

The ResourceDiffDirection enum is well-defined with clear, descriptive values that represent the direction of resource differences.

packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-factory.ts (2)

1-5: LGTM: Proper imports

All necessary imports are included for the factory implementation.


6-13:

✅ Verification successful

Factory implementation follows good design patterns

The factory function correctly implements the Factory pattern, returning different implementations based on the environment. This promotes good separation of concerns and testability.

However, the waitTimeAtTheEndInMillis parameter is set to 0, which effectively makes it a no-op. Consider if this is intentional or if a more meaningful default value should be used.


🏁 Script executed:

#!/bin/bash
# Check how waitTimeAtTheEndInMillis is used in the local implementation
rg -A 5 "waitTimeInMillis" --type ts

Length of output: 15603


Action Required: Confirm Wait Time Default is Intentional

The factory implementation is solid and follows good design patterns by returning different implementations based on the environment. After verifying across similar modules (e.g., the PatientImportQuery factory and other local handlers), it appears that setting the wait time to 0 for local instances is a consistent, intentional design choice to avoid unnecessary delays in development. If this default is truly desired, consider adding a brief comment in the code explaining that a zero delay is purposefully used in local deployments. No further changes are necessary.

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

161-167: LGTM: Added environment variable getters for resource diff queues.

These methods follow the established pattern in the Config class and properly use getEnvVarOrFail to ensure required environment variables are present.

packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff.ts (1)

1-15: Well-structured type and interface definition.

The StartResourceDiffRequest type and EhrStartResourceDiffHandler interface are clearly defined with appropriate fields and follow TypeScript best practices. The interface follows a command pattern that encapsulates the resource diff operation.

packages/api/src/command/medical/patient/consolidated-get.ts (1)

50-58:

✅ Verification successful

LGTM: Exported type for reuse.

Making the GetConsolidatedPatientData type exportable is appropriate for reuse in other modules. This follows the TypeScript best practices in the coding guidelines.

Consider creating a ticket to track the TODO on line 56 regarding the removal of forceDataFromFhir when contributed data is part of get consolidated from S3.


🌐 Web query:

What are TypeScript best practices for exporting types?

💡 Result:

TypeScript's best practices for exporting types emphasize clarity, runtime safety, and compatibility with tooling. Here are key recommendations:

Use explicit type exports

  • Always use export type for type-only exports to ensure they're elided from runtime code[1][3][4].
    interface ButtonProps { onClick: () => void }
    export type { ButtonProps };  // ✅ Correct
  • Separate type exports from value exports when re-exporting[1][2]:
    // ❌ Incorrect
    export { Button, ButtonProps };
    
    // ✅ Correct
    export { Button };
    export type { ButtonProps };

Maintain export clarity

  • Avoid mixed type/value exports using ESLint's consistent-type-exports rule[1][4]:
    // With fixMixedExportsWithInlineTypeSpecifier: true
    export { x, type T };  // ✅ Autofix available
  • Prefer named exports over default exports for types to improve import clarity[2][4]:
    // ❌ Avoid default type exports
    export default interface Foo {...}
    
    // ✅ Use named exports
    export interface Foo {...}

Tooling integration

  • Enable type-aware ESLint rules like @typescript-eslint/consistent-type-exports[1]
  • Use inline type specifiers for mixed imports/exports (TS 4.5+)[1][4]:
    import { type Foo, Bar } from './module';  // ✅ Clear type/value distinction

Module design considerations

  • Minimize exported API surface - only export types needed externally[2][4]
  • Use file-scoped modules instead of namespaces for type organization[2][4]
  • Avoid mutable type exports as they can lead to runtime inconsistencies[2]

For projects using --isolatedModules, follow these practices strictly as the compiler requires explicit type exports[1][3]. While modern bundlers can automatically elide type-only exports, explicit export type usage improves codebase readability and tooling compatibility[3][4][6].

Citations:


Approval: Exported Type Follows TypeScript Best Practices
The exported GetConsolidatedPatientData type uses the recommended explicit type export pattern, aligning well with TypeScript best practices for clarity and maintaining a clean runtime output. The usage of export type here is appropriate for ensuring that type-only artifacts are elided during the compilation process.

  • Please create a ticket to address the TODO on line 56 regarding the future removal of forceDataFromFhir once contributed data is integrated from S3.
packages/core/src/external/ehr/api/save-resource-diff.ts (2)

8-32: Good job with function documentation and parameter typing.

The function has a clear JSDoc comment explaining its purpose and parameters. The parameters are properly typed with a TypeScript interface. This is excellent for code readability and maintainability.


47-59: Good error handling implementation.

The error handling follows best practices by capturing the original error as the cause, logging the error, and providing detailed context information for debugging.

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

102-103: LGTM: New resource diff queue parameters added correctly.

The resource diff queue parameters are properly integrated into the existing parameter list, following the established pattern for other queues.


140-141: LGTM: Parameter type definitions match function signature.

The parameter types for the new queues are correctly defined as IQueue, maintaining type safety.


264-265: LGTM: Environment variables properly set for resource diff queues.

The environment variables for the resource diff queues follow the naming pattern of other queue URLs.


419-428: LGTM: Queue access permissions correctly granted.

Access to the new queues is properly granted using the provideAccessToQueue function, consistent with how other queues are handled.

packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts (1)

29-68: Good job on implementing the resource comparison logic.

The function properly retrieves patient data, validates resource types, and fetches the appropriate resources based on the diffing direction. The error handling is also well-implemented.

packages/api/src/models/resource-mapping-reversed.ts (1)

8-19: LGTM: Model properly extends BaseModel and implements interface.

The model correctly extends BaseModel and implements the ResourceMappingReversed interface, ensuring it conforms to the expected contract. The properties are properly declared with their types.

packages/core/src/external/ehr/api/compute-resource-diff.ts (1)

47-58: LGTM! Error handling follows best practices

The error handling correctly logs the error with context and throws a MetriportError with appropriate additional information, following the coding guidelines.

packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-local.ts (1)

22-35: LGTM! Clean implementation of resource diff computation

The implementation follows the functional programming style recommended in the coding guidelines - it's deterministic with clear inputs and outputs, and handles the process in a clean sequence of steps.

packages/api/src/sequelize/migrations/2025-04-03_00_create-resource-mapping-reversed.ts (1)

75-89: Down migration looks correct

The down migration correctly reverses all changes made in the up migration, including constraint removal, index removal, and table deletion, in the proper order. This follows the best practice mentioned in the retrieved learning about ensuring proper rollback functionality.

packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-cloud.ts (2)

21-23: Implementation looks good.
The method logic is straightforward and aligns with best practices for SQS message sending.


24-28:

❓ Verification inconclusive

Confirm that the targeted queue is a FIFO queue.
The fifo: true option requires the queue name to end with .fifo in AWS SQS. Verify that the queue is indeed FIFO.


🏁 Script executed:

#!/bin/bash
# Description: Confirm that the SQS queue used here ends with .fifo when using fifo: true

rg "EHR_START_RESOURCE_DIFF_QUEUE_URL"

Length of output: 261


Action Required: Verify that the SQS queue is configured as a FIFO queue.

The SQS client is explicitly sending messages with { fifo: true }, which requires that the queue name end with .fifo. Although the environment variable EHR_START_RESOURCE_DIFF_QUEUE_URL is sourced from configuration (see packages/core/src/util/config.ts) and is set from ehrStartResourceDiffQueue.queueUrl in the infra code (packages/infra/lib/api-stack/api-service.ts), please confirm that the queue is instantiated with a name ending in .fifo.

  • Action Item: In packages/infra/lib/api-stack/api-service.ts, verify that when the queue is created (e.g., via a new sqs.Queue), its queueName explicitly includes the .fifo suffix.
  • Action Item: Confirm that the environment variable EHR_START_RESOURCE_DIFF_QUEUE_URL indeed points to that FIFO queue.
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff.ts (2)

5-14: Check if partial properties are suitable.
In some cases, certain fields might be missing or defaulted. Verify if all properties should be strictly required or if you want optional fields.


16-18: Interface structure is consistent.
The contract for computeResourceDiff is concise and complements the domain logic.

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

370-373: Integration of resource diff Lambdas looks consistent.
The new references to startResourceDiffLambda and completeResourceDiffLambda fit the EHR resource difference flow.


595-596: Environment variables set correctly.
Providing API_URL to these new Lambdas ensures they can call back into the API service if needed.

packages/lambdas/src/ehr-process-resource-diff.ts (2)

31-31: Potential mismatch with AI summary about property naming.
The code references patientId, whereas the summary mentions metriportPatientId and ehrPatientId. Verify that the correct property is being used.

Likely an incorrect or invalid review comment.


55-62: Zod schema is well-defined.
The usage of z.nativeEnum and clear field validation is appropriate for guaranteeing a strict payload structure.

packages/api/src/external/ehr/canvas/command/get-resource-diff.ts (3)

1-10: Imports look consistent and well-organized.

They align with the domain boundaries, bringing in only the necessary external and local modules. There are no obvious naming or namespace conflicts here.


11-15: Type definition is clear and strongly typed.

The GetResourceDiffParams type concisely declares required fields. Ensure all call sites provide these fields.


17-41:

❓ Verification inconclusive

Function is well-structured and uses clear early returns.

  1. ``: Verify that all valid directions are indeed checked. For any future extension, consider handling an unexpected direction more explicitly or adding a default case to aid debugging.
  2. The error thrown for DIFF_METRIPORT ensures no silent failures.
  3. Consider adding logging for the empty array return at line 40 to confirm no diff was found.

🏁 Script executed:

#!/bin/bash
# Searching for getCanvasResourceDiffFromEhr usage to ensure its return values are handled properly
rg -A 5 "getCanvasResourceDiffFromEhr"

Length of output: 1911


Review on getCanvasResourceDiffFromEhr: Direction Handling and Debug Logging

The function is well-structured with clear early returns:

  • For DIFF_EHR, it returns the expected resource mappings.
  • For DIFF_METRIPORT, throwing an error prevents silent failure.
  • The default branch returns an empty array, which is handled by the caller in the API route.

Please consider:

  • Explicitly handling unexpected direction values (e.g., through a default case that logs a warning or error) to aid in debugging future extensions.
  • Adding logging when returning an empty array to clearly indicate when no diff was found.

These adjustments would improve debuggability and overall robustness if new directions are introduced.

packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-cloud.ts (2)

1-10: Imports comply with layering and domain context.

Dependencies are minimal, referencing shared code and config.


13-52: Class structure neatly encapsulates the SQS logic.

  1. The constructor pattern ensures sqsClient is either injected or created on-demand, which improves testability.
  2. The payload size check is a good safeguard against oversize messages.
  3. Consider logging successful requests for tracking. Otherwise, all essential error-handling is in place.
packages/api/src/routes/internal/ehr/canvas/patient.ts (1)

2-2: Added imports for EHR resource-diff functionality.

These appear relevant to the new endpoints below. No concerns with naming or references here.

Also applies to: 8-8, 10-10

packages/core/src/external/ehr/resource-diff/utils.ts (2)

20-38: Error handling imports and the local ResourceWithId type are consistent.

Use of MetriportError centralizes error creation. Type import from ./compute/ehr-compute-resource-diff is aligned with the existing interface.


39-174:

❓ Verification inconclusive

Logic for computeResourceDiff is clear but warrants careful testing.

  1. The short-circuit return if resource already exists prevents duplicates.
  2. Switch statements effectively branch to specialized deduplication functions.
  3. Returning an empty array if deduplication reduces the resource set ensures no diff is reported.
  4. ``: Verify correctness of the condition at line 172. If any dedup results in equal or fewer items, does that always mean no new resource? Confirm with tests or logs.

🏁 Script executed:

#!/bin/bash
# Searching for calls to computeResourceDiff to see if the caller depends on the length check logic
rg -A 5 "computeResourceDiff"

Length of output: 8989


Review Comment: Verify Deduplication Condition in computeResourceDiff

The overall structure is clear and the specialized deduplication functions are consistently used. Notably:

  • The early return when a resource already exists avoids duplicate entries.
  • The switch-case branches cleanly delegate to the relevant deduplication function.
  • The function returns an empty array when the deduplicated list’s length increases, implying a unique addition.

However, please confirm that the condition at line 172—

if (deduplicatedResources.length > deduplicatedResourceInitial.length) return [];

—is robust. Specifically:

  • Validate that when deduplication yields an equal or reduced count (as might happen when a new resource is merged with an existing one), returning the diff (i.e., all existing resource IDs) correctly signals that no additional unique resource was effectively added.
  • Review how this return value is consumed downstream (e.g., in ehr-start-resource-diff-local.ts and compute modules) to ensure that an empty array versus a non-empty diff is interpreted as intended.
  • Add targeted tests or logging to confirm the behavior under edge cases.
packages/api/src/external/ehr/canvas/command/save-resource-diff.ts (1)

8-14: LGTM: Clear type definition.

The SaveResourceDiffParams type has well-defined parameters with appropriate types.

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

13-27: Good addition of required imports.

The imports are correctly structured to add the new dependencies needed for the FHIR resource handling functionality.


44-44:

✅ Verification successful

Verify this constant's usage.

The supportedCanvasDiffResources constant is defined but not used within this file. Ensure it's properly utilized in related functionality.


🏁 Script executed:

#!/bin/bash
# Find where supportedCanvasDiffResources is used
rg "supportedCanvasDiffResources" --type ts

Length of output: 473


Constant usage verified in project.

The constant supportedCanvasDiffResources is imported and used in packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.ts to check if a resource type is included, ensuring proper functionality. No changes are required.

packages/api/src/domain/resource-mapping-reversed.ts (1)

21-21: LGTM: Clean interface extension.

The interface properly extends BaseDomain and incorporates the resource mapping properties.

packages/api/src/external/ehr/canvas/command/start-resource-diff.ts (1)

7-12: LGTM: Well-defined parameter type.

The parameters are well-structured with appropriate types.

packages/lambdas/src/ehr-start-resource-diff.ts (1)

63-72: Looks good!

The Zod-based validation and structured error handling in parseBody adhere to best practices. The approach is clear and helps ensure data correctness.

packages/shared/src/interface/external/ehr/fhir-resource.ts (1)

1-25: All set here!

The definitions for FHIR resource schemas are well-organized, and the use of Zod encourages safe parsing and validation.

packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.ts (1)

13-55: No concerns with the logic.

The class effectively batches and processes EHR resource diffs. The clean separation of directions and final wait time is clear and follows the design patterns well.

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

16-17: All good adding new wait times for resource diff.

These values are consistent with the existing pattern of per-minute thresholds (e.g., waitTimePatientSync) and follow the same approach for controlling throughput.


31-31: Shorter alarm threshold may cause more frequent alerts.

You reduced alarmMaxAgeOfOldestMessage from two days to two hours. Ensure that this shorter window won't lead to false alarms, especially if there's a temporary spike in the queue.


50-50: Watch out for a tighter time window for Elation link alarms.

Similarly, the alarmMaxAgeOfOldestMessage reduction to two hours might be aggressive. Validate whether this is sufficient for your Elation link flows.


77-95: “completeResourceDiff” configuration introduces a higher alarm threshold.

Raising maxMessageCountAlarmThreshold from 5,000 to 15,000 drastically delays alerts when there's a backlog. Make sure that’s intended, and that you won’t miss earlier warnings if the queue grows quickly.


99-100: Exporting new diff configs.

Exposing startResourceDiff and completeResourceDiff is consistent with how the other queues are declared. Looks good.


141-144: Adding new class fields for resource diff.

These additional Lambda and Queue properties align with your new resource diff logic. No issues, just ensure references to these new fields are properly tested.


171-190: Setup methods for resource diff queues.

These calls to setupStartResourceDiff and setupCompleteResourceDiff keep the stack constructor tidy. Double-check that environment variables and alarms are configured as expected.


310-367: Implementation of “setupStartResourceDiff.”

This is consistent with the existing pattern. The code is clear, respects the established naming, and includes FIFO and a DLQ. Great job.


369-426: Implementation of “setupCompleteResourceDiff.”

Same consistent pattern for the second queue. Using identical structure ensures maintainability.

packages/api/src/command/mapping/resource-reversed.ts (9)

1-2: Imports for UUID and NotFoundError are appropriate.

You’re bringing in uuidv7 and a shared NotFoundError, which aligns with the approach of raising typed errors.


3-8: Domain-related imports are correctly scoped.

Referencing ResourceMappingReversed and ResourceMappingReversedPerSource from the domain is consistent with your layering approach.


9-15: Type definitions for reversed mapping queries.

Using Omit here keeps the code DRY and ensures partial usage of ResourceMappingReversedParams. This is a neat approach.


46-57: getResourceMappingReversed returns undefined when not found.

That’s a consistent design choice for optional lookups. No issues.


59-75: getResourceMappingReversedOrFail throws NotFoundError properly.

Good approach for strict lookups—keeps the calling code simpler.


77-88: getResourceMappingModelReversed returns the full model.

It’s purposeful that here you’re returning the model itself rather than dataValues. That offers more flexibility if you need instance-level methods.


90-106: getResourceMappingModelReversedOrFail similarly throws on misses.

Keeps a consistent code pattern and fosters clarity for the caller.


108-113: Typed structure for reversed mappings.

ResourceMappingReversedMapped is well-defined. Helps keep calls typed and straightforward.


114-131: getMappedResourceIdsByPatientMappingExternalId retrieving multiple resource mappings.

A clear approach: fetch all records, map them to typed objects. No concerns here.

Ref: #2850
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: 4

🧹 Nitpick comments (3)
packages/core/src/external/ehr/resource-diff/utils.ts (3)

40-149: Consider breaking down the complex function into smaller units

The computeResourceDiff function handles multiple responsibilities: validation, deduplication, and derivation checking. Consider refactoring it into smaller, more focused functions for better maintainability and testing.

Example refactoring approach:

function validateResourceTypes(
  existingResources: ResourceWithId[],
  newResource: ResourceWithId
): void {
  // Validation logic here
}

function deduplicateResources(
  resources: ResourceWithId[],
  resourceType: string
): Resource[] {
  // Deduplication switch logic here
}

export function computeResourceDiff({
  existingResources,
  newResource,
}: {
  existingResources: ResourceWithId[];
  newResource: ResourceWithId;
}): string[] {
  validateResourceTypes(existingResources, newResource);
  
  if (existingResources.some(resource => resource.id === newResource.id)) return [];
  
  const resources = existingResources.concat([newResource]);
  const deduplicatedResources = deduplicateResources(resources, newResource.resourceType);
  
  // Rest of the function...
}

151-157: Add documentation for the helper function

The resourceIsDerivedFromExistingResource function performs an important check, but its purpose and behavior aren't immediately clear. Consider adding a JSDoc comment to explain its purpose.

+/**
+ * Checks if a resource is derived from another resource by looking for
+ * a specific extension in the resource's extension array.
+ * 
+ * @param resource - The resource to check
+ * @returns true if the resource has the artifact related extension, false otherwise
+ */
function resourceIsDerivedFromExistingResource(resource: Resource): boolean {

152-152: Improve type safety with type guard

Instead of using in operator directly, consider using a proper type guard for better type safety and readability.

-  if (!("extension" in resource)) return false;
+  if (!resource.extension || !Array.isArray(resource.extension)) return false;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e5f3dd8 and a92db12.

📒 Files selected for processing (2)
  • packages/core/src/external/ehr/resource-diff/utils.ts (1 hunks)
  • packages/core/src/fhir-deduplication/shared.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/core/src/fhir-deduplication/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/resource-diff/utils.ts
🧬 Code Definitions (1)
packages/core/src/external/ehr/resource-diff/utils.ts (5)
packages/core/src/external/ehr/api/compute-resource-diff.ts (1)
  • computeResourceDiff (19-59)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-cloud.ts (1)
  • computeResourceDiff (28-50)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff-local.ts (1)
  • computeResourceDiff (12-35)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff.ts (1)
  • ResourceWithId (5-5)
packages/core/src/fhir-deduplication/shared.ts (1)
  • artifactRelatedArtifactUrl (42-43)
🔇 Additional comments (1)
packages/core/src/external/ehr/resource-diff/utils.ts (1)

40-149:

❓ Verification inconclusive

Verify correct handling of all FHIR resource types

The function handles many FHIR resource types, but make sure all relevant types for your application are covered. Consider adding a test to verify that all supported resource types are handled appropriately.


🏁 Script executed:

#!/bin/bash
# Check if there are any FHIR resource types imported but not handled in the switch statement

# First, get all imported resource types
imported_types=$(cat packages/core/src/external/ehr/resource-diff/utils.ts | grep -P "import \{[\s\S]*\} from \"@medplum/fhirtypes\";" | grep -oP "(?<=\s)[A-Z][a-zA-Z]+(?=,|\n|\})")

# Then, get all resource types handled in the switch statement
handled_types=$(cat packages/core/src/external/ehr/resource-diff/utils.ts | grep -P "case \"[A-Z][a-zA-Z]+\":" | grep -oP "(?<=\")[A-Z][a-zA-Z]+(?=\")")

# Compare the two lists
echo "Imported resource types:"
echo "$imported_types" | sort
echo
echo "Handled resource types in switch statement:"
echo "$handled_types" | sort
echo
echo "Resource types imported but not handled:"
comm -23 <(echo "$imported_types" | sort) <(echo "$handled_types" | sort)
echo
echo "Resource types handled but not imported:"
comm -13 <(echo "$imported_types" | sort) <(echo "$handled_types" | sort)

Length of output: 2212


Action Required: Ensure Comprehensive FHIR Resource Type Coverage

The current implementation handles these resource types:

  • AllergyIntolerance
  • Composition
  • Condition
  • Encounter
  • FamilyMemberHistory
  • Immunization
  • Location
  • Medication
  • MedicationAdministration
  • MedicationRequest
  • MedicationStatement
  • Observation
  • Organization
  • Practitioner
  • Procedure
  • RelatedPerson

However, our automated check to compare imported FHIR types versus the ones handled was inconclusive due to an error with the missing comm command (and no imported types were identified via our script). To mitigate any risk of unsupported resource types slipping through, please manually verify that all FHIR resource types relevant to our application are accounted for. Additionally, consider adding a dedicated test to auto-verify that the switch statement remains aligned with the complete set of supported resource types.

… 2850-canvas-diff

Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
Thomas Yopes added 2 commits April 7, 2025 13:58
… 2850-canvas-diff

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

🧹 Nitpick comments (4)
packages/api/src/command/mapping/resource-reversed.ts (4)

16-47: Consider relying on Sequelize’s built-in timestamps for updatedAt.

Manually setting existing.updatedAt = new Date(); alongside existing.changed("externalId", true); may be redundant if timestamps is enabled. Typically, assigning existing.externalId = externalId automatically marks the attribute as changed. You might simplify by removing these explicit updates and rely on Sequelize’s built-in behavior.


49-60: Prefer using .get({ plain: true }) instead of dataValues.

Returning dataValues works, but using .get({ plain: true }) or .toJSON() can improve clarity and avoid accidental references to the underlying Sequelize model fields.


117-134: Potential pagination.

If this endpoint is called for a large dataset, consider adding pagination or filtering to protect against excessive data loads.


136-146: Optional use of transactions or concurrency checks.

destroy queries all matching rows. If concurrency or partial updates are a concern, consider wrapping in a transaction or ensuring DB constraints align with your usage.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7144edb and 928dc19.

📒 Files selected for processing (1)
  • packages/api/src/command/mapping/resource-reversed.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...

**/*.ts: - Use the Onion Pattern to organize a package's code in layers

  • Try to use immutable code and avoid sharing state across different functions, objects, and systems
  • Try to build code that's idempotent whenever possible
  • Prefer functional programming style functions: small, deterministic, 1 input, 1 output
  • Minimize coupling / dependencies
  • Avoid modifying objects received as parameter
  • Only add comments to code to explain why something was done, not how it works
  • Naming
    • classes, enums: PascalCase
    • constants, variables, functions: camelCase
    • file names: kebab-case
    • table and column names: snake_case
    • Use meaningful names, so whoever is reading the code understands what it means
    • Don’t use negative names, like notEnabled, prefer isDisabled
    • For numeric values, if the type doesn’t convey the unit, add the unit to the name
  • Typescript
    • Use types
    • Prefer const instead of let
    • Avoid any and casting from any to other types
    • Type predicates: only applicable to narrow down the type, not to force a complete type conversion
    • Prefer deconstructing parameters for functions instead of multiple parameters that might be of
      the same type
    • Don’t use null inside the app, only on code interacting with external interfaces/services,
      like DB and HTTP; convert to undefined before sending inwards into the code
    • Use async/await instead of .then()
    • Use the strict equality operator ===, don’t use abstract equality operator ==
    • When calling a Promise-returning function asynchronously (i.e., not awaiting), use .catch() to
      handle errors (see processAsyncError and emptyFunction depending on the case)
    • Date and Time
      • Always use buildDayjs() to create dayjs instances
      • Prefer dayjs.duration(...) to create duration consts and keep them as duration
  • Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
  • Avoid creating arrow functions
  • Use truthy syntax instead of in - i.e., if (data.link) not if ('link' in data)
  • Error handling
    • Pass the original error as the new one’s cause so the stack trace is persisted
    • Error messages should have a static message - add dynamic data to MetriportError's additionalInfo prop
    • Avoid sending multiple events to Sentry for a single error
  • Global constants and variables
    • Move literals to constants declared after imports when possible (avoid magic numbers)
    • Avoid shared, global objects
  • Avoid using console.log and console.error in packages other than utils, infra and shared,
    and try to use out().log instead
  • Avoid multi-line logs
    • don't send objects as a second parameter to console.log() or out().log()
    • don't create multi-line strings when using JSON.stringify()
  • Use eslint to enforce code style
  • Use prettier to format code
  • max column length is 100 chars
  • multi-line comments use /** */
  • scripts: top-level comments go after the import
  • packages/api/src/command/mapping/resource-reversed.ts
🧬 Code Definitions (1)
packages/api/src/command/mapping/resource-reversed.ts (3)
packages/api/src/domain/resource-mapping-reversed.ts (2)
  • ResourceMappingReversedPerSource (12-19)
  • ResourceMappingReversed (21-21)
packages/api/src/models/resource-mapping-reversed.ts (1)
  • ResourceMappingReversedModel (8-49)
packages/shared/src/index.ts (1)
  • NotFoundError (38-38)
🔇 Additional comments (9)
packages/api/src/command/mapping/resource-reversed.ts (9)

1-2: Looks good.

Imports for uuidv7 and NotFoundError are appropriately placed.


3-6: Well-structured domain imports.

No issues identified with the domain/type imports.


7-7: Model import is clear and concise.

All good here.


9-10: Straightforward type alias.

No issues with ResourceMappingReversedParams definition.


11-15: Appropriate usage of Omit utility.

The type ResourceMappingReversedLookUpParams neatly excludes unneeded properties.


62-78: Clear error handling.

Throwing a NotFoundError if the resource is missing is a clean way to handle retrieval failures.


80-91: Returning the model instance.

This function is consistent with getResourceMappingModelReversed usage. No concerns here.


93-109: *Good use of a dedicated OrFail variant.

Mirrors the logic from getResourceMappingReversedOrFail, providing a model-based not-found check.


111-115: Structured type for mapped resource.

ResourceMappingReversedMapped is clear and matches typical usage for returning minimal fields.

Thomas Yopes added 3 commits April 8, 2025 09:55
… 2850-canvas-diff

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

♻️ Duplicate comments (4)
packages/core/src/external/ehr/resource-diff/utils.ts (4)

49-49: Fix typo in variable name

There's a typo in the variable name mewResourceType - it should be newResourceType.

-  const mewResourceType = newResource.resourceType;
+  const newResourceType = newResource.resourceType;

68-68: Same typo in switch statement

The same typo from above is used in the switch condition.

-  switch (mewResourceType) {
+  switch (newResourceType) {

141-141: Same typo in error message

The same typo appears in the error message template string.

-      throw new BadRequestError(`Unsupported resource type: ${mewResourceType}`, undefined, {
+      throw new BadRequestError(`Unsupported resource type: ${newResourceType}`, undefined, {

142-142: Same typo in error details

The typo also affects the error details object.

-        resourceType: mewResourceType,
+        resourceType: newResourceType,
🧹 Nitpick comments (10)
packages/core/src/external/ehr/api/save-resource-diff.ts (1)

37-43: Consider potential issue with array joining.

When joining matchedResourceIds with commas, there could be potential issues if any of the IDs themselves contain commas.

Consider using a different delimiter or URL-safe encoding if you expect complex IDs:

-    matchedResourceIds: matchedResourceIds.join(","),
+    matchedResourceIds: JSON.stringify(matchedResourceIds), // Or another approach that handles special characters
packages/api/src/external/ehr/canvas/command/save-resource-diff.ts (1)

33-33: Fix typo in variable name.

There's a typo in the variable name fonud which should be found.

-    const fonud = matchedResourceIds.length > 0;
+    const found = matchedResourceIds.length > 0;
packages/shared/src/interface/external/ehr/fhir-resource.ts (1)

3-9: Well-structured schema definition with an important consideration

The schema definition using z.intersection allows for flexible FHIR resource definitions by combining fixed fields with an open record type. This approach is common for handling FHIR resources where certain properties are required while accommodating additional arbitrary properties.

However, be mindful that using z.any() in the record type removes type safety for those additional properties. While this flexibility is often necessary for FHIR resources, it may be worth considering if there are common optional properties that could be explicitly typed for better validation.

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

43-59: Document the roadmap for supported resources

The array contains many commented-out resource types with only "DiagnosticReport" currently enabled. This suggests a phased implementation approach, but lacks documentation explaining the current state and future plans.

 export const supportedCanvasDiffResources = [
+  // Current implementation only supports DiagnosticReport.
+  // The commented resources below represent the planned roadmap for future support.
   /*
   "AllergyIntolerance",
   "Condition",
   */
   "DiagnosticReport",
   /*
   "Encounter",
   */
   //"Medication",
   /*
   "MedicationStatement",
   "MedicationRequest",
   //"Observation",
   "Procedure",
   */
 ];

373-416: Well-structured method with comprehensive error handling

The getFhirResourcesByResourceType method is well-implemented with proper parameter typing, URL construction, and error handling. It correctly handles the NotFoundError case by returning an empty array and properly validates the resource types in the response.

Two suggestions for improvement:

  1. Consider adding a more descriptive error message in the MetriportError that includes the expected resource type
  2. The error handling could be more specific to capture different error scenarios
 throw new MetriportError(`Invalid resource type found`, undefined, {
   invalidResourceType: invalidResource.resource.resourceType,
+  expectedResourceType: resourceType,
 });
 try {
   // ... existing code
 } catch (error) {
   if (error instanceof NotFoundError) return [];
+  if (error instanceof MetriportError) {
+    // Re-throw MetriportError instances (including our own validation errors)
+    throw error;
+  }
+  // Handle other types of errors (network, parsing, etc.)
+  throw new MetriportError(`Failed to fetch ${resourceType} resources`, error, additionalInfo);
 }
packages/core/src/external/ehr/resource-diff/utils.ts (5)

42-48: Add JSDoc comment to describe the function's purpose and return value

Add documentation to explain what this function does, what its parameters represent, and what the returned string array signifies.

+/**
+ * Computes the difference between existing resources and a new resource.
+ * Determines if a new resource should be kept after deduplication with existing resources.
+ * @param existingResources An array of existing resources with IDs
+ * @param newResource A new resource with ID to check against existing resources
+ * @returns An array of existing resource IDs if the new resource should be ignored, empty array if the new resource should be kept
+ */
 export function computeResourceDiff({
   existingResources,
   newResource,
 }: {
   existingResources: ResourceWithId[];
   newResource: ResourceWithId;
 }): string[] {

66-144: Extract deduplication logic to a separate function

The switch statement is lengthy and makes the function harder to read. Consider extracting it to a separate function for better readability and maintainability.

 const resources = existingResources.concat([newResource]);
-  let deduplicatedResources: Resource[];
-  switch (mewResourceType) {
-    case "AllergyIntolerance":
-      deduplicatedResources = deduplicateAllergyIntolerances(
-        resources as AllergyIntolerance[]
-      ).combinedResources;
-      break;
-    // ...other cases...
-    default:
-      throw new BadRequestError(`Unsupported resource type: ${mewResourceType}`, undefined, {
-        resourceType: mewResourceType,
-      });
-  }
+  const deduplicatedResources = deduplicateResources(resources, newResourceType);

// ... later in the file
+/**
+ * Deduplicates resources based on their resource type
+ * @param resources The resources to deduplicate
+ * @param resourceType The type of resources being deduplicated
+ * @returns The deduplicated resources
+ */
+function deduplicateResources(resources: Resource[], resourceType: string): Resource[] {
+  switch (resourceType) {
+    case "AllergyIntolerance":
+      return deduplicateAllergyIntolerances(
+        resources as AllergyIntolerance[]
+      ).combinedResources;
+    // ...other cases...
+    default:
+      throw new BadRequestError(`Unsupported resource type: ${resourceType}`, undefined, {
+        resourceType,
+      });
+  }
+}

158-164: Add JSDoc comment for helper function

Add documentation to explain the purpose of the resourceIsDerivedFromExistingResource function and what it checks for.

+/**
+ * Determines if a resource is derived from an existing resource by checking for a specific extension.
+ * @param resource The resource to check
+ * @returns True if the resource is derived from an existing resource, false otherwise
+ */
 function resourceIsDerivedFromExistingResource(resource: Resource): boolean {
   if (!("extension" in resource)) return false;
   const derivedFrom = resource.extension.find(
     extension => extension.url === artifactRelatedArtifactUrl
   );
   return derivedFrom !== undefined;
 }

159-159: Use truthy syntax for checking extensions

According to the coding guidelines, prefer truthy syntax instead of using the in operator.

-  if (!("extension" in resource)) return false;
+  if (!resource.extension) return false;

146-155: Consider adding more descriptive variable names

The return logic here is a bit hard to follow at first glance. Consider using more descriptive variable names to make the intent clearer.

   const existingResourceIds = existingResources.map(resource => resource.id);
-  const newResourceInDeduplicatedResources = deduplicatedResources.find(
+  const newResourceSurvivedDeduplication = deduplicatedResources.find(
     resource => resource.id === newResource.id
   );
-  if (!newResourceInDeduplicatedResources) return existingResourceIds;
+  if (!newResourceSurvivedDeduplication) return existingResourceIds;
   const newResourceIsDerivedFromExistingResource = resourceIsDerivedFromExistingResource(
-    newResourceInDeduplicatedResources
+    newResourceSurvivedDeduplication
   );
-  if (newResourceIsDerivedFromExistingResource) return existingResourceIds;
+  // If resource is derived, we should use existing resources
+  if (newResourceIsDerivedFromExistingResource) return existingResourceIds;
+  // New resource should be kept
   return [];
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 06320fc and fb819a8.

📒 Files selected for processing (9)
  • packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts (1 hunks)
  • packages/api/src/external/ehr/canvas/command/save-resource-diff.ts (1 hunks)
  • packages/api/src/routes/ehr/canvas/patient.ts (2 hunks)
  • packages/api/src/routes/internal/ehr/canvas/patient.ts (2 hunks)
  • packages/core/src/external/ehr/api/save-resource-diff.ts (1 hunks)
  • packages/core/src/external/ehr/canvas/index.ts (4 hunks)
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.ts (1 hunks)
  • packages/core/src/external/ehr/resource-diff/utils.ts (1 hunks)
  • packages/shared/src/interface/external/ehr/fhir-resource.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.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/api/save-resource-diff.ts
  • packages/api/src/external/ehr/canvas/command/save-resource-diff.ts
  • packages/core/src/external/ehr/resource-diff/utils.ts
  • packages/api/src/routes/internal/ehr/canvas/patient.ts
  • packages/api/src/routes/ehr/canvas/patient.ts
  • packages/core/src/external/ehr/canvas/index.ts
  • packages/shared/src/interface/external/ehr/fhir-resource.ts
🧬 Code Graph Analysis (6)
packages/core/src/external/ehr/api/save-resource-diff.ts (5)
packages/api/src/external/ehr/canvas/command/save-resource-diff.ts (1)
  • SaveResourceDiffParams (8-14)
packages/shared/src/interface/external/ehr/source.ts (1)
  • EhrSource (7-7)
packages/core/src/util/log.ts (3)
  • out (29-34)
  • debug (24-27)
  • log (9-22)
packages/core/src/util/config.ts (1)
  • Config (9-186)
packages/shared/src/index.ts (1)
  • MetriportError (37-37)
packages/api/src/external/ehr/canvas/command/save-resource-diff.ts (5)
packages/core/src/external/ehr/api/save-resource-diff.ts (1)
  • SaveResourceDiffParams (8-15)
packages/api/src/command/mapping/patient.ts (1)
  • getPatientMappingOrFail (48-62)
packages/api/src/command/medical/patient/get-patient.ts (1)
  • getPatientOrFail (289-293)
packages/api/src/command/mapping/resource-reversed.ts (1)
  • createOrUpdateResourceMappingReversed (16-47)
packages/shared/src/index.ts (1)
  • MetriportError (37-37)
packages/core/src/external/ehr/resource-diff/utils.ts (7)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff.ts (1)
  • ResourceWithId (5-5)
packages/shared/src/index.ts (1)
  • BadRequestError (36-36)
packages/core/src/fhir-deduplication/resources/encounter.ts (1)
  • deduplicateEncounters (38-47)
packages/core/src/fhir-deduplication/resources/immunization.ts (1)
  • deduplicateImmunizations (27-39)
packages/core/src/fhir-deduplication/resources/observation.ts (1)
  • deduplicateObservations (20-32)
packages/core/src/fhir-deduplication/resources/procedure.ts (1)
  • deduplicateProcedures (48-57)
packages/core/src/fhir-deduplication/shared.ts (1)
  • artifactRelatedArtifactUrl (42-43)
packages/api/src/routes/internal/ehr/canvas/patient.ts (6)
packages/api/src/routes/util.ts (1)
  • getFromQueryAsArrayOrFail (79-83)
packages/shared/src/interface/external/ehr/resource-diff.ts (1)
  • isResourceDiffDirection (5-7)
packages/shared/src/interface/external/ehr/fhir-resource.ts (1)
  • fhirResourceSchema (3-9)
packages/api/src/external/ehr/canvas/command/compute-resource-diff.ts (1)
  • computeCanvasResourceDiff (33-120)
packages/core/src/util/error/shared.ts (1)
  • processAsyncError (56-64)
packages/api/src/external/ehr/canvas/command/save-resource-diff.ts (1)
  • saveCanvasResourceDiff (16-45)
packages/api/src/routes/ehr/canvas/patient.ts (6)
packages/api/src/routes/util.ts (4)
  • asyncHandler (10-28)
  • getCxIdOrFail (140-144)
  • getFrom (131-133)
  • getFromQueryOrFail (61-65)
packages/shared/src/interface/external/ehr/resource-diff.ts (1)
  • isResourceDiffDirection (5-7)
packages/shared/src/index.ts (1)
  • BadRequestError (36-36)
packages/api/src/external/ehr/canvas/command/start-resource-diff.ts (1)
  • startCanvasResourceDiff (14-38)
packages/core/src/util/error/shared.ts (1)
  • processAsyncError (56-64)
packages/api/src/external/ehr/canvas/command/get-resource-diff.ts (1)
  • getCanvasResourceDiffFromEhr (17-41)
packages/core/src/external/ehr/canvas/index.ts (2)
packages/shared/src/interface/external/ehr/fhir-resource.ts (3)
  • FhirResources (13-13)
  • FhirResourceBundle (24-24)
  • fhirResourceBundleSchema (20-23)
packages/shared/src/index.ts (2)
  • MetriportError (37-37)
  • NotFoundError (38-38)
🔇 Additional comments (26)
packages/core/src/external/ehr/api/save-resource-diff.ts (4)

8-15: Type definition looks good.

The SaveResourceDiffParams type is well-defined with all required parameters clearly documented.


17-26: Documentation is thorough and helpful.

The function documentation clearly describes the purpose and all parameters, which is excellent for maintainability.


27-44: Well-structured function implementation.

The implementation properly creates an Axios instance, builds query parameters, and constructs the URL following best practices. Good use of the out utility for logging.


45-61: Robust error handling.

The error handling is excellent - you're using try-catch, logging the error, and throwing a MetriportError with appropriate context information. This follows the error handling best practices from the coding guidelines.

packages/api/src/routes/ehr/canvas/patient.ts (5)

64-70: Good API documentation.

The endpoint documentation clearly describes the purpose and parameters, which is essential for API consumers.


71-88: Well-implemented POST endpoint for resource diff.

The implementation correctly validates inputs and handles errors. The pattern of starting an async task without waiting for its completion is appropriate for potentially long-running operations.


80-82: Good error handling for direction validation.

The direction validation is properly implemented with clear error messaging.


83-86: Non-blocking asynchronous processing.

The function initiates a potentially long-running process without blocking the response, which is good for API responsiveness. The error handling with processAsyncError ensures any issues are properly logged.


90-111: Well-implemented GET endpoint for resource diff.

The implementation follows the same pattern as the POST endpoint, with proper input validation and error handling. Returns the resource IDs with the appropriate HTTP status code.

packages/api/src/routes/internal/ehr/canvas/patient.ts (6)

64-70: Clear endpoint documentation.

The documentation effectively describes the endpoint's purpose and parameters.


71-92: Well-structured compute resource diff endpoint.

The implementation validates all required inputs, checks for valid direction, and parses the request body against the FHIR resource schema. The non-blocking async pattern is appropriate here.


82-83: Appropriate request body validation.

Using the fhirResourceSchema to parse and validate the request body ensures the data conforms to the expected structure.


94-100: Good endpoint documentation.

The documentation clearly describes the purpose of the save-resource-diff endpoint.


101-122: Well-implemented save resource diff endpoint.

The implementation extracts and validates all required parameters, handles validation errors properly, and uses the non-blocking pattern for the async operation.


118-119: Good data cleaning.

Filtering out empty strings from matchedResourceIds prevents potential issues with empty IDs.

packages/api/src/external/ehr/canvas/command/save-resource-diff.ts (5)

8-14: Well-defined type.

The SaveResourceDiffParams type clearly specifies all the required parameters.


16-31: Good patient retrieval implementation.

The function correctly retrieves the existing patient mapping and the corresponding Metriport patient, with proper error handling via the OrFail functions.


32-44: Direction handling logic looks good.

The function correctly handles different directions with appropriate actions for each case.


39-40: Clear status representation.

Using "found" or "not-found" as external ID values clearly indicates the status of the resource match.


42-44: Appropriate error handling.

The error message for unsupported direction is static and includes dynamic data in the additionalInfo prop, which follows the error handling guidelines.

packages/shared/src/interface/external/ehr/fhir-resource.ts (3)

12-13: Good array type definition

The array schema is properly defined and exported with its corresponding type, making it easy to validate lists of FHIR resources.


15-18: Clear wrapper schema definition

The wrapper schema is well-defined for handling wrapped FHIR resources, which is a common pattern in FHIR bundle entries.


20-24: Correctly defined bundle schema with optional entry array

The bundle schema correctly defines the FHIR Bundle structure with the mandatory resourceType and optional entry property. The optional entry field is appropriate as some bundles might not contain entries.

Note that code using this schema will need null checks when accessing the entry array to handle cases where it's undefined.

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

13-13: Good addition of NotFoundError import

Adding the NotFoundError import improves error handling capabilities in the Canvas API.


23-27: Well-structured imports for FHIR resource types

The imports are properly organized and include all the necessary FHIR resource types from the shared interface.


60-65: Well-implemented type guard function

The type guard function is correctly implemented to provide proper type narrowing, which enhances type safety when working with resource types.

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

🧹 Nitpick comments (1)
packages/core/src/external/ehr/canvas/index.ts (1)

367-410: Well-implemented FHIR resource retrieval method.

The getFhirResourcesByResourceType method follows good practices:

  • Strong typing with the SupportedCanvasDiffResource type
  • Proper URL parameter handling
  • Resource validation to ensure correct types
  • Appropriate error handling, particularly for NotFoundError
  • Follows functional programming style with deterministic behavior

One minor suggestion for improvement:

Consider extracting the resource validation logic into a separate helper function to improve readability and maintainability:

  async getFhirResourcesByResourceType({
    cxId,
    patientId,
    resourceType,
    extraParams,
  }: {
    cxId: string;
    patientId: string;
    resourceType: SupportedCanvasDiffResource;
    extraParams?: Record<string, string>;
  }): Promise<FhirResources> {
    const { debug } = out(
      `Canvas getFhirResourcesByResourceType - cxId ${cxId} practiceId ${this.practiceId} patientId ${patientId} resourceType ${resourceType}`
    );
    const params = { ...(extraParams ?? {}), patient: `Patient/${patientId}` };
    const urlParams = new URLSearchParams(params);
    const resourceTypeUrl = `/${resourceType}?${urlParams.toString()}`;
    const additionalInfo = { cxId, practiceId: this.practiceId, patientId, resourceType };
    try {
      const bundle = await this.makeRequest<FhirResourceBundle>({
        cxId,
        patientId,
        s3Path: `fhir-resources-${resourceType}`,
        method: "GET",
        url: resourceTypeUrl,
        schema: fhirResourceBundleSchema,
        additionalInfo,
        debug,
        useFhir: true,
      });
-      const invalidResource = bundle.entry?.find(
-        resource => resource.resource.resourceType !== resourceType
-      );
-      if (invalidResource) {
-        throw new MetriportError(`Invalid resource type found`, undefined, {
-          invalidResourceType: invalidResource.resource.resourceType,
-        });
-      }
+      this.validateResourceTypes(bundle, resourceType);
      return bundle.entry?.map(resource => resource.resource) ?? [];
    } catch (error) {
      if (error instanceof NotFoundError) return [];
      throw error;
    }
  }
+
+  private validateResourceTypes(bundle: FhirResourceBundle, expectedType: string): void {
+    const invalidResource = bundle.entry?.find(
+      resource => resource.resource.resourceType !== expectedType
+    );
+    if (invalidResource) {
+      throw new MetriportError(`Invalid resource type found`, undefined, {
+        invalidResourceType: invalidResource.resource.resourceType,
+      });
+    }
+  }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between fb819a8 and d7f2f52.

📒 Files selected for processing (2)
  • packages/core/src/external/ehr/canvas/index.ts (4 hunks)
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/external/ehr/resource-diff/start/ehr-start-resource-diff-local.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/canvas/index.ts
🧬 Code Graph Analysis (1)
packages/core/src/external/ehr/canvas/index.ts (2)
packages/shared/src/interface/external/ehr/fhir-resource.ts (3)
  • FhirResources (13-13)
  • FhirResourceBundle (24-24)
  • fhirResourceBundleSchema (20-23)
packages/shared/src/index.ts (2)
  • MetriportError (37-37)
  • NotFoundError (38-38)
🔇 Additional comments (3)
packages/core/src/external/ehr/canvas/index.ts (3)

13-13: Good addition of NotFoundError import.

The import modification now includes NotFoundError, which is used in the new functionality for proper error handling of resource not found scenarios.


23-27: Proper import organization for FHIR resources.

The added imports for FHIR resource types are necessary for the new functionality and follow the code organization principles.


43-59: Clean implementation of resource type validation.

The implementation of a constant array with supported resource types, along with a type and type guard function, provides strong type safety while maintaining a single source of truth for supported resources.

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

♻️ Duplicate comments (2)
packages/core/src/external/ehr/resource-diff/utils.ts (2)

68-68: ⚠️ Potential issue

Fix typo in variable name

There's a typo in the variable name mewResourceType - it should be newResourceType.

-  switch (mewResourceType) {
+  switch (newResourceType) {

140-144: ⚠️ Potential issue

Fix typo in error message and details

The same typo appears in the error message and context object.

-  throw new BadRequestError(`Unsupported resource type: ${mewResourceType}`, undefined, {
-    resourceType: mewResourceType,
+  throw new BadRequestError(`Unsupported resource type: ${newResourceType}`, undefined, {
+    resourceType: newResourceType,
🧹 Nitpick comments (6)
packages/core/src/external/ehr/resource-diff/utils.ts (2)

158-164: Consider adding JSDoc comments for the helper function

The resourceIsDerivedFromExistingResource function is well-implemented, but adding JSDoc comments would improve code documentation and clarity.

+/**
+ * Determines if a resource is derived from an existing resource by checking
+ * its extensions for the artifact-related artifact URL.
+ * @param resource - The FHIR resource to check
+ * @returns true if the resource is derived from an existing resource
+ */
function resourceIsDerivedFromExistingResource(resource: Resource): boolean {

158-164: Consider using type predicate for safer property access

Using a type guard with a truthy check on resource.extension would be more type-safe than using "extension" in resource.

function resourceIsDerivedFromExistingResource(resource: Resource): boolean {
-  if (!("extension" in resource)) return false;
+  if (!resource.extension || !Array.isArray(resource.extension)) return false;
  const derivedFrom = resource.extension.find(
    extension => extension.url === artifactRelatedArtifactUrl
  );
  return derivedFrom !== undefined;
}
packages/lambdas/src/ehr-compute-resource-diff.ts (2)

17-22: Validate parseInt usage.
If the environment variable is invalid or non-numeric, parseInt may produce NaN. Ensure a check for NaN to avoid potential runtime issues.

 const waitTimeInMillis = parseInt(waitTimeInMillisRaw);
+ if (isNaN(waitTimeInMillis)) {
+   throw new MetriportError("Invalid WAIT_TIME_IN_MILLIS value", {
+     additionalInfo: { waitTimeInMillisRaw },
+   });
+ }

29-29: Use the recommended logging utility.
The coding guidelines advise against using console.log outside packages like infra or shared. Switch to a standardized logger such as out().log for consistency.

- console.log(`Running with unparsed body: ${message.body}`);
+ out().log(`Running with unparsed body: ${message.body}`);

- console.log(`${msg}: ${errorToString(error)}`);
+ out().log(`${msg}: ${errorToString(error)}`);

Also applies to: 45-45

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

77-95: Use kebab-case for file entries.
The entry "ehr-Compute-resource-diff" uses uppercase “C.” Rename to align with the kebab-case standard.

- entry: "ehr-Compute-resource-diff",
+ entry: "ehr-compute-resource-diff",

369-426: Rename entry for compute resource diff.
Again, consider replacing the uppercase “C” in "ehr-Compute-resource-diff" to use kebab-case.

- entry: "ehr-Compute-resource-diff",
+ entry: "ehr-compute-resource-diff",
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d7f2f52 and 30f47e8.

📒 Files selected for processing (5)
  • packages/core/src/external/ehr/resource-diff/utils.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 (6 hunks)
  • packages/lambdas/src/ehr-compute-resource-diff.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/infra/lib/api-stack/api-service.ts
  • packages/infra/lib/api-stack.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.ts
  • packages/core/src/external/ehr/resource-diff/utils.ts
  • packages/infra/lib/ehr-nested-stack.ts
🧬 Code Graph Analysis (2)
packages/core/src/external/ehr/resource-diff/utils.ts (3)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff.ts (1)
  • ResourceWithId (5-5)
packages/shared/src/index.ts (1)
  • BadRequestError (36-36)
packages/core/src/fhir-deduplication/shared.ts (1)
  • artifactRelatedArtifactUrl (42-43)
packages/infra/lib/ehr-nested-stack.ts (2)
packages/infra/lib/shared/sqs.ts (1)
  • createQueue (50-103)
packages/infra/lib/api-stack/fhir-converter-connector.ts (1)
  • createLambda (203-282)
🔇 Additional comments (17)
packages/core/src/external/ehr/resource-diff/utils.ts (4)

1-40: Good organization of imports

The imports are well-organized, categorized by external dependencies, internal deduplication functions, and shared utilities. This structure makes the file easy to navigate.


42-60: LGTM! Robust validation of input resources

The function properly validates that all existing resources match the new resource type before proceeding with deduplication, preventing type mismatches and providing clear error context.


69-139: LGTM! Comprehensive resource type handling

The switch statement handles all the supported FHIR resource types with appropriate deduplication functions. The structure makes it easy to add new resource types in the future.


146-156: LGTM! Clear resource diff determination logic

The function properly checks if the new resource exists in the deduplicated results and whether it's derived from existing resources. The return values clearly communicate whether there's a change (empty array) or not (array of existing resource IDs).

packages/lambdas/src/ehr-compute-resource-diff.ts (5)

1-13: Imports are consistent and well-structured.
No issues found in the import statements.


14-16: Initialization order is valid.
Sentry capture initialization is placed correctly for accurate error tracking.


23-53: Overall logic is correct.
The Sentry-wrapped handler, error catching, and rethrow as MetriportError are well-structured.


55-63: Schema definition is appropriate.
Using Zod and z.nativeEnum is suitable for type safety.


64-73: Body parsing logic is comprehensive.
Throws early for missing or invalid body, ensuring predictable failure paths.

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

16-17: Consistent queue wait durations.
Both startResourceDiff and computeResourceDiff share the same 1-second wait time, which may be suitable for high throughput.


31-31: Reduced alarm threshold for syncPatient queue.
From a longer period to Duration.hours(2). Confirm if more frequent alerts are acceptable.


50-50: Reduced alarm threshold for elationLinkPatient queue.
Similarly, confirm that 2 hours is the intended maximum age for messages before triggering an alarm.


58-76: New startResourceDiff settings.
Configuration mirrors the existing queue-Lambda pattern, which helps maintain consistency.


99-100: Properly exports configuration.
Returning startResourceDiff and ComputeResourceDiff from settings() is consistent with the existing structure.


141-144: Additional Lambda/Queue properties.
Declaring these new resource diff combos as readonly is consistent with other definitions.


171-190: Wiring up startResourceDiff.
Setup pattern is analogous to setupSyncPatient and setupElationLinkPatient. Looks consistent.


310-367: setupStartResourceDiff method.
Implements the same queue and Lambda creation logic used elsewhere, maintaining uniform code style.

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

🧹 Nitpick comments (3)
packages/core/src/external/ehr/resource-diff/utils.ts (3)

54-59: Consider adding more descriptive error messages

The current error messaging is brief. For better debuggability, consider adding more context about what the error means and potential resolution steps.

- throw new BadRequestError("Invalid existing resource types", undefined, {
+ throw new BadRequestError("Invalid existing resource types - all resources must have the same type as the new resource", undefined, {

68-144: Consider extracting the resource deduplication logic into a separate function

The switch statement is large and may grow as more resource types are supported. Consider refactoring this into a separate function to improve maintainability.

- let deduplicatedResources: Resource[];
- switch (newResourceType) {
-   case "AllergyIntolerance":
-     deduplicatedResources = deduplicateAllergyIntolerances(
-       resources as AllergyIntolerance[]
-     ).combinedResources;
-     break;
-   // ... other cases ...
- }
+ const deduplicatedResources = deduplicateResources(resources, newResourceType);

// Add this function below:
+ function deduplicateResources(resources: ResourceWithId[], resourceType: string): Resource[] {
+   switch (resourceType) {
+     case "AllergyIntolerance":
+       return deduplicateAllergyIntolerances(
+         resources as AllergyIntolerance[]
+       ).combinedResources;
+     // ... other cases ...
+   }
+ }

146-155: Logic looks good but could benefit from more explanatory comments

The logic that determines whether to return existing resource IDs or an empty array is crucial to the function's purpose but might not be immediately clear to maintainers. Consider adding a comment explaining the logic.

  const existingResourceIds = existingResources.map(resource => resource.id);
+ // If the new resource has been deduplicated away or is derived from existing ones,
+ // return the existing resource IDs to indicate the resource already exists
  const newResourceInDeduplicatedResources = deduplicatedResources.find(
    resource => resource.id === newResource.id
  );
  if (!newResourceInDeduplicatedResources) return existingResourceIds;
  const newResourceIsDerivedFromExistingResource = resourceIsDerivedFromExistingResource(
    newResourceInDeduplicatedResources
  );
  if (newResourceIsDerivedFromExistingResource) return existingResourceIds;
+ // Return an empty array to indicate this is a truly new resource
  return [];
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 30f47e8 and cf9e741.

📒 Files selected for processing (1)
  • packages/core/src/external/ehr/resource-diff/utils.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...

**/*.ts: - Use the Onion Pattern to organize a package's code in layers

  • Try to use immutable code and avoid sharing state across different functions, objects, and systems
  • Try to build code that's idempotent whenever possible
  • Prefer functional programming style functions: small, deterministic, 1 input, 1 output
  • Minimize coupling / dependencies
  • Avoid modifying objects received as parameter
  • Only add comments to code to explain why something was done, not how it works
  • Naming
    • classes, enums: PascalCase
    • constants, variables, functions: camelCase
    • file names: kebab-case
    • table and column names: snake_case
    • Use meaningful names, so whoever is reading the code understands what it means
    • Don’t use negative names, like notEnabled, prefer isDisabled
    • For numeric values, if the type doesn’t convey the unit, add the unit to the name
  • Typescript
    • Use types
    • Prefer const instead of let
    • Avoid any and casting from any to other types
    • Type predicates: only applicable to narrow down the type, not to force a complete type conversion
    • Prefer deconstructing parameters for functions instead of multiple parameters that might be of
      the same type
    • Don’t use null inside the app, only on code interacting with external interfaces/services,
      like DB and HTTP; convert to undefined before sending inwards into the code
    • Use async/await instead of .then()
    • Use the strict equality operator ===, don’t use abstract equality operator ==
    • When calling a Promise-returning function asynchronously (i.e., not awaiting), use .catch() to
      handle errors (see processAsyncError and emptyFunction depending on the case)
    • Date and Time
      • Always use buildDayjs() to create dayjs instances
      • Prefer dayjs.duration(...) to create duration consts and keep them as duration
  • Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
  • Avoid creating arrow functions
  • Use truthy syntax instead of in - i.e., if (data.link) not if ('link' in data)
  • Error handling
    • Pass the original error as the new one’s cause so the stack trace is persisted
    • Error messages should have a static message - add dynamic data to MetriportError's additionalInfo prop
    • Avoid sending multiple events to Sentry for a single error
  • Global constants and variables
    • Move literals to constants declared after imports when possible (avoid magic numbers)
    • Avoid shared, global objects
  • Avoid using console.log and console.error in packages other than utils, infra and shared,
    and try to use out().log instead
  • Avoid multi-line logs
    • don't send objects as a second parameter to console.log() or out().log()
    • don't create multi-line strings when using JSON.stringify()
  • Use eslint to enforce code style
  • Use prettier to format code
  • max column length is 100 chars
  • multi-line comments use /** */
  • scripts: top-level comments go after the import
  • packages/core/src/external/ehr/resource-diff/utils.ts
🧬 Code Graph Analysis (1)
packages/core/src/external/ehr/resource-diff/utils.ts (7)
packages/core/src/external/ehr/resource-diff/compute/ehr-compute-resource-diff.ts (1)
  • ResourceWithId (5-5)
packages/shared/src/index.ts (1)
  • BadRequestError (36-36)
packages/core/src/fhir-deduplication/resources/encounter.ts (1)
  • deduplicateEncounters (38-47)
packages/core/src/fhir-deduplication/resources/immunization.ts (1)
  • deduplicateImmunizations (27-39)
packages/core/src/fhir-deduplication/resources/observation.ts (1)
  • deduplicateObservations (20-32)
packages/core/src/fhir-deduplication/resources/procedure.ts (1)
  • deduplicateProcedures (48-57)
packages/core/src/fhir-deduplication/shared.ts (1)
  • artifactRelatedArtifactUrl (42-43)
🔇 Additional comments (2)
packages/core/src/external/ehr/resource-diff/utils.ts (2)

42-156: Well implemented resource differencing with comprehensive type handling.

The computeResourceDiff function effectively implements a robust workflow for determining if a new resource is truly unique after deduplication. It handles each FHIR resource type appropriately with specific deduplication strategies.


158-164: Helper function correctly checks derived relationships

The resourceIsDerivedFromExistingResource function correctly checks for the presence of the relevant extension URL to determine if a resource is derived from an existing one.

Ref: ENG-47

Ref: #1040
Signed-off-by: Thomas Yopes <thomasyopes@Thomass-MBP.attlocal.net>
@thomasyopes thomasyopes added this pull request to the merge queue Apr 23, 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: 0

♻️ Duplicate comments (1)
packages/infra/lib/ehr-nested-stack.ts (1)

175-182: 🛠️ Refactor suggestion

Consider adding a RemovalPolicy to the S3 bucket.

The S3 bucket will default to RemovalPolicy.DESTROY, which could lead to accidental data loss during stack updates or removals. Since this bucket contains important EHR data, it's recommended to add a more protective removal policy.

import { Duration, NestedStack, NestedStackProps } from "aws-cdk-lib";
+import { RemovalPolicy } from "aws-cdk-lib";
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import { Function as Lambda } from "aws-cdk-lib/aws-lambda";
import { SqsEventSource } from "aws-cdk-lib/aws-lambda-event-sources";
import * as s3 from "aws-cdk-lib/aws-s3";
import { Queue } from "aws-cdk-lib/aws-sqs";
import { Construct } from "constructs";
// ...

const ehrBundleBucket = new s3.Bucket(this, "EhrBundleBucket", {
  bucketName: props.config.ehrBundleBucketName,
  publicReadAccess: false,
  encryption: s3.BucketEncryption.S3_MANAGED,
  versioned: true,
+  removalPolicy: RemovalPolicy.RETAIN,
});
🧹 Nitpick comments (6)
packages/infra/lib/ehr-nested-stack.ts (6)

94-96: Fix variable naming inconsistency.

The variable ComputeResourceDiffBundlesLambdaTimeout uses PascalCase instead of camelCase, which is inconsistent with other similar variables in the codebase and violates the naming convention specified in your coding guidelines.

-const ComputeResourceDiffBundlesLambdaTimeout = waitTimeComputeResourceDiff.plus(
+const computeResourceDiffBundlesLambdaTimeout = waitTimeComputeResourceDiff.plus(
   Duration.minutes(5)
 );

120-120: Maintain naming consistency for timeout variables.

Similarly, keep the same naming pattern for all timeout variables.

-const RefreshEhrBundlesLambdaTimeout = waitTimeRefreshBundle.plus(Duration.minutes(5));
+const refreshEhrBundlesLambdaTimeout = waitTimeRefreshBundle.plus(Duration.minutes(5));

100-103: Update Lambda timeout reference in configuration.

Make sure to update the reference to the timeout variable in the Lambda settings.

   lambda: {
     memory: 1024,
-    timeout: ComputeResourceDiffBundlesLambdaTimeout,
+    timeout: computeResourceDiffBundlesLambdaTimeout,
   },

125-127: Update Lambda timeout reference in configuration.

Same issue with the refreshEhrBundles Lambda settings.

   lambda: {
     memory: 1024,
-    timeout: RefreshEhrBundlesLambdaTimeout,
+    timeout: refreshEhrBundlesLambdaTimeout,
   },

108-110: Update Queue visibility timeout reference.

Update the reference to the timeout variable in the queue configuration.

     visibilityTimeout: Duration.seconds(
-      ComputeResourceDiffBundlesLambdaTimeout.toSeconds() * 2 + 1
+      computeResourceDiffBundlesLambdaTimeout.toSeconds() * 2 + 1
     ),

132-132: Update Queue visibility timeout reference.

Update the reference to the timeout variable in the refreshEhrBundles queue configuration.

-    visibilityTimeout: Duration.seconds(RefreshEhrBundlesLambdaTimeout.toSeconds() * 2 + 1),
+    visibilityTimeout: Duration.seconds(refreshEhrBundlesLambdaTimeout.toSeconds() * 2 + 1),
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6880841 and 0d58b1a.

📒 Files selected for processing (3)
  • packages/api/src/external/ehr/canvas/command/bundle/create-resource-diff-bundles.ts (1 hunks)
  • packages/core/src/external/ehr/bundle/create-resource-diff-bundles/utils.ts (1 hunks)
  • packages/infra/lib/ehr-nested-stack.ts (8 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/core/src/external/ehr/bundle/create-resource-diff-bundles/utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/api/src/external/ehr/canvas/command/bundle/create-resource-diff-bundles.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/infra/lib/ehr-nested-stack.ts
🔇 Additional comments (7)
packages/infra/lib/ehr-nested-stack.ts (7)

373-374: LGTM! Properly included bucket name in environment variables.

Good job including the S3 bucket name as an environment variable for the startResourceDiffBundles Lambda. This ensures the Lambda has access to the correct bucket information at runtime.


430-431: LGTM! Properly included bucket name in environment variables.

Good job including the S3 bucket name as an environment variable for the computeResourceDiffBundles Lambda. This ensures the Lambda has access to the correct bucket information at runtime.


386-387: LGTM! Appropriate permissions granted to Lambda.

Appropriate permissions have been granted to the startResourceDiffBundles Lambda - it can send messages to the computeResourceDiffBundles queue and write to the S3 bucket.


440-440: LGTM! Appropriate permissions granted to Lambda.

The computeResourceDiffBundles Lambda has been granted read/write permissions to the S3 bucket, which is appropriate for its function.


116-117: Good use of concurrency limits for resource-intensive Lambda.

Setting a maximum concurrency of 10 for the computeResourceDiffBundles Lambda is a good practice to avoid overloading downstream services. This helps control resource usage and costs.


38-39: LGTM! Consistent alarm configuration across queues.

Good job setting consistent alarm thresholds across all queues. Reducing the max age of oldest message to 2 hours (from what appears to have been much longer) for all queues improves alert responsiveness.

Also applies to: 59-60, 80-81, 105-106, 129-130


445-492:

✅ Verification successful

Verify that refreshBundle Lambda doesn't need S3 bucket access.

Unlike startResourceDiffBundlesLambda and computeResourceDiffBundlesLambda, the refreshEhrBundlesLambda doesn't have S3 bucket permissions granted. Does this Lambda need to read or write to the S3 bucket directly?


🏁 Script executed:

#!/bin/bash
# Look for S3 operations in the refreshBundle Lambda handler
cd packages/lambdas/src
grep -r "S3" --include="ehr-refresh-ehr-bundles.ts" .
# Also check if it imports any modules that might interact with S3
grep -r "import" --include="ehr-refresh-ehr-bundles.ts" .

# Check if it calls any functions that might use S3
cd ../../core/src/external/ehr/bundle
grep -r "S3\|s3\|bucket" --include="*.ts" .

Length of output: 4579


No S3 permissions needed for refreshEhrBundles Lambda
Verified that neither the handler (ehr-refresh-ehr-bundles.ts) nor any of its imported core modules (refresh‑ehr‑bundles or its “local” variant) reference S3, buckets, or S3Utils. This Lambda only orchestrates bundle refresh requests via SQS and does not read from or write to the bundle bucket directly.

Merged via the queue into develop with commit 01abfc2 Apr 23, 2025
21 checks passed
@thomasyopes thomasyopes deleted the 2850-canvas-diff branch April 23, 2025 19:10
@thomasyopes thomasyopes mentioned this pull request Apr 23, 2025
2 tasks
@coderabbitai coderabbitai bot mentioned this pull request Jul 8, 2025
5 tasks
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.

3 participants