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"` } 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"` } 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 { releases = append(releases, buildAdminReleaseView(item)) } 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", } } 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) (*PublishedReleaseView, error) { return s.configService.PublishBuild(ctx, PublishBuildInput{BuildID: buildID}) } 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), } } func derefStringOrEmpty(value *string) string { if value == nil { return "" } return *value }