Skip to content

Commit c2fbb88

Browse files
committed
feat: Refactor API routes for stats and search; optimize user and challenge queries with Promise.all; enhance database indexing for search performance
1 parent 444c0d3 commit c2fbb88

File tree

8 files changed

+94
-153
lines changed

8 files changed

+94
-153
lines changed

server/app.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ app.use('/api/leaderboard', leaderboardRoutes);
6767
app.use('/auth/admin', adminAuthRoutes);
6868
app.use('/api/admin', adminRoutes);
6969
app.use('/platforms', platformRoute);
70-
app.use('/api', statsRoutes);
70+
app.use('/api/stats', statsRoutes);
7171
app.use('/api/user', userRoutes);
72-
// app.use('/api', typeSenseRoutes);
72+
app.use('/api/search', typeSenseRoutes);
7373

7474
startStreakCronJob();
7575
appLifecycle.initialize();

server/controllers/adminControllers.js

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,18 @@ export const getUsers = async (req, res) => {
4747
const sortObject = {};
4848
sortObject[sortBy] = sortDirection === 'asc' ? 1 : -1;
4949

50-
const totalUsers = await User.countDocuments();
50+
const [totalUsers, users, uniqueColleges, uniqueBranches] = await Promise.all([
51+
User.countDocuments(searchQuery),
52+
User.find(searchQuery)
53+
.select("-password -resetPasswordToken -resetPasswordExpires -googleId -githubId -otp -otpExpires -gfg -leetCode -codechef -codeforces -otherLinks -solveChallenges")
54+
.sort(sortObject)
55+
.skip(skip)
56+
.limit(pageLimit)
57+
.lean(),
58+
User.distinct('collegeName', { collegeName: { $exists: true, $ne: "" } }),
59+
User.distinct('branch', { branch: { $exists: true, $ne: "" } })
60+
]);
5161

52-
const users = await User.find(searchQuery)
53-
.select("-password -resetPasswordToken -resetPasswordExpires -googleId -githubId -otp -otpExpires -gfg -leetCode -codechef -codeforces -otherLinks -solveChallenges")
54-
.sort(sortObject)
55-
.skip(skip)
56-
.limit(pageLimit);
57-
58-
const uniqueColleges = await User.distinct('collegeName', { collegeName: { $exists: true, $ne: "" } });
59-
const uniqueBranches = await User.distinct('branch', { branch: { $exists: true, $ne: "" } });
60-
61-
const totalPages = Math.ceil(totalUsers / pageLimit);
6262

6363
return res.status(200).json({
6464
success: true,
@@ -347,9 +347,10 @@ export const getChallenges = async (req, res) => {
347347
const sortObject = {}
348348
sortObject[sortBy] = sortDirection === "asc" ? 1 : -1
349349

350-
const totalChallenges = await Challenge.countDocuments(searchQuery)
351-
352-
const challenges = await Challenge.find(searchQuery).sort(sortObject).skip(skip).limit(pageLimit)
350+
const [totalChallenges, challenges] = await Promise.all([
351+
Challenge.countDocuments(searchQuery),
352+
Challenge.find(searchQuery).sort(sortObject).skip(skip).limit(pageLimit).lean()
353+
]);
353354

354355
const totalPages = Math.ceil(totalChallenges / pageLimit)
355356

server/controllers/statsControllers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const getCounts = async (req, res) => {
2222

2323
const usersCount = await User.countDocuments();
2424
const challengesCount = await Challenge.countDocuments();
25-
const collegesCount = await User.distinct("college").then(colleges => colleges.length);
25+
const collegesCount = await User.distinct("collegeName").then(colleges => colleges.length);
2626
const affiliatesCount = await User.countDocuments({ isAffiliate: true });
2727
const solvedChallenges = await Challenge.countDocuments({
2828
solvedUsers: { $exists: true, $not: { $size: 0 } }
Lines changed: 64 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,41 @@
1-
// import { client } from "../utils/typesenseClient.js";
21
import { User } from "../models/User.js";
32
import { Challenge } from "../models/Challenge.js";
43

5-
// export const test = async () => {
6-
// try {
7-
// const res = await client.health.retrieve();
8-
// console.log("Typesense is alive:", res);
9-
// } catch (err) {
10-
// console.error("Connection failed:", err);
11-
// }
12-
// };
13-
14-
// export const search = async (req, res) => {
15-
// const { q } = req.query;
16-
17-
// if (!q) return res.status(400).json({ error: "Missing search query" });
18-
19-
// try {
20-
// // Search users
21-
// const userResults = await client.collections("users").documents().search({
22-
// q,
23-
// query_by: "name,username,profilePicture,collegeName,branch,points,rank", // adjust as per your schema
24-
// per_page: 5,
25-
// });
26-
27-
// // Search challenges
28-
// const challengeResults = await client
29-
// .collections("challenges")
30-
// .documents()
31-
// .search({
32-
// q,
33-
// query_by: "title,description,category,difficulty,problemLink", // adjust as per your schema
34-
// per_page: 5,
35-
// });
36-
37-
// // Add type labels
38-
// const users = userResults.hits.map(hit => ({
39-
// ...hit.document,
40-
// type: "user",
41-
// }));
42-
43-
// const challenges = challengeResults.hits.map(hit => ({
44-
// ...hit.document,
45-
// type: "challenge",
46-
// }));
47-
48-
// // Merge & send
49-
// res.json([...users, ...challenges]);
50-
// } catch (err) {
51-
// console.error(err);
52-
// res.status(500).json({ error: "Search failed" });
53-
// }
54-
// };
55-
56-
// export const searchUser = async (req, res) => {
57-
// const { q } = req.query;
58-
59-
// if (!q) return res.status(400).json({ error: "Missing search query" });
60-
61-
// try {
62-
// // Search users
63-
// const userResults = await client.collections("users").documents().search({
64-
// q,
65-
// query_by: "name,username,profilePicture,collegeName,branch,points,rank",
66-
// });
67-
68-
// // Add type labels
69-
// const users = userResults.hits.map(hit => ({
70-
// ...hit.document,
71-
// type: "user",
72-
// }));
73-
// res.json([...users]);
74-
// } catch (err) {
75-
// console.error(err);
76-
// res.status(500).json({ error: "Search failed" });
77-
// }
78-
// };
79-
804
export const search = async (req, res) => {
815
const { q } = req.query;
82-
if (!q) {
6+
if (!q || q.trim() === "") {
837
return res.status(400).json({ error: "Missing search query" });
848
}
859

8610
try {
87-
const usersList = await User.find({
88-
$or: [
89-
{ name: { $regex: q, $options: "i" } },
90-
{ username: { $regex: q, $options: "i" } },
91-
{ collegeName: { $regex: q, $options: "i" } },
92-
{ branch: { $regex: q, $options: "i" } },
93-
]
94-
}).limit(5).lean();
95-
96-
const challengesList = await Challenge.find(
97-
{
11+
const searchTerm = q.trim();
12+
13+
const [usersList, challengesList] = await Promise.all([
14+
User.find({
15+
$or: [
16+
{ name: { $regex: searchTerm, $options: "i" } },
17+
{ username: { $regex: searchTerm, $options: "i" } },
18+
{ collegeName: { $regex: searchTerm, $options: "i" } },
19+
{ branch: { $regex: searchTerm, $options: "i" } }
20+
]
21+
})
22+
.select("name username profilePicture collegeName branch points rank")
23+
.limit(5)
24+
.lean(),
25+
26+
Challenge.find({
9827
$or: [
99-
{ title: new RegExp(q, "i") },
100-
{ description: new RegExp(q, "i") },
101-
{ category: { $in: [new RegExp(q, "i")] } },
102-
{ difficulty: new RegExp(q, "i") },
103-
{ platform: new RegExp(q, "i") },
104-
],
105-
}
106-
).limit(5).lean();
28+
{ title: { $regex: searchTerm, $options: "i" } },
29+
{ description: { $regex: searchTerm, $options: "i" } },
30+
{ category: { $regex: searchTerm, $options: "i" } },
31+
{ difficulty: { $regex: searchTerm, $options: "i" } },
32+
{ platform: { $regex: searchTerm, $options: "i" } }
33+
]
34+
})
35+
.select("title description category difficulty platform")
36+
.limit(5)
37+
.lean()
38+
]);
10739

10840
const users = usersList.map((u) => ({
10941
id: u._id?.toString(),
@@ -129,45 +61,48 @@ export const search = async (req, res) => {
12961

13062
res.json([...users, ...challenges]);
13163
} catch (err) {
132-
// console.error("Search error:", err);
64+
console.error("Search error:", err.message);
13365
res.status(500).json({ error: "Search failed" });
13466
}
13567
};
13668

13769
export const searchUser = async (req, res) => {
138-
const { q } = req.query;
139-
140-
if (!q) {
141-
return res.status(400).json({ error: "Missing search query" });
142-
}
70+
const { q } = req.query;
14371

144-
try {
145-
const users = await User.find({
146-
$or: [
147-
{ name: { $regex: q, $options: "i" } },
148-
{ username: { $regex: q, $options: "i" } },
149-
{ collegeName: { $regex: q, $options: "i" } },
150-
{ branch: { $regex: q, $options: "i" } }
151-
]
152-
})
153-
.limit(10)
154-
.lean();
72+
if (!q || q.trim() === "") {
73+
return res.status(400).json({ error: "Missing search query" });
74+
}
15575

156-
const formattedUsers = users.map(user => ({
157-
id: user._id?.toString(),
158-
name: user.name,
159-
username: user.username,
160-
profilePicture: user.profilePicture,
161-
collegeName: user.collegeName,
162-
branch: user.branch,
163-
points: user.points,
164-
rank: user.rank,
165-
type: "user"
166-
}));
76+
try {
77+
const searchTerm = q.trim();
78+
79+
const users = await User.find({
80+
$or: [
81+
{ name: { $regex: searchTerm, $options: "i" } },
82+
{ username: { $regex: searchTerm, $options: "i" } },
83+
{ collegeName: { $regex: searchTerm, $options: "i" } },
84+
{ branch: { $regex: searchTerm, $options: "i" } }
85+
]
86+
})
87+
.select("name username profilePicture collegeName branch points rank")
88+
.limit(10)
89+
.lean();
90+
91+
const formattedUsers = users.map(user => ({
92+
id: user._id?.toString(),
93+
name: user.name,
94+
username: user.username,
95+
profilePicture: user.profilePicture,
96+
collegeName: user.collegeName,
97+
branch: user.branch,
98+
points: user.points,
99+
rank: user.rank,
100+
type: "user"
101+
}));
167102

168-
res.json(formattedUsers);
169-
} catch (err) {
170-
// console.error(err);
171-
res.status(500).json({ error: "Search failed" });
172-
}
103+
res.json(formattedUsers);
104+
} catch (err) {
105+
console.error("Search user error:", err.message);
106+
res.status(500).json({ error: "Search failed" });
107+
}
173108
};

server/lib/dbIndexes.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,17 @@ export const createOptimalIndexes = async () => {
2828
await usersCollection.createIndex({ 'codeforces.username': 1 }, { sparse: true, name: 'cf_username_idx' });
2929
await usersCollection.createIndex({ 'codechef.username': 1 }, { sparse: true, name: 'cc_username_idx' });
3030

31+
// Search indexes for better performance
32+
await usersCollection.createIndex({ name: 1, username: 1 }, { name: 'user_search_idx' });
33+
await usersCollection.createIndex({ collegeName: 1 }, { name: 'college_idx' });
34+
await usersCollection.createIndex({ branch: 1 }, { name: 'branch_idx' });
35+
3136
// Challenge indexes
3237
await challengesCollection.createIndex({ category: 1, difficulty: 1, createdAt: -1 }, { name: 'challenge_filter_idx' });
3338
await challengesCollection.createIndex({ createdAt: -1 }, { name: 'challenge_date_idx' });
34-
await challengesCollection.createIndex({ title: 'text', category: 'text' }, { name: 'challenge_search_idx' });
39+
await challengesCollection.createIndex({ title: 1, platform: 1 }, { name: 'challenge_search_idx' });
3540
await challengesCollection.createIndex({ solvedUsers: 1 }, { sparse: true, name: 'solved_users_idx' });
41+
await challengesCollection.createIndex({ difficulty: 1 }, { name: 'difficulty_idx' });
3642

3743

3844
} catch (error) {

server/middleware/auth.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const protect = async (req, res, next) => {
4343
export const handleGoogleCallback = (req, res, next) => {
4444
passport.authenticate("google", { session: false }, (err, user, info) => {
4545
if (err) {
46-
console.error("Google auth error:", err);
46+
4747
return res.redirect(`${process.env.CLIENT_URL}/register?error=auth_error&message=Authentication error. Please try again.`);
4848
}
4949

@@ -60,7 +60,7 @@ export const handleGoogleCallback = (req, res, next) => {
6060
export const handleGithubCallback = (req, res, next) => {
6161
passport.authenticate("github", { session: false }, (err, user, info) => {
6262
if (err) {
63-
console.error("GitHub auth error:", err);
63+
6464
return res.redirect(`${process.env.CLIENT_URL}/register?error=auth_error&message=Authentication error. Please try again.`);
6565
}
6666

server/routes/statsRoutes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ import { getCounts } from "../controllers/statsControllers.js";
55
const router = express.Router();
66

77
// Get total stats
8-
router.get("/stats", getCounts);
8+
router.get("/", getCounts);
99

1010
export default router;

server/routes/typeSenseRoutes.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import express from "express";
22
import { search, searchUser } from "../controllers/typeSenseController.js";
33
const router = express.Router();
44

5-
// router.get("/typesense-test", test);
6-
router.get("/search", search);
7-
router.get("/search-user", searchUser);
5+
router.get("/", search);
6+
router.get("/user", searchUser);
87

98
export default router;

0 commit comments

Comments
 (0)