diff --git a/server/src/app.ts b/server/src/app.ts index fe9d99c..089ee9b 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -5,6 +5,7 @@ import mongodb from './database/mongodb'; import ingesterRouter from './routes/ingester'; import metricsRouter from './routes/metrics'; import workoutsRouter from './routes/workouts'; +import medicationsRouter from './routes/medications'; import { requireReadAuth, requireWriteAuth } from './middleware/auth'; const app = express(); @@ -29,6 +30,7 @@ app.use('/api/data', requireWriteAuth, ingesterRouter); // Apply read auth middleware to data retrieval routes app.use('/api/metrics', requireReadAuth, metricsRouter); app.use('/api/workouts', requireReadAuth, workoutsRouter); +app.use('/api/medications', requireReadAuth, medicationsRouter); app.get('/', (req: express.Request, res: express.Response) => { res.json({ message: 'Hello world!' }); diff --git a/server/src/controllers/ingester.ts b/server/src/controllers/ingester.ts index ddcaa23..61d0724 100644 --- a/server/src/controllers/ingester.ts +++ b/server/src/controllers/ingester.ts @@ -2,6 +2,7 @@ import { Request, Response } from 'express'; import { saveMetrics } from './metrics'; import { saveWorkouts } from './workouts'; +import { saveMedications } from './medications'; import { IngestData } from '../models/IngestData'; import { IngestResponse } from '../models/IngestResponse'; @@ -15,12 +16,13 @@ export const ingestData = async (req: Request, res: Response) => { throw new Error('No data provided'); } - const [metricsResponse, workoutsResponse] = await Promise.all([ + const [metricsResponse, workoutsResponse, medicationsResponse] = await Promise.all([ saveMetrics(data), saveWorkouts(data), + saveMedications(data), ]); - response = { ...metricsResponse, ...workoutsResponse }; + response = { ...metricsResponse, ...workoutsResponse, ...medicationsResponse }; const hasErrors = Object.values(response).some((r) => !r.success); const allFailed = Object.values(response).every((r) => !r.success); diff --git a/server/src/controllers/medications.ts b/server/src/controllers/medications.ts new file mode 100644 index 0000000..27d8140 --- /dev/null +++ b/server/src/controllers/medications.ts @@ -0,0 +1,90 @@ +import { Request, Response } from 'express'; + +import { IngestData } from '../models/IngestData'; +import { IngestResponse } from '../models/IngestResponse'; +import { MedicationModel, mapMedicationData, getMedicationId } from '../models/Medication'; +import { filterFields, parseDate } from '../utils'; + +export const getMedications = async (req: Request, res: Response) => { + try { + const { from, to, include, exclude } = req.query; + + const fromDate = parseDate(from as string); + const toDate = parseDate(to as string); + + let query = {}; + + if (fromDate && toDate) { + query = { + start: { + $gte: fromDate, + $lte: toDate, + }, + }; + } + + let medications = await MedicationModel.find(query) + .sort({ start: -1 }) + .lean(); + + if (include || exclude) { + medications = medications.map((med) => filterFields(med, include, exclude)); + } + + console.log(`${medications.length} medications fetched`); + res.status(200).json(medications); + } catch (error) { + console.error('Error fetching medications:', error); + res.status(500).json({ error: 'Error fetching medications' }); + } +}; + +export const saveMedications = async (ingestData: IngestData): Promise => { + try { + const response: IngestResponse = {}; + const medications = ingestData.data.medications; + + if (!medications || !medications.length) { + response.medications = { + success: true, + message: 'No medication data provided', + }; + return response; + } + + const medicationOperations = medications.map((medication) => ({ + updateOne: { + filter: { + medicationId: getMedicationId(medication), + start: new Date(medication.start), + }, + update: { + $set: mapMedicationData(medication), + }, + upsert: true, + }, + })); + + await MedicationModel.bulkWrite(medicationOperations); + + response.medications = { + success: true, + message: `${medications.length} medications saved successfully`, + }; + + console.debug(`Processed ${medications.length} medications`); + + return response; + } catch (error) { + console.error('Error processing medications:', error); + + const errorResponse: IngestResponse = {}; + errorResponse.medications = { + success: false, + message: 'Medications not saved', + error: error instanceof Error ? error.message : 'An error occurred', + }; + + return errorResponse; + } +}; diff --git a/server/src/models/IngestData.ts b/server/src/models/IngestData.ts index ae33d98..c3ba02a 100644 --- a/server/src/models/IngestData.ts +++ b/server/src/models/IngestData.ts @@ -1,9 +1,11 @@ import { MetricData } from './Metric'; import { WorkoutData } from './Workout'; +import { MedicationData } from './Medication'; export interface IngestData { data: { metrics?: MetricData[]; workouts?: WorkoutData[]; + medications?: MedicationData[]; }; } diff --git a/server/src/models/IngestResponse.ts b/server/src/models/IngestResponse.ts index a2b796e..c018366 100644 --- a/server/src/models/IngestResponse.ts +++ b/server/src/models/IngestResponse.ts @@ -9,4 +9,9 @@ export interface IngestResponse { message?: string; error?: string; }; + medications?: { + success: boolean; + message?: string; + error?: string; + }; } diff --git a/server/src/models/Medication.ts b/server/src/models/Medication.ts new file mode 100644 index 0000000..8faad64 --- /dev/null +++ b/server/src/models/Medication.ts @@ -0,0 +1,110 @@ +import mongoose, { Schema, Document } from 'mongoose'; + +export interface MedicationCoding { + system: string; + version: string; + code: string; +} + +export interface MedicationData { + start: string; + end: string; + displayText: string; + status: string; + dosage: number; + units: string; + isArchived: boolean; + codings: MedicationCoding[]; + scheduledDosage?: number; + scheduledDate?: string; +} + +export interface IMedication extends Document { + medicationId: string; // Computed: prefers RxNorm code, then sorted codings, then displayText + start: Date; + end: Date; + displayText: string; + status: string; + dosage: number; + units: string; + isArchived: boolean; + codings: MedicationCoding[]; + scheduledDosage?: number; + scheduledDate?: Date; + createdAt: Date; + updatedAt: Date; +} + +const CodingSchema = new Schema( + { + system: { type: String, required: true }, + version: { type: String, required: false }, + code: { type: String, required: true }, + }, + { _id: false }, +); + +const MedicationSchema = new Schema( + { + medicationId: { type: String, required: true }, + start: { type: Date, required: true }, + end: { type: Date, required: true }, + displayText: { type: String, required: true }, + status: { type: String, required: true }, + dosage: { type: Number, required: true }, + units: { type: String, required: true }, + isArchived: { type: Boolean, required: true }, + codings: { type: [CodingSchema], required: false }, + scheduledDosage: { type: Number, required: false }, + scheduledDate: { type: Date, required: false }, + }, + { timestamps: true }, +); + +// Unique index on medicationId + start time to prevent duplicate dose logs +MedicationSchema.index( + { medicationId: 1, start: 1 }, + { unique: true }, +); + +const RXNORM_SYSTEM = 'http://www.nlm.nih.gov/research/umls/rxnorm'; + +// Returns the medication identifier, preferring RxNorm code for stability +// when multiple coding systems are present (order may vary between syncs) +export function getMedicationId(data: MedicationData): string { + if (!data.codings?.length) { + return data.displayText; + } + + // Prefer RxNorm coding for consistent identification + const rxnorm = data.codings.find((c) => c.system === RXNORM_SYSTEM); + if (rxnorm) { + return rxnorm.code; + } + + // Fall back to first coding, sorted by system for stability + const sorted = [...data.codings].sort((a, b) => a.system.localeCompare(b.system)); + return sorted[0].code; +} + +export function mapMedicationData(data: MedicationData) { + return { + medicationId: getMedicationId(data), + start: new Date(data.start), + end: new Date(data.end), + displayText: data.displayText, + status: data.status, + dosage: data.dosage, + units: data.units, + isArchived: data.isArchived, + codings: data.codings || [], + scheduledDosage: data.scheduledDosage, + scheduledDate: data.scheduledDate ? new Date(data.scheduledDate) : undefined, + }; +} + +export const MedicationModel = mongoose.model( + 'Medication', + MedicationSchema, + 'medications', +); diff --git a/server/src/routes/medications.ts b/server/src/routes/medications.ts new file mode 100644 index 0000000..5c3068e --- /dev/null +++ b/server/src/routes/medications.ts @@ -0,0 +1,9 @@ +import express from 'express'; + +import { getMedications } from '../controllers/medications'; + +const router = express.Router(); + +router.get('/', getMedications); + +export default router;