From eb48d97ee34dfb4fc16efd79ec3174534968613a Mon Sep 17 00:00:00 2001 From: "groombook-ci[bot]" Date: Sat, 28 Mar 2026 11:09:51 +0000 Subject: [PATCH] fix(db): make seed script idempotent using upserts Convert raw inserts to upserts (ON CONFLICT DO UPDATE) for: - staff: upsert on email (unique constraint) - services: upsert on id (deterministic UUID) - clients: upsert on email (unique constraint) - pets: upsert on id (deterministic UUID) This fixes the duplicate key violation when re-running the seed script against an existing database (e.g., after schema migrations or test restarts). Note: appointments, invoices, visit logs still use raw inserts and would need DELETE-before-insert for full idempotency. Those tables use deterministic UUIDs so a second seed run without prior DELETE would still fail. This is scoped to the immediate staff email constraint violation reported. Co-Authored-By: Paperclip --- packages/db/src/seed.ts | 74 +++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index f7936be..dc6ddfa 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -407,14 +407,19 @@ async function seed() { const allStaff = [...managerStaff, ...receptionistStaff, ...groomers, ...bathers]; for (const s of allStaff) { - await db.insert(schema.staff).values({ - id: s.id, - name: s.name, - email: s.email, - role: s.role, - isSuperUser: s.isSuperUser, - active: true, - }); + await db.insert(schema.staff) + .values({ + id: s.id, + name: s.name, + email: s.email, + role: s.role, + isSuperUser: s.isSuperUser, + active: true, + }) + .onConflictDoUpdate({ + target: schema.staff.email, + set: { name: s.name, role: s.role, isSuperUser: s.isSuperUser, active: true }, + }); } console.log(`✓ Created ${allStaff.length} staff (1 manager, 1 receptionist, 3 groomers, 3 bathers)`); @@ -423,14 +428,19 @@ async function seed() { for (const s of servicesDef) { const id = uuid(); serviceIds.push(id); - await db.insert(schema.services).values({ - id, - name: s.name, - description: s.desc, - basePriceCents: s.price, - durationMinutes: s.dur, - active: true, - }); + await db.insert(schema.services) + .values({ + id, + name: s.name, + description: s.desc, + basePriceCents: s.price, + durationMinutes: s.dur, + active: true, + }) + .onConflictDoUpdate({ + target: schema.services.id, + set: { name: s.name, description: s.desc, basePriceCents: s.price, durationMinutes: s.dur, active: true }, + }); } console.log(`✓ Created ${servicesDef.length} services`); @@ -502,8 +512,36 @@ async function seed() { } } - await db.insert(schema.clients).values(clientBatch); - await db.insert(schema.pets).values(petBatch); + for (const client of clientBatch) { + await db.insert(schema.clients) + .values(client) + .onConflictDoUpdate({ + target: schema.clients.email, + set: { name: client.name, phone: client.phone, address: client.address, notes: client.notes, emailOptOut: client.emailOptOut }, + }); + } + + for (const pet of petBatch) { + await db.insert(schema.pets) + .values(pet) + .onConflictDoUpdate({ + target: schema.pets.id, + set: { + clientId: pet.clientId, + name: pet.name, + species: pet.species, + breed: pet.breed, + weightKg: pet.weightKg, + dateOfBirth: pet.dateOfBirth, + healthAlerts: pet.healthAlerts, + groomingNotes: pet.groomingNotes, + cutStyle: pet.cutStyle, + shampooPreference: pet.shampooPreference, + specialCareNotes: pet.specialCareNotes, + customFields: pet.customFields, + }, + }); + } } console.log(`✓ Created 500 clients with ${petRecords.length} pets`); -- 2.52.0