package services import ( "context" "database/sql" "errors" "fmt" "github.com/google/uuid" "github.com/jmoiron/sqlx" "mgit.msbls.de/m/paliad/internal/models" ) // AppointmentTargetService — CRUD on paliad.appointment_caldav_targets. // // Each row is the per-(appointment, binding) push state: the caldav_uid // PUT into that binding's calendar (canonical per Appointment) plus the // ETag returned by the server on the last successful PUT. Replaces the // scalar paliad.appointments.caldav_uid / caldav_etag columns for the // post-Slice-2a sync engine; those scalars stay populated for back-compat // through Slice 4. type AppointmentTargetService struct { db *sqlx.DB } func NewAppointmentTargetService(db *sqlx.DB) *AppointmentTargetService { return &AppointmentTargetService{db: db} } const targetColumns = `appointment_id, binding_id, caldav_uid, caldav_etag, last_pushed_at` // UpsertAfterPush records the result of a successful PUT to the binding's // calendar. Called by CalDAVService.pushAll after each PUT. func (s *AppointmentTargetService) UpsertAfterPush(ctx context.Context, appointmentID, bindingID uuid.UUID, uid, etag string) error { var etagPtr *string if etag != "" { etagPtr = &etag } _, err := s.db.ExecContext(ctx, `INSERT INTO paliad.appointment_caldav_targets (appointment_id, binding_id, caldav_uid, caldav_etag, last_pushed_at) VALUES ($1, $2, $3, $4, NOW()) ON CONFLICT (appointment_id, binding_id) DO UPDATE SET caldav_uid = EXCLUDED.caldav_uid, caldav_etag = EXCLUDED.caldav_etag, last_pushed_at = NOW()`, appointmentID, bindingID, uid, etagPtr) if err != nil { return fmt.Errorf("upsert caldav target: %w", err) } return nil } // FindByUIDAndBinding returns the target row matching this (uid, binding) // pair, or nil when no such row exists. func (s *AppointmentTargetService) FindByUIDAndBinding(ctx context.Context, uid string, bindingID uuid.UUID) (*models.AppointmentCalDAVTarget, error) { var t models.AppointmentCalDAVTarget err := s.db.GetContext(ctx, &t, `SELECT `+targetColumns+` FROM paliad.appointment_caldav_targets WHERE caldav_uid = $1 AND binding_id = $2`, uid, bindingID) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, fmt.Errorf("find target by uid+binding: %w", err) } return &t, nil } // ListForBinding returns every target row attached to this binding. // Used by the pull-reconciliation pass to detect remote deletions. func (s *AppointmentTargetService) ListForBinding(ctx context.Context, bindingID uuid.UUID) ([]models.AppointmentCalDAVTarget, error) { rows := []models.AppointmentCalDAVTarget{} if err := s.db.SelectContext(ctx, &rows, `SELECT `+targetColumns+` FROM paliad.appointment_caldav_targets WHERE binding_id = $1`, bindingID); err != nil { return nil, fmt.Errorf("list targets for binding: %w", err) } return rows, nil } // DeleteByAppointmentAndBinding removes one specific target row. // Used after a successful remote DELETE. func (s *AppointmentTargetService) DeleteByAppointmentAndBinding(ctx context.Context, appointmentID, bindingID uuid.UUID) error { _, err := s.db.ExecContext(ctx, `DELETE FROM paliad.appointment_caldav_targets WHERE appointment_id = $1 AND binding_id = $2`, appointmentID, bindingID) if err != nil { return fmt.Errorf("delete target: %w", err) } return nil } // StaleForBinding returns target rows whose appointment_id is no longer // in the in-scope set. Used by the post-pull cleanup pass to delete // appointments that left the binding's scope (e.g. project unshared, // scope_kind PATCHed). currentAppointmentIDs may be empty — in that // case every target row is considered stale. func (s *AppointmentTargetService) StaleForBinding(ctx context.Context, bindingID uuid.UUID, currentAppointmentIDs []uuid.UUID) ([]models.AppointmentCalDAVTarget, error) { rows := []models.AppointmentCalDAVTarget{} if len(currentAppointmentIDs) == 0 { if err := s.db.SelectContext(ctx, &rows, `SELECT `+targetColumns+` FROM paliad.appointment_caldav_targets WHERE binding_id = $1`, bindingID); err != nil { return nil, fmt.Errorf("stale-targets all: %w", err) } return rows, nil } query, args, err := sqlx.In( `SELECT `+targetColumns+` FROM paliad.appointment_caldav_targets WHERE binding_id = ? AND appointment_id NOT IN (?)`, bindingID, currentAppointmentIDs) if err != nil { return nil, fmt.Errorf("stale-targets prepare: %w", err) } query = s.db.Rebind(query) if err := s.db.SelectContext(ctx, &rows, query, args...); err != nil { return nil, fmt.Errorf("stale-targets exec: %w", err) } return rows, nil }