refactor: add tag and relation methods to node to enforce integrity

This commit is contained in:
2026-03-29 23:16:44 +02:00
parent 4ebcb88628
commit dadd3d9e13
15 changed files with 313 additions and 179 deletions

View File

@@ -1,17 +0,0 @@
package service
import (
"maps"
"regexp"
"slices"
)
var mentionRegex = regexp.MustCompile(`@([a-z0-9_]+)`)
func mentions(t string) []string {
seen := make(map[string]bool)
for _, m := range mentionRegex.FindAllStringSubmatch(t, -1) {
seen[m[1]] = true
}
return slices.Collect(maps.Keys(seen))
}

View File

@@ -25,21 +25,16 @@ func GetNodeService(cfg Config) (NodeService, error) {
}
type listFilter struct {
tagPrefixes []string
assignee string
mentionsUser string
tagPrefixes []string
relPrefixes []*models.Rel
}
type ListOption func(*listFilter)
func WithTags(prefixes ...string) ListOption {
return func(f *listFilter) { f.tagPrefixes = prefixes }
return func(f *listFilter) { f.tagPrefixes = append(f.tagPrefixes, prefixes...) }
}
func WithAssignee(userID string) ListOption {
return func(f *listFilter) { f.assignee = userID }
}
func WithMentions(userID string) ListOption {
return func(f *listFilter) { f.mentionsUser = userID }
func WithRels(prefixes ...*models.Rel) ListOption {
return func(f *listFilter) { f.relPrefixes = append(f.relPrefixes, prefixes...) }
}

View File

@@ -5,9 +5,11 @@ import (
"database/sql"
"errors"
"fmt"
"maps"
"math/rand"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"time"
@@ -27,6 +29,16 @@ var migrations = []string{
`CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag)`, `CREATE INDEX IF NOT EXISTS idx_rels_from ON rels(from_id)`, `CREATE INDEX IF NOT EXISTS idx_rels_to ON rels(to_id)`,
}
var mentionRegex = regexp.MustCompile(`@([a-z0-9_]+)`)
func mentions(t string) []string {
seen := make(map[string]bool)
for _, m := range mentionRegex.FindAllStringSubmatch(t, -1) {
seen[m[1]] = true
}
return slices.Collect(maps.Keys(seen))
}
func InitSqliteDB(path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
@@ -67,7 +79,7 @@ func GetSqliteDB(cfg Config) (*sql.DB, error) {
}
func (s *sqliteNodeService) GetByID(id string) (*models.Node, error) {
n := &models.Node{Relations: make(map[string][]string)}
n := models.NewNode()
q := s.db.QueryRow("SELECT id, title, COALESCE(content, ''), COALESCE(due_date, ''), created_at, updated_at FROM nodes WHERE id = ?", id)
if err := q.Scan(&n.ID, &n.Title, &n.Content, &n.DueDate, &n.CreatedAt, &n.UpdatedAt); err != nil {
return nil, err
@@ -78,7 +90,7 @@ func (s *sqliteNodeService) GetByID(id string) (*models.Node, error) {
for rows.Next() {
var tag string
rows.Scan(&tag)
n.Tags = append(n.Tags, tag)
n.AddTag(tag)
}
} else {
return nil, err
@@ -89,7 +101,7 @@ func (s *sqliteNodeService) GetByID(id string) (*models.Node, error) {
for rows.Next() {
var toID, relType string
rows.Scan(&toID, &relType)
n.Relations[relType] = append(n.Relations[relType], toID)
n.AddRelation(models.RelType(relType), toID)
}
} else {
return nil, err
@@ -299,27 +311,31 @@ func (s *sqliteNodeService) Update(node *models.Node) error {
}
}
for _, t := range current.Tags {
if !slices.Contains(node.Tags, t) {
currentTags := current.Tags()
nodeTags := node.Tags()
for _, t := range currentTags {
if !slices.Contains(nodeTags, t) {
tx.Exec("DELETE FROM tags WHERE node_id = ? AND tag = ?", node.ID, t)
}
}
for _, t := range node.Tags {
if !slices.Contains(current.Tags, t) {
for _, t := range nodeTags {
if !slices.Contains(currentTags, t) {
tx.Exec("INSERT OR IGNORE INTO tags (node_id, tag) VALUES (?, ?)", node.ID, t)
}
}
for rt, tgts := range current.Relations {
currentRels := current.Relations()
nodeRels := node.Relations()
for rt, tgts := range currentRels {
for _, tgt := range tgts {
if node.Relations[rt] == nil || !slices.Contains(node.Relations[rt], tgt) {
if nodeRels[rt] == nil || !slices.Contains(nodeRels[rt], tgt) {
tx.Exec("DELETE FROM rels WHERE from_id = ? AND to_id = ? AND rel_type = ?", node.ID, tgt, rt)
}
}
}
for rt, tgts := range node.Relations {
for rt, tgts := range nodeRels {
for _, tgt := range tgts {
if current.Relations[rt] == nil || !slices.Contains(current.Relations[rt], tgt) {
if currentRels[rt] == nil || !slices.Contains(currentRels[rt], tgt) {
resolvedTgt := tgt
if models.RelType(rt) == models.RelAssignee || models.RelType(rt) == models.RelCreated {
var err error
@@ -402,31 +418,6 @@ func (s *sqliteNodeService) List(opts ...ListOption) ([]*models.Node, error) {
havingConds = append(havingConds, "SUM(CASE WHEN "+cond[:len(cond)-4]+" THEN 1 ELSE 0 END) >= ?")
havingArgs = append(havingArgs, len(f.tagPrefixes))
if f.assignee != "" {
userID, err := s.resolveUserIDByName(f.assignee)
if err != nil {
return nil, err
}
if userID == "" {
return []*models.Node{}, nil
}
joins = append(joins, "JOIN rels r_assign ON n.id = r_assign.from_id")
whereConds = append(whereConds, "r_assign.to_id = ? AND r_assign.rel_type = ?")
whereArgs = append(whereArgs, userID, models.RelAssignee)
}
if f.mentionsUser != "" {
userID, err := s.resolveUserIDByName(f.mentionsUser)
if err != nil {
return nil, err
}
if userID == "" {
return []*models.Node{}, nil
}
joins = append(joins, "JOIN rels r_mentions ON n.id = r_mentions.from_id")
whereConds = append(whereConds, "r_mentions.to_id = ? AND r_mentions.rel_type = ?")
whereArgs = append(whereArgs, userID, models.RelMentions)
}
if len(joins) > 0 {
q += " " + strings.Join(joins, " ") + " "
}