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