first commit
This commit is contained in:
283
backend/internal/application/services/category_service.go
Normal file
283
backend/internal/application/services/category_service.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
"github.com/noteapp/backend/internal/application/dto"
|
||||
"github.com/noteapp/backend/internal/domain/entities"
|
||||
"github.com/noteapp/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)
|
||||
}
|
||||
Reference in New Issue
Block a user