inou/api/api_genome.go

250 lines
5.8 KiB
Go

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)
}