From 2988a70d8f724647eedb0fff47e6a5ee23d72a64 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 13 Aug 2026 21:32:32 +0200 Subject: [PATCH] Add search to the admin guest book endpoint At scale (many submissions after a concert), paging through the guest book 20 entries at a time with no way to find a specific person is impractical. Add an optional ?search= query param that filters entries whose name or message contains the term (case-insensitive), with LIKE wildcards escaped so a literal % or _ in a search term can't be misinterpreted as a pattern. Co-Authored-By: Claude Sonnet 5 --- .../feedback/admin/reports.admin.router.ts | 8 ++++++- .../feedback/admin/reports.admin.service.ts | 24 +++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/models/feedback/admin/reports.admin.router.ts b/src/models/feedback/admin/reports.admin.router.ts index 2685935..a048405 100644 --- a/src/models/feedback/admin/reports.admin.router.ts +++ b/src/models/feedback/admin/reports.admin.router.ts @@ -82,6 +82,11 @@ reportsAdminRouter.get('/:eventId/report', async (req: Request, res: Response) = * name: pageSize * schema: * type: integer + * - in: query + * name: search + * description: Filters entries whose name or message contains this text (case-insensitive). + * schema: + * type: string * responses: * 200: * description: Success @@ -92,7 +97,8 @@ reportsAdminRouter.get('/:eventId/guestbook', async (req: Request, res: Response try { const page = Math.max(1, Number(req.query.page) || 1); const pageSize = Math.min(200, Math.max(1, Number(req.query.pageSize) || 50)); - const result = await ReportsAdminService.getGuestBookEntries(Number(req.params.eventId), page, pageSize); + const search = typeof req.query.search === 'string' ? req.query.search : undefined; + const result = await ReportsAdminService.getGuestBookEntries(Number(req.params.eventId), page, pageSize, search); res.status(200).send(result); } catch (e: any) { sendServerError(res, e); diff --git a/src/models/feedback/admin/reports.admin.service.ts b/src/models/feedback/admin/reports.admin.service.ts index 4042561..32f92da 100644 --- a/src/models/feedback/admin/reports.admin.service.ts +++ b/src/models/feedback/admin/reports.admin.service.ts @@ -166,13 +166,29 @@ export interface GuestBookEntry { message: string | null; } -export const getGuestBookEntries = async (eventId: number, page: number, pageSize: number): Promise<{entries: GuestBookEntry[]; total: number}> => { +// Escapes LIKE wildcards (% and _) so a search term is matched literally, +// not interpreted as a pattern - a search for "50%" must not match everything. +const escapeLikeTerm = (term: string) => term.replace(/[\\%_]/g, (c) => `\\${c}`); + +export const getGuestBookEntries = async ( + eventId: number, + page: number, + pageSize: number, + search?: string +): Promise<{entries: GuestBookEntry[]; total: number}> => { let conn = await NachklangFeedbackDB.getConnection(); try { - const totalRows = await conn.query('SELECT COUNT(*) as cnt FROM guest_book_entries WHERE event_id = ?', [eventId]); + const trimmedSearch = search?.trim(); + const whereClause = trimmedSearch + ? 'WHERE event_id = ? AND (display_name LIKE ? ESCAPE \'\\\\\' OR message LIKE ? ESCAPE \'\\\\\')' + : 'WHERE event_id = ?'; + const likeParam = trimmedSearch ? `%${escapeLikeTerm(trimmedSearch)}%` : undefined; + const whereParams = trimmedSearch ? [eventId, likeParam, likeParam] : [eventId]; + + const totalRows = await conn.query(`SELECT COUNT(*) as cnt FROM guest_book_entries ${whereClause}`, whereParams); const rows = await conn.query( - 'SELECT entry_id, submission_id, created_at, display_name, message FROM guest_book_entries WHERE event_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?', - [eventId, pageSize, (page - 1) * pageSize] + `SELECT entry_id, submission_id, created_at, display_name, message FROM guest_book_entries ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`, + [...whereParams, pageSize, (page - 1) * pageSize] ); return { total: Number(totalRows[0].cnt),