Files
box-manifest-android/server/internal/tree/metadata.go
T

320 lines
9.0 KiB
Go

package tree
import (
"context"
"database/sql"
"errors"
"strings"
)
type NodeType struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
IconKey *string `json:"iconKey"`
Color *string `json:"color"`
}
type Tag struct {
ID string `json:"id"`
Name string `json:"name"`
Color *string `json:"color"`
}
func (s *Store) ListNodeTypes(ctx context.Context) ([]NodeType, error) {
rows, err := s.db.QueryContext(ctx, `SELECT id, name, description, icon_key, color FROM node_types ORDER BY name COLLATE NOCASE`)
if err != nil {
return nil, err
}
defer rows.Close()
result := []NodeType{}
for rows.Next() {
value, err := scanNodeType(rows)
if err != nil {
return nil, err
}
result = append(result, value)
}
return result, rows.Err()
}
func (s *Store) CreateNodeType(ctx context.Context, name string, description, iconKey, color *string) (NodeType, error) {
name = strings.TrimSpace(name)
if name == "" {
return NodeType{}, ErrInvalid
}
id, err := newID()
if err != nil {
return NodeType{}, err
}
if _, err = s.db.ExecContext(ctx, `INSERT INTO node_types (id, name, description, icon_key, color) VALUES (?, ?, ?, ?, ?)`, id, name, cleanOptionalString(description), cleanOptionalString(iconKey), cleanOptionalString(color)); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "unique") {
return NodeType{}, ErrConflict
}
return NodeType{}, err
}
return s.GetNodeType(ctx, id)
}
func (s *Store) GetNodeType(ctx context.Context, id string) (NodeType, error) {
return scanNodeType(s.db.QueryRowContext(ctx, `SELECT id, name, description, icon_key, color FROM node_types WHERE id = ?`, id))
}
func (s *Store) UpdateNodeType(ctx context.Context, id, name string, description, iconKey, color *string) (NodeType, error) {
name = strings.TrimSpace(name)
if name == "" {
return NodeType{}, ErrInvalid
}
result, err := s.db.ExecContext(ctx, `UPDATE node_types SET name = ?, description = ?, icon_key = ?, color = ? WHERE id = ?`, name, cleanOptionalString(description), cleanOptionalString(iconKey), cleanOptionalString(color), id)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "unique") {
return NodeType{}, ErrConflict
}
return NodeType{}, err
}
if count, _ := result.RowsAffected(); count == 0 {
return NodeType{}, ErrNotFound
}
return s.GetNodeType(ctx, id)
}
func (s *Store) DeleteNodeType(ctx context.Context, id string) error {
result, err := s.db.ExecContext(ctx, `DELETE FROM node_types WHERE id = ?`, id)
if err != nil {
return err
}
if count, _ := result.RowsAffected(); count == 0 {
return ErrNotFound
}
return nil
}
func (s *Store) ListTags(ctx context.Context) ([]Tag, error) {
rows, err := s.db.QueryContext(ctx, `SELECT id, name, color FROM tags ORDER BY name COLLATE NOCASE`)
if err != nil {
return nil, err
}
defer rows.Close()
result := []Tag{}
for rows.Next() {
value, err := scanTag(rows)
if err != nil {
return nil, err
}
result = append(result, value)
}
return result, rows.Err()
}
func (s *Store) CreateTag(ctx context.Context, name string, color *string) (Tag, error) {
name = strings.TrimSpace(name)
if name == "" {
return Tag{}, ErrInvalid
}
id, err := newID()
if err != nil {
return Tag{}, err
}
if _, err = s.db.ExecContext(ctx, `INSERT INTO tags (id, name, color) VALUES (?, ?, ?)`, id, name, cleanOptionalString(color)); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "unique") {
return Tag{}, ErrConflict
}
return Tag{}, err
}
return s.GetTag(ctx, id)
}
func (s *Store) GetTag(ctx context.Context, id string) (Tag, error) {
return scanTag(s.db.QueryRowContext(ctx, `SELECT id, name, color FROM tags WHERE id = ?`, id))
}
func (s *Store) UpdateTag(ctx context.Context, id, name string, color *string) (Tag, error) {
name = strings.TrimSpace(name)
if name == "" {
return Tag{}, ErrInvalid
}
result, err := s.db.ExecContext(ctx, `UPDATE tags SET name = ?, color = ? WHERE id = ?`, name, cleanOptionalString(color), id)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "unique") {
return Tag{}, ErrConflict
}
return Tag{}, err
}
if count, _ := result.RowsAffected(); count == 0 {
return Tag{}, ErrNotFound
}
return s.GetTag(ctx, id)
}
func (s *Store) DeleteTag(ctx context.Context, id string) error {
result, err := s.db.ExecContext(ctx, `DELETE FROM tags WHERE id = ?`, id)
if err != nil {
return err
}
if count, _ := result.RowsAffected(); count == 0 {
return ErrNotFound
}
return nil
}
func (s *Store) SetClassification(ctx context.Context, nodeID string, typeID *string, tagIDs []string) (Node, error) {
if hasDuplicateIDsAllowEmpty(tagIDs) {
return Node{}, ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return Node{}, err
}
defer tx.Rollback()
if _, err := getWith(ctx, tx, nodeID); err != nil {
return Node{}, err
}
if typeID != nil {
if _, err := scanNodeType(tx.QueryRowContext(ctx, `SELECT id, name, description, icon_key, color FROM node_types WHERE id = ?`, *typeID)); err != nil {
return Node{}, err
}
}
for _, tagID := range tagIDs {
if _, err := scanTag(tx.QueryRowContext(ctx, `SELECT id, name, color FROM tags WHERE id = ?`, tagID)); err != nil {
return Node{}, err
}
}
if _, err := tx.ExecContext(ctx, `UPDATE nodes SET type_id = ? WHERE id = ?`, typeID, nodeID); err != nil {
return Node{}, err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM node_tags WHERE node_id = ?`, nodeID); err != nil {
return Node{}, err
}
for _, tagID := range tagIDs {
if _, err := tx.ExecContext(ctx, `INSERT INTO node_tags (node_id, tag_id) VALUES (?, ?)`, nodeID, tagID); err != nil {
return Node{}, err
}
}
if err := tx.Commit(); err != nil {
return Node{}, err
}
return s.Get(ctx, nodeID)
}
func (s *Store) ClassifyMany(ctx context.Context, nodeIDs []string, setType bool, typeID *string, addTagIDs, removeTagIDs []string) error {
if len(nodeIDs) == 0 || hasDuplicateIDs(nodeIDs) || hasDuplicateIDsAllowEmpty(addTagIDs) || hasDuplicateIDsAllowEmpty(removeTagIDs) {
return ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if typeID != nil {
if _, err := scanNodeType(tx.QueryRowContext(ctx, `SELECT id, name, description, icon_key, color FROM node_types WHERE id = ?`, *typeID)); err != nil {
return err
}
}
for _, tagID := range append(append([]string{}, addTagIDs...), removeTagIDs...) {
if _, err := scanTag(tx.QueryRowContext(ctx, `SELECT id, name, color FROM tags WHERE id = ?`, tagID)); err != nil {
return err
}
}
for _, nodeID := range nodeIDs {
if nodeID == RootID || nodeID == UnsortedID {
return ErrConflict
}
if _, err := getWith(ctx, tx, nodeID); err != nil {
return err
}
if setType {
if _, err := tx.ExecContext(ctx, `UPDATE nodes SET type_id = ? WHERE id = ?`, typeID, nodeID); err != nil {
return err
}
}
for _, tagID := range addTagIDs {
if _, err := tx.ExecContext(ctx, `INSERT OR IGNORE INTO node_tags (node_id, tag_id) VALUES (?, ?)`, nodeID, tagID); err != nil {
return err
}
}
for _, tagID := range removeTagIDs {
if _, err := tx.ExecContext(ctx, `DELETE FROM node_tags WHERE node_id = ? AND tag_id = ?`, nodeID, tagID); err != nil {
return err
}
}
}
return tx.Commit()
}
func (s *Store) loadTagIDs(ctx context.Context, node *Node) error {
rows, err := s.db.QueryContext(ctx, `SELECT tag_id FROM node_tags WHERE node_id = ? ORDER BY tag_id`, node.ID)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return err
}
node.TagIDs = append(node.TagIDs, id)
}
return rows.Err()
}
func (s *Store) loadTreeTagIDs(ctx context.Context, byID map[string]*TreeNode) error {
rows, err := s.db.QueryContext(ctx, `SELECT node_id, tag_id FROM node_tags ORDER BY tag_id`)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var nodeID, tagID string
if err := rows.Scan(&nodeID, &tagID); err != nil {
return err
}
if node := byID[nodeID]; node != nil {
node.TagIDs = append(node.TagIDs, tagID)
}
}
return rows.Err()
}
func scanNodeType(row scanner) (NodeType, error) {
var value NodeType
var description, iconKey, color sql.NullString
if err := row.Scan(&value.ID, &value.Name, &description, &iconKey, &color); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return NodeType{}, ErrNotFound
}
return NodeType{}, err
}
if description.Valid {
value.Description = &description.String
}
if iconKey.Valid {
value.IconKey = &iconKey.String
}
if color.Valid {
value.Color = &color.String
}
return value, nil
}
func scanTag(row scanner) (Tag, error) {
var value Tag
var color sql.NullString
if err := row.Scan(&value.ID, &value.Name, &color); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return Tag{}, ErrNotFound
}
return Tag{}, err
}
if color.Valid {
value.Color = &color.String
}
return value, nil
}
func hasDuplicateIDsAllowEmpty(ids []string) bool {
if len(ids) == 0 {
return false
}
return hasDuplicateIDs(ids)
}