284 lines
8.0 KiB
Go
284 lines
8.0 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
|
|
"gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/application/dto"
|
|
"gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/domain/entities"
|
|
"gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/domain/repositories"
|
|
)
|
|
|
|
// CategoryService handles category operations
|
|
type CategoryService struct {
|
|
categoryRepo repositories.CategoryRepository
|
|
membershipRepo repositories.MembershipRepository
|
|
noteRepo repositories.NoteRepository
|
|
permissionService *PermissionService
|
|
}
|
|
|
|
// NewCategoryService creates a new category service
|
|
func NewCategoryService(
|
|
categoryRepo repositories.CategoryRepository,
|
|
membershipRepo repositories.MembershipRepository,
|
|
noteRepo repositories.NoteRepository,
|
|
permissionService *PermissionService,
|
|
) *CategoryService {
|
|
return &CategoryService{
|
|
categoryRepo: categoryRepo,
|
|
membershipRepo: membershipRepo,
|
|
noteRepo: noteRepo,
|
|
permissionService: permissionService,
|
|
}
|
|
}
|
|
|
|
// CreateCategory creates a new category
|
|
func (s *CategoryService) CreateCategory(ctx context.Context, spaceID, userID bson.ObjectID, req *dto.CreateCategoryRequest) (*dto.CategoryDTO, error) {
|
|
// Verify user has access to space
|
|
membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID)
|
|
if err != nil {
|
|
return nil, errors.New("unauthorized")
|
|
}
|
|
_ = membership
|
|
hasPermission, permErr := s.hasSpacePermission(ctx, userID, spaceID, "category.create")
|
|
if permErr != nil {
|
|
return nil, permErr
|
|
}
|
|
if !hasPermission {
|
|
return nil, errors.New("insufficient permissions")
|
|
}
|
|
|
|
var parentID *bson.ObjectID
|
|
if req.ParentID != nil {
|
|
id, _ := bson.ObjectIDFromHex(*req.ParentID)
|
|
parentID = &id
|
|
|
|
// Verify parent category exists and belongs to same space
|
|
parent, err := s.categoryRepo.GetCategoryByID(ctx, id)
|
|
if err != nil || parent.SpaceID != spaceID {
|
|
return nil, errors.New("invalid parent category")
|
|
}
|
|
}
|
|
|
|
// Get next order value
|
|
categories, err := s.categoryRepo.GetCategoriesBySpaceID(ctx, spaceID)
|
|
order := len(categories)
|
|
|
|
category := &entities.Category{
|
|
SpaceID: spaceID,
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
ParentID: parentID,
|
|
Icon: req.Icon,
|
|
Order: order,
|
|
CreatedBy: userID,
|
|
UpdatedBy: userID,
|
|
}
|
|
|
|
if err := s.categoryRepo.CreateCategory(ctx, category); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return dto.NewCategoryDTO(category), nil
|
|
}
|
|
|
|
// GetCategoryTree retrieves the full tree structure for a space
|
|
func (s *CategoryService) GetCategoryTree(ctx context.Context, spaceID, userID bson.ObjectID) ([]*dto.CategoryTreeDTO, error) {
|
|
// Verify user has access to space
|
|
if _, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID); err != nil {
|
|
return nil, errors.New("unauthorized")
|
|
}
|
|
|
|
// Get root categories
|
|
categories, err := s.categoryRepo.GetRootCategories(ctx, spaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var trees []*dto.CategoryTreeDTO
|
|
for _, category := range categories {
|
|
tree, err := s.buildCategoryTree(ctx, category, spaceID)
|
|
if err == nil {
|
|
trees = append(trees, tree)
|
|
}
|
|
}
|
|
|
|
return trees, nil
|
|
}
|
|
|
|
// buildCategoryTree recursively builds a category tree
|
|
func (s *CategoryService) buildCategoryTree(ctx context.Context, category *entities.Category, spaceID bson.ObjectID) (*dto.CategoryTreeDTO, error) {
|
|
tree := &dto.CategoryTreeDTO{
|
|
CategoryDTO: dto.NewCategoryDTO(category),
|
|
}
|
|
|
|
// Get subcategories
|
|
subcategories, err := s.categoryRepo.GetSubcategories(ctx, category.ID)
|
|
if err == nil {
|
|
for _, subcat := range subcategories {
|
|
subtree, err := s.buildCategoryTree(ctx, subcat, spaceID)
|
|
if err == nil {
|
|
tree.Subcategories = append(tree.Subcategories, subtree)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get notes in this category
|
|
notes, err := s.noteRepo.GetNotesByCategory(ctx, spaceID, category.ID)
|
|
if err == nil {
|
|
for _, note := range notes {
|
|
tree.Notes = append(tree.Notes, dto.NewNoteListItemDTO(note))
|
|
}
|
|
}
|
|
|
|
return tree, nil
|
|
}
|
|
|
|
// UpdateCategory updates a category
|
|
func (s *CategoryService) UpdateCategory(ctx context.Context, categoryID, spaceID, userID bson.ObjectID, req *dto.UpdateCategoryRequest) (*dto.CategoryDTO, error) {
|
|
// Verify user has access to space
|
|
membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID)
|
|
if err != nil {
|
|
return nil, errors.New("unauthorized")
|
|
}
|
|
_ = membership
|
|
hasPermission, permErr := s.hasSpacePermission(ctx, userID, spaceID, "category.edit")
|
|
if permErr != nil {
|
|
return nil, permErr
|
|
}
|
|
if !hasPermission {
|
|
return nil, errors.New("insufficient permissions")
|
|
}
|
|
|
|
category, err := s.categoryRepo.GetCategoryByID(ctx, categoryID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Verify category belongs to this space
|
|
if category.SpaceID != spaceID {
|
|
return nil, errors.New("category not found in this space")
|
|
}
|
|
|
|
if req.Name != "" {
|
|
category.Name = req.Name
|
|
}
|
|
if req.Description != "" {
|
|
category.Description = req.Description
|
|
}
|
|
if req.Icon != "" {
|
|
category.Icon = req.Icon
|
|
}
|
|
|
|
category.UpdatedBy = userID
|
|
|
|
if err := s.categoryRepo.UpdateCategory(ctx, category); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return dto.NewCategoryDTO(category), nil
|
|
}
|
|
|
|
// DeleteCategory deletes a category (and optionally move notes)
|
|
func (s *CategoryService) DeleteCategory(ctx context.Context, categoryID, spaceID, userID bson.ObjectID, moveNotesTo *string) error {
|
|
// Verify user has access to space
|
|
membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID)
|
|
if err != nil {
|
|
return errors.New("unauthorized")
|
|
}
|
|
_ = membership
|
|
hasPermission, permErr := s.hasSpacePermission(ctx, userID, spaceID, "category.delete")
|
|
if permErr != nil {
|
|
return permErr
|
|
}
|
|
if !hasPermission {
|
|
return errors.New("insufficient permissions")
|
|
}
|
|
|
|
category, err := s.categoryRepo.GetCategoryByID(ctx, categoryID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify category belongs to this space
|
|
if category.SpaceID != spaceID {
|
|
return errors.New("category not found in this space")
|
|
}
|
|
|
|
// Handle notes in this category
|
|
notes, err := s.noteRepo.GetNotesByCategory(ctx, spaceID, categoryID)
|
|
if err == nil {
|
|
for _, note := range notes {
|
|
if moveNotesTo != nil {
|
|
targetID, _ := bson.ObjectIDFromHex(*moveNotesTo)
|
|
note.CategoryID = &targetID
|
|
s.noteRepo.UpdateNote(ctx, note)
|
|
} else {
|
|
// Move to root (no category)
|
|
note.CategoryID = nil
|
|
s.noteRepo.UpdateNote(ctx, note)
|
|
}
|
|
}
|
|
}
|
|
|
|
return s.categoryRepo.DeleteCategory(ctx, categoryID)
|
|
}
|
|
|
|
// MoveCategory moves a category to a new parent
|
|
func (s *CategoryService) MoveCategory(ctx context.Context, categoryID, spaceID, userID bson.ObjectID, newParentID *string) (*dto.CategoryDTO, error) {
|
|
// Verify user has access to space
|
|
membership, err := s.membershipRepo.GetUserMembership(ctx, userID, spaceID)
|
|
if err != nil {
|
|
return nil, errors.New("unauthorized")
|
|
}
|
|
_ = membership
|
|
hasPermission, permErr := s.hasSpacePermission(ctx, userID, spaceID, "category.edit")
|
|
if permErr != nil {
|
|
return nil, permErr
|
|
}
|
|
if !hasPermission {
|
|
return nil, errors.New("insufficient permissions")
|
|
}
|
|
|
|
category, err := s.categoryRepo.GetCategoryByID(ctx, categoryID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Verify category belongs to this space
|
|
if category.SpaceID != spaceID {
|
|
return nil, errors.New("category not found in this space")
|
|
}
|
|
|
|
// Validate new parent
|
|
if newParentID != nil {
|
|
parentID, _ := bson.ObjectIDFromHex(*newParentID)
|
|
parent, err := s.categoryRepo.GetCategoryByID(ctx, parentID)
|
|
if err != nil || parent.SpaceID != spaceID {
|
|
return nil, errors.New("invalid parent category")
|
|
}
|
|
category.ParentID = &parentID
|
|
} else {
|
|
category.ParentID = nil
|
|
}
|
|
|
|
category.UpdatedBy = userID
|
|
category.UpdatedAt = time.Now()
|
|
|
|
if err := s.categoryRepo.UpdateCategory(ctx, category); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return dto.NewCategoryDTO(category), nil
|
|
}
|
|
|
|
func (s *CategoryService) hasSpacePermission(ctx context.Context, userID, spaceID bson.ObjectID, action string) (bool, error) {
|
|
if s.permissionService == nil {
|
|
return false, errors.New("permission service unavailable")
|
|
}
|
|
return s.permissionService.HasSpacePermission(ctx, userID, spaceID, action)
|
|
}
|