WIP: DICOM import improvements and database query optimizations
This commit is contained in:
parent
75e9ec7722
commit
d5133fd56f
|
|
@ -450,8 +450,8 @@ func getOrCreateStudy(data []byte, dossierID string) (string, error) {
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query for existing study by category+type (parent-agnostic)
|
// Query for existing study
|
||||||
studies, err := lib.EntryQuery(nil, dossierID, lib.CategoryImaging, "study", "*")
|
studies, err := lib.EntryRead("", dossierID, &lib.Filter{Category: lib.CategoryImaging, Type: "study"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, s := range studies {
|
for _, s := range studies {
|
||||||
if s.Value == studyUID {
|
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
|
// Extract study metadata
|
||||||
patientName := formatPatientName(readStringTag(data, 0x0010, 0x0010))
|
patientName := formatPatientName(readStringTag(data, 0x0010, 0x0010))
|
||||||
studyDesc := readStringTag(data, 0x0008, 0x1030)
|
studyDesc := readStringTag(data, 0x0008, 0x1030)
|
||||||
|
|
@ -503,7 +497,7 @@ func getOrCreateStudy(data []byte, dossierID string) (string, error) {
|
||||||
e := &lib.Entry{
|
e := &lib.Entry{
|
||||||
EntryID: lib.NewID(),
|
EntryID: lib.NewID(),
|
||||||
DossierID: dossierID,
|
DossierID: dossierID,
|
||||||
ParentID: catRootID, // child of imaging category root
|
ParentID: dossierID, // child of dossier root
|
||||||
Category: lib.CategoryImaging,
|
Category: lib.CategoryImaging,
|
||||||
Type: "study",
|
Type: "study",
|
||||||
Value: studyUID,
|
Value: studyUID,
|
||||||
|
|
@ -527,11 +521,8 @@ func getOrCreateSeries(data []byte, dossierID, studyID string) (string, error) {
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query for existing series using V2 API
|
// Query for existing series
|
||||||
children, err := lib.EntryList(lib.SystemAccessorID, studyID, lib.CategoryImaging, &lib.EntryFilter{ // nil ctx - import tool
|
children, err := lib.EntryRead("", dossierID, &lib.Filter{Category: lib.CategoryImaging, ParentID: studyID, Type: "series"})
|
||||||
DossierID: dossierID,
|
|
||||||
Type: "series",
|
|
||||||
})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, c := range children {
|
for _, c := range children {
|
||||||
if c.Value == seriesUID && c.Tags == seriesDesc {
|
if c.Value == seriesUID && c.Tags == seriesDesc {
|
||||||
|
|
@ -578,7 +569,7 @@ func getOrCreateSeries(data []byte, dossierID, studyID string) (string, error) {
|
||||||
Data: string(dataJSON),
|
Data: string(dataJSON),
|
||||||
SearchKey: modality,
|
SearchKey: modality,
|
||||||
}
|
}
|
||||||
if err := lib.EntryWrite("", e); err != nil { // nil ctx - import tool
|
if err := lib.EntryWrite("", e); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
seriesCache[cacheKey] = e.EntryID
|
seriesCache[cacheKey] = e.EntryID
|
||||||
|
|
@ -676,7 +667,7 @@ func insertSlice(data []byte, sliceID, dossierID, seriesID string, pixelMin, pix
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
Data: string(dataJSON),
|
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)
|
fmt.Printf(" Error encoding RGB: %v\n", err)
|
||||||
continue
|
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)
|
fmt.Printf(" Error saving RGB: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -992,7 +983,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err
|
||||||
fmt.Printf(" Error encoding 16-bit: %v\n", err)
|
fmt.Printf(" Error encoding 16-bit: %v\n", err)
|
||||||
continue
|
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)
|
fmt.Printf(" Error saving 16-bit: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -1032,7 +1023,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err
|
||||||
fmt.Printf(" Error encoding RGB: %v\n", err)
|
fmt.Printf(" Error encoding RGB: %v\n", err)
|
||||||
continue
|
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)
|
fmt.Printf(" Error saving RGB: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -1076,7 +1067,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err
|
||||||
fmt.Printf(" Error encoding 16-bit: %v\n", err)
|
fmt.Printf(" Error encoding 16-bit: %v\n", err)
|
||||||
continue
|
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)
|
fmt.Printf(" Error saving 16-bit: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -1098,7 +1089,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err
|
||||||
|
|
||||||
if savedPct > 5.0 {
|
if savedPct > 5.0 {
|
||||||
// Read existing series entry to update its Data field
|
// 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 {
|
if err == nil {
|
||||||
var seriesDataMap map[string]interface{}
|
var seriesDataMap map[string]interface{}
|
||||||
json.Unmarshal([]byte(series.Data), &seriesDataMap)
|
json.Unmarshal([]byte(series.Data), &seriesDataMap)
|
||||||
|
|
@ -1113,7 +1104,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err
|
||||||
seriesDataMap["crop_height"] = cropH
|
seriesDataMap["crop_height"] = cropH
|
||||||
updatedData, _ := json.Marshal(seriesDataMap)
|
updatedData, _ := json.Marshal(seriesDataMap)
|
||||||
series.Data = string(updatedData)
|
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",
|
log(" Crop: %d,%d → %d,%d (%dx%d, saves %.0f%% pixels)\n",
|
||||||
seriesBBox[0], seriesBBox[1], seriesBBox[2], seriesBBox[3], cropW, cropH, savedPct)
|
seriesBBox[0], seriesBBox[1], seriesBBox[2], seriesBBox[3], cropW, cropH, savedPct)
|
||||||
}
|
}
|
||||||
|
|
@ -1184,27 +1175,23 @@ func main() {
|
||||||
fmt.Println("Initialized")
|
fmt.Println("Initialized")
|
||||||
|
|
||||||
// Look up dossier
|
// Look up dossier
|
||||||
dossier, err := lib.DossierGet(nil, dossierID) // nil ctx - import tool
|
dossierEntries, err := lib.EntryRead("", dossierID, &lib.Filter{Category: 0})
|
||||||
if err != nil {
|
if err != nil || len(dossierEntries) == 0 {
|
||||||
fmt.Printf("Error: dossier %s not found\n", dossierID)
|
fmt.Printf("Error: dossier %s not found\n", dossierID)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
dob := "unknown"
|
fmt.Printf("Dossier: %s\n", dossierEntries[0].Summary)
|
||||||
if !dossier.DOB.IsZero() {
|
|
||||||
dob = dossier.DOB.Format("2006-01-02")
|
|
||||||
}
|
|
||||||
fmt.Printf("Dossier: %s (DOB: %s)\n", dossier.Name, dob)
|
|
||||||
|
|
||||||
// Check for existing imaging data
|
// 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 {
|
if len(existing) > 0 {
|
||||||
fmt.Printf("Clean existing imaging data? (yes/no): ")
|
fmt.Printf("Clean existing imaging data? (yes/no): ")
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
answer, _ := reader.ReadString('\n')
|
answer, _ := reader.ReadString('\n')
|
||||||
if strings.TrimSpace(answer) == "yes" {
|
if strings.TrimSpace(answer) == "yes" {
|
||||||
fmt.Print("Cleaning...")
|
fmt.Print("Cleaning...")
|
||||||
lib.EntryRemoveByDossier(nil, dossierID) // nil ctx - import tool
|
lib.EntryDeleteByCategory(nil, dossierID, lib.CategoryImaging)
|
||||||
lib.ObjectRemoveByDossier(nil, dossierID) // nil ctx - import tool
|
lib.ObjectRemoveByDossier(nil, dossierID)
|
||||||
fmt.Println(" done")
|
fmt.Println(" done")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
func createScanDest(t reflect.Type) any {
|
||||||
switch t.Kind() {
|
switch t.Kind() {
|
||||||
case reflect.Int, reflect.Int64:
|
case reflect.Int, reflect.Int64:
|
||||||
return new(sql.NullInt64)
|
return new(sql.NullInt64)
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
return new(sql.NullString)
|
return new([]byte)
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
if t.Elem().Kind() == reflect.Uint8 {
|
if t.Elem().Kind() == reflect.Uint8 {
|
||||||
return new([]byte)
|
return new([]byte)
|
||||||
|
|
@ -656,12 +657,10 @@ func decryptAndSet(field reflect.Value, scanned any, t reflect.Type, fieldName s
|
||||||
}
|
}
|
||||||
|
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
if strings.HasSuffix(fieldName, "ID") {
|
if b, ok := scanned.(*[]byte); ok && b != nil && len(*b) > 0 {
|
||||||
if ns, ok := scanned.(*sql.NullString); ok && ns.Valid {
|
if strings.HasSuffix(fieldName, "ID") {
|
||||||
field.SetString(ns.String)
|
field.SetString(string(*b))
|
||||||
}
|
} else {
|
||||||
} else {
|
|
||||||
if b, ok := scanned.(*[]byte); ok && b != nil && len(*b) > 0 {
|
|
||||||
if unpacked := Unpack(*b); unpacked != nil {
|
if unpacked := Unpack(*b); unpacked != nil {
|
||||||
field.SetString(string(unpacked))
|
field.SetString(string(unpacked))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,18 @@ func EntryWrite(accessorID string, entries ...*Entry) error {
|
||||||
return nil
|
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.
|
// entrySave inserts or replaces one entry. Internal only.
|
||||||
func entrySave(e *Entry) error {
|
func entrySave(e *Entry) error {
|
||||||
_, err := db.Exec(`INSERT OR REPLACE INTO entries
|
_, err := db.Exec(`INSERT OR REPLACE INTO entries
|
||||||
|
|
|
||||||
129
lib/stubs.go
129
lib/stubs.go
|
|
@ -85,12 +85,18 @@ func EntryAddBatchValues(entries []Entry) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func EntryGet(ctx *AccessContext, id string) (*Entry, error) {
|
func EntryGet(ctx *AccessContext, id string) (*Entry, error) {
|
||||||
log.Printf("[STUB] EntryGet(id=%s) — needs migration to EntryRead", id)
|
accessorID := ""
|
||||||
return nil, fmt.Errorf("EntryGet stub: not implemented")
|
if ctx != nil {
|
||||||
|
accessorID = ctx.AccessorID
|
||||||
|
}
|
||||||
|
return entryGetByID(accessorID, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func EntryQuery(ctx *AccessContext, dossierID string, category int, typ, parent string) ([]*Entry, error) {
|
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}
|
f := &Filter{Category: category}
|
||||||
if typ != "" {
|
if typ != "" {
|
||||||
f.Type = typ
|
f.Type = typ
|
||||||
|
|
@ -98,31 +104,28 @@ func EntryQuery(ctx *AccessContext, dossierID string, category int, typ, parent
|
||||||
if parent != "" && parent != "*" {
|
if parent != "" && parent != "*" {
|
||||||
f.ParentID = 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) {
|
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 := ""
|
dossierID := ""
|
||||||
typ := ""
|
filter := &Filter{Category: category, ParentID: parent}
|
||||||
if f != nil {
|
if f != nil {
|
||||||
dossierID = f.DossierID
|
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) {
|
func EntryQueryOld(dossierID string, category int, typ string) ([]*Entry, error) {
|
||||||
log.Printf("[STUB] EntryQueryOld(dossier=%s, cat=%d, typ=%s)", dossierID, category, typ)
|
return EntryRead("", dossierID, &Filter{Category: category, Type: typ})
|
||||||
f := &Filter{Category: category}
|
|
||||||
if typ != "" {
|
|
||||||
f.Type = typ
|
|
||||||
}
|
|
||||||
return entryQuery(dossierID, f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func EntryCount(ctx *AccessContext, dossierID string, category int, typ string) (int, error) {
|
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, "*")
|
entries, err := EntryQuery(ctx, dossierID, category, typ, "*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
|
@ -165,18 +168,15 @@ func EntryRemoveByDossier(ctx *AccessContext, dossierID string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func EntryChildren(dossierID, parentID string) ([]*Entry, error) {
|
func EntryChildren(dossierID, parentID string) ([]*Entry, error) {
|
||||||
log.Printf("[STUB] EntryChildren(dossier=%s, parent=%s)", dossierID, parentID)
|
return EntryRead("", dossierID, &Filter{Category: -1, ParentID: parentID})
|
||||||
return entryQuery(dossierID, &Filter{Category: -1, ParentID: parentID})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func EntryChildrenByType(dossierID, parentID string, typ string) ([]*Entry, error) {
|
func EntryChildrenByType(dossierID, parentID string, typ string) ([]*Entry, error) {
|
||||||
log.Printf("[STUB] EntryChildrenByType(dossier=%s, parent=%s, typ=%s)", dossierID, parentID, typ)
|
return EntryRead("", dossierID, &Filter{Category: -1, ParentID: parentID, Type: typ})
|
||||||
return entryQuery(dossierID, &Filter{Category: -1, ParentID: parentID, Type: typ})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func EntryTypes(dossierID string, category int) ([]string, error) {
|
func EntryTypes(dossierID string, category int) ([]string, error) {
|
||||||
log.Printf("[STUB] EntryTypes(dossier=%s, cat=%d)", dossierID, category)
|
entries, err := EntryRead("", dossierID, &Filter{Category: category})
|
||||||
entries, err := entryQuery(dossierID, &Filter{Category: category})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -194,7 +194,11 @@ func EntryTypes(dossierID string, category int) ([]string, error) {
|
||||||
// --- Dossier stubs ---
|
// --- Dossier stubs ---
|
||||||
|
|
||||||
func DossierGet(ctx *AccessContext, id string) (*Dossier, error) {
|
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 {
|
if err != nil || len(entries) == 0 {
|
||||||
return nil, fmt.Errorf("dossier not found: %s", id)
|
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) {
|
func DossierQuery(accessorID string) ([]*DossierQueryRow, error) {
|
||||||
log.Printf("[STUB] DossierQuery(accessor=%s)", accessorID)
|
// Get all accessible dossier profiles via RBAC
|
||||||
return nil, nil
|
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 {
|
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) {
|
func EntryQueryByDate(dossierID string, from, to int64) ([]*Entry, error) {
|
||||||
log.Printf("[STUB] EntryQueryByDate(dossier=%s, from=%d, to=%d)", dossierID, from, to)
|
return EntryRead("", dossierID, &Filter{Category: -1, FromDate: from, ToDate: to})
|
||||||
return entryQuery(dossierID, &Filter{Category: -1, FromDate: from, ToDate: to})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Audit types and stubs ---
|
// --- Audit types and stubs ---
|
||||||
|
|
@ -478,8 +525,34 @@ type AuditFilter struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func AuditList(f *AuditFilter) ([]*AuditEntry, error) {
|
func AuditList(f *AuditFilter) ([]*AuditEntry, error) {
|
||||||
log.Printf("[STUB] AuditList")
|
if f == nil {
|
||||||
return nil, 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 ---
|
// --- Tracker types and stubs ---
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue