diff --git a/import-dicom/main.go b/import-dicom/main.go index 4247fbe..00c7ffd 100644 --- a/import-dicom/main.go +++ b/import-dicom/main.go @@ -450,8 +450,8 @@ func getOrCreateStudy(data []byte, dossierID string) (string, error) { return id, nil } - // Query for existing study by category+type (parent-agnostic) - studies, err := lib.EntryQuery(nil, dossierID, lib.CategoryImaging, "study", "*") + // Query for existing study + studies, err := lib.EntryRead("", dossierID, &lib.Filter{Category: lib.CategoryImaging, Type: "study"}) if err == nil { for _, s := range studies { if s.Value == studyUID { @@ -461,12 +461,6 @@ func getOrCreateStudy(data []byte, dossierID string) (string, error) { } } - // Get imaging category root (create if needed) - catRootID, err := lib.EnsureCategoryRoot(dossierID, lib.CategoryImaging) - if err != nil { - return "", fmt.Errorf("ensure imaging category root: %w", err) - } - // Extract study metadata patientName := formatPatientName(readStringTag(data, 0x0010, 0x0010)) studyDesc := readStringTag(data, 0x0008, 0x1030) @@ -503,7 +497,7 @@ func getOrCreateStudy(data []byte, dossierID string) (string, error) { e := &lib.Entry{ EntryID: lib.NewID(), DossierID: dossierID, - ParentID: catRootID, // child of imaging category root + ParentID: dossierID, // child of dossier root Category: lib.CategoryImaging, Type: "study", Value: studyUID, @@ -527,11 +521,8 @@ func getOrCreateSeries(data []byte, dossierID, studyID string) (string, error) { return id, nil } - // Query for existing series using V2 API - children, err := lib.EntryList(lib.SystemAccessorID, studyID, lib.CategoryImaging, &lib.EntryFilter{ // nil ctx - import tool - DossierID: dossierID, - Type: "series", - }) + // Query for existing series + children, err := lib.EntryRead("", dossierID, &lib.Filter{Category: lib.CategoryImaging, ParentID: studyID, Type: "series"}) if err == nil { for _, c := range children { if c.Value == seriesUID && c.Tags == seriesDesc { @@ -578,7 +569,7 @@ func getOrCreateSeries(data []byte, dossierID, studyID string) (string, error) { Data: string(dataJSON), SearchKey: modality, } - if err := lib.EntryWrite("", e); err != nil { // nil ctx - import tool + if err := lib.EntryWrite("", e); err != nil { return "", err } seriesCache[cacheKey] = e.EntryID @@ -676,7 +667,7 @@ func insertSlice(data []byte, sliceID, dossierID, seriesID string, pixelMin, pix Timestamp: time.Now().Unix(), Data: string(dataJSON), } - return lib.EntryWrite("", e) // nil ctx - import tool + return lib.EntryWrite("", e) } // ============================================================================ @@ -948,7 +939,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err fmt.Printf(" Error encoding RGB: %v\n", err) continue } - if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { // nil ctx - import tool + if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { fmt.Printf(" Error saving RGB: %v\n", err) continue } @@ -992,7 +983,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err fmt.Printf(" Error encoding 16-bit: %v\n", err) continue } - if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { // nil ctx - import tool + if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { fmt.Printf(" Error saving 16-bit: %v\n", err) continue } @@ -1032,7 +1023,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err fmt.Printf(" Error encoding RGB: %v\n", err) continue } - if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { // nil ctx - import tool + if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { fmt.Printf(" Error saving RGB: %v\n", err) continue } @@ -1076,7 +1067,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err fmt.Printf(" Error encoding 16-bit: %v\n", err) continue } - if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { // nil ctx - import tool + if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { fmt.Printf(" Error saving 16-bit: %v\n", err) continue } @@ -1098,7 +1089,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err if savedPct > 5.0 { // Read existing series entry to update its Data field - series, err := lib.EntryGet(nil, seriesID) // nil ctx - import tool + series, err := lib.EntryGet(nil, seriesID) if err == nil { var seriesDataMap map[string]interface{} json.Unmarshal([]byte(series.Data), &seriesDataMap) @@ -1113,7 +1104,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err seriesDataMap["crop_height"] = cropH updatedData, _ := json.Marshal(seriesDataMap) series.Data = string(updatedData) - lib.EntryWrite("", series) // nil ctx - import tool + lib.EntryWrite("", series) log(" Crop: %d,%d → %d,%d (%dx%d, saves %.0f%% pixels)\n", seriesBBox[0], seriesBBox[1], seriesBBox[2], seriesBBox[3], cropW, cropH, savedPct) } @@ -1184,27 +1175,23 @@ func main() { fmt.Println("Initialized") // Look up dossier - dossier, err := lib.DossierGet(nil, dossierID) // nil ctx - import tool - if err != nil { + dossierEntries, err := lib.EntryRead("", dossierID, &lib.Filter{Category: 0}) + if err != nil || len(dossierEntries) == 0 { fmt.Printf("Error: dossier %s not found\n", dossierID) os.Exit(1) } - dob := "unknown" - if !dossier.DOB.IsZero() { - dob = dossier.DOB.Format("2006-01-02") - } - fmt.Printf("Dossier: %s (DOB: %s)\n", dossier.Name, dob) + fmt.Printf("Dossier: %s\n", dossierEntries[0].Summary) // Check for existing imaging data - existing, _ := lib.EntryList(lib.SystemAccessorID, "", lib.CategoryImaging, &lib.EntryFilter{DossierID: dossierID, Limit: 1}) // nil ctx - import tool + existing, _ := lib.EntryRead("", dossierID, &lib.Filter{Category: lib.CategoryImaging, Limit: 1}) if len(existing) > 0 { fmt.Printf("Clean existing imaging data? (yes/no): ") reader := bufio.NewReader(os.Stdin) answer, _ := reader.ReadString('\n') if strings.TrimSpace(answer) == "yes" { fmt.Print("Cleaning...") - lib.EntryRemoveByDossier(nil, dossierID) // nil ctx - import tool - lib.ObjectRemoveByDossier(nil, dossierID) // nil ctx - import tool + lib.EntryDeleteByCategory(nil, dossierID, lib.CategoryImaging) + lib.ObjectRemoveByDossier(nil, dossierID) fmt.Println(" done") } } diff --git a/lib/db_queries.go b/lib/db_queries.go index 099acf6..94dbe52 100644 --- a/lib/db_queries.go +++ b/lib/db_queries.go @@ -626,13 +626,14 @@ func encryptField(v reflect.Value, fieldName string) any { } } -// createScanDest creates an appropriate scan destination for a Go type +// createScanDest creates an appropriate scan destination for a Go type. +// String fields scan as []byte because they may be packed BLOBs. func createScanDest(t reflect.Type) any { switch t.Kind() { case reflect.Int, reflect.Int64: return new(sql.NullInt64) case reflect.String: - return new(sql.NullString) + return new([]byte) case reflect.Slice: if t.Elem().Kind() == reflect.Uint8 { return new([]byte) @@ -656,12 +657,10 @@ func decryptAndSet(field reflect.Value, scanned any, t reflect.Type, fieldName s } case reflect.String: - if strings.HasSuffix(fieldName, "ID") { - if ns, ok := scanned.(*sql.NullString); ok && ns.Valid { - field.SetString(ns.String) - } - } else { - if b, ok := scanned.(*[]byte); ok && b != nil && len(*b) > 0 { + if b, ok := scanned.(*[]byte); ok && b != nil && len(*b) > 0 { + if strings.HasSuffix(fieldName, "ID") { + field.SetString(string(*b)) + } else { if unpacked := Unpack(*b); unpacked != nil { field.SetString(string(unpacked)) } diff --git a/lib/dbcore.go b/lib/dbcore.go index 275ab93..234be3f 100644 --- a/lib/dbcore.go +++ b/lib/dbcore.go @@ -252,6 +252,18 @@ func EntryWrite(accessorID string, entries ...*Entry) error { return nil } +// entryGetByID loads a single entry by ID with RBAC. Internal choke point. +func entryGetByID(accessorID, entryID string) (*Entry, error) { + var e Entry + if err := dbLoad("entries", entryID, &e); err != nil { + return nil, err + } + if !CheckAccess(accessorID, e.DossierID, entryID, PermRead) { + return nil, nil + } + return &e, nil +} + // entrySave inserts or replaces one entry. Internal only. func entrySave(e *Entry) error { _, err := db.Exec(`INSERT OR REPLACE INTO entries diff --git a/lib/stubs.go b/lib/stubs.go index 3d43b76..e349aec 100644 --- a/lib/stubs.go +++ b/lib/stubs.go @@ -85,12 +85,18 @@ func EntryAddBatchValues(entries []Entry) error { } func EntryGet(ctx *AccessContext, id string) (*Entry, error) { - log.Printf("[STUB] EntryGet(id=%s) — needs migration to EntryRead", id) - return nil, fmt.Errorf("EntryGet stub: not implemented") + accessorID := "" + if ctx != nil { + accessorID = ctx.AccessorID + } + return entryGetByID(accessorID, id) } func EntryQuery(ctx *AccessContext, dossierID string, category int, typ, parent string) ([]*Entry, error) { - log.Printf("[STUB] EntryQuery(dossier=%s, cat=%d, typ=%s, parent=%s)", dossierID, category, typ, parent) + accessorID := "" + if ctx != nil { + accessorID = ctx.AccessorID + } f := &Filter{Category: category} if typ != "" { f.Type = typ @@ -98,31 +104,28 @@ func EntryQuery(ctx *AccessContext, dossierID string, category int, typ, parent if parent != "" && parent != "*" { f.ParentID = parent } - return entryQuery(dossierID, f) + return EntryRead(accessorID, dossierID, f) } func EntryList(accessorID string, parent string, category int, f *EntryFilter) ([]*Entry, error) { - log.Printf("[STUB] EntryList(accessor=%s, parent=%s, cat=%d)", accessorID, parent, category) dossierID := "" - typ := "" + filter := &Filter{Category: category, ParentID: parent} if f != nil { dossierID = f.DossierID - typ = f.Type + filter.Type = f.Type + filter.SearchKey = f.SearchKey + filter.FromDate = f.FromDate + filter.ToDate = f.ToDate + filter.Limit = f.Limit } - return EntryQuery(&AccessContext{AccessorID: accessorID}, dossierID, category, typ, parent) + return EntryRead(accessorID, dossierID, filter) } func EntryQueryOld(dossierID string, category int, typ string) ([]*Entry, error) { - log.Printf("[STUB] EntryQueryOld(dossier=%s, cat=%d, typ=%s)", dossierID, category, typ) - f := &Filter{Category: category} - if typ != "" { - f.Type = typ - } - return entryQuery(dossierID, f) + return EntryRead("", dossierID, &Filter{Category: category, Type: typ}) } func EntryCount(ctx *AccessContext, dossierID string, category int, typ string) (int, error) { - log.Printf("[STUB] EntryCount(dossier=%s, cat=%d, typ=%s)", dossierID, category, typ) entries, err := EntryQuery(ctx, dossierID, category, typ, "*") if err != nil { return 0, err @@ -165,18 +168,15 @@ func EntryRemoveByDossier(ctx *AccessContext, dossierID string) error { } func EntryChildren(dossierID, parentID string) ([]*Entry, error) { - log.Printf("[STUB] EntryChildren(dossier=%s, parent=%s)", dossierID, parentID) - return entryQuery(dossierID, &Filter{Category: -1, ParentID: parentID}) + return EntryRead("", dossierID, &Filter{Category: -1, ParentID: parentID}) } func EntryChildrenByType(dossierID, parentID string, typ string) ([]*Entry, error) { - log.Printf("[STUB] EntryChildrenByType(dossier=%s, parent=%s, typ=%s)", dossierID, parentID, typ) - return entryQuery(dossierID, &Filter{Category: -1, ParentID: parentID, Type: typ}) + return EntryRead("", dossierID, &Filter{Category: -1, ParentID: parentID, Type: typ}) } func EntryTypes(dossierID string, category int) ([]string, error) { - log.Printf("[STUB] EntryTypes(dossier=%s, cat=%d)", dossierID, category) - entries, err := entryQuery(dossierID, &Filter{Category: category}) + entries, err := EntryRead("", dossierID, &Filter{Category: category}) if err != nil { return nil, err } @@ -194,7 +194,11 @@ func EntryTypes(dossierID string, category int) ([]string, error) { // --- Dossier stubs --- func DossierGet(ctx *AccessContext, id string) (*Dossier, error) { - entries, err := entryQuery(id, &Filter{Category: 0}) + accessorID := "" + if ctx != nil { + accessorID = ctx.AccessorID + } + entries, err := EntryRead(accessorID, id, &Filter{Category: 0}) if err != nil || len(entries) == 0 { return nil, fmt.Errorf("dossier not found: %s", id) } @@ -243,8 +247,52 @@ func DossierList(ctx *AccessContext, f *DossierFilter) ([]*Dossier, error) { } func DossierQuery(accessorID string) ([]*DossierQueryRow, error) { - log.Printf("[STUB] DossierQuery(accessor=%s)", accessorID) - return nil, nil + // Get all accessible dossier profiles via RBAC + dossierEntries, err := EntryRead(accessorID, "", &Filter{Category: 0}) + if err != nil { + return nil, err + } + + var rows []*DossierQueryRow + for _, de := range dossierEntries { + d := Dossier{DossierID: de.DossierID, Name: de.Summary, Email: de.SearchKey} + if de.Data != "" { + var data struct { + DOB string `json:"dob"` + Sex int `json:"sex"` + Lang string `json:"lang"` + } + if json.Unmarshal([]byte(de.Data), &data) == nil { + d.DateOfBirth = data.DOB + d.Sex = data.Sex + if data.Lang != "" { + d.Preferences.Language = data.Lang + } + } + } + + // Count entries by category for this dossier + allEntries, err := EntryRead(accessorID, de.DossierID, &Filter{Category: -1}) + if err != nil { + continue + } + catCounts := map[int]int{} + for _, e := range allEntries { + if e.Category > 0 { + catCounts[e.Category]++ + } + } + + if len(catCounts) == 0 { + // Still include dossier even with no entries + rows = append(rows, &DossierQueryRow{Dossier: d}) + } else { + for cat, count := range catCounts { + rows = append(rows, &DossierQueryRow{Dossier: d, Category: cat, EntryCount: count}) + } + } + } + return rows, nil } func DossierSetAuthCode(dossierID string, code int, expiresAt int64) error { @@ -462,8 +510,7 @@ func EntryCategoryCounts(ctx *AccessContext, dossierID string) (map[string]int, } func EntryQueryByDate(dossierID string, from, to int64) ([]*Entry, error) { - log.Printf("[STUB] EntryQueryByDate(dossier=%s, from=%d, to=%d)", dossierID, from, to) - return entryQuery(dossierID, &Filter{Category: -1, FromDate: from, ToDate: to}) + return EntryRead("", dossierID, &Filter{Category: -1, FromDate: from, ToDate: to}) } // --- Audit types and stubs --- @@ -478,8 +525,34 @@ type AuditFilter struct { } func AuditList(f *AuditFilter) ([]*AuditEntry, error) { - log.Printf("[STUB] AuditList") - return nil, nil + if f == nil { + return nil, nil + } + q := "SELECT AuditID, Actor1ID, Actor2ID, TargetID, Action, Details, RelationID, Timestamp FROM audit WHERE 1=1" + var args []any + if f.TargetID != "" { + q += " AND TargetID = ?" + args = append(args, f.TargetID) + } + if f.ActorID != "" { + q += " AND Actor1ID = ?" + args = append(args, f.ActorID) + } + 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.Limit > 0 { + q += fmt.Sprintf(" LIMIT %d", f.Limit) + } + var entries []*AuditEntry + err := dbQuery(q, args, &entries) + return entries, err } // --- Tracker types and stubs ---