task(E6-backend): guests CRUD - schema, adapter, routes
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user