package main import ( "encoding/json" "net/http" "sort" "strconv" "strings" "inou/lib" ) type GenomeMatch struct { RSID string `json:"rsid"` Genotype string `json:"genotype"` Gene string `json:"gene,omitempty"` Matched bool `json:"matched"` Magnitude *float64 `json:"magnitude,omitempty"` Repute string `json:"repute,omitempty"` Summary string `json:"summary,omitempty"` Category string `json:"category,omitempty"` Subcategory string `json:"subcategory,omitempty"` } type GenomeResponse struct { Matches []GenomeMatch `json:"matches"` Returned int `json:"returned"` Total int `json:"total"` } func handleGenomeQuery(w http.ResponseWriter, r *http.Request) { ctx := getAccessContextOrFail(w, r) if ctx == nil { return } dossierID := r.URL.Query().Get("dossier") if dossierID == "" { http.Error(w, "missing dossier", http.StatusBadRequest) return } // RBAC enforced in lib layer - no checks here category := r.URL.Query().Get("category") search := r.URL.Query().Get("search") rsidsParam := r.URL.Query().Get("rsids") gene := r.URL.Query().Get("gene") includeHidden := r.URL.Query().Get("include_hidden") == "true" minMagStr := r.URL.Query().Get("min_magnitude") sortBy := r.URL.Query().Get("sort") // "magnitude" (default), "gene", "rsid" offsetStr := r.URL.Query().Get("offset") limitStr := r.URL.Query().Get("limit") var minMag float64 if minMagStr != "" { minMag, _ = strconv.ParseFloat(minMagStr, 64) } offset := 0 if offsetStr != "" { offset, _ = strconv.Atoi(offsetStr) } limit := 100 // default limit if limitStr != "" { limit, _ = strconv.Atoi(limitStr) if limit > 500 { limit = 500 // max limit } } var rsids []string if rsidsParam != "" { rsids = strings.Split(rsidsParam, ",") } var genes []string if gene != "" { for _, g := range strings.Split(gene, ",") { genes = append(genes, strings.TrimSpace(g)) } } // Find extraction entry (RBAC enforced in lib) extraction, err := lib.GenomeGetExtraction(ctx, dossierID) if err != nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"error": "no genome data for this dossier"}) return } // Get tiers to query (RBAC enforced in lib) var tiers []lib.GenomeTier tierCategories := make(map[string]string) // tierID -> category name if category != "" { // Specific category requested tier, err := lib.GenomeGetTierByCategory(ctx, dossierID, extraction.EntryID, category) if err == nil { tiers = append(tiers, *tier) tierCategories[tier.TierID] = tier.Category } } else { // All tiers tiers, _ = lib.GenomeGetTiers(ctx, dossierID, extraction.EntryID) for _, t := range tiers { tierCategories[t.TierID] = t.Category } } if len(tiers) == 0 { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(GenomeResponse{Matches: []GenomeMatch{}, Returned: 0, Total: 0}) return } // Get tier IDs tierIDs := make([]string, len(tiers)) for i, t := range tiers { tierIDs[i] = t.TierID } // Query variants (RBAC enforced in lib) variants, err := lib.GenomeGetVariants(ctx, dossierID, tierIDs) if err != nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(GenomeResponse{Matches: []GenomeMatch{}, Returned: 0, Total: 0}) return } // Filter and collect matches var matches []GenomeMatch total := 0 for _, v := range variants { // Filter by rsids if specified if len(rsids) > 0 { found := false for _, r := range rsids { if r == v.RSID { found = true break } } if !found { continue } } // Filter by gene(s) if len(genes) > 0 { found := false for _, g := range genes { if strings.EqualFold(v.Gene, g) { found = true break } } if !found { continue } } // Filter by search if search != "" { searchLower := strings.ToLower(search) if !strings.Contains(strings.ToLower(v.Gene), searchLower) && !strings.Contains(strings.ToLower(v.Summary), searchLower) && !strings.Contains(strings.ToLower(v.Subcategory), searchLower) && !strings.Contains(strings.ToLower(v.RSID), searchLower) { continue } } // Filter by magnitude if minMag > 0 && v.Magnitude < minMag { continue } if !includeHidden && v.Magnitude > 4.0 { continue } // Filter out "Bad" repute variants by default (scary/negative results) if !includeHidden && strings.EqualFold(v.Repute, "bad") { continue } total++ // Apply offset and limit if total <= offset { continue } if len(matches) >= limit { continue } match := GenomeMatch{ RSID: v.RSID, Genotype: v.Genotype, Gene: v.Gene, Matched: true, Category: tierCategories[v.TierID], Subcategory: v.Subcategory, } if v.Magnitude > 0 { mag := v.Magnitude match.Magnitude = &mag } if v.Repute != "" { match.Repute = v.Repute } if v.Summary != "" { match.Summary = v.Summary } matches = append(matches, match) } // Sort results (default: magnitude descending) switch sortBy { case "gene": sort.Slice(matches, func(i, j int) bool { return matches[i].Gene < matches[j].Gene }) case "rsid": sort.Slice(matches, func(i, j int) bool { return matches[i].RSID < matches[j].RSID }) default: // "magnitude" or empty - sort by magnitude descending sort.Slice(matches, func(i, j int) bool { mi, mj := float64(0), float64(0) if matches[i].Magnitude != nil { mi = *matches[i].Magnitude } if matches[j].Magnitude != nil { mj = *matches[j].Magnitude } return mi > mj }) } resp := GenomeResponse{ Matches: matches, Returned: len(matches), Total: total, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }