A2 made narrators/narrators_search NOT NULL DEFAULT '{}' (86f0af6), but an
explicit NULL in an INSERT/UPDATE column list bypasses the column default
— the API media-create path (POST /api/media-items without narrators)
500'd on every call, taking the whole integration suite down with it
(ISBN tests, capture seeds, all direct-DB seeds).
- set_library_type_name() trigger now coalesces NULL narrators/
narrators_search to '{}' at the row level, and its update trigger
widens from BEFORE UPDATE OF library_id to BEFORE UPDATE so every
write path is covered
- defense in depth at the callers: handler create/update normalize nil
to empty slices (normalizeNarrators), scanner seeds the same
(nilToEmpty) for files that carry no narrator credits
- analytics device-usage tests move to the dedicated per-test user
(the shared admin's device list accrues live rows mid-suite — the
documented testuser flapping class)
82 lines
2.4 KiB
Go
82 lines
2.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookhoard/internal/config"
|
|
"bookhoard/internal/database"
|
|
"bookhoard/internal/services"
|
|
wsync "bookhoard/internal/sync"
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v5"
|
|
)
|
|
|
|
type Handler struct {
|
|
db *database.Queries
|
|
scanner *services.MediaScanner
|
|
worker *services.Worker
|
|
queueProcessor *wsync.SyncQueueProcessor
|
|
queueCtx context.Context
|
|
queueCancel context.CancelFunc
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
mu sync.Mutex
|
|
watchModeCtx context.Context
|
|
watchModeCancel context.CancelFunc
|
|
watchingLibraries map[string]bool
|
|
connManager *wsync.ConnectionManager
|
|
cleanupTaskCancel context.CancelFunc
|
|
config *config.Config
|
|
}
|
|
|
|
func NewHandler(db *database.Queries, connManager *wsync.ConnectionManager, queueProcessor *wsync.SyncQueueProcessor, cfg *config.Config) *Handler {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
worker := services.NewWorker(3, connManager)
|
|
|
|
queueCtx, queueCancel := context.WithCancel(context.Background())
|
|
|
|
watchCtx, watchCancel := context.WithCancel(context.Background())
|
|
|
|
return &Handler{
|
|
db: db,
|
|
scanner: services.NewMediaScanner(db),
|
|
worker: worker,
|
|
queueProcessor: queueProcessor,
|
|
queueCtx: queueCtx,
|
|
queueCancel: queueCancel,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
watchModeCtx: watchCtx,
|
|
watchModeCancel: watchCancel,
|
|
watchingLibraries: make(map[string]bool),
|
|
connManager: connManager,
|
|
}
|
|
}
|
|
|
|
// parseDate parses a date string in YYYY-MM-DD format
|
|
func parseDate(dateStr string) time.Time {
|
|
if dateStr == "" {
|
|
return time.Time{}
|
|
}
|
|
if t, err := time.Parse("2006-01-02", dateStr); err == nil {
|
|
return t
|
|
}
|
|
return time.Time{}
|
|
}
|
|
|
|
// normalizeNarrators guards the NOT NULL narrators columns (A2): a Go nil
|
|
// slice becomes an explicit NULL in INSERT/UPDATE params and 23502s — the
|
|
// column DEFAULT never fires for explicit NULLs. Omitted narrators mean
|
|
// "none known", which is an empty array, never NULL.
|
|
func normalizeNarrators(narrators []string) []string {
|
|
if narrators == nil {
|
|
return []string{}
|
|
}
|
|
return narrators
|
|
}
|
|
|
|
func SetupRoutes(g *echo.Group, db *database.Queries, connManager *wsync.ConnectionManager, queueProcessor *wsync.SyncQueueProcessor, cfg *config.Config) *Handler {
|
|
return NewHandler(db, connManager, queueProcessor, cfg)
|
|
}
|