package main import ( // External werr "git.gibonuddevalla.se/go/wrappederror" "github.com/jmoiron/sqlx" "github.com/lib/pq" // Standard "encoding/json" "errors" "time" ) type Node struct { ID int Name string ParentID int `db:"parent_id"` TypeID int `db:"type_id"` TypeName string `db:"type_name"` TypeSchema any `db:"type_schema"` TypeSchemaRaw []byte `db:"type_schema_raw" json:"-"` TypeIcon string `db:"type_icon"` Updated time.Time Data any DataRaw []byte `db:"data_raw" json:"-"` NumChildren int `db:"num_children"` Children []*Node } func GetNode(nodeID int) (node Node, err error) { // {{{ row := db.QueryRowx(` SELECT n.id, COALESCE(n.parent_id, -1) AS parent_id, n.name, n.updated, n.data AS data_raw, t.id AS type_id, t.name AS type_name, t.schema AS type_schema_raw, t.schema->>'icon' AS type_icon FROM public.node n INNER JOIN public.type t ON n.type_id = t.id WHERE n.id = $1 `, nodeID) err = row.StructScan(&node) if err != nil { err = werr.Wrap(err) return } err = json.Unmarshal(node.TypeSchemaRaw, &node.TypeSchema) if err != nil { err = werr.Wrap(err) return } err = json.Unmarshal(node.DataRaw, &node.Data) if err != nil { err = werr.Wrap(err) return } return } // }}} func GetNodeTree(startNodeID, maxDepth int) (topNode *Node, err error) { // {{{ nodes := make(map[int]*Node) var rows *sqlx.Rows rows, err = GetNodeRows(startNodeID, maxDepth) if err != nil { err = werr.Wrap(err) return } defer rows.Close() first := true for rows.Next() { var node Node err = rows.StructScan(&node) if err != nil { err = werr.Wrap(err) return } if first { topNode = &node first = false } ComposeTree(nodes, &node) } return } // }}} func GetNodeRows(startNodeID, maxDepth int) (rows *sqlx.Rows, err error) { // {{{ rows, err = db.Queryx(` WITH RECURSIVE nodes AS ( SELECT $1::int AS id, 0 AS depth UNION SELECT n.id, ns.depth+1 AS depth FROM node n INNER JOIN nodes ns ON ns.depth < $2 AND n.parent_id = ns.id ) SEARCH DEPTH FIRST BY id SET ordercol SELECT COALESCE(n.parent_id, -1) AS parent_id, n.id, CONCAT(REPEAT(' ', ns.depth), n.name) AS name, n.type_id, t.name AS type_name, COALESCE(t.schema->>'icon', '') AS type_icon, n.updated, n.data AS data_raw, COUNT(node_children.id) AS num_children FROM nodes ns INNER JOIN public.node n ON ns.id = n.id INNER JOIN public.type t ON n.type_id = t.id LEFT JOIN node node_children ON node_children.parent_id = n.id GROUP BY ns.depth, n.parent_id, n.id, t.name, t.schema, ns.ordercol ORDER BY ordercol `, startNodeID, maxDepth, ) if err != nil { err = werr.Wrap(err) } return } // }}} func ComposeTree(nodes map[int]*Node, node *Node) { // {{{ if node.Children == nil { node.Children = []*Node{} } parentNode, found := nodes[node.ParentID] if found { if parentNode.Children == nil { parentNode.Children = []*Node{node} } else { parentNode.Children = append(parentNode.Children, node) } } nodes[node.ID] = node } // }}} func UpdateNode(nodeID int, data []byte) (err error) { // {{{ _, err = db.Exec(`UPDATE public.node SET data=$2 WHERE id=$1`, nodeID, data) return } // }}} func RenameNode(nodeID int, name string) (err error) { // {{{ _, err = db.Exec(`UPDATE node SET name=$2 WHERE id=$1`, nodeID, name) return } // }}} func CreateNode(parentNodeID, typeID int, name string) (nodeID int, err error) { // {{{ j, _ := json.Marshal( struct { New bool `json:"x-new"` }{ true, }) row := db.QueryRow(` INSERT INTO node(parent_id, type_id, name, data) VALUES($1, $2, $3, $4::jsonb) RETURNING id `, parentNodeID, typeID, name, j) err = row.Scan(&nodeID) if err != nil { err = werr.Wrap(err) } return } // }}} func DeleteNode(nodeID int) (err error) { // {{{ _, err = db.Exec(`DELETE FROM node WHERE id=$1`, nodeID) if err != nil { pqErr, ok := err.(*pq.Error) if ok && pqErr.Code == "23503" { err = errors.New("Can't delete a node with children.") return } } return } // }}} // vim: foldmethod=marker