amulyakh456
Resolve relative redirect URLs in downloadOne
d9e607e
/**
* Express server for the drug-interaction backend.
* GET /api/health
* GET /api/search?q=...
* POST /api/check body: { brands: [...] }
*
* Run:
* npm start # production
* npm run dev # auto-reload on file changes
*/
'use strict';
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const https = require('https');
const express = require('express');
const cors = require('cors');
const { InteractionEngine } = require('./src/engine');
const { Gap1Cache } = require('./src/gap1Cache');
const { Gap3Cache } = require('./src/gap3Cache');
const { GeminiEnricher } = require('./src/gemini');
const PORT = process.env.PORT || 3001;
const DATA_DIR = process.env.DATA_DIR || path.resolve(__dirname, '..', 'data');
const REPO_DATA_DIR = path.resolve(__dirname, '..', 'data');
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || '';
// Download dataset files from HF via direct HTTPS (resolve URL bypasses LFS pointer issue)
const HF_DATASET_FILES = [
'interactions.csv',
'1mg_medicines_normalized.csv',
'canonical_generics.csv',
'gap1_cache.csv',
'gap3_cache.csv',
];
const HF_BASE = 'https://huggingface.co/datasets/amulyakh/drug-interactions-data/resolve/main';
const MIN_BYTES = 10_000; // anything smaller is treated as a stale stub and re-downloaded
function downloadOne(url, dest) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
const req = https.get(url, res => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
file.close();
fs.unlinkSync(dest);
const next = new URL(res.headers.location, url).toString();
return downloadOne(next, dest).then(resolve, reject);
}
if (res.statusCode !== 200) {
file.close();
fs.unlinkSync(dest);
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
}
res.pipe(file);
file.on('finish', () => file.close(() => resolve()));
});
req.on('error', err => {
file.close();
if (fs.existsSync(dest)) fs.unlinkSync(dest);
reject(err);
});
});
}
async function ensureData() {
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
for (const f of HF_DATASET_FILES) {
const dest = path.join(DATA_DIR, f);
const existing = fs.existsSync(dest) ? fs.statSync(dest).size : 0;
if (existing >= MIN_BYTES) {
console.log(`[startup] ${f} already present (${existing} bytes)`);
continue;
}
if (existing > 0) {
console.log(`[startup] ${f} is stub (${existing} bytes) β€” re-downloading`);
fs.unlinkSync(dest);
}
console.log(`[startup] downloading ${f}...`);
await downloadOne(`${HF_BASE}/${f}`, dest);
const size = fs.statSync(dest).size;
console.log(`[startup] ${f} β†’ ${size} bytes`);
}
}
// First-boot bootstrap: if DATA_DIR points at an empty persistent disk,
// copy the read-only CSVs shipped in the repo into it.
if (DATA_DIR !== REPO_DATA_DIR && fs.existsSync(REPO_DATA_DIR)) {
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
for (const f of fs.readdirSync(REPO_DATA_DIR)) {
const src = path.join(REPO_DATA_DIR, f);
const dst = path.join(DATA_DIR, f);
if (!fs.existsSync(dst) && fs.statSync(src).isFile()) {
fs.copyFileSync(src, dst);
console.log('[bootstrap] copied', f, 'β†’', DATA_DIR);
}
}
}
console.log('[startup] GEMINI_API_KEY:', GEMINI_API_KEY ? 'βœ“ set' : 'βœ— not set');
const app = express();
app.use(cors());
app.use(express.json());
let engine;
(async () => {
await ensureData();
console.log('Loading interaction engine from', DATA_DIR);
engine = new InteractionEngine(DATA_DIR);
engine.load();
const gap1Cache = new Gap1Cache();
const gap3Cache = new Gap3Cache();
let geminiEnricher = null;
if (GEMINI_API_KEY) {
geminiEnricher = new GeminiEnricher(GEMINI_API_KEY);
console.log('[gap1] Gemini enricher active');
} else {
console.log('[gap1] No GEMINI_API_KEY β€” Gap 1 fallback disabled (set env var to enable)');
}
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
medicines: engine.medicines.length,
pairs: engine.pairIndex.size,
ddinter_drugs: engine.knownDDInterDrugs.size,
gap1_cached: gap1Cache.medicines.size,
gap3_cached: gap3Cache.ingredients.size,
gemini_active: !!geminiEnricher,
});
});
app.get('/api/search', (req, res) => {
const q = String(req.query.q || '');
const limit = Math.min(parseInt(req.query.limit || '10', 10), 50);
res.json({ results: engine.search(q, limit) });
});
app.post('/api/check', async (req, res) => {
const brands = Array.isArray(req.body && req.body.brands) ? req.body.brands : null;
if (!brands) return res.status(400).json({ error: 'Body must be { brands: [...] }' });
console.log('[/api/check] brands:', brands, 'geminiEnricher:', !!geminiEnricher);
try {
if (geminiEnricher) {
console.log('[/api/check] using Gap 1 & Gap 3 fallback...');
const result = await engine.checkWithGap1Fallback(brands, gap1Cache, geminiEnricher, gap3Cache);
console.log('[/api/check] result unresolved:', result.unresolved_brands);
return res.json(result);
}
console.log('[/api/check] no geminiEnricher, using plain check');
res.json(engine.check(brands));
} catch (e) {
console.error('/api/check error:', e);
res.status(500).json({ error: e.message });
}
});
app.listen(PORT, () => {
console.log(`βœ“ Backend listening on http://localhost:${PORT}`);
});
})();