From 9b250c20f9959b3d5e48d566bd7f51a5ae6c09c8 Mon Sep 17 00:00:00 2001 From: Elias Kohout Date: Fri, 27 Mar 2026 18:11:13 +0100 Subject: [PATCH] auto-create users on mention and resolve user refs in relationships --- cmd/create.go | 9 +++++++- db/node.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ output/output.go | 10 ++++----- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index 80c77f6..ec3c6e9 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -31,18 +31,25 @@ var createCmd = &cobra.Command{ cTags = append(cTags, "_status::open") } if !slices.ContainsFunc(cTags, func(e string) bool { return strings.HasPrefix(e, "_namespace::") }) { - cTags = append(cTags, "_namespace::" + db.GetCurrentUser()) + cTags = append(cTags, "_namespace::"+db.GetCurrentUser()) } rels := make(map[models.RelType][]string) + relCreated := false for _, r := range cRels { rt, tgt, err := db.ParseRelFlag(r) if err != nil { fmt.Fprintln(os.Stderr, err) return } + if rt == models.RelCreated { + relCreated = true + } rels[rt] = append(rels[rt], tgt) } + if !relCreated { + rels[models.RelCreated] = append(rels[models.RelCreated], db.GetCurrentUser()) + } if n, err := d.CreateNode(db.CreateParams{Title: args[0], Content: cContent, DueDate: cDue, Tags: cTags, Rels: rels}); err != nil { fmt.Fprintln(os.Stderr, "failed to create:", err) diff --git a/db/node.go b/db/node.go index 570e09b..be27cb9 100644 --- a/db/node.go +++ b/db/node.go @@ -35,6 +35,38 @@ func (db *DB) generateUniqueID() string { } } +func (db *DB) ensureUser(tx *sql.Tx, username string) (string, error) { + var existingID string + err := tx.QueryRow(` + SELECT n.id FROM nodes n + JOIN tags t ON n.id = t.node_id + WHERE n.title = ? AND t.tag = '_type::user'`, username).Scan(&existingID) + if err == nil { + return existingID, nil + } + if err != sql.ErrNoRows { + return "", err + } + + id := db.generateUniqueID() + now := time.Now().UTC().Format(time.RFC3339) + if _, err := tx.Exec("INSERT INTO nodes (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)", + id, username, now, now); err != nil { + return "", err + } + if _, err := tx.Exec("INSERT INTO tags (node_id, tag) VALUES (?, '_type::user')", id); err != nil { + return "", err + } + return id, nil +} + +func (db *DB) resolveUserRef(tx *sql.Tx, ref string) (string, error) { + if exists, _ := db.NodeExists(ref); exists { + return ref, nil + } + return db.ensureUser(tx, ref) +} + type CreateParams struct { Title, Content, DueDate string Tags []string @@ -64,6 +96,13 @@ func (db *DB) CreateNode(p CreateParams) (*models.Node, error) { if err != nil { return nil, err } + + for _, m := range parse.Mentions(p.Title + " " + p.Content) { + if _, err := db.ensureUser(tx, m); err != nil { + return nil, err + } + } + for _, t := range append(p.Tags, parse.Mentions(p.Title+" "+p.Content)...) { if !strings.HasPrefix(t, "_") && strings.HasPrefix(t, "@") { if _, err = tx.Exec("INSERT INTO tags (node_id, tag) VALUES (?, ?)", id, "_inbox::"+t[1:]); err != nil { @@ -75,6 +114,12 @@ func (db *DB) CreateNode(p CreateParams) (*models.Node, error) { } for rt, tgts := range p.Rels { for _, tgt := range tgts { + if rt == models.RelAssignee || rt == models.RelCreated { + var err error + if tgt, err = db.resolveUserRef(tx, tgt); err != nil { + return nil, err + } + } if _, err := tx.Exec("INSERT INTO rels (from_id, to_id, rel_type) VALUES (?, ?, ?)", id, tgt, rt); err != nil { return nil, err } @@ -106,6 +151,11 @@ func (db *DB) UpdateNode(id string, p UpdateParams) error { if err := upd("content", *p.Content); err != nil { return err } + for _, m := range parse.Mentions(*p.Content) { + if _, err := db.ensureUser(tx, m); err != nil { + return err + } + } } if p.DueDate != nil { if err := upd("due_date", *p.DueDate); err != nil { @@ -131,6 +181,12 @@ func (db *DB) UpdateNode(id string, p UpdateParams) error { } for rt, tgts := range p.AddRels { for _, tgt := range tgts { + if rt == models.RelAssignee || rt == models.RelCreated { + var err error + if tgt, err = db.resolveUserRef(tx, tgt); err != nil { + return err + } + } tx.Exec("INSERT OR IGNORE INTO rels (from_id, to_id, rel_type) VALUES (?, ?, ?)", id, tgt, rt) } } diff --git a/output/output.go b/output/output.go index df23b53..1ba188c 100644 --- a/output/output.go +++ b/output/output.go @@ -107,7 +107,7 @@ func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error { render(prioRM, n.GetProperty("prio"), true), render(statusRM, n.GetProperty("status"), true), render(typeRM, n.GetProperty("type"), true), - cTitle.Sprint(truncate(n.Title, 35)), + cTitle.Sprint(truncate(n.Title, 80)), cDim.Sprint("["+n.GetProperty("namespace")+"]"), ) if len(tags) > 0 { @@ -147,14 +147,14 @@ func PrintNode(w io.Writer, n *models.Node, jsonOut bool) error { } if tags := getDisplayTags(n); len(tags) > 0 { - fmt.Fprintf(w, "\n tags: %s\n\n", cPrimary.Sprint(strings.Join(tags, " • "))) + fmt.Fprintf(w, "\n tags: %s\n", cPrimary.Sprint(strings.Join(tags, " • "))) } if db, err := db.GetDB(); err == nil { if len(n.Relations) > 0 { for relType, ids := range n.Relations { - fmt.Fprintf(w, " %s\n", string(relType)) - if relIcon, ok := relIcons[string(relType)]; ok && relType != "created" { + if relIcon, ok := relIcons[string(relType)]; ok { + fmt.Fprintf(w, "\n %s\n", string(relType)) for _, id := range ids { node, err := db.NodeByID(id) if err == nil { @@ -167,7 +167,7 @@ func PrintNode(w io.Writer, n *models.Node, jsonOut bool) error { } else { fmt.Fprintf(w, "failed to attach to db: %v", err) } - + fmt.Fprintln(w) return nil }