Add OpenAPI docs, frontend, migrations, and API updates
- OpenAPI: add missing endpoints (add-from-url, subscriptions, public availability) - OpenAPI: CalendarSubscription schema, Subscriptions tag - Frontend app - Migrations: count_for_availability, subscriptions_sync, user_preferences, calendar_settings - Config, rate limit, auth, calendar, booking, ICS, availability, user service updates Made-with: Cursor
This commit is contained in:
@@ -3,12 +3,15 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type AvailabilityService struct {
|
||||
@@ -84,3 +87,142 @@ func (s *AvailabilityService) GetBusyBlocks(ctx context.Context, userID uuid.UUI
|
||||
Busy: busy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getBusyBlocksForCalendar
|
||||
func (s *AvailabilityService) getBusyBlocksForCalendar(ctx context.Context, calID uuid.UUID, rangeStart, rangeEnd time.Time) ([]models.BusyBlock, error) {
|
||||
pgCalID := utils.ToPgUUID(calID)
|
||||
events, err := s.queries.ListEventsByCalendarInRange(ctx, repository.ListEventsByCalendarInRangeParams{
|
||||
CalendarID: pgCalID,
|
||||
EndTime: utils.ToPgTimestamptz(rangeStart),
|
||||
StartTime: utils.ToPgTimestamptz(rangeEnd),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recurring, err := s.queries.ListRecurringEventsByCalendar(ctx, repository.ListRecurringEventsByCalendarParams{
|
||||
CalendarID: pgCalID,
|
||||
StartTime: utils.ToPgTimestamptz(rangeEnd),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var busy []models.BusyBlock
|
||||
for _, ev := range events {
|
||||
if ev.RecurrenceRule.Valid {
|
||||
continue
|
||||
}
|
||||
busy = append(busy, models.BusyBlock{
|
||||
Start: utils.FromPgTimestamptz(ev.StartTime),
|
||||
End: utils.FromPgTimestamptz(ev.EndTime),
|
||||
EventID: utils.FromPgUUID(ev.ID),
|
||||
})
|
||||
}
|
||||
|
||||
for _, ev := range recurring {
|
||||
occs := s.event.expandRecurrence(ev, rangeStart, rangeEnd)
|
||||
for _, occ := range occs {
|
||||
if occ.OccurrenceStartTime != nil {
|
||||
busy = append(busy, models.BusyBlock{
|
||||
Start: *occ.OccurrenceStartTime,
|
||||
End: *occ.OccurrenceEndTime,
|
||||
EventID: occ.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return busy, nil
|
||||
}
|
||||
|
||||
// GetBusyBlocksByTokenPublic returns busy blocks for a calendar by its public token. No auth required.
|
||||
func (s *AvailabilityService) GetBusyBlocksByTokenPublic(ctx context.Context, token string, rangeStart, rangeEnd time.Time) (*models.AvailabilityResponse, error) {
|
||||
if token == "" {
|
||||
return nil, models.ErrNotFound
|
||||
}
|
||||
|
||||
cal, err := s.queries.GetCalendarByToken(ctx, pgtype.Text{String: token, Valid: true})
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, models.ErrNotFound
|
||||
}
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
|
||||
calID := utils.FromPgUUID(cal.ID)
|
||||
busy, err := s.getBusyBlocksForCalendar(ctx, calID, rangeStart, rangeEnd)
|
||||
if err != nil {
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
|
||||
sort.Slice(busy, func(i, j int) bool {
|
||||
return busy[i].Start.Before(busy[j].Start)
|
||||
})
|
||||
|
||||
if busy == nil {
|
||||
busy = []models.BusyBlock{}
|
||||
}
|
||||
|
||||
return &models.AvailabilityResponse{
|
||||
CalendarID: calID,
|
||||
RangeStart: rangeStart,
|
||||
RangeEnd: rangeEnd,
|
||||
Busy: busy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetBusyBlocksAggregate returns merged busy blocks across multiple calendars by their tokens. No auth required.
|
||||
func (s *AvailabilityService) GetBusyBlocksAggregate(ctx context.Context, tokens []string, rangeStart, rangeEnd time.Time) (*models.AvailabilityResponse, error) {
|
||||
if len(tokens) == 0 {
|
||||
return &models.AvailabilityResponse{
|
||||
RangeStart: rangeStart,
|
||||
RangeEnd: rangeEnd,
|
||||
Busy: []models.BusyBlock{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
seen := make(map[uuid.UUID]bool)
|
||||
var allBusy []models.BusyBlock
|
||||
|
||||
for _, token := range tokens {
|
||||
token = strings.TrimSpace(token)
|
||||
if token == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
cal, err := s.queries.GetCalendarByToken(ctx, pgtype.Text{String: token, Valid: true})
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
continue
|
||||
}
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
|
||||
calID := utils.FromPgUUID(cal.ID)
|
||||
if seen[calID] {
|
||||
continue
|
||||
}
|
||||
seen[calID] = true
|
||||
|
||||
busy, err := s.getBusyBlocksForCalendar(ctx, calID, rangeStart, rangeEnd)
|
||||
if err != nil {
|
||||
return nil, models.ErrInternal
|
||||
}
|
||||
allBusy = append(allBusy, busy...)
|
||||
}
|
||||
|
||||
sort.Slice(allBusy, func(i, j int) bool {
|
||||
return allBusy[i].Start.Before(allBusy[j].Start)
|
||||
})
|
||||
|
||||
if allBusy == nil {
|
||||
allBusy = []models.BusyBlock{}
|
||||
}
|
||||
|
||||
return &models.AvailabilityResponse{
|
||||
RangeStart: rangeStart,
|
||||
RangeEnd: rangeEnd,
|
||||
Busy: allBusy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user