Spaces:
Sleeping
Sleeping
Commit ·
cd5e33d
1
Parent(s): f1b26cb
first
Browse files- .dockerignore +33 -0
- .gitignore +30 -0
- Dockerfile +32 -0
- app.js +62 -0
- bun.lock +299 -0
- db/mongo.js +73 -0
- docker-compose.yml +28 -0
- middleware/auth.js +121 -0
- package.json +31 -0
- routes/appointments.js +282 -0
- routes/auth.js +166 -0
- routes/patients.js +431 -0
- routes/warnings.js +145 -0
- services/aiService.js +22 -0
- services/appointmentService.js +419 -0
- services/authService.js +158 -0
- services/llmClient.js +57 -0
- services/ragService.js +108 -0
- services/timelineService.js +286 -0
- services/vectorService.js +261 -0
- services/warningService.js +293 -0
.dockerignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependencies
|
| 2 |
+
node_modules/
|
| 3 |
+
npm-debug.log*
|
| 4 |
+
|
| 5 |
+
# Environment files
|
| 6 |
+
.env
|
| 7 |
+
.env.local
|
| 8 |
+
.env.*.local
|
| 9 |
+
|
| 10 |
+
# Vector database (will be created in container)
|
| 11 |
+
chroma_db/
|
| 12 |
+
|
| 13 |
+
# Git
|
| 14 |
+
.git/
|
| 15 |
+
.gitignore
|
| 16 |
+
|
| 17 |
+
# IDE
|
| 18 |
+
.idea/
|
| 19 |
+
.vscode/
|
| 20 |
+
*.swp
|
| 21 |
+
*.swo
|
| 22 |
+
|
| 23 |
+
# OS files
|
| 24 |
+
.DS_Store
|
| 25 |
+
Thumbs.db
|
| 26 |
+
|
| 27 |
+
# Documentation
|
| 28 |
+
README.md
|
| 29 |
+
|
| 30 |
+
# Build artifacts
|
| 31 |
+
dist/
|
| 32 |
+
build/
|
| 33 |
+
|
.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependencies
|
| 2 |
+
node_modules/
|
| 3 |
+
|
| 4 |
+
# Environment variables
|
| 5 |
+
.env
|
| 6 |
+
.env.local
|
| 7 |
+
.env.*.local
|
| 8 |
+
|
| 9 |
+
# Vector database
|
| 10 |
+
chroma_db/
|
| 11 |
+
|
| 12 |
+
# Logs
|
| 13 |
+
logs/
|
| 14 |
+
*.log
|
| 15 |
+
npm-debug.log*
|
| 16 |
+
|
| 17 |
+
# OS files
|
| 18 |
+
.DS_Store
|
| 19 |
+
Thumbs.db
|
| 20 |
+
|
| 21 |
+
# IDE
|
| 22 |
+
.idea/
|
| 23 |
+
.vscode/
|
| 24 |
+
*.swp
|
| 25 |
+
*.swo
|
| 26 |
+
|
| 27 |
+
# Build
|
| 28 |
+
dist/
|
| 29 |
+
build/
|
| 30 |
+
|
Dockerfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Node.js LTS version
|
| 2 |
+
FROM node:20-alpine
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy package files
|
| 8 |
+
COPY package*.json ./
|
| 9 |
+
|
| 10 |
+
# Install dependencies
|
| 11 |
+
RUN npm install --production
|
| 12 |
+
|
| 13 |
+
# Copy source code
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Create directory for ChromaDB data
|
| 17 |
+
RUN mkdir -p /app/chroma_db
|
| 18 |
+
|
| 19 |
+
# Expose port
|
| 20 |
+
EXPOSE 7860
|
| 21 |
+
|
| 22 |
+
# Set environment variables
|
| 23 |
+
ENV NODE_ENV=production
|
| 24 |
+
ENV PORT=7860
|
| 25 |
+
|
| 26 |
+
# Health check
|
| 27 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 28 |
+
CMD node -e "require('http').get('http://localhost:7860/', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
|
| 29 |
+
|
| 30 |
+
# Start the server
|
| 31 |
+
CMD ["node", "app.js"]
|
| 32 |
+
|
app.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import cors from 'cors';
|
| 3 |
+
import dotenv from 'dotenv';
|
| 4 |
+
import { connectDB, ensureIndexes } from './db/mongo.js';
|
| 5 |
+
import authRoutes from './routes/auth.js';
|
| 6 |
+
import patientRoutes from './routes/patients.js';
|
| 7 |
+
import appointmentRoutes from './routes/appointments.js';
|
| 8 |
+
import warningRoutes from './routes/warnings.js';
|
| 9 |
+
|
| 10 |
+
dotenv.config();
|
| 11 |
+
|
| 12 |
+
const app = express();
|
| 13 |
+
const PORT = process.env.PORT || 7860;
|
| 14 |
+
|
| 15 |
+
// Middleware
|
| 16 |
+
app.use(express.json());
|
| 17 |
+
app.use(cors({
|
| 18 |
+
origin: [
|
| 19 |
+
'http://localhost:3000',
|
| 20 |
+
'http://localhost:5173',
|
| 21 |
+
'http://127.0.0.1:3000',
|
| 22 |
+
'http://127.0.0.1:5173'
|
| 23 |
+
],
|
| 24 |
+
credentials: true,
|
| 25 |
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
| 26 |
+
allowedHeaders: ['Content-Type', 'Authorization']
|
| 27 |
+
}));
|
| 28 |
+
|
| 29 |
+
// Routes
|
| 30 |
+
app.use('/api/auth', authRoutes);
|
| 31 |
+
app.use('/api', patientRoutes);
|
| 32 |
+
app.use('/api', appointmentRoutes);
|
| 33 |
+
app.use('/api', warningRoutes);
|
| 34 |
+
|
| 35 |
+
// Root endpoint
|
| 36 |
+
app.get('/', (req, res) => {
|
| 37 |
+
res.json({ message: 'Welcome to the Personalized Patient Information System!' });
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
// Error handling middleware
|
| 41 |
+
app.use((err, req, res, next) => {
|
| 42 |
+
console.error(err.stack);
|
| 43 |
+
res.status(500).json({ detail: err.message || 'Internal Server Error' });
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
// Start server
|
| 47 |
+
const startServer = async () => {
|
| 48 |
+
try {
|
| 49 |
+
await connectDB();
|
| 50 |
+
await ensureIndexes();
|
| 51 |
+
|
| 52 |
+
app.listen(PORT, () => {
|
| 53 |
+
console.log(`✓ Server running on port ${PORT}`);
|
| 54 |
+
});
|
| 55 |
+
} catch (error) {
|
| 56 |
+
console.error('Failed to start server:', error);
|
| 57 |
+
process.exit(1);
|
| 58 |
+
}
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
startServer();
|
| 62 |
+
|
bun.lock
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"lockfileVersion": 1,
|
| 3 |
+
"configVersion": 1,
|
| 4 |
+
"workspaces": {
|
| 5 |
+
"": {
|
| 6 |
+
"name": "patients-backend-express",
|
| 7 |
+
"dependencies": {
|
| 8 |
+
"@google/generative-ai": "^0.21.0",
|
| 9 |
+
"bcryptjs": "^2.4.3",
|
| 10 |
+
"chromadb": "^1.9.2",
|
| 11 |
+
"cors": "^2.8.5",
|
| 12 |
+
"dotenv": "^16.3.1",
|
| 13 |
+
"express": "^4.18.2",
|
| 14 |
+
"jsonwebtoken": "^9.0.2",
|
| 15 |
+
"mongodb": "^6.3.0",
|
| 16 |
+
"openai": "^4.24.0",
|
| 17 |
+
},
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
"packages": {
|
| 21 |
+
"@google/generative-ai": ["@google/generative-ai@0.21.0", "", {}, "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg=="],
|
| 22 |
+
|
| 23 |
+
"@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.0", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-ZHzx7Z3rdlWL1mECydvpryWN/ETXJiCxdgQKTAH+djzIPe77HdnSizKBDi1TVDXZjXyOj2IqEG/vPw71ULF06w=="],
|
| 24 |
+
|
| 25 |
+
"@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
| 26 |
+
|
| 27 |
+
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
| 28 |
+
|
| 29 |
+
"@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="],
|
| 30 |
+
|
| 31 |
+
"@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="],
|
| 32 |
+
|
| 33 |
+
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
| 34 |
+
|
| 35 |
+
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
| 36 |
+
|
| 37 |
+
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
| 38 |
+
|
| 39 |
+
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
| 40 |
+
|
| 41 |
+
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
| 42 |
+
|
| 43 |
+
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
| 44 |
+
|
| 45 |
+
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
| 46 |
+
|
| 47 |
+
"bcryptjs": ["bcryptjs@2.4.3", "", {}, "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="],
|
| 48 |
+
|
| 49 |
+
"body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
|
| 50 |
+
|
| 51 |
+
"bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="],
|
| 52 |
+
|
| 53 |
+
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
| 54 |
+
|
| 55 |
+
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
| 56 |
+
|
| 57 |
+
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
| 58 |
+
|
| 59 |
+
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
| 60 |
+
|
| 61 |
+
"chromadb": ["chromadb@1.10.5", "", { "dependencies": { "cliui": "^8.0.1", "isomorphic-fetch": "^3.0.0" }, "peerDependencies": { "@google/generative-ai": "^0.1.1", "cohere-ai": "^5.0.0 || ^6.0.0 || ^7.0.0", "ollama": "^0.5.0", "openai": "^3.0.0 || ^4.0.0", "voyageai": "^0.0.3-1" }, "optionalPeers": ["@google/generative-ai", "cohere-ai", "ollama", "openai", "voyageai"] }, "sha512-+IeTjjf44pKUY3vp1BacwO2tFAPcWCd64zxPZZm98dVj/kbSBeaHKB2D6eX7iRLHS1PTVASuqoR6mAJ+nrsTBg=="],
|
| 62 |
+
|
| 63 |
+
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
| 64 |
+
|
| 65 |
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
| 66 |
+
|
| 67 |
+
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
| 68 |
+
|
| 69 |
+
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
| 70 |
+
|
| 71 |
+
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
| 72 |
+
|
| 73 |
+
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
| 74 |
+
|
| 75 |
+
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
| 76 |
+
|
| 77 |
+
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
|
| 78 |
+
|
| 79 |
+
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
| 80 |
+
|
| 81 |
+
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
| 82 |
+
|
| 83 |
+
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
| 84 |
+
|
| 85 |
+
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
| 86 |
+
|
| 87 |
+
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
| 88 |
+
|
| 89 |
+
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
| 90 |
+
|
| 91 |
+
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
| 92 |
+
|
| 93 |
+
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
| 94 |
+
|
| 95 |
+
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
| 96 |
+
|
| 97 |
+
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
| 98 |
+
|
| 99 |
+
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
| 100 |
+
|
| 101 |
+
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
| 102 |
+
|
| 103 |
+
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
| 104 |
+
|
| 105 |
+
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
| 106 |
+
|
| 107 |
+
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
| 108 |
+
|
| 109 |
+
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
| 110 |
+
|
| 111 |
+
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
| 112 |
+
|
| 113 |
+
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
| 114 |
+
|
| 115 |
+
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
|
| 116 |
+
|
| 117 |
+
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
|
| 118 |
+
|
| 119 |
+
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
| 120 |
+
|
| 121 |
+
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
| 122 |
+
|
| 123 |
+
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
|
| 124 |
+
|
| 125 |
+
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
| 126 |
+
|
| 127 |
+
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
| 128 |
+
|
| 129 |
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
| 130 |
+
|
| 131 |
+
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
| 132 |
+
|
| 133 |
+
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
| 134 |
+
|
| 135 |
+
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
| 136 |
+
|
| 137 |
+
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
| 138 |
+
|
| 139 |
+
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
| 140 |
+
|
| 141 |
+
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
| 142 |
+
|
| 143 |
+
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
| 144 |
+
|
| 145 |
+
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
| 146 |
+
|
| 147 |
+
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
| 148 |
+
|
| 149 |
+
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
| 150 |
+
|
| 151 |
+
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
| 152 |
+
|
| 153 |
+
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
| 154 |
+
|
| 155 |
+
"isomorphic-fetch": ["isomorphic-fetch@3.0.0", "", { "dependencies": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" } }, "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA=="],
|
| 156 |
+
|
| 157 |
+
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
|
| 158 |
+
|
| 159 |
+
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
| 160 |
+
|
| 161 |
+
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
| 162 |
+
|
| 163 |
+
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
| 164 |
+
|
| 165 |
+
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
| 166 |
+
|
| 167 |
+
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
| 168 |
+
|
| 169 |
+
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
| 170 |
+
|
| 171 |
+
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
| 172 |
+
|
| 173 |
+
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
| 174 |
+
|
| 175 |
+
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
| 176 |
+
|
| 177 |
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
| 178 |
+
|
| 179 |
+
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
| 180 |
+
|
| 181 |
+
"memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="],
|
| 182 |
+
|
| 183 |
+
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
|
| 184 |
+
|
| 185 |
+
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
|
| 186 |
+
|
| 187 |
+
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
| 188 |
+
|
| 189 |
+
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
| 190 |
+
|
| 191 |
+
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
| 192 |
+
|
| 193 |
+
"mongodb": ["mongodb@6.21.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", "mongodb-connection-string-url": "^3.0.2" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.3.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A=="],
|
| 194 |
+
|
| 195 |
+
"mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="],
|
| 196 |
+
|
| 197 |
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
| 198 |
+
|
| 199 |
+
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
| 200 |
+
|
| 201 |
+
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
| 202 |
+
|
| 203 |
+
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
| 204 |
+
|
| 205 |
+
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
| 206 |
+
|
| 207 |
+
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
| 208 |
+
|
| 209 |
+
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
| 210 |
+
|
| 211 |
+
"openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
|
| 212 |
+
|
| 213 |
+
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
| 214 |
+
|
| 215 |
+
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
|
| 216 |
+
|
| 217 |
+
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
| 218 |
+
|
| 219 |
+
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
| 220 |
+
|
| 221 |
+
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
| 222 |
+
|
| 223 |
+
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
| 224 |
+
|
| 225 |
+
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
| 226 |
+
|
| 227 |
+
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
| 228 |
+
|
| 229 |
+
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
| 230 |
+
|
| 231 |
+
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
| 232 |
+
|
| 233 |
+
"send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="],
|
| 234 |
+
|
| 235 |
+
"serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
|
| 236 |
+
|
| 237 |
+
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
| 238 |
+
|
| 239 |
+
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
| 240 |
+
|
| 241 |
+
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
| 242 |
+
|
| 243 |
+
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
| 244 |
+
|
| 245 |
+
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
| 246 |
+
|
| 247 |
+
"sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="],
|
| 248 |
+
|
| 249 |
+
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
| 250 |
+
|
| 251 |
+
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
| 252 |
+
|
| 253 |
+
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
| 254 |
+
|
| 255 |
+
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
| 256 |
+
|
| 257 |
+
"tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
|
| 258 |
+
|
| 259 |
+
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
| 260 |
+
|
| 261 |
+
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
| 262 |
+
|
| 263 |
+
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
| 264 |
+
|
| 265 |
+
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
| 266 |
+
|
| 267 |
+
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
| 268 |
+
|
| 269 |
+
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
| 270 |
+
|
| 271 |
+
"webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
|
| 272 |
+
|
| 273 |
+
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
|
| 274 |
+
|
| 275 |
+
"whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
|
| 276 |
+
|
| 277 |
+
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
| 278 |
+
|
| 279 |
+
"debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
| 280 |
+
|
| 281 |
+
"node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
| 282 |
+
|
| 283 |
+
"send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
| 284 |
+
|
| 285 |
+
"send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
| 286 |
+
|
| 287 |
+
"serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
| 288 |
+
|
| 289 |
+
"node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
| 290 |
+
|
| 291 |
+
"node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
| 292 |
+
|
| 293 |
+
"serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
| 294 |
+
|
| 295 |
+
"serve-static/send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
| 296 |
+
|
| 297 |
+
"serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
| 298 |
+
}
|
| 299 |
+
}
|
db/mongo.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { MongoClient, ObjectId } from 'mongodb';
|
| 2 |
+
import dotenv from 'dotenv';
|
| 3 |
+
|
| 4 |
+
dotenv.config();
|
| 5 |
+
|
| 6 |
+
const MONGO_URI = process.env.MONGO_URI;
|
| 7 |
+
|
| 8 |
+
let client;
|
| 9 |
+
let db;
|
| 10 |
+
|
| 11 |
+
// Collections
|
| 12 |
+
let patientsCollection;
|
| 13 |
+
let usersCollection;
|
| 14 |
+
let appointmentsCollection;
|
| 15 |
+
let warningsCollection;
|
| 16 |
+
|
| 17 |
+
export const connectDB = async () => {
|
| 18 |
+
try {
|
| 19 |
+
client = new MongoClient(MONGO_URI, {
|
| 20 |
+
serverSelectionTimeoutMS: 5000
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
await client.connect();
|
| 24 |
+
await client.db('admin').command({ ping: 1 });
|
| 25 |
+
console.log('✓ Connected to MongoDB Atlas successfully!');
|
| 26 |
+
|
| 27 |
+
db = client.db('patient_info');
|
| 28 |
+
|
| 29 |
+
// Initialize collections
|
| 30 |
+
patientsCollection = db.collection('patients');
|
| 31 |
+
usersCollection = db.collection('users');
|
| 32 |
+
appointmentsCollection = db.collection('appointments');
|
| 33 |
+
warningsCollection = db.collection('clinical_warnings');
|
| 34 |
+
|
| 35 |
+
return db;
|
| 36 |
+
} catch (error) {
|
| 37 |
+
console.log(`⚠️ MongoDB connection warning: ${error.message}`);
|
| 38 |
+
console.log('The server will start, but database operations may fail.');
|
| 39 |
+
console.log('Please check:');
|
| 40 |
+
console.log(' 1. Your MongoDB Atlas IP whitelist (add your current IP)');
|
| 41 |
+
console.log(' 2. Your internet connection');
|
| 42 |
+
console.log(' 3. If your Atlas cluster is paused');
|
| 43 |
+
|
| 44 |
+
// Create client anyway for later retry
|
| 45 |
+
client = new MongoClient(MONGO_URI);
|
| 46 |
+
db = client.db('patient_info');
|
| 47 |
+
|
| 48 |
+
patientsCollection = db.collection('patients');
|
| 49 |
+
usersCollection = db.collection('users');
|
| 50 |
+
appointmentsCollection = db.collection('appointments');
|
| 51 |
+
warningsCollection = db.collection('clinical_warnings');
|
| 52 |
+
|
| 53 |
+
return db;
|
| 54 |
+
}
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
export const ensureIndexes = async () => {
|
| 58 |
+
try {
|
| 59 |
+
await usersCollection.createIndex({ email: 1 }, { unique: true });
|
| 60 |
+
console.log('✓ Database indexes created');
|
| 61 |
+
} catch (error) {
|
| 62 |
+
console.log(`⚠️ Could not create indexes: ${error.message}`);
|
| 63 |
+
}
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
export const getDB = () => db;
|
| 67 |
+
export const getPatientsCollection = () => patientsCollection;
|
| 68 |
+
export const getUsersCollection = () => usersCollection;
|
| 69 |
+
export const getAppointmentsCollection = () => appointmentsCollection;
|
| 70 |
+
export const getWarningsCollection = () => warningsCollection;
|
| 71 |
+
|
| 72 |
+
export { ObjectId };
|
| 73 |
+
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
backend:
|
| 5 |
+
build: .
|
| 6 |
+
container_name: patients-express-backend
|
| 7 |
+
ports:
|
| 8 |
+
- "8000:8000"
|
| 9 |
+
environment:
|
| 10 |
+
- NODE_ENV=production
|
| 11 |
+
- PORT=8000
|
| 12 |
+
- MONGO_URI=${MONGO_URI}
|
| 13 |
+
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
|
| 14 |
+
- HF_TOKEN=${HF_TOKEN}
|
| 15 |
+
volumes:
|
| 16 |
+
- chroma_data:/app/chroma_db
|
| 17 |
+
restart: unless-stopped
|
| 18 |
+
healthcheck:
|
| 19 |
+
test: ["CMD", "node", "-e", "require('http').get('http://localhost:8000/', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"]
|
| 20 |
+
interval: 30s
|
| 21 |
+
timeout: 10s
|
| 22 |
+
retries: 3
|
| 23 |
+
start_period: 10s
|
| 24 |
+
|
| 25 |
+
volumes:
|
| 26 |
+
chroma_data:
|
| 27 |
+
driver: local
|
| 28 |
+
|
middleware/auth.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { authService } from '../services/authService.js';
|
| 2 |
+
|
| 3 |
+
// User roles enum
|
| 4 |
+
export const UserRole = {
|
| 5 |
+
DOCTOR: 'doctor',
|
| 6 |
+
PATIENT: 'patient'
|
| 7 |
+
};
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Middleware to get the current authenticated user from JWT token
|
| 11 |
+
*/
|
| 12 |
+
export const getCurrentUser = async (req, res, next) => {
|
| 13 |
+
try {
|
| 14 |
+
const authHeader = req.headers.authorization;
|
| 15 |
+
|
| 16 |
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
| 17 |
+
return res.status(401).json({
|
| 18 |
+
detail: 'Could not validate credentials',
|
| 19 |
+
headers: { 'WWW-Authenticate': 'Bearer' }
|
| 20 |
+
});
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const token = authHeader.split(' ')[1];
|
| 24 |
+
const tokenData = authService.decodeToken(token);
|
| 25 |
+
|
| 26 |
+
if (!tokenData) {
|
| 27 |
+
return res.status(401).json({
|
| 28 |
+
detail: 'Could not validate credentials',
|
| 29 |
+
headers: { 'WWW-Authenticate': 'Bearer' }
|
| 30 |
+
});
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
const user = await authService.getUserById(tokenData.userId);
|
| 34 |
+
|
| 35 |
+
if (!user) {
|
| 36 |
+
return res.status(401).json({
|
| 37 |
+
detail: 'Could not validate credentials',
|
| 38 |
+
headers: { 'WWW-Authenticate': 'Bearer' }
|
| 39 |
+
});
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
if (!user.is_active) {
|
| 43 |
+
return res.status(403).json({
|
| 44 |
+
detail: 'User account is disabled'
|
| 45 |
+
});
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
req.user = user;
|
| 49 |
+
next();
|
| 50 |
+
} catch (error) {
|
| 51 |
+
return res.status(401).json({
|
| 52 |
+
detail: 'Could not validate credentials'
|
| 53 |
+
});
|
| 54 |
+
}
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* Middleware to ensure the current user is active
|
| 59 |
+
*/
|
| 60 |
+
export const getCurrentActiveUser = async (req, res, next) => {
|
| 61 |
+
await getCurrentUser(req, res, () => {
|
| 62 |
+
if (!req.user.is_active) {
|
| 63 |
+
return res.status(403).json({
|
| 64 |
+
detail: 'Inactive user'
|
| 65 |
+
});
|
| 66 |
+
}
|
| 67 |
+
next();
|
| 68 |
+
});
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* Factory function to require specific roles
|
| 73 |
+
*/
|
| 74 |
+
export const requireRoles = (allowedRoles) => {
|
| 75 |
+
return async (req, res, next) => {
|
| 76 |
+
await getCurrentUser(req, res, () => {
|
| 77 |
+
const userRole = req.user.role || 'patient';
|
| 78 |
+
|
| 79 |
+
if (!allowedRoles.includes(userRole)) {
|
| 80 |
+
return res.status(403).json({
|
| 81 |
+
detail: `Access denied. Required roles: ${allowedRoles.join(', ')}`
|
| 82 |
+
});
|
| 83 |
+
}
|
| 84 |
+
next();
|
| 85 |
+
});
|
| 86 |
+
};
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
// Pre-built role middlewares for common use cases
|
| 90 |
+
export const requireDoctor = requireRoles([UserRole.DOCTOR]);
|
| 91 |
+
export const requireStaff = requireRoles([UserRole.DOCTOR]);
|
| 92 |
+
|
| 93 |
+
/**
|
| 94 |
+
* Optional authentication - doesn't fail if no token provided
|
| 95 |
+
*/
|
| 96 |
+
export const optionalAuth = async (req, res, next) => {
|
| 97 |
+
try {
|
| 98 |
+
const authHeader = req.headers.authorization;
|
| 99 |
+
|
| 100 |
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
| 101 |
+
req.user = null;
|
| 102 |
+
return next();
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
const token = authHeader.split(' ')[1];
|
| 106 |
+
const tokenData = authService.decodeToken(token);
|
| 107 |
+
|
| 108 |
+
if (!tokenData) {
|
| 109 |
+
req.user = null;
|
| 110 |
+
return next();
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
const user = await authService.getUserById(tokenData.userId);
|
| 114 |
+
req.user = user || null;
|
| 115 |
+
next();
|
| 116 |
+
} catch (error) {
|
| 117 |
+
req.user = null;
|
| 118 |
+
next();
|
| 119 |
+
}
|
| 120 |
+
};
|
| 121 |
+
|
package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "patients-backend-express",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Personalized Patient Information System - Express.js Backend",
|
| 5 |
+
"main": "app.js",
|
| 6 |
+
"type": "module",
|
| 7 |
+
"scripts": {
|
| 8 |
+
"start": "node app.js",
|
| 9 |
+
"dev": "node --watch app.js"
|
| 10 |
+
},
|
| 11 |
+
"keywords": [
|
| 12 |
+
"healthcare",
|
| 13 |
+
"patient",
|
| 14 |
+
"medical",
|
| 15 |
+
"express",
|
| 16 |
+
"mongodb"
|
| 17 |
+
],
|
| 18 |
+
"author": "",
|
| 19 |
+
"license": "ISC",
|
| 20 |
+
"dependencies": {
|
| 21 |
+
"express": "^4.18.2",
|
| 22 |
+
"cors": "^2.8.5",
|
| 23 |
+
"dotenv": "^16.3.1",
|
| 24 |
+
"mongodb": "^6.3.0",
|
| 25 |
+
"jsonwebtoken": "^9.0.2",
|
| 26 |
+
"bcryptjs": "^2.4.3",
|
| 27 |
+
"openai": "^4.24.0",
|
| 28 |
+
"chromadb": "^1.9.2"
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
routes/appointments.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import { ObjectId } from '../db/mongo.js';
|
| 3 |
+
import { appointmentService } from '../services/appointmentService.js';
|
| 4 |
+
import { getCurrentUser, requireStaff } from '../middleware/auth.js';
|
| 5 |
+
|
| 6 |
+
const router = express.Router();
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Validate ObjectId format
|
| 10 |
+
*/
|
| 11 |
+
const isValidObjectId = (id) => {
|
| 12 |
+
try {
|
| 13 |
+
return ObjectId.isValid(id) && new ObjectId(id).toString() === id;
|
| 14 |
+
} catch {
|
| 15 |
+
return false;
|
| 16 |
+
}
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* POST /api/appointments
|
| 21 |
+
* Create a new appointment
|
| 22 |
+
*/
|
| 23 |
+
router.post('/appointments', requireStaff, async (req, res) => {
|
| 24 |
+
try {
|
| 25 |
+
const userId = req.user._id.toString();
|
| 26 |
+
const appointmentData = req.body;
|
| 27 |
+
|
| 28 |
+
// If no doctor_id specified, use current user as doctor
|
| 29 |
+
if (!appointmentData.doctor_id) {
|
| 30 |
+
appointmentData.doctor_id = userId;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Check for conflicts if doctor is assigned
|
| 34 |
+
if (appointmentData.doctor_id) {
|
| 35 |
+
const conflicts = await appointmentService.checkConflicts(
|
| 36 |
+
appointmentData.doctor_id,
|
| 37 |
+
appointmentData.date_time,
|
| 38 |
+
appointmentData.duration_minutes || 30
|
| 39 |
+
);
|
| 40 |
+
|
| 41 |
+
if (conflicts.length > 0) {
|
| 42 |
+
return res.status(409).json({
|
| 43 |
+
detail: {
|
| 44 |
+
message: 'Scheduling conflict detected',
|
| 45 |
+
conflicts
|
| 46 |
+
}
|
| 47 |
+
});
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
const appointment = await appointmentService.createAppointment(appointmentData, userId);
|
| 52 |
+
res.status(201).json(appointment);
|
| 53 |
+
} catch (error) {
|
| 54 |
+
res.status(500).json({ detail: `Error creating appointment: ${error.message}` });
|
| 55 |
+
}
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* GET /api/appointments
|
| 60 |
+
* List appointments with optional filters
|
| 61 |
+
*/
|
| 62 |
+
router.get('/appointments', getCurrentUser, async (req, res) => {
|
| 63 |
+
try {
|
| 64 |
+
const { patient_id, doctor_id, status, from_date, to_date, limit = 50 } = req.query;
|
| 65 |
+
|
| 66 |
+
const appointments = await appointmentService.listAppointments({
|
| 67 |
+
patientId: patient_id,
|
| 68 |
+
doctorId: doctor_id,
|
| 69 |
+
status,
|
| 70 |
+
fromDate: from_date,
|
| 71 |
+
toDate: to_date,
|
| 72 |
+
limit: Math.min(parseInt(limit), 100)
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
res.json(appointments);
|
| 76 |
+
} catch (error) {
|
| 77 |
+
res.status(500).json({ detail: `Error fetching appointments: ${error.message}` });
|
| 78 |
+
}
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* GET /api/appointments/upcoming
|
| 83 |
+
* Get upcoming appointments
|
| 84 |
+
*/
|
| 85 |
+
router.get('/appointments/upcoming', getCurrentUser, async (req, res) => {
|
| 86 |
+
try {
|
| 87 |
+
const { limit = 20 } = req.query;
|
| 88 |
+
const appointments = await appointmentService.getUpcomingAppointments(
|
| 89 |
+
Math.min(parseInt(limit), 100)
|
| 90 |
+
);
|
| 91 |
+
res.json(appointments);
|
| 92 |
+
} catch (error) {
|
| 93 |
+
res.status(500).json({ detail: `Error fetching appointments: ${error.message}` });
|
| 94 |
+
}
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
/**
|
| 98 |
+
* GET /api/appointments/stats
|
| 99 |
+
* Get appointment statistics
|
| 100 |
+
*/
|
| 101 |
+
router.get('/appointments/stats', getCurrentUser, async (req, res) => {
|
| 102 |
+
try {
|
| 103 |
+
const stats = await appointmentService.getAppointmentStats();
|
| 104 |
+
res.json(stats);
|
| 105 |
+
} catch (error) {
|
| 106 |
+
res.status(500).json({ detail: `Error fetching stats: ${error.message}` });
|
| 107 |
+
}
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
/**
|
| 111 |
+
* GET /api/appointments/:appointmentId
|
| 112 |
+
* Get a specific appointment
|
| 113 |
+
*/
|
| 114 |
+
router.get('/appointments/:appointmentId', getCurrentUser, async (req, res) => {
|
| 115 |
+
try {
|
| 116 |
+
const { appointmentId } = req.params;
|
| 117 |
+
|
| 118 |
+
if (!isValidObjectId(appointmentId)) {
|
| 119 |
+
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
const appointment = await appointmentService.getAppointment(appointmentId);
|
| 123 |
+
|
| 124 |
+
if (!appointment) {
|
| 125 |
+
return res.status(404).json({ detail: 'Appointment not found' });
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
res.json(appointment);
|
| 129 |
+
} catch (error) {
|
| 130 |
+
res.status(500).json({ detail: `Error fetching appointment: ${error.message}` });
|
| 131 |
+
}
|
| 132 |
+
});
|
| 133 |
+
|
| 134 |
+
/**
|
| 135 |
+
* PUT /api/appointments/:appointmentId
|
| 136 |
+
* Update an appointment
|
| 137 |
+
*/
|
| 138 |
+
router.put('/appointments/:appointmentId', requireStaff, async (req, res) => {
|
| 139 |
+
try {
|
| 140 |
+
const { appointmentId } = req.params;
|
| 141 |
+
const updateData = req.body;
|
| 142 |
+
|
| 143 |
+
if (!isValidObjectId(appointmentId)) {
|
| 144 |
+
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Check for conflicts if time is being updated
|
| 148 |
+
const existing = await appointmentService.getAppointment(appointmentId);
|
| 149 |
+
if (!existing) {
|
| 150 |
+
return res.status(404).json({ detail: 'Appointment not found' });
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
if (updateData.date_time && existing.doctor_id) {
|
| 154 |
+
const conflicts = await appointmentService.checkConflicts(
|
| 155 |
+
existing.doctor_id,
|
| 156 |
+
updateData.date_time,
|
| 157 |
+
updateData.duration_minutes || existing.duration_minutes || 30,
|
| 158 |
+
appointmentId
|
| 159 |
+
);
|
| 160 |
+
|
| 161 |
+
if (conflicts.length > 0) {
|
| 162 |
+
return res.status(409).json({
|
| 163 |
+
detail: {
|
| 164 |
+
message: 'Scheduling conflict detected',
|
| 165 |
+
conflicts
|
| 166 |
+
}
|
| 167 |
+
});
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
const appointment = await appointmentService.updateAppointment(appointmentId, updateData);
|
| 172 |
+
|
| 173 |
+
if (!appointment) {
|
| 174 |
+
return res.status(404).json({ detail: 'Appointment not found' });
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
res.json(appointment);
|
| 178 |
+
} catch (error) {
|
| 179 |
+
res.status(500).json({ detail: `Error updating appointment: ${error.message}` });
|
| 180 |
+
}
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
/**
|
| 184 |
+
* POST /api/appointments/:appointmentId/cancel
|
| 185 |
+
* Cancel an appointment
|
| 186 |
+
*/
|
| 187 |
+
router.post('/appointments/:appointmentId/cancel', requireStaff, async (req, res) => {
|
| 188 |
+
try {
|
| 189 |
+
const { appointmentId } = req.params;
|
| 190 |
+
const { reason } = req.body || {};
|
| 191 |
+
|
| 192 |
+
if (!isValidObjectId(appointmentId)) {
|
| 193 |
+
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
const appointment = await appointmentService.cancelAppointment(appointmentId, reason);
|
| 197 |
+
|
| 198 |
+
if (!appointment) {
|
| 199 |
+
return res.status(404).json({ detail: 'Appointment not found' });
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
res.json(appointment);
|
| 203 |
+
} catch (error) {
|
| 204 |
+
res.status(500).json({ detail: `Error cancelling appointment: ${error.message}` });
|
| 205 |
+
}
|
| 206 |
+
});
|
| 207 |
+
|
| 208 |
+
/**
|
| 209 |
+
* POST /api/appointments/:appointmentId/complete
|
| 210 |
+
* Mark an appointment as completed
|
| 211 |
+
*/
|
| 212 |
+
router.post('/appointments/:appointmentId/complete', requireStaff, async (req, res) => {
|
| 213 |
+
try {
|
| 214 |
+
const { appointmentId } = req.params;
|
| 215 |
+
const { notes } = req.body || {};
|
| 216 |
+
|
| 217 |
+
if (!isValidObjectId(appointmentId)) {
|
| 218 |
+
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
const appointment = await appointmentService.completeAppointment(appointmentId, notes);
|
| 222 |
+
|
| 223 |
+
if (!appointment) {
|
| 224 |
+
return res.status(404).json({ detail: 'Appointment not found' });
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
res.json(appointment);
|
| 228 |
+
} catch (error) {
|
| 229 |
+
res.status(500).json({ detail: `Error completing appointment: ${error.message}` });
|
| 230 |
+
}
|
| 231 |
+
});
|
| 232 |
+
|
| 233 |
+
/**
|
| 234 |
+
* GET /api/appointments/:appointmentId/summary
|
| 235 |
+
* Generate an AI-powered pre-appointment summary
|
| 236 |
+
*/
|
| 237 |
+
router.get('/appointments/:appointmentId/summary', requireStaff, async (req, res) => {
|
| 238 |
+
try {
|
| 239 |
+
const { appointmentId } = req.params;
|
| 240 |
+
|
| 241 |
+
if (!isValidObjectId(appointmentId)) {
|
| 242 |
+
return res.status(400).json({ detail: 'Invalid appointment ID format' });
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
const summary = await appointmentService.generatePreAppointmentSummary(appointmentId);
|
| 246 |
+
|
| 247 |
+
if (summary.error) {
|
| 248 |
+
return res.status(500).json({ detail: summary.error });
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
res.json(summary);
|
| 252 |
+
} catch (error) {
|
| 253 |
+
res.status(500).json({ detail: `Error generating summary: ${error.message}` });
|
| 254 |
+
}
|
| 255 |
+
});
|
| 256 |
+
|
| 257 |
+
/**
|
| 258 |
+
* GET /api/patients/:patientId/appointments
|
| 259 |
+
* Get all appointments for a specific patient
|
| 260 |
+
*/
|
| 261 |
+
router.get('/patients/:patientId/appointments', getCurrentUser, async (req, res) => {
|
| 262 |
+
try {
|
| 263 |
+
const { patientId } = req.params;
|
| 264 |
+
const { include_past = 'false' } = req.query;
|
| 265 |
+
|
| 266 |
+
if (!isValidObjectId(patientId)) {
|
| 267 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
const appointments = await appointmentService.getPatientAppointments(
|
| 271 |
+
patientId,
|
| 272 |
+
include_past === 'true'
|
| 273 |
+
);
|
| 274 |
+
|
| 275 |
+
res.json(appointments);
|
| 276 |
+
} catch (error) {
|
| 277 |
+
res.status(500).json({ detail: `Error fetching appointments: ${error.message}` });
|
| 278 |
+
}
|
| 279 |
+
});
|
| 280 |
+
|
| 281 |
+
export default router;
|
| 282 |
+
|
routes/auth.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import { authService, ACCESS_TOKEN_EXPIRE_MINUTES } from '../services/authService.js';
|
| 3 |
+
import { getCurrentUser, requireDoctor, UserRole } from '../middleware/auth.js';
|
| 4 |
+
import { ObjectId, getUsersCollection } from '../db/mongo.js';
|
| 5 |
+
|
| 6 |
+
const router = express.Router();
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* POST /api/auth/register
|
| 10 |
+
* Register a new user account
|
| 11 |
+
*/
|
| 12 |
+
router.post('/register', async (req, res) => {
|
| 13 |
+
try {
|
| 14 |
+
const { email, name, password, role = 'doctor' } = req.body;
|
| 15 |
+
|
| 16 |
+
// Validation
|
| 17 |
+
if (!email || !name || !password) {
|
| 18 |
+
return res.status(400).json({ detail: 'Email, name, and password are required' });
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
if (password.length < 6) {
|
| 22 |
+
return res.status(400).json({ detail: 'Password must be at least 6 characters' });
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Validate role - only doctor and patient allowed
|
| 26 |
+
const validRoles = [UserRole.DOCTOR, UserRole.PATIENT];
|
| 27 |
+
if (!validRoles.includes(role)) {
|
| 28 |
+
return res.status(400).json({ detail: 'Invalid role. Must be "doctor" or "patient"' });
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const user = await authService.createUser({ email, name, password, role });
|
| 32 |
+
res.status(201).json(authService.userToResponse(user));
|
| 33 |
+
} catch (error) {
|
| 34 |
+
if (error.message.includes('already exists')) {
|
| 35 |
+
return res.status(400).json({ detail: error.message });
|
| 36 |
+
}
|
| 37 |
+
res.status(500).json({ detail: `Error creating user: ${error.message}` });
|
| 38 |
+
}
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* POST /api/auth/login
|
| 43 |
+
* Authenticate and receive a JWT token
|
| 44 |
+
*/
|
| 45 |
+
router.post('/login', async (req, res) => {
|
| 46 |
+
try {
|
| 47 |
+
const { email, password } = req.body;
|
| 48 |
+
|
| 49 |
+
if (!email || !password) {
|
| 50 |
+
return res.status(400).json({ detail: 'Email and password are required' });
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
const user = await authService.authenticateUser(email, password);
|
| 54 |
+
|
| 55 |
+
if (!user) {
|
| 56 |
+
return res.status(401).json({
|
| 57 |
+
detail: 'Incorrect email or password',
|
| 58 |
+
headers: { 'WWW-Authenticate': 'Bearer' }
|
| 59 |
+
});
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
if (!user.is_active) {
|
| 63 |
+
return res.status(403).json({ detail: 'User account is disabled' });
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Create access token
|
| 67 |
+
const accessToken = authService.createAccessToken({
|
| 68 |
+
sub: user._id.toString(),
|
| 69 |
+
email: user.email,
|
| 70 |
+
role: user.role
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
res.json({
|
| 74 |
+
access_token: accessToken,
|
| 75 |
+
token_type: 'bearer'
|
| 76 |
+
});
|
| 77 |
+
} catch (error) {
|
| 78 |
+
res.status(500).json({ detail: `Error during login: ${error.message}` });
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
/**
|
| 83 |
+
* GET /api/auth/me
|
| 84 |
+
* Get the current authenticated user's profile
|
| 85 |
+
*/
|
| 86 |
+
router.get('/me', getCurrentUser, async (req, res) => {
|
| 87 |
+
res.json(authService.userToResponse(req.user));
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
/**
|
| 91 |
+
* PUT /api/auth/me
|
| 92 |
+
* Update the current authenticated user's profile
|
| 93 |
+
*/
|
| 94 |
+
router.put('/me', getCurrentUser, async (req, res) => {
|
| 95 |
+
try {
|
| 96 |
+
const { name, role } = req.body;
|
| 97 |
+
const updateData = {};
|
| 98 |
+
|
| 99 |
+
if (name) updateData.name = name;
|
| 100 |
+
if (role) updateData.role = role;
|
| 101 |
+
|
| 102 |
+
const updatedUser = await authService.updateUser(req.user._id.toString(), updateData);
|
| 103 |
+
|
| 104 |
+
if (!updatedUser) {
|
| 105 |
+
return res.status(404).json({ detail: 'User not found' });
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
res.json(authService.userToResponse(updatedUser));
|
| 109 |
+
} catch (error) {
|
| 110 |
+
res.status(500).json({ detail: `Error updating user: ${error.message}` });
|
| 111 |
+
}
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
/**
|
| 115 |
+
* POST /api/auth/logout
|
| 116 |
+
* Logout the current user
|
| 117 |
+
*/
|
| 118 |
+
router.post('/logout', getCurrentUser, async (req, res) => {
|
| 119 |
+
res.json({
|
| 120 |
+
message: 'Successfully logged out',
|
| 121 |
+
user_id: req.user._id.toString()
|
| 122 |
+
});
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
/**
|
| 126 |
+
* GET /api/auth/users
|
| 127 |
+
* List all users (Doctor only)
|
| 128 |
+
*/
|
| 129 |
+
router.get('/users', requireDoctor, async (req, res) => {
|
| 130 |
+
try {
|
| 131 |
+
const usersCollection = getUsersCollection();
|
| 132 |
+
const users = await usersCollection.find().toArray();
|
| 133 |
+
res.json(users.map(user => authService.userToResponse(user)));
|
| 134 |
+
} catch (error) {
|
| 135 |
+
res.status(500).json({ detail: `Error fetching users: ${error.message}` });
|
| 136 |
+
}
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
/**
|
| 140 |
+
* DELETE /api/auth/users/:userId
|
| 141 |
+
* Delete a user (Doctor only)
|
| 142 |
+
*/
|
| 143 |
+
router.delete('/users/:userId', requireDoctor, async (req, res) => {
|
| 144 |
+
try {
|
| 145 |
+
const { userId } = req.params;
|
| 146 |
+
|
| 147 |
+
// Prevent self-deletion
|
| 148 |
+
if (req.user._id.toString() === userId) {
|
| 149 |
+
return res.status(400).json({ detail: 'Cannot delete your own account' });
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
const usersCollection = getUsersCollection();
|
| 153 |
+
const result = await usersCollection.deleteOne({ _id: new ObjectId(userId) });
|
| 154 |
+
|
| 155 |
+
if (result.deletedCount === 0) {
|
| 156 |
+
return res.status(404).json({ detail: 'User not found' });
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
res.json({ message: 'User deleted successfully', id: userId });
|
| 160 |
+
} catch (error) {
|
| 161 |
+
res.status(500).json({ detail: `Error deleting user: ${error.message}` });
|
| 162 |
+
}
|
| 163 |
+
});
|
| 164 |
+
|
| 165 |
+
export default router;
|
| 166 |
+
|
routes/patients.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import { ObjectId, getPatientsCollection } from '../db/mongo.js';
|
| 3 |
+
import { summarizePatient } from '../services/aiService.js';
|
| 4 |
+
import { ragService } from '../services/ragService.js';
|
| 5 |
+
import { vectorService } from '../services/vectorService.js';
|
| 6 |
+
import { timelineService } from '../services/timelineService.js';
|
| 7 |
+
|
| 8 |
+
const router = express.Router();
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* Helper to format patient document for response
|
| 12 |
+
*/
|
| 13 |
+
const patientHelper = (patient) => ({
|
| 14 |
+
_id: patient._id.toString(),
|
| 15 |
+
name: patient.name || '',
|
| 16 |
+
age: patient.age || 0,
|
| 17 |
+
gender: patient.gender || '',
|
| 18 |
+
medical_history: patient.medical_history || [],
|
| 19 |
+
medications: patient.medications || [],
|
| 20 |
+
date_of_birth: patient.date_of_birth || null,
|
| 21 |
+
allergies: patient.allergies || [],
|
| 22 |
+
blood_type: patient.blood_type || null,
|
| 23 |
+
notes: patient.notes || null
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Validate ObjectId format
|
| 28 |
+
*/
|
| 29 |
+
const isValidObjectId = (id) => {
|
| 30 |
+
try {
|
| 31 |
+
return ObjectId.isValid(id) && new ObjectId(id).toString() === id;
|
| 32 |
+
} catch {
|
| 33 |
+
return false;
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* POST /api/patients/
|
| 39 |
+
* Create a new patient
|
| 40 |
+
*/
|
| 41 |
+
router.post('/patients/', async (req, res) => {
|
| 42 |
+
try {
|
| 43 |
+
const patientsCollection = getPatientsCollection();
|
| 44 |
+
const patientData = req.body;
|
| 45 |
+
|
| 46 |
+
// Validation
|
| 47 |
+
if (!patientData.name || patientData.name.length < 1) {
|
| 48 |
+
return res.status(400).json({ detail: 'Name is required' });
|
| 49 |
+
}
|
| 50 |
+
if (patientData.age === undefined || patientData.age < 0 || patientData.age > 150) {
|
| 51 |
+
return res.status(400).json({ detail: 'Valid age (0-150) is required' });
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const result = await patientsCollection.insertOne(patientData);
|
| 55 |
+
const patientId = result.insertedId.toString();
|
| 56 |
+
|
| 57 |
+
// Index patient data in vector database
|
| 58 |
+
try {
|
| 59 |
+
await vectorService.addPatientRecord(patientId, patientData);
|
| 60 |
+
} catch (error) {
|
| 61 |
+
console.log(`Warning: Failed to index patient in vector DB: ${error.message}`);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
res.status(201).json({ id: patientId, message: 'Patient created successfully' });
|
| 65 |
+
} catch (error) {
|
| 66 |
+
res.status(400).json({ detail: `Error creating patient: ${error.message}` });
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
/**
|
| 71 |
+
* GET /api/patients/
|
| 72 |
+
* List all patients
|
| 73 |
+
*/
|
| 74 |
+
router.get('/patients/', async (req, res) => {
|
| 75 |
+
try {
|
| 76 |
+
const patientsCollection = getPatientsCollection();
|
| 77 |
+
const patients = await patientsCollection.find().toArray();
|
| 78 |
+
res.json(patients.map(patientHelper));
|
| 79 |
+
} catch (error) {
|
| 80 |
+
res.status(500).json({ detail: `Error fetching patients: ${error.message}` });
|
| 81 |
+
}
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
/**
|
| 85 |
+
* GET /api/patients/search/semantic
|
| 86 |
+
* Semantic search across patients
|
| 87 |
+
*/
|
| 88 |
+
router.get('/patients/search/semantic', async (req, res) => {
|
| 89 |
+
try {
|
| 90 |
+
const { query, limit = 5 } = req.query;
|
| 91 |
+
|
| 92 |
+
if (!query) {
|
| 93 |
+
return res.status(400).json({ detail: 'Query parameter is required' });
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
const results = await vectorService.searchPatientRecords(query, null, parseInt(limit));
|
| 97 |
+
res.json({
|
| 98 |
+
query,
|
| 99 |
+
results,
|
| 100 |
+
count: results.length
|
| 101 |
+
});
|
| 102 |
+
} catch (error) {
|
| 103 |
+
res.status(500).json({ detail: `Error performing semantic search: ${error.message}` });
|
| 104 |
+
}
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
/**
|
| 108 |
+
* GET /api/patients/:patientId
|
| 109 |
+
* Get a specific patient
|
| 110 |
+
*/
|
| 111 |
+
router.get('/patients/:patientId', async (req, res) => {
|
| 112 |
+
try {
|
| 113 |
+
const { patientId } = req.params;
|
| 114 |
+
|
| 115 |
+
if (!isValidObjectId(patientId)) {
|
| 116 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
const patientsCollection = getPatientsCollection();
|
| 120 |
+
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 121 |
+
|
| 122 |
+
if (!patient) {
|
| 123 |
+
return res.status(404).json({ detail: 'Patient not found' });
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
res.json(patientHelper(patient));
|
| 127 |
+
} catch (error) {
|
| 128 |
+
res.status(500).json({ detail: `Error fetching patient: ${error.message}` });
|
| 129 |
+
}
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
/**
|
| 133 |
+
* PUT /api/patients/:patientId
|
| 134 |
+
* Update a patient
|
| 135 |
+
*/
|
| 136 |
+
router.put('/patients/:patientId', async (req, res) => {
|
| 137 |
+
try {
|
| 138 |
+
const { patientId } = req.params;
|
| 139 |
+
|
| 140 |
+
if (!isValidObjectId(patientId)) {
|
| 141 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
const patientsCollection = getPatientsCollection();
|
| 145 |
+
const updateData = req.body;
|
| 146 |
+
|
| 147 |
+
const result = await patientsCollection.updateOne(
|
| 148 |
+
{ _id: new ObjectId(patientId) },
|
| 149 |
+
{ $set: updateData }
|
| 150 |
+
);
|
| 151 |
+
|
| 152 |
+
if (result.matchedCount === 0) {
|
| 153 |
+
return res.status(404).json({ detail: 'Patient not found' });
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
const updatedPatient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 157 |
+
|
| 158 |
+
// Re-index patient data in vector database
|
| 159 |
+
try {
|
| 160 |
+
await vectorService.deletePatientRecords(patientId);
|
| 161 |
+
await vectorService.addPatientRecord(patientId, patientHelper(updatedPatient));
|
| 162 |
+
} catch (error) {
|
| 163 |
+
console.log(`Warning: Failed to re-index patient in vector DB: ${error.message}`);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
res.json(patientHelper(updatedPatient));
|
| 167 |
+
} catch (error) {
|
| 168 |
+
res.status(500).json({ detail: `Error updating patient: ${error.message}` });
|
| 169 |
+
}
|
| 170 |
+
});
|
| 171 |
+
|
| 172 |
+
/**
|
| 173 |
+
* DELETE /api/patients/:patientId
|
| 174 |
+
* Delete a patient
|
| 175 |
+
*/
|
| 176 |
+
router.delete('/patients/:patientId', async (req, res) => {
|
| 177 |
+
try {
|
| 178 |
+
const { patientId } = req.params;
|
| 179 |
+
|
| 180 |
+
if (!isValidObjectId(patientId)) {
|
| 181 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
const patientsCollection = getPatientsCollection();
|
| 185 |
+
const result = await patientsCollection.deleteOne({ _id: new ObjectId(patientId) });
|
| 186 |
+
|
| 187 |
+
if (result.deletedCount === 0) {
|
| 188 |
+
return res.status(404).json({ detail: 'Patient not found' });
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
res.json({ message: 'Patient deleted successfully', id: patientId });
|
| 192 |
+
} catch (error) {
|
| 193 |
+
res.status(500).json({ detail: `Error deleting patient: ${error.message}` });
|
| 194 |
+
}
|
| 195 |
+
});
|
| 196 |
+
|
| 197 |
+
/**
|
| 198 |
+
* GET /api/patients/:patientId/summary
|
| 199 |
+
* Get AI-generated patient summary
|
| 200 |
+
*/
|
| 201 |
+
router.get('/patients/:patientId/summary', async (req, res) => {
|
| 202 |
+
try {
|
| 203 |
+
const { patientId } = req.params;
|
| 204 |
+
const { query } = req.query;
|
| 205 |
+
|
| 206 |
+
if (!query) {
|
| 207 |
+
return res.status(400).json({ detail: 'Query parameter is required' });
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
if (!isValidObjectId(patientId)) {
|
| 211 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
const patientsCollection = getPatientsCollection();
|
| 215 |
+
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 216 |
+
|
| 217 |
+
if (!patient) {
|
| 218 |
+
return res.status(404).json({ detail: 'Patient not found' });
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
const summary = await summarizePatient(patient, query);
|
| 222 |
+
res.json(summary);
|
| 223 |
+
} catch (error) {
|
| 224 |
+
res.status(500).json({ detail: `Error generating summary: ${error.message}` });
|
| 225 |
+
}
|
| 226 |
+
});
|
| 227 |
+
|
| 228 |
+
/**
|
| 229 |
+
* GET /api/patients/:patientId/rag-summary
|
| 230 |
+
* Get RAG-enhanced patient summary
|
| 231 |
+
*/
|
| 232 |
+
router.get('/patients/:patientId/rag-summary', async (req, res) => {
|
| 233 |
+
try {
|
| 234 |
+
const { patientId } = req.params;
|
| 235 |
+
const { query } = req.query;
|
| 236 |
+
|
| 237 |
+
if (!query) {
|
| 238 |
+
return res.status(400).json({ detail: 'Query parameter is required' });
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
if (!isValidObjectId(patientId)) {
|
| 242 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
const patientsCollection = getPatientsCollection();
|
| 246 |
+
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 247 |
+
|
| 248 |
+
if (!patient) {
|
| 249 |
+
return res.status(404).json({ detail: 'Patient not found' });
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
patient._id = patient._id.toString();
|
| 253 |
+
const summary = await ragService.generateRAGResponse(patientId, patient, query);
|
| 254 |
+
res.json(summary);
|
| 255 |
+
} catch (error) {
|
| 256 |
+
res.status(500).json({ detail: `Error generating RAG summary: ${error.message}` });
|
| 257 |
+
}
|
| 258 |
+
});
|
| 259 |
+
|
| 260 |
+
/**
|
| 261 |
+
* GET /api/patients/:patientId/suggest-treatment
|
| 262 |
+
* Get treatment suggestions for a condition
|
| 263 |
+
*/
|
| 264 |
+
router.get('/patients/:patientId/suggest-treatment', async (req, res) => {
|
| 265 |
+
try {
|
| 266 |
+
const { patientId } = req.params;
|
| 267 |
+
const { condition } = req.query;
|
| 268 |
+
|
| 269 |
+
if (!condition) {
|
| 270 |
+
return res.status(400).json({ detail: 'Condition parameter is required' });
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
if (!isValidObjectId(patientId)) {
|
| 274 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
const patientsCollection = getPatientsCollection();
|
| 278 |
+
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 279 |
+
|
| 280 |
+
if (!patient) {
|
| 281 |
+
return res.status(404).json({ detail: 'Patient not found' });
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
patient._id = patient._id.toString();
|
| 285 |
+
const suggestions = await ragService.suggestTreatments(patientId, patient, condition);
|
| 286 |
+
res.json(suggestions);
|
| 287 |
+
} catch (error) {
|
| 288 |
+
res.status(500).json({ detail: `Error suggesting treatment: ${error.message}` });
|
| 289 |
+
}
|
| 290 |
+
});
|
| 291 |
+
|
| 292 |
+
/**
|
| 293 |
+
* GET /api/patients/:patientId/drug-interactions
|
| 294 |
+
* Analyze drug interactions for new medication
|
| 295 |
+
*/
|
| 296 |
+
router.get('/patients/:patientId/drug-interactions', async (req, res) => {
|
| 297 |
+
try {
|
| 298 |
+
const { patientId } = req.params;
|
| 299 |
+
const { new_medication } = req.query;
|
| 300 |
+
|
| 301 |
+
if (!new_medication) {
|
| 302 |
+
return res.status(400).json({ detail: 'new_medication parameter is required' });
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
if (!isValidObjectId(patientId)) {
|
| 306 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
const patientsCollection = getPatientsCollection();
|
| 310 |
+
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 311 |
+
|
| 312 |
+
if (!patient) {
|
| 313 |
+
return res.status(404).json({ detail: 'Patient not found' });
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
patient._id = patient._id.toString();
|
| 317 |
+
const analysis = await ragService.analyzeDrugInteractions(patientId, patient, new_medication);
|
| 318 |
+
res.json(analysis);
|
| 319 |
+
} catch (error) {
|
| 320 |
+
res.status(500).json({ detail: `Error analyzing drug interactions: ${error.message}` });
|
| 321 |
+
}
|
| 322 |
+
});
|
| 323 |
+
|
| 324 |
+
/**
|
| 325 |
+
* GET /api/vector-db/stats
|
| 326 |
+
* Get vector database statistics
|
| 327 |
+
*/
|
| 328 |
+
router.get('/vector-db/stats', async (req, res) => {
|
| 329 |
+
try {
|
| 330 |
+
const stats = await vectorService.getCollectionStats();
|
| 331 |
+
res.json(stats);
|
| 332 |
+
} catch (error) {
|
| 333 |
+
res.status(500).json({ detail: `Error fetching stats: ${error.message}` });
|
| 334 |
+
}
|
| 335 |
+
});
|
| 336 |
+
|
| 337 |
+
/**
|
| 338 |
+
* POST /api/vector-db/reindex
|
| 339 |
+
* Reindex all patients into the vector database
|
| 340 |
+
*/
|
| 341 |
+
router.post('/vector-db/reindex', async (req, res) => {
|
| 342 |
+
try {
|
| 343 |
+
const patientsCollection = getPatientsCollection();
|
| 344 |
+
const patients = await patientsCollection.find().toArray();
|
| 345 |
+
let indexedCount = 0;
|
| 346 |
+
const errors = [];
|
| 347 |
+
|
| 348 |
+
for (const patient of patients) {
|
| 349 |
+
try {
|
| 350 |
+
const patientId = patient._id.toString();
|
| 351 |
+
const patientData = patientHelper(patient);
|
| 352 |
+
await vectorService.deletePatientRecords(patientId);
|
| 353 |
+
await vectorService.addPatientRecord(patientId, patientData);
|
| 354 |
+
indexedCount++;
|
| 355 |
+
} catch (error) {
|
| 356 |
+
errors.push(`${patient.name || 'Unknown'}: ${error.message}`);
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
res.json({
|
| 361 |
+
message: `Reindexed ${indexedCount} patients`,
|
| 362 |
+
total_patients: patients.length,
|
| 363 |
+
indexed: indexedCount,
|
| 364 |
+
errors: errors.length > 0 ? errors : null
|
| 365 |
+
});
|
| 366 |
+
} catch (error) {
|
| 367 |
+
res.status(500).json({ detail: `Error reindexing: ${error.message}` });
|
| 368 |
+
}
|
| 369 |
+
});
|
| 370 |
+
|
| 371 |
+
// Timeline Routes
|
| 372 |
+
|
| 373 |
+
/**
|
| 374 |
+
* GET /api/patients/:patientId/timeline
|
| 375 |
+
* Get the complete timeline of events for a patient
|
| 376 |
+
*/
|
| 377 |
+
router.get('/patients/:patientId/timeline', async (req, res) => {
|
| 378 |
+
try {
|
| 379 |
+
const { patientId } = req.params;
|
| 380 |
+
|
| 381 |
+
if (!isValidObjectId(patientId)) {
|
| 382 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
const timeline = await timelineService.getPatientTimeline(patientId);
|
| 386 |
+
res.json({ patient_id: patientId, events: timeline, count: timeline.length });
|
| 387 |
+
} catch (error) {
|
| 388 |
+
res.status(500).json({ detail: `Error fetching timeline: ${error.message}` });
|
| 389 |
+
}
|
| 390 |
+
});
|
| 391 |
+
|
| 392 |
+
/**
|
| 393 |
+
* GET /api/patients/:patientId/timeline/summary
|
| 394 |
+
* Get an AI-generated summary of the patient's medical timeline
|
| 395 |
+
*/
|
| 396 |
+
router.get('/patients/:patientId/timeline/summary', async (req, res) => {
|
| 397 |
+
try {
|
| 398 |
+
const { patientId } = req.params;
|
| 399 |
+
|
| 400 |
+
if (!isValidObjectId(patientId)) {
|
| 401 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
const summary = await timelineService.generateTimelineSummary(patientId);
|
| 405 |
+
res.json(summary);
|
| 406 |
+
} catch (error) {
|
| 407 |
+
res.status(500).json({ detail: `Error generating timeline summary: ${error.message}` });
|
| 408 |
+
}
|
| 409 |
+
});
|
| 410 |
+
|
| 411 |
+
/**
|
| 412 |
+
* GET /api/patients/:patientId/timeline/stats
|
| 413 |
+
* Get statistics about the patient's timeline
|
| 414 |
+
*/
|
| 415 |
+
router.get('/patients/:patientId/timeline/stats', async (req, res) => {
|
| 416 |
+
try {
|
| 417 |
+
const { patientId } = req.params;
|
| 418 |
+
|
| 419 |
+
if (!isValidObjectId(patientId)) {
|
| 420 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
const stats = await timelineService.getTimelineStats(patientId);
|
| 424 |
+
res.json(stats);
|
| 425 |
+
} catch (error) {
|
| 426 |
+
res.status(500).json({ detail: `Error fetching timeline stats: ${error.message}` });
|
| 427 |
+
}
|
| 428 |
+
});
|
| 429 |
+
|
| 430 |
+
export default router;
|
| 431 |
+
|
routes/warnings.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import { ObjectId } from '../db/mongo.js';
|
| 3 |
+
import { warningService } from '../services/warningService.js';
|
| 4 |
+
import { getCurrentUser, requireStaff } from '../middleware/auth.js';
|
| 5 |
+
|
| 6 |
+
const router = express.Router();
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Validate ObjectId format
|
| 10 |
+
*/
|
| 11 |
+
const isValidObjectId = (id) => {
|
| 12 |
+
try {
|
| 13 |
+
return ObjectId.isValid(id) && new ObjectId(id).toString() === id;
|
| 14 |
+
} catch {
|
| 15 |
+
return false;
|
| 16 |
+
}
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* GET /api/patients/:patientId/warnings
|
| 21 |
+
* Get all clinical warnings for a specific patient
|
| 22 |
+
*/
|
| 23 |
+
router.get('/patients/:patientId/warnings', getCurrentUser, async (req, res) => {
|
| 24 |
+
try {
|
| 25 |
+
const { patientId } = req.params;
|
| 26 |
+
const { include_acknowledged = 'false' } = req.query;
|
| 27 |
+
|
| 28 |
+
if (!isValidObjectId(patientId)) {
|
| 29 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const warnings = await warningService.getPatientWarnings(
|
| 33 |
+
patientId,
|
| 34 |
+
include_acknowledged === 'true'
|
| 35 |
+
);
|
| 36 |
+
|
| 37 |
+
res.json(warnings);
|
| 38 |
+
} catch (error) {
|
| 39 |
+
res.status(500).json({ detail: `Error fetching warnings: ${error.message}` });
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* POST /api/patients/:patientId/warnings/analyze
|
| 45 |
+
* Trigger AI analysis to generate clinical warnings for a patient
|
| 46 |
+
*/
|
| 47 |
+
router.post('/patients/:patientId/warnings/analyze', requireStaff, async (req, res) => {
|
| 48 |
+
try {
|
| 49 |
+
const { patientId } = req.params;
|
| 50 |
+
const { new_medication, new_condition } = req.body || {};
|
| 51 |
+
|
| 52 |
+
if (!isValidObjectId(patientId)) {
|
| 53 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
const warnings = await warningService.analyzePatientForWarnings(
|
| 57 |
+
patientId,
|
| 58 |
+
new_medication,
|
| 59 |
+
new_condition
|
| 60 |
+
);
|
| 61 |
+
|
| 62 |
+
res.json(warnings);
|
| 63 |
+
} catch (error) {
|
| 64 |
+
res.status(500).json({ detail: `Error analyzing patient: ${error.message}` });
|
| 65 |
+
}
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
/**
|
| 69 |
+
* PUT /api/warnings/:warningId/acknowledge
|
| 70 |
+
* Acknowledge a clinical warning
|
| 71 |
+
*/
|
| 72 |
+
router.put('/warnings/:warningId/acknowledge', requireStaff, async (req, res) => {
|
| 73 |
+
try {
|
| 74 |
+
const { warningId } = req.params;
|
| 75 |
+
const { notes } = req.body || {};
|
| 76 |
+
|
| 77 |
+
if (!isValidObjectId(warningId)) {
|
| 78 |
+
return res.status(400).json({ detail: 'Invalid warning ID format' });
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
const userId = req.user._id.toString();
|
| 82 |
+
const warning = await warningService.acknowledgeWarning(warningId, userId, notes);
|
| 83 |
+
|
| 84 |
+
if (!warning) {
|
| 85 |
+
return res.status(404).json({ detail: 'Warning not found' });
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
res.json(warning);
|
| 89 |
+
} catch (error) {
|
| 90 |
+
res.status(500).json({ detail: `Error acknowledging warning: ${error.message}` });
|
| 91 |
+
}
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
/**
|
| 95 |
+
* GET /api/warnings/unacknowledged
|
| 96 |
+
* Get all unacknowledged warnings across all patients
|
| 97 |
+
*/
|
| 98 |
+
router.get('/warnings/unacknowledged', getCurrentUser, async (req, res) => {
|
| 99 |
+
try {
|
| 100 |
+
const { limit = 50 } = req.query;
|
| 101 |
+
|
| 102 |
+
const warnings = await warningService.getAllUnacknowledgedWarnings(
|
| 103 |
+
Math.min(parseInt(limit), 100)
|
| 104 |
+
);
|
| 105 |
+
|
| 106 |
+
res.json(warnings);
|
| 107 |
+
} catch (error) {
|
| 108 |
+
res.status(500).json({ detail: `Error fetching warnings: ${error.message}` });
|
| 109 |
+
}
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
/**
|
| 113 |
+
* GET /api/warnings/stats
|
| 114 |
+
* Get warning statistics
|
| 115 |
+
*/
|
| 116 |
+
router.get('/warnings/stats', getCurrentUser, async (req, res) => {
|
| 117 |
+
try {
|
| 118 |
+
const stats = await warningService.getWarningStats();
|
| 119 |
+
res.json(stats);
|
| 120 |
+
} catch (error) {
|
| 121 |
+
res.status(500).json({ detail: `Error fetching stats: ${error.message}` });
|
| 122 |
+
}
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
/**
|
| 126 |
+
* DELETE /api/patients/:patientId/warnings
|
| 127 |
+
* Delete all warnings for a patient (Staff only)
|
| 128 |
+
*/
|
| 129 |
+
router.delete('/patients/:patientId/warnings', requireStaff, async (req, res) => {
|
| 130 |
+
try {
|
| 131 |
+
const { patientId } = req.params;
|
| 132 |
+
|
| 133 |
+
if (!isValidObjectId(patientId)) {
|
| 134 |
+
return res.status(400).json({ detail: 'Invalid patient ID format' });
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
const deletedCount = await warningService.deletePatientWarnings(patientId);
|
| 138 |
+
res.json({ message: `Deleted ${deletedCount} warnings`, deleted_count: deletedCount });
|
| 139 |
+
} catch (error) {
|
| 140 |
+
res.status(500).json({ detail: `Error deleting warnings: ${error.message}` });
|
| 141 |
+
}
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
export default router;
|
| 145 |
+
|
services/aiService.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { generateResponse } from './llmClient.js';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Summarize patient data based on a query using LLM
|
| 5 |
+
*/
|
| 6 |
+
export const summarizePatient = async (patientData, query) => {
|
| 7 |
+
const prompt = `
|
| 8 |
+
You are a medical assistant AI.
|
| 9 |
+
Analyze the following patient data and answer the query in a concise,
|
| 10 |
+
structured, and professional format.
|
| 11 |
+
|
| 12 |
+
Patient data: ${JSON.stringify(patientData)}
|
| 13 |
+
Question: ${query}
|
| 14 |
+
`;
|
| 15 |
+
|
| 16 |
+
try {
|
| 17 |
+
const content = await generateResponse(prompt);
|
| 18 |
+
return { content, role: 'assistant' };
|
| 19 |
+
} catch (error) {
|
| 20 |
+
return { content: `Error generating summary: ${error.message}`, error: true };
|
| 21 |
+
}
|
| 22 |
+
};
|
services/appointmentService.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { generateResponse } from './llmClient.js';
|
| 2 |
+
import { ObjectId, getAppointmentsCollection, getPatientsCollection, getUsersCollection } from '../db/mongo.js';
|
| 3 |
+
import dotenv from 'dotenv';
|
| 4 |
+
|
| 5 |
+
dotenv.config();
|
| 6 |
+
|
| 7 |
+
// Appointment status enum
|
| 8 |
+
export const AppointmentStatus = {
|
| 9 |
+
SCHEDULED: 'scheduled',
|
| 10 |
+
CONFIRMED: 'confirmed',
|
| 11 |
+
IN_PROGRESS: 'in_progress',
|
| 12 |
+
COMPLETED: 'completed',
|
| 13 |
+
CANCELLED: 'cancelled',
|
| 14 |
+
NO_SHOW: 'no_show'
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
class AppointmentService {
|
| 18 |
+
/**
|
| 19 |
+
* Convert MongoDB appointment document to response format
|
| 20 |
+
*/
|
| 21 |
+
_appointmentHelper(appointment) {
|
| 22 |
+
return {
|
| 23 |
+
id: appointment._id.toString(),
|
| 24 |
+
patient_id: appointment.patient_id,
|
| 25 |
+
doctor_id: appointment.doctor_id || null,
|
| 26 |
+
date_time: appointment.date_time,
|
| 27 |
+
duration_minutes: appointment.duration_minutes || 30,
|
| 28 |
+
appointment_type: appointment.appointment_type || 'checkup',
|
| 29 |
+
purpose: appointment.purpose || '',
|
| 30 |
+
notes: appointment.notes || null,
|
| 31 |
+
location: appointment.location || null,
|
| 32 |
+
status: appointment.status || 'scheduled',
|
| 33 |
+
created_at: appointment.created_at,
|
| 34 |
+
updated_at: appointment.updated_at,
|
| 35 |
+
created_by: appointment.created_by || '',
|
| 36 |
+
cancelled_reason: appointment.cancelled_reason || null,
|
| 37 |
+
patient_name: appointment.patient_name || null,
|
| 38 |
+
doctor_name: appointment.doctor_name || null
|
| 39 |
+
};
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Add patient and doctor names to appointment
|
| 44 |
+
*/
|
| 45 |
+
async _enrichWithNames(appointment) {
|
| 46 |
+
const patientsCollection = getPatientsCollection();
|
| 47 |
+
const usersCollection = getUsersCollection();
|
| 48 |
+
|
| 49 |
+
// Get patient name
|
| 50 |
+
try {
|
| 51 |
+
const patient = await patientsCollection.findOne({ _id: new ObjectId(appointment.patient_id) });
|
| 52 |
+
appointment.patient_name = patient ? patient.name : null;
|
| 53 |
+
} catch {
|
| 54 |
+
appointment.patient_name = null;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// Get doctor name
|
| 58 |
+
if (appointment.doctor_id) {
|
| 59 |
+
try {
|
| 60 |
+
const doctor = await usersCollection.findOne({ _id: new ObjectId(appointment.doctor_id) });
|
| 61 |
+
appointment.doctor_name = doctor ? doctor.name : null;
|
| 62 |
+
} catch {
|
| 63 |
+
appointment.doctor_name = null;
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
return appointment;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/**
|
| 71 |
+
* Create a new appointment
|
| 72 |
+
*/
|
| 73 |
+
async createAppointment(appointmentData, userId) {
|
| 74 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 75 |
+
const now = new Date();
|
| 76 |
+
|
| 77 |
+
const appointmentDoc = {
|
| 78 |
+
...appointmentData,
|
| 79 |
+
status: AppointmentStatus.SCHEDULED,
|
| 80 |
+
created_at: now,
|
| 81 |
+
updated_at: now,
|
| 82 |
+
created_by: userId
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const result = await appointmentsCollection.insertOne(appointmentDoc);
|
| 86 |
+
appointmentDoc._id = result.insertedId;
|
| 87 |
+
const enriched = await this._enrichWithNames(appointmentDoc);
|
| 88 |
+
|
| 89 |
+
return this._appointmentHelper(enriched);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/**
|
| 93 |
+
* Get a single appointment by ID
|
| 94 |
+
*/
|
| 95 |
+
async getAppointment(appointmentId) {
|
| 96 |
+
try {
|
| 97 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 98 |
+
const appointment = await appointmentsCollection.findOne({ _id: new ObjectId(appointmentId) });
|
| 99 |
+
|
| 100 |
+
if (appointment) {
|
| 101 |
+
const enriched = await this._enrichWithNames(appointment);
|
| 102 |
+
return this._appointmentHelper(enriched);
|
| 103 |
+
}
|
| 104 |
+
return null;
|
| 105 |
+
} catch {
|
| 106 |
+
return null;
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/**
|
| 111 |
+
* List appointments with optional filters
|
| 112 |
+
*/
|
| 113 |
+
async listAppointments({ patientId, doctorId, status, fromDate, toDate, limit = 50 }) {
|
| 114 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 115 |
+
const query = {};
|
| 116 |
+
|
| 117 |
+
if (patientId) query.patient_id = patientId;
|
| 118 |
+
if (doctorId) query.doctor_id = doctorId;
|
| 119 |
+
if (status) query.status = status;
|
| 120 |
+
|
| 121 |
+
if (fromDate || toDate) {
|
| 122 |
+
query.date_time = {};
|
| 123 |
+
if (fromDate) query.date_time.$gte = fromDate;
|
| 124 |
+
if (toDate) query.date_time.$lte = toDate;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
const appointments = await appointmentsCollection
|
| 128 |
+
.find(query)
|
| 129 |
+
.sort({ date_time: 1 })
|
| 130 |
+
.limit(limit)
|
| 131 |
+
.toArray();
|
| 132 |
+
|
| 133 |
+
const result = [];
|
| 134 |
+
for (const apt of appointments) {
|
| 135 |
+
const enriched = await this._enrichWithNames(apt);
|
| 136 |
+
result.push(this._appointmentHelper(enriched));
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
return result;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
/**
|
| 143 |
+
* Get upcoming appointments
|
| 144 |
+
*/
|
| 145 |
+
async getUpcomingAppointments(limit = 20) {
|
| 146 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 147 |
+
const now = new Date().toISOString();
|
| 148 |
+
|
| 149 |
+
const appointments = await appointmentsCollection
|
| 150 |
+
.find({
|
| 151 |
+
date_time: { $gte: now },
|
| 152 |
+
status: { $in: ['scheduled', 'confirmed'] }
|
| 153 |
+
})
|
| 154 |
+
.sort({ date_time: 1 })
|
| 155 |
+
.limit(limit)
|
| 156 |
+
.toArray();
|
| 157 |
+
|
| 158 |
+
const result = [];
|
| 159 |
+
for (const apt of appointments) {
|
| 160 |
+
const enriched = await this._enrichWithNames(apt);
|
| 161 |
+
result.push(this._appointmentHelper(enriched));
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
return result;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/**
|
| 168 |
+
* Get all appointments for a specific patient
|
| 169 |
+
*/
|
| 170 |
+
async getPatientAppointments(patientId, includePast = false) {
|
| 171 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 172 |
+
const query = { patient_id: patientId };
|
| 173 |
+
|
| 174 |
+
if (!includePast) {
|
| 175 |
+
const now = new Date().toISOString();
|
| 176 |
+
query.$or = [
|
| 177 |
+
{ date_time: { $gte: now } },
|
| 178 |
+
{ status: { $in: ['scheduled', 'confirmed'] } }
|
| 179 |
+
];
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
const appointments = await appointmentsCollection
|
| 183 |
+
.find(query)
|
| 184 |
+
.sort({ date_time: -1 })
|
| 185 |
+
.toArray();
|
| 186 |
+
|
| 187 |
+
const result = [];
|
| 188 |
+
for (const apt of appointments) {
|
| 189 |
+
const enriched = await this._enrichWithNames(apt);
|
| 190 |
+
result.push(this._appointmentHelper(enriched));
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
return result;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/**
|
| 197 |
+
* Update an appointment
|
| 198 |
+
*/
|
| 199 |
+
async updateAppointment(appointmentId, updateData) {
|
| 200 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 201 |
+
const updateDict = { ...updateData, updated_at: new Date() };
|
| 202 |
+
|
| 203 |
+
const result = await appointmentsCollection.updateOne(
|
| 204 |
+
{ _id: new ObjectId(appointmentId) },
|
| 205 |
+
{ $set: updateDict }
|
| 206 |
+
);
|
| 207 |
+
|
| 208 |
+
if (result.matchedCount === 0) {
|
| 209 |
+
return null;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
return await this.getAppointment(appointmentId);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/**
|
| 216 |
+
* Cancel an appointment
|
| 217 |
+
*/
|
| 218 |
+
async cancelAppointment(appointmentId, reason = null) {
|
| 219 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 220 |
+
|
| 221 |
+
const result = await appointmentsCollection.updateOne(
|
| 222 |
+
{ _id: new ObjectId(appointmentId) },
|
| 223 |
+
{
|
| 224 |
+
$set: {
|
| 225 |
+
status: AppointmentStatus.CANCELLED,
|
| 226 |
+
cancelled_reason: reason,
|
| 227 |
+
updated_at: new Date()
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
);
|
| 231 |
+
|
| 232 |
+
if (result.matchedCount === 0) {
|
| 233 |
+
return null;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
return await this.getAppointment(appointmentId);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/**
|
| 240 |
+
* Mark an appointment as completed
|
| 241 |
+
*/
|
| 242 |
+
async completeAppointment(appointmentId, notes = null) {
|
| 243 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 244 |
+
const updateData = {
|
| 245 |
+
status: AppointmentStatus.COMPLETED,
|
| 246 |
+
updated_at: new Date()
|
| 247 |
+
};
|
| 248 |
+
|
| 249 |
+
if (notes) {
|
| 250 |
+
updateData.notes = notes;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
const result = await appointmentsCollection.updateOne(
|
| 254 |
+
{ _id: new ObjectId(appointmentId) },
|
| 255 |
+
{ $set: updateData }
|
| 256 |
+
);
|
| 257 |
+
|
| 258 |
+
if (result.matchedCount === 0) {
|
| 259 |
+
return null;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
return await this.getAppointment(appointmentId);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
/**
|
| 266 |
+
* Check for scheduling conflicts
|
| 267 |
+
*/
|
| 268 |
+
async checkConflicts(doctorId, dateTime, durationMinutes, excludeId = null) {
|
| 269 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 270 |
+
|
| 271 |
+
// Parse the proposed datetime
|
| 272 |
+
let proposedStart, proposedEnd;
|
| 273 |
+
try {
|
| 274 |
+
proposedStart = new Date(dateTime.replace('Z', '+00:00'));
|
| 275 |
+
proposedEnd = new Date(proposedStart.getTime() + durationMinutes * 60000);
|
| 276 |
+
} catch {
|
| 277 |
+
return [];
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
// Find overlapping appointments
|
| 281 |
+
const query = {
|
| 282 |
+
doctor_id: doctorId,
|
| 283 |
+
status: { $in: ['scheduled', 'confirmed'] }
|
| 284 |
+
};
|
| 285 |
+
|
| 286 |
+
if (excludeId) {
|
| 287 |
+
query._id = { $ne: new ObjectId(excludeId) };
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
const appointments = await appointmentsCollection.find(query).toArray();
|
| 291 |
+
|
| 292 |
+
const conflicts = [];
|
| 293 |
+
for (const apt of appointments) {
|
| 294 |
+
try {
|
| 295 |
+
const aptStart = new Date(apt.date_time.replace('Z', '+00:00'));
|
| 296 |
+
const aptEnd = new Date(aptStart.getTime() + (apt.duration_minutes || 30) * 60000);
|
| 297 |
+
|
| 298 |
+
// Check for overlap
|
| 299 |
+
if (!(proposedEnd <= aptStart || proposedStart >= aptEnd)) {
|
| 300 |
+
conflicts.push(this._appointmentHelper(apt));
|
| 301 |
+
}
|
| 302 |
+
} catch {
|
| 303 |
+
continue;
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
return conflicts;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
/**
|
| 311 |
+
* Generate an AI-powered pre-appointment summary
|
| 312 |
+
*/
|
| 313 |
+
async generatePreAppointmentSummary(appointmentId) {
|
| 314 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 315 |
+
const patientsCollection = getPatientsCollection();
|
| 316 |
+
|
| 317 |
+
const appointment = await appointmentsCollection.findOne({ _id: new ObjectId(appointmentId) });
|
| 318 |
+
if (!appointment) {
|
| 319 |
+
return { error: 'Appointment not found' };
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
// Get patient data
|
| 323 |
+
const patient = await patientsCollection.findOne({ _id: new ObjectId(appointment.patient_id) });
|
| 324 |
+
if (!patient) {
|
| 325 |
+
return { error: 'Patient not found' };
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
// Build context
|
| 329 |
+
const medicalHistory = patient.medical_history || [];
|
| 330 |
+
const historyText = medicalHistory.map(item => {
|
| 331 |
+
if (typeof item === 'string') return `- ${item}`;
|
| 332 |
+
return `- ${item.condition || 'Unknown'}`;
|
| 333 |
+
}).join('\n');
|
| 334 |
+
|
| 335 |
+
const medications = patient.medications || [];
|
| 336 |
+
const medsText = medications.map(med => {
|
| 337 |
+
if (typeof med === 'object') {
|
| 338 |
+
return `- ${med.name || 'Unknown'}: ${med.dosage || 'N/A'}`;
|
| 339 |
+
}
|
| 340 |
+
return `- ${med}`;
|
| 341 |
+
}).join('\n');
|
| 342 |
+
|
| 343 |
+
const prompt = `You are a medical AI assistant preparing a pre-appointment briefing for a doctor.
|
| 344 |
+
|
| 345 |
+
UPCOMING APPOINTMENT:
|
| 346 |
+
- Purpose: ${appointment.purpose || 'General checkup'}
|
| 347 |
+
- Type: ${appointment.appointment_type || 'checkup'}
|
| 348 |
+
- Notes: ${appointment.notes || 'None'}
|
| 349 |
+
|
| 350 |
+
PATIENT INFORMATION:
|
| 351 |
+
- Name: ${patient.name || 'Unknown'}
|
| 352 |
+
- Age: ${patient.age || 'Unknown'}
|
| 353 |
+
- Gender: ${patient.gender || 'Unknown'}
|
| 354 |
+
- Blood Type: ${patient.blood_type || 'Unknown'}
|
| 355 |
+
- Allergies: ${(patient.allergies || []).join(', ') || 'None recorded'}
|
| 356 |
+
|
| 357 |
+
MEDICAL HISTORY:
|
| 358 |
+
${historyText || 'No medical history recorded'}
|
| 359 |
+
|
| 360 |
+
CURRENT MEDICATIONS:
|
| 361 |
+
${medsText || 'No medications recorded'}
|
| 362 |
+
|
| 363 |
+
Please provide a concise pre-appointment briefing including:
|
| 364 |
+
1. **Patient Overview**: Quick summary of the patient
|
| 365 |
+
2. **Key Concerns**: Important medical factors to consider
|
| 366 |
+
3. **Suggested Focus Areas**: What to examine based on history
|
| 367 |
+
4. **Questions to Ask**: Relevant questions for this appointment type
|
| 368 |
+
5. **Precautions**: Any special considerations
|
| 369 |
+
|
| 370 |
+
Keep it concise and actionable for a busy doctor.`;
|
| 371 |
+
|
| 372 |
+
try {
|
| 373 |
+
const summary = await generateResponse(prompt);
|
| 374 |
+
|
| 375 |
+
return {
|
| 376 |
+
appointment_id: appointmentId,
|
| 377 |
+
patient_name: patient.name,
|
| 378 |
+
purpose: appointment.purpose,
|
| 379 |
+
summary,
|
| 380 |
+
generated_at: new Date().toISOString()
|
| 381 |
+
};
|
| 382 |
+
} catch (error) {
|
| 383 |
+
return { error: `Error generating summary: ${error.message}` };
|
| 384 |
+
}
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
/**
|
| 388 |
+
* Get appointment statistics
|
| 389 |
+
*/
|
| 390 |
+
async getAppointmentStats() {
|
| 391 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 392 |
+
|
| 393 |
+
const pipeline = [
|
| 394 |
+
{
|
| 395 |
+
$group: {
|
| 396 |
+
_id: '$status',
|
| 397 |
+
count: { $sum: 1 }
|
| 398 |
+
}
|
| 399 |
+
}
|
| 400 |
+
];
|
| 401 |
+
|
| 402 |
+
const results = await appointmentsCollection.aggregate(pipeline).toArray();
|
| 403 |
+
|
| 404 |
+
const stats = {
|
| 405 |
+
total: 0,
|
| 406 |
+
by_status: {}
|
| 407 |
+
};
|
| 408 |
+
|
| 409 |
+
for (const r of results) {
|
| 410 |
+
stats.by_status[r._id] = r.count;
|
| 411 |
+
stats.total += r.count;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
return stats;
|
| 415 |
+
}
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
// Singleton instance
|
| 419 |
+
export const appointmentService = new AppointmentService();
|
services/authService.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import jwt from 'jsonwebtoken';
|
| 2 |
+
import bcrypt from 'bcryptjs';
|
| 3 |
+
import { ObjectId, getUsersCollection } from '../db/mongo.js';
|
| 4 |
+
import dotenv from 'dotenv';
|
| 5 |
+
|
| 6 |
+
dotenv.config();
|
| 7 |
+
|
| 8 |
+
const SECRET_KEY = process.env.JWT_SECRET_KEY || 'your-super-secret-key-change-in-production';
|
| 9 |
+
const ALGORITHM = 'HS256';
|
| 10 |
+
export const ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24; // 24 hours
|
| 11 |
+
|
| 12 |
+
class AuthService {
|
| 13 |
+
/**
|
| 14 |
+
* Verify a password against its hash
|
| 15 |
+
*/
|
| 16 |
+
verifyPassword(plainPassword, hashedPassword) {
|
| 17 |
+
return bcrypt.compareSync(plainPassword, hashedPassword);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Hash a password
|
| 22 |
+
*/
|
| 23 |
+
getPasswordHash(password) {
|
| 24 |
+
const salt = bcrypt.genSaltSync(10);
|
| 25 |
+
return bcrypt.hashSync(password, salt);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Create a JWT access token
|
| 30 |
+
*/
|
| 31 |
+
createAccessToken(data, expiresInMinutes = ACCESS_TOKEN_EXPIRE_MINUTES) {
|
| 32 |
+
const payload = {
|
| 33 |
+
...data,
|
| 34 |
+
exp: Math.floor(Date.now() / 1000) + (expiresInMinutes * 60)
|
| 35 |
+
};
|
| 36 |
+
return jwt.sign(payload, SECRET_KEY, { algorithm: ALGORITHM });
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* Decode and validate a JWT token
|
| 41 |
+
*/
|
| 42 |
+
decodeToken(token) {
|
| 43 |
+
try {
|
| 44 |
+
const payload = jwt.verify(token, SECRET_KEY, { algorithms: [ALGORITHM] });
|
| 45 |
+
const userId = payload.sub;
|
| 46 |
+
const email = payload.email;
|
| 47 |
+
const role = payload.role;
|
| 48 |
+
|
| 49 |
+
if (!userId) {
|
| 50 |
+
return null;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
return { userId, email, role };
|
| 54 |
+
} catch (error) {
|
| 55 |
+
return null;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* Get a user by email
|
| 61 |
+
*/
|
| 62 |
+
async getUserByEmail(email) {
|
| 63 |
+
const usersCollection = getUsersCollection();
|
| 64 |
+
return await usersCollection.findOne({ email });
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* Get a user by ID
|
| 69 |
+
*/
|
| 70 |
+
async getUserById(userId) {
|
| 71 |
+
try {
|
| 72 |
+
const usersCollection = getUsersCollection();
|
| 73 |
+
return await usersCollection.findOne({ _id: new ObjectId(userId) });
|
| 74 |
+
} catch (error) {
|
| 75 |
+
return null;
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Create a new user
|
| 81 |
+
*/
|
| 82 |
+
async createUser(userData) {
|
| 83 |
+
const usersCollection = getUsersCollection();
|
| 84 |
+
|
| 85 |
+
// Check if user already exists
|
| 86 |
+
const existingUser = await this.getUserByEmail(userData.email);
|
| 87 |
+
if (existingUser) {
|
| 88 |
+
throw new Error('User with this email already exists');
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Create user document
|
| 92 |
+
const now = new Date();
|
| 93 |
+
const userDoc = {
|
| 94 |
+
email: userData.email,
|
| 95 |
+
name: userData.name,
|
| 96 |
+
role: userData.role || 'doctor',
|
| 97 |
+
hashed_password: this.getPasswordHash(userData.password),
|
| 98 |
+
created_at: now,
|
| 99 |
+
updated_at: now,
|
| 100 |
+
is_active: true
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
const result = await usersCollection.insertOne(userDoc);
|
| 104 |
+
userDoc._id = result.insertedId;
|
| 105 |
+
return userDoc;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/**
|
| 109 |
+
* Authenticate a user by email and password
|
| 110 |
+
*/
|
| 111 |
+
async authenticateUser(email, password) {
|
| 112 |
+
const user = await this.getUserByEmail(email);
|
| 113 |
+
if (!user) {
|
| 114 |
+
return null;
|
| 115 |
+
}
|
| 116 |
+
if (!this.verifyPassword(password, user.hashed_password)) {
|
| 117 |
+
return null;
|
| 118 |
+
}
|
| 119 |
+
return user;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/**
|
| 123 |
+
* Convert a user document to a response object
|
| 124 |
+
*/
|
| 125 |
+
userToResponse(user) {
|
| 126 |
+
return {
|
| 127 |
+
id: user._id.toString(),
|
| 128 |
+
email: user.email,
|
| 129 |
+
name: user.name,
|
| 130 |
+
role: user.role,
|
| 131 |
+
created_at: user.created_at,
|
| 132 |
+
is_active: user.is_active !== false
|
| 133 |
+
};
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
/**
|
| 137 |
+
* Update a user's information
|
| 138 |
+
*/
|
| 139 |
+
async updateUser(userId, updateData) {
|
| 140 |
+
const usersCollection = getUsersCollection();
|
| 141 |
+
updateData.updated_at = new Date();
|
| 142 |
+
|
| 143 |
+
const result = await usersCollection.updateOne(
|
| 144 |
+
{ _id: new ObjectId(userId) },
|
| 145 |
+
{ $set: updateData }
|
| 146 |
+
);
|
| 147 |
+
|
| 148 |
+
if (result.matchedCount === 0) {
|
| 149 |
+
return null;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
return await this.getUserById(userId);
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// Singleton instance
|
| 157 |
+
export const authService = new AuthService();
|
| 158 |
+
|
services/llmClient.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import OpenAI from 'openai';
|
| 2 |
+
import dotenv from 'dotenv';
|
| 3 |
+
|
| 4 |
+
dotenv.config();
|
| 5 |
+
|
| 6 |
+
const client = new OpenAI({
|
| 7 |
+
baseURL: 'https://router.huggingface.co/v1',
|
| 8 |
+
apiKey: process.env.HF_TOKEN
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
const MODEL = 'meta-llama/Llama-3.2-3B-Instruct:novita';
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Generate a response using the Llama model
|
| 15 |
+
* @param {string} prompt - The prompt to send to the model
|
| 16 |
+
* @returns {Promise<string>} - The generated text response
|
| 17 |
+
*/
|
| 18 |
+
export const generateResponse = async (prompt) => {
|
| 19 |
+
const completion = await client.chat.completions.create({
|
| 20 |
+
model: MODEL,
|
| 21 |
+
messages: [
|
| 22 |
+
{
|
| 23 |
+
role: 'user',
|
| 24 |
+
content: prompt
|
| 25 |
+
}
|
| 26 |
+
]
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
return completion.choices[0].message.content;
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Generate a response with system message
|
| 34 |
+
* @param {string} systemPrompt - The system message
|
| 35 |
+
* @param {string} userPrompt - The user message
|
| 36 |
+
* @returns {Promise<string>} - The generated text response
|
| 37 |
+
*/
|
| 38 |
+
export const generateResponseWithSystem = async (systemPrompt, userPrompt) => {
|
| 39 |
+
const completion = await client.chat.completions.create({
|
| 40 |
+
model: MODEL,
|
| 41 |
+
messages: [
|
| 42 |
+
{
|
| 43 |
+
role: 'system',
|
| 44 |
+
content: systemPrompt
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
role: 'user',
|
| 48 |
+
content: userPrompt
|
| 49 |
+
}
|
| 50 |
+
]
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
return completion.choices[0].message.content;
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
export { client, MODEL };
|
| 57 |
+
|
services/ragService.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { generateResponse } from './llmClient.js';
|
| 2 |
+
import { vectorService } from './vectorService.js';
|
| 3 |
+
import dotenv from 'dotenv';
|
| 4 |
+
|
| 5 |
+
dotenv.config();
|
| 6 |
+
|
| 7 |
+
class RAGService {
|
| 8 |
+
constructor() {
|
| 9 |
+
this.vectorService = vectorService;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
async retrievePatientContext(patientId, query, nResults = 5) {
|
| 13 |
+
const patientResults = await this.vectorService.searchPatientRecords(
|
| 14 |
+
query,
|
| 15 |
+
patientId,
|
| 16 |
+
nResults
|
| 17 |
+
);
|
| 18 |
+
return { patient_records: patientResults };
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
async retrieveResearchContext(query, nResults = 3) {
|
| 22 |
+
return await this.vectorService.searchResearchPapers(query, nResults);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
formatContextForLLM(patientContext, researchContext) {
|
| 26 |
+
const contextParts = [];
|
| 27 |
+
|
| 28 |
+
contextParts.push('=== PATIENT MEDICAL RECORDS ===');
|
| 29 |
+
if (patientContext.patient_records && patientContext.patient_records.length > 0) {
|
| 30 |
+
patientContext.patient_records.forEach((record, idx) => {
|
| 31 |
+
contextParts.push(`\n${idx + 1}. ${record.document}`);
|
| 32 |
+
contextParts.push(` Type: ${record.metadata.type || 'unknown'}`);
|
| 33 |
+
});
|
| 34 |
+
} else {
|
| 35 |
+
contextParts.push('No patient records found.');
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
contextParts.push('\n\n=== RELEVANT MEDICAL RESEARCH ===');
|
| 39 |
+
if (researchContext && researchContext.length > 0) {
|
| 40 |
+
researchContext.forEach((paper, idx) => {
|
| 41 |
+
contextParts.push(`\n${idx + 1}. Title: ${paper.metadata.title || 'Unknown'}`);
|
| 42 |
+
contextParts.push(` Content: ${paper.document}`);
|
| 43 |
+
});
|
| 44 |
+
} else {
|
| 45 |
+
contextParts.push('No relevant research found.');
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
return contextParts.join('\n');
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
async generateRAGResponse(patientId, patientData, query) {
|
| 52 |
+
const patientContext = await this.retrievePatientContext(patientId, query, 5);
|
| 53 |
+
const researchContext = await this.retrieveResearchContext(query, 3);
|
| 54 |
+
const formattedContext = this.formatContextForLLM(patientContext, researchContext);
|
| 55 |
+
|
| 56 |
+
const prompt = `
|
| 57 |
+
You are an expert medical AI assistant.
|
| 58 |
+
|
| 59 |
+
PATIENT:
|
| 60 |
+
- Name: ${patientData.name}
|
| 61 |
+
- Age: ${patientData.age}
|
| 62 |
+
- Gender: ${patientData.gender}
|
| 63 |
+
|
| 64 |
+
CONTEXT:
|
| 65 |
+
${formattedContext}
|
| 66 |
+
|
| 67 |
+
DOCTOR'S QUESTION:
|
| 68 |
+
${query}
|
| 69 |
+
|
| 70 |
+
Please give a medically accurate, evidence-based answer.
|
| 71 |
+
`;
|
| 72 |
+
|
| 73 |
+
try {
|
| 74 |
+
const text = await generateResponse(prompt);
|
| 75 |
+
|
| 76 |
+
return {
|
| 77 |
+
text,
|
| 78 |
+
patient_records_used: (patientContext.patient_records || []).length,
|
| 79 |
+
research_papers_used: researchContext.length,
|
| 80 |
+
sources: {
|
| 81 |
+
patient_records: (patientContext.patient_records || []).map(r => ({
|
| 82 |
+
type: r.metadata.type,
|
| 83 |
+
content: r.document.substring(0, 200) + '...'
|
| 84 |
+
})),
|
| 85 |
+
research_papers: researchContext.map(r => ({
|
| 86 |
+
title: r.metadata.title,
|
| 87 |
+
excerpt: r.document.substring(0, 200) + '...'
|
| 88 |
+
}))
|
| 89 |
+
}
|
| 90 |
+
};
|
| 91 |
+
} catch (error) {
|
| 92 |
+
return { text: `Error generating AI response: ${error.message}`, error: true };
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
async suggestTreatments(patientId, patientData, condition) {
|
| 97 |
+
const query = `Treatment options for ${condition}`;
|
| 98 |
+
return await this.generateRAGResponse(patientId, patientData, query);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
async analyzeDrugInteractions(patientId, patientData, newMedication) {
|
| 102 |
+
const currentMeds = (patientData.medications || []).map(m => m.name).join(', ');
|
| 103 |
+
const query = `Drug interactions between ${newMedication} and current medications: ${currentMeds}`;
|
| 104 |
+
return await this.generateRAGResponse(patientId, patientData, query);
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
export const ragService = new RAGService();
|
services/timelineService.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { generateResponse } from './llmClient.js';
|
| 2 |
+
import { ObjectId, getPatientsCollection, getAppointmentsCollection, getWarningsCollection } from '../db/mongo.js';
|
| 3 |
+
import dotenv from 'dotenv';
|
| 4 |
+
|
| 5 |
+
dotenv.config();
|
| 6 |
+
|
| 7 |
+
class TimelineService {
|
| 8 |
+
/**
|
| 9 |
+
* Try to parse a date string in various formats
|
| 10 |
+
*/
|
| 11 |
+
_parseDate(dateStr) {
|
| 12 |
+
if (!dateStr) return null;
|
| 13 |
+
|
| 14 |
+
const formats = [
|
| 15 |
+
// ISO format
|
| 16 |
+
/^(\d{4})-(\d{2})-(\d{2})$/,
|
| 17 |
+
// DD/MM/YYYY
|
| 18 |
+
/^(\d{2})\/(\d{2})\/(\d{4})$/,
|
| 19 |
+
// MM/DD/YYYY
|
| 20 |
+
/^(\d{2})\/(\d{2})\/(\d{4})$/,
|
| 21 |
+
];
|
| 22 |
+
|
| 23 |
+
// Try direct Date parsing first
|
| 24 |
+
const date = new Date(dateStr);
|
| 25 |
+
if (!isNaN(date.getTime())) {
|
| 26 |
+
return date;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
return null;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Create a timeline event
|
| 34 |
+
*/
|
| 35 |
+
_createEvent(eventType, title, description, date = null, dateStr = '', metadata = {}) {
|
| 36 |
+
return {
|
| 37 |
+
type: eventType,
|
| 38 |
+
title,
|
| 39 |
+
description,
|
| 40 |
+
date: date ? date.toISOString() : null,
|
| 41 |
+
date_display: dateStr || (date ? date.toLocaleDateString('en-US', {
|
| 42 |
+
year: 'numeric',
|
| 43 |
+
month: 'long',
|
| 44 |
+
day: 'numeric'
|
| 45 |
+
}) : 'Unknown date'),
|
| 46 |
+
metadata: metadata || {},
|
| 47 |
+
sort_date: date ? date.getTime() : 0
|
| 48 |
+
};
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/**
|
| 52 |
+
* Generate a chronological timeline of all patient events
|
| 53 |
+
*/
|
| 54 |
+
async getPatientTimeline(patientId) {
|
| 55 |
+
const patientsCollection = getPatientsCollection();
|
| 56 |
+
const appointmentsCollection = getAppointmentsCollection();
|
| 57 |
+
const warningsCollection = getWarningsCollection();
|
| 58 |
+
|
| 59 |
+
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 60 |
+
if (!patient) {
|
| 61 |
+
return [];
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
const events = [];
|
| 65 |
+
|
| 66 |
+
// Process medical history
|
| 67 |
+
const medicalHistory = patient.medical_history || [];
|
| 68 |
+
for (const item of medicalHistory) {
|
| 69 |
+
if (typeof item === 'object' && item !== null) {
|
| 70 |
+
// Structured MedicalEvent
|
| 71 |
+
const date = this._parseDate(item.date_diagnosed);
|
| 72 |
+
events.push(this._createEvent(
|
| 73 |
+
'condition',
|
| 74 |
+
`Diagnosed: ${item.condition || 'Unknown'}`,
|
| 75 |
+
item.notes || '',
|
| 76 |
+
date,
|
| 77 |
+
item.date_diagnosed || '',
|
| 78 |
+
{
|
| 79 |
+
severity: item.severity,
|
| 80 |
+
resolved: item.date_resolved
|
| 81 |
+
}
|
| 82 |
+
));
|
| 83 |
+
|
| 84 |
+
// Add resolution event if exists
|
| 85 |
+
if (item.date_resolved) {
|
| 86 |
+
const resolvedDate = this._parseDate(item.date_resolved);
|
| 87 |
+
events.push(this._createEvent(
|
| 88 |
+
'condition_resolved',
|
| 89 |
+
`Resolved: ${item.condition || 'Unknown'}`,
|
| 90 |
+
'Condition resolved',
|
| 91 |
+
resolvedDate,
|
| 92 |
+
item.date_resolved || '',
|
| 93 |
+
{ original_condition: item.condition }
|
| 94 |
+
));
|
| 95 |
+
}
|
| 96 |
+
} else {
|
| 97 |
+
// Legacy string format
|
| 98 |
+
events.push(this._createEvent(
|
| 99 |
+
'condition',
|
| 100 |
+
`Medical History: ${item}`,
|
| 101 |
+
item,
|
| 102 |
+
null,
|
| 103 |
+
'',
|
| 104 |
+
{ legacy_format: true }
|
| 105 |
+
));
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Process medications
|
| 110 |
+
const medications = patient.medications || [];
|
| 111 |
+
for (const med of medications) {
|
| 112 |
+
if (typeof med === 'object' && med !== null) {
|
| 113 |
+
const startDate = this._parseDate(med.start_date);
|
| 114 |
+
events.push(this._createEvent(
|
| 115 |
+
'medication_start',
|
| 116 |
+
`Started: ${med.name || 'Unknown Medication'}`,
|
| 117 |
+
`Dosage: ${med.dosage || 'N/A'}`,
|
| 118 |
+
startDate,
|
| 119 |
+
med.start_date || '',
|
| 120 |
+
{
|
| 121 |
+
medication_name: med.name,
|
| 122 |
+
dosage: med.dosage
|
| 123 |
+
}
|
| 124 |
+
));
|
| 125 |
+
|
| 126 |
+
// Add end event if medication was stopped
|
| 127 |
+
if (med.end_date) {
|
| 128 |
+
const endDate = this._parseDate(med.end_date);
|
| 129 |
+
events.push(this._createEvent(
|
| 130 |
+
'medication_end',
|
| 131 |
+
`Stopped: ${med.name || 'Unknown Medication'}`,
|
| 132 |
+
'Medication course completed',
|
| 133 |
+
endDate,
|
| 134 |
+
med.end_date || '',
|
| 135 |
+
{ medication_name: med.name }
|
| 136 |
+
));
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Process appointments
|
| 142 |
+
try {
|
| 143 |
+
const appointments = await appointmentsCollection.find({ patient_id: patientId }).toArray();
|
| 144 |
+
for (const apt of appointments) {
|
| 145 |
+
let aptDate = apt.date_time;
|
| 146 |
+
if (typeof aptDate === 'string') {
|
| 147 |
+
aptDate = this._parseDate(aptDate);
|
| 148 |
+
}
|
| 149 |
+
events.push(this._createEvent(
|
| 150 |
+
'appointment',
|
| 151 |
+
`Appointment: ${apt.purpose || 'Check-up'}`,
|
| 152 |
+
apt.notes || '',
|
| 153 |
+
aptDate instanceof Date ? aptDate : null,
|
| 154 |
+
'',
|
| 155 |
+
{
|
| 156 |
+
status: apt.status,
|
| 157 |
+
doctor_id: apt.doctor_id
|
| 158 |
+
}
|
| 159 |
+
));
|
| 160 |
+
}
|
| 161 |
+
} catch (error) {
|
| 162 |
+
// Appointments collection might not have data yet
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// Process warnings (only acknowledged ones for history)
|
| 166 |
+
try {
|
| 167 |
+
const warnings = await warningsCollection.find({
|
| 168 |
+
patient_id: patientId,
|
| 169 |
+
is_acknowledged: true
|
| 170 |
+
}).toArray();
|
| 171 |
+
|
| 172 |
+
for (const warning of warnings) {
|
| 173 |
+
const createdAt = warning.created_at;
|
| 174 |
+
events.push(this._createEvent(
|
| 175 |
+
'warning',
|
| 176 |
+
`Warning: ${warning.title || 'Clinical Warning'}`,
|
| 177 |
+
warning.description || '',
|
| 178 |
+
createdAt instanceof Date ? createdAt : null,
|
| 179 |
+
'',
|
| 180 |
+
{
|
| 181 |
+
severity: warning.severity,
|
| 182 |
+
warning_type: warning.warning_type
|
| 183 |
+
}
|
| 184 |
+
));
|
| 185 |
+
}
|
| 186 |
+
} catch (error) {
|
| 187 |
+
// Warnings collection might not have data yet
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// Sort events by date (most recent first), unknown dates at the end
|
| 191 |
+
events.sort((a, b) => b.sort_date - a.sort_date);
|
| 192 |
+
|
| 193 |
+
// Remove sort_date from output
|
| 194 |
+
return events.map(event => {
|
| 195 |
+
const { sort_date, ...rest } = event;
|
| 196 |
+
return rest;
|
| 197 |
+
});
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/**
|
| 201 |
+
* Generate an AI-powered summary of the patient's medical timeline
|
| 202 |
+
*/
|
| 203 |
+
async generateTimelineSummary(patientId) {
|
| 204 |
+
const patientsCollection = getPatientsCollection();
|
| 205 |
+
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 206 |
+
|
| 207 |
+
if (!patient) {
|
| 208 |
+
return { error: 'Patient not found' };
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
const timeline = await this.getPatientTimeline(patientId);
|
| 212 |
+
|
| 213 |
+
// Format timeline for AI
|
| 214 |
+
const timelineText = timeline.slice(0, 20).map(event =>
|
| 215 |
+
`- ${event.date_display}: ${event.title} - ${event.description}`
|
| 216 |
+
).join('\n');
|
| 217 |
+
|
| 218 |
+
const prompt = `You are a medical AI assistant. Analyze this patient's medical timeline and provide a comprehensive summary.
|
| 219 |
+
|
| 220 |
+
PATIENT: ${patient.name || 'Unknown'}
|
| 221 |
+
Age: ${patient.age || 'Unknown'}
|
| 222 |
+
Gender: ${patient.gender || 'Unknown'}
|
| 223 |
+
|
| 224 |
+
MEDICAL TIMELINE:
|
| 225 |
+
${timelineText || 'No timeline events available.'}
|
| 226 |
+
|
| 227 |
+
Please provide:
|
| 228 |
+
1. **Overview**: A brief summary of the patient's medical journey
|
| 229 |
+
2. **Key Milestones**: The most significant medical events
|
| 230 |
+
3. **Patterns**: Any notable patterns in conditions or treatments
|
| 231 |
+
4. **Current Status**: Assessment of current health based on timeline
|
| 232 |
+
5. **Recommendations**: Suggested follow-ups or areas of attention
|
| 233 |
+
|
| 234 |
+
Be concise but thorough. Use bullet points where appropriate.`;
|
| 235 |
+
|
| 236 |
+
try {
|
| 237 |
+
const summary = await generateResponse(prompt);
|
| 238 |
+
|
| 239 |
+
return {
|
| 240 |
+
patient_id: patientId,
|
| 241 |
+
patient_name: patient.name,
|
| 242 |
+
summary,
|
| 243 |
+
events_analyzed: timeline.length,
|
| 244 |
+
generated_at: new Date().toISOString()
|
| 245 |
+
};
|
| 246 |
+
} catch (error) {
|
| 247 |
+
return { error: `Error generating summary: ${error.message}` };
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
/**
|
| 252 |
+
* Get statistics about the patient's timeline
|
| 253 |
+
*/
|
| 254 |
+
async getTimelineStats(patientId) {
|
| 255 |
+
const timeline = await this.getPatientTimeline(patientId);
|
| 256 |
+
|
| 257 |
+
const stats = {
|
| 258 |
+
total_events: timeline.length,
|
| 259 |
+
by_type: {},
|
| 260 |
+
date_range: {
|
| 261 |
+
earliest: null,
|
| 262 |
+
latest: null
|
| 263 |
+
}
|
| 264 |
+
};
|
| 265 |
+
|
| 266 |
+
const dates = [];
|
| 267 |
+
for (const event of timeline) {
|
| 268 |
+
const eventType = event.type;
|
| 269 |
+
stats.by_type[eventType] = (stats.by_type[eventType] || 0) + 1;
|
| 270 |
+
if (event.date) {
|
| 271 |
+
dates.push(event.date);
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
if (dates.length > 0) {
|
| 276 |
+
dates.sort();
|
| 277 |
+
stats.date_range.earliest = dates[0];
|
| 278 |
+
stats.date_range.latest = dates[dates.length - 1];
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
return stats;
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
// Singleton instance
|
| 286 |
+
export const timelineService = new TimelineService();
|
services/vectorService.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ChromaClient } from 'chromadb';
|
| 2 |
+
|
| 3 |
+
class VectorService {
|
| 4 |
+
constructor() {
|
| 5 |
+
this.chromaClient = null;
|
| 6 |
+
this.patientCollection = null;
|
| 7 |
+
this.researchCollection = null;
|
| 8 |
+
this.initialized = false;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
async initialize() {
|
| 12 |
+
if (this.initialized) return;
|
| 13 |
+
|
| 14 |
+
try {
|
| 15 |
+
this.chromaClient = new ChromaClient({
|
| 16 |
+
path: './chroma_db'
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
this.patientCollection = await this._getOrCreateCollection('patient_records');
|
| 20 |
+
this.researchCollection = await this._getOrCreateCollection('medical_research');
|
| 21 |
+
this.initialized = true;
|
| 22 |
+
console.log('✓ Vector database initialized');
|
| 23 |
+
} catch (error) {
|
| 24 |
+
console.log(`⚠️ Vector database initialization warning: ${error.message}`);
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
async _getOrCreateCollection(name) {
|
| 29 |
+
try {
|
| 30 |
+
return await this.chromaClient.getCollection({ name });
|
| 31 |
+
} catch {
|
| 32 |
+
return await this.chromaClient.createCollection({
|
| 33 |
+
name,
|
| 34 |
+
metadata: { description: `Collection for ${name}` }
|
| 35 |
+
});
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
async addPatientRecord(patientId, patientData) {
|
| 40 |
+
await this.initialize();
|
| 41 |
+
|
| 42 |
+
const documents = [];
|
| 43 |
+
const metadatas = [];
|
| 44 |
+
const ids = [];
|
| 45 |
+
|
| 46 |
+
// Add medical history
|
| 47 |
+
if (patientData.medical_history) {
|
| 48 |
+
patientData.medical_history.forEach((condition, idx) => {
|
| 49 |
+
const docId = `patient_${patientId}_history_${idx}`;
|
| 50 |
+
const conditionText = typeof condition === 'string' ? condition : condition.condition || 'Unknown';
|
| 51 |
+
documents.push(`Patient ${patientData.name} has medical history: ${conditionText}`);
|
| 52 |
+
metadatas.push({
|
| 53 |
+
patient_id: patientId,
|
| 54 |
+
patient_name: patientData.name,
|
| 55 |
+
type: 'medical_history',
|
| 56 |
+
condition: conditionText,
|
| 57 |
+
age: String(patientData.age || ''),
|
| 58 |
+
gender: patientData.gender || ''
|
| 59 |
+
});
|
| 60 |
+
ids.push(docId);
|
| 61 |
+
});
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Add medications
|
| 65 |
+
if (patientData.medications) {
|
| 66 |
+
patientData.medications.forEach((med, idx) => {
|
| 67 |
+
const docId = `patient_${patientId}_medication_${idx}`;
|
| 68 |
+
let medText = `Patient ${patientData.name} is taking ${med.name}, dosage: ${med.dosage}, started: ${med.start_date}`;
|
| 69 |
+
if (med.end_date) {
|
| 70 |
+
medText += `, ended: ${med.end_date}`;
|
| 71 |
+
}
|
| 72 |
+
documents.push(medText);
|
| 73 |
+
metadatas.push({
|
| 74 |
+
patient_id: patientId,
|
| 75 |
+
patient_name: patientData.name,
|
| 76 |
+
type: 'medication',
|
| 77 |
+
medication_name: med.name,
|
| 78 |
+
dosage: med.dosage,
|
| 79 |
+
start_date: med.start_date
|
| 80 |
+
});
|
| 81 |
+
ids.push(docId);
|
| 82 |
+
});
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Add summary
|
| 86 |
+
const medHistory = patientData.medical_history || ['None'];
|
| 87 |
+
const medNames = (patientData.medications || []).map(m => m.name);
|
| 88 |
+
const summaryText = `
|
| 89 |
+
Patient Profile: ${patientData.name}, Age: ${patientData.age}, Gender: ${patientData.gender}.
|
| 90 |
+
Medical History: ${medHistory.join(', ')}.
|
| 91 |
+
Current Medications: ${medNames.join(', ') || 'None'}.
|
| 92 |
+
`.trim();
|
| 93 |
+
|
| 94 |
+
documents.push(summaryText);
|
| 95 |
+
metadatas.push({
|
| 96 |
+
patient_id: patientId,
|
| 97 |
+
patient_name: patientData.name,
|
| 98 |
+
type: 'summary',
|
| 99 |
+
age: String(patientData.age || ''),
|
| 100 |
+
gender: patientData.gender || ''
|
| 101 |
+
});
|
| 102 |
+
ids.push(`patient_${patientId}_summary`);
|
| 103 |
+
|
| 104 |
+
if (documents.length > 0) {
|
| 105 |
+
try {
|
| 106 |
+
await this.patientCollection.upsert({
|
| 107 |
+
documents,
|
| 108 |
+
metadatas,
|
| 109 |
+
ids
|
| 110 |
+
});
|
| 111 |
+
console.log(` ✓ Indexed ${documents.length} records for patient ${patientData.name}`);
|
| 112 |
+
} catch (error) {
|
| 113 |
+
console.log(` ✗ Error indexing patient ${patientData.name}: ${error.message}`);
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
return documents.length;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
async searchPatientRecords(query, patientId = null, nResults = 5) {
|
| 121 |
+
await this.initialize();
|
| 122 |
+
|
| 123 |
+
try {
|
| 124 |
+
const whereFilter = patientId ? { patient_id: patientId } : undefined;
|
| 125 |
+
|
| 126 |
+
const results = await this.patientCollection.query({
|
| 127 |
+
queryTexts: [query],
|
| 128 |
+
nResults,
|
| 129 |
+
where: whereFilter
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
const formattedResults = [];
|
| 133 |
+
if (results && results.documents && results.documents[0]) {
|
| 134 |
+
for (let idx = 0; idx < results.documents[0].length; idx++) {
|
| 135 |
+
formattedResults.push({
|
| 136 |
+
document: results.documents[0][idx],
|
| 137 |
+
metadata: results.metadatas[0][idx],
|
| 138 |
+
distance: results.distances ? results.distances[0][idx] : null
|
| 139 |
+
});
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
return formattedResults;
|
| 144 |
+
} catch (error) {
|
| 145 |
+
console.log(`Search error: ${error.message}`);
|
| 146 |
+
return [];
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
async addResearchPaper(paperId, title, content, metadata) {
|
| 151 |
+
await this.initialize();
|
| 152 |
+
|
| 153 |
+
try {
|
| 154 |
+
const chunkSize = 1000;
|
| 155 |
+
const chunks = [];
|
| 156 |
+
const words = content.split(' ');
|
| 157 |
+
|
| 158 |
+
for (let i = 0; i < words.length; i += chunkSize) {
|
| 159 |
+
const chunk = words.slice(i, i + chunkSize).join(' ');
|
| 160 |
+
chunks.push(chunk);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
const documents = [];
|
| 164 |
+
const metadatas = [];
|
| 165 |
+
const ids = [];
|
| 166 |
+
|
| 167 |
+
chunks.forEach((chunk, idx) => {
|
| 168 |
+
documents.push(chunk);
|
| 169 |
+
const chunkMetadata = {
|
| 170 |
+
paper_id: paperId,
|
| 171 |
+
title,
|
| 172 |
+
chunk_index: String(idx),
|
| 173 |
+
total_chunks: String(chunks.length),
|
| 174 |
+
...Object.fromEntries(Object.entries(metadata).map(([k, v]) => [k, String(v)]))
|
| 175 |
+
};
|
| 176 |
+
metadatas.push(chunkMetadata);
|
| 177 |
+
ids.push(`research_${paperId}_chunk_${idx}`);
|
| 178 |
+
});
|
| 179 |
+
|
| 180 |
+
if (documents.length > 0) {
|
| 181 |
+
await this.researchCollection.upsert({
|
| 182 |
+
documents,
|
| 183 |
+
metadatas,
|
| 184 |
+
ids
|
| 185 |
+
});
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
return chunks.length;
|
| 189 |
+
} catch (error) {
|
| 190 |
+
console.log(`Error adding research paper: ${error.message}`);
|
| 191 |
+
return 0;
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
async searchResearchPapers(query, nResults = 5) {
|
| 196 |
+
await this.initialize();
|
| 197 |
+
|
| 198 |
+
try {
|
| 199 |
+
const results = await this.researchCollection.query({
|
| 200 |
+
queryTexts: [query],
|
| 201 |
+
nResults
|
| 202 |
+
});
|
| 203 |
+
|
| 204 |
+
const formattedResults = [];
|
| 205 |
+
if (results && results.documents && results.documents[0]) {
|
| 206 |
+
for (let idx = 0; idx < results.documents[0].length; idx++) {
|
| 207 |
+
formattedResults.push({
|
| 208 |
+
document: results.documents[0][idx],
|
| 209 |
+
metadata: results.metadatas[0][idx],
|
| 210 |
+
distance: results.distances ? results.distances[0][idx] : null
|
| 211 |
+
});
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
return formattedResults;
|
| 216 |
+
} catch (error) {
|
| 217 |
+
console.log(`Research search error: ${error.message}`);
|
| 218 |
+
return [];
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
async deletePatientRecords(patientId) {
|
| 223 |
+
await this.initialize();
|
| 224 |
+
|
| 225 |
+
try {
|
| 226 |
+
const results = await this.patientCollection.get({
|
| 227 |
+
where: { patient_id: patientId }
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
if (results && results.ids && results.ids.length > 0) {
|
| 231 |
+
await this.patientCollection.delete({ ids: results.ids });
|
| 232 |
+
}
|
| 233 |
+
return true;
|
| 234 |
+
} catch (error) {
|
| 235 |
+
console.log(`Error deleting patient records: ${error.message}`);
|
| 236 |
+
return false;
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
async getCollectionStats() {
|
| 241 |
+
await this.initialize();
|
| 242 |
+
|
| 243 |
+
try {
|
| 244 |
+
const patientCount = await this.patientCollection.count();
|
| 245 |
+
const researchCount = await this.researchCollection.count();
|
| 246 |
+
|
| 247 |
+
return {
|
| 248 |
+
patient_records_count: patientCount,
|
| 249 |
+
research_papers_count: researchCount
|
| 250 |
+
};
|
| 251 |
+
} catch {
|
| 252 |
+
return {
|
| 253 |
+
patient_records_count: 0,
|
| 254 |
+
research_papers_count: 0
|
| 255 |
+
};
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
export const vectorService = new VectorService();
|
| 261 |
+
|
services/warningService.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { generateResponse } from './llmClient.js';
|
| 2 |
+
import { ObjectId, getWarningsCollection, getPatientsCollection } from '../db/mongo.js';
|
| 3 |
+
import dotenv from 'dotenv';
|
| 4 |
+
|
| 5 |
+
dotenv.config();
|
| 6 |
+
|
| 7 |
+
// Warning types enum
|
| 8 |
+
export const WarningType = {
|
| 9 |
+
DRUG_INTERACTION: 'drug_interaction',
|
| 10 |
+
ALLERGY: 'allergy',
|
| 11 |
+
CONTRAINDICATION: 'contraindication',
|
| 12 |
+
ABNORMAL_PATTERN: 'abnormal_pattern',
|
| 13 |
+
DOSAGE_ALERT: 'dosage_alert',
|
| 14 |
+
DUPLICATE_THERAPY: 'duplicate_therapy'
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
// Warning severity enum
|
| 18 |
+
export const WarningSeverity = {
|
| 19 |
+
LOW: 'low',
|
| 20 |
+
MEDIUM: 'medium',
|
| 21 |
+
HIGH: 'high',
|
| 22 |
+
CRITICAL: 'critical'
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
class WarningService {
|
| 26 |
+
/**
|
| 27 |
+
* Convert MongoDB warning document to response format
|
| 28 |
+
*/
|
| 29 |
+
_warningHelper(warning) {
|
| 30 |
+
return {
|
| 31 |
+
id: warning._id.toString(),
|
| 32 |
+
patient_id: warning.patient_id,
|
| 33 |
+
warning_type: warning.warning_type,
|
| 34 |
+
severity: warning.severity,
|
| 35 |
+
title: warning.title,
|
| 36 |
+
description: warning.description,
|
| 37 |
+
related_medications: warning.related_medications || [],
|
| 38 |
+
related_conditions: warning.related_conditions || [],
|
| 39 |
+
recommendation: warning.recommendation || null,
|
| 40 |
+
created_at: warning.created_at,
|
| 41 |
+
is_acknowledged: warning.is_acknowledged || false,
|
| 42 |
+
acknowledged_by: warning.acknowledged_by || null,
|
| 43 |
+
acknowledged_at: warning.acknowledged_at || null
|
| 44 |
+
};
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* Get all warnings for a patient
|
| 49 |
+
*/
|
| 50 |
+
async getPatientWarnings(patientId, includeAcknowledged = false) {
|
| 51 |
+
const warningsCollection = getWarningsCollection();
|
| 52 |
+
const query = { patient_id: patientId };
|
| 53 |
+
|
| 54 |
+
if (!includeAcknowledged) {
|
| 55 |
+
query.is_acknowledged = false;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
const warnings = await warningsCollection
|
| 59 |
+
.find(query)
|
| 60 |
+
.sort({ created_at: -1 })
|
| 61 |
+
.toArray();
|
| 62 |
+
|
| 63 |
+
return warnings.map(w => this._warningHelper(w));
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Get all unacknowledged warnings across all patients
|
| 68 |
+
*/
|
| 69 |
+
async getAllUnacknowledgedWarnings(limit = 50) {
|
| 70 |
+
const warningsCollection = getWarningsCollection();
|
| 71 |
+
|
| 72 |
+
const warnings = await warningsCollection
|
| 73 |
+
.find({ is_acknowledged: false })
|
| 74 |
+
.sort({ severity: -1, created_at: -1 })
|
| 75 |
+
.limit(limit)
|
| 76 |
+
.toArray();
|
| 77 |
+
|
| 78 |
+
return warnings.map(w => this._warningHelper(w));
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* Acknowledge a clinical warning
|
| 83 |
+
*/
|
| 84 |
+
async acknowledgeWarning(warningId, userId, notes = null) {
|
| 85 |
+
const warningsCollection = getWarningsCollection();
|
| 86 |
+
|
| 87 |
+
const result = await warningsCollection.updateOne(
|
| 88 |
+
{ _id: new ObjectId(warningId) },
|
| 89 |
+
{
|
| 90 |
+
$set: {
|
| 91 |
+
is_acknowledged: true,
|
| 92 |
+
acknowledged_by: userId,
|
| 93 |
+
acknowledged_at: new Date(),
|
| 94 |
+
acknowledgement_notes: notes
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
);
|
| 98 |
+
|
| 99 |
+
if (result.matchedCount === 0) {
|
| 100 |
+
return null;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
const warning = await warningsCollection.findOne({ _id: new ObjectId(warningId) });
|
| 104 |
+
return this._warningHelper(warning);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/**
|
| 108 |
+
* Create a new clinical warning
|
| 109 |
+
*/
|
| 110 |
+
async createWarning(warningData) {
|
| 111 |
+
const warningsCollection = getWarningsCollection();
|
| 112 |
+
|
| 113 |
+
const warningDoc = {
|
| 114 |
+
...warningData,
|
| 115 |
+
created_at: new Date(),
|
| 116 |
+
is_acknowledged: false
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
const result = await warningsCollection.insertOne(warningDoc);
|
| 120 |
+
warningDoc._id = result.insertedId;
|
| 121 |
+
return this._warningHelper(warningDoc);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/**
|
| 125 |
+
* Delete all warnings for a patient
|
| 126 |
+
*/
|
| 127 |
+
async deletePatientWarnings(patientId) {
|
| 128 |
+
const warningsCollection = getWarningsCollection();
|
| 129 |
+
const result = await warningsCollection.deleteMany({ patient_id: patientId });
|
| 130 |
+
return result.deletedCount;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/**
|
| 134 |
+
* Use AI to analyze patient data and generate clinical warnings
|
| 135 |
+
*/
|
| 136 |
+
async analyzePatientForWarnings(patientId, newMedication = null, newCondition = null) {
|
| 137 |
+
const patientsCollection = getPatientsCollection();
|
| 138 |
+
const patient = await patientsCollection.findOne({ _id: new ObjectId(patientId) });
|
| 139 |
+
|
| 140 |
+
if (!patient) {
|
| 141 |
+
return [];
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// Build the analysis prompt
|
| 145 |
+
const medications = patient.medications || [];
|
| 146 |
+
const medNames = medications
|
| 147 |
+
.filter(m => typeof m === 'object')
|
| 148 |
+
.map(m => m.name || '');
|
| 149 |
+
|
| 150 |
+
let conditions = patient.medical_history || [];
|
| 151 |
+
|
| 152 |
+
if (newMedication) {
|
| 153 |
+
medNames.push(`${newMedication} (NEW)`);
|
| 154 |
+
}
|
| 155 |
+
if (newCondition) {
|
| 156 |
+
conditions = [...conditions, `${newCondition} (NEW)`];
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
const conditionsText = conditions.map(c => {
|
| 160 |
+
if (typeof c === 'string') return c;
|
| 161 |
+
return c.condition || 'Unknown';
|
| 162 |
+
}).join(', ');
|
| 163 |
+
|
| 164 |
+
const prompt = `You are a clinical decision support AI. Analyze this patient's data for potential clinical warnings.
|
| 165 |
+
|
| 166 |
+
PATIENT INFORMATION:
|
| 167 |
+
- Age: ${patient.age || 'Unknown'}
|
| 168 |
+
- Gender: ${patient.gender || 'Unknown'}
|
| 169 |
+
- Medical Conditions: ${conditionsText || 'None recorded'}
|
| 170 |
+
- Current Medications: ${medNames.join(', ') || 'None recorded'}
|
| 171 |
+
|
| 172 |
+
Analyze for:
|
| 173 |
+
1. DRUG INTERACTIONS - Check if any medications interact negatively with each other
|
| 174 |
+
2. CONTRAINDICATIONS - Check if any medications are contraindicated given the patient's conditions
|
| 175 |
+
3. ALLERGY RISKS - Common allergy cross-reactions
|
| 176 |
+
4. DOSAGE CONCERNS - Age-related dosage considerations
|
| 177 |
+
5. DUPLICATE THERAPY - Multiple drugs for the same purpose
|
| 178 |
+
|
| 179 |
+
Return a JSON array of warnings. Each warning should have:
|
| 180 |
+
- warning_type: one of "drug_interaction", "allergy", "contraindication", "abnormal_pattern", "dosage_alert", "duplicate_therapy"
|
| 181 |
+
- severity: one of "low", "medium", "high", "critical"
|
| 182 |
+
- title: short title (max 50 chars)
|
| 183 |
+
- description: detailed explanation
|
| 184 |
+
- related_medications: array of medication names involved
|
| 185 |
+
- related_conditions: array of conditions involved
|
| 186 |
+
- recommendation: suggested action
|
| 187 |
+
|
| 188 |
+
If there are no warnings, return an empty array [].
|
| 189 |
+
Return ONLY valid JSON, no markdown or other text.`;
|
| 190 |
+
|
| 191 |
+
try {
|
| 192 |
+
let responseText = await generateResponse(prompt);
|
| 193 |
+
responseText = responseText.trim();
|
| 194 |
+
|
| 195 |
+
// Remove markdown code blocks if present
|
| 196 |
+
if (responseText.startsWith('```')) {
|
| 197 |
+
responseText = responseText.split('```')[1];
|
| 198 |
+
if (responseText.startsWith('json')) {
|
| 199 |
+
responseText = responseText.substring(4);
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
responseText = responseText.trim();
|
| 203 |
+
|
| 204 |
+
const warningsData = JSON.parse(responseText);
|
| 205 |
+
|
| 206 |
+
if (!Array.isArray(warningsData)) {
|
| 207 |
+
return [];
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// Create warnings in database
|
| 211 |
+
const createdWarnings = [];
|
| 212 |
+
for (const warning of warningsData) {
|
| 213 |
+
try {
|
| 214 |
+
const warningCreate = {
|
| 215 |
+
patient_id: patientId,
|
| 216 |
+
warning_type: warning.warning_type || 'abnormal_pattern',
|
| 217 |
+
severity: warning.severity || 'medium',
|
| 218 |
+
title: (warning.title || 'Clinical Warning').substring(0, 100),
|
| 219 |
+
description: warning.description || '',
|
| 220 |
+
related_medications: warning.related_medications || [],
|
| 221 |
+
related_conditions: warning.related_conditions || [],
|
| 222 |
+
recommendation: warning.recommendation || null
|
| 223 |
+
};
|
| 224 |
+
|
| 225 |
+
const createdWarning = await this.createWarning(warningCreate);
|
| 226 |
+
createdWarnings.push(createdWarning);
|
| 227 |
+
} catch (error) {
|
| 228 |
+
console.log(`Error creating warning: ${error.message}`);
|
| 229 |
+
continue;
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
return createdWarnings;
|
| 234 |
+
} catch (error) {
|
| 235 |
+
if (error instanceof SyntaxError) {
|
| 236 |
+
console.log(`JSON parse error: ${error.message}`);
|
| 237 |
+
} else {
|
| 238 |
+
console.log(`Error analyzing patient: ${error.message}`);
|
| 239 |
+
}
|
| 240 |
+
return [];
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
/**
|
| 245 |
+
* Get warning statistics
|
| 246 |
+
*/
|
| 247 |
+
async getWarningStats() {
|
| 248 |
+
const warningsCollection = getWarningsCollection();
|
| 249 |
+
|
| 250 |
+
const pipeline = [
|
| 251 |
+
{
|
| 252 |
+
$group: {
|
| 253 |
+
_id: {
|
| 254 |
+
severity: '$severity',
|
| 255 |
+
acknowledged: '$is_acknowledged'
|
| 256 |
+
},
|
| 257 |
+
count: { $sum: 1 }
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
];
|
| 261 |
+
|
| 262 |
+
const results = await warningsCollection.aggregate(pipeline).toArray();
|
| 263 |
+
|
| 264 |
+
const stats = {
|
| 265 |
+
total: 0,
|
| 266 |
+
unacknowledged: 0,
|
| 267 |
+
by_severity: {
|
| 268 |
+
critical: 0,
|
| 269 |
+
high: 0,
|
| 270 |
+
medium: 0,
|
| 271 |
+
low: 0
|
| 272 |
+
}
|
| 273 |
+
};
|
| 274 |
+
|
| 275 |
+
for (const r of results) {
|
| 276 |
+
const count = r.count;
|
| 277 |
+
stats.total += count;
|
| 278 |
+
|
| 279 |
+
if (!r._id.acknowledged) {
|
| 280 |
+
stats.unacknowledged += count;
|
| 281 |
+
const severity = r._id.severity;
|
| 282 |
+
if (severity in stats.by_severity) {
|
| 283 |
+
stats.by_severity[severity] += count;
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
return stats;
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// Singleton instance
|
| 293 |
+
export const warningService = new WarningService();
|