Notes/node.go

806 lines
14 KiB
Go
Raw Permalink Normal View History

2023-06-17 09:11:14 +02:00
package main
import (
// External
"github.com/jmoiron/sqlx"
2024-04-03 17:37:32 +02:00
werr "git.gibonuddevalla.se/go/wrappederror"
2023-06-17 09:11:14 +02:00
// Standard
"time"
2024-03-28 21:49:48 +01:00
"database/sql"
2023-06-17 09:11:14 +02:00
)
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
}
2023-06-17 09:11:14 +02:00
type Node struct {
ID int
UserID int `db:"user_id"`
ParentID int `db:"parent_id"`
2023-07-12 22:35:38 +02:00
CryptoKeyID int `db:"crypto_key_id"`
2023-06-17 09:11:14 +02:00
Name string
Content string
Updated time.Time
2023-06-17 09:11:14 +02:00
Children []Node
Crumbs []Node
2023-06-22 08:28:51 +02:00
Files []File
2023-06-17 09:11:14 +02:00
Complete bool
Level int
ChecklistGroups []ChecklistGroup
ContentEncrypted string `db:"content_encrypted" json:"-"`
2024-01-09 16:28:40 +01:00
Markdown bool
2023-06-17 09:11:14 +02:00
}
2024-01-09 16:28:40 +01:00
func NodeTree(userID, startNodeID int) (nodes []Node, err error) { // {{{
var rows *sqlx.Rows
2024-01-05 20:00:02 +01:00
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
`,
2024-01-05 20:00:02 +01:00
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
2024-01-05 21:14:55 +01:00
node.Crumbs = []Node{}
node.Children = []Node{}
node.Files = []File{}
if err = rows.StructScan(&node); err != nil {
return
}
nodes = append(nodes, node)
}
return
2024-01-09 16:28:40 +01:00
} // }}}
func RootNode(userID int) (node Node, err error) { // {{{
2023-06-17 09:11:14 +02:00
var rows *sqlx.Rows
2024-01-05 20:00:02 +01:00
rows, err = service.Db.Conn.Queryx(`
2023-06-17 09:11:14 +02:00
SELECT
id,
user_id,
0 AS parent_id,
name
FROM node
WHERE
user_id = $1 AND
parent_id IS NULL
`,
2024-01-05 20:00:02 +01:00
userID,
2023-06-17 09:11:14 +02:00
)
if err != nil {
return
}
defer rows.Close()
node.Name = "Start"
2024-01-05 20:00:02 +01:00
node.UserID = userID
2023-06-17 09:11:14 +02:00
node.Complete = true
node.Children = []Node{}
2024-01-09 16:28:40 +01:00
node.Crumbs = []Node{}
node.Files = []File{}
2023-06-17 09:11:14 +02:00
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
2024-01-09 16:28:40 +01:00
} // }}}
func RetrieveNode(userID, nodeID int) (node Node, err error) { // {{{
2023-06-17 09:11:14 +02:00
if nodeID == 0 {
2024-01-05 20:00:02 +01:00
return RootNode(userID)
2023-06-17 09:11:14 +02:00
}
var rows *sqlx.Rows
2024-01-05 20:00:02 +01:00
rows, err = service.Db.Conn.Queryx(`
2023-06-17 09:11:14 +02:00
WITH RECURSIVE recurse AS (
SELECT
id,
user_id,
COALESCE(parent_id, 0) AS parent_id,
2023-07-12 22:35:38 +02:00
COALESCE(crypto_key_id, 0) AS crypto_key_id,
2023-06-17 09:11:14 +02:00
name,
content,
content_encrypted,
2024-01-09 16:28:40 +01:00
markdown,
2023-06-17 09:11:14 +02:00
0 AS level
FROM node
WHERE
user_id = $1 AND
id = $2
UNION
SELECT
n.id,
n.user_id,
n.parent_id,
2023-07-12 22:35:38 +02:00
COALESCE(n.crypto_key_id, 0) AS crypto_key_id,
2023-06-17 09:11:14 +02:00
n.name,
'' AS content,
'' AS content_encrypted,
2024-01-09 16:28:40 +01:00
false AS markdown,
2023-06-17 09:11:14 +02:00
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
`,
2024-01-05 20:00:02 +01:00
userID,
2023-06-17 09:11:14 +02:00
nodeID,
)
if err != nil {
return
}
defer rows.Close()
type resultRow struct {
Node
Level int
}
2023-06-18 20:13:35 +02:00
node = Node{}
node.Children = []Node{}
2023-06-17 09:11:14 +02:00
for rows.Next() {
row := resultRow{}
if err = rows.StructScan(&row); err != nil {
return
}
if row.Level == 0 {
2024-01-09 16:28:40 +01:00
node.ID = row.ID
node.UserID = row.UserID
node.ParentID = row.ParentID
2023-07-12 22:35:38 +02:00
node.CryptoKeyID = row.CryptoKeyID
2024-01-09 16:28:40 +01:00
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()
2023-06-17 09:11:14 +02:00
}
if row.Level == 1 {
node.Children = append(node.Children, Node{
2023-07-12 22:35:38 +02:00
ID: row.ID,
UserID: row.UserID,
ParentID: row.ParentID,
CryptoKeyID: row.CryptoKeyID,
Name: row.Name,
2023-06-17 09:11:14 +02:00
})
}
}
2024-01-05 20:00:02 +01:00
node.Crumbs, err = NodeCrumbs(node.ID)
node.Files, err = Files(userID, node.ID, 0)
2023-06-17 09:11:14 +02:00
return
2024-01-09 16:28:40 +01:00
} // }}}
func NodeCrumbs(nodeID int) (nodes []Node, err error) { // {{{
2023-06-17 09:11:14 +02:00
var rows *sqlx.Rows
2024-01-05 20:00:02 +01:00
rows, err = service.Db.Conn.Queryx(`
2023-06-17 09:11:14 +02:00
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()
2024-01-09 16:28:40 +01:00
2023-06-17 09:11:14 +02:00
nodes = []Node{}
for rows.Next() {
node := Node{}
if err = rows.StructScan(&node); err != nil {
return
}
nodes = append(nodes, node)
}
return
2024-01-09 16:28:40 +01:00
} // }}}
func CreateNode(userID, parentID int, name string) (node Node, err error) { // {{{
2023-06-18 20:13:35 +02:00
var rows *sqlx.Rows
2024-01-05 20:00:02 +01:00
rows, err = service.Db.Conn.Queryx(`
2023-06-18 20:13:35 +02:00
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
`,
2024-01-05 20:00:02 +01:00
userID,
2023-06-18 20:13:35 +02:00
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{}
2023-06-22 08:28:51 +02:00
node.Files = []File{}
2023-06-18 20:13:35 +02:00
node.Complete = true
}
2024-01-05 20:00:02 +01:00
node.Crumbs, err = NodeCrumbs(node.ID)
2023-06-18 20:13:35 +02:00
return
2024-01-09 16:28:40 +01:00
} // }}}
2024-03-30 09:46:48 +01:00
func UpdateNode(userID, nodeID, timeOffset int, content string, cryptoKeyID int, markdown bool) (err error) { // {{{
2024-04-18 20:08:07 +02:00
if nodeID == 0 {
return
}
2024-04-03 17:37:32 +02:00
var timezone string
2024-04-17 18:43:24 +02:00
row := service.Db.Conn.QueryRow(`SELECT timezone FROM _webservice.user WHERE id=$1`, userID)
2024-04-03 17:37:32 +02:00
err = row.Scan(&timezone)
if err != nil {
err = werr.Wrap(err).WithCode("002-000F")
return
}
2024-03-28 21:49:48 +01:00
var scannedSchedules, dbSchedules, add, remove []Schedule
2024-04-03 17:37:32 +02:00
scannedSchedules = ScanForSchedules(timezone, content)
2024-03-28 21:49:48 +01:00
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 {
2024-03-28 21:49:48 +01:00
_, err = tsx.Exec(`
UPDATE node
SET
content = '',
content_encrypted = $1,
2024-01-09 16:28:40 +01:00
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,
2024-01-05 20:00:02 +01:00
userID,
2024-01-09 16:28:40 +01:00
markdown,
)
} else {
2024-03-28 21:49:48 +01:00
_, 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,
2024-01-05 20:00:02 +01:00
userID,
markdown,
)
}
2024-03-28 21:49:48 +01:00
if err != nil {
tsx.Rollback()
return
}
err = tsx.Commit()
2023-06-18 20:13:35 +02:00
return
2024-01-09 16:28:40 +01:00
} // }}}
func RenameNode(userID, nodeID int, name string) (err error) { // {{{
2024-01-05 20:00:02 +01:00
_, err = service.Db.Conn.Exec(`
2023-06-18 22:05:10 +02:00
UPDATE node SET name = $1 WHERE user_id = $2 AND id = $3
`,
name,
2024-01-05 20:00:02 +01:00
userID,
2023-06-18 22:05:10 +02:00
nodeID,
)
return
2024-01-09 16:28:40 +01:00
} // }}}
func DeleteNode(userID, nodeID int) (err error) { // {{{
2024-01-05 20:00:02 +01:00
_, err = service.Db.Conn.Exec(`
2023-06-18 22:05:10 +02:00
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
)`,
2024-01-05 20:00:02 +01:00
userID,
2023-06-18 22:05:10 +02:00
nodeID,
)
return
2024-01-09 16:28:40 +01:00
} // }}}
func SearchNodes(userID int, search string) (nodes []Node, err error) { // {{{
2023-07-19 10:00:36 +02:00
nodes = []Node{}
var rows *sqlx.Rows
2024-01-05 20:00:02 +01:00
rows, err = service.Db.Conn.Queryx(`
2023-07-19 10:00:36 +02:00
SELECT
id,
user_id,
COALESCE(parent_id, 0) AS parent_id,
name,
updated
FROM node
WHERE
2024-01-05 21:14:55 +01:00
user_id = $1 AND
2023-07-19 10:00:36 +02:00
crypto_key_id IS NULL AND
2023-07-20 07:27:29 +02:00
(
2024-01-05 20:00:02 +01:00
content ~* $2 OR
name ~* $2
2023-07-20 07:27:29 +02:00
)
2023-07-19 10:00:36 +02:00
ORDER BY
updated DESC
2024-01-05 20:00:02 +01:00
`, userID, search)
2023-07-19 10:00:36 +02:00
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
2024-01-09 16:28:40 +01:00
} // }}}
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
} // }}}
2024-01-13 10:01:10 +01:00
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
} // }}}
2023-06-17 09:11:14 +02:00
// vim: foldmethod=marker