first commit
Made-with: Cursor
This commit is contained in:
318
internal/service/calendar.go
Normal file
318
internal/service/calendar.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/calendarapi/internal/models"
|
||||
"github.com/calendarapi/internal/repository"
|
||||
"github.com/calendarapi/internal/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type CalendarService struct {
|
||||
pool *pgxpool.Pool
|
||||
queries *repository.Queries
|
||||
audit *AuditService
|
||||
}
|
||||
|
||||
func NewCalendarService(pool *pgxpool.Pool, queries *repository.Queries, audit *AuditService) *CalendarService {
|
||||
return &CalendarService{pool: pool, queries: queries, audit: audit}
|
||||
}
|
||||
|
||||
func (s *CalendarService) Create(ctx context.Context, userID uuid.UUID, name, color string) (*models.Calendar, error) {
|
||||
if err := utils.ValidateCalendarName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := utils.ValidateColor(color); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if color == "" {
|
||||
color = "#3B82F6"
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := s.queries.WithTx(tx)
|
||||
|
||||
calID := uuid.New()
|
||||
cal, err := qtx.CreateCalendar(ctx, repository.CreateCalendarParams{
|
||||
ID: utils.ToPgUUID(calID),
|
||||
OwnerID: utils.ToPgUUID(userID),
|
||||
Name: name,
|
||||
Color: color,
|
||||
IsPublic: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
|
||||
err = qtx.UpsertCalendarMember(ctx, repository.UpsertCalendarMemberParams{
|
||||
CalendarID: utils.ToPgUUID(calID),
|
||||
UserID: utils.ToPgUUID(userID),
|
||||
Role: "owner",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
|
||||
s.audit.Log(ctx, "calendar", calID, "CREATE_CALENDAR", userID)
|
||||
|
||||
return &models.Calendar{
|
||||
ID: utils.FromPgUUID(cal.ID),
|
||||
Name: cal.Name,
|
||||
Color: cal.Color,
|
||||
IsPublic: cal.IsPublic,
|
||||
Role: "owner",
|
||||
CreatedAt: utils.FromPgTimestamptz(cal.CreatedAt),
|
||||
UpdatedAt: utils.FromPgTimestamptz(cal.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CalendarService) List(ctx context.Context, userID uuid.UUID) ([]models.Calendar, error) {
|
||||
rows, err := s.queries.ListCalendarsByUser(ctx, utils.ToPgUUID(userID))
|
||||
if err != nil {
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
|
||||
calendars := make([]models.Calendar, len(rows))
|
||||
for i, r := range rows {
|
||||
calendars[i] = models.Calendar{
|
||||
ID: utils.FromPgUUID(r.ID),
|
||||
Name: r.Name,
|
||||
Color: r.Color,
|
||||
IsPublic: r.IsPublic,
|
||||
Role: r.Role,
|
||||
CreatedAt: utils.FromPgTimestamptz(r.CreatedAt),
|
||||
UpdatedAt: utils.FromPgTimestamptz(r.UpdatedAt),
|
||||
}
|
||||
}
|
||||
return calendars, nil
|
||||
}
|
||||
|
||||
func (s *CalendarService) Get(ctx context.Context, userID uuid.UUID, calID uuid.UUID) (*models.Calendar, error) {
|
||||
role, err := s.getRole(ctx, calID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cal, err := s.queries.GetCalendarByID(ctx, utils.ToPgUUID(calID))
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, models.ErrNotFound
|
||||
}
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
|
||||
return &models.Calendar{
|
||||
ID: utils.FromPgUUID(cal.ID),
|
||||
Name: cal.Name,
|
||||
Color: cal.Color,
|
||||
IsPublic: cal.IsPublic,
|
||||
Role: role,
|
||||
CreatedAt: utils.FromPgTimestamptz(cal.CreatedAt),
|
||||
UpdatedAt: utils.FromPgTimestamptz(cal.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CalendarService) Update(ctx context.Context, userID uuid.UUID, calID uuid.UUID, name, color *string, isPublic *bool) (*models.Calendar, error) {
|
||||
role, err := s.getRole(ctx, calID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if role != "owner" && role != "editor" {
|
||||
return nil, models.ErrForbidden
|
||||
}
|
||||
if isPublic != nil && role != "owner" {
|
||||
return nil, models.NewForbiddenError("only owner can change is_public")
|
||||
}
|
||||
|
||||
if name != nil {
|
||||
if err := utils.ValidateCalendarName(*name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if color != nil {
|
||||
if err := utils.ValidateColor(*color); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var pgPublic pgtype.Bool
|
||||
if isPublic != nil {
|
||||
pgPublic = pgtype.Bool{Bool: *isPublic, Valid: true}
|
||||
}
|
||||
|
||||
cal, err := s.queries.UpdateCalendar(ctx, repository.UpdateCalendarParams{
|
||||
ID: utils.ToPgUUID(calID),
|
||||
Name: utils.ToPgTextPtr(name),
|
||||
Color: utils.ToPgTextPtr(color),
|
||||
IsPublic: pgPublic,
|
||||
})
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, models.ErrNotFound
|
||||
}
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
|
||||
s.audit.Log(ctx, "calendar", calID, "UPDATE_CALENDAR", userID)
|
||||
|
||||
return &models.Calendar{
|
||||
ID: utils.FromPgUUID(cal.ID),
|
||||
Name: cal.Name,
|
||||
Color: cal.Color,
|
||||
IsPublic: cal.IsPublic,
|
||||
Role: role,
|
||||
CreatedAt: utils.FromPgTimestamptz(cal.CreatedAt),
|
||||
UpdatedAt: utils.FromPgTimestamptz(cal.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *CalendarService) Delete(ctx context.Context, userID uuid.UUID, calID uuid.UUID) error {
|
||||
role, err := s.getRole(ctx, calID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if role != "owner" {
|
||||
return models.ErrForbidden
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return models.ErrInternal
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := s.queries.WithTx(tx)
|
||||
|
||||
pgCalID := utils.ToPgUUID(calID)
|
||||
if err := qtx.SoftDeleteEventsByCalendar(ctx, pgCalID); err != nil {
|
||||
return models.ErrInternal
|
||||
}
|
||||
if err := qtx.SoftDeleteCalendar(ctx, pgCalID); err != nil {
|
||||
return models.ErrInternal
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return models.ErrInternal
|
||||
}
|
||||
|
||||
s.audit.Log(ctx, "calendar", calID, "DELETE_CALENDAR", userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CalendarService) Share(ctx context.Context, ownerID uuid.UUID, calID uuid.UUID, targetEmail, role string) error {
|
||||
ownerRole, err := s.getRole(ctx, calID, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ownerRole != "owner" {
|
||||
return models.ErrForbidden
|
||||
}
|
||||
if role != "editor" && role != "viewer" {
|
||||
return models.NewValidationError("role must be editor or viewer")
|
||||
}
|
||||
|
||||
targetUser, err := s.queries.GetUserByEmail(ctx, utils.NormalizeEmail(targetEmail))
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return models.NewNotFoundError("user not found")
|
||||
}
|
||||
return models.ErrInternal
|
||||
}
|
||||
|
||||
targetID := utils.FromPgUUID(targetUser.ID)
|
||||
if targetID == ownerID {
|
||||
return models.NewValidationError("cannot share with yourself")
|
||||
}
|
||||
|
||||
err = s.queries.UpsertCalendarMember(ctx, repository.UpsertCalendarMemberParams{
|
||||
CalendarID: utils.ToPgUUID(calID),
|
||||
UserID: utils.ToPgUUID(targetID),
|
||||
Role: role,
|
||||
})
|
||||
if err != nil {
|
||||
return models.ErrInternal
|
||||
}
|
||||
|
||||
s.audit.Log(ctx, "calendar", calID, "SHARE_CALENDAR", ownerID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CalendarService) ListMembers(ctx context.Context, userID uuid.UUID, calID uuid.UUID) ([]models.CalendarMember, error) {
|
||||
if _, err := s.getRole(ctx, calID, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := s.queries.ListCalendarMembers(ctx, utils.ToPgUUID(calID))
|
||||
if err != nil {
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
|
||||
members := make([]models.CalendarMember, len(rows))
|
||||
for i, r := range rows {
|
||||
members[i] = models.CalendarMember{
|
||||
UserID: utils.FromPgUUID(r.UserID),
|
||||
Email: r.Email,
|
||||
Role: r.Role,
|
||||
}
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (s *CalendarService) RemoveMember(ctx context.Context, ownerID uuid.UUID, calID uuid.UUID, targetUserID uuid.UUID) error {
|
||||
role, err := s.getRole(ctx, calID, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if role != "owner" {
|
||||
return models.ErrForbidden
|
||||
}
|
||||
|
||||
targetRole, err := s.getRole(ctx, calID, targetUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if targetRole == "owner" {
|
||||
return models.NewValidationError("cannot remove owner")
|
||||
}
|
||||
|
||||
err = s.queries.DeleteCalendarMember(ctx, repository.DeleteCalendarMemberParams{
|
||||
CalendarID: utils.ToPgUUID(calID),
|
||||
UserID: utils.ToPgUUID(targetUserID),
|
||||
})
|
||||
if err != nil {
|
||||
return models.ErrInternal
|
||||
}
|
||||
|
||||
s.audit.Log(ctx, "calendar", calID, "REMOVE_MEMBER", ownerID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CalendarService) GetRole(ctx context.Context, calID uuid.UUID, userID uuid.UUID) (string, error) {
|
||||
return s.getRole(ctx, calID, userID)
|
||||
}
|
||||
|
||||
func (s *CalendarService) getRole(ctx context.Context, calID uuid.UUID, userID uuid.UUID) (string, error) {
|
||||
role, err := s.queries.GetCalendarMemberRole(ctx, repository.GetCalendarMemberRoleParams{
|
||||
CalendarID: utils.ToPgUUID(calID),
|
||||
UserID: utils.ToPgUUID(userID),
|
||||
})
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return "", models.ErrNotFound
|
||||
}
|
||||
return "", models.ErrInternal
|
||||
}
|
||||
return role, nil
|
||||
}
|
||||
Reference in New Issue
Block a user