279 lines
5.4 KiB
Go
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
|