package controllers import ( "strconv" "gitea.tbdevent.eu/TBD/reforger_crawler_main/initializers" "gitea.tbdevent.eu/TBD/reforger_crawler_main/models" "github.com/gin-gonic/gin" "gorm.io/gorm" "gorm.io/gorm/clause" ) // SetAddonDependencies replaces all outgoing dependency edges for an addon. // An empty dependency list is valid and clears the edges. func SetAddonDependencies(c *gin.Context) { var report models.DependencyReport if err := c.ShouldBindJSON(&report); err != nil { c.JSON(400, gin.H{"error": "Invalid JSON"}) return } if report.AddonID == "" { c.JSON(400, gin.H{"error": "addonId is required"}) return } seen := make(map[string]bool) edges := make([]models.AddonDependency, 0, len(report.Dependencies)) for _, dep := range report.Dependencies { if dep.ID == "" || dep.ID == report.AddonID || seen[dep.ID] { continue } seen[dep.ID] = true edges = append(edges, models.AddonDependency{ AddonID: report.AddonID, DependencyID: dep.ID, Version: dep.Version, DependencyName: dep.Name, }) } err := initializers.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Where("addon_id = ?", report.AddonID).Delete(&models.AddonDependency{}).Error; err != nil { return err } if len(edges) == 0 { return nil } return tx.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(&edges, 100).Error }) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{"status": "success", "stored": len(edges)}) } // GetDependents returns the addons that depend on the given addon. func GetDependents(c *gin.Context) { addonID := c.Param("id") var addon models.Addon if err := initializers.DB.Where("id = ?", addonID).First(&addon).Error; err != nil { c.JSON(404, gin.H{"error": "Addon not found"}) return } limit := parseLimit(c.Query("limit")) offset := parseOffset(c.Query("offset")) 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 } c.JSON(200, models.DependentsResponse{ AddonID: addonID, Dependents: dependents, Total: total, Limit: limit, Offset: offset, }) } // GetDependencies returns the addons that the given addon depends on. func GetDependencies(c *gin.Context) { addonID := c.Param("id") var addon models.Addon if err := initializers.DB.Where("id = ?", addonID).First(&addon).Error; err != nil { c.JSON(404, gin.H{"error": "Addon not found"}) return } limit := parseLimit(c.Query("limit")) offset := parseOffset(c.Query("offset")) 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 } c.JSON(200, models.DependenciesResponse{ AddonID: addonID, Dependencies: 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 != "" { if val, err := strconv.Atoi(raw); err == nil && val > 0 { limit = val } } if limit > 500 { limit = 500 } 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 }