Skip to content
115 changes: 96 additions & 19 deletions backend/src/controllers/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type MovieResult = {
localRating: string | null;
languages: any;
numRatings: string | null;
imageUrl: string | null;
source: "local" | "tmdb";
};

Expand Down Expand Up @@ -38,6 +39,7 @@ async function searchTMDB(query: string): Promise<any[]> {
title: movie.title,
overview: movie.overview,
vote_average: movie.vote_average,
poster_path: movie.poster_path,
spoken_languages: [],
}));
}
Expand Down Expand Up @@ -104,6 +106,7 @@ export const searchMovies = async (req: Request, res: Response) => {
localRating: true,
languages: true,
numRatings: true,
imageUrl: true,
},
});

Expand Down Expand Up @@ -161,6 +164,7 @@ export const searchMovies = async (req: Request, res: Response) => {
localRating: saved.localRating,
languages: saved.languages,
numRatings: saved.numRatings,
imageUrl: saved.imageUrl,
source: "tmdb" as const,
});
} catch (saveErr) {
Expand Down Expand Up @@ -240,6 +244,16 @@ export const searchUsers = async (req: Request, res: Response) => {
OR: orClauses,
},
take: limitNum,

select: {
userId: true,
username: true,
favoriteGenres: true,
secondaryLanguage: true,
favoriteMovies: true,
createdAt: true,
profilePicture: true,
},
});

const toStrings = (val?: string[] | null) =>
Expand Down Expand Up @@ -326,7 +340,7 @@ export const searchReviews = async (req: Request, res: Response) => {
where: whereClause,
take: limitNum,
orderBy: {
votes: "desc" // sorting by most votes to least, essentially most relevant
date: "desc" // sorting by most votes to least, essentially most relevant
},
include: {
UserProfile: {
Expand Down Expand Up @@ -364,7 +378,6 @@ export const searchReviews = async (req: Request, res: Response) => {
export const searchPosts = async (req: Request, res: Response) => {
const { q, type, limit = "10" } = req.query;

// Validate query parameter
if (!q || typeof q !== "string") {
return res.status(400).json({ message: "Query parameter 'q' is required" });
}
Expand All @@ -378,15 +391,13 @@ export const searchPosts = async (req: Request, res: Response) => {
}

try {
// Build where clause dynamically
const whereClause: any = {
content: {
contains: q,
mode: "insensitive"
}
};

// Add optional type filter
if (type && (type === "SHORT" || type === "LONG")) {
whereClause.type = type;
}
Expand All @@ -395,31 +406,39 @@ export const searchPosts = async (req: Request, res: Response) => {
where: whereClause,
take: limitNum,
orderBy: {
votes: "desc" // sorting by most votes to least, essentially most relevant
createdAt: "desc"
},
include: {
UserProfile: {
select: {
userId: true,
username: true,
},
},
_count: {
select: {
Comment: true
}
}
},
UserProfile: {
select: {
userId: true,
username: true,
},
},
movie: true, // Just include all movie fields
_count: {
select: {
Comment: true
}
}
},
});

// Serialize the response - convert all BigInts and handle _count
const serializedPosts = JSON.parse(
JSON.stringify(posts, (key, value) =>
typeof value === 'bigint' ? Number(value) : value
)
);

return res.json({
type: "posts",
query: q,
count: posts.length,
count: serializedPosts.length,
filters: {
postType: type || "any",
},
results: posts,
results: serializedPosts,
});
} catch (error) {
console.error("searchPosts error:", error);
Expand All @@ -429,3 +448,61 @@ export const searchPosts = async (req: Request, res: Response) => {
});
}
};

/**
* Search events by title or description
* GET /search/events?q={query}&limit=10
*/
export const searchEvents = async (req: Request, res: Response) => {
const { q, limit = "10" } = req.query;

if (!q || typeof q !== "string") {
return res.status(400).json({ message: "Query parameter 'q' is required" });
}

const limitNum = parseInt(limit as string);

if (limitNum > 50) {
return res.status(400).json({
message: "limit cannot exceed 50"
});
}

try {
const events = await prisma.local_event.findMany({
where: {
OR: [
{
title: {
contains: q,
mode: "insensitive"
}
},
{
description: {
contains: q,
mode: "insensitive"
}
}
]
},
take: limitNum,
orderBy: {
time: "asc" // Show upcoming events first
},
});

return res.json({
type: "events",
query: q,
count: events.length,
results: events,
});
} catch (error) {
console.error("searchEvents error:", error);
return res.status(500).json({
message: "Failed to search events",
error: error instanceof Error ? error.message : String(error),
});
}
};
3 changes: 2 additions & 1 deletion backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { getComment, createComment, updateComment, deleteComment, getMovieCommen
import { createRating, getRatings, getRatingById, deleteRating, updateRating,getMovieRatings } from "../controllers/ratings";
import { getAllMovies, getMoviesAfterYear, getRandomTenMovies } from "../controllers/movies";
import { createPost, getPostById, getPosts, updatePost, deletePost, getPostReposts, toggleReaction, getPostReactions } from "../controllers/post.js";
import { searchMovies, searchUsers, searchReviews, searchPosts } from "../controllers/search.js";
import { searchMovies, searchUsers, searchReviews, searchPosts, searchEvents } from "../controllers/search.js";
import { getHomeFeed } from "../controllers/feed";
import { getMovieSummaryHandler } from "../controllers/movies.js";
import { translateText, getSupportedLanguages } from "../controllers/translate";
Expand Down Expand Up @@ -124,5 +124,6 @@ router.get("/api/search/movies", searchMovies)
router.get("/api/search/users", searchUsers)
router.get("/api/search/reviews", searchReviews)
router.get("/api/search/posts", searchPosts)
router.get("/api/search/events", searchEvents)

export default router;
18 changes: 14 additions & 4 deletions backend/src/tests/api/userFollows.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,18 @@ describe('Follow Controller', () => {
const dbError = new Error('Database error');
(prisma.userFollow.findMany as jest.Mock).mockRejectedValue(dbError);

await getFollowers(mockReq as Request, mockRes as Response);
// Suppress console.error for this test
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();

await getFollowing(mockReq as Request, mockRes as Response);

expect(statusMock).toHaveBeenCalledWith(500);
expect(jsonMock).toHaveBeenCalledWith({
message: 'Failed to get followers',
message: 'Failed to get following',
});

// Restore console.error
consoleErrorSpy.mockRestore();
});

it('should return empty array when user has no followers', async () => {
Expand All @@ -202,15 +208,19 @@ describe('Follow Controller', () => {
});

it('should return 500 on database error', async () => {
const dbError = new Error('Database error');
(prisma.userFollow.findMany as jest.Mock).mockRejectedValue(dbError);
const dbError = new Error('Database error');
(prisma.userFollow.findMany as jest.Mock).mockRejectedValue(dbError);

const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();

await getFollowing(mockReq as Request, mockRes as Response);

expect(statusMock).toHaveBeenCalledWith(500);
expect(jsonMock).toHaveBeenCalledWith({
message: 'Failed to get following',
});

consoleErrorSpy.mockRestore();
});

it('should return empty array when user is not following anyone', async () => {
Expand Down
Loading