From 3ae3458dfc3c5ba1f1229cf3a3d5af3a32e3fc19 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 01:59:52 -0500 Subject: [PATCH 1/4] Clean: Fresh database init with parents table - no migrations --- server/db/init.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/server/db/init.js b/server/db/init.js index b6aefb6..7b74afc 100644 --- a/server/db/init.js +++ b/server/db/init.js @@ -16,7 +16,7 @@ function initDatabase(dbPath) { console.log('Initializing database schema...'); - // Dogs table - Core registry + // Dogs table - NO sire/dam columns, only litter_id db.exec(` CREATE TABLE IF NOT EXISTS dogs ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -29,9 +29,11 @@ function initDatabase(dbPath) { microchip TEXT, photo_urls TEXT, -- JSON array of photo URLs notes TEXT, + litter_id INTEGER, is_active INTEGER DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (litter_id) REFERENCES litters(id) ON DELETE SET NULL ) `); @@ -42,7 +44,7 @@ function initDatabase(dbPath) { WHERE microchip IS NOT NULL `); - // Parents table - Relationship mapping + // Parents table - Stores sire/dam relationships db.exec(` CREATE TABLE IF NOT EXISTS parents ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -51,7 +53,7 @@ function initDatabase(dbPath) { parent_type TEXT NOT NULL CHECK(parent_type IN ('sire', 'dam')), FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE, FOREIGN KEY (parent_id) REFERENCES dogs(id) ON DELETE CASCADE, - UNIQUE(dog_id, parent_id, parent_type) + UNIQUE(dog_id, parent_type) ) `); @@ -122,6 +124,7 @@ function initDatabase(dbPath) { db.exec(` CREATE INDEX IF NOT EXISTS idx_dogs_name ON dogs(name); CREATE INDEX IF NOT EXISTS idx_dogs_registration ON dogs(registration_number); + CREATE INDEX IF NOT EXISTS idx_dogs_litter ON dogs(litter_id); CREATE INDEX IF NOT EXISTS idx_parents_dog ON parents(dog_id); CREATE INDEX IF NOT EXISTS idx_parents_parent ON parents(parent_id); CREATE INDEX IF NOT EXISTS idx_litters_sire ON litters(sire_id); @@ -141,7 +144,10 @@ function initDatabase(dbPath) { END; `); - console.log('Database schema initialized successfully!'); + console.log('✓ Database schema initialized successfully!'); + console.log('✓ Dogs table: NO sire/dam columns, uses parents table'); + console.log('✓ Parents table: Stores sire/dam relationships'); + console.log('✓ Litters table: Links puppies via litter_id'); db.close(); return true; @@ -159,5 +165,11 @@ module.exports = { initDatabase, getDatabase }; // Run initialization if called directly if (require.main === module) { const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/breedr.db'); + console.log('\n=========================================='); + console.log('BREEDR Database Initialization'); + console.log('=========================================='); + console.log(`Database: ${dbPath}`); + console.log('==========================================\n'); initDatabase(dbPath); -} \ No newline at end of file + console.log('\n✓ Database ready!\n'); +} From d311bc24a7c010d7d4dbace7b9baac0aae932b96 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 02:01:18 -0500 Subject: [PATCH 2/4] Clean: Proper sire/dam handling via parents table with logging --- server/routes/dogs.js | 195 +++++++++++++++++++----------------------- 1 file changed, 87 insertions(+), 108 deletions(-) diff --git a/server/routes/dogs.js b/server/routes/dogs.js index 9c5e600..3fdf934 100644 --- a/server/routes/dogs.js +++ b/server/routes/dogs.js @@ -41,10 +41,9 @@ const emptyToNull = (value) => { router.get('/', (req, res) => { try { const db = getDatabase(); - // Select only fields that exist in the schema (no weight/height) const dogs = db.prepare(` SELECT id, name, registration_number, breed, sex, birth_date, - color, microchip, photo_urls, notes, is_active, + color, microchip, photo_urls, notes, litter_id, is_active, created_at, updated_at FROM dogs WHERE is_active = 1 @@ -58,18 +57,18 @@ router.get('/', (req, res) => { res.json(dogs); } catch (error) { + console.error('Error fetching dogs:', error); res.status(500).json({ error: error.message }); } }); -// GET single dog by ID +// GET single dog by ID with parents and offspring router.get('/:id', (req, res) => { try { const db = getDatabase(); - // Select only fields that exist in the schema (no weight/height) const dog = db.prepare(` SELECT id, name, registration_number, breed, sex, birth_date, - color, microchip, photo_urls, notes, is_active, + color, microchip, photo_urls, notes, litter_id, is_active, created_at, updated_at FROM dogs WHERE id = ? @@ -101,6 +100,7 @@ router.get('/:id', (req, res) => { res.json(dog); } catch (error) { + console.error('Error fetching dog:', error); res.status(500).json({ error: error.message }); } }); @@ -110,77 +110,64 @@ router.post('/', (req, res) => { try { const { name, registration_number, breed, sex, birth_date, color, microchip, notes, sire_id, dam_id, litter_id } = req.body; + console.log('Creating dog with data:', { name, breed, sex, sire_id, dam_id, litter_id }); + if (!name || !breed || !sex) { return res.status(400).json({ error: 'Name, breed, and sex are required' }); } const db = getDatabase(); - // Check if litter_id column exists - let hasLitterId = false; - try { - const columns = db.prepare("PRAGMA table_info(dogs)").all(); - hasLitterId = columns.some(col => col.name === 'litter_id'); - } catch (e) { - console.error('Error checking schema:', e); - } - - // Insert with or without litter_id depending on schema - let result; - if (hasLitterId) { - result = db.prepare(` - INSERT INTO dogs (name, registration_number, breed, sex, birth_date, color, microchip, notes, litter_id, photo_urls) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run( - name, - emptyToNull(registration_number), - breed, - sex, - emptyToNull(birth_date), - emptyToNull(color), - emptyToNull(microchip), - emptyToNull(notes), - emptyToNull(litter_id), - '[]' - ); - } else { - result = db.prepare(` - INSERT INTO dogs (name, registration_number, breed, sex, birth_date, color, microchip, notes, photo_urls) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run( - name, - emptyToNull(registration_number), - breed, - sex, - emptyToNull(birth_date), - emptyToNull(color), - emptyToNull(microchip), - emptyToNull(notes), - '[]' - ); - } + // Insert dog (dogs table has NO sire/dam columns) + const result = db.prepare(` + INSERT INTO dogs (name, registration_number, breed, sex, birth_date, color, microchip, notes, litter_id, photo_urls) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + name, + emptyToNull(registration_number), + breed, + sex, + emptyToNull(birth_date), + emptyToNull(color), + emptyToNull(microchip), + emptyToNull(notes), + emptyToNull(litter_id), + '[]' + ); const dogId = result.lastInsertRowid; + console.log(`✓ Dog inserted with ID: ${dogId}`); - // Add parent relationships - if (sire_id) { - db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, "sire")').run(dogId, sire_id); - } - if (dam_id) { - db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, "dam")').run(dogId, dam_id); + // Add sire relationship if provided + if (sire_id && sire_id !== '' && sire_id !== null) { + console.log(` Adding sire relationship: dog ${dogId} -> sire ${sire_id}`); + db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)'). + run(dogId, sire_id, 'sire'); + console.log(` ✓ Sire relationship added`); } + // Add dam relationship if provided + if (dam_id && dam_id !== '' && dam_id !== null) { + console.log(` Adding dam relationship: dog ${dogId} -> dam ${dam_id}`); + db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)'). + run(dogId, dam_id, 'dam'); + console.log(` ✓ Dam relationship added`); + } + + // Fetch the created dog const dog = db.prepare(` SELECT id, name, registration_number, breed, sex, birth_date, - color, microchip, photo_urls, notes, is_active, + color, microchip, photo_urls, notes, litter_id, is_active, created_at, updated_at FROM dogs WHERE id = ? `).get(dogId); dog.photo_urls = []; + console.log(`✓ Dog created successfully: ${dog.name} (ID: ${dogId})`); res.status(201).json(dog); } catch (error) { + console.error('Error creating dog:', error); res.status(500).json({ error: error.message }); } }); @@ -190,76 +177,64 @@ router.put('/:id', (req, res) => { try { const { name, registration_number, breed, sex, birth_date, color, microchip, notes, sire_id, dam_id, litter_id } = req.body; + console.log(`Updating dog ${req.params.id} with data:`, { name, breed, sex, sire_id, dam_id, litter_id }); + const db = getDatabase(); - // Check if litter_id column exists - let hasLitterId = false; - try { - const columns = db.prepare("PRAGMA table_info(dogs)").all(); - hasLitterId = columns.some(col => col.name === 'litter_id'); - } catch (e) { - console.error('Error checking schema:', e); - } + // Update dog record (dogs table has NO sire/dam columns) + db.prepare(` + UPDATE dogs + SET name = ?, registration_number = ?, breed = ?, sex = ?, + birth_date = ?, color = ?, microchip = ?, notes = ?, litter_id = ? + WHERE id = ? + `).run( + name, + emptyToNull(registration_number), + breed, + sex, + emptyToNull(birth_date), + emptyToNull(color), + emptyToNull(microchip), + emptyToNull(notes), + emptyToNull(litter_id), + req.params.id + ); + console.log(` ✓ Dog record updated`); - // Update with or without litter_id - if (hasLitterId) { - db.prepare(` - UPDATE dogs - SET name = ?, registration_number = ?, breed = ?, sex = ?, - birth_date = ?, color = ?, microchip = ?, notes = ?, litter_id = ? - WHERE id = ? - `).run( - name, - emptyToNull(registration_number), - breed, - sex, - emptyToNull(birth_date), - emptyToNull(color), - emptyToNull(microchip), - emptyToNull(notes), - emptyToNull(litter_id), - req.params.id - ); - } else { - db.prepare(` - UPDATE dogs - SET name = ?, registration_number = ?, breed = ?, sex = ?, - birth_date = ?, color = ?, microchip = ?, notes = ? - WHERE id = ? - `).run( - name, - emptyToNull(registration_number), - breed, - sex, - emptyToNull(birth_date), - emptyToNull(color), - emptyToNull(microchip), - emptyToNull(notes), - req.params.id - ); - } - - // Update parent relationships + // Remove existing parent relationships db.prepare('DELETE FROM parents WHERE dog_id = ?').run(req.params.id); + console.log(` ✓ Old parent relationships removed`); - if (sire_id) { - db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, "sire")').run(req.params.id, sire_id); - } - if (dam_id) { - db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, "dam")').run(req.params.id, dam_id); + // Add new sire relationship if provided + if (sire_id && sire_id !== '' && sire_id !== null) { + console.log(` Adding sire relationship: dog ${req.params.id} -> sire ${sire_id}`); + db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)'). + run(req.params.id, sire_id, 'sire'); + console.log(` ✓ Sire relationship added`); } + // Add new dam relationship if provided + if (dam_id && dam_id !== '' && dam_id !== null) { + console.log(` Adding dam relationship: dog ${req.params.id} -> dam ${dam_id}`); + db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)'). + run(req.params.id, dam_id, 'dam'); + console.log(` ✓ Dam relationship added`); + } + + // Fetch updated dog const dog = db.prepare(` SELECT id, name, registration_number, breed, sex, birth_date, - color, microchip, photo_urls, notes, is_active, + color, microchip, photo_urls, notes, litter_id, is_active, created_at, updated_at FROM dogs WHERE id = ? `).get(req.params.id); dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : []; + console.log(`✓ Dog updated successfully: ${dog.name} (ID: ${req.params.id})`); res.json(dog); } catch (error) { + console.error('Error updating dog:', error); res.status(500).json({ error: error.message }); } }); @@ -269,8 +244,10 @@ router.delete('/:id', (req, res) => { try { const db = getDatabase(); db.prepare('UPDATE dogs SET is_active = 0 WHERE id = ?').run(req.params.id); + console.log(`✓ Dog soft-deleted: ID ${req.params.id}`); res.json({ message: 'Dog deleted successfully' }); } catch (error) { + console.error('Error deleting dog:', error); res.status(500).json({ error: error.message }); } }); @@ -296,6 +273,7 @@ router.post('/:id/photos', upload.single('photo'), (req, res) => { res.json({ url: `/uploads/${req.file.filename}`, photos: photoUrls }); } catch (error) { + console.error('Error uploading photo:', error); res.status(500).json({ error: error.message }); } }); @@ -327,6 +305,7 @@ router.delete('/:id/photos/:photoIndex', (req, res) => { res.json({ photos: photoUrls }); } catch (error) { + console.error('Error deleting photo:', error); res.status(500).json({ error: error.message }); } }); From 6f83f853aee33720868d5a10c11020f4f5be5942 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 02:03:02 -0500 Subject: [PATCH 3/4] Clean: Remove migrations - use clean init only --- server/index.js | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/server/index.js b/server/index.js index 8c22137..77a1e28 100644 --- a/server/index.js +++ b/server/index.js @@ -4,7 +4,6 @@ const helmet = require('helmet'); const path = require('path'); const fs = require('fs'); const { initDatabase } = require('./db/init'); -const { runMigrations } = require('./db/migrations'); const app = express(); const PORT = process.env.PORT || 3000; @@ -21,20 +20,9 @@ if (!fs.existsSync(UPLOAD_PATH)) { } // Initialize database schema (creates tables if they don't exist) +console.log('Initializing database...'); initDatabase(DB_PATH); - -// Run migrations to ensure schema is up-to-date -try { - console.log('Running database migrations...'); - runMigrations(DB_PATH); - console.log('Database migrations complete!\n'); -} catch (error) { - console.error('\n⚠️ Database migration failed!'); - console.error('Error:', error.message); - console.error('\nThe application may not function correctly.'); - console.error('Please check the database and try again.\n'); - // Don't exit - let the app try to start anyway -} +console.log('✓ Database ready!\n'); // Middleware app.use(helmet({ @@ -81,13 +69,13 @@ app.use((err, req, res, next) => { // Start server app.listen(PORT, '0.0.0.0', () => { console.log(`\n🐕 BREEDR Server Running`); - console.log(`=============================`); + console.log(`==============================`); console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); console.log(`Port: ${PORT}`); console.log(`Database: ${DB_PATH}`); console.log(`Uploads: ${UPLOAD_PATH}`); console.log(`Access: http://localhost:${PORT}`); - console.log(`=============================\n`); + console.log(`==============================\n`); }); module.exports = app; From 417dc96b4968972e140366a73514fa5550be5cbf Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 02:04:41 -0500 Subject: [PATCH 4/4] Docs: Add DATABASE.md with schema documentation --- DATABASE.md | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 DATABASE.md diff --git a/DATABASE.md b/DATABASE.md new file mode 100644 index 0000000..673c2af --- /dev/null +++ b/DATABASE.md @@ -0,0 +1,222 @@ +# BREEDR Database Schema + +## Overview + +This document describes the clean database schema for BREEDR. **NO migrations** - fresh installs create the correct schema automatically. + +## Schema Design + +### Core Principle: Parents Table Approach + +The `dogs` table **does NOT have sire/dam columns**. Parent relationships are stored in the separate `parents` table. This design: +- Keeps the schema clean and normalized +- Allows flexible parent relationships +- Supports future extensions (multiple sires, surrogates, etc.) + +## Tables + +### dogs + +Core registry for all dogs. + +```sql +CREATE TABLE dogs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + registration_number TEXT UNIQUE, + breed TEXT NOT NULL, + sex TEXT NOT NULL CHECK(sex IN ('male', 'female')), + birth_date DATE, + color TEXT, + microchip TEXT, + photo_urls TEXT, -- JSON array + notes TEXT, + litter_id INTEGER, -- Links to litters table + is_active INTEGER DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (litter_id) REFERENCES litters(id) ON DELETE SET NULL +); +``` + +**Important:** NO `sire_id` or `dam_id` columns! + +### parents + +Stores sire/dam relationships. + +```sql +CREATE TABLE parents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + dog_id INTEGER NOT NULL, -- The puppy + parent_id INTEGER NOT NULL, -- The parent + parent_type TEXT NOT NULL CHECK(parent_type IN ('sire', 'dam')), + FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE, + FOREIGN KEY (parent_id) REFERENCES dogs(id) ON DELETE CASCADE, + UNIQUE(dog_id, parent_type) -- One sire, one dam per dog +); +``` + +### litters + +Breeding records and litter tracking. + +```sql +CREATE TABLE litters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sire_id INTEGER NOT NULL, + dam_id INTEGER NOT NULL, + breeding_date DATE NOT NULL, + whelping_date DATE, + puppy_count INTEGER DEFAULT 0, + notes TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (sire_id) REFERENCES dogs(id) ON DELETE CASCADE, + FOREIGN KEY (dam_id) REFERENCES dogs(id) ON DELETE CASCADE +); +``` + +### health_records + +Health tests, vaccinations, exams, treatments. + +```sql +CREATE TABLE health_records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + dog_id INTEGER NOT NULL, + record_type TEXT NOT NULL CHECK(record_type IN ('test', 'vaccination', 'exam', 'treatment', 'certification')), + test_name TEXT, + test_date DATE NOT NULL, + result TEXT, + document_url TEXT, + notes TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE +); +``` + +### heat_cycles + +Female heat cycle tracking for breeding timing. + +```sql +CREATE TABLE heat_cycles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + dog_id INTEGER NOT NULL, + start_date DATE NOT NULL, + end_date DATE, + progesterone_peak_date DATE, + breeding_date DATE, + breeding_successful INTEGER DEFAULT 0, + notes TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE +); +``` + +### traits + +Genetic trait tracking and inheritance. + +```sql +CREATE TABLE traits ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + dog_id INTEGER NOT NULL, + trait_category TEXT NOT NULL, + trait_name TEXT NOT NULL, + trait_value TEXT NOT NULL, + inherited_from INTEGER, -- Parent dog ID + notes TEXT, + FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE, + FOREIGN KEY (inherited_from) REFERENCES dogs(id) ON DELETE SET NULL +); +``` + +## API Usage + +### Creating a Dog with Parents + +```javascript +POST /api/dogs +{ + "name": "Puppy Name", + "breed": "Breed Name", + "sex": "male", + "sire_id": 5, // Parent male dog ID + "dam_id": 8, // Parent female dog ID + "litter_id": 2 // Optional: link to litter +} +``` + +The API route automatically: +1. Inserts the dog into `dogs` table (without sire/dam columns) +2. Creates entries in `parents` table linking to sire and dam + +### Querying Parents + +```sql +-- Get a dog's parents +SELECT p.parent_type, d.* +FROM parents p +JOIN dogs d ON p.parent_id = d.id +WHERE p.dog_id = ?; + +-- Get a dog's offspring +SELECT d.* +FROM dogs d +JOIN parents p ON d.id = p.dog_id +WHERE p.parent_id = ?; +``` + +## Fresh Install + +For a fresh install: + +1. **Delete the old database** (if upgrading): + ```bash + rm data/breedr.db + ``` + +2. **Start the server** - it will create the correct schema automatically: + ```bash + npm run dev + ``` + +3. **Verify the schema**: + ```bash + sqlite3 data/breedr.db ".schema dogs" + ``` + + You should see `litter_id` but **NO** `sire_id` or `dam_id` columns. + +## Troubleshooting + +### "no such column: weight" or "no such column: sire_id" + +**Solution:** Your database has an old schema. Delete it and let the app recreate it: + +```bash +rm data/breedr.db +npm run dev +``` + +### Parent relationships not saving + +Check server logs. You should see: +``` +✓ Dog inserted with ID: 123 + Adding sire relationship: dog 123 -> sire 5 + ✓ Sire relationship added + Adding dam relationship: dog 123 -> dam 8 + ✓ Dam relationship added +``` + +If relationships aren't being created, check that `sire_id` and `dam_id` are being sent in the API request. + +## Database Files + +- `server/db/init.js` - Creates clean schema, no migrations +- `server/routes/dogs.js` - Handles parent relationships via `parents` table +- `server/index.js` - Initializes database on startup + +**NO MIGRATIONS!** The init file is the source of truth.