468 lines
9.2 KiB
Go
468 lines
9.2 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
|
|
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
|
|
)
|
|
|
|
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
|