Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 182 additions & 4 deletions lib/actions/event.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,50 @@ import Event from "@/database/event.model";

const escapeRegex = (text: string) => text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');

export async function getAllEvents(filters?: { query?: string; mode?: string; tag?: string }) {
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
Comment on lines +17 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n lib/actions/event.actions.ts | sed -n '1,60p'

Repository: niharika-mente/DevEvent_Tracker

Length of output: 2252


Optimize $lookup to calculate booking counts without materializing full documents.

Lines 17–27 currently join all related bookings into a full array before calculating the size. For events with many bookings, this creates large intermediate documents that impact memory and performance. Instead, use a $lookup pipeline to count records directly within the database.

Suggested improvement
 function getBookingCountStages() {
   return [
     {
       $lookup: {
         from: 'bookings',
-        localField: '_id',
-        foreignField: 'eventId',
-        as: 'bookings',
+        let: { eventId: '$_id' },
+        pipeline: [
+          { $match: { $expr: { $eq: ['$eventId', '$$eventId'] } } },
+          { $count: 'count' },
+        ],
+        as: 'bookingStats',
       },
     },
     {
       $addFields: {
-        bookingCount: { $size: '$bookings' },
+        bookingCount: {
+          $ifNull: [{ $arrayElemAt: ['$bookingStats.count', 0] }, 0],
+        },
       },
     },
     {
       $project: { 
-        bookings: 0,  // Remove raw bookings array to keep response size small
+        bookingStats: 0,
         // bookingCount is kept in the results for display/sorting
       },
     },
   ];
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$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
$lookup: {
from: 'bookings',
let: { eventId: '$_id' },
pipeline: [
{ $match: { $expr: { $eq: ['$eventId', '$$eventId'] } } },
{ $count: 'count' },
],
as: 'bookingStats',
},
},
{
$addFields: {
bookingCount: {
$ifNull: [{ $arrayElemAt: ['$bookingStats.count', 0] }, 0],
},
},
},
{
$project: {
bookingStats: 0, // Remove raw booking stats to keep response size small
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/event.actions.ts` around lines 17 - 31, The aggregation in
event.actions.ts currently materializes the full bookings array before computing
bookingCount, which is inefficient for large events. Update the event
aggregation around the $lookup/$addFields stages to use a pipeline-based $lookup
that counts matching bookings directly in the database, and keep the response
shape the same by exposing bookingCount without pulling full booking documents.

// bookingCount is kept in the results for display/sorting
},
},
];
}

export interface PaginatedEvents {
events: any[];
total: number;
totalPages: number;
currentPage: number;
}

export async function getAllEvents(
filters?: { query?: string; mode?: string; tag?: string; sortBy?: SortBy },
page = 1,
limit = 9
): Promise<PaginatedEvents> {
try {
await connectToDatabase();
const queryCondition: any = {};
Expand All @@ -27,11 +70,146 @@ export async function getAllEvents(filters?: { query?: string; mode?: string; ta
queryCondition.tags = { $regex: new RegExp(`^${safeTag}$`, 'i') };
}

const events = await Event.find(queryCondition).sort({ createdAt: -1 });
return JSON.parse(JSON.stringify(events));
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)));

// 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') {
const matchStage = Object.keys(queryCondition).length ? [{ $match: queryCondition }] : [];
const results = await Event.aggregate([
...matchStage,
...getBookingCountStages(),
{ $sort: { bookingCount: -1, createdAt: -1 } },
{ $skip: skip },
{ $limit: safeLimit },
]);
return {
events: JSON.parse(JSON.stringify(results)),
total,
totalPages,
currentPage,
};
}

let query = Event.find(queryCondition);

// Apply sort
const sortBy = filters?.sortBy;
if (sortBy === 'date_asc') {
query = query.sort({ date: 1 });
} else if (sortBy === 'date_desc') {
query = query.sort({ date: -1 });
} else if (sortBy === 'name_asc') {
query = query.sort({ title: 1 });
} else if (sortBy === 'name_desc') {
query = query.sort({ title: -1 });
} else if (filters?.query) {
// Relevance sort when full-text searching
query = query.select({ score: { $meta: 'textScore' } }).sort({ score: { $meta: 'textScore' } });
} else {
// Default: newest first
query = query.sort({ createdAt: -1 });
}

const events = await query.skip(skip).limit(safeLimit);
return {
events: JSON.parse(JSON.stringify(events)),
total,
totalPages,
currentPage,
};

} catch (error) {
console.error('Error fetching events:', error);
return [];
return {
events: [],
total: 0,
totalPages: 1,
currentPage: 1,
};
}
}

/**
* 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[] = []
) {
try {
await connectToDatabase();

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 [];
}
}

/**
* 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();

if (!userTags.length) {
return [];
}

// 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 },
},
Comment thread
Shikigami1606 marked this conversation as resolved.
{
$limit: 3,
},
]);

return JSON.parse(JSON.stringify(recommendedEvents));
} catch (error) {
console.error('Error fetching recommended events:', error);
return [];
}
}
Loading