package usecase import ( "context" "fmt" "net/url" "sort" "strings" "sync" "time" "github.com/bnema/dumber/internal/application/dto" "github.com/bnema/dumber/internal/application/port" "github.com/bnema/dumber/internal/domain/entity" "github.com/bnema/dumber/internal/domain/repository" domainurl "github.com/bnema/dumber/internal/domain/url" "github.com/bnema/dumber/internal/logging" ) // Compile-time check: ManageFavoritesUseCase must satisfy port.HomepageFavorites. var _ port.HomepageFavorites = (*ManageFavoritesUseCase)(nil) // ManageFavoritesUseCase handles favorite, folder, and tag operations. type ManageFavoritesUseCase struct { favoriteRepo repository.FavoriteRepository folderRepo repository.FolderRepository tagRepo repository.TagRepository cacheMu sync.Mutex favoritesCache []*entity.Favorite favoriteURLsCache map[string]struct{} cacheTime time.Time } const favoritesCacheTTL = 2 * time.Second // NewManageFavoritesUseCase creates a new favorites management use case. func NewManageFavoritesUseCase( favoriteRepo repository.FavoriteRepository, folderRepo repository.FolderRepository, tagRepo repository.TagRepository, ) *ManageFavoritesUseCase { return &ManageFavoritesUseCase{ favoriteRepo: favoriteRepo, folderRepo: folderRepo, tagRepo: tagRepo, } } // AddFavoriteInput contains parameters for adding a favorite. type AddFavoriteInput struct { URL string Title string FaviconURL string FolderID *entity.FolderID Tags []entity.TagID } // Add creates a new favorite. func (uc *ManageFavoritesUseCase) Add(ctx context.Context, input AddFavoriteInput) (*entity.Favorite, error) { log := logging.FromContext(ctx) log.Debug().Str("url", logging.RedactURL(input.URL)).Msg("adding favorite") // Check if already favorited existing, err := uc.favoriteRepo.FindByURL(ctx, input.URL) if err != nil { return nil, fmt.Errorf("failed to check existing favorite: %w", err) } if existing != nil { log.Debug().Str("url", logging.RedactURL(input.URL)).Msg("URL already favorited") return existing, nil } return uc.addNewFavorite(ctx, input) } func (uc *ManageFavoritesUseCase) addNewFavorite(ctx context.Context, input AddFavoriteInput) (*entity.Favorite, error) { log := logging.FromContext(ctx) fav := entity.NewFavorite(input.URL, input.Title) fav.FaviconURL = input.FaviconURL fav.FolderID = input.FolderID if err := uc.favoriteRepo.Save(ctx, fav); err != nil { return nil, fmt.Errorf("failed to save favorite: %w", err) } // Assign tags for _, tagID := range input.Tags { if err := uc.tagRepo.AssignToFavorite(ctx, tagID, fav.ID); err != nil { log.Warn(). Int64("tag_id", int64(tagID)). Int64("favorite_id", int64(fav.ID)). Err(err). Msg("failed to assign tag") } } log.Info().Str("url", logging.RedactURL(input.URL)).Int64("id", int64(fav.ID)).Msg("favorite added") uc.invalidateCache() return fav, nil } // AddFavorite creates a favorite from the UI-facing application port. func (uc *ManageFavoritesUseCase) AddFavorite(ctx context.Context, input dto.FavoriteCreateInput) (*entity.Favorite, error) { for _, tagID := range input.Tags { if tagID <= 0 { return nil, fmt.Errorf("favorite tag id must be positive, got %d", tagID) } } favoriteURL, err := normalizeFavoriteURL(input.URL) if err != nil { return nil, err } if existing, err := uc.findFavoriteByCanonicalOrRawURL(ctx, favoriteURL, input.URL); err != nil { return nil, err } else if existing != nil { return existing, nil } tags := append([]entity.TagID(nil), input.Tags...) title := strings.TrimSpace(input.Title) if title == "" { title = favoriteURL } return uc.addNewFavorite(ctx, AddFavoriteInput{ URL: favoriteURL, Title: title, FaviconURL: strings.TrimSpace(input.FaviconURL), FolderID: input.FolderID, Tags: tags, }) } func (uc *ManageFavoritesUseCase) findFavoriteByCanonicalOrRawURL( ctx context.Context, canonicalURL string, rawURL string, ) (*entity.Favorite, error) { existing, err := uc.favoriteRepo.FindByURL(ctx, canonicalURL) if err != nil { return nil, fmt.Errorf("failed to check existing favorite: %w", err) } if existing != nil { return existing, nil } rawURL = strings.TrimSpace(rawURL) if rawURL == "" || rawURL == canonicalURL { return nil, nil } existing, err = uc.favoriteRepo.FindByURL(ctx, rawURL) if err != nil { return nil, fmt.Errorf("failed to check legacy favorite: %w", err) } return existing, nil } func normalizeFavoriteURL(raw string) (string, error) { favoriteURL := domainurl.Normalize(strings.TrimSpace(raw)) if favoriteURL == "" { return "", fmt.Errorf("favorite URL is required") } parsed, err := url.Parse(favoriteURL) if err != nil || parsed.Scheme == "" { return "", fmt.Errorf("favorite URL must be absolute") } switch strings.ToLower(parsed.Scheme) { case "http", "https": if parsed.Host == "" { return "", fmt.Errorf("favorite URL host is required") } case "dumb": if parsed.Host != "" { return favoriteURL, nil } if parsed.Opaque != "" { return "dumb://" + strings.TrimPrefix(parsed.Opaque, "//"), nil } return "", fmt.Errorf("favorite URL host is required") default: return "", fmt.Errorf("favorite URL must use http, https, or dumb scheme") } return favoriteURL, nil } // UpdateFavorite updates editable favorite metadata from the UI-facing application port. func (uc *ManageFavoritesUseCase) UpdateFavorite(ctx context.Context, input dto.FavoriteUpdateInput) (*entity.Favorite, error) { if input.ID <= 0 { return nil, fmt.Errorf("favorite ID is required") } if input.ShortcutKey != nil && (*input.ShortcutKey < 1 || *input.ShortcutKey > 9) { return nil, fmt.Errorf("shortcut key must be 1-9, got %d", *input.ShortcutKey) } fav, err := uc.favoriteRepo.FindByID(ctx, input.ID) if err != nil { return nil, fmt.Errorf("failed to find favorite: %w", err) } if fav == nil { return nil, fmt.Errorf("favorite %d not found", input.ID) } title := strings.TrimSpace(input.Title) if title == "" { return nil, fmt.Errorf("favorite title is required") } fav.Title = title fav.FaviconURL = strings.TrimSpace(input.FaviconURL) fav.FolderID = input.FolderID fav.ShortcutKey = input.ShortcutKey if err := uc.Update(ctx, fav); err != nil { return nil, err } return fav, nil } // DeleteFavorite deletes a favorite from the UI-facing application port. func (uc *ManageFavoritesUseCase) DeleteFavorite(ctx context.Context, id entity.FavoriteID) error { return uc.Remove(ctx, id) } // Remove deletes a favorite by ID. func (uc *ManageFavoritesUseCase) Remove(ctx context.Context, id entity.FavoriteID) error { log := logging.FromContext(ctx) log.Debug().Int64("id", int64(id)).Msg("removing favorite") if err := uc.favoriteRepo.Delete(ctx, id); err != nil { return fmt.Errorf("failed to delete favorite: %w", err) } log.Info().Int64("id", int64(id)).Msg("favorite removed") uc.invalidateCache() return nil } // RemoveByURL deletes a favorite by its URL. func (uc *ManageFavoritesUseCase) RemoveByURL(ctx context.Context, favoriteURL string) error { log := logging.FromContext(ctx) log.Debug().Str("url", logging.RedactURL(favoriteURL)).Msg("removing favorite by URL") fav, err := uc.favoriteRepo.FindByURL(ctx, favoriteURL) if err != nil { return fmt.Errorf("failed to find favorite: %w", err) } if fav == nil { log.Debug().Str("url", logging.RedactURL(favoriteURL)).Msg("favorite not found") return nil } return uc.Remove(ctx, fav.ID) } // Update modifies a favorite's metadata. func (uc *ManageFavoritesUseCase) Update(ctx context.Context, fav *entity.Favorite) error { log := logging.FromContext(ctx) log.Debug().Int64("id", int64(fav.ID)).Msg("updating favorite") if err := uc.favoriteRepo.Save(ctx, fav); err != nil { return fmt.Errorf("failed to update favorite: %w", err) } log.Info().Int64("id", int64(fav.ID)).Msg("favorite updated") uc.invalidateCache() return nil } // Move changes a favorite's folder. func (uc *ManageFavoritesUseCase) Move(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error { log := logging.FromContext(ctx) log.Debug(). Int64("favorite_id", int64(id)). Msg("moving favorite to folder") if err := uc.favoriteRepo.SetFolder(ctx, id, folderID); err != nil { return fmt.Errorf("failed to move favorite: %w", err) } log.Info().Int64("id", int64(id)).Msg("favorite moved") uc.invalidateCache() return nil } // SetShortcut assigns or removes a keyboard shortcut (1-9). func (uc *ManageFavoritesUseCase) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error { log := logging.FromContext(ctx) if key != nil && (*key < 1 || *key > 9) { return fmt.Errorf("shortcut key must be 1-9, got %d", *key) } log.Debug(). Int64("id", int64(id)). Msg("setting favorite shortcut") if err := uc.favoriteRepo.SetShortcut(ctx, id, key); err != nil { return fmt.Errorf("failed to set shortcut: %w", err) } uc.invalidateCache() return nil } // GetByShortcut finds the favorite with the given shortcut key. func (uc *ManageFavoritesUseCase) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) { log := logging.FromContext(ctx) log.Debug().Int("key", key).Msg("getting favorite by shortcut") if key < 1 || key > 9 { return nil, fmt.Errorf("shortcut key must be 1-9, got %d", key) } return uc.favoriteRepo.GetByShortcut(ctx, key) } // GetByURL finds a favorite by its URL. func (uc *ManageFavoritesUseCase) GetByURL(ctx context.Context, favoriteURL string) (*entity.Favorite, error) { log := logging.FromContext(ctx) log.Debug().Str("url", logging.RedactURL(favoriteURL)).Msg("getting favorite by URL") return uc.favoriteRepo.FindByURL(ctx, favoriteURL) } // GetAll retrieves all favorites. func (uc *ManageFavoritesUseCase) GetAll(ctx context.Context) ([]*entity.Favorite, error) { log := logging.FromContext(ctx) log.Debug().Msg("getting all favorites") uc.cacheMu.Lock() if time.Since(uc.cacheTime) < favoritesCacheTTL && uc.favoritesCache != nil { cached := uc.favoritesCache uc.cacheMu.Unlock() return cached, nil } uc.cacheMu.Unlock() favs, err := uc.favoriteRepo.GetAll(ctx) if err != nil { return nil, fmt.Errorf("failed to get favorites: %w", err) } uc.cacheMu.Lock() if time.Since(uc.cacheTime) < favoritesCacheTTL && uc.favoritesCache != nil { cached := uc.favoritesCache uc.cacheMu.Unlock() return cached, nil } uc.favoritesCache = favs uc.favoriteURLsCache = buildFavoriteURLSet(favs) uc.cacheTime = time.Now() uc.cacheMu.Unlock() log.Debug().Int("count", len(favs)).Msg("retrieved favorites") return favs, nil } // FilterForOmnibox returns favorites filtered and ranked for omnibox display. func (uc *ManageFavoritesUseCase) FilterForOmnibox(ctx context.Context, query string) ([]*entity.Favorite, error) { favs, err := uc.GetAll(ctx) if err != nil { return nil, err } queryLower := strings.ToLower(strings.TrimSpace(query)) if queryLower == "" { return favs, nil } filtered := make([]*entity.Favorite, 0, len(favs)) for _, fav := range favs { if fav == nil { continue } titleLower := strings.ToLower(fav.Title) urlLower := strings.ToLower(fav.URL) if !strings.Contains(titleLower, queryLower) && !strings.Contains(urlLower, queryLower) { continue } filtered = append(filtered, fav) } sort.SliceStable(filtered, func(i, j int) bool { leftScore := omniboxFavoriteScore(filtered[i], queryLower) rightScore := omniboxFavoriteScore(filtered[j], queryLower) if leftScore != rightScore { return leftScore > rightScore } if filtered[i].Position != filtered[j].Position { return filtered[i].Position < filtered[j].Position } return filtered[i].URL < filtered[j].URL }) return filtered, nil } // IsFavorite checks if a URL is favorited. func (uc *ManageFavoritesUseCase) IsFavorite(ctx context.Context, favoriteURL string) (bool, error) { fav, err := uc.favoriteRepo.FindByURL(ctx, favoriteURL) if err != nil { return false, fmt.Errorf("failed to check favorite: %w", err) } return fav != nil, nil } func omniboxFavoriteScore(fav *entity.Favorite, queryLower string) int { if fav == nil || queryLower == "" { return 0 } const ( favoriteExactMatchScore = 600 favoriteHostPrefixScore = 520 favoriteTitlePrefixScore = 440 favoriteTitleContainsScore = 320 favoriteURLContainsScore = 280 ) urlLower := strings.ToLower(fav.URL) strippedURLLower := strings.ToLower(strings.TrimPrefix(domainurl.ExtractDomain(fav.URL), "www.")) if strippedURLLower == "" { strippedURLLower = strings.ToLower(strings.TrimPrefix(strings.TrimPrefix(fav.URL, "https://"), "http://")) } titleLower := strings.ToLower(fav.Title) switch { case titleLower == queryLower || urlLower == queryLower: return favoriteExactMatchScore case strings.HasPrefix(strippedURLLower, queryLower) || strings.HasPrefix(urlLower, queryLower): return favoriteHostPrefixScore case strings.HasPrefix(titleLower, queryLower): return favoriteTitlePrefixScore case strings.Contains(titleLower, queryLower): return favoriteTitleContainsScore case strings.Contains(urlLower, queryLower): return favoriteURLContainsScore default: return 0 } } // ToggleResult indicates the result of a toggle operation. type ToggleResult struct { Added bool // true if favorite was added, false if removed URL string Title string Message string // User-friendly message } // Toggle adds or removes a URL from favorites based on current state. // If the URL is already a favorite, it removes it. Otherwise, it adds it. func (uc *ManageFavoritesUseCase) Toggle(ctx context.Context, rawURL, title string) (*ToggleResult, error) { favoriteURL, err := normalizeFavoriteURL(rawURL) if err != nil { return nil, err } title = strings.TrimSpace(title) log := logging.FromContext(ctx) log.Debug().Str("url", logging.RedactURL(favoriteURL)).Msg("toggling favorite") // Check if already favorited, including legacy rows stored before URL canonicalization. existing, err := uc.findFavoriteByCanonicalOrRawURL(ctx, favoriteURL, rawURL) if err != nil { return nil, err } if existing != nil { // Remove favorite if err := uc.favoriteRepo.Delete(ctx, existing.ID); err != nil { return nil, fmt.Errorf("failed to remove favorite: %w", err) } log.Info().Str("url", logging.RedactURL(favoriteURL)).Int64("id", int64(existing.ID)).Msg("favorite removed via toggle") uc.invalidateCache() return &ToggleResult{ Added: false, URL: favoriteURL, Title: existing.Title, Message: "Favorite removed", }, nil } // Add favorite fav := entity.NewFavorite(favoriteURL, title) if err := uc.favoriteRepo.Save(ctx, fav); err != nil { return nil, fmt.Errorf("failed to add favorite: %w", err) } log.Info().Str("url", logging.RedactURL(favoriteURL)).Int64("id", int64(fav.ID)).Msg("favorite added via toggle") uc.invalidateCache() return &ToggleResult{ Added: true, URL: favoriteURL, Title: title, Message: "Favorite added", }, nil } // GetAllURLs returns a set of all favorited URLs for efficient batch lookup. func (uc *ManageFavoritesUseCase) GetAllURLs(ctx context.Context) (map[string]struct{}, error) { log := logging.FromContext(ctx) log.Debug().Msg("getting all favorite URLs") uc.cacheMu.Lock() if time.Since(uc.cacheTime) < favoritesCacheTTL && uc.favoriteURLsCache != nil { cached := uc.favoriteURLsCache uc.cacheMu.Unlock() return cached, nil } uc.cacheMu.Unlock() favs, err := uc.GetAll(ctx) if err != nil { return nil, fmt.Errorf("failed to get favorites: %w", err) } urls := buildFavoriteURLSet(favs) log.Debug().Int("count", len(urls)).Msg("retrieved favorite URLs") return urls, nil } func buildFavoriteURLSet(favs []*entity.Favorite) map[string]struct{} { urls := make(map[string]struct{}, len(favs)) for _, fav := range favs { if fav == nil || fav.URL == "" { continue } urls[fav.URL] = struct{}{} } return urls } func (uc *ManageFavoritesUseCase) invalidateCache() { uc.cacheMu.Lock() defer uc.cacheMu.Unlock() uc.favoritesCache = nil uc.favoriteURLsCache = nil uc.cacheTime = time.Time{} } // GetTree builds a complete hierarchical view of folders and favorites. func (uc *ManageFavoritesUseCase) GetTree(ctx context.Context) (*entity.FavoriteTree, error) { log := logging.FromContext(ctx) log.Debug().Msg("building favorite tree") tree := entity.NewFavoriteTree() // Get all folders folders, err := uc.folderRepo.GetAll(ctx) if err != nil { return nil, fmt.Errorf("failed to get folders: %w", err) } // Build folder map and organize by parent for _, folder := range folders { tree.FolderMap[folder.ID] = folder if folder.ParentID == nil { tree.RootFolders = append(tree.RootFolders, folder) } else { tree.ChildFolders[*folder.ParentID] = append(tree.ChildFolders[*folder.ParentID], folder) } } // Get all favorites favorites, err := uc.favoriteRepo.GetAll(ctx) if err != nil { return nil, fmt.Errorf("failed to get favorites: %w", err) } // Organize favorites by folder for _, fav := range favorites { if fav.FolderID == nil { tree.RootFavorites = append(tree.RootFavorites, fav) } else { tree.ChildFavorites[*fav.FolderID] = append(tree.ChildFavorites[*fav.FolderID], fav) } } log.Debug(). Int("folders", len(folders)). Int("favorites", len(favorites)). Msg("favorite tree built") return tree, nil } // CreateFolder creates a new folder. func (uc *ManageFavoritesUseCase) CreateFolder(ctx context.Context, name, icon string, parentID *entity.FolderID) (*entity.Folder, error) { name = strings.TrimSpace(name) icon = strings.TrimSpace(icon) if name == "" { return nil, fmt.Errorf("folder name is required") } log := logging.FromContext(ctx) log.Debug().Str("name", name).Msg("creating folder") folder := entity.NewFolder(name) folder.Icon = icon folder.ParentID = parentID if err := uc.folderRepo.Save(ctx, folder); err != nil { return nil, fmt.Errorf("failed to create folder: %w", err) } log.Info().Str("name", name).Int64("id", int64(folder.ID)).Msg("folder created") return folder, nil } // DeleteFolder removes a folder. Favorites in the folder are moved to root. func (uc *ManageFavoritesUseCase) DeleteFolder(ctx context.Context, id entity.FolderID) error { log := logging.FromContext(ctx) log.Debug().Int64("id", int64(id)).Msg("deleting folder") // Move favorites from this folder to root (or parent) folder, err := uc.folderRepo.FindByID(ctx, id) if err != nil { return fmt.Errorf("failed to find folder: %w", err) } if folder == nil { log.Debug().Int64("id", int64(id)).Msg("folder not found") return nil } // Get favorites in this folder favorites, err := uc.favoriteRepo.GetByFolder(ctx, &id) if err != nil { return fmt.Errorf("failed to get folder favorites: %w", err) } // Move favorites to parent folder (or root) for _, fav := range favorites { if setErr := uc.favoriteRepo.SetFolder(ctx, fav.ID, folder.ParentID); setErr != nil { log.Warn(). Int64("favorite_id", int64(fav.ID)). Err(setErr). Msg("failed to move favorite from deleted folder") } } uc.invalidateCache() // Get child folders children, err := uc.folderRepo.GetChildren(ctx, &id) if err != nil { return fmt.Errorf("failed to get child folders: %w", err) } // Recursively delete child folders for _, child := range children { if err := uc.DeleteFolder(ctx, child.ID); err != nil { log.Warn(). Int64("child_id", int64(child.ID)). Err(err). Msg("failed to delete child folder") } } // Delete the folder itself if err := uc.folderRepo.Delete(ctx, id); err != nil { return fmt.Errorf("failed to delete folder: %w", err) } log.Info().Int64("id", int64(id)).Msg("folder deleted") return nil } // GetAllFolders retrieves all folders. func (uc *ManageFavoritesUseCase) GetAllFolders(ctx context.Context) ([]*entity.Folder, error) { log := logging.FromContext(ctx) log.Debug().Msg("getting all folders") return uc.folderRepo.GetAll(ctx) } // AddTag creates a new tag. func (uc *ManageFavoritesUseCase) AddTag(ctx context.Context, name, color string) (*entity.Tag, error) { log := logging.FromContext(ctx) log.Debug().Str("name", name).Str("color", color).Msg("creating tag") // Check if tag already exists existing, err := uc.tagRepo.FindByName(ctx, name) if err != nil { return nil, fmt.Errorf("failed to check existing tag: %w", err) } if existing != nil { log.Debug().Str("name", name).Msg("tag already exists") return existing, nil } tag := entity.NewTag(name) if color != "" { tag.Color = color } if err := uc.tagRepo.Save(ctx, tag); err != nil { return nil, fmt.Errorf("failed to create tag: %w", err) } log.Info().Str("name", name).Int64("id", int64(tag.ID)).Msg("tag created") return tag, nil } // DeleteTag removes a tag. func (uc *ManageFavoritesUseCase) DeleteTag(ctx context.Context, id entity.TagID) error { log := logging.FromContext(ctx) log.Debug().Int64("id", int64(id)).Msg("deleting tag") if err := uc.tagRepo.Delete(ctx, id); err != nil { return fmt.Errorf("failed to delete tag: %w", err) } log.Info().Int64("id", int64(id)).Msg("tag deleted") uc.invalidateCache() return nil } // GetAllTags retrieves all tags. func (uc *ManageFavoritesUseCase) GetAllTags(ctx context.Context) ([]*entity.Tag, error) { log := logging.FromContext(ctx) log.Debug().Msg("getting all tags") return uc.tagRepo.GetAll(ctx) } // TagFavorite assigns a tag to a favorite. func (uc *ManageFavoritesUseCase) TagFavorite(ctx context.Context, favID entity.FavoriteID, tagID entity.TagID) error { log := logging.FromContext(ctx) log.Debug(). Int64("favorite_id", int64(favID)). Int64("tag_id", int64(tagID)). Msg("tagging favorite") if err := uc.tagRepo.AssignToFavorite(ctx, tagID, favID); err != nil { return fmt.Errorf("failed to tag favorite: %w", err) } uc.invalidateCache() return nil } // UntagFavorite removes a tag from a favorite. func (uc *ManageFavoritesUseCase) UntagFavorite(ctx context.Context, favID entity.FavoriteID, tagID entity.TagID) error { log := logging.FromContext(ctx) log.Debug(). Int64("favorite_id", int64(favID)). Int64("tag_id", int64(tagID)). Msg("untagging favorite") if err := uc.tagRepo.RemoveFromFavorite(ctx, tagID, favID); err != nil { return fmt.Errorf("failed to untag favorite: %w", err) } uc.invalidateCache() return nil } // GetTagsForFavorite retrieves all tags for a favorite. func (uc *ManageFavoritesUseCase) GetTagsForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) { log := logging.FromContext(ctx) log.Debug().Int64("favorite_id", int64(favID)).Msg("getting tags for favorite") return uc.tagRepo.GetForFavorite(ctx, favID) } // UpdateFolder updates a folder's name and icon. func (uc *ManageFavoritesUseCase) UpdateFolder(ctx context.Context, id entity.FolderID, name, icon string) error { log := logging.FromContext(ctx) log.Debug().Int64("id", int64(id)).Str("name", name).Msg("updating folder") folder, err := uc.folderRepo.FindByID(ctx, id) if err != nil { return fmt.Errorf("failed to find folder: %w", err) } if folder == nil { return fmt.Errorf("folder %d not found", id) } if name != "" { folder.Name = name } folder.Icon = icon if err := uc.folderRepo.Save(ctx, folder); err != nil { return fmt.Errorf("failed to update folder: %w", err) } log.Info().Int64("id", int64(id)).Msg("folder updated") return nil } // UpdateTag updates a tag's name and color. func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID, name, color string) error { log := logging.FromContext(ctx) log.Debug().Int64("id", int64(id)).Str("name", name).Str("color", color).Msg("updating tag") tag, err := uc.tagRepo.FindByID(ctx, id) if err != nil { return fmt.Errorf("failed to find tag: %w", err) } if tag == nil { return fmt.Errorf("tag %d not found", id) } if name != "" { tag.Name = name } if color != "" { tag.Color = color } if err := uc.tagRepo.Save(ctx, tag); err != nil { return fmt.Errorf("failed to update tag: %w", err) } log.Info().Int64("id", int64(id)).Msg("tag updated") uc.invalidateCache() return nil }