package postgres import ( "context" "errors" "fmt" "time" "github.com/jackc/pgx/v5" ) type Session struct { ID string SessionPublicID string UserID string EventID string EventReleaseID string ReleasePublicID *string ConfigLabel *string ManifestURL *string ManifestChecksum *string DeviceKey string ClientType string RouteCode *string Status string SessionTokenHash string SessionTokenExpiresAt time.Time LaunchedAt time.Time StartedAt *time.Time EndedAt *time.Time EventPublicID *string EventDisplayName *string } type FinishSessionParams struct { SessionID string Status string } func (s *Store) GetSessionByPublicID(ctx context.Context, sessionPublicID string) (*Session, error) { row := s.pool.QueryRow(ctx, ` SELECT gs.id, gs.session_public_id, gs.user_id, gs.event_id, gs.event_release_id, er.release_public_id, er.config_label, er.manifest_url, er.manifest_checksum_sha256, gs.device_key, gs.client_type, gs.route_code, gs.status, gs.session_token_hash, gs.session_token_expires_at, gs.launched_at, gs.started_at, gs.ended_at, e.event_public_id, e.display_name FROM game_sessions gs JOIN events e ON e.id = gs.event_id JOIN event_releases er ON er.id = gs.event_release_id WHERE gs.session_public_id = $1 LIMIT 1 `, sessionPublicID) return scanSession(row) } func (s *Store) GetSessionByPublicIDForUpdate(ctx context.Context, tx Tx, sessionPublicID string) (*Session, error) { row := tx.QueryRow(ctx, ` SELECT gs.id, gs.session_public_id, gs.user_id, gs.event_id, gs.event_release_id, er.release_public_id, er.config_label, er.manifest_url, er.manifest_checksum_sha256, gs.device_key, gs.client_type, gs.route_code, gs.status, gs.session_token_hash, gs.session_token_expires_at, gs.launched_at, gs.started_at, gs.ended_at, e.event_public_id, e.display_name FROM game_sessions gs JOIN events e ON e.id = gs.event_id JOIN event_releases er ON er.id = gs.event_release_id WHERE gs.session_public_id = $1 FOR UPDATE `, sessionPublicID) return scanSession(row) } func (s *Store) ListSessionsByUserID(ctx context.Context, userID string, limit int) ([]Session, error) { if limit <= 0 || limit > 100 { limit = 20 } rows, err := s.pool.Query(ctx, ` SELECT gs.id, gs.session_public_id, gs.user_id, gs.event_id, gs.event_release_id, er.release_public_id, er.config_label, er.manifest_url, er.manifest_checksum_sha256, gs.device_key, gs.client_type, gs.route_code, gs.status, gs.session_token_hash, gs.session_token_expires_at, gs.launched_at, gs.started_at, gs.ended_at, e.event_public_id, e.display_name FROM game_sessions gs JOIN events e ON e.id = gs.event_id JOIN event_releases er ON er.id = gs.event_release_id WHERE gs.user_id = $1 ORDER BY gs.created_at DESC LIMIT $2 `, userID, limit) if err != nil { return nil, fmt.Errorf("list sessions by user id: %w", err) } defer rows.Close() var sessions []Session for rows.Next() { session, err := scanSessionFromRows(rows) if err != nil { return nil, err } sessions = append(sessions, *session) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate sessions by user id: %w", err) } return sessions, nil } func (s *Store) ListSessionsByUserAndEvent(ctx context.Context, userID, eventID string, limit int) ([]Session, error) { if limit <= 0 || limit > 100 { limit = 20 } rows, err := s.pool.Query(ctx, ` SELECT gs.id, gs.session_public_id, gs.user_id, gs.event_id, gs.event_release_id, er.release_public_id, er.config_label, er.manifest_url, er.manifest_checksum_sha256, gs.device_key, gs.client_type, gs.route_code, gs.status, gs.session_token_hash, gs.session_token_expires_at, gs.launched_at, gs.started_at, gs.ended_at, e.event_public_id, e.display_name FROM game_sessions gs JOIN events e ON e.id = gs.event_id JOIN event_releases er ON er.id = gs.event_release_id WHERE gs.user_id = $1 AND gs.event_id = $2 ORDER BY gs.created_at DESC LIMIT $3 `, userID, eventID, limit) if err != nil { return nil, fmt.Errorf("list sessions by user and event: %w", err) } defer rows.Close() var sessions []Session for rows.Next() { session, err := scanSessionFromRows(rows) if err != nil { return nil, err } sessions = append(sessions, *session) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate sessions by user and event: %w", err) } return sessions, nil } func (s *Store) StartSession(ctx context.Context, tx Tx, sessionID string) error { _, err := tx.Exec(ctx, ` UPDATE game_sessions SET status = CASE WHEN status = 'launched' THEN 'running' ELSE status END, started_at = COALESCE(started_at, NOW()) WHERE id = $1 `, sessionID) if err != nil { return fmt.Errorf("start session: %w", err) } return nil } func (s *Store) FinishSession(ctx context.Context, tx Tx, params FinishSessionParams) error { _, err := tx.Exec(ctx, ` UPDATE game_sessions SET status = $2, started_at = COALESCE(started_at, NOW()), ended_at = COALESCE(ended_at, NOW()) WHERE id = $1 `, params.SessionID, params.Status) if err != nil { return fmt.Errorf("finish session: %w", err) } return nil } func scanSession(row pgx.Row) (*Session, error) { var session Session err := row.Scan( &session.ID, &session.SessionPublicID, &session.UserID, &session.EventID, &session.EventReleaseID, &session.ReleasePublicID, &session.ConfigLabel, &session.ManifestURL, &session.ManifestChecksum, &session.DeviceKey, &session.ClientType, &session.RouteCode, &session.Status, &session.SessionTokenHash, &session.SessionTokenExpiresAt, &session.LaunchedAt, &session.StartedAt, &session.EndedAt, &session.EventPublicID, &session.EventDisplayName, ) if errors.Is(err, pgx.ErrNoRows) { return nil, nil } if err != nil { return nil, fmt.Errorf("scan session: %w", err) } return &session, nil } func scanSessionFromRows(rows pgx.Rows) (*Session, error) { var session Session err := rows.Scan( &session.ID, &session.SessionPublicID, &session.UserID, &session.EventID, &session.EventReleaseID, &session.ReleasePublicID, &session.ConfigLabel, &session.ManifestURL, &session.ManifestChecksum, &session.DeviceKey, &session.ClientType, &session.RouteCode, &session.Status, &session.SessionTokenHash, &session.SessionTokenExpiresAt, &session.LaunchedAt, &session.StartedAt, &session.EndedAt, &session.EventPublicID, &session.EventDisplayName, ) if err != nil { return nil, fmt.Errorf("scan session row: %w", err) } return &session, nil }