Add backend foundation and config-driven workbench
This commit is contained in:
195
backend/internal/service/event_service.go
Normal file
195
backend/internal/service/event_service.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
"cmr-backend/internal/platform/security"
|
||||
"cmr-backend/internal/store/postgres"
|
||||
)
|
||||
|
||||
type EventService struct {
|
||||
store *postgres.Store
|
||||
}
|
||||
|
||||
type EventDetailResult struct {
|
||||
Event struct {
|
||||
ID string `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Summary *string `json:"summary,omitempty"`
|
||||
Status string `json:"status"`
|
||||
} `json:"event"`
|
||||
Release *struct {
|
||||
ID string `json:"id"`
|
||||
ConfigLabel string `json:"configLabel"`
|
||||
ManifestURL string `json:"manifestUrl"`
|
||||
ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
} `json:"release,omitempty"`
|
||||
ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
|
||||
}
|
||||
|
||||
type LaunchEventInput struct {
|
||||
EventPublicID string
|
||||
UserID string
|
||||
ReleaseID string `json:"releaseId,omitempty"`
|
||||
ClientType string `json:"clientType"`
|
||||
DeviceKey string `json:"deviceKey"`
|
||||
}
|
||||
|
||||
type LaunchEventResult struct {
|
||||
Event struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
} `json:"event"`
|
||||
Launch struct {
|
||||
Source string `json:"source"`
|
||||
ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
|
||||
Config struct {
|
||||
ConfigURL string `json:"configUrl"`
|
||||
ConfigLabel string `json:"configLabel"`
|
||||
ConfigChecksumSha256 *string `json:"configChecksumSha256,omitempty"`
|
||||
ReleaseID string `json:"releaseId"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
} `json:"config"`
|
||||
Business struct {
|
||||
Source string `json:"source"`
|
||||
EventID string `json:"eventId"`
|
||||
SessionID string `json:"sessionId"`
|
||||
SessionToken string `json:"sessionToken"`
|
||||
SessionTokenExpiresAt string `json:"sessionTokenExpiresAt"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
} `json:"business"`
|
||||
} `json:"launch"`
|
||||
}
|
||||
|
||||
func NewEventService(store *postgres.Store) *EventService {
|
||||
return &EventService{store: store}
|
||||
}
|
||||
|
||||
func (s *EventService) GetEventDetail(ctx context.Context, eventPublicID string) (*EventDetailResult, error) {
|
||||
eventPublicID = strings.TrimSpace(eventPublicID)
|
||||
if eventPublicID == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id is required")
|
||||
}
|
||||
|
||||
event, err := s.store.GetEventByPublicID(ctx, eventPublicID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if event == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
|
||||
result := &EventDetailResult{}
|
||||
result.Event.ID = event.PublicID
|
||||
result.Event.Slug = event.Slug
|
||||
result.Event.DisplayName = event.DisplayName
|
||||
result.Event.Summary = event.Summary
|
||||
result.Event.Status = event.Status
|
||||
|
||||
if event.CurrentReleasePubID != nil && event.ConfigLabel != nil && event.ManifestURL != nil {
|
||||
result.Release = &struct {
|
||||
ID string `json:"id"`
|
||||
ConfigLabel string `json:"configLabel"`
|
||||
ManifestURL string `json:"manifestUrl"`
|
||||
ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
}{
|
||||
ID: *event.CurrentReleasePubID,
|
||||
ConfigLabel: *event.ConfigLabel,
|
||||
ManifestURL: *event.ManifestURL,
|
||||
ManifestChecksumSha256: event.ManifestChecksum,
|
||||
RouteCode: event.RouteCode,
|
||||
}
|
||||
}
|
||||
result.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *EventService) LaunchEvent(ctx context.Context, input LaunchEventInput) (*LaunchEventResult, error) {
|
||||
input.EventPublicID = strings.TrimSpace(input.EventPublicID)
|
||||
input.ReleaseID = strings.TrimSpace(input.ReleaseID)
|
||||
input.DeviceKey = strings.TrimSpace(input.DeviceKey)
|
||||
if err := validateClientType(input.ClientType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.EventPublicID == "" || input.UserID == "" || input.DeviceKey == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id, user and deviceKey are required")
|
||||
}
|
||||
|
||||
event, err := s.store.GetEventByPublicID(ctx, input.EventPublicID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if event == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
if event.Status != "active" {
|
||||
return nil, apperr.New(http.StatusConflict, "event_not_launchable", "event is not active")
|
||||
}
|
||||
if event.CurrentReleaseID == nil || event.CurrentReleasePubID == nil || event.ConfigLabel == nil || event.ManifestURL == nil {
|
||||
return nil, apperr.New(http.StatusConflict, "event_release_missing", "event does not have a published release")
|
||||
}
|
||||
if input.ReleaseID != "" && input.ReleaseID != *event.CurrentReleasePubID {
|
||||
return nil, apperr.New(http.StatusConflict, "release_not_launchable", "requested release is not the current published release")
|
||||
}
|
||||
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
sessionPublicID, err := security.GeneratePublicID("sess")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionToken, err := security.GenerateToken(32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionTokenExpiresAt := time.Now().UTC().Add(2 * time.Hour)
|
||||
|
||||
session, err := s.store.CreateGameSession(ctx, tx, postgres.CreateGameSessionParams{
|
||||
SessionPublicID: sessionPublicID,
|
||||
UserID: input.UserID,
|
||||
EventID: event.ID,
|
||||
EventReleaseID: *event.CurrentReleaseID,
|
||||
DeviceKey: input.DeviceKey,
|
||||
ClientType: input.ClientType,
|
||||
RouteCode: event.RouteCode,
|
||||
SessionTokenHash: security.HashText(sessionToken),
|
||||
SessionTokenExpiresAt: sessionTokenExpiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &LaunchEventResult{}
|
||||
result.Event.ID = event.PublicID
|
||||
result.Event.DisplayName = event.DisplayName
|
||||
result.Launch.Source = LaunchSourceEventCurrentRelease
|
||||
result.Launch.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
|
||||
result.Launch.Config.ConfigURL = *event.ManifestURL
|
||||
result.Launch.Config.ConfigLabel = *event.ConfigLabel
|
||||
result.Launch.Config.ConfigChecksumSha256 = event.ManifestChecksum
|
||||
result.Launch.Config.ReleaseID = *event.CurrentReleasePubID
|
||||
result.Launch.Config.RouteCode = event.RouteCode
|
||||
result.Launch.Business.Source = "direct-event"
|
||||
result.Launch.Business.EventID = event.PublicID
|
||||
result.Launch.Business.SessionID = session.SessionPublicID
|
||||
result.Launch.Business.SessionToken = sessionToken
|
||||
result.Launch.Business.SessionTokenExpiresAt = session.SessionTokenExpiresAt.Format(time.RFC3339)
|
||||
result.Launch.Business.RouteCode = event.RouteCode
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user