package db import ( "axolotl/models" "axolotl/parse" "database/sql" "fmt" "math/rand" "strings" "time" ) func generateID() string { const chars = "abcdefghijklmnopqrstuvwxyz" b := make([]byte, 5) for i := range b { b[i] = chars[rand.Intn(26)] } return string(b) } func (db *DB) generateUniqueID() string { //TODO: Check if all ids are reserved for { id := generateID() var exists bool db.QueryRow("SELECT EXISTS(SELECT 1 FROM nodes WHERE id = ?)", id).Scan(&exists) if !exists { return id } } } type CreateParams struct { Title string Content string DueDate string Type string Status string Priority string Namespace string Tags []string Rels map[models.RelType][]string } func (db *DB) CreateNode(p CreateParams) (*models.Node, error) { tx, err := db.Begin() if err != nil { return nil, err } defer tx.Rollback() now := time.Now().UTC().Format(time.RFC3339) id := db.generateUniqueID() _, err = tx.Exec( "INSERT INTO nodes (id, title, content, due_date, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", id, p.Title, p.Content, p.DueDate, now, now, ) if err != nil { return nil, err } tags := p.Tags if p.Type != "" { tags = append(tags, models.PropertyTag("type", p.Type)) } else { tags = append(tags, models.PropertyTag("type", "issue")) } if p.Status != "" { tags = append(tags, models.PropertyTag("status", p.Status)) } if p.Priority != "" { tags = append(tags, models.PropertyTag("prio", p.Priority)) } ns := p.Namespace if ns == "" { ns = GetCurrentUser() } tags = append(tags, models.PropertyTag("namespace", ns)) for _, tag := range tags { if _, err := tx.Exec("INSERT INTO tags (node_id, tag) VALUES (?, ?)", id, tag); err != nil { return nil, err } } for relType, targets := range p.Rels { for _, target := range targets { if _, err := tx.Exec( "INSERT INTO rels (from_id, to_id, rel_type) VALUES (?, ?, ?)", id, target, relType, ); err != nil { return nil, err } } } allText := p.Title + " " + p.Content for _, u := range parse.Mentions(allText) { tx.Exec("INSERT INTO tags (node_id, tag) VALUES (?, ?)", id, models.PropertyTag("inbox", u)) } if err := tx.Commit(); err != nil { return nil, err } return db.NodeByID(id) } type UpdateParams struct { Title string Content string DueDate string ClearDue bool Status string Priority string AddTags []string RemoveTags []string AddRels map[models.RelType][]string RemoveRels map[models.RelType][]string } func (db *DB) UpdateNode(id string, p UpdateParams) error { tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() now := time.Now().UTC().Format(time.RFC3339) _, err = tx.Exec("UPDATE nodes SET updated_at = ? WHERE id = ?", now, id) if err != nil { return err } if p.Title != "" { _, err = tx.Exec("UPDATE nodes SET title = ? WHERE id = ?", p.Title, id) if err != nil { return err } } if p.Content != "" { _, err = tx.Exec("UPDATE nodes SET content = ? WHERE id = ?", p.Content, id) if err != nil { return err } } if p.DueDate != "" { _, err = tx.Exec("UPDATE nodes SET due_date = ? WHERE id = ?", p.DueDate, id) if err != nil { return err } } if p.ClearDue { _, err = tx.Exec("UPDATE nodes SET due_date = NULL WHERE id = ?", id) if err != nil { return err } } for _, tag := range p.AddTags { tx.Exec("INSERT OR IGNORE INTO tags (node_id, tag) VALUES (?, ?)", id, tag) } for _, tag := range p.RemoveTags { tx.Exec("DELETE FROM tags WHERE node_id = ? AND tag = ?", id, tag) } if p.Status != "" { tx.Exec("DELETE FROM tags WHERE node_id = ? AND tag LIKE '_status::%'", id) tx.Exec("INSERT INTO tags (node_id, tag) VALUES (?, ?)", id, models.PropertyTag("status", p.Status)) } if p.Priority != "" { tx.Exec("DELETE FROM tags WHERE node_id = ? AND tag LIKE '_prio::%'", id) tx.Exec("INSERT INTO tags (node_id, tag) VALUES (?, ?)", id, models.PropertyTag("prio", p.Priority)) } for relType, targets := range p.AddRels { for _, target := range targets { tx.Exec("INSERT OR IGNORE INTO rels (from_id, to_id, rel_type) VALUES (?, ?, ?)", id, target, relType) } } for relType, targets := range p.RemoveRels { for _, target := range targets { tx.Exec("DELETE FROM rels WHERE from_id = ? AND to_id = ? AND rel_type = ?", id, target, relType) } } return tx.Commit() } func (db *DB) DeleteNode(id string) error { _, err := db.Exec("DELETE FROM nodes WHERE id = ?", id) return err } type ListFilter struct { Type string Status string Priority string Namespace string Tag string Inbox string Assignee string } func (db *DB) ListNodes(f ListFilter) ([]*models.Node, error) { q := "SELECT DISTINCT n.id FROM nodes n" args := []interface{}{} joins := []string{} conds := []string{} if f.Type != "" { joins = append(joins, "JOIN tags t_type ON n.id = t_type.node_id") conds = append(conds, "t_type.tag = ?") args = append(args, models.PropertyTag("type", f.Type)) } if f.Status != "" { joins = append(joins, "JOIN tags t_status ON n.id = t_status.node_id") conds = append(conds, "t_status.tag = ?") args = append(args, models.PropertyTag("status", f.Status)) } if f.Priority != "" { joins = append(joins, "JOIN tags t_prio ON n.id = t_prio.node_id") conds = append(conds, "t_prio.tag = ?") args = append(args, models.PropertyTag("prio", f.Priority)) } if f.Namespace != "" { joins = append(joins, "JOIN tags t_ns ON n.id = t_ns.node_id") conds = append(conds, "t_ns.tag = ?") args = append(args, models.PropertyTag("namespace", f.Namespace)) } if f.Tag != "" { joins = append(joins, "JOIN tags t_tag ON n.id = t_tag.node_id") conds = append(conds, "t_tag.tag = ?") args = append(args, f.Tag) } if f.Inbox != "" { joins = append(joins, "JOIN tags t_inbox ON n.id = t_inbox.node_id") conds = append(conds, "t_inbox.tag = ?") args = append(args, models.PropertyTag("inbox", f.Inbox)) } if f.Assignee != "" { joins = append(joins, "JOIN rels r_assign ON n.id = r_assign.from_id") conds = append(conds, "r_assign.to_id = ? AND r_assign.rel_type = ?") args = append(args, f.Assignee, models.RelAssignee) } if len(joins) > 0 { q += " " + strings.Join(joins, " ") } if len(conds) > 0 { q += " WHERE " + strings.Join(conds, " AND ") } q += " ORDER BY n.created_at DESC" rows, err := db.Query(q, args...) if err != nil { return nil, err } defer rows.Close() var nodes []*models.Node for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } n, err := db.NodeByID(id) if err != nil { return nil, err } nodes = append(nodes, n) } return nodes, nil } func (db *DB) CanClose(id string) (bool, []string, error) { rows, err := db.Query( "SELECT to_id FROM rels WHERE from_id = ? AND rel_type = ?", id, models.RelBlocks, ) if err != nil { return false, nil, err } defer rows.Close() var blocking []string for rows.Next() { var blockerID string if err := rows.Scan(&blockerID); err != nil { return false, nil, err } var tag string err := db.QueryRow( "SELECT tag FROM tags WHERE node_id = ? AND tag LIKE '_status::%'", blockerID, ).Scan(&tag) if err == sql.ErrNoRows { blocking = append(blocking, blockerID) continue } if err != nil { return false, nil, err } if strings.HasSuffix(tag, "::open") { blocking = append(blocking, blockerID) } } return len(blocking) == 0, blocking, nil } func (db *DB) GetAllUsers() ([]string, error) { rows, err := db.Query("SELECT n.id FROM nodes n JOIN tags t ON n.id = t.node_id WHERE t.tag = ?", models.PropertyTag("type", "user")) if err != nil { return nil, err } defer rows.Close() var users []string for rows.Next() { var id string rows.Scan(&id) users = append(users, id) } return users, nil } func (db *DB) GetSubtasks(parentID string) ([]*models.Node, error) { rows, err := db.Query( "SELECT to_id FROM rels WHERE from_id = ? AND rel_type = ?", parentID, models.RelSubtask, ) if err != nil { return nil, err } defer rows.Close() var nodes []*models.Node for rows.Next() { var id string rows.Scan(&id) n, err := db.NodeByID(id) if err != nil { return nil, err } nodes = append(nodes, n) } return nodes, nil } func ParseRelFlag(s string) (models.RelType, string, error) { parts := strings.SplitN(s, ":", 2) if len(parts) != 2 { return "", "", fmt.Errorf("invalid relation format: %s (expected type:id)", s) } return models.RelType(parts[0]), parts[1], nil }