Files
story/internal/webevents/service.go
T

112 lines
3.8 KiB
Go

package webevents
import (
"context"
"errors"
"time"
"data-terminal-cloud/internal/identity"
"github.com/jackc/pgx/v5/pgxpool"
)
var ErrCursorUnavailable = errors.New("web event cursor is unavailable")
type Event struct {
ID int64 `json:"id"`
Type string `json:"type"`
HouseholdID string `json:"household_id"`
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
SubjectID string `json:"subject_id,omitempty"`
Status string `json:"status"`
OccurredAt time.Time `json:"occurred_at"`
}
type Service struct{ pool *pgxpool.Pool }
func NewService(pool *pgxpool.Pool) *Service { return &Service{pool: pool} }
func (s *Service) StartingCursor(ctx context.Context, principal identity.Principal) (int64, error) {
var cursor int64
err := s.pool.QueryRow(ctx, `
SELECT COALESCE(max(we.id),0)
FROM web_event we
JOIN household_member hm ON hm.tenant_id=we.tenant_id AND hm.household_id=we.household_id AND hm.status='active'
JOIN user_account ua ON ua.id=hm.user_id AND ua.tenant_id=we.tenant_id AND ua.status='active'
WHERE ua.oidc_issuer=$1 AND ua.oidc_subject=$2 AND we.expires_at>now()`, principal.Issuer, principal.Subject,
).Scan(&cursor)
return cursor, err
}
func (s *Service) ValidateCursor(ctx context.Context, principal identity.Principal, cursor int64) error {
if cursor == 0 {
return nil
}
var valid bool
err := s.pool.QueryRow(ctx, `
SELECT EXISTS(
SELECT 1 FROM web_event we
JOIN household_member hm ON hm.tenant_id=we.tenant_id AND hm.household_id=we.household_id AND hm.status='active'
JOIN user_account ua ON ua.id=hm.user_id AND ua.tenant_id=we.tenant_id AND ua.status='active'
WHERE we.id=$1 AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND we.expires_at>now()
)`, cursor, principal.Issuer, principal.Subject,
).Scan(&valid)
if err != nil {
return err
}
if !valid {
return ErrCursorUnavailable
}
return nil
}
func (s *Service) ListAfter(ctx context.Context, principal identity.Principal, cursor int64, limit int) ([]Event, error) {
if limit < 1 || limit > 100 {
limit = 100
}
rows, err := s.pool.Query(ctx, `
SELECT we.id,we.event_type,h.public_id::text,we.resource_type,we.resource_public_id::text,
COALESCE(sp.public_id::text,''),we.status,we.occurred_at
FROM web_event we
JOIN household h ON h.id=we.household_id AND h.tenant_id=we.tenant_id
JOIN household_member hm ON hm.tenant_id=we.tenant_id AND hm.household_id=we.household_id AND hm.status='active'
JOIN user_account ua ON ua.id=hm.user_id AND ua.tenant_id=we.tenant_id AND ua.status='active'
LEFT JOIN subject_profile sp ON sp.id=we.subject_id AND sp.tenant_id=we.tenant_id
WHERE we.id>$1 AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND we.expires_at>now()
ORDER BY we.id LIMIT $4`, cursor, principal.Issuer, principal.Subject, limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]Event, 0)
for rows.Next() {
var item Event
if err := rows.Scan(&item.ID, &item.Type, &item.HouseholdID, &item.ResourceType, &item.ResourceID, &item.SubjectID, &item.Status, &item.OccurredAt); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) DeleteExpired(ctx context.Context, limit int) (int64, error) {
if limit < 1 || limit > 10000 {
limit = 1000
}
result, err := s.pool.Exec(ctx, `DELETE FROM web_event WHERE id IN (SELECT id FROM web_event WHERE expires_at<=now() ORDER BY id LIMIT $1)`, limit)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
func (s *Service) ExpireDevicePresence(ctx context.Context, staleBefore time.Time, limit int) (int, error) {
if limit < 1 || limit > 10000 {
limit = 1000
}
var count int
err := s.pool.QueryRow(ctx, `SELECT expire_device_presence($1,$2)`, staleBefore.UTC(), limit).Scan(&count)
return count, err
}