From 346647e586fd08269a6cd99042a08c515e4f1b7b Mon Sep 17 00:00:00 2001 From: Anurag Date: Thu, 25 Jun 2026 22:40:50 +0530 Subject: [PATCH 1/2] fix(events): eliminate N+1 queries in event recommendations --- lib/actions/event.actions.ts | 109 +++++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 25 deletions(-) diff --git a/lib/actions/event.actions.ts b/lib/actions/event.actions.ts index 51343a9..7f601f4 100644 --- a/lib/actions/event.actions.ts +++ b/lib/actions/event.actions.ts @@ -5,6 +5,36 @@ const escapeRegex = (text: string) => text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, ' type SortBy = 'date_asc' | 'date_desc' | 'name_asc' | 'name_desc' | 'popularity'; +/** + * Helper function to add booking count to aggregation pipeline. + * Prevents N+1 queries by joining bookings data in a single aggregation stage. + * Returns the aggregation pipeline stages needed to attach booking counts. + * Note: bookingCount is calculated and included in results, NOT removed. + */ +function getBookingCountStages() { + return [ + { + $lookup: { + from: 'bookings', + localField: '_id', + foreignField: 'eventId', + as: 'bookings', + }, + }, + { + $addFields: { + bookingCount: { $size: '$bookings' }, + }, + }, + { + $project: { + bookings: 0, // Remove raw bookings array to keep response size small + // bookingCount is kept in the results for display/sorting + }, + }, + ]; +} + export interface PaginatedEvents { events: any[]; total: number; @@ -48,17 +78,8 @@ export async function getAllEvents( const matchStage = Object.keys(queryCondition).length ? [{ $match: queryCondition }] : []; const results = await Event.aggregate([ ...matchStage, - { - $lookup: { - from: 'bookings', - localField: '_id', - foreignField: 'eventId', - as: 'bookings', - }, - }, - { $addFields: { bookingCount: { $size: '$bookings' } } }, + ...getBookingCountStages(), { $sort: { bookingCount: -1, createdAt: -1 } }, - { $project: { bookings: 0, bookingCount: 0 } }, { $skip: skip }, { $limit: safeLimit }, ]); @@ -104,24 +125,52 @@ export async function getAllEvents( } } +/** + * Fetches similar events using aggregation pipeline with booking counts. + * This prevents N+1 queries by fetching booking data in a single aggregation. + * Optimized with $lookup for efficient booking count calculation. + */ export async function getSimilarEventsBySlug( slug: string, tags: string[] = [] ) { - await connectToDatabase(); + try { + await connectToDatabase(); - if (!tags.length) { + if (!tags.length) { + return []; + } + + // Use aggregation pipeline to fetch events with booking counts in a single query + const events = await Event.aggregate([ + { + $match: { + slug: { $ne: slug }, + tags: { $in: tags }, + }, + }, + ...getBookingCountStages(), + // Sort by booking count (popularity) as primary, then by date + { + $sort: { bookingCount: -1, createdAt: -1 }, + }, + { + $limit: 3, + }, + ]); + + return JSON.parse(JSON.stringify(events)); + } catch (error) { + console.error('Error fetching similar events:', error); return []; } - - const events = await Event.find({ - slug: { $ne: slug }, - tags: { $in: tags } - }).limit(3); - - return JSON.parse(JSON.stringify(events)); } +/** + * Fetches recommended events using aggregation pipeline with booking counts. + * This prevents N+1 queries by fetching booking data in a single aggregation. + * Sorts by popularity (booking count) to show trending events first. + */ export async function getRecommendedEvents(userTags: string[] = []) { try { await connectToDatabase(); @@ -130,12 +179,22 @@ export async function getRecommendedEvents(userTags: string[] = []) { return []; } - // Find up to 3 events that match the user's interested tags, sorted by newest - const recommendedEvents = await Event.find({ - tags: { $in: userTags } - }) - .sort({ createdAt: -1 }) - .limit(3); + // Use aggregation pipeline to fetch events with booking counts in a single query + const recommendedEvents = await Event.aggregate([ + { + $match: { + tags: { $in: userTags }, + }, + }, + ...getBookingCountStages(), + // Sort by booking count (popularity) first, then by creation date + { + $sort: { bookingCount: -1, createdAt: -1 }, + }, + { + $limit: 3, + }, + ]); return JSON.parse(JSON.stringify(recommendedEvents)); } catch (error) { From 2577077f39737a56cbe43fa9166977bd65c3c5aa Mon Sep 17 00:00:00 2001 From: Anurag Date: Sat, 27 Jun 2026 19:54:50 +0530 Subject: [PATCH 2/2] Fix pagination page clamping and PaginatedEvents return contract --- lib/actions/event.actions.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/actions/event.actions.ts b/lib/actions/event.actions.ts index fd5879f..a594ccb 100644 --- a/lib/actions/event.actions.ts +++ b/lib/actions/event.actions.ts @@ -70,13 +70,14 @@ export async function getAllEvents( queryCondition.tags = { $regex: new RegExp(`^${safeTag}$`, 'i') }; } - const safePage = Math.max(1, isNaN(Number(page)) ? 1 : Number(page)); + const requestedPage = Math.max(1, isNaN(Number(page)) ? 1 : Number(page)); const safeLimit = Math.min(100, Math.max(1, isNaN(Number(limit)) ? 9 : Number(limit))); - const skip = (safePage - 1) * safeLimit; // Get total count for pagination metadata const total = await Event.countDocuments(queryCondition); const totalPages = Math.max(1, Math.ceil(total / safeLimit)); + const currentPage = Math.min(requestedPage, totalPages); + const skip = (currentPage - 1) * safeLimit; // Popularity sort — use aggregation pipeline with $lookup on bookings if (filters?.sortBy === 'popularity') { @@ -92,7 +93,7 @@ export async function getAllEvents( events: JSON.parse(JSON.stringify(results)), total, totalPages, - currentPage: safePage, + currentPage, }; } @@ -121,12 +122,17 @@ export async function getAllEvents( events: JSON.parse(JSON.stringify(events)), total, totalPages, - currentPage: safePage, + currentPage, }; } catch (error) { console.error('Error fetching events:', error); - return []; + return { + events: [], + total: 0, + totalPages: 1, + currentPage: 1, + }; } }