-
Notifications
You must be signed in to change notification settings - Fork 72
Eng-479 backend tcm viewer changes #4044
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
WalkthroughTraditional Chinese Medicine (TCM) encounter management functionality has been introduced. This includes new domain and Sequelize model definitions, migration scripts for the database, command functions for creating, updating, and querying encounters, validation schemas, API routes for creating, updating, and listing encounters, and associated test utilities and suites. Model associations and routing logic have been updated accordingly. Changes
Sequence Diagram(s)TCM Encounter Creation (Internal POST Endpoint)sequenceDiagram
participant Client
participant InternalRouter
participant Validation
participant Command
participant TcmEncounterModel
Client->>InternalRouter: POST /internal/tcm/encounter
InternalRouter->>Validation: Validate request body
Validation-->>InternalRouter: Validated data
InternalRouter->>Command: createTcmEncounter(data)
Command->>TcmEncounterModel: create(data)
TcmEncounterModel-->>Command: Created encounter
Command-->>InternalRouter: Created encounter
InternalRouter-->>Client: 201 Created + encounter JSON
TCM Encounter Update (PUT Endpoint)sequenceDiagram
participant Client
participant Router
participant Validation
participant Command
participant TcmEncounterModel
Client->>Router: PUT /tcm/encounter/:id
Router->>Validation: Validate request body
Validation-->>Router: Validated data
Router->>Command: updateTcmEncounter({id, cxId, data})
Command->>TcmEncounterModel: findByPk(id)
TcmEncounterModel-->>Command: Encounter or null
alt Encounter found
Command->>TcmEncounterModel: update(data)
TcmEncounterModel-->>Command: Updated
Command->>TcmEncounterModel: findByPk(id)
TcmEncounterModel-->>Command: Updated encounter
Command-->>Router: Updated encounter
Router-->>Client: 200 OK + encounter JSON
else Not found
Command-->>Router: NotFoundError
Router-->>Client: Error response
end
TCM Encounter Listing (GET Endpoint)sequenceDiagram
participant Client
participant Router
participant Validation
participant ListCommand
participant TcmEncounterModel
Client->>Router: GET /tcm/encounter?after=...&limit=...
Router->>Validation: Validate query params
Validation-->>Router: Validated params
Router->>ListCommand: getTcmEncounters({cxId, after, limit, offset})
ListCommand->>TcmEncounterModel: findAll (with filters, pagination)
TcmEncounterModel-->>ListCommand: Encounter list
ListCommand-->>Router: Encounter list
Router-->>Client: 200 OK + paginated results
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
npm error code ERR_SSL_WRONG_VERSION_NUMBER 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms (2)
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🧹 Nitpick comments (13)
packages/api/src/models/db.ts (1)
44-45
: Nit: keep import list sorted for readability.The new import breaks the existing alpha sort in the medical-model block (DocRefMapping → MAPIAccess → Patient …). Consider moving it to keep the list ordered.
packages/api/src/routes/index.ts (1)
40-43
: New dash router means feature only available via dashboard path.
medicalDash
supersedesmedical
only on the/dash-oss
prefix, leaving the original/medical/v1
path without TCM encounter endpoints. Is that intentional, or should the base medical router also include them?packages/api/src/routes/medical/dash.ts (1)
7-9
: Potential double-mount ifmedical
later imports the same route.Right now
medical
does not includetcmEncounter
, so this is fine.
If somebody later adds it there, it will be mounted twice under different prefixes.
Add a comment explaining why it lives separately to prevent future duplication.packages/api/src/routes/internal/__tests__/tcm-encounter.test.ts (1)
40-43
:jest.restoreAllMocks()
is redundant hereYou aren’t using
jest.spyOn
anywhere in this suite, only top-leveljest.mock
.
restoreAllMocks()
buys nothing and adds minor cognitive overhead. Consider dropping it and keeping justclearAllMocks()
.- jest.clearAllMocks(); - jest.restoreAllMocks(); + jest.clearAllMocks();packages/api/src/domain/medical/__tests__/tcm-encounter.ts (1)
10-12
: Field nameclass
collides with reserved keyword – prefer a safer aliasWhile valid, using
class
as a property forces awkward escapes (encounter["class"]
) in many linters/TS configs and hurts readability.If the DB & API contract permits, rename to something like
encounterClass
/visitClass
. Otherwise, wrap it in a/** eslint-disable-line @typescript-eslint/no-unsafe-member-access */
comment wherever accessed to avoid future friction.packages/api/src/routes/internal/medical/tcm-encounter.ts (1)
20-24
: Return aLocation
header for the newly-created resourceREST best-practice: after
201 Created
, include the canonical URL of the new resource.
Easy win for client discoverability and HTTP tooling support.- return res.status(httpStatus.CREATED).json(encounter); + return res + .status(httpStatus.CREATED) + .location(`/internal/tcm/encounter/${encounter.id}`) + .json(encounter);packages/api/src/domain/medical/tcm-encounter.ts (1)
5-14
: Reconsiderclass
field name and date representation
class
collides with the TypeScript reserved keyword. Access is allowed but regularly bites consumers doing object-deconstruction ({ class } = x
is invalid). ConsiderencounterClass
for clarity.admitTime / dischargeTime
are typed asDate
, yet project guidelines prescribedayjs
withbuildDayjs()
. Persisting JSDate
objects and dayjs interchangeably quickly becomes messy. Define them asDayjs
or plainstring
(ISO) and convert at the boundary.Refactoring here also propagates to the Sequelize model & Zod schemas.
packages/api/src/models/medical/tcm-encounter.ts (1)
32-33
: Persist enum as constant – avoid magic strings in multiple placesThe literal list
"Admitted" | "Transferred" | "Discharged"
is hard-coded here and in the domain file. Extract a singleTcmEncounterLatestEvent
enum/const and reuse to minimise drift.export const TCM_ENCOUNTER_EVENTS = ["Admitted", "Transferred", "Discharged"] as const; export type TcmEncounterLatestEvent = typeof TCM_ENCOUNTER_EVENTS[number]; … latestEvent: { type: DataTypes.ENUM(...TCM_ENCOUNTER_EVENTS) }packages/api/src/command/medical/tcm-encounter/list-tcm-encounters.ts (1)
21-33
:after
filter overrides default but keeps stale value in memory objectYou mutate
where.admitTime
by spreading the existing object and then adding a new[Op.gt]
. Since the key is identical, the first entry is silently overwritten – ok, but unnecessary.- where.admitTime = { - ...(where.admitTime as Record<string, unknown>), - [Op.gt]: new Date(after), - }; + where.admitTime = { [Op.gt]: new Date(after) };Also, convert with
buildDayjs(after).toDate()
to respect the project’s date-handling rule set.packages/api/src/command/medical/tcm-encounter/update-tcm-encounter.ts (1)
38-44
: Use optimistic-locking (eTag
) and update timestampAfter
update()
,updatedEncounter
is fetched again but the code does not bumpupdatedAt
nor rotateeTag
, defeating cache invalidation / concurrency control. Consider using a model hook or include:await encounter.update({ ...data, updatedAt: buildDayjs(), eTag: uuidv4() });packages/api/src/routes/medical/__tests__/tcm-encounter.test.ts (1)
80-104
: Hard-codedSymbol.for("gt")
risks false positives
expectedQuery.where.admitTime
usesSymbol.for("gt")
directly.
If the implementation later switches toSequelize.Op.gt
(a different symbol instance in some dialects/tests) the expectation will silently pass even though the call shape changed.Prefer asserting on business semantics instead of raw symbols, e.g.:
expect(TcmEncounterModel.findAndCountAll).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ admitTime: expect.any(Object), }), }), );Or inject
Op
from Sequelize into the test.packages/api/src/routes/medical/schemas/tcm-encounter.ts (2)
47-55
: DuplicatecxId
definition in response schema
cxId
is already present intcmEncounterCreateSchema
; redefining it in.extend()
is redundant and may cause maintenance churn if the type ever changes.-export const tcmEncounterResponseSchema = tcmEncounterCreateSchema - .extend({ - id: z.string().uuid(), - cxId: z.string().uuid(), // duplicate +export const tcmEncounterResponseSchema = tcmEncounterCreateSchema + .extend({ + id: z.string().uuid(),
11-20
: Consider delaying ISO-string → Date conversionTransforming strings to
Date
at the API boundary mixes transport-layer and runtime representations.
Downstream layers that need the raw ISO string (e.g., logging, caching) must re-serialize.
You can keep the schema asz.string().datetime()
and perform conversion deeper in the domain/DB layer where aDate
is truly required.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
packages/api/src/command/medical/tcm-encounter/create-tcm-encounter.ts
(1 hunks)packages/api/src/command/medical/tcm-encounter/list-tcm-encounters.ts
(1 hunks)packages/api/src/command/medical/tcm-encounter/update-tcm-encounter.ts
(1 hunks)packages/api/src/domain/medical/__tests__/tcm-encounter.ts
(1 hunks)packages/api/src/domain/medical/tcm-encounter.ts
(1 hunks)packages/api/src/models/db.ts
(2 hunks)packages/api/src/models/medical/__tests__/tcm-encounter.ts
(1 hunks)packages/api/src/models/medical/tcm-encounter.ts
(1 hunks)packages/api/src/routes/index.ts
(2 hunks)packages/api/src/routes/internal/__tests__/tcm-encounter.test.ts
(1 hunks)packages/api/src/routes/internal/index.ts
(2 hunks)packages/api/src/routes/internal/medical/tcm-encounter.ts
(1 hunks)packages/api/src/routes/medical/__tests__/tcm-encounter.test.ts
(1 hunks)packages/api/src/routes/medical/dash.ts
(1 hunks)packages/api/src/routes/medical/index.ts
(1 hunks)packages/api/src/routes/medical/schemas/tcm-encounter.ts
(1 hunks)packages/api/src/routes/medical/tcm-encounter.ts
(1 hunks)packages/api/src/sequelize/migrations/2025-06-17_00_create-tcm-encounter.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
to other types- Type predicates: only applicable to narrow down the type, not to force a complete type conversion
- Prefer deconstructing parameters for functions instead of multiple parameters that might be of
the same type- Don’t use
null
inside the app, only on code interacting with external interfaces/services,
like DB and HTTP; convert toundefined
before sending inwards into the code- Use
async/await
instead of.then()
- Use the strict equality operator
===
, don’t use abstract equality operator==
- When calling a Promise-returning function asynchronously (i.e., not awaiting), use
.catch()
to
handle errors (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
- Avoid creating arrow functions
- Use truthy syntax instead of
in
- i.e.,if (data.link)
notif ('link' in data)
- Error handling
- Pass the original error as the new one’s
cause
so the stack trace is persisted- Error messages should have a static message - add dynamic data to MetriportError's
additionalInfo
prop- Avoid sending multiple events to Sentry for a single error
- Global constants and variables
- Move literals to constants declared after imports when possible (avoid magic numbers)
- Avoid shared, global objects
- Avoid using
console.log
andconsole.error
in packages other than utils, infra and shared,
and try to useout().log
instead- Avoid multi-line logs
- don't send objects as a second parameter to
console.log()
orout().log()
- don't create multi-line strings when using
JSON.stringify()
- Use
eslint
to enforce code style- Use
prettier
to format code- max column length is 100 chars
- multi-line comments use
/** */
- scripts: top-level comments go after the import
packages/api/src/routes/medical/index.ts
packages/api/src/models/db.ts
packages/api/src/routes/internal/index.ts
packages/api/src/routes/index.ts
packages/api/src/routes/medical/dash.ts
packages/api/src/models/medical/__tests__/tcm-encounter.ts
packages/api/src/routes/internal/__tests__/tcm-encounter.test.ts
packages/api/src/domain/medical/__tests__/tcm-encounter.ts
packages/api/src/command/medical/tcm-encounter/create-tcm-encounter.ts
packages/api/src/routes/medical/__tests__/tcm-encounter.test.ts
packages/api/src/command/medical/tcm-encounter/update-tcm-encounter.ts
packages/api/src/sequelize/migrations/2025-06-17_00_create-tcm-encounter.ts
packages/api/src/command/medical/tcm-encounter/list-tcm-encounters.ts
packages/api/src/models/medical/tcm-encounter.ts
packages/api/src/domain/medical/tcm-encounter.ts
packages/api/src/routes/medical/schemas/tcm-encounter.ts
packages/api/src/routes/medical/tcm-encounter.ts
packages/api/src/routes/internal/medical/tcm-encounter.ts
🧠 Learnings (1)
packages/api/src/routes/medical/index.ts (1)
Learnt from: leite08
PR: metriport/metriport#3814
File: packages/api/src/routes/internal/medical/patient-consolidated.ts:141-174
Timestamp: 2025-05-20T21:26:26.804Z
Learning: The functionality introduced in packages/api/src/routes/internal/medical/patient-consolidated.ts is planned to be refactored in downstream PR #3857, including improvements to error handling and validation.
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: check-pr / lint-build-test
- GitHub Check: Analyze (javascript)
🔇 Additional comments (5)
packages/api/src/models/db.ts (1)
44-45
: Addassociate
hook (if present) forTcmEncounterModel
.
PatientSettingsModel.associate
is wired up explicitly a few lines below (146-148).
IfTcmEncounterModel
defines its own staticassociate
method (very common for Sequelize models that referenceFacility
,Patient
, etc.), it also needs to be invoked after all models are registered, otherwise FK constraints orinclude
queries will silently fail at runtime.PatientJobModel.setup, TcmEncounterModel.setup, ]; // … PatientJobModel.setup(sequelize); TcmEncounterModel.setup(sequelize); + // wire up associations + if ('associate' in TcmEncounterModel) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore – generic signature difference is acceptable here + TcmEncounterModel.associate(sequelize.models); + }(Adjust call signature to match your convention.)
Failing to wire associations now will only surface once the first join is executed.packages/api/src/routes/medical/index.ts (1)
2-9
: Import shuffle only – no action required.The change merely re-orders existing imports; runtime behaviour is unchanged.
packages/api/src/routes/internal/index.ts (2)
56-57
: Confirm internal route is intentionally singular.The public router mounts
/tcm/encounter
, and the internal router now mirrors that path.
Double-check naming conventions – other collections are plural (/patient
,/facility
). If we expect multiple rows,/tcm/encounters
may be clearer.
73-74
: Auth / RBAC check for new endpoint.Most internal routes rely on API-GW protection only. If
tcmEncounter
exposes any destructive operations (e.g. DELETE) ensure it performs its own RBAC / cxId validation internally just like sibling routes.packages/api/src/command/medical/tcm-encounter/list-tcm-encounters.ts (1)
35-40
: Pagination lacks cursor/offset –limit
alone won’t page past first slice
limit = pagination.count + 1
is good for “hasNextPage” detection, but withoutoffset
,where admitTime < lastSeen
or a proper cursor you always fetch the first window. Verify the API contract; if the UI expects infinite scroll you’ll need either:where: { …, admitTime: { [Op.lt]: cursorDate } }or
offset: pagination.skip
.
TcmEncounterModel.setup, | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Read-only replica missing the new model.
modelsReadOnly
only contains PatientModelReadOnly
.
If TCM encounters are queried on the read replica (dash pages often list them), add a view-only model analogue or include the full model in modelsReadOnly
.
🤖 Prompt for AI Agents
In packages/api/src/models/db.ts around lines 75 to 76, the modelsReadOnly array
currently only includes PatientModelReadOnly and is missing the
TcmEncounterModel or its read-only equivalent. To fix this, add a view-only
model for TcmEncounter or include the full TcmEncounterModel in the
modelsReadOnly array to ensure read-only replica queries for TCM encounters work
correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@leite08 is this necessary? Can i just do this against the RW instance for now? I'm still surprised we don't just pass all models to two separate clients, and then use the appropriate client in each command around the app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
const encounter = makeTcmEncounter(params) as unknown as TcmEncounterModel; | ||
encounter.dataValues = encounter; | ||
encounter.save = jest.fn(); | ||
encounter.update = jest.fn(); | ||
encounter.destroy = jest.fn(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Mocked persistence methods should mirror Sequelize’s async contract
save
, update
, and destroy
are async in Sequelize – production code may rely on awaiting them.
Returning plain jest.fn()
(which resolves to undefined
) risks await
-chains or .then()
calls exploding during tests.
- encounter.save = jest.fn();
- encounter.update = jest.fn();
- encounter.destroy = jest.fn();
+ // Each method resolves to the same mocked instance to emulate Sequelize’s behaviour
+ encounter.save = jest.fn().mockResolvedValue(encounter);
+ encounter.update = jest.fn().mockResolvedValue(encounter);
+ encounter.destroy = jest.fn().mockResolvedValue(encounter);
This tiny tweak keeps the helper drop-in compatible with real models.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const encounter = makeTcmEncounter(params) as unknown as TcmEncounterModel; | |
encounter.dataValues = encounter; | |
encounter.save = jest.fn(); | |
encounter.update = jest.fn(); | |
encounter.destroy = jest.fn(); | |
const encounter = makeTcmEncounter(params) as unknown as TcmEncounterModel; | |
encounter.dataValues = encounter; | |
+ // Each method resolves to the same mocked instance to emulate Sequelize’s behaviour | |
encounter.save = jest.fn().mockResolvedValue(encounter); | |
encounter.update = jest.fn().mockResolvedValue(encounter); | |
encounter.destroy = jest.fn().mockResolvedValue(encounter); |
🤖 Prompt for AI Agents
In packages/api/src/models/medical/__tests__/tcm-encounter.ts around lines 5 to
9, the mocked persistence methods save, update, and destroy are currently
synchronous jest.fn() mocks, but Sequelize's real methods are async and return
promises. To fix this, update these mocks to return resolved promises by using
jest.fn().mockResolvedValue(undefined) so that any await or then calls in the
production code work correctly during tests.
clinicalInformation: { | ||
type: DataTypes.JSONB, | ||
field: "clinical_information", | ||
allowNull: false, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing e_tag
column – breaks domain/model contract
TcmEncounter
domain object contains an eTag
.
createTcmEncounter
command (not shown) reportedly sets this field, yet the table schema omits it, leading to either silent data loss or runtime errors when Model.create()
receives an unknown column.
clinicalInformation: {
type: DataTypes.JSONB,
field: "clinical_information",
allowNull: false,
},
+ eTag: {
+ type: DataTypes.UUID,
+ field: "e_tag",
+ allowNull: false,
+ },
If the helper shared.createTable(..., { addVersion: true })
is already injecting an e_tag
column, please ignore – otherwise, add it to keep persistence in sync.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
clinicalInformation: { | |
type: DataTypes.JSONB, | |
field: "clinical_information", | |
allowNull: false, | |
}, | |
clinicalInformation: { | |
type: DataTypes.JSONB, | |
field: "clinical_information", | |
allowNull: false, | |
}, | |
eTag: { | |
type: DataTypes.UUID, | |
field: "e_tag", | |
allowNull: false, | |
}, |
🤖 Prompt for AI Agents
In packages/api/src/sequelize/migrations/2025-06-17_00_create-tcm-encounter.ts
around lines 54 to 58, the migration is missing the e_tag column required by the
TcmEncounter domain model. Verify if the shared.createTable helper with
addVersion: true option is adding the e_tag column automatically; if not,
explicitly add an e_tag column of appropriate type to the table schema to ensure
the database matches the domain model and prevent data loss or runtime errors
during Model.create().
const now = new Date(); | ||
const encounter = await TcmEncounterModel.create({ | ||
...data, | ||
id: uuidv4(), | ||
createdAt: now, | ||
updatedAt: now, | ||
eTag: uuidv4(), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Create timestamps & IDs via BaseModel + buildDayjs() for guideline compliance
– The coding-guidelines mandate creating dates with buildDayjs()
rather than new Date()
.
– BaseModel.attributes()
already wires default id
, createdAt
, updatedAt
, and probably eTag
; supplying them manually risks divergence from the central logic (e.g., database default values, hooks, optimistic-lock versioning).
- const now = new Date();
- const encounter = await TcmEncounterModel.create({
- ...data,
- id: uuidv4(),
- createdAt: now,
- updatedAt: now,
- eTag: uuidv4(),
- });
+ const encounter = await TcmEncounterModel.create({
+ ...data,
+ // let BaseModel generate id / timestamps / eTag
+ });
Besides reducing duplication this keeps all timestamp handling consistent across models.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const now = new Date(); | |
const encounter = await TcmEncounterModel.create({ | |
...data, | |
id: uuidv4(), | |
createdAt: now, | |
updatedAt: now, | |
eTag: uuidv4(), | |
}); | |
const encounter = await TcmEncounterModel.create({ | |
...data, | |
// let BaseModel generate id / timestamps / eTag | |
}); |
🤖 Prompt for AI Agents
In packages/api/src/command/medical/tcm-encounter/create-tcm-encounter.ts around
lines 17 to 24, replace the manual creation of timestamps and IDs using new
Date() and uuidv4() with the centralized logic provided by BaseModel and
buildDayjs(). Remove the explicit setting of id, createdAt, updatedAt, and eTag
in the create call to avoid duplication and ensure consistency with the model's
default attributes and hooks. Use buildDayjs() for any date handling to comply
with coding guidelines.
cxId: { | ||
type: DataTypes.UUID, | ||
}, | ||
patientId: { | ||
type: DataTypes.UUID, | ||
}, | ||
facilityName: { | ||
type: DataTypes.STRING, | ||
}, | ||
latestEvent: { | ||
type: DataTypes.ENUM("Admitted", "Transferred", "Discharged"), | ||
}, | ||
class: { | ||
type: DataTypes.STRING, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add allowNull: false
(and constraints) to mandatory columns
cxId
, patientId
, facilityName
, latestEvent
, and class
are semantically required, but the column definitions omit allowNull: false
. This allows NULL
rows, contradicting the domain contract and complicating validation at higher layers.
- cxId: {
- type: DataTypes.UUID,
- },
+ cxId: {
+ type: DataTypes.UUID,
+ allowNull: false,
+ },
# …repeat for the other mandatory columns…
Also consider validate: { notEmpty: true }
for strings to prevent empty-string edge cases.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
cxId: { | |
type: DataTypes.UUID, | |
}, | |
patientId: { | |
type: DataTypes.UUID, | |
}, | |
facilityName: { | |
type: DataTypes.STRING, | |
}, | |
latestEvent: { | |
type: DataTypes.ENUM("Admitted", "Transferred", "Discharged"), | |
}, | |
class: { | |
type: DataTypes.STRING, | |
}, | |
cxId: { | |
type: DataTypes.UUID, | |
allowNull: false, | |
}, | |
patientId: { | |
type: DataTypes.UUID, | |
allowNull: false, | |
}, | |
facilityName: { | |
type: DataTypes.STRING, | |
allowNull: false, | |
}, | |
latestEvent: { | |
type: DataTypes.ENUM("Admitted", "Transferred", "Discharged"), | |
allowNull: false, | |
}, | |
class: { | |
type: DataTypes.STRING, | |
allowNull: false, | |
}, |
🤖 Prompt for AI Agents
In packages/api/src/models/medical/tcm-encounter.ts between lines 22 and 36, the
columns cxId, patientId, facilityName, latestEvent, and class are missing
allowNull: false, which is necessary to enforce that these fields cannot be null
in the database. Update each of these column definitions to include allowNull:
false. Additionally, for string fields like facilityName and class, add
validate: { notEmpty: true } to prevent empty strings from being saved. This
will ensure the database schema enforces the mandatory nature of these fields
and improves data integrity.
const encounter = await TcmEncounterModel.findByPk(id); | ||
if (!encounter) { | ||
return { | ||
encounter: null, | ||
status: 404, | ||
message: `TCM encounter with ID ${id} not found`, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential PII leakage – record fetched without cxId
constraint
findByPk(id)
may return an encounter belonging to a different customer. Even though the subsequent update
is guarded by cxId
, the initial read (and the 404 decision) exposes existence/metadata across tenants.
- const encounter = await TcmEncounterModel.findByPk(id);
+ const encounter = await TcmEncounterModel.findOne({ where: { id, cxId } });
Return 404 when not found to maintain tenant isolation.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const encounter = await TcmEncounterModel.findByPk(id); | |
if (!encounter) { | |
return { | |
encounter: null, | |
status: 404, | |
message: `TCM encounter with ID ${id} not found`, | |
}; | |
} | |
// Ensure we only fetch an encounter belonging to the current customer | |
const encounter = await TcmEncounterModel.findOne({ | |
where: { id, cxId }, | |
}); | |
if (!encounter) { | |
return { | |
encounter: null, | |
status: 404, | |
message: `TCM encounter with ID ${id} not found`, | |
}; | |
} |
🤖 Prompt for AI Agents
In packages/api/src/command/medical/tcm-encounter/update-tcm-encounter.ts around
lines 22 to 29, the code fetches the encounter by primary key without filtering
by cxId, which risks exposing data across tenants. Modify the query to include a
condition that matches both the id and the cxId to ensure tenant isolation. This
way, the 404 response will correctly reflect whether the encounter exists for
the specific customer, preventing potential PII leakage.
await TcmEncounterModel.update(data, { | ||
where: { | ||
id, | ||
cxId, | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Allowing cxId
updates breaks data integrity
TcmEncounterUpdateData
is Partial<Omit<TcmEncounter,"id">>
– this lets callers modify cxId
, which would orphan the row from its owner. Exclude immutable/ownership fields:
-type TcmEncounterUpdateData = Partial<Omit<TcmEncounter, "id">>;
+type TcmEncounterUpdateData = Partial<
+ Omit<TcmEncounter, "id" | "cxId" | "createdAt" | "eTag">
+>;
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
await TcmEncounterModel.update(data, { | |
where: { | |
id, | |
cxId, | |
}, | |
}); | |
- type TcmEncounterUpdateData = Partial<Omit<TcmEncounter, "id">>; | |
+ type TcmEncounterUpdateData = Partial< | |
+ Omit<TcmEncounter, "id" | "cxId" | "createdAt" | "eTag"> | |
+ >; |
🤖 Prompt for AI Agents
In packages/api/src/command/medical/tcm-encounter/update-tcm-encounter.ts around
lines 31 to 36, the update operation allows modifying the cxId field, which
should be immutable to maintain data integrity. To fix this, exclude cxId from
the update data before calling TcmEncounterModel.update, ensuring that cxId
remains unchanged and the row stays correctly linked to its owner.
packages/api/src/routes/medical/__tests__/tcm-encounter.test.ts
Outdated
Show resolved
Hide resolved
const result = await paginated({ | ||
request: req, | ||
additionalQueryParams: undefined, | ||
getItems: async pagination => { | ||
const { items } = await listTcmEncounters({ | ||
cxId, | ||
after: query.after, | ||
pagination, | ||
}); | ||
return items; | ||
}, | ||
getTotalCount: async () => { | ||
const { totalCount } = await listTcmEncounters({ | ||
cxId, | ||
after: query.after, | ||
pagination: { count: 1, fromItem: undefined, toItem: undefined }, | ||
}); | ||
return totalCount; | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Inline arrow functions violate house style & duplicate DB work
- Coding-guidelines advise avoiding arrow functions; define small named helpers instead.
listTcmEncounters
is invoked twice, doubling latency. Because the command already returns{ items, totalCount }
, call it once and derive both pieces.
- const result = await paginated({
- request: req,
- additionalQueryParams: undefined,
- getItems: async pagination => {
- const { items } = await listTcmEncounters({ cxId, after: query.after, pagination });
- return items;
- },
- getTotalCount: async () => {
- const { totalCount } = await listTcmEncounters({
- cxId,
- after: query.after,
- pagination: { count: 1, fromItem: undefined, toItem: undefined },
- });
- return totalCount;
- },
- });
+ const result = await paginated({
+ request: req,
+ getItemsAndTotal: async pagination =>
+ listTcmEncounters({ cxId, after: query.after, pagination }),
+ });
This both respects the style guide and halves the query load.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/api/src/routes/medical/tcm-encounter.ts around lines 57 to 76, the
code uses inline arrow functions for getItems and getTotalCount, which violates
the house style that prefers named helper functions. Additionally,
listTcmEncounters is called twice, causing redundant database queries and
increased latency. To fix this, define a single named async helper function that
calls listTcmEncounters once, retrieves both items and totalCount, and then use
this helper to provide the results needed for pagination, eliminating duplicate
calls and adhering to the style guide.
patientId: string; | ||
facilityName: string; | ||
latestEvent: TcmEncounterLatestEvent; | ||
class: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a TSDoc for this prop, it's not clear what it means.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -0,0 +1,14 @@ | |||
import { BaseDomain } from "@metriport/core/domain/base-domain"; | |||
|
|||
export type TcmEncounterLatestEvent = "Admitted" | "Transferred" | "Discharged"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this represent TcmEncounterEvent, that can be used on the latest column but also to represent other non-lastest events?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep good point, renamed
packages/api/src/command/medical/tcm-encounter/update-tcm-encounter.ts
Outdated
Show resolved
Hide resolved
latestEvent: TcmEncounterLatestEvent; | ||
class: string; | ||
admitTime?: Date | undefined; | ||
dischargeTime?: Date | null | undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No null
inside the code, please, only at the "edges"/interfaces when needed to by said interface (e.g., HTTP or DB), and convert it to undefined
before it make it's way into our code - ref
@coderabbitai add a learning about this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@leite08, understood! I'll make note of this guideline about avoiding null
in internal code and only using it at interface boundaries.
✏️ Learnings added
Learnt from: leite08
PR: metriport/metriport#4044
File: packages/api/src/command/medical/tcm-encounter/create-tcm-encounter.ts:12-12
Timestamp: 2025-06-18T04:50:32.441Z
Learning: Do not use `null` inside the application code. Only use `null` at "edges"/interfaces when required by external interfaces (e.g., HTTP or DB). Convert `null` to `undefined` before it makes its way into the internal codebase. This applies to TypeScript type definitions and function parameters - prefer `undefined` over `null` for optional values in internal code.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
id: uuidv4(), | ||
createdAt: now, | ||
updatedAt: now, | ||
eTag: uuidv4(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't set this upon creation - this is automatically calculated based on the version. See BaseModel.attributes()
export type TcmEncounterCreate = z.infer<typeof tcmEncounterCreateSchema>; | ||
export type TcmEncounterCreateInput = z.input<typeof tcmEncounterCreateSchema>; | ||
export type TcmEncounterUpdate = z.infer<typeof tcmEncounterUpdateSchema>; | ||
export type TcmEncounterResponse = z.infer<typeof tcmEncounterResponseSchema>; | ||
export type TcmEncounterListQuery = z.infer<typeof tcmEncounterListQuerySchema>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest we keep the type close to the schema, like we've been doing everywhere else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
router.put("/:id", requestLogger, asyncHandler(handleUpdateTcmEncounter)); | ||
router.get("/", requestLogger, asyncHandler(handleListTcmEncounters)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto single route definition block, unless needed to reuse it like a deprecated route like POST /patient/:id/consolidated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can do
} catch (error) { | ||
if (error instanceof NotFoundError) { | ||
return res.status(httpStatus.NOT_FOUND).json({ message: error.message }); | ||
} | ||
throw error; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sure we don't need this and the default handler already takes care of this - can you check please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
getItems: async pagination => { | ||
const { items } = await listTcmEncounters({ | ||
cxId, | ||
after: query.after, | ||
pagination, | ||
}); | ||
return items; | ||
}, | ||
getTotalCount: async () => { | ||
const { totalCount } = await listTcmEncounters({ | ||
cxId, | ||
after: query.after, | ||
pagination: { count: 1, fromItem: undefined, toItem: undefined }, | ||
}); | ||
return totalCount; | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we listing the entries twice, and in the list we issue two queries to the DB (one for entries and one for count)? With this we're issuing 4 queries to the DB, aren't we?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
}, | ||
admitTime: { | ||
type: DataTypes.DATE, | ||
allowNull: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these (allowNull
) we can leave to the migration only. We only need types here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we get the nullability right on the model as well we end up getting better type inference, right? You're right though, columns are nullable by default. I updated some columns to have allowNull:false instead
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (3)
packages/api/src/command/medical/tcm-encounter/list-tcm-encounters.ts (3)
7-7
: Address the context for the fixed date as previously requested.This fixed date still lacks explanation. Please provide context for why encounters are filtered from 2020-01-01 onwards.
11-11
: Improve type safety for the after parameter.The
after
parameter should be typed asDate
instead ofstring
for better type safety, as previously suggested.export type ListTcmEncountersCmd = { cxId: string; - after?: string; + after?: Date; pagination: Pagination; };
27-39
: Simplify the where clause logic and use buildDayjs().The current where clause logic is unnecessarily complex. Please implement the simpler approach suggested in previous reviews and use
buildDayjs()
for consistent timezone handling.+import { buildDayjs } from "../../../shared/date"; -const DEFAULT_FILTER_DATE = new Date("2020-01-01T00:00:00.000Z"); +const DEFAULT_FILTER_DATE = buildDayjs("2020-01-01T00:00:00.000Z").toDate(); - const where: Record<string, unknown> = { - cxId, - admitTime: { - [Op.gt]: DEFAULT_FILTER_DATE, - }, - }; - - if (after) { - where.admitTime = { - ...(where.admitTime as Record<string, unknown>), - [Op.gt]: new Date(after), - }; - } + const admitTimeFilter = after ?? DEFAULT_FILTER_DATE; + const where: WhereOptions<TcmEncounterModel> = { + cxId, + admitTime: { + [Op.gt]: admitTimeFilter, + }, + };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
packages/api/src/command/medical/tcm-encounter/list-tcm-encounters.ts
(1 hunks)packages/api/src/models/db.ts
(3 hunks)packages/api/src/models/medical/organization.ts
(2 hunks)packages/api/src/models/medical/patient.ts
(2 hunks)packages/api/src/models/medical/tcm-encounter.ts
(1 hunks)packages/api/src/routes/medical/schemas/tcm-encounter.ts
(1 hunks)packages/api/src/sequelize/migrations/2025-06-17_00_create-tcm-encounter.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/api/src/sequelize/migrations/2025-06-17_00_create-tcm-encounter.ts
- packages/api/src/models/db.ts
- packages/api/src/routes/medical/schemas/tcm-encounter.ts
- packages/api/src/models/medical/tcm-encounter.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
to other types- Type predicates: only applicable to narrow down the type, not to force a complete type conversion
- Prefer deconstructing parameters for functions instead of multiple parameters that might be of
the same type- Don’t use
null
inside the app, only on code interacting with external interfaces/services,
like DB and HTTP; convert toundefined
before sending inwards into the code- Use
async/await
instead of.then()
- Use the strict equality operator
===
, don’t use abstract equality operator==
- When calling a Promise-returning function asynchronously (i.e., not awaiting), use
.catch()
to
handle errors (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
- Avoid creating arrow functions
- Use truthy syntax instead of
in
- i.e.,if (data.link)
notif ('link' in data)
- Error handling
- Pass the original error as the new one’s
cause
so the stack trace is persisted- Error messages should have a static message - add dynamic data to MetriportError's
additionalInfo
prop- Avoid sending multiple events to Sentry for a single error
- Global constants and variables
- Move literals to constants declared after imports when possible (avoid magic numbers)
- Avoid shared, global objects
- Avoid using
console.log
andconsole.error
in packages other than utils, infra and shared,
and try to useout().log
instead- Avoid multi-line logs
- don't send objects as a second parameter to
console.log()
orout().log()
- don't create multi-line strings when using
JSON.stringify()
- Use
eslint
to enforce code style- Use
prettier
to format code- max column length is 100 chars
- multi-line comments use
/** */
- scripts: top-level comments go after the import
packages/api/src/models/medical/patient.ts
packages/api/src/models/medical/organization.ts
packages/api/src/command/medical/tcm-encounter/list-tcm-encounters.ts
🪛 Biome (1.9.4)
packages/api/src/command/medical/tcm-encounter/list-tcm-encounters.ts
[error] 59-59: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: check-pr / lint-build-test
- GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
packages/api/src/models/medical/patient.ts (1)
5-5
: LGTM! Association follows Sequelize best practices.The import and association setup correctly establishes the one-to-many relationship between patients and TCM encounters using standard Sequelize patterns.
Also applies to: 20-25
packages/api/src/models/medical/organization.ts (1)
8-8
: LGTM! Association correctly uses cxId for organization linkage.The association properly establishes the one-to-many relationship between organizations and TCM encounters using cxId, which is consistent with the organization model's design.
Also applies to: 71-76
packages/api/src/command/medical/tcm-encounter/list-tcm-encounters.ts
Outdated
Show resolved
Hide resolved
const items = rows.map(row => { | ||
const patient = (row.get("PatientModel") as PatientModel).dataValues.data; | ||
|
||
/** Hack to get around Sequelize type inference not seeing associations */ | ||
const encounterData = { ...row.dataValues, PatientModel: undefined }; | ||
delete encounterData.PatientModel; | ||
|
||
return { | ||
...encounterData, | ||
patientName: patient.firstName + " " + patient.lastName, | ||
patientDateOfBirth: patient.dob, | ||
patientPhoneNumbers: patient.contact?.map(contact => contact.phone), | ||
patientStates: patient.address.map(address => address.state), | ||
}; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Improve data access safety and avoid delete operation.
The current mapping has several issues:
- Use of
delete
operator (flagged by static analysis) - Potential null safety issues with patient data access
- Complex data extraction logic
const items = rows.map(row => {
- const patient = (row.get("PatientModel") as PatientModel).dataValues.data;
+ const patientModel = row.get("PatientModel") as PatientModel;
+ const patient = patientModel?.dataValues?.data;
+
+ if (!patient) {
+ throw new Error("Patient data not found for TCM encounter");
+ }
- /** Hack to get around Sequelize type inference not seeing associations */
- const encounterData = { ...row.dataValues, PatientModel: undefined };
- delete encounterData.PatientModel;
+ const { PatientModel: _, ...encounterData } = row.dataValues;
return {
...encounterData,
patientName: patient.firstName + " " + patient.lastName,
patientDateOfBirth: patient.dob,
- patientPhoneNumbers: patient.contact?.map(contact => contact.phone),
- patientStates: patient.address.map(address => address.state),
+ patientPhoneNumbers: patient.contact?.map(contact => contact.phone) ?? [],
+ patientStates: patient.address?.map(address => address.state) ?? [],
};
});
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const items = rows.map(row => { | |
const patient = (row.get("PatientModel") as PatientModel).dataValues.data; | |
/** Hack to get around Sequelize type inference not seeing associations */ | |
const encounterData = { ...row.dataValues, PatientModel: undefined }; | |
delete encounterData.PatientModel; | |
return { | |
...encounterData, | |
patientName: patient.firstName + " " + patient.lastName, | |
patientDateOfBirth: patient.dob, | |
patientPhoneNumbers: patient.contact?.map(contact => contact.phone), | |
patientStates: patient.address.map(address => address.state), | |
}; | |
}); | |
const items = rows.map(row => { | |
- const patient = (row.get("PatientModel") as PatientModel).dataValues.data; | |
+ const patientModel = row.get("PatientModel") as PatientModel; | |
+ const patient = patientModel?.dataValues?.data; | |
+ | |
+ if (!patient) { | |
+ throw new Error("Patient data not found for TCM encounter"); | |
+ } | |
- /** Hack to get around Sequelize type inference not seeing associations */ | |
- const encounterData = { ...row.dataValues, PatientModel: undefined }; | |
- delete encounterData.PatientModel; | |
+ const { PatientModel: _, ...encounterData } = row.dataValues; | |
return { | |
...encounterData, | |
patientName: patient.firstName + " " + patient.lastName, | |
patientDateOfBirth: patient.dob, | |
- patientPhoneNumbers: patient.contact?.map(contact => contact.phone), | |
- patientStates: patient.address.map(address => address.state), | |
+ patientPhoneNumbers: patient.contact?.map(contact => contact.phone) ?? [], | |
+ patientStates: patient.address?.map(address => address.state) ?? [], | |
}; | |
}); |
🧰 Tools
🪛 Biome (1.9.4)
[error] 59-59: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
🤖 Prompt for AI Agents
In packages/api/src/command/medical/tcm-encounter/list-tcm-encounters.ts between
lines 54 and 68, avoid using the delete operator on encounterData by creating a
new object that excludes PatientModel directly. Add null checks for patient and
its nested properties like contact and address to prevent runtime errors.
Simplify data extraction by safely accessing patient fields and mapping over
arrays only if they exist, ensuring robust and clean data handling without
mutating original objects.
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
d13cad8
to
5aa6bf8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (1)
packages/api/src/command/medical/tcm-encounter/get-tcm-encounters.ts (1)
31-36
: Extract shared where clause logic to reduce duplication.The where clause building logic is duplicated between
getTcmEncounters
andgetTcmEncountersCount
functions.Extract a helper function:
+function buildTcmEncounterWhereClause(cxId: string, after?: string): WhereOptions<TcmEncounterModel> { + return { + cxId, + admitTime: { + [Op.gt]: after ? buildDayjs(after).toDate() : DEFAULT_FILTER_DATE, + }, + }; +} export async function getTcmEncounters({ cxId, after, pagination }: GetTcmEncountersCmd): Promise<{ items: TcmEncounterForDisplay[]; }> { - const where: WhereOptions<TcmEncounterModel> = { - cxId, - admitTime: { - [Op.gt]: after ? buildDayjs(after).toDate() : DEFAULT_FILTER_DATE, - }, - }; + const where = buildTcmEncounterWhereClause(cxId, after);Also applies to: 75-80
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
packages/api/src/command/medical/tcm-encounter/get-tcm-encounters.ts
(1 hunks)packages/api/src/routes/medical/__tests__/tcm-encounter.test.ts
(1 hunks)packages/api/src/routes/medical/tcm-encounter.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/api/src/routes/medical/tests/tcm-encounter.test.ts
- packages/api/src/routes/medical/tcm-encounter.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: - Use the Onion Pattern to organize a package's code in layers - Try to use immutable code and avoid sharing state across different functions, objects, and systems - Try...
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
to other types- Type predicates: only applicable to narrow down the type, not to force a complete type conversion
- Prefer deconstructing parameters for functions instead of multiple parameters that might be of
the same type- Don’t use
null
inside the app, only on code interacting with external interfaces/services,
like DB and HTTP; convert toundefined
before sending inwards into the code- Use
async/await
instead of.then()
- Use the strict equality operator
===
, don’t use abstract equality operator==
- When calling a Promise-returning function asynchronously (i.e., not awaiting), use
.catch()
to
handle errors (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
- Avoid creating arrow functions
- Use truthy syntax instead of
in
- i.e.,if (data.link)
notif ('link' in data)
- Error handling
- Pass the original error as the new one’s
cause
so the stack trace is persisted- Error messages should have a static message - add dynamic data to MetriportError's
additionalInfo
prop- Avoid sending multiple events to Sentry for a single error
- Global constants and variables
- Move literals to constants declared after imports when possible (avoid magic numbers)
- Avoid shared, global objects
- Avoid using
console.log
andconsole.error
in packages other than utils, infra and shared,
and try to useout().log
instead- Avoid multi-line logs
- don't send objects as a second parameter to
console.log()
orout().log()
- don't create multi-line strings when using
JSON.stringify()
- Use
eslint
to enforce code style- Use
prettier
to format code- max column length is 100 chars
- multi-line comments use
/** */
- scripts: top-level comments go after the import
packages/api/src/command/medical/tcm-encounter/get-tcm-encounters.ts
🪛 Biome (1.9.4)
packages/api/src/command/medical/tcm-encounter/get-tcm-encounters.ts
[error] 56-56: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: check-pr / lint-build-test
- GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
packages/api/src/command/medical/tcm-encounter/get-tcm-encounters.ts (2)
1-11
: LGTM! Clean imports and well-documented constant.The imports are appropriate and the
DEFAULT_FILTER_DATE
constant with its explanatory comment clearly indicates the database optimization intent.
13-26
: LGTM! Well-structured type definitions.The command types and display type extension are clean and follow TypeScript best practices.
export async function getTcmEncounters({ cxId, after, pagination }: GetTcmEncountersCmd): Promise<{ | ||
items: TcmEncounterForDisplay[]; | ||
}> { | ||
const where: WhereOptions<TcmEncounterModel> = { | ||
cxId, | ||
admitTime: { | ||
[Op.gt]: after ? buildDayjs(after).toDate() : DEFAULT_FILTER_DATE, | ||
}, | ||
}; | ||
|
||
const rows = await TcmEncounterModel.findAll({ | ||
where, | ||
include: [ | ||
{ | ||
model: PatientModel, | ||
as: "PatientModel", | ||
attributes: ["id", "cxId", "data"], | ||
}, | ||
], | ||
limit: pagination.count + 1, // Get one extra to determine if there's a next page | ||
order: [["admitTime", "DESC"]], | ||
}); | ||
|
||
const items = rows.map((row: TcmEncounterModel) => { | ||
const patient = (row.get("PatientModel") as PatientModel).dataValues.data; | ||
|
||
/** Hack to get around Sequelize type inference not seeing associations */ | ||
const encounterData = { ...row.dataValues, PatientModel: undefined }; | ||
delete encounterData.PatientModel; | ||
|
||
return { | ||
...encounterData, | ||
patientName: patient.firstName + " " + patient.lastName, | ||
patientDateOfBirth: patient.dob, | ||
patientPhoneNumbers: patient.contact?.map(contact => contact.phone) ?? [], | ||
patientStates: patient.address?.map(address => address.state) ?? [], | ||
}; | ||
}); | ||
|
||
return { | ||
items, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add error handling for database operations.
Both functions lack error handling for database operations, which could lead to unhandled promise rejections.
Consider adding try-catch blocks or let the errors bubble up with proper context:
export async function getTcmEncounters({ cxId, after, pagination }: GetTcmEncountersCmd): Promise<{
items: TcmEncounterForDisplay[];
}> {
+ try {
const where = buildTcmEncounterWhereClause(cxId, after);
const rows = await TcmEncounterModel.findAll({
// ... existing code
});
// ... rest of function
+ } catch (error) {
+ throw new Error(`Failed to get TCM encounters for cxId ${cxId}`, { cause: error });
+ }
}
Also applies to: 72-89
🧰 Tools
🪛 Biome (1.9.4)
[error] 56-56: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
🤖 Prompt for AI Agents
In packages/api/src/command/medical/tcm-encounter/get-tcm-encounters.ts around
lines 28 to 70, the database query and data processing lack error handling,
risking unhandled promise rejections. Wrap the entire function body in a
try-catch block to catch any errors during the database operations. In the catch
block, either log the error with relevant context or rethrow it with additional
information to ensure proper error propagation and easier debugging. Apply the
same pattern to the similar code in lines 72 to 89.
}); | ||
|
||
const items = rows.map((row: TcmEncounterModel) => { | ||
const patient = (row.get("PatientModel") as PatientModel).dataValues.data; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fragile data access pattern - add null safety.
The nested property access (row.get("PatientModel") as PatientModel).dataValues.data
is fragile and could fail if the association isn't loaded or the structure changes.
Add proper null checking:
- const patient = (row.get("PatientModel") as PatientModel).dataValues.data;
+ const patientModel = row.get("PatientModel") as PatientModel | null;
+ if (!patientModel?.dataValues?.data) {
+ throw new Error(`Patient data not found for TCM encounter ${row.id}`);
+ }
+ const patient = patientModel.dataValues.data;
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const patient = (row.get("PatientModel") as PatientModel).dataValues.data; | |
const patientModel = row.get("PatientModel") as PatientModel | null; | |
if (!patientModel?.dataValues?.data) { | |
throw new Error(`Patient data not found for TCM encounter ${row.id}`); | |
} | |
const patient = patientModel.dataValues.data; |
🤖 Prompt for AI Agents
In packages/api/src/command/medical/tcm-encounter/get-tcm-encounters.ts at line
52, the code directly accesses nested properties without checking if they exist,
which can cause runtime errors if the association is missing or the structure
changes. Modify the code to safely check that row.get("PatientModel") returns a
valid object and that dataValues and data are defined before accessing them,
using conditional checks or optional chaining to prevent exceptions.
/** Hack to get around Sequelize type inference not seeing associations */ | ||
const encounterData = { ...row.dataValues, PatientModel: undefined }; | ||
delete encounterData.PatientModel; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Replace delete
operator for better performance.
The static analysis tool correctly identifies that the delete
operator can impact performance. The code comment acknowledges this is a hack.
Apply the suggested fix from the static analysis tool:
- /** Hack to get around Sequelize type inference not seeing associations */
- const encounterData = { ...row.dataValues, PatientModel: undefined };
- delete encounterData.PatientModel;
+ /** Extract encounter data without associations */
+ const encounterData = { ...row.dataValues };
+ encounterData.PatientModel = undefined;
Better yet, consider destructuring to avoid the mutation entirely:
- /** Hack to get around Sequelize type inference not seeing associations */
- const encounterData = { ...row.dataValues, PatientModel: undefined };
- delete encounterData.PatientModel;
+ /** Extract encounter data without associations */
+ const { PatientModel: _, ...encounterData } = row.dataValues;
🧰 Tools
🪛 Biome (1.9.4)
[error] 56-56: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
🤖 Prompt for AI Agents
In packages/api/src/command/medical/tcm-encounter/get-tcm-encounters.ts around
lines 54 to 56, avoid using the delete operator on encounterData.PatientModel
for better performance. Instead of creating encounterData by spreading
row.dataValues and then deleting PatientModel, use destructuring to exclude
PatientModel from the copied object in one step, preventing mutation and
improving efficiency.
attributes: ["id", "cxId", "data"], | ||
}, | ||
], | ||
limit: pagination.count + 1, // Get one extra to determine if there's a next page |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pagination logic incomplete - extra record not handled.
The code fetches pagination.count + 1
records to detect if there's a next page, but the function doesn't remove the extra record or provide pagination metadata to indicate whether more pages exist.
Consider this approach to properly handle pagination:
const rows = await TcmEncounterModel.findAll({
where,
include: [
{
model: PatientModel,
as: "PatientModel",
attributes: ["id", "cxId", "data"],
},
],
limit: pagination.count + 1, // Get one extra to determine if there's a next page
order: [["admitTime", "DESC"]],
});
+ const hasNextPage = rows.length > pagination.count;
+ const items = rows.slice(0, pagination.count).map((row: TcmEncounterModel) => {
- const items = rows.map((row: TcmEncounterModel) => {
And update the return type to include pagination info:
-export async function getTcmEncounters({ cxId, after, pagination }: GetTcmEncountersCmd): Promise<{
- items: TcmEncounterForDisplay[];
-}> {
+export async function getTcmEncounters({ cxId, after, pagination }: GetTcmEncountersCmd): Promise<{
+ items: TcmEncounterForDisplay[];
+ hasNextPage: boolean;
+}> {
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
limit: pagination.count + 1, // Get one extra to determine if there's a next page | |
export async function getTcmEncounters({ | |
cxId, | |
after, | |
pagination, | |
}: GetTcmEncountersCmd): Promise<{ | |
items: TcmEncounterForDisplay[]; | |
hasNextPage: boolean; | |
}> { | |
const rows = await TcmEncounterModel.findAll({ | |
where, | |
include: [ | |
{ | |
model: PatientModel, | |
as: "PatientModel", | |
attributes: ["id", "cxId", "data"], | |
}, | |
], | |
limit: pagination.count + 1, // Get one extra to determine if there's a next page | |
order: [["admitTime", "DESC"]], | |
}); | |
const hasNextPage = rows.length > pagination.count; | |
const items = rows | |
.slice(0, pagination.count) | |
.map((row: TcmEncounterModel) => { | |
// existing mapping logic to TcmEncounterForDisplay | |
}); | |
return { items, hasNextPage }; | |
} |
🤖 Prompt for AI Agents
In packages/api/src/command/medical/tcm-encounter/get-tcm-encounters.ts at line
47, the code fetches one extra record to check for a next page but does not
remove this extra record from the results or return pagination metadata. To fix
this, adjust the returned data to exclude the extra record if it exists, and add
a flag or metadata indicating whether there is a next page. Also, update the
function's return type to include this pagination information.
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
metriport/metriport-internal#1040 Ref: ENG-479 Signed-off-by: Lucas Della Bella <dellabella.lucas@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approving to get this out as discussed - please address minor outstanding comments from @leite08 on a follow up PR
Dependencies
Description
This PR adds a migration and some endpoints to allow for tracking encounter rows that would be displayed in the TCM viewer
Testing
Wrote some unit tests to check happy path and common failure cases.
curl-ed
Release Plan
Summary by CodeRabbit