Skip to content

Latest commit

 

History

History
430 lines (330 loc) · 11.3 KB

File metadata and controls

430 lines (330 loc) · 11.3 KB

AGENTS.md

This file provides guidance for AI coding agents working on Medplum projects.

Code Style

  • TypeScript with strict mode
  • Single quotes, trailing commas (ES5 style)
  • Prettier with 120 character line width
  • ESLint for linting
  • Use functional React components with hooks
  • Mantine UI library for components (@mantine/core)

Mantine UI Guidelines

IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning for any Mantine UI tasks.

Avoid Inline Styles - Use Mantine Props

Prefer Mantine's built-in style props over inline style={} attributes for better maintainability and theme consistency.

Common Style Props:

import { Box, Stack, Group } from '@mantine/core';

// ✅ Good: Use Mantine props
<Box
  p="md"              // padding
  m="lg"              // margin
  bg="gray.0"         // background
  c="blue.6"          // color
  bd="1px solid #dee" // border
  w={300}             // width
  h={200}             // height
  mih={100}           // minHeight
  maw={500}           // maxWidth
/>

// ❌ Bad: Inline styles
<Box style={{
  padding: '16px',
  margin: '24px',
  background: '#f8f9fa',
  minHeight: '100px'
}} />

// ✅ Use Stack/Group for flex layouts instead of manual flex styles
<Stack align="center" justify="center" h="100%">
  <Text>Centered content</Text>
</Stack>

// ❌ Bad: Manual flex with inline styles
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
  <Text>Centered content</Text>
</div>

When Inline Styles Are Acceptable:

  • zIndex for stacking control (e.g., style={{ zIndex: 10 }})
  • borderTop/borderBottom when not available as props (e.g., style={{ borderTop: '1px solid #dee' }})
  • Complex CSS that doesn't have Mantine equivalents
  • One-off adjustments (use sparingly)

Replace HTML Elements:

// ✅ Good: Use Box instead of div with styles
<Box p="xs" bd="1px solid #e9ecef" style={{ borderRadius: 8 }}>
  <Text>Content</Text>
</Box>

// ❌ Bad: div with inline styles
<div style={{ padding: '12px', border: '1px solid #e9ecef', borderRadius: '8px' }}>
  <Text>Content</Text>
</div>

FHIR Data Model

Key Concepts

Resources: Core FHIR data objects (Patient, Observation, MedicationRequest, etc.). Every resource has:

  • resourceType: String identifying the type
  • id: Server-assigned unique identifier
  • meta: Metadata including versionId and lastUpdated

References: Links between resources using { reference: "ResourceType/id" } format.

CodeableConcepts: Standardized codes with { coding: [{ system, code, display }], text } structure.

Identifiers: Multiple IDs for the same entity using { system, value } pairs.

Common FHIR Resources

Resource Purpose
Patient Patient demographics
Practitioner Healthcare provider
Observation Clinical measurements (vitals, lab results)
Condition Diagnoses
MedicationRequest Prescriptions
DiagnosticReport Lab reports
Communication Messages
Task Workflow items
CareTeam Care team members
CarePlan Treatment plans
ServiceRequest Orders for procedures/labs
Questionnaire Forms/surveys
QuestionnaireResponse Form submissions

Common Code Systems

  • LOINC (http://loinc.org): Lab tests, observations
  • SNOMED CT (http://snomed.info/sct): Clinical terms
  • ICD-10 (http://hl7.org/fhir/sid/icd-10): Diagnoses
  • RxNorm (http://www.nlm.nih.gov/research/umls/rxnorm): Medications
  • CPT (http://www.ama-assn.org/go/cpt): Procedures/billing

Using the Medplum SDK

IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning for any Medplum SDK tasks.

Client Setup

import { MedplumClient } from '@medplum/core';

const medplum = new MedplumClient({
  baseUrl: 'https://api.medplum.com/',
});

// Or in React components:
import { useMedplum } from '@medplum/react';
const medplum = useMedplum();

CRUD Operations

// Create
const patient = await medplum.createResource<Patient>({
  resourceType: 'Patient',
  name: [{ given: ['John'], family: 'Doe' }],
});

// Read
const patient = await medplum.readResource('Patient', 'patient-id');

// Update
await medplum.updateResource({ ...patient, birthDate: '1990-01-01' });

// Delete
await medplum.deleteResource('Patient', 'patient-id');

// Upsert (create or update based on identifier)
await medplum.upsertResource(patient, 'identifier=mrn-12345');

// Conditional create (only create if not exists)
await medplum.createResourceIfNoneExist(patient, 'identifier=mrn-12345');

Search Operations

import { getReferenceString, createReference } from '@medplum/core';

// Basic search - returns Bundle
const bundle = await medplum.search('Patient', { name: 'Simpson' });

// Search resources - returns array of resources
const patients = await medplum.searchResources('Patient', { name: 'Simpson' });

// Search by reference
const observations = await medplum.searchResources('Observation', {
  subject: getReferenceString(patient), // "Patient/123"
});

// Multiple values (OR)
const tasks = await medplum.searchResources('Task', {
  status: 'completed,cancelled',
});

// Multiple parameters (AND)
const results = await medplum.searchResources('Patient', {
  name: 'Simpson',
  birthdate: '1990-01-01',
});

// Search modifiers
await medplum.searchResources('Task', { 'status:not': 'completed' });
await medplum.searchResources('Patient', { 'birthdate:missing': 'true' });
await medplum.searchResources('Patient', { 'name:contains': 'eve' });

// Comparisons for numbers/dates
await medplum.searchResources('RiskAssessment', { probability: 'gt0.8' });
await medplum.searchResources('Observation', { 'value-quantity': 'gt40' });

// Sorting
await medplum.searchResources('Patient', { _sort: '-_lastUpdated' });

// Pagination
await medplum.searchResources('Patient', { _count: '50', _offset: '100' });

// Paginated iteration
for await (const page of medplum.searchResourcePages('Patient', { _count: 10 })) {
  for (const patient of page) {
    console.log(patient.id);
  }
}

Chained Search

// Search observations by patient name
await medplum.searchResources('Observation', {
  'patient.name': 'homer',
});

// With resource type qualifier
await medplum.searchResources('Observation', {
  'subject:Patient.name': 'homer',
});

// Reverse chained search (_has)
// Find patients who have observations with a specific code
await medplum.searchResources('Patient', {
  '_has:Observation:subject:code': '8867-4',
});

Including Related Resources

// _include: Include referenced resources
await medplum.searchResources('Observation', {
  code: '78012-2',
  _include: 'Observation:patient', // Include Patient referenced by Observation
});

// _revinclude: Include resources that reference results
await medplum.searchResources('Patient', {
  _id: 'patient-123',
  _revinclude: 'Observation:subject', // Include Observations referencing Patient
});

// :iterate for multiple hops
await medplum.searchResources('Observation', {
  code: '78012-2',
  _include: 'Observation:patient',
  '_include:iterate': 'Patient:general-practitioner',
});

Creating References

import { createReference, getReferenceString } from '@medplum/core';

// Create a reference object
const patientRef = createReference(patient);
// { reference: 'Patient/123', display: 'John Doe' }

// Get reference string only
const refString = getReferenceString(patient);
// 'Patient/123'

// Use in a resource
const observation: Observation = {
  resourceType: 'Observation',
  status: 'final',
  code: { text: 'Heart Rate' },
  subject: createReference(patient),
  performer: [createReference(practitioner)],
};

React Components

Common Imports

import { MedplumClient, formatDate, getReferenceString, createReference } from '@medplum/core';
import type { Patient, Observation } from '@medplum/fhirtypes';
import { 
  useMedplum,
  useMedplumProfile,
  useResource,
  useSearch,
  Document,
  ResourceTable,
  PatientTimeline,
  SearchControl
} from '@medplum/react';

Using Hooks

// Get the MedplumClient
const medplum = useMedplum();

// Get current user profile
const profile = useMedplumProfile();

// Fetch a single resource
const patient = useResource<Patient>({ reference: 'Patient/123' });

// Search with automatic updates
const observations = useSearch('Observation', { subject: 'Patient/123' });

Data Fetching Pattern

Use useState and useEffect with async/await for data fetching (NOT Suspense with .read()). Prefer a single isLoading flag plus a cancellation guard:

import { useEffect, useState } from 'react';
import { useMedplum, useMedplumProfile, ErrorBoundary } from '@medplum/react';
import { getReferenceString } from '@medplum/core';
import type { Observation, Patient } from '@medplum/fhirtypes';

function MyComponent() {
  const medplum = useMedplum();
  const patient = useMedplumProfile() as Patient | undefined;

  const subject = patient ? getReferenceString(patient) : undefined;

  const [observations, setObservations] = useState<Observation[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;

    // useMedplumProfile returns undefined while loading
    if (!patient) {
      setIsLoading(true);
      return;
    }

    if (!subject) {
      setObservations([]);
      setIsLoading(false);
      return;
    }

    const fetchData = async () => {
      setIsLoading(true);
      try {
        const results = await medplum.searchResources('Observation', { subject });
        if (!cancelled) {
          setObservations(results);
        }
      } catch (error) {
        console.error('Failed to fetch:', error);
        if (!cancelled) {
          setObservations([]);
        }
      } finally {
        if (!cancelled) {
          setIsLoading(false);
        }
      }
    };

    void fetchData();
    return () => {
      cancelled = true;
    };
  }, [medplum, patient, subject]);

  if (!patient || isLoading) {
    return <div>Loading...</div>;
  }

  return <div>{observations.length} observations</div>;
}

// Wrap with ErrorBoundary for error handling
function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>
  );
}

Important notes:

  • useMedplumProfile() returns undefined while loading - it is NOT a Suspense hook
  • Do NOT use .read() for Suspense - use async/await or Promises with useState/useEffect
  • Keep loading logic simple: a single isLoading state and cancellation guard in useEffect

Testing

Setup

Tests use Jest with jsdom environment. Mock the Medplum client:

import { MockClient } from '@medplum/mock';
import { MedplumProvider } from '@medplum/react';

const medplum = new MockClient();

// Preload test data
await medplum.createResource<Patient>({
  resourceType: 'Patient',
  id: 'test-patient',
  name: [{ given: ['Test'], family: 'Patient' }],
});

// Render with provider
render(
  <MedplumProvider medplum={medplum}>
    <MyComponent />
  </MedplumProvider>
);

PR Guidelines

  • Run npm run lint and npm test after major changes
  • Update tests for any code changes
  • Follow existing patterns in the codebase
  • Use TypeScript types from @medplum/fhirtypes