341 lines
10 KiB
Go
341 lines
10 KiB
Go
package lib
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// Category enum for entries and prompts
|
|
const (
|
|
CategoryAll = iota
|
|
CategoryImaging
|
|
CategoryDocument
|
|
CategoryLab
|
|
CategoryGenome
|
|
CategoryUpload
|
|
CategoryConsultation
|
|
CategoryDiagnosis
|
|
CategoryVital
|
|
CategoryExercise
|
|
CategoryMedication
|
|
CategorySupplement
|
|
CategoryNutrition
|
|
CategoryFertility
|
|
CategorySymptom
|
|
CategoryNote
|
|
CategoryHistory
|
|
CategoryFamilyHistory
|
|
CategorySurgery
|
|
CategoryHospital
|
|
CategoryBirth
|
|
CategoryDevice
|
|
CategoryTherapy
|
|
CategoryAssessment
|
|
CategoryProvider
|
|
CategoryQuestion
|
|
)
|
|
|
|
// GenomeTier enum - ordered by "fun" (scary last, other always last)
|
|
const (
|
|
GenomeTierTraits = iota + 1
|
|
GenomeTierAncestry
|
|
GenomeTierLongevity
|
|
GenomeTierMetabolism
|
|
GenomeTierMedication
|
|
GenomeTierMentalHealth
|
|
GenomeTierNeurological
|
|
GenomeTierFertility
|
|
GenomeTierBlood
|
|
GenomeTierCardiovascular
|
|
GenomeTierAutoimmune
|
|
GenomeTierDisease
|
|
GenomeTierCancer
|
|
GenomeTierOther = 99
|
|
)
|
|
|
|
// GenomeTierFromString maps tier category names to ints
|
|
var GenomeTierFromString = map[string]int{
|
|
"traits": GenomeTierTraits,
|
|
"ancestry": GenomeTierAncestry,
|
|
"longevity": GenomeTierLongevity,
|
|
"metabolism": GenomeTierMetabolism,
|
|
"medication": GenomeTierMedication,
|
|
"mental_health": GenomeTierMentalHealth,
|
|
"neurological": GenomeTierNeurological,
|
|
"fertility": GenomeTierFertility,
|
|
"blood": GenomeTierBlood,
|
|
"cardiovascular": GenomeTierCardiovascular,
|
|
"autoimmune": GenomeTierAutoimmune,
|
|
"disease": GenomeTierDisease,
|
|
"cancer": GenomeTierCancer,
|
|
"other": GenomeTierOther,
|
|
}
|
|
|
|
// categoryNames maps category ints to their English names (source of truth)
|
|
var categoryNames = map[int]string{
|
|
CategoryImaging: "imaging",
|
|
CategoryDocument: "document",
|
|
CategoryLab: "lab",
|
|
CategoryGenome: "genome",
|
|
CategoryUpload: "upload",
|
|
CategoryConsultation: "consultation",
|
|
CategoryDiagnosis: "diagnosis",
|
|
CategoryVital: "vital",
|
|
CategoryExercise: "exercise",
|
|
CategoryMedication: "medication",
|
|
CategorySupplement: "supplement",
|
|
CategoryNutrition: "nutrition",
|
|
CategoryFertility: "fertility",
|
|
CategorySymptom: "symptom",
|
|
CategoryNote: "note",
|
|
CategoryHistory: "history",
|
|
CategoryFamilyHistory: "family_history",
|
|
CategorySurgery: "surgery",
|
|
CategoryHospital: "hospitalization",
|
|
CategoryBirth: "birth",
|
|
CategoryDevice: "device",
|
|
CategoryTherapy: "therapy",
|
|
CategoryAssessment: "assessment",
|
|
CategoryProvider: "provider",
|
|
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"},
|
|
"document": {"radiology_report", "ultrasound", "other"},
|
|
"lab": {"lab_report", "result"},
|
|
"genome": {"extraction", "tier", "variant"},
|
|
"upload": {"pending", "processed"},
|
|
"consultation": {"visit", "telehealth"},
|
|
"diagnosis": {"active", "resolved"},
|
|
"vital": {"weight", "height", "blood_pressure", "heart_rate", "temperature", "oxygen", "glucose"},
|
|
"exercise": {"activity", "workout"},
|
|
"medication": {"prescription", "otc"},
|
|
"supplement": {"vitamin", "mineral", "herbal"},
|
|
"nutrition": {"meal", "snack"},
|
|
"fertility": {"cycle", "ovulation", "pregnancy"},
|
|
"symptom": {"acute", "chronic"},
|
|
"note": {"general", "clinical"},
|
|
"history": {"medical", "surgical"},
|
|
"family_history": {"parent", "sibling", "grandparent"},
|
|
"surgery": {"inpatient", "outpatient"},
|
|
"hospitalization": {"admission", "discharge"},
|
|
"birth": {"delivery", "newborn"},
|
|
"device": {"implant", "wearable"},
|
|
"therapy": {"physical", "occupational", "speech"},
|
|
"assessment": {"screening", "evaluation"},
|
|
"provider": {"physician", "specialist", "nurse"},
|
|
"question": {"inquiry", "followup"},
|
|
}
|
|
|
|
// Categories returns all category definitions
|
|
func Categories() []struct {
|
|
ID int
|
|
Name string
|
|
Types []string
|
|
} {
|
|
var result []struct {
|
|
ID int
|
|
Name string
|
|
Types []string
|
|
}
|
|
for i := 1; i <= CategoryQuestion; i++ {
|
|
name := categoryNames[i]
|
|
result = append(result, struct {
|
|
ID int
|
|
Name string
|
|
Types []string
|
|
}{i, name, CategoryTypes[name]})
|
|
}
|
|
return result
|
|
}
|
|
|
|
// CategoryName returns the string name for a category int
|
|
func CategoryName(cat int) string {
|
|
if name, ok := categoryNames[cat]; ok {
|
|
return name
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// ObjectDir is the base path for encrypted object storage
|
|
const ObjectDir = "/tank/inou/objects"
|
|
|
|
// ObjectPath returns the storage path for an entry: {base}/{dossier_hex}/{first_byte}/{entry_hex}
|
|
func ObjectPath(dossierID, entryID string) string {
|
|
return filepath.Join(ObjectDir, dossierID, entryID[0:2], entryID)
|
|
}
|
|
|
|
|
|
// Access represents a permission grant
|
|
type Access struct {
|
|
AccessID string `db:"AccessID,pk"`
|
|
DossierID string `db:"DossierID"` // whose data
|
|
GranteeID string `db:"GranteeID"` // who gets access
|
|
EntryID string `db:"EntryID"` // specific entry (empty = root level)
|
|
Relation int `db:"Relation"` // relationship type
|
|
Ops int `db:"Ops"` // bitmask: 1=read, 2=write, 4=delete, 8=manage
|
|
CreatedAt int64 `db:"CreatedAt"`
|
|
}
|
|
|
|
// CanRead returns true if ops includes read bit (1)
|
|
func CanRead(ops int) bool {
|
|
return ops&1 != 0
|
|
}
|
|
|
|
// CanWrite returns true if ops includes write bit (2)
|
|
func CanWrite(ops int) bool {
|
|
return ops&2 != 0
|
|
}
|
|
|
|
// CanDelete returns true if ops includes delete bit (4)
|
|
func CanDelete(ops int) bool {
|
|
return ops&4 != 0
|
|
}
|
|
|
|
// CanManage returns true if ops includes manage bit (8)
|
|
func CanManage(ops int) bool {
|
|
return ops&8 != 0
|
|
}
|
|
|
|
// MakeOps creates an ops bitmask from individual permissions
|
|
func MakeOps(read, write, delete, manage bool) int {
|
|
ops := 0
|
|
if read {
|
|
ops |= 1
|
|
}
|
|
if write {
|
|
ops |= 2
|
|
}
|
|
if delete {
|
|
ops |= 4
|
|
}
|
|
if manage {
|
|
ops |= 8
|
|
}
|
|
return ops
|
|
}
|
|
|
|
// DossierPreferences holds user preferences stored as JSON
|
|
type DossierPreferences struct {
|
|
Language string `json:"language,omitempty"`
|
|
Timezone string `json:"timezone,omitempty"`
|
|
WeightUnit string `json:"weight_unit,omitempty"`
|
|
HeightUnit string `json:"height_unit,omitempty"`
|
|
IsProvider bool `json:"is_provider,omitempty"`
|
|
ProviderName string `json:"provider_name,omitempty"`
|
|
AwayEnabled bool `json:"away_enabled,omitempty"`
|
|
AwayMessage string `json:"away_message,omitempty"`
|
|
}
|
|
|
|
// Dossier represents a user profile (decrypted)
|
|
type Dossier struct {
|
|
DossierID string `db:"DossierID,pk"`
|
|
Email string `db:"Email"`
|
|
Name string `db:"Name"`
|
|
DateOfBirth string `db:"DateOfBirth"`
|
|
DOB time.Time `db:"-"`
|
|
Sex int `db:"Sex"`
|
|
Phone string `db:"Phone"`
|
|
Preferences DossierPreferences `db:"Preferences"`
|
|
AuthCode int `db:"AuthCode"`
|
|
AuthCodeExpiresAt int64 `db:"AuthCodeExpiresAt"`
|
|
LastLogin int64 `db:"LastLogin"`
|
|
InvitedByDossierID string `db:"InvitedByDossierID"`
|
|
CreatedAt int64 `db:"CreatedAt"`
|
|
LastPullAt int64 `db:"LastPullAt"`
|
|
SessionToken string `db:"SessionToken"`
|
|
}
|
|
|
|
// SexKey returns the translation key for the sex enum (ISO/IEC 5218)
|
|
// 0=not known, 1=male, 2=female, 9=not applicable
|
|
func (d *Dossier) SexKey() string {
|
|
switch d.Sex {
|
|
case 1:
|
|
return "sex_male"
|
|
case 2:
|
|
return "sex_female"
|
|
case 9:
|
|
return "sex_na"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// Entry represents any data item (decrypted).
|
|
// Dossier profiles are entries with Category=0.
|
|
// IDs are string now, will be int64 when migration is complete.
|
|
type Entry struct {
|
|
EntryID string `db:"EntryID,pk"`
|
|
DossierID string `db:"DossierID"`
|
|
ParentID string `db:"ParentID"`
|
|
Category int `db:"Category"`
|
|
Type string `db:"Type"`
|
|
Value string `db:"Value"`
|
|
Summary string `db:"Summary"`
|
|
Ordinal int `db:"Ordinal"`
|
|
Timestamp int64 `db:"Timestamp"`
|
|
TimestampEnd int64 `db:"TimestampEnd"`
|
|
Status int `db:"Status"`
|
|
Tags string `db:"Tags"`
|
|
Data string `db:"Data"`
|
|
SearchKey string `db:"SearchKey"`
|
|
}
|
|
|
|
// AuditEntry represents an audit log entry
|
|
type AuditEntry struct {
|
|
AuditID string `db:"AuditID,pk"`
|
|
Actor1ID string `db:"Actor1ID"`
|
|
Actor2ID string `db:"Actor2ID"`
|
|
TargetID string `db:"TargetID"`
|
|
Action string `db:"Action"`
|
|
Details string `db:"Details"`
|
|
RelationID int `db:"RelationID"`
|
|
Timestamp int64 `db:"Timestamp"`
|
|
}
|
|
|
|
// Tracker represents a scheduled question or tracker (decrypted)
|
|
type Tracker struct {
|
|
TrackerID string `db:"TrackerID,pk"`
|
|
DossierID string `db:"DossierID"`
|
|
Category string `db:"Category"`
|
|
Type string `db:"Type"`
|
|
Question string `db:"Question"`
|
|
Frequency string `db:"Frequency"`
|
|
TimeOfDay string `db:"TimeOfDay"`
|
|
Schedule string `db:"Schedule"`
|
|
NextAsk int64 `db:"NextAsk"`
|
|
ExpiresAt int64 `db:"ExpiresAt"`
|
|
InputType string `db:"InputType"`
|
|
InputConfig string `db:"InputConfig"`
|
|
GroupName string `db:"GroupName"`
|
|
TriggerEntry int64 `db:"TriggerEntry"`
|
|
CreatedBy int64 `db:"CreatedBy"`
|
|
SourceInput string `db:"SourceInput"`
|
|
LastResponse string `db:"LastResponse"`
|
|
LastResponseRaw string `db:"LastResponseRaw"`
|
|
LastResponseAt int64 `db:"LastResponseAt"`
|
|
Dismissed bool `db:"Dismissed"`
|
|
Open bool `db:"Open"`
|
|
Active bool `db:"Active"`
|
|
CreatedAt int64 `db:"CreatedAt"`
|
|
UpdatedAt int64 `db:"UpdatedAt"`
|
|
} |