From b0e80f4a06539ac67261c09f90924b5aa11a6c4b Mon Sep 17 00:00:00 2001 From: Aluvala Sai Akhilesh Date: Sun, 2 Nov 2025 22:28:51 +0530 Subject: [PATCH 1/3] batch fetch: made an api to get multiple users data by username --- .gitignore | 1 + gex/Controllers/Api/UserApiController.cs | 66 +++++++++++++++++++ gex/Services/Db/UserStats/BarUserDb.cs | 62 +++++++++++++++++ gex/Services/Db/UserStats/BarUserSkillDb.cs | 26 ++++++++ .../Repositories/BarUserRepository.cs | 25 ++++++- 5 files changed, 179 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5f29986..4a0baa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .github/ .vs/ TestResults +.vscode/ diff --git a/gex/Controllers/Api/UserApiController.cs b/gex/Controllers/Api/UserApiController.cs index 5b8dd1c..8c1aa69 100644 --- a/gex/Controllers/Api/UserApiController.cs +++ b/gex/Controllers/Api/UserApiController.cs @@ -318,6 +318,72 @@ public async Task>> Search(string search, return ApiOk(users); } + /// + /// get multiple users by usernames. A max of 50 usernames can be looked up at once. + /// This endpoint performs case-sensitive username matching. + /// + /// list of usernames to find + /// if will be populated. defaults to false + /// will previous names be searched against as well? defaults to false + /// cancellation token + /// + /// the response will contain a list of s that match any of the usernames + /// + /// + /// one or more usernames are empty, or no usernames provided + /// + [HttpPost("batch")] + public async Task>> GetUsersByUsernames( + [FromBody] List usernames, + [FromQuery] bool includeSkill = false, + [FromQuery] bool includePreviousNames = false, + CancellationToken cancel = default + ) { + + if (usernames == null || usernames.Count == 0) { + return ApiBadRequest>($"usernames list cannot be empty"); + } + + if (usernames.Count >= 50) { + return ApiBadRequest>($"usernames list cannot contain more than 50 items"); + } + + foreach (string username in usernames) { + if (string.IsNullOrWhiteSpace(username)) { + return ApiBadRequest>($"all usernames must be non-empty, found empty or whitespace-only username"); + } + } + + List apiUsers = await _UserRepository.GetByUsernames(usernames, includePreviousNames, cancel); + _Logger.LogInformation($"Found {apiUsers.Count} users matching usernames"); + + // Get unique users by UserID (in case there are duplicates from previous name searches) + apiUsers = apiUsers + .GroupBy(u => u.UserID) + .Select(g => g.First()) + .ToList(); + + // Fetch skills if requested + if (includeSkill == true && apiUsers.Count > 0) { + List userIds = apiUsers.Select(u => u.UserID).ToList(); + Dictionary> skillsDict = await _SkillDb.GetByUserIDs(userIds, cancel); + + // Populate skills for each user + foreach (ApiBarUser user in apiUsers) { + user.Skill = skillsDict.GetValueOrDefault(user.UserID) ?? new List(); + } + } + + // Fetch previous names if requested + if (includePreviousNames == true && apiUsers.Count > 0) { + foreach (ApiBarUser user in apiUsers) { + user.PreviousNames = await _UserRepository.GetUserNames(user.UserID, cancel); + } + } + + return ApiOk(apiUsers); + } + /// /// get the start spots of a user on a specific map, either by map name or map filename /// diff --git a/gex/Services/Db/UserStats/BarUserDb.cs b/gex/Services/Db/UserStats/BarUserDb.cs index ceaaa7d..b8deb3b 100644 --- a/gex/Services/Db/UserStats/BarUserDb.cs +++ b/gex/Services/Db/UserStats/BarUserDb.cs @@ -101,6 +101,68 @@ WHERE lower(u.username) LIKE lower(@Search) OR lower(p.user_name) LIKE lower(@Se ); } + /// + /// Search for users by a list of names using LIKE pattern matching, and optionally previous names. Case-insensitive. + /// Uses the ANY operator for efficient searching of multiple patterns. + /// + /// List of names to search for (each will be wrapped with % for LIKE matching) + /// If true, previous names will be searched as well + /// Cancellation token + /// A list of s + public async Task> SearchByNames(List names, bool includePreviousNames, CancellationToken cancel) { + if (names == null || names.Count == 0) { + return new List(); + } + + using NpgsqlConnection conn = _DbHelper.Connection(Dbs.MAIN); + + // Convert names to LIKE patterns + string[] searchPatterns = names.Select(n => $"%{n.ToLower()}%").ToArray(); + + return await conn.QueryListAsync( + includePreviousNames == true + ? @"SELECT distinct(u.id) ""user_id"", u.username, u.last_updated, p.user_name ""previous_name"" + FROM bar_user u LEFT JOIN bar_match_player p ON p.user_id = u.id + WHERE lower(u.username) LIKE ANY(@SearchPatterns) OR lower(p.user_name) LIKE ANY(@SearchPatterns)" + : @"SELECT id ""user_id"", username, last_updated, username ""previous_name"" + FROM bar_user + WHERE lower(username) LIKE ANY(@SearchPatterns)", + new { SearchPatterns = searchPatterns }, + cancellationToken: cancel + ); + } + + /// + /// Get users by username matches, and optionally previous names. Case-insensitive. + /// Uses equality matching instead of LIKE pattern matching. + /// + /// List of usernames to find + /// If true, previous names will be searched as well + /// Cancellation token + /// A list of s + public async Task> GetByUsernames(List usernames, bool includePreviousNames, CancellationToken cancel) { + if (usernames == null || usernames.Count == 0) { + return new List(); + } + + using NpgsqlConnection conn = _DbHelper.Connection(Dbs.MAIN); + + // Convert usernames to lowercase for case-insensitive matching + string[] lowercaseUsernames = usernames.Select(n => n.ToLower()).ToArray(); + + return await conn.QueryListAsync( + includePreviousNames == true + ? @"SELECT DISTINCT u.id, u.username, u.last_updated, u.country_code + FROM bar_user u LEFT JOIN bar_match_player p ON p.user_id = u.id + WHERE lower(u.username) = ANY(@Usernames) OR lower(p.user_name) = ANY(@Usernames)" + : @"SELECT id, username, last_updated, country_code + FROM bar_user + WHERE lower(username) = ANY(@Usernames)", + new { Usernames = lowercaseUsernames }, + cancellationToken: cancel + ); + } + /// /// get all names that a user has used /// diff --git a/gex/Services/Db/UserStats/BarUserSkillDb.cs b/gex/Services/Db/UserStats/BarUserSkillDb.cs index c3cca94..76d594d 100644 --- a/gex/Services/Db/UserStats/BarUserSkillDb.cs +++ b/gex/Services/Db/UserStats/BarUserSkillDb.cs @@ -70,5 +70,31 @@ public async Task> GetByUserID(long userID, CancellationToken ))).ToList(); } + /// + /// get the entries for multiple users + /// + /// List of user IDs to get skills for + /// cancellation token + /// + /// a dictionary mapping user IDs to lists of s + /// + public async Task>> GetByUserIDs(List userIDs, CancellationToken cancel) { + if (userIDs == null || userIDs.Count == 0) { + return new Dictionary>(); + } + + using NpgsqlConnection conn = _DbHelper.Connection(Dbs.MAIN); + IEnumerable skills = (await conn.QueryAsync(new CommandDefinition( + "SELECT * FROM bar_user_skill WHERE user_id = ANY(@UserIDs)", + new { UserIDs = userIDs.ToArray() }, + cancellationToken: cancel + ))).ToList(); + + // Group by user ID + return skills + .GroupBy(s => s.UserID) + .ToDictionary(g => g.Key, g => g.ToList()); + } + } } diff --git a/gex/Services/Repositories/BarUserRepository.cs b/gex/Services/Repositories/BarUserRepository.cs index 7fe6df5..d883340 100644 --- a/gex/Services/Repositories/BarUserRepository.cs +++ b/gex/Services/Repositories/BarUserRepository.cs @@ -1,10 +1,12 @@ -using gex.Models.Db; +using gex.Models.Api; +using gex.Models.Db; using gex.Models.UserStats; using gex.Services.Db.UserStats; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -83,6 +85,27 @@ public Task> SearchByName(string name, bool includePrevio return _UserDb.SearchByName(name, includePreviousNames, cancel); } + /// + /// get users by username matches, and optionally previous names. case-insensitive + /// + /// list of usernames to find + /// will previous names be searched as well + /// cancellation token + /// a list of s + public async Task> GetByUsernames(List usernames, bool includePreviousNames, CancellationToken cancel) { + List dbUsers = await _UserDb.GetByUsernames(usernames, includePreviousNames, cancel); + + // Convert BarUser to ApiBarUser + return dbUsers + .Select(u => new ApiBarUser { + UserID = u.UserID, + Username = u.Username, + LastUpdated = u.LastUpdated, + CountryCode = u.CountryCode + }) + .ToList(); + } + /// /// get all names that a user has used /// From 425594e9ba262466ef76f7d16ff68d837b7b56dd Mon Sep 17 00:00:00 2001 From: Aluvala Sai Akhilesh Date: Sun, 2 Nov 2025 22:33:26 +0530 Subject: [PATCH 2/3] removed Search by names method as its not used --- gex/Services/Db/UserStats/BarUserDb.cs | 30 -------------------------- 1 file changed, 30 deletions(-) diff --git a/gex/Services/Db/UserStats/BarUserDb.cs b/gex/Services/Db/UserStats/BarUserDb.cs index b8deb3b..ca84008 100644 --- a/gex/Services/Db/UserStats/BarUserDb.cs +++ b/gex/Services/Db/UserStats/BarUserDb.cs @@ -101,36 +101,6 @@ WHERE lower(u.username) LIKE lower(@Search) OR lower(p.user_name) LIKE lower(@Se ); } - /// - /// Search for users by a list of names using LIKE pattern matching, and optionally previous names. Case-insensitive. - /// Uses the ANY operator for efficient searching of multiple patterns. - /// - /// List of names to search for (each will be wrapped with % for LIKE matching) - /// If true, previous names will be searched as well - /// Cancellation token - /// A list of s - public async Task> SearchByNames(List names, bool includePreviousNames, CancellationToken cancel) { - if (names == null || names.Count == 0) { - return new List(); - } - - using NpgsqlConnection conn = _DbHelper.Connection(Dbs.MAIN); - - // Convert names to LIKE patterns - string[] searchPatterns = names.Select(n => $"%{n.ToLower()}%").ToArray(); - - return await conn.QueryListAsync( - includePreviousNames == true - ? @"SELECT distinct(u.id) ""user_id"", u.username, u.last_updated, p.user_name ""previous_name"" - FROM bar_user u LEFT JOIN bar_match_player p ON p.user_id = u.id - WHERE lower(u.username) LIKE ANY(@SearchPatterns) OR lower(p.user_name) LIKE ANY(@SearchPatterns)" - : @"SELECT id ""user_id"", username, last_updated, username ""previous_name"" - FROM bar_user - WHERE lower(username) LIKE ANY(@SearchPatterns)", - new { SearchPatterns = searchPatterns }, - cancellationToken: cancel - ); - } /// /// Get users by username matches, and optionally previous names. Case-insensitive. From 31fdbe6a08536a9ddc873eb20bd3f5697da7ab70 Mon Sep 17 00:00:00 2001 From: Aluvala Sai Akhilesh Date: Sun, 2 Nov 2025 22:35:54 +0530 Subject: [PATCH 3/3] made the search case sensitive --- gex/Services/Db/UserStats/BarUserDb.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/gex/Services/Db/UserStats/BarUserDb.cs b/gex/Services/Db/UserStats/BarUserDb.cs index ca84008..6f25375 100644 --- a/gex/Services/Db/UserStats/BarUserDb.cs +++ b/gex/Services/Db/UserStats/BarUserDb.cs @@ -103,8 +103,7 @@ WHERE lower(u.username) LIKE lower(@Search) OR lower(p.user_name) LIKE lower(@Se /// - /// Get users by username matches, and optionally previous names. Case-insensitive. - /// Uses equality matching instead of LIKE pattern matching. + /// Get users by username matches, and optionally previous names. Case-sensitive. /// /// List of usernames to find /// If true, previous names will be searched as well @@ -116,9 +115,6 @@ public async Task> GetByUsernames(List usernames, bool inc } using NpgsqlConnection conn = _DbHelper.Connection(Dbs.MAIN); - - // Convert usernames to lowercase for case-insensitive matching - string[] lowercaseUsernames = usernames.Select(n => n.ToLower()).ToArray(); return await conn.QueryListAsync( includePreviousNames == true @@ -127,8 +123,8 @@ public async Task> GetByUsernames(List usernames, bool inc WHERE lower(u.username) = ANY(@Usernames) OR lower(p.user_name) = ANY(@Usernames)" : @"SELECT id, username, last_updated, country_code FROM bar_user - WHERE lower(username) = ANY(@Usernames)", - new { Usernames = lowercaseUsernames }, + WHERE username = ANY(@Usernames)", + new { Usernames = usernames }, cancellationToken: cancel ); }