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:
- Calling
delete()setsdeleted_attimestamp instead of removing the record - Sets
retention_expires_atto 7 years in the future (HIPAA requirement) - Records
deleted_by(Clerk user ID) and optionalreasonfor audit trail - Soft-deleted records excluded from queries by default (use
includeDeleted: trueto include) - 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:
- Every
create()initializes version 1 snapshot - Every
update()creates a new version snapshot with incremented version number - Snapshots store full protocol state at time of change
- Captures
changedBy(Clerk user ID),changeReason, andchangeType - Stores
previousSnapshotfor 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-manualRetention Schedule:
| Data Type | Soft Delete Retention | After Retention | Audit Trail |
|---|---|---|---|
| Customer Profiles | 7 years | Permanent delete (GDPR) | Yes |
| Lab Results | 7 years | Permanent delete | Yes |
| Protocols | 7 years | Permanent delete | Yes (includes version history) |
| Wearable Readings | 7 years | Permanent delete | Yes |
| Protocol Versions | Indefinite | Never deleted | Yes |