Merge pull request 'feature/enhanced-litters-and-pedigree' (#15) from feature/enhanced-litters-and-pedigree into master
Reviewed-on: #15
This commit was merged in pull request #15.
This commit is contained in:
222
DATABASE.md
Normal file
222
DATABASE.md
Normal file
@@ -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.
|
||||||
@@ -16,7 +16,7 @@ function initDatabase(dbPath) {
|
|||||||
|
|
||||||
console.log('Initializing database schema...');
|
console.log('Initializing database schema...');
|
||||||
|
|
||||||
// Dogs table - Core registry
|
// Dogs table - NO sire/dam columns, only litter_id
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS dogs (
|
CREATE TABLE IF NOT EXISTS dogs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -29,9 +29,11 @@ function initDatabase(dbPath) {
|
|||||||
microchip TEXT,
|
microchip TEXT,
|
||||||
photo_urls TEXT, -- JSON array of photo URLs
|
photo_urls TEXT, -- JSON array of photo URLs
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
|
litter_id INTEGER,
|
||||||
is_active INTEGER DEFAULT 1,
|
is_active INTEGER DEFAULT 1,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
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
|
WHERE microchip IS NOT NULL
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Parents table - Relationship mapping
|
// Parents table - Stores sire/dam relationships
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS parents (
|
CREATE TABLE IF NOT EXISTS parents (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -51,7 +53,7 @@ function initDatabase(dbPath) {
|
|||||||
parent_type TEXT NOT NULL CHECK(parent_type IN ('sire', 'dam')),
|
parent_type TEXT NOT NULL CHECK(parent_type IN ('sire', 'dam')),
|
||||||
FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE,
|
FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (parent_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(`
|
db.exec(`
|
||||||
CREATE INDEX IF NOT EXISTS idx_dogs_name ON dogs(name);
|
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_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_dog ON parents(dog_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_parents_parent ON parents(parent_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);
|
CREATE INDEX IF NOT EXISTS idx_litters_sire ON litters(sire_id);
|
||||||
@@ -141,7 +144,10 @@ function initDatabase(dbPath) {
|
|||||||
END;
|
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();
|
db.close();
|
||||||
return true;
|
return true;
|
||||||
@@ -159,5 +165,11 @@ module.exports = { initDatabase, getDatabase };
|
|||||||
// Run initialization if called directly
|
// Run initialization if called directly
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/breedr.db');
|
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);
|
initDatabase(dbPath);
|
||||||
}
|
console.log('\n✓ Database ready!\n');
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ const helmet = require('helmet');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { initDatabase } = require('./db/init');
|
const { initDatabase } = require('./db/init');
|
||||||
const { runMigrations } = require('./db/migrations');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
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)
|
// Initialize database schema (creates tables if they don't exist)
|
||||||
|
console.log('Initializing database...');
|
||||||
initDatabase(DB_PATH);
|
initDatabase(DB_PATH);
|
||||||
|
console.log('✓ Database ready!\n');
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(helmet({
|
app.use(helmet({
|
||||||
@@ -81,13 +69,13 @@ app.use((err, req, res, next) => {
|
|||||||
// Start server
|
// Start server
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`\n🐕 BREEDR Server Running`);
|
console.log(`\n🐕 BREEDR Server Running`);
|
||||||
console.log(`=============================`);
|
console.log(`==============================`);
|
||||||
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||||
console.log(`Port: ${PORT}`);
|
console.log(`Port: ${PORT}`);
|
||||||
console.log(`Database: ${DB_PATH}`);
|
console.log(`Database: ${DB_PATH}`);
|
||||||
console.log(`Uploads: ${UPLOAD_PATH}`);
|
console.log(`Uploads: ${UPLOAD_PATH}`);
|
||||||
console.log(`Access: http://localhost:${PORT}`);
|
console.log(`Access: http://localhost:${PORT}`);
|
||||||
console.log(`=============================\n`);
|
console.log(`==============================\n`);
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
@@ -41,10 +41,9 @@ const emptyToNull = (value) => {
|
|||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
// Select only fields that exist in the schema (no weight/height)
|
|
||||||
const dogs = db.prepare(`
|
const dogs = db.prepare(`
|
||||||
SELECT id, name, registration_number, breed, sex, birth_date,
|
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
|
created_at, updated_at
|
||||||
FROM dogs
|
FROM dogs
|
||||||
WHERE is_active = 1
|
WHERE is_active = 1
|
||||||
@@ -58,18 +57,18 @@ router.get('/', (req, res) => {
|
|||||||
|
|
||||||
res.json(dogs);
|
res.json(dogs);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error fetching dogs:', error);
|
||||||
res.status(500).json({ error: error.message });
|
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) => {
|
router.get('/:id', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
// Select only fields that exist in the schema (no weight/height)
|
|
||||||
const dog = db.prepare(`
|
const dog = db.prepare(`
|
||||||
SELECT id, name, registration_number, breed, sex, birth_date,
|
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
|
created_at, updated_at
|
||||||
FROM dogs
|
FROM dogs
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
@@ -101,6 +100,7 @@ router.get('/:id', (req, res) => {
|
|||||||
|
|
||||||
res.json(dog);
|
res.json(dog);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error fetching dog:', error);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -110,77 +110,64 @@ router.post('/', (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { name, registration_number, breed, sex, birth_date, color, microchip, notes, sire_id, dam_id, litter_id } = req.body;
|
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) {
|
if (!name || !breed || !sex) {
|
||||||
return res.status(400).json({ error: 'Name, breed, and sex are required' });
|
return res.status(400).json({ error: 'Name, breed, and sex are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
|
|
||||||
// Check if litter_id column exists
|
// Insert dog (dogs table has NO sire/dam columns)
|
||||||
let hasLitterId = false;
|
const result = db.prepare(`
|
||||||
try {
|
INSERT INTO dogs (name, registration_number, breed, sex, birth_date, color, microchip, notes, litter_id, photo_urls)
|
||||||
const columns = db.prepare("PRAGMA table_info(dogs)").all();
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
hasLitterId = columns.some(col => col.name === 'litter_id');
|
`).run(
|
||||||
} catch (e) {
|
name,
|
||||||
console.error('Error checking schema:', e);
|
emptyToNull(registration_number),
|
||||||
}
|
breed,
|
||||||
|
sex,
|
||||||
// Insert with or without litter_id depending on schema
|
emptyToNull(birth_date),
|
||||||
let result;
|
emptyToNull(color),
|
||||||
if (hasLitterId) {
|
emptyToNull(microchip),
|
||||||
result = db.prepare(`
|
emptyToNull(notes),
|
||||||
INSERT INTO dogs (name, registration_number, breed, sex, birth_date, color, microchip, notes, litter_id, photo_urls)
|
emptyToNull(litter_id),
|
||||||
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),
|
|
||||||
'[]'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dogId = result.lastInsertRowid;
|
const dogId = result.lastInsertRowid;
|
||||||
|
console.log(`✓ Dog inserted with ID: ${dogId}`);
|
||||||
|
|
||||||
// Add parent relationships
|
// Add sire relationship if provided
|
||||||
if (sire_id) {
|
if (sire_id && sire_id !== '' && sire_id !== null) {
|
||||||
db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, "sire")').run(dogId, sire_id);
|
console.log(` Adding sire relationship: dog ${dogId} -> sire ${sire_id}`);
|
||||||
}
|
db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').
|
||||||
if (dam_id) {
|
run(dogId, sire_id, 'sire');
|
||||||
db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, "dam")').run(dogId, dam_id);
|
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(`
|
const dog = db.prepare(`
|
||||||
SELECT id, name, registration_number, breed, sex, birth_date,
|
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
|
created_at, updated_at
|
||||||
FROM dogs
|
FROM dogs
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).get(dogId);
|
`).get(dogId);
|
||||||
dog.photo_urls = [];
|
dog.photo_urls = [];
|
||||||
|
|
||||||
|
console.log(`✓ Dog created successfully: ${dog.name} (ID: ${dogId})`);
|
||||||
res.status(201).json(dog);
|
res.status(201).json(dog);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error creating dog:', error);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -190,76 +177,64 @@ router.put('/:id', (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { name, registration_number, breed, sex, birth_date, color, microchip, notes, sire_id, dam_id, litter_id } = req.body;
|
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();
|
const db = getDatabase();
|
||||||
|
|
||||||
// Check if litter_id column exists
|
// Update dog record (dogs table has NO sire/dam columns)
|
||||||
let hasLitterId = false;
|
db.prepare(`
|
||||||
try {
|
UPDATE dogs
|
||||||
const columns = db.prepare("PRAGMA table_info(dogs)").all();
|
SET name = ?, registration_number = ?, breed = ?, sex = ?,
|
||||||
hasLitterId = columns.some(col => col.name === 'litter_id');
|
birth_date = ?, color = ?, microchip = ?, notes = ?, litter_id = ?
|
||||||
} catch (e) {
|
WHERE id = ?
|
||||||
console.error('Error checking schema:', e);
|
`).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
|
// Remove existing parent relationships
|
||||||
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
|
|
||||||
db.prepare('DELETE FROM parents WHERE dog_id = ?').run(req.params.id);
|
db.prepare('DELETE FROM parents WHERE dog_id = ?').run(req.params.id);
|
||||||
|
console.log(` ✓ Old parent relationships removed`);
|
||||||
|
|
||||||
if (sire_id) {
|
// Add new sire relationship if provided
|
||||||
db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, "sire")').run(req.params.id, sire_id);
|
if (sire_id && sire_id !== '' && sire_id !== null) {
|
||||||
}
|
console.log(` Adding sire relationship: dog ${req.params.id} -> sire ${sire_id}`);
|
||||||
if (dam_id) {
|
db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').
|
||||||
db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, "dam")').run(req.params.id, dam_id);
|
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(`
|
const dog = db.prepare(`
|
||||||
SELECT id, name, registration_number, breed, sex, birth_date,
|
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
|
created_at, updated_at
|
||||||
FROM dogs
|
FROM dogs
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).get(req.params.id);
|
`).get(req.params.id);
|
||||||
dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : [];
|
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);
|
res.json(dog);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error updating dog:', error);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -269,8 +244,10 @@ router.delete('/:id', (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
db.prepare('UPDATE dogs SET is_active = 0 WHERE id = ?').run(req.params.id);
|
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' });
|
res.json({ message: 'Dog deleted successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error deleting dog:', error);
|
||||||
res.status(500).json({ error: error.message });
|
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 });
|
res.json({ url: `/uploads/${req.file.filename}`, photos: photoUrls });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error uploading photo:', error);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -327,6 +305,7 @@ router.delete('/:id/photos/:photoIndex', (req, res) => {
|
|||||||
|
|
||||||
res.json({ photos: photoUrls });
|
res.json({ photos: photoUrls });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error deleting photo:', error);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user