datagraph/node.go
2025-08-18 18:35:40 +02:00

470 lines
9.3 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"`
ConnectionID int
ConnectionData any
Updated time.Time
Data any
DataRaw []byte `db:"data_raw" json:"-"`
NumChildren int `db:"num_children"`
Children []*Node
ConnectedNodes []Node
ScriptHooks []Hook
}
func GetNode(nodeID int) (node Node, err error) { // {{{
row := db.QueryRowx(`
SELECT
to_json(res) AS node
FROM (
SELECT
n.id,
COALESCE(n.parent_id, -1) AS ParentID,
n.name,
n.updated,
n.data AS data,
t.id AS TypeID,
t.name AS TypeName,
t.schema AS TypeSchema,
t.schema->>'icon' AS TypeIcon,
COALESCE(
(
SELECT jsonb_agg(res)
FROM (
SELECT
nn.id,
COALESCE(nn.parent_id, -1) AS ParentID,
nn.name,
nn.updated,
nn.data AS data,
tt.id AS TypeID,
tt.name AS TypeName,
tt.schema AS TypeSchema,
tt.schema->>'icon' AS TypeIcon,
c.id AS ConnectionID,
c.data AS ConnectionData
FROM connection c
INNER JOIN public.node nn ON c.child_node_id = nn.id
INNER JOIN public.type tt ON nn.type_id = tt.id
WHERE
c.parent_node_id = n.id
) AS res
)
, '[]'::jsonb
) AS ConnectedNodes,
COALESCE(
(
SELECT jsonb_agg(res)
FROM (
SELECT
h.id,
to_jsonb(s) AS script,
ssh,
env,
schedule_on_child_update AS ScheduleOnChildUpdate
FROM hook h
INNER JOIN public.script s ON h.script_id = s.id
WHERE
h.node_id = n.id
ORDER BY
s.group ASC,
s.name ASC
) AS res
)
, '[]'::jsonb
) AS ScriptHooks
FROM public.node n
INNER JOIN public.type t ON n.type_id = t.id
WHERE
n.id = $1
) res
`,
nodeID,
)
var body []byte
err = row.Scan(&body)
if err != nil {
err = werr.Wrap(err)
return
}
err = json.Unmarshal(body, &node)
if err != nil {
err = werr.Wrap(err)
return
}
return
} // }}}
func GetNodeTree(startNodeID, maxDepth int, withData bool) (topNode *Node, err error) { // {{{
nodes := make(map[int]*Node)
var nodesFromRow []Node
row := db.QueryRow(`
SELECT json_agg(res) FROM (
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 AND n.id != ns.id
)
SEARCH DEPTH FIRST BY id SET ordercol
SELECT
COALESCE(n.parent_id, -1) AS ParentID,
n.id,
n.name,
n.type_id AS TypeID,
t.name AS TypeName,
COALESCE(t.schema->>'icon', '') AS TypeIcon,
n.updated,
n.data AS data,
COUNT(node_children.id) AS NumChildren,
COALESCE(
(
SELECT jsonb_agg(res)
FROM (
SELECT
nn.ID,
COALESCE(nn.parent_id, -1) AS ParentID,
nn.Name,
nn.Updated,
nn.data AS Data,
tt.id AS TypeID,
tt.name AS TypeName,
tt.schema AS TypeSchema,
tt.schema->>'icon' AS TypeIcon,
c.id AS ConnectionID,
c.data AS ConnectionData
FROM connection c
INNER JOIN public.node nn ON c.child_node_id = nn.id
INNER JOIN public.type tt ON nn.type_id = tt.id
WHERE
c.parent_node_id = n.id
) AS res
)
, '[]'::jsonb
) AS ConnectedNodes
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
) AS res
`,
startNodeID,
maxDepth,
)
var body []byte
err = row.Scan(&body)
if err != nil {
err = werr.Wrap(err)
return
}
err = json.Unmarshal(body, &nodesFromRow)
if err != nil {
err = werr.Wrap(err)
return
}
first := true
for _, node := range nodesFromRow {
if first {
topNode = &node
first = false
}
ComposeTree(nodes, &node)
}
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.child_node_id = n.id
INNER JOIN public.type t ON n.type_id = t.id
WHERE
c.parent_node_id = $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
} // }}}
func ConnectNode(parentID, childID int) (err error) { // {{{
_, err = db.Exec(`INSERT INTO public.connection(parent_node_id, child_node_id) VALUES($1, $2)`, parentID, childID)
return
} // }}}
func SearchNodes(typeID int, search string, maxResults int) (nodes []Node, err error) { // {{{
nodes = []Node{}
row := db.QueryRowx(`
SELECT
COALESCE(json_agg(res), '[]'::json) AS node
FROM (
SELECT
n.id,
n.name,
n.updated,
n.data,
COALESCE(n.parent_id, 0) AS ParentID,
t.id AS TypeID,
t.name AS TypeName,
t.schema AS TypeSchema,
t.schema->>'icon' AS TypeIcon,
COALESCE(
(
SELECT jsonb_agg(res)
FROM (
SELECT
nn.id,
COALESCE(nn.parent_id, -1) AS ParentID,
nn.name,
nn.updated,
nn.data AS data,
tt.id AS TypeID,
tt.name AS TypeName,
tt.schema AS TypeSchema,
tt.schema->>'icon' AS TypeIcon,
c.id AS ConnectionID,
c.data AS ConnectionData
FROM connection c
INNER JOIN public.node nn ON c.child_node_id = nn.id
INNER JOIN public.type tt ON nn.type_id = tt.id
WHERE
c.parent_node_id = n.id
) AS res
)
, '[]'::jsonb
) AS ConnectedNodes
FROM public.node n
INNER JOIN public.type t ON n.type_id = t.id
WHERE
n.id > 0 AND
n.name ILIKE $2 AND
(CASE
WHEN $1 = -1 THEN true
ELSE
type_id = $1
END)
ORDER BY
t.schema->>'title' ASC,
UPPER(n.name) ASC
LIMIT $3
) AS res
`,
typeID,
search,
maxResults+1,
)
var body []byte
err = row.Scan(&body)
if err != nil {
err = werr.Wrap(err)
return
}
err = json.Unmarshal(body, &nodes)
if err != nil {
err = werr.Wrap(err)
return
}
return
} // }}}
func UpdateConnection(connID int, data []byte) (err error) { // {{{
_, err = db.Exec(`UPDATE public.connection SET data=$2 WHERE id=$1`, connID, data)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok && pqErr.Code == "22P02" {
err = errors.New("Invalid JSON")
} else {
err = werr.Wrap(err)
}
return
}
return
} // }}}
func DeleteConnection(connID int) (err error) { // {{{
_, err = db.Exec(`DELETE FROM public.connection WHERE id=$1`, connID)
if err != nil {
err = werr.Wrap(err)
return
}
return
} // }}}
// vim: foldmethod=marker