256 lines
5.9 KiB
Go
256 lines
5.9 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
|
|
}
|
|
|
|
// Check dossier access first
|
|
if !requireDossierAccess(w, ctx, dossierID) {
|
|
return
|
|
}
|
|
|
|
// Use system context for genome queries (dossier access already checked)
|
|
sysCtx := &lib.AccessContext{IsSystem: true}
|
|
|
|
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
|
|
extraction, err := lib.GenomeGetExtraction(sysCtx, 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
|
|
var tiers []lib.GenomeTier
|
|
tierCategories := make(map[string]string) // tierID -> category name
|
|
|
|
if category != "" {
|
|
// Specific category requested
|
|
tier, err := lib.GenomeGetTierByCategory(sysCtx, dossierID, extraction.EntryID, category)
|
|
if err == nil {
|
|
tiers = append(tiers, *tier)
|
|
tierCategories[tier.TierID] = tier.Category
|
|
}
|
|
} else {
|
|
// All tiers
|
|
tiers, _ = lib.GenomeGetTiers(sysCtx, 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
|
|
variants, err := lib.GenomeGetVariants(sysCtx, 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)
|
|
}
|