task(E6-backend): guests CRUD - schema, adapter, routes

This commit is contained in:
Agent Zero
2026-08-28 07:25:48 +02:00
parent 6a214c2e9c
commit e99cf597f8
5 changed files with 132 additions and 0 deletions
+19
View File
@@ -213,4 +213,23 @@ export interface DatabaseInterface {
createGlobalBlock(data: Partial<DBGlobalBlock>): DBGlobalBlock;
updateGlobalBlock(id: string, data: Partial<DBGlobalBlock>): DBGlobalBlock | null;
deleteGlobalBlock(id: string): boolean;
// Guests (Task E6)
listGuests(drawingId: string): DBGuest[];
getGuest(id: string): DBGuest | null;
createGuest(data: Partial<DBGuest>): DBGuest;
updateGuest(id: string, data: Partial<DBGuest>): 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;
}
+34
View File
@@ -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>): 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>): 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;
}
}
+14
View File
@@ -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);
+63
View File
@@ -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();
});
}
+2
View File
@@ -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