From e3ede0033e265c246fd08ebd369eb97513fdc2d9 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Wed, 10 Dec 2025 19:42:03 +0100 Subject: [PATCH] Fix UNIQUE constraint: Use (service_name, version, username) instead of service_fqn - Change UNIQUE constraint to composite key on separate columns - Move upsert logic into D1Storage.createService() for atomic operation - Delete existing service and its offers before inserting new one - Remove redundant delete logic from app.ts endpoint - Fixes 'UNIQUE constraint failed: services.service_fqn' error when republishing --- migrations/fresh_schema.sql | 2 +- src/app.ts | 6 +----- src/storage/d1.ts | 21 ++++++++++++++++++++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/migrations/fresh_schema.sql b/migrations/fresh_schema.sql index b44e91a..b968e57 100644 --- a/migrations/fresh_schema.sql +++ b/migrations/fresh_schema.sql @@ -68,7 +68,7 @@ CREATE TABLE services ( created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE, - UNIQUE(service_fqn) + UNIQUE(service_name, version, username) ); CREATE INDEX idx_services_fqn ON services(service_fqn); diff --git a/src/app.ts b/src/app.ts index 40010b0..1550d65 100644 --- a/src/app.ts +++ b/src/app.ts @@ -337,11 +337,7 @@ export function createApp(storage: Storage, config: Config) { return c.json({ error: 'Invalid signature for username' }, 403); } - // Delete existing service if one exists (upsert behavior) - const existingService = await storage.getServiceByFqn(serviceFqn); - if (existingService) { - await storage.deleteService(existingService.id, username); - } + // Note: createService handles upsert behavior (deletes existing service if it exists) // Validate all offers for (const offer of offers) { diff --git a/src/storage/d1.ts b/src/storage/d1.ts index 192f760..d39f576 100644 --- a/src/storage/d1.ts +++ b/src/storage/d1.ts @@ -399,7 +399,26 @@ export class D1Storage implements Storage { const { serviceName, version, username } = parsed; - // Insert service with extracted fields + // Delete existing service with same (service_name, version, username) and its related offers (upsert behavior) + // First get the existing service + const existingService = await this.db.prepare(` + SELECT id FROM services + WHERE service_name = ? AND version = ? AND username = ? + `).bind(serviceName, version, username).first(); + + if (existingService) { + // Delete related offers first (no FK cascade from offers to services) + await this.db.prepare(` + DELETE FROM offers WHERE service_id = ? + `).bind(existingService.id).run(); + + // Delete the service + await this.db.prepare(` + DELETE FROM services WHERE id = ? + `).bind(existingService.id).run(); + } + + // Insert new service with extracted fields await this.db.prepare(` INSERT INTO services (id, service_fqn, service_name, version, username, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)