From 0ef27b00727945c6309d02e669b6abd3de1a999a Mon Sep 17 00:00:00 2001 From: ilbinek Date: Mon, 10 Aug 2026 14:15:00 +0200 Subject: [PATCH] dependents size fix --- api/reforger_crawler/SearchLobbyRooms.bru | 66 +++++++++++++++++ .../environments/reforger.bru | 4 + controllers/dependencyController.go | 73 ++++++++++++++----- models/apiModels.go | 10 ++- 4 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 api/reforger_crawler/SearchLobbyRooms.bru create mode 100644 api/reforger_crawler/environments/reforger.bru diff --git a/api/reforger_crawler/SearchLobbyRooms.bru b/api/reforger_crawler/SearchLobbyRooms.bru new file mode 100644 index 0000000..9baafc7 --- /dev/null +++ b/api/reforger_crawler/SearchLobbyRooms.bru @@ -0,0 +1,66 @@ +meta { + name: SearchLobbyRooms + type: http + seq: 3 +} + +post { + url: {{gameApiHost}}/game-api/api/v1.0/lobby/rooms/search + body: json + auth: none +} + +headers { + accept: */* + user-agent: Arma Reforger/1.7.0.54 (Client; Windows) +} + +body:json { + { + "directJoinCode": "", + "hostAddress": "", + "order": "PlayerCount", + "includePing": 1, + "scenarioId": "", + "text": "", + "ascendent": false, + "gameClientFilter": "AnyCompatible", + "accessToken": "", + "clientVersion": "1.7.0", + "platformId": "ReforgerSteam", + "gameClientType": "PLATFORM_PC", + "lightweight": true, + "from": 0, + "limit": 50, + "pingValues": [ + { "pingSiteId": "tokyo", "value": 243.56622314453126 }, + { "pingSiteId": "los_angeles", "value": 171.9037628173828 }, + { "pingSiteId": "miami", "value": 136.6873321533203 }, + { "pingSiteId": "new_york", "value": 115.14977264404297 }, + { "pingSiteId": "singapore", "value": 271.533203125 }, + { "pingSiteId": "frankfurt", "value": 24.513813018798829 }, + { "pingSiteId": "london", "value": 32.16381072998047 }, + { "pingSiteId": "sydney", "value": 296.2795104980469 } + ] + } +} + +vars:pre-request { + gameApiHost: api-ar-game.bistudio.com +} + +script:pre-request { + // The game sends a JSON body under a form-urlencoded content-type. Bruno keys + // body variable interpolation off content-type, so setting that header + // declaratively silently disables {{...}} substitution in the body. Instead we + // inject the token here and set the header after, once interpolation is moot. + const body = req.getBody(); + body.accessToken = bru.getEnvVar("accessToken"); + req.setBody(JSON.stringify(body)); + req.setHeader("content-type", "application/x-www-form-urlencoded"); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/api/reforger_crawler/environments/reforger.bru b/api/reforger_crawler/environments/reforger.bru new file mode 100644 index 0000000..6d0e4b4 --- /dev/null +++ b/api/reforger_crawler/environments/reforger.bru @@ -0,0 +1,4 @@ +vars { + gameApiHost: https://api-ar-game.bistudio.com + accessToken: PASTE_JWT_HERE +} diff --git a/controllers/dependencyController.go b/controllers/dependencyController.go index 46762e5..6fc9223 100644 --- a/controllers/dependencyController.go +++ b/controllers/dependencyController.go @@ -67,15 +67,9 @@ func GetDependents(c *gin.Context) { } limit := parseLimit(c.Query("limit")) + offset := parseOffset(c.Query("offset")) - dependents := []models.AddonSearchResult{} - err := initializers.DB.Model(&models.Addon{}). - Select("addons.id, addons.name, addons.type, addons.summary, addons.preview, addons.subscriber_count, addons.current_version_number, addons.author"). - Joins("JOIN addon_dependencies ad ON ad.addon_id = addons.id"). - Where("ad.dependency_id = ?", addonID). - Order("addons.subscriber_count DESC"). - Limit(limit). - Find(&dependents).Error + dependents, total, err := queryRelatedAddons(addonID, "ad.addon_id", "ad.dependency_id", limit, offset) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return @@ -84,7 +78,9 @@ func GetDependents(c *gin.Context) { c.JSON(200, models.DependentsResponse{ AddonID: addonID, Dependents: dependents, - Total: len(dependents), + Total: total, + Limit: limit, + Offset: offset, }) } @@ -99,15 +95,9 @@ func GetDependencies(c *gin.Context) { } limit := parseLimit(c.Query("limit")) + offset := parseOffset(c.Query("offset")) - dependencies := []models.AddonSearchResult{} - err := initializers.DB.Model(&models.Addon{}). - Select("addons.id, addons.name, addons.type, addons.summary, addons.preview, addons.subscriber_count, addons.current_version_number, addons.author"). - Joins("JOIN addon_dependencies ad ON ad.dependency_id = addons.id"). - Where("ad.addon_id = ?", addonID). - Order("addons.subscriber_count DESC"). - Limit(limit). - Find(&dependencies).Error + dependencies, total, err := queryRelatedAddons(addonID, "ad.dependency_id", "ad.addon_id", limit, offset) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return @@ -116,10 +106,46 @@ func GetDependencies(c *gin.Context) { c.JSON(200, models.DependenciesResponse{ AddonID: addonID, Dependencies: dependencies, - Total: len(dependencies), + Total: total, + Limit: limit, + Offset: offset, }) } +// queryRelatedAddons returns one page of addons linked to addonID through the +// addon_dependencies edge table, plus the total number of matches. +// +// joinCol is the edge column joined to addons.id; filterCol is the edge column +// matched against addonID. Both are caller-supplied constants, never user input. +// +// The addons.id tiebreaker in the ORDER BY is required: subscriber_count alone +// is not a unique ordering, so without it paging could repeat or skip rows. +func queryRelatedAddons(addonID, joinCol, filterCol string, limit, offset int) ([]models.AddonSearchResult, int, error) { + base := func() *gorm.DB { + return initializers.DB.Model(&models.Addon{}). + Joins("JOIN addon_dependencies ad ON "+joinCol+" = addons.id"). + Where(filterCol+" = ?", addonID) + } + + var total int64 + if err := base().Count(&total).Error; err != nil { + return nil, 0, err + } + + results := []models.AddonSearchResult{} + err := base(). + Select("addons.id, addons.name, addons.type, addons.summary, addons.preview, addons.subscriber_count, addons.current_version_number, addons.author"). + Order("addons.subscriber_count DESC, addons.id ASC"). + Limit(limit). + Offset(offset). + Find(&results).Error + if err != nil { + return nil, 0, err + } + + return results, int(total), nil +} + func parseLimit(raw string) int { limit := 100 if raw != "" { @@ -132,3 +158,14 @@ func parseLimit(raw string) int { } return limit } + +func parseOffset(raw string) int { + if raw == "" { + return 0 + } + val, err := strconv.Atoi(raw) + if err != nil || val < 0 { + return 0 + } + return val +} diff --git a/models/apiModels.go b/models/apiModels.go index ab6ad91..7aa6347 100644 --- a/models/apiModels.go +++ b/models/apiModels.go @@ -66,18 +66,24 @@ type DependencyReport struct { Dependencies []DependencyReportItem `json:"dependencies"` } -// DependentsResponse lists the addons that depend on a given addon +// DependentsResponse lists one page of the addons that depend on a given addon. +// Total is the full number of matches, not the length of this page. type DependentsResponse struct { AddonID string `json:"addonId"` Dependents []AddonSearchResult `json:"dependents"` Total int `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` } -// DependenciesResponse lists the addons a given addon depends on +// DependenciesResponse lists one page of the addons a given addon depends on. +// Total is the full number of matches, not the length of this page. type DependenciesResponse struct { AddonID string `json:"addonId"` Dependencies []AddonSearchResult `json:"dependencies"` Total int `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` } // SearchResponse contains the search results