feat(backend): implement pool stack
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>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user