Skip to content

Commit f543b20

Browse files
authored
fix: search (#341)
1 parent 99bbf5d commit f543b20

File tree

5 files changed

+245
-24
lines changed

5 files changed

+245
-24
lines changed

infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@
388388
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
389389
CODE_SIGN_ENTITLEMENTS = "eid-wallet_iOS/eid-wallet_iOS.entitlements";
390390
CODE_SIGN_IDENTITY = "iPhone Developer";
391-
CURRENT_PROJECT_VERSION = 0.2.1.1;
391+
CURRENT_PROJECT_VERSION = 0.2.1.2;
392392
DEVELOPMENT_TEAM = M49C8XS835;
393393
ENABLE_BITCODE = NO;
394394
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
@@ -436,7 +436,7 @@
436436
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
437437
CODE_SIGN_ENTITLEMENTS = "eid-wallet_iOS/eid-wallet_iOS.entitlements";
438438
CODE_SIGN_IDENTITY = "iPhone Developer";
439-
CURRENT_PROJECT_VERSION = 0.2.1.1;
439+
CURRENT_PROJECT_VERSION = 0.2.1.2;
440440
DEVELOPMENT_TEAM = M49C8XS835;
441441
ENABLE_BITCODE = NO;
442442
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;

infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
</dict>
2929
</array>
3030
<key>CFBundleVersion</key>
31-
<string>0.2.1.1</string>
31+
<string>0.2.1.2</string>
3232
<key>LSRequiresIPhoneOS</key>
3333
<true/>
3434
<key>NSAppTransportSecurity</key>

platforms/pictique-api/src/controllers/UserController.ts

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,107 @@ export class UserController {
3434

3535
search = async (req: Request, res: Response) => {
3636
try {
37-
const { q } = req.query;
37+
const { q, page = "1", limit = "10", verified, sort = "relevance" } = req.query;
3838

3939
if (!q || typeof q !== "string") {
4040
return res
4141
.status(400)
4242
.json({ error: "Search query is required" });
4343
}
4444

45-
const users = await this.userService.searchUsers(q);
46-
res.json(users);
45+
// Validate search query length
46+
if (q.trim().length < 2) {
47+
return res
48+
.status(400)
49+
.json({ error: "Search query must be at least 2 characters long" });
50+
}
51+
52+
// Parse and validate pagination parameters
53+
const pageNum = parseInt(page as string) || 1;
54+
const limitNum = Math.min(parseInt(limit as string) || 10, 50); // Cap at 50 results
55+
56+
if (pageNum < 1 || limitNum < 1) {
57+
return res
58+
.status(400)
59+
.json({ error: "Invalid pagination parameters" });
60+
}
61+
62+
// Parse verified filter
63+
const verifiedOnly = verified === "true";
64+
65+
// Validate sort parameter
66+
const validSortOptions = ["relevance", "name", "verified", "newest"];
67+
const sortOption = validSortOptions.includes(sort as string) ? sort as string : "relevance";
68+
69+
// Get users and count in parallel
70+
const [users, total] = await Promise.all([
71+
this.userService.searchUsers(q, pageNum, limitNum, verifiedOnly, sortOption),
72+
this.userService.getSearchUsersCount(q, verifiedOnly)
73+
]);
74+
75+
// Calculate pagination metadata
76+
const totalPages = Math.ceil(total / limitNum);
77+
const hasNextPage = pageNum < totalPages;
78+
const hasPrevPage = pageNum > 1;
79+
80+
res.json({
81+
users,
82+
pagination: {
83+
page: pageNum,
84+
limit: limitNum,
85+
total,
86+
totalPages,
87+
hasNextPage,
88+
hasPrevPage
89+
},
90+
searchInfo: {
91+
query: q,
92+
verifiedOnly,
93+
sortBy: sortOption
94+
}
95+
});
4796
} catch (error) {
4897
console.error("Error searching users:", error);
4998
res.status(500).json({ error: "Internal server error" });
5099
}
51100
};
52101

102+
getSearchSuggestions = async (req: Request, res: Response) => {
103+
try {
104+
const { q, limit = "5" } = req.query;
105+
106+
if (!q || typeof q !== "string") {
107+
return res
108+
.status(400)
109+
.json({ error: "Search query is required" });
110+
}
111+
112+
// Parse limit parameter
113+
const limitNum = Math.min(parseInt(limit as string) || 5, 20); // Cap at 20 suggestions
114+
115+
const suggestions = await this.userService.getSearchSuggestions(q, limitNum);
116+
res.json({ suggestions });
117+
} catch (error) {
118+
console.error("Error getting search suggestions:", error);
119+
res.status(500).json({ error: "Internal server error" });
120+
}
121+
};
122+
123+
getPopularSearches = async (req: Request, res: Response) => {
124+
try {
125+
const { limit = "10" } = req.query;
126+
127+
// Parse limit parameter
128+
const limitNum = Math.min(parseInt(limit as string) || 10, 50); // Cap at 50 results
129+
130+
const popularSearches = await this.userService.getPopularSearches(limitNum);
131+
res.json({ popularSearches });
132+
} catch (error) {
133+
console.error("Error getting popular searches:", error);
134+
res.status(500).json({ error: "Internal server error" });
135+
}
136+
};
137+
53138
follow = async (req: Request, res: Response) => {
54139
try {
55140
const followerId = req.user?.id;

platforms/pictique-api/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ app.get(
128128
// User routes
129129
app.get("/api/users", userController.currentUser);
130130
app.get("/api/users/search", userController.search);
131+
app.get("/api/users/suggestions", userController.getSearchSuggestions);
132+
app.get("/api/users/popular", userController.getPopularSearches);
131133
app.post("/api/users/:id/follow", authGuard, userController.follow);
132134
app.get("/api/users/:id", authGuard, userController.getProfileById);
133135
app.patch("/api/users", authGuard, userController.updateProfile);

platforms/pictique-api/src/services/UserService.ts

Lines changed: 152 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { AppDataSource } from "../database/data-source";
22
import { User } from "../database/entities/User";
33
import { Post } from "../database/entities/Post";
44
import { signToken } from "../utils/jwt";
5-
import { Like } from "typeorm";
5+
import { Like, Raw } from "typeorm";
66

77
export class UserService {
88
userRepository = AppDataSource.getRepository(User);
@@ -38,24 +38,158 @@ export class UserService {
3838
return await this.userRepository.findOneBy({ id });
3939
}
4040

41-
searchUsers = async (query: string) => {
42-
const searchQuery = query;
41+
searchUsers = async (
42+
query: string,
43+
page: number = 1,
44+
limit: number = 10,
45+
verifiedOnly: boolean = false,
46+
sortBy: string = "relevance"
47+
) => {
48+
// Sanitize and trim the search query
49+
const searchQuery = query.trim();
50+
51+
// Return empty array if query is too short or empty
52+
if (searchQuery.length < 2) {
53+
return [];
54+
}
4355

44-
return this.userRepository.find({
45-
where: [
46-
{ name: Like(`%${searchQuery}%`) },
47-
{ ename: Like(`%${searchQuery}%`) },
48-
],
49-
select: {
50-
id: true,
51-
handle: true,
52-
name: true,
53-
description: true,
54-
avatarUrl: true,
55-
isVerified: true,
56-
},
57-
take: 10,
58-
});
56+
// Validate pagination parameters
57+
if (page < 1 || limit < 1 || limit > 100) {
58+
return [];
59+
}
60+
61+
// Use query builder for more complex queries
62+
const queryBuilder = this.userRepository
63+
.createQueryBuilder("user")
64+
.select([
65+
"user.id",
66+
"user.handle",
67+
"user.name",
68+
"user.description",
69+
"user.avatarUrl",
70+
"user.isVerified"
71+
])
72+
.where(
73+
"user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query OR user.description ILIKE :query OR user.name ILIKE :fuzzyQuery OR user.ename ILIKE :fuzzyQuery OR user.handle ILIKE :fuzzyQuery",
74+
{
75+
query: `%${searchQuery}%`,
76+
fuzzyQuery: `%${searchQuery.split('').join('%')}%` // Fuzzy search with wildcards between characters
77+
}
78+
);
79+
80+
// Add verified filter if requested
81+
if (verifiedOnly) {
82+
queryBuilder.andWhere("user.isVerified = :verified", { verified: true });
83+
}
84+
85+
// Add additional filters for better results
86+
queryBuilder.andWhere("user.isArchived = :archived", { archived: false });
87+
88+
// Apply sorting based on sortBy parameter
89+
switch (sortBy) {
90+
case "name":
91+
queryBuilder.orderBy("user.name", "ASC");
92+
break;
93+
case "verified":
94+
queryBuilder.orderBy("user.isVerified", "DESC").addOrderBy("user.name", "ASC");
95+
break;
96+
case "newest":
97+
queryBuilder.orderBy("user.createdAt", "DESC");
98+
break;
99+
case "relevance":
100+
default:
101+
// Default relevance sorting: verified first, then by name
102+
queryBuilder.orderBy("user.isVerified", "DESC").addOrderBy("user.name", "ASC");
103+
break;
104+
}
105+
106+
return queryBuilder
107+
.skip((page - 1) * limit)
108+
.take(limit)
109+
.getMany();
110+
};
111+
112+
getSearchUsersCount = async (
113+
query: string,
114+
verifiedOnly: boolean = false
115+
) => {
116+
// Sanitize and trim the search query
117+
const searchQuery = query.trim();
118+
119+
// Return 0 if query is too short or empty
120+
if (searchQuery.length < 2) {
121+
return 0;
122+
}
123+
124+
// Use query builder for count
125+
const queryBuilder = this.userRepository
126+
.createQueryBuilder("user")
127+
.where(
128+
"user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query OR user.description ILIKE :query OR user.name ILIKE :fuzzyQuery OR user.ename ILIKE :fuzzyQuery OR user.handle ILIKE :fuzzyQuery",
129+
{
130+
query: `%${searchQuery}%`,
131+
fuzzyQuery: `%${searchQuery.split('').join('%')}%` // Fuzzy search with wildcards between characters
132+
}
133+
);
134+
135+
// Add verified filter if requested
136+
if (verifiedOnly) {
137+
queryBuilder.andWhere("user.isVerified = :verified", { verified: true });
138+
}
139+
140+
// Add additional filters for consistency
141+
queryBuilder.andWhere("user.isArchived = :archived", { archived: false });
142+
143+
return queryBuilder.getCount();
144+
};
145+
146+
getSearchSuggestions = async (query: string, limit: number = 5) => {
147+
// Sanitize and trim the search query
148+
const searchQuery = query.trim();
149+
150+
// Return empty array if query is too short
151+
if (searchQuery.length < 1) {
152+
return [];
153+
}
154+
155+
// Use query builder for suggestions
156+
const queryBuilder = this.userRepository
157+
.createQueryBuilder("user")
158+
.select([
159+
"user.id",
160+
"user.handle",
161+
"user.name",
162+
"user.ename"
163+
])
164+
.where(
165+
"user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query",
166+
{ query: `%${searchQuery}%` }
167+
)
168+
.andWhere("user.isArchived = :archived", { archived: false })
169+
.orderBy("user.isVerified", "DESC")
170+
.addOrderBy("user.name", "ASC")
171+
.take(limit);
172+
173+
return queryBuilder.getMany();
174+
};
175+
176+
getPopularSearches = async (limit: number = 10) => {
177+
// This could be enhanced with actual search analytics in the future
178+
// For now, return some sample popular searches based on verified users
179+
return this.userRepository
180+
.createQueryBuilder("user")
181+
.select([
182+
"user.id",
183+
"user.handle",
184+
"user.name",
185+
"user.ename"
186+
])
187+
.where("user.isVerified = :verified", { verified: true })
188+
.andWhere("user.isArchived = :archived", { archived: false })
189+
.andWhere("user.name IS NOT NULL")
190+
.orderBy("user.name", "ASC")
191+
.take(limit)
192+
.getMany();
59193
};
60194

61195
followUser = async (followerId: string, followingId: string) => {

0 commit comments

Comments
 (0)