datagraph/node.go
2025-07-07 21:44:09 +02:00

279 lines
5.4 KiB
Go

package main
import (
// External
werr "git.gibonuddevalla.se/go/wrappederror"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
// Standard
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"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,
n.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
} // }}}
func MoveNodes(newParentID int, nodeIDs []int) (err error) { // {{{
// TODO - implement a method to verify that a node isn't moved underneath itself.
// Preferably using a stored procedure?
var nodeIDStr []string
for _, n := range nodeIDs {
nodeIDStr = append(nodeIDStr, strconv.Itoa(n))
}
joinedIDs := strings.Join(nodeIDStr, ",")
sql := fmt.Sprintf(
`UPDATE node
SET parent_id=$1
WHERE id IN (%s)`,
joinedIDs,
)
_, err = db.Exec(sql, newParentID)
return
} // }}}
func GetNodeConnections(nodeID int) (connections []Node, err error) { // {{{
connections = []Node{}
var rows *sqlx.Rows
rows, err = db.Queryx(`
SELECT
n.*,
t.id AS type_id,
t.name AS type_name,
t.schema AS type_schema_raw,
t.schema->>'icon' AS type_icon
FROM public.connection c
INNER JOIN public.node n ON c.to = n.id
INNER JOIN public.type t ON n.type_id = t.id
WHERE
c.from = $1
`,
nodeID,
)
if err != nil {
err = werr.Wrap(err)
return
}
defer rows.Close()
for rows.Next() {
var node Node
err = rows.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
}
connections = append(connections, node)
}
return
} // }}}
// vim: foldmethod=marker