Agent skill
dataloader-dual-mode-migration
Migrate dataloaders to dual-mode pattern supporting both patientId and enrollmentId for Member Model 2.0. Use when converting a patient-based dataloader to support the MemberModelV2Switch.
Install this agent skill to your Project
npx add-skill https://github.com/jakewaldrip/.dotfiles/tree/main/misc/commons/.opencode/skills/dataloader-dual-mode-migration
SKILL.md
Dual-Mode Dataloader Migration
Migrate patient-based dataloaders to support dual-mode operation (patientId and enrollmentId) for Member Model 2.0.
When to Use
- Converting a loader that currently keys by
patientIdto support dual-mode (patientId/enrollmentId) - The loader returns data scoped to a patient that should be filtered by enrollment when MemberModelV2Switch is ON
Prerequisites
- Model already uses PatientEnrollmentRelatedClassMixin - Required for
withDualModePatientKeys()query builder. Do NOT add this mixin yourself. Which models need the mixin is a human design decision. If the model doesn't already extend this mixin, stop and inform the user that the model is not set up for dual-mode migration. - Table has
patientEnrollmentIdcolumn - Data must be linkable to enrollments - Loader currently uses
patientIdas the key (string or included in compound key)
Stop Conditions
Do NOT proceed with the migration if any of the following are true:
- The model does not already extend
PatientEnrollmentRelatedClassMixin. Inform the user and stop. - The table does not have a
patientEnrollmentIdcolumn. Inform the user and stop.
Adding the enrollment mixin to a model is a separate architectural decision that must be made by the team, not as part of a dataloader migration.
Key Files
| File | Purpose |
|---|---|
commons-packages/backend/graphql/shared/loaders.ts |
DataLoaders class - loader definitions |
commons-packages/backend/graphql/shared/dual-mode-patient-key.ts |
DualModePatientKey type and normalizer |
Model file (e.g., models/goal/goal.ts) |
Batch load method |
| Caller files (resolvers, services) | Usage sites to update |
Migration Pattern Overview
BEFORE MIGRATION
================
Loader: returnsMultipleItems(Model.getByPatient.bind(Model), ...)
Caller: dataLoaders.xxxForPatient.load(patientId)
Model: static async getByPatient(patientIds: readonly string[])
|
v
AFTER MIGRATION
===============
Loader: returnsMultipleItems<DualModePatientKey, Model, ...>(
Model.loadByPatient.bind(Model),
(item) => { /* dual-mode key */ },
normalizeDualModeKey,
)
Caller: dataLoaders.xxxForPatient.load({ patientId })
Model: static async loadByPatient(keys: readonly DualModePatientKey[])
Templates
Model Method Template
Create a new loadByXxx method in the model:
import type { DualModePatientKey } from '@commons/backend/graphql/shared/dual-mode-patient-key.js';
/**
* Data loader method for retrieving [items] by patient keys.
* Uses dual-mode filtering based on MemberModelV2Switch.
*
* @param keys - Array of DualModePatientKey objects
*/
static async loadByPatient(keys: readonly DualModePatientKey[]) {
return ModelName.query()
.withDualModePatientKeys([...keys])
.andWhere({ deletedAt: null })
// Add other filters and ordering as needed
.orderBy('createdAt', 'DESC');
}
Loader Definition Template
Update the loader in loaders.ts:
import type { DualModePatientKey } from '@commons/backend/graphql/shared/dual-mode-patient-key.js';
import { normalizeDualModeKey } from '@commons/backend/graphql/shared/dual-mode-patient-key.js';
import { MemberModelV2Switch } from '@commons/backend/lib/member-model-v2-switch.js';
/**
* Dual-mode loader for [description].
* Accepts DualModePatientKey with optional enrollmentId.
* When MemberModelV2Switch is ON and enrollmentId is provided, keys by enrollmentId.
* Otherwise, keys by patientId (legacy behavior).
*/
xxxForPatient = returnsMultipleItems<DualModePatientKey, ModelName, DualModePatientKey>(
ModelName.loadByPatient.bind(ModelName),
(item): DualModePatientKey => {
// Return the appropriate key based on switch state
if (MemberModelV2Switch.shouldApplyEnrollmentLogic() && item.patientEnrollmentId) {
return { patientId: item.patientId, enrollmentId: item.patientEnrollmentId };
}
return { patientId: item.patientId };
},
// Normalize key for cache lookup
normalizeDualModeKey,
);
Caller Update Pattern
// BEFORE
context.dataLoaders.xxxForPatient.load(patientId);
// AFTER (basic - just patientId)
context.dataLoaders.xxxForPatient.load({ patientId });
// AFTER (with enrollmentId when available)
context.dataLoaders.xxxForPatient.load({
patientId: patient.id,
enrollmentId: patient.activeEnrollmentId,
});
Test Template
Create/update tests in the model's __tests__/ directory:
import { describeDualMode } from '@commons/backend/__tests__/helpers/member-model-v2-switch-test-helper.js';
describeDualMode('loadByPatient', () => {
it('should load items for a single patient', async () => {
const { patient, expectedItems } = await setup();
const items = await ModelName.loadByPatient([{ patientId: patient.id }]);
expect(items.length).toBeGreaterThanOrEqual(expectedItems.length);
const itemIds = items.map((item) => item.id);
expectedItems.forEach((expected) => {
expect(itemIds).toContain(expected.id);
});
});
it('should load items for multiple patients', async () => {
const { patient1, patient2 } = await setup();
const items = await ModelName.loadByPatient([
{ patientId: patient1.id },
{ patientId: patient2.id },
]);
// Assert items from both patients are returned
});
it('should not return deleted items', async () => {
const { patient, deletedItem, activeItem } = await setup();
const items = await ModelName.loadByPatient([{ patientId: patient.id }]);
const itemIds = items.map((item) => item.id);
expect(itemIds).not.toContain(deletedItem.id);
expect(itemIds).toContain(activeItem.id);
});
it('should return empty array for empty keys array', async () => {
const items = await ModelName.loadByPatient([]);
expect(items).toHaveLength(0);
});
it('should return empty array for patient with no items', async () => {
const { patientWithNoItems } = await setup();
const items = await ModelName.loadByPatient([{ patientId: patientWithNoItems.id }]);
expect(items).toHaveLength(0);
});
});
Verification Checklist
After migration:
- Model has new
loadByPatientmethod usingwithDualModePatientKeys - Loader definition uses
DualModePatientKeytype parameters - Loader uses
normalizeDualModeKeyas third argument - Loader id function returns
DualModePatientKeywith switch check - All callers updated to pass
{ patientId }object - Tests added with
describeDualModewrapper - Typecheck passes:
npm run typecheck - Tests pass:
npm test <test-file>
References
- Dataloaders documentation
- Example:
goalsForPatientincommons-packages/backend/graphql/shared/loaders.ts - Goal.loadByPatient - Example model method
- Goal tests - Example tests with
describeDualMode - DualModePatientKey
- PatientEnrollmentRelatedClassMixin
See WORKFLOW.md for step-by-step migration instructions.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
react-component-writing
React component patterns and style guide for the Commons monorepo. Use when creating React components, working with GraphQL in components, or implementing internationalization with MessageFormat.
graphite-cli
Use Graphite CLI (gt) for stacked PRs and branch management. Use when creating branches, committing changes, submitting PRs, syncing with trunk, or managing stacked pull requests.
test-running
Run Jest tests in the Commons monorepo. Use when testing code changes, validating implementations, debugging test failures, or when the user mentions running tests.
document-type-adding
Add new document types to the DocuSign integration in the Commons monorepo. Use when the user wants to add a patient document, consent form, or agreement to the DocuSign workflow.
feature-flag-create-or-remove
Create or remove feature flags in the Commons application. Use when the user wants to add a new feature flag, delete a feature flag, or asks about feature flag naming conventions.
nx-project-mapping
Maps file paths to Nx project names in the Commons monorepo. Use when you need to determine which Nx project a file belongs to for running tests, linting, or other Nx commands.
Didn't find tool you were looking for?