Missing files from commit
Run Tests on Branches / Frontend Tests (push) Has been cancelled
Run Tests on Branches / Backend Tests (push) Has been cancelled

This commit is contained in:
Antoni Nuñez Romeu
2026-07-07 17:44:36 +02:00
parent 7eb791f181
commit a58ce306bf
10 changed files with 497 additions and 126 deletions
+41 -12
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* CLI: pull pharmacies from webhook and insert into database.sqlite
* CLI: pull pharmacies from webhook and insert into database
*
* npm run import-farmacias
* FARMACIAS_WEBHOOK_URL=https://... npm run import-farmacias
@@ -10,12 +10,15 @@
* node import-farmacias.js "https://n8n.example/webhook/farmacias" --lat 41.5631 --lon 2.0038 --radio 1500
*
* Env defaults for region: FARMACIAS_IMPORT_LAT, FARMACIAS_IMPORT_LON, FARMACIAS_IMPORT_RADIO
*
* Uses PostgreSQL when PG_URL is set, otherwise falls back to local SQLite.
*/
import sqlite3 from 'sqlite3';
import { promisify } from 'util';
import path from 'path';
import { fileURLToPath } from 'url';
import pg from 'pg';
import {
runFarmaciaWebhookImport,
DEFAULT_FARMACIAS_WEBHOOK,
@@ -24,19 +27,43 @@ import {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const dbPath = path.join(__dirname, 'database.sqlite');
const db = new sqlite3.Database(dbPath);
const PG_URL = process.env.PG_URL;
let pool = null;
let db = null;
function dbRun(sql, params = []) {
return new Promise((resolve, reject) => {
db.run(sql, params, function (err) {
if (err) reject(err);
else resolve({ lastID: this.lastID, changes: this.changes });
let dbRun, dbGet;
if (PG_URL) {
pool = new pg.Pool({ connectionString: PG_URL });
function toPositional(sql) {
let i = 0;
return sql.replace(/\?/g, () => `$${++i}`);
}
dbGet = async (sql, params = []) => {
const res = await pool.query(toPositional(sql), params);
return res.rows[0] ?? null;
};
dbRun = async (sql, params = []) => {
const res = await pool.query(toPositional(sql), params);
return { lastID: res.rows[0]?.id, changes: res.rowCount };
};
} else {
const dbPath = path.join(__dirname, 'database.sqlite');
db = new sqlite3.Database(dbPath);
dbRun = (sql, params = []) =>
new Promise((resolve, reject) => {
db.run(sql, params, function (err) {
if (err) reject(err);
else resolve({ lastID: this.lastID, changes: this.changes });
});
});
});
}
const dbGet = promisify(db.get.bind(db));
dbGet = promisify(db.get.bind(db));
}
function parseCli(argv) {
const region = {};
@@ -76,6 +103,7 @@ async function main() {
const { url, region } = parseCli(process.argv);
console.log('Fetching pharmacies from:', url);
if (region) console.log('Region query:', region);
console.log('Using:', PG_URL ? 'PostgreSQL' : 'SQLite');
try {
const result = await runFarmaciaWebhookImport(dbGet, dbRun, url, region);
@@ -97,7 +125,8 @@ async function main() {
}
process.exitCode = 1;
} finally {
db.close();
if (pool) await pool.end();
else if (db) db.close();
}
}
+149 -78
View File
@@ -175,42 +175,68 @@ function serializeOpeningHours(value) {
// Initialize database tables
async function initDatabase() {
try {
// Create pharmacies table
await dbRun(`
CREATE TABLE IF NOT EXISTS pharmacies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
address TEXT NOT NULL,
phone TEXT,
latitude REAL,
longitude REAL,
opening_hours TEXT
)
`);
// Add opening_hours to pre-existing pharmacies tables (no-op if already present)
try {
await dbRun(`ALTER TABLE pharmacies ADD COLUMN opening_hours TEXT`);
} catch (e) {
if (!/duplicate column/i.test(e.message)) throw e;
// Create pharmacies table — PG when available, SQLite fallback
if (pgPool) {
await pgPool.query(`
CREATE TABLE IF NOT EXISTS pharmacies (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
address TEXT NOT NULL,
phone TEXT,
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
opening_hours TEXT
)
`);
} else {
await dbRun(`
CREATE TABLE IF NOT EXISTS pharmacies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
address TEXT NOT NULL,
phone TEXT,
latitude REAL,
longitude REAL,
opening_hours TEXT
)
`);
// Add opening_hours to pre-existing pharmacies tables (no-op if already present)
try {
await dbRun(`ALTER TABLE pharmacies ADD COLUMN opening_hours TEXT`);
} catch (e) {
if (!/duplicate column/i.test(e.message)) throw e;
}
}
// Create junction table for pharmacy-medicine relationships
// Ahora usa nregistro (número de registro de CIMA) en lugar de medicine_id local
await dbRun(`
CREATE TABLE IF NOT EXISTS pharmacy_medicines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pharmacy_id INTEGER NOT NULL,
medicine_nregistro TEXT NOT NULL,
medicine_name TEXT,
price REAL,
stock INTEGER DEFAULT 0,
FOREIGN KEY (pharmacy_id) REFERENCES pharmacies(id),
UNIQUE(pharmacy_id, medicine_nregistro)
)
`);
await dbRun(`CREATE INDEX IF NOT EXISTS idx_pharmacy_medicine ON pharmacy_medicines(medicine_nregistro)`);
if (pgPool) {
await pgPool.query(`
CREATE TABLE IF NOT EXISTS pharmacy_medicines (
id SERIAL PRIMARY KEY,
pharmacy_id INTEGER NOT NULL REFERENCES pharmacies(id),
medicine_nregistro TEXT NOT NULL,
medicine_name TEXT,
price REAL,
stock INTEGER DEFAULT 0,
UNIQUE(pharmacy_id, medicine_nregistro)
)
`);
await pgPool.query(`CREATE INDEX IF NOT EXISTS idx_pharmacy_medicine ON pharmacy_medicines(medicine_nregistro)`);
} else {
await dbRun(`
CREATE TABLE IF NOT EXISTS pharmacy_medicines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pharmacy_id INTEGER NOT NULL,
medicine_nregistro TEXT NOT NULL,
medicine_name TEXT,
price REAL,
stock INTEGER DEFAULT 0,
FOREIGN KEY (pharmacy_id) REFERENCES pharmacies(id),
UNIQUE(pharmacy_id, medicine_nregistro)
)
`);
await dbRun(`CREATE INDEX IF NOT EXISTS idx_pharmacy_medicine ON pharmacy_medicines(medicine_nregistro)`);
}
// Create users table — PG when available, SQLite fallback
if (pgPool) {
@@ -395,6 +421,36 @@ if (!pgPool) {
`);
}
// Recent searches table
if (pgPool) {
await pgPool.query(`
CREATE TABLE IF NOT EXISTS recent_searches (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
medicine_id TEXT NOT NULL,
name TEXT NOT NULL,
active_ingredient TEXT,
dosage TEXT,
form TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
)
`);
} else {
await dbRun(`
CREATE TABLE IF NOT EXISTS recent_searches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
medicine_id TEXT NOT NULL,
name TEXT NOT NULL,
active_ingredient TEXT,
dosage TEXT,
form TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
}
// User addresses table
if (pgPool) {
await pgPool.query(`
@@ -456,7 +512,7 @@ app.get('/api/medicines/:medicineId/pharmacies', async (req, res) => {
try {
const nregistro = req.params.medicineId; // Ahora es el nregistro de CIMA
const pharmacies = await dbAll(`
const pharmacies = await userDbAll(`
SELECT
p.id,
p.name,
@@ -519,46 +575,61 @@ const requireAdmin = (req, res, next) => {
return res.status(401).json({ error: 'Authentication required' });
};
// ========== RECENT SEARCHES (session-based) ==========
// ========== RECENT SEARCHES (database-based) ==========
const MAX_RECENT = 10;
const MAX_RECENT = 5;
// Get recent searches for the logged-in user
app.get('/api/search/recent', requireAuth, (req, res) => {
res.json(req.session.recentSearches || []);
app.get('/api/search/recent', requireAuth, async (req, res) => {
try {
const userId = req.session.userId;
const rows = await userDbAll(
'SELECT id, medicine_id, name, active_ingredient, dosage, form, created_at FROM recent_searches WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
[userId, MAX_RECENT]
);
res.json(rows.map(r => ({
id: r.medicine_id,
name: r.name,
active_ingredient: r.active_ingredient,
dosage: r.dosage,
form: r.form,
timestamp: r.created_at,
})));
} catch (error) {
console.error('Error fetching recent searches:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Save a medicine to recent searches
app.post('/api/search/recent', requireAuth, (req, res) => {
app.post('/api/search/recent', requireAuth, async (req, res) => {
try {
const { medicine } = req.body;
if (!medicine || !medicine.id) {
return res.status(400).json({ error: 'Medicine data required' });
}
if (!req.session.recentSearches) {
req.session.recentSearches = [];
}
const userId = req.session.userId;
// Remove duplicate if exists (by id)
req.session.recentSearches = req.session.recentSearches.filter(
(m) => m.id !== medicine.id
// Remove duplicate if exists (by medicine_id)
await userDbRun(
'DELETE FROM recent_searches WHERE user_id = ? AND medicine_id = ?',
[userId, medicine.id]
);
// Add to front with timestamp
req.session.recentSearches.unshift({
id: medicine.id,
name: medicine.name,
active_ingredient: medicine.active_ingredient,
dosage: medicine.dosage,
form: medicine.form,
timestamp: Date.now(),
});
// Insert new entry
await userDbRun(
'INSERT INTO recent_searches (user_id, medicine_id, name, active_ingredient, dosage, form) VALUES (?, ?, ?, ?, ?, ?)',
[userId, medicine.id, medicine.name, medicine.active_ingredient || null, medicine.dosage || null, medicine.form || null]
);
// Keep only last MAX_RECENT
if (req.session.recentSearches.length > MAX_RECENT) {
req.session.recentSearches = req.session.recentSearches.slice(0, MAX_RECENT);
}
// Trim to MAX_RECENT — delete oldest beyond limit
await userDbRun(
`DELETE FROM recent_searches WHERE user_id = ? AND id NOT IN (
SELECT id FROM recent_searches WHERE user_id = ? ORDER BY created_at DESC LIMIT ?
)`,
[userId, userId, MAX_RECENT]
);
res.json({ ok: true });
} catch (error) {
@@ -570,7 +641,7 @@ app.post('/api/search/recent', requireAuth, (req, res) => {
// Get all pharmacies (for admin/debugging)
app.get('/api/pharmacies', async (req, res) => {
try {
const pharmacies = await dbAll(`
const pharmacies = await userDbAll(`
SELECT * FROM pharmacies ORDER BY name
`);
res.json(pharmacies);
@@ -1447,7 +1518,7 @@ app.post('/api/admin/pharmacies', requireAdmin, async (req, res) => {
}
// Check for duplicate (same name and address)
const existing = await dbGet(
const existing = await userDbGet(
'SELECT * FROM pharmacies WHERE name = ? AND address = ?',
[name.trim(), address.trim()]
);
@@ -1458,7 +1529,7 @@ app.post('/api/admin/pharmacies', requireAdmin, async (req, res) => {
const openingHoursValue = serializeOpeningHours(opening_hours);
const result = await dbRun(
const result = await userDbRun(
'INSERT INTO pharmacies (name, address, phone, latitude, longitude, opening_hours) VALUES (?, ?, ?, ?, ?, ?)',
[name.trim(), address.trim(), phone ? phone.trim() : null, latitude || null, longitude || null, openingHoursValue]
);
@@ -1467,7 +1538,7 @@ app.post('/api/admin/pharmacies', requireAdmin, async (req, res) => {
throw new Error('Failed to get lastID from database insert');
}
const newPharmacy = await dbGet(
const newPharmacy = await userDbGet(
'SELECT * FROM pharmacies WHERE id = ?',
[result.lastID]
);
@@ -1499,12 +1570,12 @@ app.put('/api/admin/pharmacies/:id', requireAdmin, async (req, res) => {
const openingHoursValue = serializeOpeningHours(opening_hours);
await dbRun(
await userDbRun(
'UPDATE pharmacies SET name = ?, address = ?, phone = ?, latitude = ?, longitude = ?, opening_hours = ? WHERE id = ?',
[name, address, phone || null, latitude || null, longitude || null, openingHoursValue, pharmacyId]
);
const updatedPharmacy = await dbGet(
const updatedPharmacy = await userDbGet(
'SELECT * FROM pharmacies WHERE id = ?',
[pharmacyId]
);
@@ -1526,10 +1597,10 @@ app.delete('/api/admin/pharmacies/:id', requireAdmin, async (req, res) => {
const pharmacyId = parseInt(req.params.id);
// Delete related pharmacy_medicines first
await dbRun('DELETE FROM pharmacy_medicines WHERE pharmacy_id = ?', [pharmacyId]);
await userDbRun('DELETE FROM pharmacy_medicines WHERE pharmacy_id = ?', [pharmacyId]);
// Delete the pharmacy
await dbRun('DELETE FROM pharmacies WHERE id = ?', [pharmacyId]);
await userDbRun('DELETE FROM pharmacies WHERE id = ?', [pharmacyId]);
res.json({ message: 'Pharmacy deleted successfully' });
} catch (error) {
@@ -1555,8 +1626,8 @@ app.post('/api/admin/pharmacies/import-webhook', requireAdmin, async (req, res)
const hasRegion =
region.lat != null || region.lon != null || region.radio != null;
const result = await runFarmaciaWebhookImport(
dbGet,
dbRun,
userDbGet,
userDbRun,
url.trim(),
hasRegion ? region : null
);
@@ -1604,7 +1675,7 @@ app.post('/api/admin/pharmacies/import-external', requireAdmin, async (req, res)
});
}
const stats = await importPharmaciesFromRows(dbGet, dbRun, rows);
const stats = await importPharmaciesFromRows(userDbGet, userDbRun, rows);
res.json({ ...stats, totalReceived: rows.length, source });
} catch (error) {
console.error('External pharmacy import:', error);
@@ -1636,7 +1707,7 @@ app.get('/api/admin/pharmacies/:pharmacyId/medicines', requireAdmin, async (req,
try {
const pharmacyId = parseInt(req.params.pharmacyId);
const medicines = await dbAll(`
const medicines = await userDbAll(`
SELECT
pm.id,
pm.pharmacy_id,
@@ -1666,33 +1737,33 @@ app.post('/api/admin/pharmacy-medicines', requireAdmin, async (req, res) => {
}
// Check if relationship already exists
const existing = await dbGet(
const existing = await userDbGet(
'SELECT * FROM pharmacy_medicines WHERE pharmacy_id = ? AND medicine_nregistro = ?',
[pharmacy_id, medicine_nregistro]
);
if (existing) {
// Update existing relationship
await dbRun(
await userDbRun(
'UPDATE pharmacy_medicines SET medicine_name = ?, price = ?, stock = ? WHERE pharmacy_id = ? AND medicine_nregistro = ?',
[medicine_name, price || null, stock || 0, pharmacy_id, medicine_nregistro]
);
} else {
// Insert new relationship
await dbRun(
await userDbRun(
'INSERT INTO pharmacy_medicines (pharmacy_id, medicine_nregistro, medicine_name, price, stock) VALUES (?, ?, ?, ?, ?)',
[pharmacy_id, medicine_nregistro, medicine_name, price || null, stock || 0]
);
}
const relationship = await dbGet(
const relationship = await userDbGet(
`SELECT * FROM pharmacy_medicines
WHERE pharmacy_id = ? AND medicine_nregistro = ?`,
[pharmacy_id, medicine_nregistro]
);
if (!existing) {
const pharmacy = await dbGet('SELECT id, name FROM pharmacies WHERE id = ?', [pharmacy_id]);
const pharmacy = await userDbGet('SELECT id, name FROM pharmacies WHERE id = ?', [pharmacy_id]);
sendPushForMedicine({
medicine_nregistro,
medicine_name: medicine_name || relationship?.medicine_name || null,
@@ -1713,12 +1784,12 @@ app.put('/api/admin/pharmacy-medicines/:id', requireAdmin, async (req, res) => {
const id = parseInt(req.params.id);
const { price, stock } = req.body;
await dbRun(
await userDbRun(
'UPDATE pharmacy_medicines SET price = ?, stock = ? WHERE id = ?',
[price || null, stock || 0, id]
);
const updated = await dbGet(
const updated = await userDbGet(
'SELECT * FROM pharmacy_medicines WHERE id = ?',
[id]
);
@@ -1738,7 +1809,7 @@ app.put('/api/admin/pharmacy-medicines/:id', requireAdmin, async (req, res) => {
app.delete('/api/admin/pharmacy-medicines/:id', requireAdmin, async (req, res) => {
try {
const id = parseInt(req.params.id);
await dbRun('DELETE FROM pharmacy_medicines WHERE id = ?', [id]);
await userDbRun('DELETE FROM pharmacy_medicines WHERE id = ?', [id]);
res.json({ message: 'Medicine removed from pharmacy successfully' });
} catch (error) {
console.error('Error deleting pharmacy-medicine:', error);