Fix schema drift: fresh installs were broken
- Create heat_cycles table in init.js (breeding.js used it but nothing created it) - Add litters columns the routes actually use (breeding_date, whelping_date, puppy_count) - Add health_records columns used by puppy logs (record_date, description) - Puppy log insert now also sets NOT NULL test_date - Migration 002 no-ops on a fresh DB instead of crashing on missing dogs table - init.js honors DB_PATH and creates the data dir, so migrations and the app always target the same file Verified: full heat-cycle/litter/puppy-log CRUD passes against a virgin database. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+37
-2
@@ -2,8 +2,11 @@ const Database = require('better-sqlite3');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const dbPath = path.join(__dirname, '../../data');
|
// Honor DB_PATH (used by Docker and the migration runner in index.js) so
|
||||||
const db = new Database(path.join(dbPath, 'breedr.db'));
|
// migrations and the app always operate on the same file.
|
||||||
|
const dbFile = process.env.DB_PATH || path.join(__dirname, '../../data/breedr.db');
|
||||||
|
fs.mkdirSync(path.dirname(dbFile), { recursive: true });
|
||||||
|
const db = new Database(dbFile);
|
||||||
|
|
||||||
function getDatabase() {
|
function getDatabase() {
|
||||||
return db;
|
return db;
|
||||||
@@ -91,6 +94,9 @@ function initDatabase() {
|
|||||||
male_count INTEGER DEFAULT 0,
|
male_count INTEGER DEFAULT 0,
|
||||||
female_count INTEGER DEFAULT 0,
|
female_count INTEGER DEFAULT 0,
|
||||||
stillborn_count INTEGER DEFAULT 0,
|
stillborn_count INTEGER DEFAULT 0,
|
||||||
|
breeding_date TEXT,
|
||||||
|
whelping_date TEXT,
|
||||||
|
puppy_count INTEGER DEFAULT 0,
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
created_at TEXT DEFAULT (datetime('now')),
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
updated_at TEXT DEFAULT (datetime('now')),
|
updated_at TEXT DEFAULT (datetime('now')),
|
||||||
@@ -100,6 +106,33 @@ function initDatabase() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// migrate: columns the litters routes use (routes/litters.js) that older
|
||||||
|
// schemas lack
|
||||||
|
const litterMigrations = [
|
||||||
|
['breeding_date', 'TEXT'],
|
||||||
|
['whelping_date', 'TEXT'],
|
||||||
|
['puppy_count', 'INTEGER DEFAULT 0'],
|
||||||
|
];
|
||||||
|
for (const [col, def] of litterMigrations) {
|
||||||
|
try { db.exec(`ALTER TABLE litters ADD COLUMN ${col} ${def}`); } catch (_) { /* already exists */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Heat Cycles ───────────────────────────────────────────────────────────
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS heat_cycles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
dog_id INTEGER NOT NULL,
|
||||||
|
start_date TEXT NOT NULL,
|
||||||
|
end_date TEXT,
|
||||||
|
breeding_date TEXT,
|
||||||
|
breeding_successful INTEGER DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT DEFAULT (datetime('now')),
|
||||||
|
FOREIGN KEY (dog_id) REFERENCES dogs(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
// ── Health Records (OFA-extended) ─────────────────────────────────────────
|
// ── Health Records (OFA-extended) ─────────────────────────────────────────
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS health_records (
|
CREATE TABLE IF NOT EXISTS health_records (
|
||||||
@@ -135,6 +168,8 @@ function initDatabase() {
|
|||||||
['result', 'TEXT'],
|
['result', 'TEXT'],
|
||||||
['vet_name', 'TEXT'],
|
['vet_name', 'TEXT'],
|
||||||
['next_due', 'TEXT'],
|
['next_due', 'TEXT'],
|
||||||
|
['record_date', 'TEXT'],
|
||||||
|
['description', 'TEXT'],
|
||||||
];
|
];
|
||||||
for (const [col, def] of healthMigrations) {
|
for (const [col, def] of healthMigrations) {
|
||||||
try { db.exec(`ALTER TABLE health_records ADD COLUMN ${col} ${def}`); } catch (_) { /* already exists */ }
|
try { db.exec(`ALTER TABLE health_records ADD COLUMN ${col} ${def}`); } catch (_) { /* already exists */ }
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ class MigrationRunner {
|
|||||||
return columns.some(col => col.name === 'sire' || col.name === 'dam');
|
return columns.some(col => col.name === 'sire' || col.name === 'dam');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a table exists (fresh DBs have none — migrations must no-op)
|
||||||
|
tableExists(name) {
|
||||||
|
return !!this.db.prepare(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
|
||||||
|
).get(name);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if litter_id column exists
|
// Check if litter_id column exists
|
||||||
hasLitterIdColumn() {
|
hasLitterIdColumn() {
|
||||||
const columns = this.db.prepare("PRAGMA table_info(dogs)").all();
|
const columns = this.db.prepare("PRAGMA table_info(dogs)").all();
|
||||||
@@ -188,6 +195,11 @@ class MigrationRunner {
|
|||||||
migration002_addLitterIdColumn() {
|
migration002_addLitterIdColumn() {
|
||||||
console.log('[Migration 002] Checking for litter_id column...');
|
console.log('[Migration 002] Checking for litter_id column...');
|
||||||
|
|
||||||
|
if (!this.tableExists('dogs')) {
|
||||||
|
console.log('[Migration 002] Fresh database (no dogs table yet), skipping — init will create it');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hasLitterIdColumn()) {
|
if (this.hasLitterIdColumn()) {
|
||||||
console.log('[Migration 002] litter_id column already exists, skipping');
|
console.log('[Migration 002] litter_id column already exists, skipping');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -214,10 +214,11 @@ router.post('/:litterId/puppies/:puppyId/logs', (req, res) => {
|
|||||||
notes: notes || ''
|
notes: notes || ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// test_date mirrors record_date: the column is NOT NULL on fresh schemas
|
||||||
const result = db.prepare(`
|
const result = db.prepare(`
|
||||||
INSERT INTO health_records (dog_id, record_type, record_date, description, vet_name)
|
INSERT INTO health_records (dog_id, record_type, record_date, test_date, description, vet_name)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`).run(puppyId, record_type || 'weight_log', record_date, description, null);
|
`).run(puppyId, record_type || 'weight_log', record_date, record_date, description, null);
|
||||||
|
|
||||||
const log = db.prepare('SELECT * FROM health_records WHERE id = ?').get(result.lastInsertRowid);
|
const log = db.prepare('SELECT * FROM health_records WHERE id = ?').get(result.lastInsertRowid);
|
||||||
res.status(201).json(log);
|
res.status(201).json(log);
|
||||||
|
|||||||
Reference in New Issue
Block a user