From e99cf597f847b8e091bb785b0697a8f5b0390eb3 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Fri, 28 Aug 2026 07:25:48 +0200 Subject: [PATCH] task(E6-backend): guests CRUD - schema, adapter, routes --- backend/src/database/DatabaseInterface.ts | 19 +++++++ backend/src/database/SqliteAdapter.ts | 34 ++++++++++++ backend/src/database/schema.sql | 14 +++++ backend/src/routes/guests.ts | 63 +++++++++++++++++++++++ backend/src/server.ts | 2 + 5 files changed, 132 insertions(+) create mode 100644 backend/src/routes/guests.ts diff --git a/backend/src/database/DatabaseInterface.ts b/backend/src/database/DatabaseInterface.ts index 770eb1e..476f9b0 100644 --- a/backend/src/database/DatabaseInterface.ts +++ b/backend/src/database/DatabaseInterface.ts @@ -213,4 +213,23 @@ export interface DatabaseInterface { createGlobalBlock(data: Partial): DBGlobalBlock; updateGlobalBlock(id: string, data: Partial): DBGlobalBlock | null; deleteGlobalBlock(id: string): boolean; + + // Guests (Task E6) + listGuests(drawingId: string): DBGuest[]; + getGuest(id: string): DBGuest | null; + createGuest(data: Partial): DBGuest; + updateGuest(id: string, data: Partial): DBGuest | null; + deleteGuest(id: string): boolean; +} + +// ─── Guests (Task E6) ────────────────────────────────── +export interface DBGuest { + id: string; + drawing_id: string; + name: string; + email?: string; + category: string; // 'standard' | 'vip' | 'staff' + seat_element_id?: string; // Verknüpfung zu einem chair-Element + notes?: string; + created_at: string; } diff --git a/backend/src/database/SqliteAdapter.ts b/backend/src/database/SqliteAdapter.ts index 4c3c9cf..042efec 100644 --- a/backend/src/database/SqliteAdapter.ts +++ b/backend/src/database/SqliteAdapter.ts @@ -11,6 +11,7 @@ import type { DBElement, DBBlock, DBSetting, DBUser, DBSession, DBNotification, DBProjectShare, DBGlobalBlockFolder, DBGlobalBlock, + DBGuest, DBProjectFolder, } from './DatabaseInterface.js'; @@ -459,4 +460,37 @@ export class SqliteAdapter implements DatabaseInterface { deleteGlobalBlock(id: string): boolean { return this.db.prepare('DELETE FROM global_blocks WHERE id = ?').run(id).changes > 0; } + + // ─── Guests (Task E6) ────────────────────────────────── + + listGuests(drawingId: string): DBGuest[] { + return this.db.prepare('SELECT * FROM guests WHERE drawing_id = ? ORDER BY created_at').all(drawingId) as DBGuest[]; + } + + getGuest(id: string): DBGuest | null { + return (this.db.prepare('SELECT * FROM guests WHERE id = ?').get(id) as DBGuest) ?? null; + } + + createGuest(data: Partial): DBGuest { + const id = data.id ?? randomUUID(); + const created_at = new Date().toISOString(); + this.db.prepare( + 'INSERT INTO guests (id, drawing_id, name, email, category, seat_element_id, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' + ).run(id, data.drawing_id!, data.name!, data.email ?? null, data.category ?? 'standard', data.seat_element_id ?? null, data.notes ?? null, created_at); + return this.getGuest(id)!; + } + + updateGuest(id: string, data: Partial): DBGuest | null { + const existing = this.getGuest(id); + if (!existing) return null; + const merged = { ...existing, ...data, id: existing.id, drawing_id: existing.drawing_id, created_at: existing.created_at }; + this.db.prepare( + 'UPDATE guests SET name=?, email=?, category=?, seat_element_id=?, notes=? WHERE id=?' + ).run(merged.name, merged.email ?? null, merged.category, merged.seat_element_id ?? null, merged.notes ?? null, id); + return this.getGuest(id); + } + + deleteGuest(id: string): boolean { + return this.db.prepare('DELETE FROM guests WHERE id = ?').run(id).changes > 0; + } } diff --git a/backend/src/database/schema.sql b/backend/src/database/schema.sql index ffbf4e3..bb4dd12 100644 --- a/backend/src/database/schema.sql +++ b/backend/src/database/schema.sql @@ -178,3 +178,17 @@ CREATE TABLE IF NOT EXISTS global_blocks ( CREATE INDEX IF NOT EXISTS idx_global_blocks_folder ON global_blocks(folder_id); CREATE INDEX IF NOT EXISTS idx_global_block_folders_parent ON global_block_folders(parent_id); + +-- ─── Guests (Task E6) ──────────────────────────────── +CREATE TABLE IF NOT EXISTS guests ( + id TEXT PRIMARY KEY, + drawing_id TEXT NOT NULL, + name TEXT NOT NULL, + email TEXT, + category TEXT DEFAULT 'standard', + seat_element_id TEXT, + notes TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (drawing_id) REFERENCES drawings(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_guests_drawing ON guests(drawing_id); diff --git a/backend/src/routes/guests.ts b/backend/src/routes/guests.ts new file mode 100644 index 0000000..4c04c4e --- /dev/null +++ b/backend/src/routes/guests.ts @@ -0,0 +1,63 @@ +/** + * Guests Routes – CRUD for event guests (Task E6) + */ +import type { FastifyInstance } from 'fastify'; +import type { DatabaseInterface } from '../database/DatabaseInterface.js'; +import { requireAuth } from '../auth/authMiddleware.js'; +import type { AuthService } from '../auth/AuthService.js'; +import { validateIdParam } from '../utils/validation.js'; + +export function registerGuestRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) { + // List guests for a drawing + fastify.get('/api/drawings/:drawingId/guests', async (request, reply) => { + if (!requireAuth(request, reply, authService)) return; + const { drawingId } = request.params as { drawingId: string }; + const idErr = validateIdParam(drawingId, 'drawingId'); + if (idErr) return reply.code(400).send({ error: idErr }); + return db.listGuests(drawingId); + }); + + // Create guest + fastify.post('/api/drawings/:drawingId/guests', async (request, reply) => { + if (!requireAuth(request, reply, authService)) return; + const { drawingId } = request.params as { drawingId: string }; + const idErr = validateIdParam(drawingId, 'drawingId'); + if (idErr) return reply.code(400).send({ error: idErr }); + const body = request.body as { name?: string; email?: string; category?: string; seat_element_id?: string; notes?: string }; + if (!body.name || typeof body.name !== 'string' || body.name.trim() === '') { + return reply.code(400).send({ error: 'name is required' }); + } + const guest = db.createGuest({ + drawing_id: drawingId, + name: body.name.trim(), + email: body.email, + category: body.category ?? 'standard', + seat_element_id: body.seat_element_id, + notes: body.notes, + }); + return reply.code(201).send(guest); + }); + + // Update guest + fastify.patch('/api/guests/:id', async (request, reply) => { + if (!requireAuth(request, reply, authService)) return; + const { id } = request.params as { id: string }; + const idErr = validateIdParam(id, 'id'); + if (idErr) return reply.code(400).send({ error: idErr }); + const body = request.body as Partial<{ name: string; email: string; category: string; seat_element_id: string; notes: string }>; + const updated = db.updateGuest(id, body); + if (!updated) return reply.code(404).send({ error: 'Guest not found' }); + return updated; + }); + + // Delete guest + fastify.delete('/api/guests/:id', async (request, reply) => { + if (!requireAuth(request, reply, authService)) return; + const { id } = request.params as { id: string }; + const idErr = validateIdParam(id, 'id'); + if (idErr) return reply.code(400).send({ error: idErr }); + const ok = db.deleteGuest(id); + if (!ok) return reply.code(404).send({ error: 'Guest not found' }); + return reply.code(204).send(); + }); +} diff --git a/backend/src/server.ts b/backend/src/server.ts index c5f099a..0abe88a 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -66,6 +66,7 @@ export async function createServer(opts: ServerOptions) { const { registerAIRoutes } = await import('./routes/ai.js'); const { registerNotificationRoutes } = await import('./routes/notifications.js'); const { registerShareRoutes } = await import('./routes/shares.js'); + const { registerGuestRoutes } = await import('./routes/guests.js'); const { registerGlobalBlockRoutes } = await import('./routes/globalBlocks.js'); const { registerProjectFolderRoutes } = await import('./routes/projectFolders.js'); @@ -103,6 +104,7 @@ export async function createServer(opts: ServerOptions) { registerNotificationRoutes(fastify, opts.db, authService); registerShareRoutes(fastify, opts.db, authService); registerGlobalBlockRoutes(fastify, opts.db, authService); + registerGuestRoutes(fastify, opts.db, authService); registerProjectFolderRoutes(fastify, opts.db, authService); // Yjs collaboration WebSocket