package lib import ( "bytes" "encoding/json" "fmt" "image" "image/color" "image/png" "os" "path/filepath" "strings" "time" ) // ============================================================================ // V2 API - Clean data layer using Store // ============================================================================ // // RBAC ENFORCEMENT: // All data access functions take AccessContext as first parameter. // Permission checks happen HERE at the lowest level - there is NO WAY to bypass. // // Rules: // - ctx == nil or ctx.IsSystem → allow (for internal/system operations) // - ctx.AccessorID == dossierID → allow (owner) // - Otherwise → check access grants in database // // ============================================================================ // accessorIDFromContext extracts accessorID from AccessContext for RBAC checks // Returns SystemAccessorID for system context (explicit backdoor with audit trail) func accessorIDFromContext(ctx *AccessContext) string { if ctx == nil || ctx.IsSystem { return SystemAccessorID } return ctx.AccessorID } // --- ENTRY --- type EntryFilter struct { DossierID string Type string Value string FromDate int64 ToDate int64 Limit int } // EntryWrite creates/updates entries. Requires write permission on parent. func EntryWrite(ctx *AccessContext, entries ...*Entry) error { if len(entries) == 0 { return nil } // RBAC: Check write permission for each entry for _, e := range entries { if e.DossierID == "" { return fmt.Errorf("entry missing dossier_id") } // Check write on parent (or root if no parent) if err := checkAccess(accessorIDFromContext(ctx), e.DossierID, e.ParentID, 'w'); err != nil { return err } } for _, e := range entries { if e.EntryID == "" { e.EntryID = NewID() } } if len(entries) == 1 { return Save("entries", entries[0]) } return Save("entries", entries) } // EntryRemove deletes entries. Requires delete permission. func EntryRemove(ctx *AccessContext, ids ...string) error { // RBAC: Check delete permission for each entry for _, id := range ids { e, err := entryGetRaw(id) if err != nil { continue // Entry doesn't exist, skip } if err := checkAccess(accessorIDFromContext(ctx), e.DossierID, id, 'd'); err != nil { return err } } return deleteByIDs("entries", "entry_id", ids) } // EntryRemoveByDossier removes all entries for a dossier. Requires delete permission on dossier root. func EntryRemoveByDossier(ctx *AccessContext, dossierID string) error { // RBAC: Check delete permission on dossier root if err := checkAccess(accessorIDFromContext(ctx), dossierID, "", 'd'); err != nil { return err } var entries []*Entry if err := Query("SELECT entry_id FROM entries WHERE dossier_id = ?", []any{dossierID}, &entries); err != nil { return err } for _, e := range entries { if err := Delete("entries", "entry_id", e.EntryID); err != nil { return err } } return nil } // EntryGet retrieves an entry. Requires read permission. func EntryGet(ctx *AccessContext, id string) (*Entry, error) { e, err := entryGetRaw(id) if err != nil { return nil, err } // RBAC: Check read permission if err := checkAccess(accessorIDFromContext(ctx), e.DossierID, id, 'r'); err != nil { return nil, err } return e, nil } // entryGetRaw retrieves an entry without permission check (internal use only) func entryGetRaw(id string) (*Entry, error) { e := &Entry{} return e, Load("entries", id, e) } // EntryList retrieves entries. Requires read permission on parent/dossier. // accessorID: who is asking (SystemAccessorID = internal operations) // dossierID comes from f.DossierID func EntryList(accessorID string, parent string, category int, f *EntryFilter) ([]*Entry, error) { // RBAC: Determine dossier and check read permission dossierID := "" if f != nil { dossierID = f.DossierID } if dossierID == "" && parent != "" { // Get dossier from parent entry if p, err := entryGetRaw(parent); err == nil { dossierID = p.DossierID } } if dossierID != "" { if err := checkAccess(accessorID, dossierID, parent, 'r'); err != nil { return nil, err } } q := "SELECT * FROM entries WHERE 1=1" args := []any{} if category > 0 { q += " AND category = ?" args = append(args, category) } if parent == "" { q += " AND (parent_id IS NULL OR parent_id = '')" } else { q += " AND parent_id = ?" args = append(args, parent) } if f != nil { if f.DossierID != "" { q += " AND dossier_id = ?" args = append(args, f.DossierID) } if f.Type != "" { q += " AND type = ?" args = append(args, CryptoEncrypt(f.Type)) } if f.Value != "" { q += " AND value = ?" args = append(args, CryptoEncrypt(f.Value)) } if f.FromDate > 0 { q += " AND timestamp >= ?" args = append(args, f.FromDate) } if f.ToDate > 0 { q += " AND timestamp < ?" args = append(args, f.ToDate) } } q += " ORDER BY ordinal" if f != nil && f.Limit > 0 { q += fmt.Sprintf(" LIMIT %d", f.Limit) } var result []*Entry err := Query(q, args, &result) return result, err } // --- DOSSIER --- type DossierFilter struct { EmailHash string DateOfBirth string Limit int } // DossierWrite creates/updates dossiers. Requires manage permission (or nil ctx for system). func DossierWrite(ctx *AccessContext, dossiers ...*Dossier) error { if len(dossiers) == 0 { return nil } // RBAC: For existing dossiers, check manage permission for _, d := range dossiers { if d.DossierID != "" { // Update - need manage permission (unless creating own or system) if err := checkAccess(accessorIDFromContext(ctx), d.DossierID, "", 'm'); err != nil { return err } } // New dossiers (no ID) are allowed - they'll get assigned an ID } for _, d := range dossiers { if d.DossierID == "" { d.DossierID = NewID() } // Format DOB to encrypted string if !d.DOB.IsZero() { d.DateOfBirth = d.DOB.Format("2006-01-02") } } if len(dossiers) == 1 { return Save("dossiers", dossiers[0]) } return Save("dossiers", dossiers) } // DossierRemove deletes dossiers. Requires manage permission. func DossierRemove(ctx *AccessContext, ids ...string) error { // RBAC: Check manage permission for each dossier for _, id := range ids { if err := checkAccess(accessorIDFromContext(ctx), id, "", 'm'); err != nil { return err } } return deleteByIDs("dossiers", "dossier_id", ids) } // DossierGet retrieves a dossier. Requires read permission. func DossierGet(ctx *AccessContext, id string) (*Dossier, error) { // RBAC: Check read permission if err := checkAccess(accessorIDFromContext(ctx), id, "", 'r'); err != nil { return nil, err } return dossierGetRaw(id) } // dossierGetRaw retrieves a dossier without permission check (internal use only) func dossierGetRaw(id string) (*Dossier, error) { d := &Dossier{} if err := Load("dossiers", id, d); err != nil { return nil, err } // Parse DOB from encrypted string if d.DateOfBirth != "" { d.DOB, _ = time.Parse("2006-01-02", d.DateOfBirth) } return d, nil } // DossierList retrieves dossiers. System only (for security - lists all dossiers). func DossierList(ctx *AccessContext, f *DossierFilter) ([]*Dossier, error) { // RBAC: Only system context can list all dossiers if ctx != nil && !ctx.IsSystem { return nil, ErrAccessDenied } q := "SELECT * FROM dossiers WHERE 1=1" args := []any{} if f != nil { if f.EmailHash != "" { q += " AND email_hash = ?" args = append(args, f.EmailHash) } if f.DateOfBirth != "" { q += " AND date_of_birth = ?" args = append(args, CryptoEncrypt(f.DateOfBirth)) } if f.Limit > 0 { q += fmt.Sprintf(" LIMIT %d", f.Limit) } } var result []*Dossier err := Query(q, args, &result) return result, err } // DossierGetByEmail retrieves a dossier by email. System only (auth flow). func DossierGetByEmail(ctx *AccessContext, email string) (*Dossier, error) { // RBAC: Only system context can lookup by email (for auth) if ctx != nil && !ctx.IsSystem { return nil, ErrAccessDenied } email = strings.ToLower(strings.TrimSpace(email)) if email == "" { return nil, fmt.Errorf("empty email") } q := "SELECT * FROM dossiers WHERE email = ? LIMIT 1" var result []*Dossier if err := Query(q, []any{CryptoEncrypt(email)}, &result); err != nil { return nil, err } if len(result) == 0 { return nil, fmt.Errorf("not found") } return result[0], nil } // DossierGetBySessionToken retrieves a dossier by session token. No RBAC (auth flow). func DossierGetBySessionToken(token string) *Dossier { if token == "" { return nil } q := "SELECT * FROM dossiers WHERE session_token = ? LIMIT 1" var result []*Dossier if err := Query(q, []any{CryptoEncrypt(token)}, &result); err != nil { return nil } if len(result) == 0 { return nil } return result[0] } // --- LEGACY ACCESS (dossier_access table) --- // TODO: Migrate to new RBAC access table type AccessFilter struct { AccessorID string TargetID string Status *int } func AccessWrite(records ...*DossierAccess) error { if len(records) == 0 { return nil } for _, r := range records { if r.AccessID == "" { r.AccessID = NewID() } } if len(records) == 1 { return Save("dossier_access", records[0]) } return Save("dossier_access", records) } func AccessRemove(accessorID, targetID string) error { access, err := AccessGet(accessorID, targetID) if err != nil { return err } return Delete("dossier_access", "access_id", access.AccessID) } func AccessGet(accessorID, targetID string) (*DossierAccess, error) { q := "SELECT * FROM dossier_access WHERE accessor_dossier_id = ? AND target_dossier_id = ?" var result []*DossierAccess if err := Query(q, []any{accessorID, targetID}, &result); err != nil { return nil, err } if len(result) == 0 { return nil, fmt.Errorf("not found") } return result[0], nil } func AccessList(f *AccessFilter) ([]*DossierAccess, error) { q := "SELECT * FROM dossier_access WHERE 1=1" args := []any{} if f != nil { if f.AccessorID != "" { q += " AND accessor_dossier_id = ?" args = append(args, f.AccessorID) } if f.TargetID != "" { q += " AND target_dossier_id = ?" args = append(args, f.TargetID) } if f.Status != nil { q += " AND status = ?" args = append(args, *f.Status) } } var result []*DossierAccess err := Query(q, args, &result) return result, err } // --- AUDIT --- type AuditFilter struct { ActorID string TargetID string Action string FromDate int64 ToDate int64 Limit int } func AuditWrite(entries ...*AuditEntry) error { if len(entries) == 0 { return nil } for _, a := range entries { if a.AuditID == "" { a.AuditID = NewID() } } if len(entries) == 1 { return Save("audit", entries[0]) } return Save("audit", entries) } func AuditList(f *AuditFilter) ([]*AuditEntry, error) { q := "SELECT * FROM audit WHERE 1=1" args := []any{} if f != nil { if f.ActorID != "" { q += " AND actor1_id = ?" args = append(args, f.ActorID) } if f.TargetID != "" { q += " AND target_id = ?" args = append(args, f.TargetID) } if f.Action != "" { q += " AND action = ?" args = append(args, CryptoEncrypt(f.Action)) } if f.FromDate > 0 { q += " AND timestamp >= ?" args = append(args, f.FromDate) } if f.ToDate > 0 { q += " AND timestamp < ?" args = append(args, f.ToDate) } } q += " ORDER BY timestamp DESC" if f != nil && f.Limit > 0 { q += fmt.Sprintf(" LIMIT %d", f.Limit) } var result []*AuditEntry err := Query(q, args, &result) return result, err } // --- PROMPT --- type PromptFilter struct { DossierID string Category string Type string ActiveOnly bool Limit int } func PromptWrite(prompts ...*Prompt) error { if len(prompts) == 0 { return nil } for _, p := range prompts { if p.PromptID == "" { p.PromptID = NewID() } } if len(prompts) == 1 { return Save("prompts", prompts[0]) } return Save("prompts", prompts) } func PromptRemove(ids ...string) error { return deleteByIDs("prompts", "prompt_id", ids) } func PromptList(f *PromptFilter) ([]*Prompt, error) { q := "SELECT * FROM prompts WHERE 1=1" args := []any{} if f != nil { if f.DossierID != "" { q += " AND dossier_id = ?" args = append(args, f.DossierID) } if f.Category != "" { q += " AND category = ?" args = append(args, CryptoEncrypt(f.Category)) } if f.Type != "" { q += " AND type = ?" args = append(args, CryptoEncrypt(f.Type)) } if f.ActiveOnly { q += " AND active = 1 AND dismissed = 0" } } q += " ORDER BY active DESC, time_of_day, created_at" if f != nil && f.Limit > 0 { q += fmt.Sprintf(" LIMIT %d", f.Limit) } var result []*Prompt err := Query(q, args, &result) return result, err } // --- IMAGE --- type ImageOpts struct { WC, WW float64 // window center/width (0 = defaults) Zoom float64 // unused PanX, PanY float64 // unused } // ImageGet retrieves an image. Requires read permission. func ImageGet(ctx *AccessContext, id string, opts *ImageOpts) ([]byte, error) { e, err := entryGetRaw(id) if err != nil { return nil, err } // RBAC: Check read permission if err := checkAccess(accessorIDFromContext(ctx), e.DossierID, id, 'r'); err != nil { return nil, err } var data struct { WindowCenter float64 `json:"window_center"` WindowWidth float64 `json:"window_width"` PixelMin int `json:"pixel_min"` PixelMax int `json:"pixel_max"` RescaleSlope float64 `json:"rescale_slope"` RescaleIntercept float64 `json:"rescale_intercept"` } json.Unmarshal([]byte(e.Data), &data) slope := data.RescaleSlope if slope == 0 { slope = 1 } center, width := data.WindowCenter, data.WindowWidth if data.RescaleIntercept != 0 { center = (center - data.RescaleIntercept) / slope width = width / slope } if center == 0 && width == 0 { center = float64(data.PixelMin+data.PixelMax) / 2 width = float64(data.PixelMax - data.PixelMin) if width == 0 { width = 1 } } if opts != nil { if opts.WC != 0 { center = opts.WC } if opts.WW != 0 { width = opts.WW } } dec, err := objectReadRaw(e.DossierID, id) if err != nil { return nil, err } img, err := png.Decode(bytes.NewReader(dec)) if err != nil { return nil, err } g16, ok := img.(*image.Gray16) if !ok { return nil, fmt.Errorf("not 16-bit grayscale") } low, high := center-width/2, center+width/2 lut := make([]uint8, 65536) for i := range lut { if float64(i) <= low { lut[i] = 0 } else if float64(i) >= high { lut[i] = 255 } else { lut[i] = uint8((float64(i) - low) * 255 / width) } } bounds := g16.Bounds() out := image.NewGray(bounds) for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { out.SetGray(x, y, color.Gray{Y: lut[g16.Gray16At(x, y).Y]}) } } var buf bytes.Buffer png.Encode(&buf, out) return buf.Bytes(), nil } // --- OBJECT STORAGE --- // ObjectWrite encrypts and writes data to the object store. Requires write permission. func ObjectWrite(ctx *AccessContext, dossierID, entryID string, data []byte) error { // RBAC: Check write permission if err := checkAccess(accessorIDFromContext(ctx), dossierID, entryID, 'w'); err != nil { return err } path := ObjectPath(dossierID, entryID) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } encrypted := CryptoEncryptBytes(data) return os.WriteFile(path, encrypted, 0644) } // ObjectRead reads and decrypts data from the object store. Requires read permission. func ObjectRead(ctx *AccessContext, dossierID, entryID string) ([]byte, error) { // RBAC: Check read permission if err := checkAccess(accessorIDFromContext(ctx), dossierID, entryID, 'r'); err != nil { return nil, err } raw, err := os.ReadFile(ObjectPath(dossierID, entryID)) if err != nil { return nil, err } return CryptoDecryptBytes(raw) } // objectReadRaw reads object without permission check (internal use only, e.g. ImageGet) func objectReadRaw(dossierID, entryID string) ([]byte, error) { raw, err := os.ReadFile(ObjectPath(dossierID, entryID)) if err != nil { return nil, err } return CryptoDecryptBytes(raw) } // ObjectRemove deletes an object from the store. Requires delete permission. func ObjectRemove(ctx *AccessContext, dossierID, entryID string) error { // RBAC: Check delete permission if err := checkAccess(accessorIDFromContext(ctx), dossierID, entryID, 'd'); err != nil { return err } return os.Remove(ObjectPath(dossierID, entryID)) } // ObjectRemoveByDossier removes all objects for a dossier. Requires delete permission. func ObjectRemoveByDossier(ctx *AccessContext, dossierID string) error { // RBAC: Check delete permission on dossier root if err := checkAccess(accessorIDFromContext(ctx), dossierID, "", 'd'); err != nil { return err } return os.RemoveAll(filepath.Join(ObjectDir, dossierID)) } // DossierListAccessible is now in access.go with RBAC enforcement // DossierGetByEmailHash returns a dossier by email_hash. System only. func DossierGetByEmailHash(ctx *AccessContext, emailHash string) (*Dossier, error) { dossiers, err := DossierList(ctx, &DossierFilter{EmailHash: emailHash, Limit: 1}) if err != nil { return nil, err } if len(dossiers) == 0 { return nil, fmt.Errorf("not found") } return dossiers[0], nil } // DossierGetFirst returns the first dossier (for dev/test purposes only). System only. func DossierGetFirst(ctx *AccessContext) (*Dossier, error) { dossiers, err := DossierList(ctx, &DossierFilter{Limit: 1}) if err != nil { return nil, err } if len(dossiers) == 0 { return nil, fmt.Errorf("no dossiers") } return dossiers[0], nil } // AccessListByTargetWithNames returns access records with accessor names joined from dossiers func AccessListByTargetWithNames(targetID string) ([]map[string]interface{}, error) { // Get access records via store.go accessList, err := AccessList(&AccessFilter{TargetID: targetID}) if err != nil { return nil, err } var result []map[string]interface{} for _, a := range accessList { name := "" if d, err := DossierGet(nil, a.AccessorDossierID); err == nil && d != nil { name = d.Name } result = append(result, map[string]interface{}{ "accessor_id": a.AccessorDossierID, "name": name, "relation": a.Relation, "is_care_receiver": a.IsCareReceiver, "can_edit": a.CanEdit, }) } return result, nil } // PromptDistinctTypes returns distinct category/type pairs for a dossier's active prompts func PromptDistinctTypes(dossierID string) (map[string][]string, error) { var prompts []*Prompt if err := Query("SELECT * FROM prompts WHERE dossier_id = ? AND active = 1", []any{dossierID}, &prompts); err != nil { return nil, err } // Extract distinct category/type pairs seen := make(map[string]bool) result := make(map[string][]string) for _, p := range prompts { key := p.Category + "|" + p.Type if !seen[key] && p.Category != "" && p.Type != "" { seen[key] = true result[p.Category] = append(result[p.Category], p.Type) } } return result, nil } // --- RBAC PERMISSIONS --- type PermissionFilter struct { DossierID string // whose data GranteeID string // who has access EntryID string // specific entry Role string // filter by role name } // AccessWrite saves one or more access grants func AccessGrantWrite(grants ...*Access) error { if len(grants) == 0 { return nil } for _, a := range grants { if a.AccessID == "" { a.AccessID = NewID() } if a.CreatedAt == 0 { a.CreatedAt = time.Now().Unix() } } if len(grants) == 1 { return Save("access", grants[0]) } return Save("access", grants) } // AccessGrantRemove removes access grants by ID func AccessGrantRemove(ids ...string) error { for _, id := range ids { if err := Delete("access", "access_id", id); err != nil { return err } } return nil } // AccessGrantGet retrieves a single access grant by ID func AccessGrantGet(id string) (*Access, error) { a := &Access{} return a, Load("access", id, a) } // AccessGrantList retrieves access grants with optional filtering func AccessGrantList(f *PermissionFilter) ([]*Access, error) { q := "SELECT * FROM access WHERE 1=1" args := []any{} if f != nil { if f.DossierID != "" { q += " AND dossier_id = ?" args = append(args, f.DossierID) } if f.GranteeID != "" { q += " AND grantee_id = ?" args = append(args, f.GranteeID) } if f.EntryID != "" { q += " AND entry_id = ?" args = append(args, f.EntryID) } if f.Role != "" { q += " AND role = ?" args = append(args, CryptoEncrypt(f.Role)) } } q += " ORDER BY created_at DESC" var result []*Access err := Query(q, args, &result) return result, err } // AccessGrantsForGrantee returns all grants for a specific grantee, grouped by dossier func AccessGrantsForGrantee(granteeID string) (map[string][]*Access, error) { grants, err := AccessGrantList(&PermissionFilter{GranteeID: granteeID}) if err != nil { return nil, err } result := make(map[string][]*Access) for _, g := range grants { result[g.DossierID] = append(result[g.DossierID], g) } return result, nil } // AccessRoleTemplates returns role templates (grantee_id is null) for a dossier // Pass empty dossierID for system-wide templates func AccessRoleTemplates(dossierID string) ([]*Access, error) { q := "SELECT * FROM access WHERE grantee_id IS NULL OR grantee_id = ''" args := []any{} if dossierID != "" { q += " AND (dossier_id = ? OR dossier_id IS NULL OR dossier_id = '')" args = append(args, dossierID) } else { q += " AND (dossier_id IS NULL OR dossier_id = '')" } q += " ORDER BY role, entry_id" var result []*Access err := Query(q, args, &result) return result, err } // AccessGrantRole copies role template entries for a specific grantee func AccessGrantRole(dossierID, granteeID, role string) error { // Get template entries for this role templates, err := AccessRoleTemplates(dossierID) if err != nil { return err } var grants []*Access for _, t := range templates { if t.Role != role { continue } grants = append(grants, &Access{ DossierID: dossierID, GranteeID: granteeID, EntryID: t.EntryID, Role: role, Ops: t.Ops, }) } if len(grants) == 0 { // No template found, create default read-only grant grants = append(grants, &Access{ DossierID: dossierID, GranteeID: granteeID, Role: role, Ops: "r", }) } return AccessGrantWrite(grants...) } // AccessRevokeAll removes all access grants for a grantee to a specific dossier func AccessRevokeAll(dossierID, granteeID string) error { // Query for matching grants, then delete each by PK grants, err := AccessGrantList(&PermissionFilter{DossierID: dossierID, GranteeID: granteeID}) if err != nil { return err } for _, g := range grants { if err := Delete("access", "access_id", g.AccessID); err != nil { return err } } return nil } // AccessRevokeEntry removes a specific entry grant for a grantee func AccessRevokeEntry(dossierID, granteeID, entryID string) error { grants, err := AccessGrantList(&PermissionFilter{DossierID: dossierID, GranteeID: granteeID, EntryID: entryID}) if err != nil { return err } for _, g := range grants { if err := Delete("access", "access_id", g.AccessID); err != nil { return err } } return nil } // --- GENOME QUERIES --- // Note: These functions use nil ctx for internal operations. When called from // API handlers, the API should perform its own RBAC check first. // GenomeGetExtraction returns the extraction entry for genome data func GenomeGetExtraction(ctx *AccessContext, dossierID string) (*Entry, error) { entries, err := EntryList(accessorIDFromContext(ctx), "", CategoryGenome, &EntryFilter{ DossierID: dossierID, Type: "extraction", Limit: 1, }) if err != nil || len(entries) == 0 { return nil, fmt.Errorf("no genome data") } return entries[0], nil } // GenomeTier represents a genome category tier type GenomeTier struct { TierID string Category string } // GenomeGetTiers returns all tier entries for a genome extraction func GenomeGetTiers(ctx *AccessContext, dossierID, extractionID string) ([]GenomeTier, error) { entries, err := EntryList(accessorIDFromContext(ctx), extractionID, CategoryGenome, &EntryFilter{ DossierID: dossierID, Type: "tier", }) if err != nil { return nil, err } var tiers []GenomeTier for _, e := range entries { tiers = append(tiers, GenomeTier{ TierID: e.EntryID, Category: e.Value, // Value holds category name }) } return tiers, nil } // GenomeGetTierByCategory returns a specific tier by category name func GenomeGetTierByCategory(ctx *AccessContext, dossierID, extractionID, category string) (*GenomeTier, error) { entries, err := EntryList(accessorIDFromContext(ctx), extractionID, CategoryGenome, &EntryFilter{ DossierID: dossierID, Type: "tier", Value: category, Limit: 1, }) if err != nil || len(entries) == 0 { return nil, fmt.Errorf("tier not found") } return &GenomeTier{ TierID: entries[0].EntryID, Category: entries[0].Value, }, nil } // GenomeVariant represents a genome variant with its metadata type GenomeVariant struct { EntryID string RSID string Genotype string Gene string Magnitude float64 Repute string Summary string Subcategory string TierID string } // GenomeGetVariants returns variants for specified tier IDs func GenomeGetVariants(ctx *AccessContext, dossierID string, tierIDs []string) ([]GenomeVariant, error) { if len(tierIDs) == 0 { return nil, nil } // Query entries for each tier and deduplicate by type (rsid) seen := make(map[string]bool) var variants []GenomeVariant for _, tierID := range tierIDs { entries, err := EntryList(accessorIDFromContext(ctx), tierID, CategoryGenome, &EntryFilter{DossierID: dossierID}) if err != nil { continue } for _, e := range entries { // Deduplicate by type (rsid) if seen[e.Type] { continue } seen[e.Type] = true var data struct { Mag float64 `json:"mag"` Rep string `json:"rep"` Sum string `json:"sum"` Sub string `json:"sub"` } json.Unmarshal([]byte(e.Data), &data) variants = append(variants, GenomeVariant{ EntryID: e.EntryID, RSID: e.Type, Genotype: e.Value, Gene: e.Tags, Magnitude: data.Mag, Repute: data.Rep, Summary: data.Sum, Subcategory: data.Sub, TierID: tierID, }) } } return variants, nil } // GenomeGetVariantsByTier returns variants for a specific tier func GenomeGetVariantsByTier(ctx *AccessContext, dossierID, tierID string) ([]GenomeVariant, error) { entries, err := EntryList(accessorIDFromContext(ctx), tierID, CategoryGenome, &EntryFilter{DossierID: dossierID}) if err != nil { return nil, err } var variants []GenomeVariant for _, e := range entries { var data struct { Mag float64 `json:"mag"` Rep string `json:"rep"` Sum string `json:"sum"` Sub string `json:"sub"` } json.Unmarshal([]byte(e.Data), &data) variants = append(variants, GenomeVariant{ EntryID: e.EntryID, RSID: e.Type, Genotype: e.Value, Gene: e.Tags, Magnitude: data.Mag, Repute: data.Rep, Summary: data.Sum, Subcategory: data.Sub, TierID: tierID, }) } return variants, nil } // --- HELPERS --- func deleteByIDs(table, col string, ids []string) error { for _, id := range ids { if err := Delete(table, col, id); err != nil { return err } } return nil }