package main import ( // External "github.com/jmoiron/sqlx" werr "git.gibonuddevalla.se/go/wrappederror" // Standard "time" "database/sql" ) type ChecklistItem struct { ID int GroupID int `db:"checklist_group_id"` Order int Label string Checked bool } type ChecklistGroup struct { ID int NodeID int `db:"node_id"` Order int Label string Items []ChecklistItem } type Node struct { ID int UserID int `db:"user_id"` ParentID int `db:"parent_id"` CryptoKeyID int `db:"crypto_key_id"` Name string Content string Updated time.Time Children []Node Crumbs []Node Files []File Complete bool Level int ChecklistGroups []ChecklistGroup ContentEncrypted string `db:"content_encrypted" json:"-"` Markdown bool } func NodeTree(userID, startNodeID int) (nodes []Node, err error) { // {{{ var rows *sqlx.Rows rows, err = service.Db.Conn.Queryx(` WITH RECURSIVE nodetree AS ( SELECT *, array[name::text] AS path, 0 AS level FROM node WHERE user_id = $1 AND CASE $2::int WHEN 0 THEN parent_id IS NULL ELSE parent_id = $2 END UNION ALL SELECT n.*, path||n.name::text AS path, nt.level + 1 AS level FROM node n INNER JOIN nodetree nt ON n.parent_id = nt.id ) SELECT id, user_id, COALESCE(parent_id, 0) AS parent_id, name, updated, level FROM nodetree ORDER BY path ASC `, userID, startNodeID, ) if err != nil { return } defer rows.Close() type resultRow struct { Node Level int } nodes = []Node{} for rows.Next() { node := Node{} node.Complete = false node.Crumbs = []Node{} node.Children = []Node{} node.Files = []File{} if err = rows.StructScan(&node); err != nil { return } nodes = append(nodes, node) } return } // }}} func RootNode(userID int) (node Node, err error) { // {{{ var rows *sqlx.Rows rows, err = service.Db.Conn.Queryx(` SELECT id, user_id, 0 AS parent_id, name FROM node WHERE user_id = $1 AND parent_id IS NULL `, userID, ) if err != nil { return } defer rows.Close() node.Name = "Start" node.UserID = userID node.Complete = true node.Children = []Node{} node.Crumbs = []Node{} node.Files = []File{} for rows.Next() { row := Node{} if err = rows.StructScan(&row); err != nil { return } node.Children = append(node.Children, Node{ ID: row.ID, UserID: row.UserID, ParentID: row.ParentID, Name: row.Name, }) } return } // }}} func RetrieveNode(userID, nodeID int) (node Node, err error) { // {{{ if nodeID == 0 { return RootNode(userID) } var rows *sqlx.Rows rows, err = service.Db.Conn.Queryx(` WITH RECURSIVE recurse AS ( SELECT id, user_id, COALESCE(parent_id, 0) AS parent_id, COALESCE(crypto_key_id, 0) AS crypto_key_id, name, content, content_encrypted, markdown, 0 AS level FROM node WHERE user_id = $1 AND id = $2 UNION SELECT n.id, n.user_id, n.parent_id, COALESCE(n.crypto_key_id, 0) AS crypto_key_id, n.name, '' AS content, '' AS content_encrypted, false AS markdown, r.level + 1 AS level FROM node n INNER JOIN recurse r ON n.parent_id = r.id AND r.level = 0 WHERE n.user_id = $1 ) SELECT * FROM recurse ORDER BY level ASC `, userID, nodeID, ) if err != nil { return } defer rows.Close() type resultRow struct { Node Level int } node = Node{} node.Children = []Node{} for rows.Next() { row := resultRow{} if err = rows.StructScan(&row); err != nil { return } if row.Level == 0 { node.ID = row.ID node.UserID = row.UserID node.ParentID = row.ParentID node.CryptoKeyID = row.CryptoKeyID node.Name = row.Name node.Complete = true node.Markdown = row.Markdown if node.CryptoKeyID > 0 { node.Content = row.ContentEncrypted } else { node.Content = row.Content } node.retrieveChecklist() } if row.Level == 1 { node.Children = append(node.Children, Node{ ID: row.ID, UserID: row.UserID, ParentID: row.ParentID, CryptoKeyID: row.CryptoKeyID, Name: row.Name, }) } } node.Crumbs, err = NodeCrumbs(node.ID) node.Files, err = Files(userID, node.ID, 0) return } // }}} func NodeCrumbs(nodeID int) (nodes []Node, err error) { // {{{ var rows *sqlx.Rows rows, err = service.Db.Conn.Queryx(` WITH RECURSIVE nodes AS ( SELECT id, COALESCE(parent_id, 0) AS parent_id, name FROM node WHERE id = $1 UNION SELECT n.id, COALESCE(n.parent_id, 0) AS parent_id, n.name FROM node n INNER JOIN nodes nr ON n.id = nr.parent_id ) SELECT * FROM nodes `, nodeID) if err != nil { return } defer rows.Close() nodes = []Node{} for rows.Next() { node := Node{} if err = rows.StructScan(&node); err != nil { return } nodes = append(nodes, node) } return } // }}} func CreateNode(userID, parentID int, name string) (node Node, err error) { // {{{ var rows *sqlx.Rows rows, err = service.Db.Conn.Queryx(` INSERT INTO node(user_id, parent_id, name) VALUES($1, NULLIF($2, 0)::integer, $3) RETURNING id, user_id, COALESCE(parent_id, 0) AS parent_id, name, content `, userID, parentID, name, ) if err != nil { return } defer rows.Close() for rows.Next() { node = Node{} if err = rows.StructScan(&node); err != nil { return } node.Children = []Node{} node.Files = []File{} node.Complete = true } node.Crumbs, err = NodeCrumbs(node.ID) return } // }}} func UpdateNode(userID, nodeID, timeOffset int, content string, cryptoKeyID int, markdown bool) (err error) { // {{{ if nodeID == 0 { return } var timezone string row := service.Db.Conn.QueryRow(`SELECT timezone FROM _webservice.user WHERE id=$1`, userID) err = row.Scan(&timezone) if err != nil { err = werr.Wrap(err).WithCode("002-000F") return } var scannedSchedules, dbSchedules, add, remove []Schedule scannedSchedules = ScanForSchedules(timezone, content) for i := range scannedSchedules { scannedSchedules[i].Node.ID = nodeID scannedSchedules[i].UserID = userID } var tsx *sql.Tx tsx, err = service.Db.Conn.Begin() if err != nil { return } dbSchedules, err = RetrieveSchedules(userID, nodeID) if err != nil { tsx.Rollback() return } for _, scanned := range scannedSchedules { found := false for _, db := range dbSchedules { if scanned.IsEqual(db) { found = true break } } if !found { add = append(add, scanned) } } for _, db := range dbSchedules { found := false for _, scanned := range scannedSchedules { if db.IsEqual(scanned) { found = true break } } if !found { remove = append(remove, db) } } for _, event := range remove { err = event.Delete(tsx) if err != nil { tsx.Rollback() return } } for _, event := range add { err = event.Insert(tsx) if err != nil { tsx.Rollback() return } } if cryptoKeyID > 0 { _, err = tsx.Exec(` UPDATE node SET content = '', content_encrypted = $1, markdown = $5, crypto_key_id = CASE $2::int WHEN 0 THEN NULL ELSE $2 END WHERE id = $3 AND user_id = $4 `, content, cryptoKeyID, nodeID, userID, markdown, ) } else { _, err = tsx.Exec(` UPDATE node SET content = $1, content_encrypted = '', markdown = $5, crypto_key_id = CASE $2::int WHEN 0 THEN NULL ELSE $2 END WHERE id = $3 AND user_id = $4 `, content, cryptoKeyID, nodeID, userID, markdown, ) } if err != nil { tsx.Rollback() return } err = tsx.Commit() return } // }}} func RenameNode(userID, nodeID int, name string) (err error) { // {{{ _, err = service.Db.Conn.Exec(` UPDATE node SET name = $1 WHERE user_id = $2 AND id = $3 `, name, userID, nodeID, ) return } // }}} func DeleteNode(userID, nodeID int) (err error) { // {{{ _, err = service.Db.Conn.Exec(` WITH RECURSIVE nodetree AS ( SELECT id, parent_id FROM node WHERE user_id = $1 AND id = $2 UNION SELECT n.id, n.parent_id FROM node n INNER JOIN nodetree nt ON n.parent_id = nt.id ) DELETE FROM node WHERE id IN ( SELECT id FROM nodetree )`, userID, nodeID, ) return } // }}} func SearchNodes(userID int, search string) (nodes []Node, err error) { // {{{ nodes = []Node{} var rows *sqlx.Rows rows, err = service.Db.Conn.Queryx(` SELECT id, user_id, COALESCE(parent_id, 0) AS parent_id, name, updated FROM node WHERE user_id = $1 AND crypto_key_id IS NULL AND ( content ~* $2 OR name ~* $2 ) ORDER BY updated DESC `, userID, search) if err != nil { return } defer rows.Close() for rows.Next() { node := Node{} node.Complete = false if err = rows.StructScan(&node); err != nil { return } nodes = append(nodes, node) } return } // }}} func ChecklistGroupAdd(userID, nodeID int, label string) (item ChecklistGroup, err error) { // {{{ var row *sqlx.Row row = service.Db.Conn.QueryRowx( ` INSERT INTO checklist_group(node_id, "order", "label") ( SELECT $1, MAX("order")+1 AS "order", $2 AS "label" FROM checklist_group g INNER JOIN node n ON g.node_id = n.id WHERE user_id = $3 AND node_id = $1 GROUP BY node_id ) UNION ( SELECT node.id AS node_id, 0 AS "order", $2 AS "label" FROM node WHERE user_id = $3 AND node.id = $1 ) ORDER BY "order" DESC LIMIT 1 RETURNING * `, nodeID, label, userID, ) err = row.StructScan(&item) return } // }}} func ChecklistGroupLabel(userID, checklistGroupID int, label string) (item ChecklistItem, err error) { // {{{ _, err = service.Db.Conn.Exec( ` UPDATE checklist_group g SET label = $3 FROM node n WHERE g.node_id = n.id AND n.user_id = $1 AND g.id = $2; `, userID, checklistGroupID, label, ) return } // }}} func ChecklistGroupItemAdd(userID, checklistGroupID int, label string) (item ChecklistItem, err error) { // {{{ var row *sqlx.Row row = service.Db.Conn.QueryRowx( ` INSERT INTO checklist_item(checklist_group_id, "order", "label") ( SELECT checklist_group_id, MAX("order")+1 AS "order", $1 AS "label" FROM checklist_item WHERE checklist_group_id = $2 GROUP BY checklist_group_id ) UNION ( SELECT $2 AS checklist_group_id, 0 AS "order", $1 AS "label" ) ORDER BY "order" DESC LIMIT 1 RETURNING * `, label, checklistGroupID, ) err = row.StructScan(&item) return } // }}} func ChecklistGroupDelete(userID, checklistGroupID int) (err error) { // {{{ _, err = service.Db.Conn.Exec( ` DELETE FROM checklist_group g USING node n WHERE g.id = $2 AND g.node_id = n.id AND n.user_id = $1 `, userID, checklistGroupID, ) return } // }}} func ChecklistItemState(userID, checklistItemID int, state bool) (err error) { // {{{ _, err = service.Db.Conn.Exec( ` UPDATE checklist_item i SET checked = $3 FROM checklist_group g, node n WHERE i.checklist_group_id = g.id AND g.node_id = n.id AND n.user_id = $1 AND i.id = $2; `, userID, checklistItemID, state, ) return } // }}} func ChecklistItemLabel(userID, checklistItemID int, label string) (err error) { // {{{ _, err = service.Db.Conn.Exec( ` UPDATE checklist_item i SET label = $3 FROM checklist_group g, node n WHERE i.checklist_group_id = g.id AND g.node_id = n.id AND n.user_id = $1 AND i.id = $2; `, userID, checklistItemID, label, ) return } // }}} func ChecklistItemDelete(userID, checklistItemID int) (err error) { // {{{ _, err = service.Db.Conn.Exec( ` DELETE FROM checklist_item i USING checklist_group g, node n WHERE i.id = $2 AND i.checklist_group_id = g.id AND g.node_id = n.id AND n.user_id = $1 `, userID, checklistItemID, ) return } // }}} func ChecklistItemMove(userID, checklistItemID, afterItemID int) (err error) { // {{{ _, err = service.Db.Conn.Exec( ` WITH "to" AS ( SELECT i.checklist_group_id AS group_id, i."order" FROM checklist_item i INNER JOIN checklist_group g ON i.checklist_group_id = g.id INNER JOIN node n ON g.node_id = n.id WHERE n.user_id = $1 AND i.id = $3 ), update_order AS ( UPDATE checklist_item SET "order" = CASE WHEN checklist_item."order" <= "to"."order" THEN checklist_item."order" - 1 WHEN checklist_item."order" > "to"."order" THEN checklist_item."order" + 1 END FROM "to" WHERE checklist_item.id != $2 AND checklist_item.checklist_group_id = "to".group_id ) UPDATE checklist_item SET checklist_group_id = "to".group_id, "order" = "to"."order" FROM "to" WHERE checklist_item.id = $2 `, userID, checklistItemID, afterItemID, ) return } // }}} func (node *Node) retrieveChecklist() (err error) { // {{{ var rows *sqlx.Rows rows, err = service.Db.Conn.Queryx(` SELECT g.id AS group_id, g.order AS group_order, g.label AS group_label, COALESCE(i.id, 0) AS item_id, COALESCE(i.order, 0) AS item_order, COALESCE(i.label, '') AS item_label, COALESCE(i.checked, false) AS checked FROM public.checklist_group g LEFT JOIN public.checklist_item i ON i.checklist_group_id = g.id WHERE g.node_id = $1 ORDER BY g.order DESC, i.order DESC `, node.ID) if err != nil { return } defer rows.Close() groups := make(map[int]*ChecklistGroup) var found bool var group *ChecklistGroup var item ChecklistItem for rows.Next() { row := struct { GroupID int `db:"group_id"` GroupOrder int `db:"group_order"` GroupLabel string `db:"group_label"` ItemID int `db:"item_id"` ItemOrder int `db:"item_order"` ItemLabel string `db:"item_label"` Checked bool }{} err = rows.StructScan(&row) if err != nil { return } if group, found = groups[row.GroupID]; !found { group = new(ChecklistGroup) group.ID = row.GroupID group.NodeID = node.ID group.Order = row.GroupOrder group.Label = row.GroupLabel group.Items = []ChecklistItem{} groups[group.ID] = group } item = ChecklistItem{} item.ID = row.ItemID item.GroupID = row.GroupID item.Order = row.ItemOrder item.Label = row.ItemLabel item.Checked = row.Checked if item.ID > 0 { group.Items = append(group.Items, item) } } node.ChecklistGroups = []ChecklistGroup{} for _, group := range groups { node.ChecklistGroups = append(node.ChecklistGroups, *group) } return } // }}} // vim: foldmethod=marker