From ce287f63d17ebc8509daf9329453f6fcd3d7c8d2 Mon Sep 17 00:00:00 2001 From: Jason Stedwell Date: Mon, 6 Jul 2026 14:45:22 -0500 Subject: [PATCH] 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 --- server/db/init.js | 39 +++++++++++++++++++++++++++++++++++++-- server/db/migrations.js | 14 +++++++++++++- server/routes/litters.js | 7 ++++--- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/server/db/init.js b/server/db/init.js index f87deb5..5cc8c10 100644 --- a/server/db/init.js +++ b/server/db/init.js @@ -2,8 +2,11 @@ const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); -const dbPath = path.join(__dirname, '../../data'); -const db = new Database(path.join(dbPath, 'breedr.db')); +// Honor DB_PATH (used by Docker and the migration runner in index.js) so +// 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() { return db; @@ -91,6 +94,9 @@ function initDatabase() { male_count INTEGER DEFAULT 0, female_count INTEGER DEFAULT 0, stillborn_count INTEGER DEFAULT 0, + breeding_date TEXT, + whelping_date TEXT, + puppy_count INTEGER DEFAULT 0, notes TEXT, created_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) ───────────────────────────────────────── db.exec(` CREATE TABLE IF NOT EXISTS health_records ( @@ -135,6 +168,8 @@ function initDatabase() { ['result', 'TEXT'], ['vet_name', 'TEXT'], ['next_due', 'TEXT'], + ['record_date', 'TEXT'], + ['description', 'TEXT'], ]; for (const [col, def] of healthMigrations) { try { db.exec(`ALTER TABLE health_records ADD COLUMN ${col} ${def}`); } catch (_) { /* already exists */ } diff --git a/server/db/migrations.js b/server/db/migrations.js index a8a6676..5c52ce3 100644 --- a/server/db/migrations.js +++ b/server/db/migrations.js @@ -52,6 +52,13 @@ class MigrationRunner { 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 hasLitterIdColumn() { const columns = this.db.prepare("PRAGMA table_info(dogs)").all(); @@ -187,7 +194,12 @@ class MigrationRunner { // Migration 2: Add litter_id column if missing migration002_addLitterIdColumn() { 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()) { console.log('[Migration 002] litter_id column already exists, skipping'); return; diff --git a/server/routes/litters.js b/server/routes/litters.js index a53dc44..d946ee3 100644 --- a/server/routes/litters.js +++ b/server/routes/litters.js @@ -214,10 +214,11 @@ router.post('/:litterId/puppies/:puppyId/logs', (req, res) => { notes: notes || '' }); + // test_date mirrors record_date: the column is NOT NULL on fresh schemas const result = db.prepare(` - INSERT INTO health_records (dog_id, record_type, record_date, description, vet_name) - VALUES (?, ?, ?, ?, ?) - `).run(puppyId, record_type || 'weight_log', record_date, description, null); + INSERT INTO health_records (dog_id, record_type, record_date, test_date, description, vet_name) + VALUES (?, ?, ?, ?, ?, ?) + `).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); res.status(201).json(log);