inou/lib/types.go

351 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)
}
// ParseID converts 16-char hex string to int64 (for legacy/token use)
func ParseID(s string) int64 {
var id int64
fmt.Sscanf(s, "%x", &id)
return id
}
// FormatID converts int64 to 16-char hex string (for legacy/token use)
func FormatID(id int64) string {
return fmt.Sprintf("%016x", id)
}
// Access represents a permission grant or role template
type Access struct {
AccessID string `db:"access_id,pk"`
DossierID string `db:"dossier_id"` // whose data
GranteeID string `db:"grantee_id"` // who gets access
EntryID string `db:"entry_id"` // specific entry (empty = root level)
Relation int `db:"relation"` // relationship type (0=self, 1=child, 2=parent, etc.)
Ops int `db:"ops"` // bitmask: 1=read, 2=write, 4=delete, 8=manage
CreatedAt int64 `db:"created_at"`
}
// 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
}
// Dossier represents a user profile (decrypted)
type Dossier struct {
DossierID string `db:"dossier_id,pk"`
EmailHash string `db:"email_hash"`
Email string `db:"email"`
Name string `db:"name"`
DateOfBirth string `db:"date_of_birth"` // encrypted YYYY-MM-DD
DOB time.Time `db:"-"` // parsed date, not stored
Sex int `db:"sex"`
Phone string `db:"phone"`
Language string `db:"language"`
Timezone string `db:"timezone"`
AuthCode int `db:"auth_code"`
AuthCodeExpiresAt int64 `db:"auth_code_expires_at"`
LastLogin int64 `db:"last_login"`
InvitedByDossierID string `db:"invited_by_dossier_id"`
CreatedAt int64 `db:"created_at"`
WeightUnit string `db:"weight_unit"`
HeightUnit string `db:"height_unit"`
LastPullAt int64 `db:"last_pull_at"`
IsProvider bool `db:"is_provider"`
ProviderName string `db:"provider_name"`
AwayMessage string `db:"away_message"`
AwayEnabled bool `db:"away_enabled"`
SessionToken string `db:"session_token"`
}
// 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)
type Entry struct {
EntryID string `db:"entry_id,pk"`
DossierID string `db:"dossier_id"`
ParentID string `db:"parent_id"`
ProductID string `db:"product_id"`
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:"timestamp_end"`
Status int `db:"status"`
Tags string `db:"tags"`
Data string `db:"data"`
SearchKey string `db:"search_key"` // LOINC (labs), gene (genome), modality (imaging) - encrypted
}
// Audit represents an audit log entry
type AuditEntry struct {
AuditID string `db:"audit_id,pk"`
Actor1ID string `db:"actor1_id"`
Actor2ID string `db:"actor2_id"`
TargetID string `db:"target_id"`
Action string `db:"action,encrypt"`
Details string `db:"details,encrypt"`
RelationID int `db:"relation_id"`
Timestamp int64 `db:"timestamp"`
}
// Prompt represents a scheduled question or tracker (decrypted)
type Tracker struct {
TrackerID string `db:"tracker_id,pk"`
DossierID string `db:"dossier_id"`
Category string `db:"category"`
Type string `db:"type"`
Question string `db:"question"`
Frequency string `db:"frequency"`
TimeOfDay string `db:"time_of_day"`
Schedule string `db:"schedule"`
NextAsk int64 `db:"next_ask"`
ExpiresAt int64 `db:"expires_at"`
InputType string `db:"input_type"`
InputConfig string `db:"input_config"`
GroupName string `db:"group_name"`
TriggerEntry int64 `db:"trigger_entry"`
CreatedBy int64 `db:"created_by"`
SourceInput string `db:"source_input"`
// Last response (for pre-filling)
LastResponse string `db:"last_response"`
LastResponseRaw string `db:"last_response_raw"`
LastResponseAt int64 `db:"last_response_at"`
// State
Dismissed bool `db:"dismissed"`
Open bool `db:"open"`
Active bool `db:"active"`
CreatedAt int64 `db:"created_at"`
UpdatedAt int64 `db:"updated_at"`
}