package postgres import ( "context" "database/sql" "time" "chat-switchboard/models" ) // ── SessionStore ─────────────────────────── type SessionStore struct{} func NewSessionStore() *SessionStore { return &SessionStore{} } func (s *SessionStore) Create(ctx context.Context, sp *models.SessionParticipant) error { return DB.QueryRowContext(ctx, ` INSERT INTO session_participants (session_token, channel_id, display_name, fingerprint) VALUES ($1, $2, $3, $4) RETURNING id, created_at`, sp.SessionToken, sp.ChannelID, sp.DisplayName, nullText(sp.Fingerprint), ).Scan(&sp.ID, &sp.CreatedAt) } func (s *SessionStore) GetByToken(ctx context.Context, token string) (*models.SessionParticipant, error) { sp := &models.SessionParticipant{} err := DB.QueryRowContext(ctx, ` SELECT id, session_token, channel_id, display_name, COALESCE(fingerprint, ''), created_at FROM session_participants WHERE session_token = $1`, token, ).Scan(&sp.ID, &sp.SessionToken, &sp.ChannelID, &sp.DisplayName, &sp.Fingerprint, &sp.CreatedAt) if err != nil { return nil, err } return sp, nil } func (s *SessionStore) GetByID(ctx context.Context, id string) (*models.SessionParticipant, error) { sp := &models.SessionParticipant{} err := DB.QueryRowContext(ctx, ` SELECT id, session_token, channel_id, display_name, COALESCE(fingerprint, ''), created_at FROM session_participants WHERE id = $1`, id, ).Scan(&sp.ID, &sp.SessionToken, &sp.ChannelID, &sp.DisplayName, &sp.Fingerprint, &sp.CreatedAt) if err != nil { return nil, err } return sp, nil } func (s *SessionStore) ListForChannel(ctx context.Context, channelID string) ([]models.SessionParticipant, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, session_token, channel_id, display_name, COALESCE(fingerprint, ''), created_at FROM session_participants WHERE channel_id = $1 ORDER BY created_at ASC`, channelID) if err != nil { return nil, err } defer rows.Close() var result []models.SessionParticipant for rows.Next() { var sp models.SessionParticipant if err := rows.Scan(&sp.ID, &sp.SessionToken, &sp.ChannelID, &sp.DisplayName, &sp.Fingerprint, &sp.CreatedAt); err != nil { return nil, err } result = append(result, sp) } return result, rows.Err() } func (s *SessionStore) CountForChannel(ctx context.Context, channelID string) (int, error) { var count int err := DB.QueryRowContext(ctx, ` SELECT COUNT(*) FROM session_participants WHERE channel_id = $1`, channelID, ).Scan(&count) return count, err } func (s *SessionStore) Delete(ctx context.Context, id string) error { res, err := DB.ExecContext(ctx, `DELETE FROM session_participants WHERE id = $1`, id) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return sql.ErrNoRows } return nil } // DeleteExpired removes sessions created before olderThan whose channel // has no messages. Returns the count of deleted rows. func (s *SessionStore) DeleteExpired(ctx context.Context, olderThan time.Time) (int64, error) { res, err := DB.ExecContext(ctx, ` DELETE FROM session_participants sp WHERE sp.created_at < $1 AND NOT EXISTS ( SELECT 1 FROM messages m WHERE m.channel_id = sp.channel_id )`, olderThan) if err != nil { return 0, err } return res.RowsAffected() } // nullText returns nil for empty strings. func nullText(s string) interface{} { if s == "" { return nil } return s }