inou/cmd/import-lab/main.go

489 lines
12 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"inou/lib"
)
// MyChart JSON format
type MyChartResponse struct {
OrderName string `json:"orderName"`
Key string `json:"key"`
Results []MyChartResult `json:"results"`
}
type MyChartResult struct {
OrderMetadata MyChartOrderMeta `json:"orderMetadata"`
ResultComponents []MyChartComponent `json:"resultComponents"`
IsAbnormal bool `json:"isAbnormal"`
}
type MyChartOrderMeta struct {
OrderProviderName string `json:"orderProviderName"`
AuthorizingProviderName string `json:"authorizingProviderName"`
PrioritizedInstantISO string `json:"prioritizedInstantISO"`
ResultStatus string `json:"resultStatus"`
ResultType string `json:"resultType"`
ResultingLab struct {
Name string `json:"name"`
} `json:"resultingLab"`
SpecimensDisplay string `json:"specimensDisplay"`
AssociatedDiagnoses []string `json:"associatedDiagnoses"`
}
type MyChartComponent struct {
ComponentInfo struct {
Name string `json:"name"`
CommonName string `json:"commonName"`
Units string `json:"units"`
} `json:"componentInfo"`
ComponentResultInfo struct {
Value string `json:"value"`
NumericValue *float64 `json:"numericValue,omitempty"`
} `json:"componentResultInfo"`
ComponentComments struct {
HasContent bool `json:"hasContent"`
ContentAsString string `json:"contentAsString"`
} `json:"componentComments"`
}
// Quality Labs JSON format
type QualityLabsFile struct {
LabOrder struct {
OrderName string `json:"order_name"`
OrderKey string `json:"order_key"`
CollectionDate string `json:"collection_date"`
Provider string `json:"provider"`
LabName string `json:"lab_name"`
Specimen string `json:"specimen"`
} `json:"lab_order"`
LabTests []struct {
Name string `json:"name"`
Value string `json:"value"`
NumericValue *float64 `json:"numeric_value"`
Units *string `json:"units"`
} `json:"lab_tests"`
}
// Intermediate representation
type labOrder struct {
sourceKey string
orderName string
timestamp int64
localTime string // original ISO8601 with timezone offset
provider string
labName string
specimen string
source string
tests []labTest
}
type labTest struct {
sourceKey string
name string
commonName string
value string
numericValue *float64
unit string
}
func main() {
if len(os.Args) >= 2 && os.Args[1] == "--patch-time" {
if len(os.Args) < 4 {
fmt.Println("Usage: import-lab --patch-time <dossier-id> <path>")
os.Exit(1)
}
patchLocalTime(os.Args[2], os.Args[3])
return
}
if len(os.Args) >= 2 && os.Args[1] == "--populate-refs" {
if err := lib.Init(); err != nil {
fmt.Println("lib.Init failed:", err)
os.Exit(1)
}
lib.ConfigInit()
fmt.Println("Populating reference ranges is no longer needed - use import-caliper instead.")
fmt.Println("Done.")
return
}
if len(os.Args) == 2 {
// Just normalize, no import
dossierID := os.Args[1]
if err := lib.Init(); err != nil {
fmt.Println("lib.Init failed:", err)
os.Exit(1)
}
lib.ConfigInit()
lib.RefDBInit("/tank/inou/data/reference.db")
fmt.Println("Normalizing test names...")
if err := lib.Normalize(dossierID, lib.CategoryLab); err != nil {
fmt.Printf("Normalization failed: %v\n", err)
os.Exit(1)
}
return
}
if len(os.Args) < 3 {
fmt.Println("Usage: import-lab <dossier-id> [path]")
fmt.Println(" path: directory of JSONs or single JSON file")
fmt.Println(" Without path: only normalize existing entries")
fmt.Println(" import-lab --patch-time <dossier-id> <path> (patch local_time into existing entries)")
os.Exit(1)
}
dossierID := os.Args[1]
inputPath := os.Args[2]
// Init lib (crypto + DB + LLM keys)
if err := lib.Init(); err != nil {
fmt.Println("lib.Init failed:", err)
os.Exit(1)
}
lib.ConfigInit()
lib.RefDBInit("/tank/inou/data/reference.db")
// Load existing lab entries for dedup
existing, err := lib.EntryQuery(nil, dossierID, lib.CategoryLab, "", "*")
if err != nil {
fmt.Printf("Warning: could not load existing entries: %v\n", err)
}
existingByKey := make(map[string]*lib.Entry, len(existing))
for _, e := range existing {
var data struct {
SourceKey string `json:"source_key"`
}
if json.Unmarshal([]byte(e.Data), &data) == nil && data.SourceKey != "" {
existingByKey[data.SourceKey] = e
}
}
fmt.Printf("Loaded %d existing lab entries (%d with source_key)\n", len(existing), len(existingByKey))
// Collect input files
var files []string
info, err := os.Stat(inputPath)
if err != nil {
fmt.Printf("Cannot access %s: %v\n", inputPath, err)
os.Exit(1)
}
if info.IsDir() {
matches, _ := filepath.Glob(filepath.Join(inputPath, "*.json"))
files = matches
} else {
files = []string{inputPath}
}
fmt.Printf("Processing %d files\n", len(files))
// Parse all files into orders
var orders []labOrder
var skipFiles int
for _, f := range files {
order, err := parseFile(f)
if err != nil {
skipFiles++
continue
}
if order != nil {
orders = append(orders, *order)
}
}
fmt.Printf("Parsed %d lab orders (%d files skipped)\n", len(orders), skipFiles)
// Build entries
var entries []*lib.Entry
var created, updated int
now := time.Now().Unix()
_ = now
for _, order := range orders {
// Parent entry
parentID := ""
if ex, ok := existingByKey[order.sourceKey]; ok {
parentID = ex.EntryID
updated++
} else {
parentID = lib.NewID()
created++
}
parentData, _ := json.Marshal(map[string]interface{}{
"source_key": order.sourceKey,
"source": order.source,
"provider": order.provider,
"lab_name": order.labName,
"specimen": order.specimen,
"local_time": order.localTime,
})
entries = append(entries, &lib.Entry{
EntryID: parentID,
DossierID: dossierID,
Category: lib.CategoryLab,
Type: "lab_order",
Value: order.orderName,
Timestamp: order.timestamp,
Tags: order.source,
Data: string(parentData),
})
// Child entries
for _, test := range order.tests {
childID := ""
if ex, ok := existingByKey[test.sourceKey]; ok {
childID = ex.EntryID
} else {
childID = lib.NewID()
}
// Build summary for portal display
summary := test.commonName + ": " + test.value
if summary == ": " {
summary = test.name + ": " + test.value
}
if test.unit != "" {
summary += " " + test.unit
}
childData := map[string]interface{}{
"source_key": test.sourceKey,
}
if test.commonName != "" {
childData["common_name"] = test.commonName
}
if test.numericValue != nil {
childData["numeric_value"] = *test.numericValue
}
if test.unit != "" {
childData["unit"] = test.unit
}
dataJSON, _ := json.Marshal(childData)
entries = append(entries, &lib.Entry{
EntryID: childID,
DossierID: dossierID,
ParentID: parentID,
Category: lib.CategoryLab,
Type: test.name,
Value: test.value,
Summary: summary,
Timestamp: order.timestamp,
Data: string(dataJSON),
})
}
}
// Save
fmt.Printf("Saving %d entries...\n", len(entries))
start := time.Now()
if err := lib.EntryWrite("", entries...); err != nil {
fmt.Printf("EntryWrite failed: %v\n", err)
os.Exit(1)
}
fmt.Printf("Done in %v: %d orders (%d created, %d updated), %d total entries\n",
time.Since(start), len(orders), created, updated, len(entries))
// Normalize test names within this dossier
fmt.Println("Normalizing test names...")
if err := lib.Normalize(dossierID, lib.CategoryLab); err != nil {
fmt.Printf("Warning: normalization failed: %v\n", err)
}
}
func parseFile(path string) (*labOrder, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Auto-detect format
var raw map[string]json.RawMessage
if json.Unmarshal(data, &raw) != nil {
return nil, fmt.Errorf("invalid JSON")
}
if _, ok := raw["orderName"]; ok {
return parseMyChart(data)
}
if _, ok := raw["lab_order"]; ok {
return parseQualityLabs(data)
}
return nil, fmt.Errorf("unknown format")
}
func parseMyChart(data []byte) (*labOrder, error) {
var resp MyChartResponse
if err := json.Unmarshal(data, &resp); err != nil {
return nil, err
}
if len(resp.Results) == 0 {
return nil, nil
}
r := resp.Results[0]
if r.OrderMetadata.ResultType != "LAB" {
return nil, nil
}
if len(r.ResultComponents) == 0 {
return nil, nil
}
provider := r.OrderMetadata.OrderProviderName
if provider == "" {
provider = r.OrderMetadata.AuthorizingProviderName
}
ts, _ := time.Parse(time.RFC3339, r.OrderMetadata.PrioritizedInstantISO)
order := &labOrder{
sourceKey: resp.Key,
orderName: resp.OrderName,
timestamp: ts.Unix(),
localTime: r.OrderMetadata.PrioritizedInstantISO,
provider: provider,
labName: r.OrderMetadata.ResultingLab.Name,
specimen: r.OrderMetadata.SpecimensDisplay,
source: "mychart",
}
for _, c := range r.ResultComponents {
name := c.ComponentInfo.Name
if name == "" {
continue
}
order.tests = append(order.tests, labTest{
sourceKey: resp.Key + "|" + name,
name: name,
commonName: c.ComponentInfo.CommonName,
value: c.ComponentResultInfo.Value,
numericValue: c.ComponentResultInfo.NumericValue,
unit: c.ComponentInfo.Units,
})
}
return order, nil
}
func parseQualityLabs(data []byte) (*labOrder, error) {
var ql QualityLabsFile
if err := json.Unmarshal(data, &ql); err != nil {
return nil, err
}
ts, _ := time.Parse(time.RFC3339, ql.LabOrder.CollectionDate)
order := &labOrder{
sourceKey: ql.LabOrder.OrderKey,
orderName: ql.LabOrder.OrderName,
timestamp: ts.Unix(),
localTime: ql.LabOrder.CollectionDate,
provider: ql.LabOrder.Provider,
labName: ql.LabOrder.LabName,
specimen: ql.LabOrder.Specimen,
source: "quality_labs",
}
for _, t := range ql.LabTests {
if t.Name == "" {
continue
}
unit := ""
if t.Units != nil {
unit = *t.Units
}
// Use name as common_name for quality_labs (no separate common name)
cn := strings.ToUpper(t.Name)
order.tests = append(order.tests, labTest{
sourceKey: ql.LabOrder.OrderKey + "|" + t.Name,
name: t.Name,
commonName: cn,
value: t.Value,
numericValue: t.NumericValue,
unit: unit,
})
}
return order, nil
}
// patchLocalTime reads source JSONs, matches existing entries by source_key,
// and patches local_time into their Data JSON. No new entries created.
func patchLocalTime(dossierID, inputPath string) {
if err := lib.Init(); err != nil {
fmt.Println("lib.Init failed:", err)
os.Exit(1)
}
// Load existing lab entries
existing, err := lib.EntryQuery(nil, dossierID, lib.CategoryLab, "", "*")
if err != nil {
fmt.Printf("Failed to load entries: %v\n", err)
os.Exit(1)
}
byKey := make(map[string]*lib.Entry, len(existing))
for _, e := range existing {
var data struct {
SourceKey string `json:"source_key"`
}
if json.Unmarshal([]byte(e.Data), &data) == nil && data.SourceKey != "" {
byKey[data.SourceKey] = e
}
}
fmt.Printf("Loaded %d existing entries (%d with source_key)\n", len(existing), len(byKey))
// Collect input files
var files []string
info, err := os.Stat(inputPath)
if err != nil {
fmt.Printf("Cannot access %s: %v\n", inputPath, err)
os.Exit(1)
}
if info.IsDir() {
matches, _ := filepath.Glob(filepath.Join(inputPath, "*.json"))
files = matches
} else {
files = []string{inputPath}
}
// Parse and patch
var patched int
for _, f := range files {
order, err := parseFile(f)
if err != nil || order == nil {
continue
}
if e, ok := byKey[order.sourceKey]; ok {
if patchDataLocalTime(e, order.localTime) {
if err := lib.EntryWrite("", e); err == nil {
patched++
}
}
}
}
fmt.Printf("Patched %d entries with local_time\n", patched)
}
// patchDataLocalTime adds local_time to an entry's Data JSON
func patchDataLocalTime(e *lib.Entry, localTime string) bool {
if localTime == "" {
return false
}
var data map[string]interface{}
if json.Unmarshal([]byte(e.Data), &data) != nil {
return false
}
if existing, ok := data["local_time"]; ok && existing == localTime {
return false
}
data["local_time"] = localTime
b, _ := json.Marshal(data)
e.Data = string(b)
return true
}