package service import ( "context" "net/http" "strings" "cmr-backend/internal/apperr" "cmr-backend/internal/store/postgres" ) type AdminPipelineService struct { store *postgres.Store configService *ConfigService } type AdminReleaseView struct { ID string `json:"id"` ReleaseNo int `json:"releaseNo"` ConfigLabel string `json:"configLabel"` ManifestURL string `json:"manifestUrl"` ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"` RouteCode *string `json:"routeCode,omitempty"` BuildID *string `json:"buildId,omitempty"` Status string `json:"status"` PublishedAt string `json:"publishedAt"` Runtime *RuntimeSummaryView `json:"runtime,omitempty"` Presentation *PresentationSummaryView `json:"presentation,omitempty"` ContentBundle *ContentBundleSummaryView `json:"contentBundle,omitempty"` } type AdminEventPipelineView struct { EventID string `json:"eventId"` CurrentRelease *AdminReleaseView `json:"currentRelease,omitempty"` Sources []EventConfigSourceView `json:"sources"` Builds []EventConfigBuildView `json:"builds"` Releases []AdminReleaseView `json:"releases"` } type AdminRollbackReleaseInput struct { ReleaseID string `json:"releaseId"` } type AdminBindReleaseRuntimeInput struct { RuntimeBindingID string `json:"runtimeBindingId"` } type AdminPublishBuildInput struct { RuntimeBindingID string `json:"runtimeBindingId,omitempty"` PresentationID string `json:"presentationId,omitempty"` ContentBundleID string `json:"contentBundleId,omitempty"` } func NewAdminPipelineService(store *postgres.Store, configService *ConfigService) *AdminPipelineService { return &AdminPipelineService{ store: store, configService: configService, } } func (s *AdminPipelineService) GetEventPipeline(ctx context.Context, eventPublicID string, limit int) (*AdminEventPipelineView, error) { event, err := s.store.GetEventByPublicID(ctx, strings.TrimSpace(eventPublicID)) if err != nil { return nil, err } if event == nil { return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found") } sources, err := s.configService.ListEventConfigSources(ctx, event.PublicID, limit) if err != nil { return nil, err } buildRecords, err := s.store.ListEventConfigBuildsByEventID(ctx, event.ID, limit) if err != nil { return nil, err } releaseRecords, err := s.store.ListEventReleasesByEventID(ctx, event.ID, limit) if err != nil { return nil, err } builds := make([]EventConfigBuildView, 0, len(buildRecords)) for i := range buildRecords { item, err := buildEventConfigBuildView(&buildRecords[i]) if err != nil { return nil, err } builds = append(builds, *item) } releases := make([]AdminReleaseView, 0, len(releaseRecords)) for _, item := range releaseRecords { view := buildAdminReleaseView(item) if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, item.PresentationID); err != nil { return nil, err } else if enrichedPresentation != nil { view.Presentation = enrichedPresentation } if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, item.ContentBundleID); err != nil { return nil, err } else if enrichedBundle != nil { view.ContentBundle = enrichedBundle } releases = append(releases, view) } result := &AdminEventPipelineView{ EventID: event.PublicID, Sources: sources, Builds: builds, Releases: releases, } if event.CurrentReleasePubID != nil { result.CurrentRelease = &AdminReleaseView{ ID: *event.CurrentReleasePubID, ConfigLabel: derefStringOrEmpty(event.ConfigLabel), ManifestURL: derefStringOrEmpty(event.ManifestURL), ManifestChecksumSha256: event.ManifestChecksum, RouteCode: event.RouteCode, Status: "published", Runtime: buildRuntimeSummaryFromEvent(event), Presentation: buildPresentationSummaryFromEvent(event), ContentBundle: buildContentBundleSummaryFromEvent(event), } if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil { return nil, err } else if enrichedPresentation != nil { result.CurrentRelease.Presentation = enrichedPresentation } if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil { return nil, err } else if enrichedBundle != nil { result.CurrentRelease.ContentBundle = enrichedBundle } } return result, nil } func (s *AdminPipelineService) BuildSource(ctx context.Context, sourceID string) (*EventConfigBuildView, error) { return s.configService.BuildPreview(ctx, BuildPreviewInput{SourceID: sourceID}) } func (s *AdminPipelineService) GetBuild(ctx context.Context, buildID string) (*EventConfigBuildView, error) { return s.configService.GetEventConfigBuild(ctx, buildID) } func (s *AdminPipelineService) PublishBuild(ctx context.Context, buildID string, input AdminPublishBuildInput) (*PublishedReleaseView, error) { return s.configService.PublishBuild(ctx, PublishBuildInput{ BuildID: buildID, RuntimeBindingID: input.RuntimeBindingID, PresentationID: input.PresentationID, ContentBundleID: input.ContentBundleID, }) } func (s *AdminPipelineService) GetRelease(ctx context.Context, releasePublicID string) (*AdminReleaseView, error) { release, err := s.store.GetEventReleaseByPublicID(ctx, strings.TrimSpace(releasePublicID)) if err != nil { return nil, err } if release == nil { return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found") } view := buildAdminReleaseView(*release) if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, release.PresentationID); err != nil { return nil, err } else if enrichedPresentation != nil { view.Presentation = enrichedPresentation } if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, release.ContentBundleID); err != nil { return nil, err } else if enrichedBundle != nil { view.ContentBundle = enrichedBundle } return &view, nil } func (s *AdminPipelineService) BindReleaseRuntime(ctx context.Context, releasePublicID string, input AdminBindReleaseRuntimeInput) (*AdminReleaseView, error) { release, err := s.store.GetEventReleaseByPublicID(ctx, strings.TrimSpace(releasePublicID)) if err != nil { return nil, err } if release == nil { return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found") } input.RuntimeBindingID = strings.TrimSpace(input.RuntimeBindingID) if input.RuntimeBindingID == "" { return nil, apperr.New(http.StatusBadRequest, "invalid_params", "runtimeBindingId is required") } runtimeBinding, err := s.store.GetMapRuntimeBindingByPublicID(ctx, input.RuntimeBindingID) if err != nil { return nil, err } if runtimeBinding == nil { return nil, apperr.New(http.StatusNotFound, "runtime_binding_not_found", "runtime binding not found") } if runtimeBinding.EventID != release.EventID { return nil, apperr.New(http.StatusConflict, "runtime_binding_not_belong_to_event", "runtime binding does not belong to release event") } tx, err := s.store.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) if err := s.store.SetEventReleaseRuntimeBinding(ctx, tx, release.ID, &runtimeBinding.ID); err != nil { return nil, err } if err := tx.Commit(ctx); err != nil { return nil, err } updated, err := s.store.GetEventReleaseByPublicID(ctx, release.PublicID) if err != nil { return nil, err } if updated == nil { return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found") } view := buildAdminReleaseView(*updated) return &view, nil } func (s *AdminPipelineService) RollbackRelease(ctx context.Context, eventPublicID string, input AdminRollbackReleaseInput) (*AdminReleaseView, error) { event, err := s.store.GetEventByPublicID(ctx, strings.TrimSpace(eventPublicID)) if err != nil { return nil, err } if event == nil { return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found") } input.ReleaseID = strings.TrimSpace(input.ReleaseID) if input.ReleaseID == "" { return nil, apperr.New(http.StatusBadRequest, "invalid_params", "releaseId is required") } release, err := s.store.GetEventReleaseByPublicID(ctx, input.ReleaseID) if err != nil { return nil, err } if release == nil { return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found") } if release.EventID != event.ID { return nil, apperr.New(http.StatusConflict, "release_not_belong_to_event", "release does not belong to event") } if release.Status != "published" { return nil, apperr.New(http.StatusConflict, "release_not_publishable", "release is not published") } tx, err := s.store.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) if err := s.store.SetCurrentEventRelease(ctx, tx, event.ID, release.ID); err != nil { return nil, err } if err := tx.Commit(ctx); err != nil { return nil, err } view := buildAdminReleaseView(*release) return &view, nil } func buildAdminReleaseView(item postgres.EventRelease) AdminReleaseView { return AdminReleaseView{ ID: item.PublicID, ReleaseNo: item.ReleaseNo, ConfigLabel: item.ConfigLabel, ManifestURL: item.ManifestURL, ManifestChecksumSha256: item.ManifestChecksum, RouteCode: item.RouteCode, BuildID: item.BuildID, Status: item.Status, PublishedAt: item.PublishedAt.Format(timeRFC3339), Runtime: buildRuntimeSummaryFromRelease(&item), Presentation: buildPresentationSummaryFromRelease(&item), ContentBundle: buildContentBundleSummaryFromRelease(&item), } } func derefStringOrEmpty(value *string) string { if value == nil { return "" } return *value }