Skip to Content
Patient GraphRepositories

Patient Graph Repositories

The @loop/patient-graph package provides a repository layer for type-safe data access to the Patient Graph schema. Repositories abstract Drizzle ORM queries behind a clean interface.

Setup

import { createConnection, createRepositories } from '@loop/patient-graph'; const connection = createConnection(process.env.DATABASE_URL!); const repos = createRepositories(connection);

Available Repositories

Profiles Repository

// List profiles with filtering const profiles = await repos.profiles.list({ email: 'user@example.com', subscriptionTier: 'pro', limit: 20, offset: 0, }); // Get by ID const profile = await repos.profiles.findById('prof_abc123'); // Create const newProfile = await repos.profiles.create({ externalId: 'user_clerk_456', email: 'new@example.com', firstName: 'Jane', lastName: 'Doe', biologicalSex: 'female', }); // Update await repos.profiles.update('prof_abc123', { firstName: 'Janet', tags: ['vip'], }); // Soft Delete (GDPR "right to be forgotten") await repos.profiles.delete('prof_abc123', 'admin_user_123', 'User requested deletion'); // Restore soft-deleted profile await repos.profiles.restore('prof_abc123', 'admin_user_456'); // Permanent delete (GDPR fulfillment after 7-year retention) await repos.profiles.permanentDelete('prof_abc123', 'admin_user_123');

Labs Repository

// List labs for a customer const labs = await repos.labs.list({ customerId: 'prof_abc123', status: 'reviewed', fromDate: '2024-01-01', toDate: '2024-12-31', limit: 10, }); // Create lab result const lab = await repos.labs.create({ customerId: 'prof_abc123', labDate: '2024-06-01', provider: 'Quest Diagnostics', biomarkers: [ { code: 'testosterone-total', name: 'Total Testosterone', value: 650, unit: 'ng/dL' }, ], status: 'pending', }); // Update lab status await repos.labs.updateStatus('lab_123', 'reviewed'); // Soft delete (preserves for 7 years) await repos.labs.delete('lab_123', 'admin_user_123', 'Duplicate entry'); // Restore soft-deleted lab await repos.labs.restore('lab_123', 'admin_user_456'); // Permanent delete (after retention expires) await repos.labs.permanentDelete('lab_123', 'admin_user_123');

Protocols Repository

// List active protocols const protocols = await repos.protocols.list({ customerId: 'prof_abc123', status: 'active', }); // Create protocol (with version 1 snapshot) const protocol = await repos.protocols.create( { customerId: 'prof_abc123', title: 'BPC-157 Recovery', items: [ { compound: 'BPC-157', dosage: '250mcg', frequency: '2x daily', route: 'subcutaneous' }, ], status: 'active', startDate: '2024-06-01', endDate: '2024-07-27', }, 'provider_clerk_123', // changedBy (Clerk user ID) 'Initial protocol creation' // changeReason (optional) ); // Update protocol (creates new version snapshot) await repos.protocols.update( 'proto_def456', { status: 'completed' }, 'provider_clerk_123', // changedBy 'Patient completed protocol', // changeReason 'completed' // changeType (optional: 'updated', 'dose_adjusted', 'medication_changed', etc.) ); // Get complete version history (for dose titration analysis) const versions = await repos.protocols.getVersionHistory('proto_def456'); // Returns: [ // { versionNumber: 3, snapshot: {...}, changedBy: 'provider_123', changeType: 'dose_adjusted', ... }, // { versionNumber: 2, snapshot: {...}, changedBy: 'provider_123', changeType: 'medication_changed', ... }, // { versionNumber: 1, snapshot: {...}, changedBy: 'provider_123', changeType: 'created', ... }, // ] // Get specific version const version2 = await repos.protocols.getVersionAtNumber('proto_def456', 2); // Get latest version const latest = await repos.protocols.getLatestVersion('proto_def456'); // Soft delete (preserves version history for 7 years) await repos.protocols.delete('proto_def456', 'admin_user_123', 'Protocol cancelled by patient'); // Restore soft-deleted protocol await repos.protocols.restore('proto_def456', 'admin_user_456'); // Permanent delete (after 7-year retention period) await repos.protocols.permanentDelete('proto_def456', 'admin_user_123');

Events Repository

// List events const events = await repos.patientEvents.list({ customerId: 'prof_abc123', type: 'lab_parsed', fromDate: '2024-06-01', }); // Create event await repos.patientEvents.create({ customerId: 'prof_abc123', type: 'note_added', description: 'Patient reported improved sleep quality', data: { category: 'wellness' }, source: 'luna-ai', });

Treatments Repository

const treatments = await repos.treatments.list({ customerId: 'prof_abc123', status: 'approved', }); await repos.treatments.create({ customerId: 'prof_abc123', rimoTreatmentId: 'rimo_treat_123', offeringName: 'TRT Protocol', status: 'pending', });

Prescriptions Repository

const prescriptions = await repos.prescriptions.list({ customerId: 'prof_abc123', treatmentId: 'treat_789', });

Conversation History Repository

const history = await repos.conversationHistory.list({ customerId: 'prof_abc123', channel: 'luna', limit: 50, }); await repos.conversationHistory.create({ customerId: 'prof_abc123', channel: 'luna', role: 'user', content: 'What are my latest lab results?', sessionId: 'sess_abc123', });

Wearable Data Repository

const wearableData = await repos.wearableData.list({ customerId: 'prof_abc123', source: 'oura', metricType: 'sleep', fromDate: '2024-06-01', });

RBAC Logs Repository

const logs = await repos.rbacLogs.list({ actorId: 'user_staff_123', outcome: 'denied', fromDate: '2024-06-01', });

Patient Context

The getPatientContext() function assembles a complete patient context from all repositories:

import { getPatientContext } from '@loop/patient-graph'; const context = await getPatientContext(userId); // Returns: // { // customerId: string, // profile: CustomerProfile, // conditions: string[], // medications: string[], // labResults: LabResult[], // activeProtocols: Protocol[], // emergencyFlags: string[], // consultationNotes: string[], // }

This is used by Luna AI to understand a patient’s complete health picture before responding.

Patient Timeline

import { getPatientTimeline } from '@loop/patient-graph'; const timeline = await getPatientTimeline(userId, { fromDate: '2024-01-01', limit: 100, });

Returns a chronological list of all patient events.

Data Preservation & Compliance

Soft Deletes

All clinical data repositories implement soft delete functionality to comply with HIPAA retention requirements and pharma partnership data preservation needs.

How it works:

  1. Calling delete() sets deleted_at timestamp instead of removing the record
  2. Sets retention_expires_at to 7 years in the future (HIPAA requirement)
  3. Records deleted_by (Clerk user ID) and optional reason for audit trail
  4. Soft-deleted records excluded from queries by default (use includeDeleted: true to include)
  5. After 7 years, automated cleanup job permanently deletes expired records

Repositories with soft delete:

  • customerProfiles (GDPR “right to be forgotten”)
  • labResults (clinical data retention)
  • protocols (protocol history retention)
  • wearableData (wearable readings retention)

Example:

// Soft delete a lab result await repos.labs.delete('lab_123', 'admin_user_456', 'Patient requested removal'); // Query excludes soft-deleted by default const labs = await repos.labs.list({ customerId: 'prof_abc' }); // Include soft-deleted records const allLabs = await repos.labs.list({ customerId: 'prof_abc', includeDeleted: true }); // Restore a soft-deleted record await repos.labs.restore('lab_123', 'admin_user_789'); // Permanent delete (only after retention_expires_at has passed) await repos.labs.permanentDelete('lab_123', 'system-retention-cleanup');

Protocol Version Snapshots

Protocols support full version history tracking for dose titration analysis and pharma partnership requirements.

How it works:

  1. Every create() initializes version 1 snapshot
  2. Every update() creates a new version snapshot with incremented version number
  3. Snapshots store full protocol state at time of change
  4. Captures changedBy (Clerk user ID), changeReason, and changeType
  5. Stores previousSnapshot for diff analysis

Use cases:

  • Dose titration analysis for Novo Nordisk partnership
  • Temporal queries: “What was the protocol state on date X?”
  • Audit compliance: Complete change history with who/when/why
  • Research: Analyze protocol effectiveness across version changes

Example:

// Create protocol (version 1 created automatically) const protocol = await repos.protocols.create( { customerId: 'prof_123', title: 'TRT Protocol', ... }, 'provider_clerk_456', 'Initial assessment' ); // Update protocol (version 2 created) await repos.protocols.update( protocol.id, { dosage: '200mg weekly' }, 'provider_clerk_456', 'Dose adjustment based on labs', 'dose_adjusted' ); // Get complete version history const versions = await repos.protocols.getVersionHistory(protocol.id); // [ // { versionNumber: 2, snapshot: {...}, changeType: 'dose_adjusted', ... }, // { versionNumber: 1, snapshot: {...}, changeType: 'created', ... } // ] // Time-travel: Get protocol state at specific version const version1 = await repos.protocols.getVersionAtNumber(protocol.id, 1);

Data Retention Policy

7-Year Retention:

  • All soft-deleted clinical data retained for 7 years (HIPAA requirement)
  • Automated cleanup job runs daily at 2 AM UTC
  • Only deletes records where retention_expires_at < NOW()
  • Logs all permanent deletions for compliance audit

Cleanup Job:

# Runs automatically via Trigger.dev # apps/trigger/src/jobs/data-retention-cleanup.ts # Manual trigger (for testing) trigger deploy --job data-retention-cleanup-manual

Retention Schedule:

Data TypeSoft Delete RetentionAfter RetentionAudit Trail
Customer Profiles7 yearsPermanent delete (GDPR)Yes
Lab Results7 yearsPermanent deleteYes
Protocols7 yearsPermanent deleteYes (includes version history)
Wearable Readings7 yearsPermanent deleteYes
Protocol VersionsIndefiniteNever deletedYes