srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/store
diff options
context:
space:
mode:
Diffstat (limited to 'internal/store')
-rw-r--r--internal/store/store.go96
-rw-r--r--internal/store/store_test.go16
2 files changed, 107 insertions, 5 deletions
diff --git a/internal/store/store.go b/internal/store/store.go
index e0f27d1..ecc3854 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -85,6 +85,24 @@ CREATE TABLE IF NOT EXISTS ws_messages (
);
CREATE INDEX IF NOT EXISTS ws_messages_entry_id ON ws_messages(entry_id);
+
+- Plugin-contributed markers on a history entry - see internal/ipc's
+- "tag_entry" request. plugin identifies who added it (informational,
+- not an identity/auth mechanism: any client connected to the socket
+- can tag as anyone). data is an opaque, plugin-defined JSON blob a
+- panel view can render later - e.g. a decoded JWT header/payload -
+- without needing that plugin still connected.
+CREATE TABLE IF NOT EXISTS entry_tags (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ entry_id INTEGER NOT NULL,
+ plugin TEXT NOT NULL,
+ tag TEXT NOT NULL,
+ data TEXT NOT NULL DEFAULT '',
+ created_at INTEGER NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS entry_tags_entry_id ON entry_tags(entry_id);
+CREATE INDEX IF NOT EXISTS entry_tags_tag ON entry_tags(tag);
`
// Store is a handle to the history database. Safe for concurrent use.
@@ -184,6 +202,63 @@ type Summary struct {
Error string
Source string
Flagged bool
+ // Tags is every distinct plugin tag on this entry, comma-joined -
+ // cheap enough to compute per row (a correlated subquery, see List/
+ // Search) that a plugin-tagged entry shows a badge in the history
+ // list itself, not just in its detail view.
+ Tags string
+}
+
+// EntryTag is one plugin-contributed marker on a history entry - see
+// the "tag_entry" IPC request and entry_tags' own schema comment for
+// what Plugin/Data mean.
+type EntryTag struct {
+ ID int64
+ EntryID int64
+ Plugin string
+ Tag string
+ Data string
+ CreatedAt time.Time
+}
+
+// AddEntryTag stores t and returns its assigned ID.
+func (s *Store) AddEntryTag(t EntryTag) (int64, error) {
+ if t.CreatedAt.IsZero() {
+ t.CreatedAt = time.Now()
+ }
+ res, err := s.db.Exec(
+ `INSERT INTO entry_tags (entry_id, plugin, tag, data, created_at) VALUES (?, ?, ?, ?, ?)`,
+ t.EntryID, t.Plugin, t.Tag, t.Data, t.CreatedAt.UnixMilli(),
+ )
+ if err != nil {
+ return 0, fmt.Errorf("add entry tag: %w", err)
+ }
+ return res.LastInsertId()
+}
+
+// ListEntryTags returns every tag on entryID, in the order they were
+// added.
+func (s *Store) ListEntryTags(entryID int64) ([]EntryTag, error) {
+ rows, err := s.db.Query(
+ `SELECT id, entry_id, plugin, tag, data, created_at FROM entry_tags WHERE entry_id = ? ORDER BY id`,
+ entryID,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("list entry tags: %w", err)
+ }
+ defer rows.Close()
+
+ var out []EntryTag
+ for rows.Next() {
+ var t EntryTag
+ var createdAt int64
+ if err := rows.Scan(&t.ID, &t.EntryID, &t.Plugin, &t.Tag, &t.Data, &createdAt); err != nil {
+ return nil, fmt.Errorf("scan entry tag row: %w", err)
+ }
+ t.CreatedAt = time.UnixMilli(createdAt)
+ out = append(out, t)
+ }
+ return out, rows.Err()
}
// Insert stores e (and indexes it for search) and returns its assigned ID.
@@ -255,7 +330,8 @@ func (s *Store) List(limit int, beforeID int64) ([]Summary, error) {
}
rows, err := s.db.Query(
`SELECT id, started_at, duration_ms, method, scheme, host, path,
- COALESCE(status_code, 0), length(request_raw), COALESCE(length(response_raw), 0), error, source, flagged
+ COALESCE(status_code, 0), length(request_raw), COALESCE(length(response_raw), 0), error, source, flagged,
+ COALESCE((SELECT group_concat(DISTINCT tag) FROM entry_tags WHERE entry_tags.entry_id = history.id), '')
FROM history WHERE id < ? ORDER BY id DESC LIMIT ?`,
beforeID, limit,
)
@@ -270,7 +346,7 @@ func (s *Store) List(limit int, beforeID int64) ([]Summary, error) {
var startedAt, durationMs int64
var flagged int
if err := rows.Scan(&sum.ID, &startedAt, &durationMs, &sum.Method, &sum.Scheme, &sum.Host, &sum.Path,
- &sum.StatusCode, &sum.ReqSize, &sum.RespSize, &sum.Error, &sum.Source, &flagged); err != nil {
+ &sum.StatusCode, &sum.ReqSize, &sum.RespSize, &sum.Error, &sum.Source, &flagged, &sum.Tags); err != nil {
return nil, fmt.Errorf("scan history row: %w", err)
}
sum.Flagged = flagged != 0
@@ -321,6 +397,12 @@ func (s *Store) Search(query string, limit int, beforeID int64) ([]Summary, erro
where = append(where, "h.flagged = ?")
args = append(args, boolToInt(*pred.flagged))
}
+ if pred.tag != "" {
+ where = append(where, "EXISTS (SELECT 1 FROM entry_tags WHERE entry_tags.entry_id = h.id AND entry_tags.tag = ?)")
+ args = append(args, pred.tag)
+ }
+
+ const tagsCol = `COALESCE((SELECT group_concat(DISTINCT tag) FROM entry_tags WHERE entry_tags.entry_id = h.id), '')`
var q string
if remaining == "" {
@@ -328,7 +410,7 @@ func (s *Store) Search(query string, limit int, beforeID int64) ([]Summary, erro
// FTS5 join or ranking needed.
q = `SELECT h.id, h.started_at, h.duration_ms, h.method, h.scheme, h.host, h.path,
COALESCE(h.status_code, 0), length(h.request_raw), COALESCE(length(h.response_raw), 0),
- h.error, h.source, h.flagged
+ h.error, h.source, h.flagged, ` + tagsCol + `
FROM history h
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY h.id DESC LIMIT ?`
@@ -341,7 +423,7 @@ func (s *Store) Search(query string, limit int, beforeID int64) ([]Summary, erro
args = append([]any{prepareFTSQuery(remaining)}, args...)
q = `SELECT h.id, h.started_at, h.duration_ms, h.method, h.scheme, h.host, h.path,
COALESCE(h.status_code, 0), length(h.request_raw), COALESCE(length(h.response_raw), 0),
- h.error, h.source, h.flagged
+ h.error, h.source, h.flagged, ` + tagsCol + `
FROM history_fts
JOIN history h ON h.id = history_fts.rowid
WHERE ` + strings.Join(where, " AND ") + `
@@ -361,7 +443,7 @@ func (s *Store) Search(query string, limit int, beforeID int64) ([]Summary, erro
var startedAt, durationMs int64
var flagged int
if err := rows.Scan(&sum.ID, &startedAt, &durationMs, &sum.Method, &sum.Scheme, &sum.Host, &sum.Path,
- &sum.StatusCode, &sum.ReqSize, &sum.RespSize, &sum.Error, &sum.Source, &flagged); err != nil {
+ &sum.StatusCode, &sum.ReqSize, &sum.RespSize, &sum.Error, &sum.Source, &flagged, &sum.Tags); err != nil {
return nil, fmt.Errorf("scan search row: %w", err)
}
sum.Flagged = flagged != 0
@@ -379,6 +461,7 @@ type structuredPredicate struct {
statusArgs []any
source string
flagged *bool
+ tag string
}
var (
@@ -427,6 +510,9 @@ func extractStructured(query string) (remaining string, pred structuredPredicate
pred.flagged = &b
continue
}
+ case strings.HasPrefix(lower, "tag:"):
+ pred.tag = strings.TrimPrefix(f, "tag:")
+ continue
}
kept = append(kept, f)
}
diff --git a/internal/store/store_test.go b/internal/store/store_test.go
index 7fa985a..5daa73a 100644
--- a/internal/store/store_test.go
+++ b/internal/store/store_test.go
@@ -13,6 +13,7 @@ func TestExtractStructured(t *testing.T) {
statusArgs []any
source string
flagged *bool
+ tag string
}{
{
name: "plain text only",
@@ -71,6 +72,18 @@ func TestExtractStructured(t *testing.T) {
source: "repeater",
},
{
+ name: "tag filter",
+ query: "tag:jwt",
+ remaining: "",
+ tag: "jwt",
+ },
+ {
+ name: "tag filter preserves case",
+ query: "tag:JWT",
+ remaining: "",
+ tag: "JWT",
+ },
+ {
name: "combined with free text",
query: "admin status:>=400 source:proxy",
remaining: "admin",
@@ -110,6 +123,9 @@ func TestExtractStructured(t *testing.T) {
if pred.source != tt.source {
t.Errorf("source = %q, want %q", pred.source, tt.source)
}
+ if pred.tag != tt.tag {
+ t.Errorf("tag = %q, want %q", pred.tag, tt.tag)
+ }
switch {
case pred.flagged == nil && tt.flagged == nil:
case pred.flagged == nil || tt.flagged == nil: