1+ /**
2+ * Event routes module
3+ * Handles all HTTP endpoints related to events, including:
4+ * - Retrieving event lists with filtering capabilities
5+ * - Getting individual event details with images
6+ * - Creating, updating, and deleting events (admin/organizer only)
7+ * - Managing event images (admin/organizer only)
8+ */
19import { FastifyInstance } from "fastify" ;
210import { z } from "zod" ;
311import { query } from "../db.js" ;
412import { cuid , iso } from "../utils.js" ;
513import { requireOrganizerOrAdmin } from "../auth.js" ;
614
15+ /**
16+ * Registers all event-related routes with the Fastify app instance
17+ * @param app - Fastify application instance
18+ */
719export default async function eventRoutes ( app : FastifyInstance ) {
20+ /**
21+ * Base schema for event creation and updates
22+ * Validates common fields required for event operations
23+ */
824 const baseSchema = z . object ( {
925 title : z . string ( ) . min ( 1 ) ,
1026 summary : z . string ( ) . optional ( ) . nullable ( ) ,
@@ -14,10 +30,22 @@ export default async function eventRoutes(app: FastifyInstance) {
1430 categoriesCsv : z . string ( ) . optional ( ) . default ( "" )
1531 } ) ;
1632
33+ /**
34+ * GET /events
35+ * Retrieves a list of events with optional filtering
36+ * Query parameters:
37+ * - from: Filter events starting from this date (ISO string)
38+ * - to: Filter events ending before this date (ISO string)
39+ * - category: Filter events by category name
40+ * @returns Array of event objects with cover image URL
41+ */
1742 app . get ( "/events" , async ( req ) => {
43+ // Extract and parse query parameters for filtering
1844 const q = ( req . query || { } ) as Record < string , string | undefined > ;
1945 const where : string [ ] = [ ] ;
2046 const params : any [ ] = [ ] ;
47+
48+ // Build WHERE clause dynamically based on query parameters
2149 if ( q . from ) {
2250 params . push ( q . from ) ;
2351 where . push ( `starts_at >= $${ params . length } ` ) ;
@@ -28,9 +56,12 @@ export default async function eventRoutes(app: FastifyInstance) {
2856 }
2957 if ( q . category ) {
3058 params . push ( q . category ) ;
59+ // Use PostgreSQL ANY() operator to check if category exists in categories array
3160 where . push ( `$${ params . length } = ANY(categories)` ) ;
3261 }
3362 const whereSql = where . length ? `WHERE ${ where . join ( " AND " ) } ` : "" ;
63+
64+ // Fetch events with their cover image (first image ordered by ord)
3465 const { rows } = await query (
3566 `SELECT e.*,
3667 (SELECT url FROM event_images i WHERE i.event_id=e.id ORDER BY ord ASC LIMIT 1) as cover_url
@@ -39,20 +70,31 @@ export default async function eventRoutes(app: FastifyInstance) {
3970 ORDER BY starts_at ASC` ,
4071 params
4172 ) ;
73+
74+ // Transform database rows to API response format
4275 return rows . map ( ( r : any ) => ( {
4376 id : r . id ,
4477 title : r . title ,
4578 summary : r . summary ,
46- startsAt : iso ( r . starts_at ) ,
79+ startsAt : iso ( r . starts_at ) , // Convert PostgreSQL timestamp to ISO string
4780 endsAt : iso ( r . ends_at ) ,
4881 categories : r . categories || [ ] ,
4982 coverUrl : r . cover_url || undefined ,
5083 venue : { name : r . venue_name }
5184 } ) ) ;
5285 } ) ;
5386
87+ /**
88+ * GET /events/:id
89+ * Retrieves detailed information about a specific event
90+ * Includes all event images ordered by their display order
91+ * @param id - Event ID from URL parameter
92+ * @returns Event object with all images, or 404 if not found
93+ */
5494 app . get < { Params : { id : string } } > ( "/events/:id" , async ( req , reply ) => {
5595 const { id } = req . params ;
96+
97+ // Fetch event with cover image (first image)
5698 const { rows } = await query (
5799 `SELECT e.* , e.cover_url FROM (
58100 SELECT e.*, (SELECT url FROM event_images i WHERE i.event_id=e.id ORDER BY ord ASC LIMIT 1) as cover_url FROM events e
@@ -64,7 +106,10 @@ export default async function eventRoutes(app: FastifyInstance) {
64106 reply . code ( 404 ) . send ( { error : "Not found" } ) ;
65107 return ;
66108 }
109+
110+ // Fetch all images for this event, ordered by display order
67111 const { rows : img } = await query ( `SELECT * FROM event_images WHERE event_id=$1 ORDER BY ord ASC` , [ id ] ) ;
112+
68113 return {
69114 id : r . id ,
70115 title : r . title ,
@@ -77,35 +122,61 @@ export default async function eventRoutes(app: FastifyInstance) {
77122 } ;
78123 } ) ;
79124
125+ /**
126+ * POST /admin/events
127+ * Creates a new event (admin/organizer only)
128+ * Requires authentication with organizer or admin role
129+ * @param body - Event data validated against baseSchema
130+ * @returns Object with the created event ID
131+ */
80132 app . post ( "/admin/events" , { preHandler : requireOrganizerOrAdmin } , async ( req ) => {
81133 const body = baseSchema . parse ( req . body ) ;
82- const id = cuid ( ) ;
134+ const id = cuid ( ) ; // Generate unique ID for the event
135+
136+ // Parse comma-separated categories into array, trimming whitespace
83137 const categories = body . categoriesCsv ? body . categoriesCsv . split ( "," ) . map ( ( s ) => s . trim ( ) ) . filter ( Boolean ) : [ ] ;
84138 const venueName = body . venueName || '' ;
139+
140+ // Create searchable text by combining all searchable fields (for full-text search)
85141 const searchable = [ body . title , body . summary ?? "" , venueName , categories . join ( " " ) ] . join ( " " ) . toLowerCase ( ) ;
142+
86143 await query (
87144 `INSERT INTO events (id,title,summary,starts_at,ends_at,venue_name,categories,searchable) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)` ,
88145 [ id , body . title , body . summary ?? null , body . startsAt , body . endsAt , venueName , categories , searchable ]
89146 ) ;
90147 return { id } ;
91148 } ) ;
92149
150+ /**
151+ * POST /admin/events/:id/images
152+ * Adds an image to an event (admin/organizer only)
153+ * Images are automatically ordered based on existing images
154+ * @param id - Event ID from URL parameter
155+ * @param body - Image data (url, optional width/height)
156+ * @returns Object with image ID, URL, and display order
157+ */
93158 app . post < { Params : { id : string } } > ( "/admin/events/:id/images" , { preHandler : requireOrganizerOrAdmin } , async ( req , reply ) => {
94159 const { id : eventId } = req . params ;
160+
161+ // Schema for image validation
95162 const imageSchema = z . object ( {
96163 url : z . string ( ) . min ( 1 ) ,
97164 width : z . number ( ) . int ( ) . positive ( ) . optional ( ) ,
98165 height : z . number ( ) . int ( ) . positive ( ) . optional ( )
99166 } ) ;
100167 const body = imageSchema . parse ( req . body ) ;
101168
169+ // Verify that the event exists
102170 const { rows : existing } = await query ( "SELECT id FROM events WHERE id=$1" , [ eventId ] ) ;
103171 if ( ! existing . length ) {
104172 reply . code ( 404 ) . send ( { error : "Event not found" } ) ;
105173 return ;
106174 }
107175
108176 const imageId = cuid ( ) ;
177+
178+ // Get the maximum order value for existing images, defaulting to -1 if none exist
179+ // This ensures the new image is appended to the end
109180 const { rows : ordRows } = await query < { max : number } > ( "SELECT COALESCE(MAX(ord), -1) as max FROM event_images WHERE event_id=$1" , [ eventId ] ) ;
110181 const nextOrd = ( ordRows [ 0 ] ?. max ?? - 1 ) + 1 ;
111182
@@ -117,19 +188,39 @@ export default async function eventRoutes(app: FastifyInstance) {
117188 return { id : imageId , url : body . url , ord : nextOrd } ;
118189 } ) ;
119190
191+ /**
192+ * PUT /admin/events/:id
193+ * Updates an existing event (admin/organizer only)
194+ * Updates the updated_at timestamp automatically
195+ * @param id - Event ID from URL parameter
196+ * @param body - Event data validated against baseSchema
197+ * @returns Object with the updated event ID
198+ */
120199 app . put < { Params : { id : string } } > ( "/admin/events/:id" , { preHandler : requireOrganizerOrAdmin } , async ( req ) => {
121200 const body = baseSchema . parse ( req . body ) ;
122201 const { id } = req . params ;
202+
203+ // Parse comma-separated categories into array, trimming whitespace
123204 const categories = body . categoriesCsv ? body . categoriesCsv . split ( "," ) . map ( ( s ) => s . trim ( ) ) . filter ( Boolean ) : [ ] ;
124205 const venueName = body . venueName || '' ;
206+
207+ // Update searchable text by combining all searchable fields
125208 const searchable = [ body . title , body . summary ?? "" , venueName , categories . join ( " " ) ] . join ( " " ) . toLowerCase ( ) ;
209+
126210 await query (
127211 `UPDATE events SET title=$2,summary=$3,starts_at=$4,ends_at=$5,venue_name=$6,categories=$7,searchable=$8,updated_at=now() WHERE id=$1` ,
128212 [ id , body . title , body . summary ?? null , body . startsAt , body . endsAt , venueName , categories , searchable ]
129213 ) ;
130214 return { id } ;
131215 } ) ;
132216
217+ /**
218+ * DELETE /admin/events/:id
219+ * Deletes an event (admin/organizer only)
220+ * Cascades to delete associated images and other related data
221+ * @param id - Event ID from URL parameter
222+ * @returns Object with the deleted event ID
223+ */
133224 app . delete < { Params : { id : string } } > ( "/admin/events/:id" , { preHandler : requireOrganizerOrAdmin } , async ( req ) => {
134225 const { id } = req . params ;
135226 await query ( `DELETE FROM events WHERE id=$1` , [ id ] ) ;
0 commit comments