700 lines
21 KiB
Go
700 lines
21 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"cmr-backend/internal/apperr"
|
|
"cmr-backend/internal/platform/assets"
|
|
"cmr-backend/internal/platform/security"
|
|
"cmr-backend/internal/store/postgres"
|
|
)
|
|
|
|
type ConfigService struct {
|
|
store *postgres.Store
|
|
localEventDir string
|
|
assetBaseURL string
|
|
publisher *assets.OSSUtilPublisher
|
|
}
|
|
|
|
type ConfigPipelineSummary struct {
|
|
SourceTable string `json:"sourceTable"`
|
|
BuildTable string `json:"buildTable"`
|
|
ReleaseAssetsTable string `json:"releaseAssetsTable"`
|
|
}
|
|
|
|
type LocalEventFile struct {
|
|
FileName string `json:"fileName"`
|
|
FullPath string `json:"fullPath"`
|
|
}
|
|
|
|
type EventConfigSourceView struct {
|
|
ID string `json:"id"`
|
|
EventID string `json:"eventId"`
|
|
SourceVersionNo int `json:"sourceVersionNo"`
|
|
SourceKind string `json:"sourceKind"`
|
|
SchemaID string `json:"schemaId"`
|
|
SchemaVersion string `json:"schemaVersion"`
|
|
Status string `json:"status"`
|
|
Notes *string `json:"notes,omitempty"`
|
|
Source map[string]any `json:"source"`
|
|
}
|
|
|
|
type EventConfigBuildView struct {
|
|
ID string `json:"id"`
|
|
EventID string `json:"eventId"`
|
|
SourceID string `json:"sourceId"`
|
|
BuildNo int `json:"buildNo"`
|
|
BuildStatus string `json:"buildStatus"`
|
|
BuildLog *string `json:"buildLog,omitempty"`
|
|
Manifest map[string]any `json:"manifest"`
|
|
AssetIndex []map[string]any `json:"assetIndex"`
|
|
}
|
|
|
|
type PublishedReleaseView struct {
|
|
EventID string `json:"eventId"`
|
|
Release ResolvedReleaseView `json:"release"`
|
|
ReleaseNo int `json:"releaseNo"`
|
|
PublishedAt string `json:"publishedAt"`
|
|
}
|
|
|
|
type ImportLocalEventConfigInput struct {
|
|
EventPublicID string
|
|
FileName string `json:"fileName"`
|
|
Notes *string `json:"notes,omitempty"`
|
|
}
|
|
|
|
type BuildPreviewInput struct {
|
|
SourceID string `json:"sourceId"`
|
|
}
|
|
|
|
type PublishBuildInput struct {
|
|
BuildID string `json:"buildId"`
|
|
}
|
|
|
|
func NewConfigService(store *postgres.Store, localEventDir, assetBaseURL string, publisher *assets.OSSUtilPublisher) *ConfigService {
|
|
return &ConfigService{
|
|
store: store,
|
|
localEventDir: localEventDir,
|
|
assetBaseURL: strings.TrimRight(assetBaseURL, "/"),
|
|
publisher: publisher,
|
|
}
|
|
}
|
|
|
|
func (s *ConfigService) PipelineSummary() ConfigPipelineSummary {
|
|
return ConfigPipelineSummary{
|
|
SourceTable: "event_config_sources",
|
|
BuildTable: "event_config_builds",
|
|
ReleaseAssetsTable: "event_release_assets",
|
|
}
|
|
}
|
|
|
|
func (s *ConfigService) ListLocalEventFiles() ([]LocalEventFile, error) {
|
|
dir, err := filepath.Abs(s.localEventDir)
|
|
if err != nil {
|
|
return nil, apperr.New(http.StatusInternalServerError, "config_dir_invalid", "failed to resolve local event directory")
|
|
}
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, apperr.New(http.StatusInternalServerError, "config_dir_unavailable", "failed to read local event directory")
|
|
}
|
|
|
|
files := make([]LocalEventFile, 0)
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
if strings.ToLower(filepath.Ext(entry.Name())) != ".json" {
|
|
continue
|
|
}
|
|
files = append(files, LocalEventFile{
|
|
FileName: entry.Name(),
|
|
FullPath: filepath.Join(dir, entry.Name()),
|
|
})
|
|
}
|
|
sort.Slice(files, func(i, j int) bool {
|
|
return files[i].FileName < files[j].FileName
|
|
})
|
|
return files, nil
|
|
}
|
|
|
|
func (s *ConfigService) ListEventConfigSources(ctx context.Context, eventPublicID string, limit int) ([]EventConfigSourceView, error) {
|
|
event, err := s.requireEvent(ctx, eventPublicID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items, err := s.store.ListEventConfigSourcesByEventID(ctx, event.ID, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results := make([]EventConfigSourceView, 0, len(items))
|
|
for i := range items {
|
|
view, err := buildEventConfigSourceView(&items[i], event.PublicID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results = append(results, *view)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (s *ConfigService) GetEventConfigSource(ctx context.Context, sourceID string) (*EventConfigSourceView, error) {
|
|
record, err := s.store.GetEventConfigSourceByID(ctx, strings.TrimSpace(sourceID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if record == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "config_source_not_found", "config source not found")
|
|
}
|
|
return buildEventConfigSourceView(record, "")
|
|
}
|
|
|
|
func (s *ConfigService) GetEventConfigBuild(ctx context.Context, buildID string) (*EventConfigBuildView, error) {
|
|
record, err := s.store.GetEventConfigBuildByID(ctx, strings.TrimSpace(buildID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if record == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "config_build_not_found", "config build not found")
|
|
}
|
|
return buildEventConfigBuildView(record)
|
|
}
|
|
|
|
func (s *ConfigService) ImportLocalEventConfig(ctx context.Context, input ImportLocalEventConfigInput) (*EventConfigSourceView, error) {
|
|
event, err := s.requireEvent(ctx, input.EventPublicID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fileName := strings.TrimSpace(filepath.Base(input.FileName))
|
|
if fileName == "" || strings.Contains(fileName, "..") || strings.ToLower(filepath.Ext(fileName)) != ".json" {
|
|
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "valid json fileName is required")
|
|
}
|
|
|
|
dir, err := filepath.Abs(s.localEventDir)
|
|
if err != nil {
|
|
return nil, apperr.New(http.StatusInternalServerError, "config_dir_invalid", "failed to resolve local event directory")
|
|
}
|
|
path := filepath.Join(dir, fileName)
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, apperr.New(http.StatusNotFound, "config_file_not_found", "local config file not found")
|
|
}
|
|
|
|
source := map[string]any{}
|
|
if err := json.Unmarshal(raw, &source); err != nil {
|
|
return nil, apperr.New(http.StatusBadRequest, "config_json_invalid", "local config file is not valid json")
|
|
}
|
|
if err := validateSourceConfig(source); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nextVersion, err := s.store.NextEventConfigSourceVersion(ctx, event.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
note := input.Notes
|
|
if note == nil || strings.TrimSpace(*note) == "" {
|
|
defaultNote := "imported from local event file: " + fileName
|
|
note = &defaultNote
|
|
}
|
|
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
record, err := s.store.UpsertEventConfigSource(ctx, tx, postgres.UpsertEventConfigSourceParams{
|
|
EventID: event.ID,
|
|
SourceVersionNo: nextVersion,
|
|
SourceKind: "event_bundle",
|
|
SchemaID: "event-source",
|
|
SchemaVersion: resolveSchemaVersion(source),
|
|
Status: "active",
|
|
Source: source,
|
|
Notes: note,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
return buildEventConfigSourceView(record, event.PublicID)
|
|
}
|
|
|
|
func (s *ConfigService) BuildPreview(ctx context.Context, input BuildPreviewInput) (*EventConfigBuildView, error) {
|
|
sourceRecord, err := s.store.GetEventConfigSourceByID(ctx, strings.TrimSpace(input.SourceID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if sourceRecord == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "config_source_not_found", "config source not found")
|
|
}
|
|
|
|
source, err := decodeJSONObject(sourceRecord.SourceJSON)
|
|
if err != nil {
|
|
return nil, apperr.New(http.StatusInternalServerError, "config_source_invalid", "stored source config is invalid")
|
|
}
|
|
if err := validateSourceConfig(source); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buildNo, err := s.store.NextEventConfigBuildNo(ctx, sourceRecord.EventID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
previewReleaseID := fmt.Sprintf("preview_%d", buildNo)
|
|
manifest := s.buildPreviewManifest(source, previewReleaseID)
|
|
assetIndex := s.buildAssetIndex(manifest)
|
|
buildLog := "preview build generated from source " + sourceRecord.ID
|
|
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
record, err := s.store.UpsertEventConfigBuild(ctx, tx, postgres.UpsertEventConfigBuildParams{
|
|
EventID: sourceRecord.EventID,
|
|
SourceID: sourceRecord.ID,
|
|
BuildNo: buildNo,
|
|
BuildStatus: "success",
|
|
BuildLog: &buildLog,
|
|
Manifest: manifest,
|
|
AssetIndex: assetIndex,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
return buildEventConfigBuildView(record)
|
|
}
|
|
|
|
func (s *ConfigService) PublishBuild(ctx context.Context, input PublishBuildInput) (*PublishedReleaseView, error) {
|
|
buildRecord, err := s.store.GetEventConfigBuildByID(ctx, strings.TrimSpace(input.BuildID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if buildRecord == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "config_build_not_found", "config build not found")
|
|
}
|
|
if buildRecord.BuildStatus != "success" {
|
|
return nil, apperr.New(http.StatusConflict, "config_build_not_publishable", "config build is not publishable")
|
|
}
|
|
|
|
event, err := s.store.GetEventByID(ctx, buildRecord.EventID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if event == nil {
|
|
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
|
}
|
|
|
|
manifest, err := decodeJSONObject(buildRecord.ManifestJSON)
|
|
if err != nil {
|
|
return nil, apperr.New(http.StatusInternalServerError, "config_build_invalid", "stored build manifest is invalid")
|
|
}
|
|
assetIndex, err := decodeJSONArray(buildRecord.AssetIndexJSON)
|
|
if err != nil {
|
|
return nil, apperr.New(http.StatusInternalServerError, "config_build_invalid", "stored build asset index is invalid")
|
|
}
|
|
|
|
releaseNo, err := s.store.NextEventReleaseNo(ctx, event.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
releasePublicID, err := security.GeneratePublicID("rel")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
configLabel := deriveConfigLabel(event, manifest, releaseNo)
|
|
manifestURL := fmt.Sprintf("%s/event/releases/%s/%s/manifest.json", s.assetBaseURL, event.PublicID, releasePublicID)
|
|
assetIndexURL := fmt.Sprintf("%s/event/releases/%s/%s/asset-index.json", s.assetBaseURL, event.PublicID, releasePublicID)
|
|
checksum := security.HashText(buildRecord.ManifestJSON)
|
|
routeCode := deriveRouteCode(manifest)
|
|
|
|
if s.publisher == nil || !s.publisher.Enabled() {
|
|
return nil, apperr.New(http.StatusInternalServerError, "asset_publish_unavailable", "asset publisher is not configured")
|
|
}
|
|
if err := s.publisher.UploadJSON(ctx, manifestURL, []byte(buildRecord.ManifestJSON)); err != nil {
|
|
return nil, apperr.New(http.StatusInternalServerError, "asset_publish_failed", "failed to upload manifest: "+err.Error())
|
|
}
|
|
if err := s.publisher.UploadJSON(ctx, assetIndexURL, []byte(buildRecord.AssetIndexJSON)); err != nil {
|
|
return nil, apperr.New(http.StatusInternalServerError, "asset_publish_failed", "failed to upload asset index: "+err.Error())
|
|
}
|
|
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
releaseRecord, err := s.store.CreateEventRelease(ctx, tx, postgres.CreateEventReleaseParams{
|
|
PublicID: releasePublicID,
|
|
EventID: event.ID,
|
|
ReleaseNo: releaseNo,
|
|
ConfigLabel: configLabel,
|
|
ManifestURL: manifestURL,
|
|
ManifestChecksum: &checksum,
|
|
RouteCode: routeCode,
|
|
BuildID: &buildRecord.ID,
|
|
Status: "published",
|
|
PayloadJSON: buildRecord.ManifestJSON,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.store.ReplaceEventReleaseAssets(ctx, tx, releaseRecord.ID, s.mapBuildAssetsToReleaseAssets(releaseRecord.ID, manifestURL, assetIndexURL, &checksum, assetIndex)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.store.SetCurrentEventRelease(ctx, tx, event.ID, releaseRecord.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &PublishedReleaseView{
|
|
EventID: event.PublicID,
|
|
Release: ResolvedReleaseView{
|
|
LaunchMode: LaunchModeManifestRelease,
|
|
Source: LaunchSourceEventCurrentRelease,
|
|
EventID: event.PublicID,
|
|
ReleaseID: releaseRecord.PublicID,
|
|
ConfigLabel: releaseRecord.ConfigLabel,
|
|
ManifestURL: releaseRecord.ManifestURL,
|
|
ManifestChecksumSha256: releaseRecord.ManifestChecksum,
|
|
RouteCode: releaseRecord.RouteCode,
|
|
},
|
|
ReleaseNo: releaseRecord.ReleaseNo,
|
|
PublishedAt: releaseRecord.PublishedAt.Format(timeRFC3339),
|
|
}, nil
|
|
}
|
|
|
|
func (s *ConfigService) requireEvent(ctx context.Context, eventPublicID string) (*postgres.Event, 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")
|
|
}
|
|
return event, nil
|
|
}
|
|
|
|
func buildEventConfigSourceView(record *postgres.EventConfigSource, eventPublicID string) (*EventConfigSourceView, error) {
|
|
source, err := decodeJSONObject(record.SourceJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
view := &EventConfigSourceView{
|
|
ID: record.ID,
|
|
EventID: eventPublicID,
|
|
SourceVersionNo: record.SourceVersionNo,
|
|
SourceKind: record.SourceKind,
|
|
SchemaID: record.SchemaID,
|
|
SchemaVersion: record.SchemaVersion,
|
|
Status: record.Status,
|
|
Notes: record.Notes,
|
|
Source: source,
|
|
}
|
|
return view, nil
|
|
}
|
|
|
|
func buildEventConfigBuildView(record *postgres.EventConfigBuild) (*EventConfigBuildView, error) {
|
|
manifest, err := decodeJSONObject(record.ManifestJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assetIndex, err := decodeJSONArray(record.AssetIndexJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &EventConfigBuildView{
|
|
ID: record.ID,
|
|
EventID: record.EventID,
|
|
SourceID: record.SourceID,
|
|
BuildNo: record.BuildNo,
|
|
BuildStatus: record.BuildStatus,
|
|
BuildLog: record.BuildLog,
|
|
Manifest: manifest,
|
|
AssetIndex: assetIndex,
|
|
}, nil
|
|
}
|
|
|
|
func validateSourceConfig(source map[string]any) error {
|
|
requiredMap := func(parent map[string]any, key string) (map[string]any, error) {
|
|
value, ok := parent[key]
|
|
if !ok {
|
|
return nil, apperr.New(http.StatusBadRequest, "config_missing_field", "missing required field: "+key)
|
|
}
|
|
asMap, ok := value.(map[string]any)
|
|
if !ok {
|
|
return nil, apperr.New(http.StatusBadRequest, "config_invalid_field", "invalid object field: "+key)
|
|
}
|
|
return asMap, nil
|
|
}
|
|
requiredString := func(parent map[string]any, key string) error {
|
|
value, ok := parent[key]
|
|
if !ok {
|
|
return apperr.New(http.StatusBadRequest, "config_missing_field", "missing required field: "+key)
|
|
}
|
|
text, ok := value.(string)
|
|
if !ok || strings.TrimSpace(text) == "" {
|
|
return apperr.New(http.StatusBadRequest, "config_invalid_field", "invalid string field: "+key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if err := requiredString(source, "schemaVersion"); err != nil {
|
|
return err
|
|
}
|
|
app, err := requiredMap(source, "app")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := requiredString(app, "id"); err != nil {
|
|
return err
|
|
}
|
|
if err := requiredString(app, "title"); err != nil {
|
|
return err
|
|
}
|
|
m, err := requiredMap(source, "map")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := requiredString(m, "tiles"); err != nil {
|
|
return err
|
|
}
|
|
if err := requiredString(m, "mapmeta"); err != nil {
|
|
return err
|
|
}
|
|
playfield, err := requiredMap(source, "playfield")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := requiredString(playfield, "kind"); err != nil {
|
|
return err
|
|
}
|
|
playfieldSource, err := requiredMap(playfield, "source")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := requiredString(playfieldSource, "type"); err != nil {
|
|
return err
|
|
}
|
|
if err := requiredString(playfieldSource, "url"); err != nil {
|
|
return err
|
|
}
|
|
game, err := requiredMap(source, "game")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := requiredString(game, "mode"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resolveSchemaVersion(source map[string]any) string {
|
|
if value, ok := source["schemaVersion"].(string); ok && strings.TrimSpace(value) != "" {
|
|
return value
|
|
}
|
|
return "1"
|
|
}
|
|
|
|
func (s *ConfigService) buildPreviewManifest(source map[string]any, previewReleaseID string) map[string]any {
|
|
manifest := cloneJSONObject(source)
|
|
manifest["releaseId"] = previewReleaseID
|
|
manifest["preview"] = true
|
|
manifest["assetBaseUrl"] = s.assetBaseURL
|
|
if version, ok := manifest["version"]; !ok || version == "" {
|
|
manifest["version"] = "preview"
|
|
}
|
|
|
|
if m, ok := manifest["map"].(map[string]any); ok {
|
|
if tiles, ok := m["tiles"].(string); ok {
|
|
m["tiles"] = s.normalizeAssetURL(tiles)
|
|
}
|
|
if meta, ok := m["mapmeta"].(string); ok {
|
|
m["mapmeta"] = s.normalizeAssetURL(meta)
|
|
}
|
|
}
|
|
if playfield, ok := manifest["playfield"].(map[string]any); ok {
|
|
if src, ok := playfield["source"].(map[string]any); ok {
|
|
if url, ok := src["url"].(string); ok {
|
|
src["url"] = s.normalizeAssetURL(url)
|
|
}
|
|
}
|
|
}
|
|
if assets, ok := manifest["assets"].(map[string]any); ok {
|
|
for key, value := range assets {
|
|
if text, ok := value.(string); ok {
|
|
assets[key] = s.normalizeAssetURL(text)
|
|
}
|
|
}
|
|
}
|
|
|
|
return manifest
|
|
}
|
|
|
|
func (s *ConfigService) buildAssetIndex(manifest map[string]any) []map[string]any {
|
|
var assets []map[string]any
|
|
if m, ok := manifest["map"].(map[string]any); ok {
|
|
if tiles, ok := m["tiles"].(string); ok {
|
|
assets = append(assets, map[string]any{"assetType": "tiles", "assetKey": "tiles-root", "assetUrl": tiles})
|
|
}
|
|
if meta, ok := m["mapmeta"].(string); ok {
|
|
assets = append(assets, map[string]any{"assetType": "mapmeta", "assetKey": "mapmeta", "assetUrl": meta})
|
|
}
|
|
}
|
|
if playfield, ok := manifest["playfield"].(map[string]any); ok {
|
|
if src, ok := playfield["source"].(map[string]any); ok {
|
|
if url, ok := src["url"].(string); ok {
|
|
assets = append(assets, map[string]any{"assetType": "playfield", "assetKey": "playfield-source", "assetUrl": url})
|
|
}
|
|
}
|
|
}
|
|
if rawAssets, ok := manifest["assets"].(map[string]any); ok {
|
|
keys := make([]string, 0, len(rawAssets))
|
|
for key := range rawAssets {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
if url, ok := rawAssets[key].(string); ok {
|
|
assets = append(assets, map[string]any{"assetType": "other", "assetKey": key, "assetUrl": url})
|
|
}
|
|
}
|
|
}
|
|
return assets
|
|
}
|
|
|
|
func (s *ConfigService) normalizeAssetURL(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return value
|
|
}
|
|
if strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") {
|
|
return value
|
|
}
|
|
trimmed := strings.TrimPrefix(value, "../")
|
|
trimmed = strings.TrimPrefix(trimmed, "./")
|
|
trimmed = strings.TrimLeft(trimmed, "/")
|
|
return s.assetBaseURL + "/" + trimmed
|
|
}
|
|
|
|
func cloneJSONObject(source map[string]any) map[string]any {
|
|
raw, _ := json.Marshal(source)
|
|
cloned := map[string]any{}
|
|
_ = json.Unmarshal(raw, &cloned)
|
|
return cloned
|
|
}
|
|
|
|
func decodeJSONObject(raw string) (map[string]any, error) {
|
|
result := map[string]any{}
|
|
if err := json.Unmarshal([]byte(raw), &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func decodeJSONArray(raw string) ([]map[string]any, error) {
|
|
if strings.TrimSpace(raw) == "" {
|
|
return []map[string]any{}, nil
|
|
}
|
|
var result []map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func deriveConfigLabel(event *postgres.Event, manifest map[string]any, releaseNo int) string {
|
|
if app, ok := manifest["app"].(map[string]any); ok {
|
|
if title, ok := app["title"].(string); ok && strings.TrimSpace(title) != "" {
|
|
return fmt.Sprintf("%s Release %d", strings.TrimSpace(title), releaseNo)
|
|
}
|
|
}
|
|
if event != nil && strings.TrimSpace(event.DisplayName) != "" {
|
|
return fmt.Sprintf("%s Release %d", event.DisplayName, releaseNo)
|
|
}
|
|
return fmt.Sprintf("Release %d", releaseNo)
|
|
}
|
|
|
|
func deriveRouteCode(manifest map[string]any) *string {
|
|
if playfield, ok := manifest["playfield"].(map[string]any); ok {
|
|
if value, ok := playfield["kind"].(string); ok && strings.TrimSpace(value) != "" {
|
|
route := strings.TrimSpace(value)
|
|
return &route
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ConfigService) mapBuildAssetsToReleaseAssets(eventReleaseID, manifestURL, assetIndexURL string, checksum *string, assetIndex []map[string]any) []postgres.UpsertEventReleaseAssetParams {
|
|
assets := []postgres.UpsertEventReleaseAssetParams{
|
|
{
|
|
EventReleaseID: eventReleaseID,
|
|
AssetType: "manifest",
|
|
AssetKey: "manifest",
|
|
AssetURL: manifestURL,
|
|
Checksum: checksum,
|
|
Meta: map[string]any{"source": "published-build"},
|
|
},
|
|
{
|
|
EventReleaseID: eventReleaseID,
|
|
AssetType: "other",
|
|
AssetKey: "asset-index",
|
|
AssetURL: assetIndexURL,
|
|
Meta: map[string]any{"source": "published-build"},
|
|
},
|
|
}
|
|
|
|
for _, asset := range assetIndex {
|
|
assetType, _ := asset["assetType"].(string)
|
|
assetKey, _ := asset["assetKey"].(string)
|
|
assetURL, _ := asset["assetUrl"].(string)
|
|
if strings.TrimSpace(assetType) == "" || strings.TrimSpace(assetKey) == "" || strings.TrimSpace(assetURL) == "" {
|
|
continue
|
|
}
|
|
mappedType := assetType
|
|
if mappedType != "manifest" && mappedType != "mapmeta" && mappedType != "tiles" && mappedType != "playfield" && mappedType != "content_html" && mappedType != "media" {
|
|
mappedType = "other"
|
|
}
|
|
assets = append(assets, postgres.UpsertEventReleaseAssetParams{
|
|
EventReleaseID: eventReleaseID,
|
|
AssetType: mappedType,
|
|
AssetKey: assetKey,
|
|
AssetURL: assetURL,
|
|
Meta: asset,
|
|
})
|
|
}
|
|
|
|
return assets
|
|
}
|