refactor: move genome query to lib, add sensitive variant redaction
- Move GenomeQuery logic from api/api_genome.go to lib/v2.go so MCP handler calls lib directly instead of HTTP round-trip (fixes 403 on genome queries via Claude.ai MCP - was hitting RBAC table mismatch) - Generate CategoryFromString from categoryNames in init() (single source of truth, removes 9 unused aliases) - Redact sensitive variants (Bad repute, magnitude >4) in targeted queries: genotype/summary replaced with "hidden" + hint to use include_hidden=true. Broad queries still suppress entirely. - API handler is now a thin wrapper parsing query params Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e8d0656fc6
commit
d2d77d1503
|
|
@ -3,31 +3,12 @@ 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 {
|
||||
|
|
@ -40,224 +21,48 @@ func handleGenomeQuery(w http.ResponseWriter, r *http.Request) {
|
|||
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")
|
||||
|
||||
// Parse query params into opts
|
||||
q := r.URL.Query()
|
||||
var minMag float64
|
||||
if minMagStr != "" {
|
||||
minMag, _ = strconv.ParseFloat(minMagStr, 64)
|
||||
if s := q.Get("min_magnitude"); s != "" {
|
||||
minMag, _ = strconv.ParseFloat(s, 64)
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if offsetStr != "" {
|
||||
offset, _ = strconv.Atoi(offsetStr)
|
||||
if s := q.Get("offset"); s != "" {
|
||||
offset, _ = strconv.Atoi(s)
|
||||
}
|
||||
limit := 100 // default limit
|
||||
if limitStr != "" {
|
||||
limit, _ = strconv.Atoi(limitStr)
|
||||
if limit > 500 {
|
||||
limit = 500 // max limit
|
||||
}
|
||||
limit := 100
|
||||
if s := q.Get("limit"); s != "" {
|
||||
limit, _ = strconv.Atoi(s)
|
||||
}
|
||||
|
||||
var rsids []string
|
||||
if rsidsParam != "" {
|
||||
rsids = strings.Split(rsidsParam, ",")
|
||||
if s := q.Get("rsids"); s != "" {
|
||||
rsids = strings.Split(s, ",")
|
||||
}
|
||||
|
||||
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)
|
||||
result, err := lib.GenomeQuery(dossierID, lib.GenomeQueryOpts{
|
||||
Category: q.Get("category"),
|
||||
Search: q.Get("search"),
|
||||
Gene: q.Get("gene"),
|
||||
RSIDs: rsids,
|
||||
MinMagnitude: minMag,
|
||||
IncludeHidden: q.Get("include_hidden") == "true",
|
||||
Sort: q.Get("sort"),
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
})
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"error": "genome extraction entry not found",
|
||||
"code": "GENOME_NO_EXTRACTION",
|
||||
"detail": err.Error(),
|
||||
"error": err.Error(),
|
||||
})
|
||||
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(map[string]interface{}{
|
||||
"error": "failed to retrieve genome variants",
|
||||
"code": "GENOME_VARIANT_QUERY_FAILED",
|
||||
"detail": err.Error(),
|
||||
})
|
||||
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)
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
|
|
|||
62
lib/types.go
62
lib/types.go
|
|
@ -72,51 +72,7 @@ var GenomeTierFromString = map[string]int{
|
|||
"other": GenomeTierOther,
|
||||
}
|
||||
|
||||
// CategoryFromString converts LLM triage output to category enum
|
||||
var CategoryFromString = map[string]int{
|
||||
"imaging": CategoryImaging,
|
||||
"slice": CategoryImaging,
|
||||
"series": CategoryImaging,
|
||||
"study": CategoryImaging,
|
||||
"document": CategoryDocument,
|
||||
"radiology_report": CategoryDocument,
|
||||
"ultrasound": CategoryDocument,
|
||||
"other": CategoryDocument,
|
||||
"lab": CategoryLab,
|
||||
"lab_report": CategoryLab,
|
||||
"genome": CategoryGenome,
|
||||
"genome_tier": CategoryGenome,
|
||||
"rsid": CategoryGenome,
|
||||
"variant": CategoryGenome,
|
||||
"upload": CategoryUpload,
|
||||
"consultation": CategoryConsultation,
|
||||
"diagnosis": CategoryDiagnosis,
|
||||
"vital": CategoryVital,
|
||||
"exercise": CategoryExercise,
|
||||
"medication": CategoryMedication,
|
||||
"supplement": CategorySupplement,
|
||||
"nutrition": CategoryNutrition,
|
||||
"fertility": CategoryFertility,
|
||||
"symptom": CategorySymptom,
|
||||
"note": CategoryNote,
|
||||
"history": CategoryHistory,
|
||||
"family_history": CategoryFamilyHistory,
|
||||
"surgery": CategorySurgery,
|
||||
"hospitalization": CategoryHospital,
|
||||
"birth": CategoryBirth,
|
||||
"device": CategoryDevice,
|
||||
"therapy": CategoryTherapy,
|
||||
"assessment": CategoryAssessment,
|
||||
"provider": CategoryProvider,
|
||||
"question": CategoryQuestion,
|
||||
}
|
||||
|
||||
// CategoryKey returns the translation key for a category (e.g. "category003")
|
||||
func CategoryKey(cat int) string {
|
||||
return fmt.Sprintf("category%03d", cat)
|
||||
}
|
||||
|
||||
// categoryNames maps category ints back to their string names
|
||||
// categoryNames maps category ints to their English names (source of truth)
|
||||
var categoryNames = map[int]string{
|
||||
CategoryImaging: "imaging",
|
||||
CategoryDocument: "document",
|
||||
|
|
@ -145,6 +101,22 @@ var categoryNames = map[int]string{
|
|||
CategoryQuestion: "question",
|
||||
}
|
||||
|
||||
// CategoryFromString converts category names to ints (generated from categoryNames)
|
||||
var CategoryFromString map[string]int
|
||||
|
||||
func init() {
|
||||
// Generate reverse map from categoryNames (single source of truth)
|
||||
CategoryFromString = make(map[string]int, len(categoryNames))
|
||||
for catInt, name := range categoryNames {
|
||||
CategoryFromString[name] = catInt
|
||||
}
|
||||
}
|
||||
|
||||
// CategoryKey returns the translation key for a category (e.g. "category003")
|
||||
func CategoryKey(cat int) string {
|
||||
return fmt.Sprintf("category%03d", cat)
|
||||
}
|
||||
|
||||
// CategoryTypes maps category names to their valid type values
|
||||
var CategoryTypes = map[string][]string{
|
||||
"imaging": {"study", "series", "slice"},
|
||||
|
|
|
|||
211
lib/v2.go
211
lib/v2.go
|
|
@ -9,6 +9,7 @@ import (
|
|||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -1175,6 +1176,216 @@ func GenomeGetVariantsByTier(ctx *AccessContext, dossierID, tierID string) ([]Ge
|
|||
return variants, nil
|
||||
}
|
||||
|
||||
// GenomeMatch represents a single genome query result
|
||||
type GenomeMatch struct {
|
||||
RSID string `json:"rsid"`
|
||||
Genotype string `json:"genotype"`
|
||||
Gene string `json:"gene,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// GenomeQueryResult is the response from GenomeQuery
|
||||
type GenomeQueryResult struct {
|
||||
Matches []GenomeMatch `json:"matches"`
|
||||
Returned int `json:"returned"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// GenomeQueryOpts are the filter/sort/pagination options for GenomeQuery
|
||||
type GenomeQueryOpts struct {
|
||||
Category string
|
||||
Search string
|
||||
Gene string // comma-separated
|
||||
RSIDs []string
|
||||
MinMagnitude float64
|
||||
IncludeHidden bool
|
||||
Sort string // "magnitude" (default), "gene", "rsid"
|
||||
Offset int
|
||||
Limit int
|
||||
}
|
||||
|
||||
// GenomeQuery queries genome variants for a dossier with filtering, sorting, and pagination.
|
||||
func GenomeQuery(dossierID string, opts GenomeQueryOpts) (*GenomeQueryResult, error) {
|
||||
sysCtx := &AccessContext{IsSystem: true}
|
||||
|
||||
extraction, err := GenomeGetExtraction(sysCtx, dossierID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GENOME_NO_EXTRACTION: %w", err)
|
||||
}
|
||||
|
||||
// Get tiers
|
||||
var tiers []GenomeTier
|
||||
tierCategories := make(map[string]string)
|
||||
|
||||
if opts.Category != "" {
|
||||
tier, err := GenomeGetTierByCategory(sysCtx, dossierID, extraction.EntryID, opts.Category)
|
||||
if err == nil {
|
||||
tiers = append(tiers, *tier)
|
||||
tierCategories[tier.TierID] = tier.Category
|
||||
}
|
||||
} else {
|
||||
tiers, _ = GenomeGetTiers(sysCtx, dossierID, extraction.EntryID)
|
||||
for _, t := range tiers {
|
||||
tierCategories[t.TierID] = t.Category
|
||||
}
|
||||
}
|
||||
|
||||
if len(tiers) == 0 {
|
||||
return &GenomeQueryResult{Matches: []GenomeMatch{}}, nil
|
||||
}
|
||||
|
||||
tierIDs := make([]string, len(tiers))
|
||||
for i, t := range tiers {
|
||||
tierIDs[i] = t.TierID
|
||||
}
|
||||
|
||||
variants, err := GenomeGetVariants(sysCtx, dossierID, tierIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GENOME_VARIANT_QUERY_FAILED: %w", err)
|
||||
}
|
||||
|
||||
// Parse genes
|
||||
var genes []string
|
||||
if opts.Gene != "" {
|
||||
for _, g := range strings.Split(opts.Gene, ",") {
|
||||
genes = append(genes, strings.TrimSpace(g))
|
||||
}
|
||||
}
|
||||
|
||||
limit := opts.Limit
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
if limit > 500 {
|
||||
limit = 500
|
||||
}
|
||||
|
||||
// Filter and collect
|
||||
var matches []GenomeMatch
|
||||
total := 0
|
||||
|
||||
for _, v := range variants {
|
||||
if len(opts.RSIDs) > 0 {
|
||||
found := false
|
||||
for _, r := range opts.RSIDs {
|
||||
if r == v.RSID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(genes) > 0 {
|
||||
found := false
|
||||
for _, g := range genes {
|
||||
if strings.EqualFold(v.Gene, g) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Search != "" {
|
||||
sl := strings.ToLower(opts.Search)
|
||||
if !strings.Contains(strings.ToLower(v.Gene), sl) &&
|
||||
!strings.Contains(strings.ToLower(v.Summary), sl) &&
|
||||
!strings.Contains(strings.ToLower(v.Subcategory), sl) &&
|
||||
!strings.Contains(strings.ToLower(v.RSID), sl) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if opts.MinMagnitude > 0 && v.Magnitude < opts.MinMagnitude {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine if variant would be hidden
|
||||
isHidden := v.Magnitude > 4.0 || strings.EqualFold(v.Repute, "bad")
|
||||
|
||||
// Targeted queries (specific rsIDs or gene) return redacted results
|
||||
// Broad queries skip hidden variants entirely
|
||||
targeted := len(opts.RSIDs) > 0 || len(genes) > 0
|
||||
|
||||
if isHidden && !opts.IncludeHidden && !targeted {
|
||||
continue
|
||||
}
|
||||
|
||||
total++
|
||||
if total <= opts.Offset {
|
||||
continue
|
||||
}
|
||||
if len(matches) >= limit {
|
||||
continue
|
||||
}
|
||||
|
||||
// Redact sensitive fields unless include_hidden is set
|
||||
redact := isHidden && !opts.IncludeHidden
|
||||
|
||||
match := GenomeMatch{
|
||||
RSID: v.RSID,
|
||||
Gene: v.Gene,
|
||||
Category: tierCategories[v.TierID],
|
||||
Subcategory: v.Subcategory,
|
||||
}
|
||||
if redact {
|
||||
match.Genotype = "hidden"
|
||||
match.Summary = "Sensitive variant hidden. Query with include_hidden=true to reveal."
|
||||
} else {
|
||||
match.Genotype = v.Genotype
|
||||
if v.Summary != "" {
|
||||
match.Summary = v.Summary
|
||||
}
|
||||
}
|
||||
if v.Magnitude > 0 {
|
||||
mag := v.Magnitude
|
||||
match.Magnitude = &mag
|
||||
}
|
||||
if v.Repute != "" {
|
||||
match.Repute = v.Repute
|
||||
}
|
||||
matches = append(matches, match)
|
||||
}
|
||||
|
||||
// Sort
|
||||
switch opts.Sort {
|
||||
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:
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
return &GenomeQueryResult{
|
||||
Matches: matches,
|
||||
Returned: len(matches),
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// --- HELPERS ---
|
||||
|
||||
func deleteByIDs(table, col string, ids []string) error {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"inou/lib"
|
||||
)
|
||||
|
||||
// MCP Tool Implementations
|
||||
|
|
@ -198,33 +201,23 @@ func mcpGetCategories(accessToken, dossier, typ, category string) (string, error
|
|||
}
|
||||
|
||||
func mcpQueryGenome(accessToken, dossier, gene, search, category, rsids string, minMag float64, includeHidden bool) (string, error) {
|
||||
params := map[string]string{"dossier": dossier}
|
||||
if gene != "" {
|
||||
params["gene"] = gene
|
||||
}
|
||||
if search != "" {
|
||||
params["search"] = search
|
||||
}
|
||||
if category != "" {
|
||||
params["category"] = category
|
||||
}
|
||||
var rsidList []string
|
||||
if rsids != "" {
|
||||
params["rsids"] = rsids
|
||||
}
|
||||
if minMag > 0 {
|
||||
params["min_magnitude"] = strconv.FormatFloat(minMag, 'f', -1, 64)
|
||||
}
|
||||
if includeHidden {
|
||||
params["include_hidden"] = "true"
|
||||
rsidList = strings.Split(rsids, ",")
|
||||
}
|
||||
|
||||
body, err := mcpAPICall(accessToken, "/api/genome", params)
|
||||
result, err := lib.GenomeQuery(dossier, lib.GenomeQueryOpts{
|
||||
Category: category,
|
||||
Search: search,
|
||||
Gene: gene,
|
||||
RSIDs: rsidList,
|
||||
MinMagnitude: minMag,
|
||||
IncludeHidden: includeHidden,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var data interface{}
|
||||
json.Unmarshal(body, &data)
|
||||
pretty, _ := json.MarshalIndent(data, "", " ")
|
||||
pretty, _ := json.MarshalIndent(result, "", " ")
|
||||
return string(pretty), nil
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue