Add pool repo (gap-based position ordering, cursor pagination, add/remove/reorder files), service, handler, and wire all /pools endpoints including /pools/:id/files, /pools/:id/files/remove, and /pools/:id/files/reorder. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
177 lines
4.6 KiB
Go
177 lines
4.6 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
|
||
"github.com/google/uuid"
|
||
|
||
"tanabata/backend/internal/domain"
|
||
"tanabata/backend/internal/port"
|
||
)
|
||
|
||
const poolObjectType = "pool"
|
||
const poolObjectTypeID int16 = 4 // fourth row in 007_seed_data.sql object_types
|
||
|
||
// PoolParams holds the fields for creating or patching a pool.
|
||
type PoolParams struct {
|
||
Name string
|
||
Notes *string
|
||
Metadata json.RawMessage
|
||
IsPublic *bool
|
||
}
|
||
|
||
// PoolService handles pool CRUD and pool–file management with ACL + audit.
|
||
type PoolService struct {
|
||
pools port.PoolRepo
|
||
acl *ACLService
|
||
audit *AuditService
|
||
}
|
||
|
||
// NewPoolService creates a PoolService.
|
||
func NewPoolService(
|
||
pools port.PoolRepo,
|
||
acl *ACLService,
|
||
audit *AuditService,
|
||
) *PoolService {
|
||
return &PoolService{pools: pools, acl: acl, audit: audit}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CRUD
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// List returns a paginated list of pools.
|
||
func (s *PoolService) List(ctx context.Context, params port.OffsetParams) (*domain.PoolOffsetPage, error) {
|
||
return s.pools.List(ctx, params)
|
||
}
|
||
|
||
// Get returns a pool by ID.
|
||
func (s *PoolService) Get(ctx context.Context, id uuid.UUID) (*domain.Pool, error) {
|
||
return s.pools.GetByID(ctx, id)
|
||
}
|
||
|
||
// Create inserts a new pool.
|
||
func (s *PoolService) Create(ctx context.Context, p PoolParams) (*domain.Pool, error) {
|
||
userID, _, _ := domain.UserFromContext(ctx)
|
||
|
||
pool := &domain.Pool{
|
||
Name: p.Name,
|
||
Notes: p.Notes,
|
||
Metadata: p.Metadata,
|
||
CreatorID: userID,
|
||
}
|
||
if p.IsPublic != nil {
|
||
pool.IsPublic = *p.IsPublic
|
||
}
|
||
|
||
created, err := s.pools.Create(ctx, pool)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
objType := poolObjectType
|
||
_ = s.audit.Log(ctx, "pool_create", &objType, &created.ID, nil)
|
||
return created, nil
|
||
}
|
||
|
||
// Update applies a partial patch to a pool.
|
||
func (s *PoolService) Update(ctx context.Context, id uuid.UUID, p PoolParams) (*domain.Pool, error) {
|
||
userID, isAdmin, _ := domain.UserFromContext(ctx)
|
||
|
||
current, err := s.pools.GetByID(ctx, id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
ok, err := s.acl.CanEdit(ctx, userID, isAdmin, current.CreatorID, poolObjectTypeID, id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !ok {
|
||
return nil, domain.ErrForbidden
|
||
}
|
||
|
||
patch := *current
|
||
if p.Name != "" {
|
||
patch.Name = p.Name
|
||
}
|
||
if p.Notes != nil {
|
||
patch.Notes = p.Notes
|
||
}
|
||
if len(p.Metadata) > 0 {
|
||
patch.Metadata = p.Metadata
|
||
}
|
||
if p.IsPublic != nil {
|
||
patch.IsPublic = *p.IsPublic
|
||
}
|
||
|
||
updated, err := s.pools.Update(ctx, id, &patch)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
objType := poolObjectType
|
||
_ = s.audit.Log(ctx, "pool_edit", &objType, &id, nil)
|
||
return updated, nil
|
||
}
|
||
|
||
// Delete removes a pool by ID, enforcing edit ACL.
|
||
func (s *PoolService) Delete(ctx context.Context, id uuid.UUID) error {
|
||
userID, isAdmin, _ := domain.UserFromContext(ctx)
|
||
|
||
pool, err := s.pools.GetByID(ctx, id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
ok, err := s.acl.CanEdit(ctx, userID, isAdmin, pool.CreatorID, poolObjectTypeID, id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !ok {
|
||
return domain.ErrForbidden
|
||
}
|
||
|
||
if err := s.pools.Delete(ctx, id); err != nil {
|
||
return err
|
||
}
|
||
|
||
objType := poolObjectType
|
||
_ = s.audit.Log(ctx, "pool_delete", &objType, &id, nil)
|
||
return nil
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Pool–file operations
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// ListFiles returns cursor-paginated files within a pool ordered by position.
|
||
func (s *PoolService) ListFiles(ctx context.Context, poolID uuid.UUID, params port.PoolFileListParams) (*domain.PoolFilePage, error) {
|
||
return s.pools.ListFiles(ctx, poolID, params)
|
||
}
|
||
|
||
// AddFiles adds files to a pool at the given position (nil = append).
|
||
func (s *PoolService) AddFiles(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID, position *int) error {
|
||
if err := s.pools.AddFiles(ctx, poolID, fileIDs, position); err != nil {
|
||
return err
|
||
}
|
||
objType := poolObjectType
|
||
_ = s.audit.Log(ctx, "file_pool_add", &objType, &poolID, map[string]any{"count": len(fileIDs)})
|
||
return nil
|
||
}
|
||
|
||
// RemoveFiles removes files from a pool.
|
||
func (s *PoolService) RemoveFiles(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID) error {
|
||
if err := s.pools.RemoveFiles(ctx, poolID, fileIDs); err != nil {
|
||
return err
|
||
}
|
||
objType := poolObjectType
|
||
_ = s.audit.Log(ctx, "file_pool_remove", &objType, &poolID, map[string]any{"count": len(fileIDs)})
|
||
return nil
|
||
}
|
||
|
||
// Reorder sets the full ordered sequence of file IDs within a pool.
|
||
func (s *PoolService) Reorder(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID) error {
|
||
return s.pools.Reorder(ctx, poolID, fileIDs)
|
||
} |