完善活动运营域与联调标准化
This commit is contained in:
@@ -38,6 +38,7 @@ func New(ctx context.Context, cfg Config) (*App, error) {
|
||||
entryService := service.NewEntryService(store)
|
||||
entryHomeService := service.NewEntryHomeService(store)
|
||||
adminResourceService := service.NewAdminResourceService(store)
|
||||
adminProductionService := service.NewAdminProductionService(store)
|
||||
adminEventService := service.NewAdminEventService(store)
|
||||
eventService := service.NewEventService(store)
|
||||
eventPlayService := service.NewEventPlayService(store)
|
||||
@@ -50,7 +51,7 @@ func New(ctx context.Context, cfg Config) (*App, error) {
|
||||
sessionService := service.NewSessionService(store)
|
||||
devService := service.NewDevService(cfg.AppEnv, store)
|
||||
meService := service.NewMeService(store)
|
||||
router := httpapi.NewRouter(cfg.AppEnv, jwtManager, authService, entryService, entryHomeService, adminResourceService, adminEventService, adminPipelineService, eventService, eventPlayService, configService, homeService, profileService, resultService, sessionService, devService, meService)
|
||||
router := httpapi.NewRouter(cfg.AppEnv, jwtManager, authService, entryService, entryHomeService, adminResourceService, adminProductionService, adminEventService, adminPipelineService, eventService, eventPlayService, configService, homeService, profileService, resultService, sessionService, devService, meService)
|
||||
|
||||
return &App{
|
||||
router: router,
|
||||
|
||||
@@ -82,3 +82,121 @@ func (h *AdminEventHandler) SaveSource(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminEventHandler) ListPresentations(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 50
|
||||
if raw := r.URL.Query().Get("limit"); raw != "" {
|
||||
if parsed, err := strconv.Atoi(raw); err == nil {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
result, err := h.service.ListEventPresentations(r.Context(), r.PathValue("eventPublicID"), limit)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminEventHandler) CreatePresentation(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CreateAdminEventPresentationInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.CreateEventPresentation(r.Context(), r.PathValue("eventPublicID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminEventHandler) ImportPresentation(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.ImportAdminEventPresentationInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.ImportEventPresentation(r.Context(), r.PathValue("eventPublicID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminEventHandler) GetPresentation(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetEventPresentation(r.Context(), r.PathValue("presentationPublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminEventHandler) ListContentBundles(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 50
|
||||
if raw := r.URL.Query().Get("limit"); raw != "" {
|
||||
if parsed, err := strconv.Atoi(raw); err == nil {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
result, err := h.service.ListContentBundles(r.Context(), r.PathValue("eventPublicID"), limit)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminEventHandler) CreateContentBundle(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CreateAdminContentBundleInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.CreateContentBundle(r.Context(), r.PathValue("eventPublicID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminEventHandler) ImportContentBundle(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.ImportAdminContentBundleInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.ImportContentBundle(r.Context(), r.PathValue("eventPublicID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminEventHandler) GetContentBundle(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetContentBundle(r.Context(), r.PathValue("contentBundlePublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminEventHandler) UpdateEventDefaults(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.UpdateAdminEventDefaultsInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.UpdateEventDefaults(r.Context(), r.PathValue("eventPublicID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
"cmr-backend/internal/httpx"
|
||||
@@ -51,7 +53,46 @@ func (h *AdminPipelineHandler) GetBuild(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *AdminPipelineHandler) PublishBuild(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.PublishBuild(r.Context(), r.PathValue("buildID"))
|
||||
var req service.AdminPublishBuildInput
|
||||
if r.Body != nil {
|
||||
raw, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "failed to read request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
if len(raw) > 0 {
|
||||
r.Body = io.NopCloser(strings.NewReader(string(raw)))
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.service.PublishBuild(r.Context(), r.PathValue("buildID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminPipelineHandler) GetRelease(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetRelease(r.Context(), r.PathValue("releasePublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminPipelineHandler) BindReleaseRuntime(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.AdminBindReleaseRuntimeInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.BindReleaseRuntime(r.Context(), r.PathValue("releasePublicID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
|
||||
187
backend/internal/httpapi/handlers/admin_production_handler.go
Normal file
187
backend/internal/httpapi/handlers/admin_production_handler.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
"cmr-backend/internal/httpx"
|
||||
"cmr-backend/internal/service"
|
||||
)
|
||||
|
||||
type AdminProductionHandler struct {
|
||||
service *service.AdminProductionService
|
||||
}
|
||||
|
||||
func NewAdminProductionHandler(service *service.AdminProductionService) *AdminProductionHandler {
|
||||
return &AdminProductionHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) ListPlaces(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.ListPlaces(r.Context(), parseAdminLimit(r))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) CreatePlace(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CreateAdminPlaceInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.CreatePlace(r.Context(), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) GetPlace(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetPlaceDetail(r.Context(), r.PathValue("placePublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) CreateMapAsset(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CreateAdminMapAssetInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.CreateMapAsset(r.Context(), r.PathValue("placePublicID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) GetMapAsset(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetMapAssetDetail(r.Context(), r.PathValue("mapAssetPublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) CreateTileRelease(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CreateAdminTileReleaseInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.CreateTileRelease(r.Context(), r.PathValue("mapAssetPublicID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) ListCourseSources(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.ListCourseSources(r.Context(), parseAdminLimit(r))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) CreateCourseSource(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CreateAdminCourseSourceInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.CreateCourseSource(r.Context(), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) GetCourseSource(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetCourseSource(r.Context(), r.PathValue("sourcePublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) CreateCourseSet(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CreateAdminCourseSetInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.CreateCourseSet(r.Context(), r.PathValue("mapAssetPublicID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) GetCourseSet(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetCourseSetDetail(r.Context(), r.PathValue("courseSetPublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) CreateCourseVariant(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CreateAdminCourseVariantInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.CreateCourseVariant(r.Context(), r.PathValue("courseSetPublicID"), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) ListRuntimeBindings(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.ListRuntimeBindings(r.Context(), parseAdminLimit(r))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) CreateRuntimeBinding(w http.ResponseWriter, r *http.Request) {
|
||||
var req service.CreateAdminRuntimeBindingInput
|
||||
if err := httpx.DecodeJSON(r, &req); err != nil {
|
||||
httpx.WriteError(w, apperr.New(http.StatusBadRequest, "invalid_json", "invalid request body: "+err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := h.service.CreateRuntimeBinding(r.Context(), req)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"data": result})
|
||||
}
|
||||
|
||||
func (h *AdminProductionHandler) GetRuntimeBinding(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.service.GetRuntimeBinding(r.Context(), r.PathValue("runtimeBindingPublicID"))
|
||||
if err != nil {
|
||||
httpx.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ func NewRouter(
|
||||
entryService *service.EntryService,
|
||||
entryHomeService *service.EntryHomeService,
|
||||
adminResourceService *service.AdminResourceService,
|
||||
adminProductionService *service.AdminProductionService,
|
||||
adminEventService *service.AdminEventService,
|
||||
adminPipelineService *service.AdminPipelineService,
|
||||
eventService *service.EventService,
|
||||
@@ -35,6 +36,7 @@ func NewRouter(
|
||||
entryHandler := handlers.NewEntryHandler(entryService)
|
||||
entryHomeHandler := handlers.NewEntryHomeHandler(entryHomeService)
|
||||
adminResourceHandler := handlers.NewAdminResourceHandler(adminResourceService)
|
||||
adminProductionHandler := handlers.NewAdminProductionHandler(adminProductionService)
|
||||
adminEventHandler := handlers.NewAdminEventHandler(adminEventService)
|
||||
adminPipelineHandler := handlers.NewAdminPipelineHandler(adminPipelineService)
|
||||
eventHandler := handlers.NewEventHandler(eventService)
|
||||
@@ -56,6 +58,21 @@ func NewRouter(
|
||||
mux.Handle("POST /admin/maps", authMiddleware(http.HandlerFunc(adminResourceHandler.CreateMap)))
|
||||
mux.Handle("GET /admin/maps/{mapPublicID}", authMiddleware(http.HandlerFunc(adminResourceHandler.GetMap)))
|
||||
mux.Handle("POST /admin/maps/{mapPublicID}/versions", authMiddleware(http.HandlerFunc(adminResourceHandler.CreateMapVersion)))
|
||||
mux.Handle("GET /admin/places", authMiddleware(http.HandlerFunc(adminProductionHandler.ListPlaces)))
|
||||
mux.Handle("POST /admin/places", authMiddleware(http.HandlerFunc(adminProductionHandler.CreatePlace)))
|
||||
mux.Handle("GET /admin/places/{placePublicID}", authMiddleware(http.HandlerFunc(adminProductionHandler.GetPlace)))
|
||||
mux.Handle("POST /admin/places/{placePublicID}/map-assets", authMiddleware(http.HandlerFunc(adminProductionHandler.CreateMapAsset)))
|
||||
mux.Handle("GET /admin/map-assets/{mapAssetPublicID}", authMiddleware(http.HandlerFunc(adminProductionHandler.GetMapAsset)))
|
||||
mux.Handle("POST /admin/map-assets/{mapAssetPublicID}/tile-releases", authMiddleware(http.HandlerFunc(adminProductionHandler.CreateTileRelease)))
|
||||
mux.Handle("POST /admin/map-assets/{mapAssetPublicID}/course-sets", authMiddleware(http.HandlerFunc(adminProductionHandler.CreateCourseSet)))
|
||||
mux.Handle("GET /admin/course-sources", authMiddleware(http.HandlerFunc(adminProductionHandler.ListCourseSources)))
|
||||
mux.Handle("POST /admin/course-sources", authMiddleware(http.HandlerFunc(adminProductionHandler.CreateCourseSource)))
|
||||
mux.Handle("GET /admin/course-sources/{sourcePublicID}", authMiddleware(http.HandlerFunc(adminProductionHandler.GetCourseSource)))
|
||||
mux.Handle("GET /admin/course-sets/{courseSetPublicID}", authMiddleware(http.HandlerFunc(adminProductionHandler.GetCourseSet)))
|
||||
mux.Handle("POST /admin/course-sets/{courseSetPublicID}/variants", authMiddleware(http.HandlerFunc(adminProductionHandler.CreateCourseVariant)))
|
||||
mux.Handle("GET /admin/runtime-bindings", authMiddleware(http.HandlerFunc(adminProductionHandler.ListRuntimeBindings)))
|
||||
mux.Handle("POST /admin/runtime-bindings", authMiddleware(http.HandlerFunc(adminProductionHandler.CreateRuntimeBinding)))
|
||||
mux.Handle("GET /admin/runtime-bindings/{runtimeBindingPublicID}", authMiddleware(http.HandlerFunc(adminProductionHandler.GetRuntimeBinding)))
|
||||
mux.Handle("GET /admin/playfields", authMiddleware(http.HandlerFunc(adminResourceHandler.ListPlayfields)))
|
||||
mux.Handle("POST /admin/playfields", authMiddleware(http.HandlerFunc(adminResourceHandler.CreatePlayfield)))
|
||||
mux.Handle("GET /admin/playfields/{playfieldPublicID}", authMiddleware(http.HandlerFunc(adminResourceHandler.GetPlayfield)))
|
||||
@@ -69,10 +86,21 @@ func NewRouter(
|
||||
mux.Handle("GET /admin/events/{eventPublicID}", authMiddleware(http.HandlerFunc(adminEventHandler.GetEvent)))
|
||||
mux.Handle("PUT /admin/events/{eventPublicID}", authMiddleware(http.HandlerFunc(adminEventHandler.UpdateEvent)))
|
||||
mux.Handle("POST /admin/events/{eventPublicID}/source", authMiddleware(http.HandlerFunc(adminEventHandler.SaveSource)))
|
||||
mux.Handle("GET /admin/events/{eventPublicID}/presentations", authMiddleware(http.HandlerFunc(adminEventHandler.ListPresentations)))
|
||||
mux.Handle("POST /admin/events/{eventPublicID}/presentations", authMiddleware(http.HandlerFunc(adminEventHandler.CreatePresentation)))
|
||||
mux.Handle("POST /admin/events/{eventPublicID}/presentations/import", authMiddleware(http.HandlerFunc(adminEventHandler.ImportPresentation)))
|
||||
mux.Handle("GET /admin/presentations/{presentationPublicID}", authMiddleware(http.HandlerFunc(adminEventHandler.GetPresentation)))
|
||||
mux.Handle("GET /admin/events/{eventPublicID}/content-bundles", authMiddleware(http.HandlerFunc(adminEventHandler.ListContentBundles)))
|
||||
mux.Handle("POST /admin/events/{eventPublicID}/content-bundles", authMiddleware(http.HandlerFunc(adminEventHandler.CreateContentBundle)))
|
||||
mux.Handle("POST /admin/events/{eventPublicID}/content-bundles/import", authMiddleware(http.HandlerFunc(adminEventHandler.ImportContentBundle)))
|
||||
mux.Handle("GET /admin/content-bundles/{contentBundlePublicID}", authMiddleware(http.HandlerFunc(adminEventHandler.GetContentBundle)))
|
||||
mux.Handle("POST /admin/events/{eventPublicID}/defaults", authMiddleware(http.HandlerFunc(adminEventHandler.UpdateEventDefaults)))
|
||||
mux.Handle("GET /admin/events/{eventPublicID}/pipeline", authMiddleware(http.HandlerFunc(adminPipelineHandler.GetEventPipeline)))
|
||||
mux.Handle("POST /admin/sources/{sourceID}/build", authMiddleware(http.HandlerFunc(adminPipelineHandler.BuildSource)))
|
||||
mux.Handle("GET /admin/builds/{buildID}", authMiddleware(http.HandlerFunc(adminPipelineHandler.GetBuild)))
|
||||
mux.Handle("POST /admin/builds/{buildID}/publish", authMiddleware(http.HandlerFunc(adminPipelineHandler.PublishBuild)))
|
||||
mux.Handle("GET /admin/releases/{releasePublicID}", authMiddleware(http.HandlerFunc(adminPipelineHandler.GetRelease)))
|
||||
mux.Handle("POST /admin/releases/{releasePublicID}/runtime-binding", authMiddleware(http.HandlerFunc(adminPipelineHandler.BindReleaseRuntime)))
|
||||
mux.Handle("POST /admin/events/{eventPublicID}/rollback", authMiddleware(http.HandlerFunc(adminPipelineHandler.RollbackRelease)))
|
||||
if appEnv != "production" {
|
||||
mux.HandleFunc("GET /dev/workbench", devHandler.Workbench)
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -27,18 +28,25 @@ type AdminEventSummary struct {
|
||||
}
|
||||
|
||||
type AdminEventReleaseRef struct {
|
||||
ID string `json:"id"`
|
||||
ConfigLabel *string `json:"configLabel,omitempty"`
|
||||
ManifestURL *string `json:"manifestUrl,omitempty"`
|
||||
ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
ID string `json:"id"`
|
||||
ConfigLabel *string `json:"configLabel,omitempty"`
|
||||
ManifestURL *string `json:"manifestUrl,omitempty"`
|
||||
ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
Presentation *PresentationSummaryView `json:"presentation,omitempty"`
|
||||
ContentBundle *ContentBundleSummaryView `json:"contentBundle,omitempty"`
|
||||
}
|
||||
|
||||
type AdminEventDetail struct {
|
||||
Event AdminEventSummary `json:"event"`
|
||||
LatestSource *EventConfigSourceView `json:"latestSource,omitempty"`
|
||||
SourceCount int `json:"sourceCount"`
|
||||
CurrentSource *AdminAssembledSource `json:"currentSource,omitempty"`
|
||||
Event AdminEventSummary `json:"event"`
|
||||
LatestSource *EventConfigSourceView `json:"latestSource,omitempty"`
|
||||
SourceCount int `json:"sourceCount"`
|
||||
CurrentSource *AdminAssembledSource `json:"currentSource,omitempty"`
|
||||
PresentationCount int `json:"presentationCount"`
|
||||
ContentBundleCount int `json:"contentBundleCount"`
|
||||
CurrentPresentation *PresentationSummaryView `json:"currentPresentation,omitempty"`
|
||||
CurrentContentBundle *ContentBundleSummaryView `json:"currentContentBundle,omitempty"`
|
||||
CurrentRuntime *RuntimeSummaryView `json:"currentRuntime,omitempty"`
|
||||
}
|
||||
|
||||
type CreateAdminEventInput struct {
|
||||
@@ -76,6 +84,84 @@ type SaveAdminEventSourceInput struct {
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
type AdminEventPresentationView struct {
|
||||
ID string `json:"id"`
|
||||
EventID string `json:"eventId"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
PresentationType string `json:"presentationType"`
|
||||
Status string `json:"status"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
TemplateKey *string `json:"templateKey,omitempty"`
|
||||
Version *string `json:"version,omitempty"`
|
||||
SourceType *string `json:"sourceType,omitempty"`
|
||||
SchemaURL *string `json:"schemaUrl,omitempty"`
|
||||
Schema map[string]any `json:"schema"`
|
||||
}
|
||||
|
||||
type CreateAdminEventPresentationInput struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
PresentationType string `json:"presentationType"`
|
||||
Status string `json:"status"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Schema map[string]any `json:"schema,omitempty"`
|
||||
}
|
||||
|
||||
type ImportAdminEventPresentationInput struct {
|
||||
Title string `json:"title"`
|
||||
TemplateKey string `json:"templateKey"`
|
||||
SourceType string `json:"sourceType"`
|
||||
SchemaURL string `json:"schemaUrl"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
}
|
||||
|
||||
type AdminContentBundleView struct {
|
||||
ID string `json:"id"`
|
||||
EventID string `json:"eventId"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
BundleType *string `json:"bundleType,omitempty"`
|
||||
Version *string `json:"version,omitempty"`
|
||||
SourceType *string `json:"sourceType,omitempty"`
|
||||
ManifestURL *string `json:"manifestUrl,omitempty"`
|
||||
AssetManifest any `json:"assetManifest,omitempty"`
|
||||
EntryURL *string `json:"entryUrl,omitempty"`
|
||||
AssetRootURL *string `json:"assetRootUrl,omitempty"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
type CreateAdminContentBundleInput struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
EntryURL *string `json:"entryUrl,omitempty"`
|
||||
AssetRootURL *string `json:"assetRootUrl,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type ImportAdminContentBundleInput struct {
|
||||
Title string `json:"title"`
|
||||
BundleType string `json:"bundleType"`
|
||||
SourceType string `json:"sourceType"`
|
||||
ManifestURL string `json:"manifestUrl"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
AssetManifest map[string]any `json:"assetManifest,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateAdminEventDefaultsInput struct {
|
||||
PresentationID *string `json:"presentationId,omitempty"`
|
||||
ContentBundleID *string `json:"contentBundleId,omitempty"`
|
||||
RuntimeBindingID *string `json:"runtimeBindingId,omitempty"`
|
||||
}
|
||||
|
||||
type AdminAssembledSource struct {
|
||||
Refs map[string]any `json:"refs"`
|
||||
Runtime map[string]any `json:"runtime"`
|
||||
@@ -240,10 +326,20 @@ func (s *AdminEventService) GetEventDetail(ctx context.Context, eventPublicID st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
presentations, err := s.store.ListEventPresentationsByEventID(ctx, record.ID, 200)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentBundles, err := s.store.ListContentBundlesByEventID(ctx, record.ID, 200)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &AdminEventDetail{
|
||||
Event: buildAdminEventSummary(*record),
|
||||
SourceCount: len(allSources),
|
||||
Event: buildAdminEventSummary(*record),
|
||||
SourceCount: len(allSources),
|
||||
PresentationCount: len(presentations),
|
||||
ContentBundleCount: len(contentBundles),
|
||||
}
|
||||
if len(sources) > 0 {
|
||||
latest, err := buildEventConfigSourceView(&sources[0], record.PublicID)
|
||||
@@ -253,9 +349,427 @@ func (s *AdminEventService) GetEventDetail(ctx context.Context, eventPublicID st
|
||||
result.LatestSource = latest
|
||||
result.CurrentSource = buildAdminAssembledSource(latest.Source)
|
||||
}
|
||||
result.CurrentPresentation = buildPresentationSummaryFromEventRecord(record)
|
||||
result.CurrentContentBundle = buildContentBundleSummaryFromEventRecord(record)
|
||||
result.CurrentRuntime = buildRuntimeSummaryFromAdminEventRecord(record)
|
||||
if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, record.CurrentPresentationID); err != nil {
|
||||
return nil, err
|
||||
} else if enrichedPresentation != nil {
|
||||
result.CurrentPresentation = enrichedPresentation
|
||||
}
|
||||
if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, record.CurrentContentBundleID); err != nil {
|
||||
return nil, err
|
||||
} else if enrichedBundle != nil {
|
||||
result.CurrentContentBundle = enrichedBundle
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AdminEventService) ListEventPresentations(ctx context.Context, eventPublicID string, limit int) ([]AdminEventPresentationView, error) {
|
||||
eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if eventRecord == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
items, err := s.store.ListEventPresentationsByEventID(ctx, eventRecord.ID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]AdminEventPresentationView, 0, len(items))
|
||||
for _, item := range items {
|
||||
view, err := buildAdminEventPresentationView(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, view)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AdminEventService) CreateEventPresentation(ctx context.Context, eventPublicID string, input CreateAdminEventPresentationInput) (*AdminEventPresentationView, error) {
|
||||
eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if eventRecord == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
input.Code = strings.TrimSpace(input.Code)
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
input.PresentationType = normalizePresentationType(input.PresentationType)
|
||||
if input.Code == "" || input.Name == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
|
||||
}
|
||||
publicID, err := security.GeneratePublicID("pres")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
schema := input.Schema
|
||||
if schema == nil {
|
||||
schema = map[string]any{}
|
||||
}
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
record, err := s.store.CreateEventPresentation(ctx, tx, postgres.CreateEventPresentationParams{
|
||||
PublicID: publicID,
|
||||
EventID: eventRecord.ID,
|
||||
Code: input.Code,
|
||||
Name: input.Name,
|
||||
PresentationType: input.PresentationType,
|
||||
Status: normalizeEventCatalogStatus(input.Status),
|
||||
IsDefault: input.IsDefault,
|
||||
SchemaJSON: mustMarshalJSONObject(schema),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, err := buildAdminEventPresentationView(*record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
func (s *AdminEventService) ImportEventPresentation(ctx context.Context, eventPublicID string, input ImportAdminEventPresentationInput) (*AdminEventPresentationView, error) {
|
||||
eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if eventRecord == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
|
||||
input.Title = strings.TrimSpace(input.Title)
|
||||
input.TemplateKey = strings.TrimSpace(input.TemplateKey)
|
||||
input.SourceType = strings.TrimSpace(input.SourceType)
|
||||
input.SchemaURL = strings.TrimSpace(input.SchemaURL)
|
||||
input.Version = strings.TrimSpace(input.Version)
|
||||
if input.Title == "" || input.TemplateKey == "" || input.SourceType == "" || input.SchemaURL == "" || input.Version == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "title, templateKey, sourceType, schemaUrl and version are required")
|
||||
}
|
||||
|
||||
publicID, err := security.GeneratePublicID("pres")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code := generateImportedPresentationCode(input.Title, publicID)
|
||||
status := normalizeEventCatalogStatus(input.Status)
|
||||
if strings.TrimSpace(input.Status) == "" {
|
||||
status = "active"
|
||||
}
|
||||
schema := map[string]any{
|
||||
"templateKey": input.TemplateKey,
|
||||
"sourceType": input.SourceType,
|
||||
"schemaUrl": input.SchemaURL,
|
||||
"version": input.Version,
|
||||
}
|
||||
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
record, err := s.store.CreateEventPresentation(ctx, tx, postgres.CreateEventPresentationParams{
|
||||
PublicID: publicID,
|
||||
EventID: eventRecord.ID,
|
||||
Code: code,
|
||||
Name: input.Title,
|
||||
PresentationType: "generic",
|
||||
Status: status,
|
||||
IsDefault: input.IsDefault,
|
||||
SchemaJSON: mustMarshalJSONObject(schema),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, err := buildAdminEventPresentationView(*record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
func (s *AdminEventService) GetEventPresentation(ctx context.Context, presentationPublicID string) (*AdminEventPresentationView, error) {
|
||||
record, err := s.store.GetEventPresentationByPublicID(ctx, strings.TrimSpace(presentationPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "presentation_not_found", "presentation not found")
|
||||
}
|
||||
view, err := buildAdminEventPresentationView(*record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
func (s *AdminEventService) ListContentBundles(ctx context.Context, eventPublicID string, limit int) ([]AdminContentBundleView, error) {
|
||||
eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if eventRecord == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
items, err := s.store.ListContentBundlesByEventID(ctx, eventRecord.ID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]AdminContentBundleView, 0, len(items))
|
||||
for _, item := range items {
|
||||
view, err := buildAdminContentBundleView(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, view)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AdminEventService) CreateContentBundle(ctx context.Context, eventPublicID string, input CreateAdminContentBundleInput) (*AdminContentBundleView, error) {
|
||||
eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if eventRecord == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
input.Code = strings.TrimSpace(input.Code)
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
if input.Code == "" || input.Name == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
|
||||
}
|
||||
publicID, err := security.GeneratePublicID("bundle")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata := input.Metadata
|
||||
if metadata == nil {
|
||||
metadata = map[string]any{}
|
||||
}
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
record, err := s.store.CreateContentBundle(ctx, tx, postgres.CreateContentBundleParams{
|
||||
PublicID: publicID,
|
||||
EventID: eventRecord.ID,
|
||||
Code: input.Code,
|
||||
Name: input.Name,
|
||||
Status: normalizeEventCatalogStatus(input.Status),
|
||||
IsDefault: input.IsDefault,
|
||||
EntryURL: trimStringPtr(input.EntryURL),
|
||||
AssetRootURL: trimStringPtr(input.AssetRootURL),
|
||||
MetadataJSON: mustMarshalJSONObject(metadata),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, err := buildAdminContentBundleView(*record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
func (s *AdminEventService) ImportContentBundle(ctx context.Context, eventPublicID string, input ImportAdminContentBundleInput) (*AdminContentBundleView, error) {
|
||||
eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if eventRecord == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
|
||||
input.Title = strings.TrimSpace(input.Title)
|
||||
input.BundleType = strings.TrimSpace(input.BundleType)
|
||||
input.SourceType = strings.TrimSpace(input.SourceType)
|
||||
input.ManifestURL = strings.TrimSpace(input.ManifestURL)
|
||||
input.Version = strings.TrimSpace(input.Version)
|
||||
if input.Title == "" || input.BundleType == "" || input.SourceType == "" || input.ManifestURL == "" || input.Version == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "title, bundleType, sourceType, manifestUrl and version are required")
|
||||
}
|
||||
|
||||
publicID, err := security.GeneratePublicID("bundle")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code := generateImportedBundleCode(input.Title, publicID)
|
||||
assetManifest := input.AssetManifest
|
||||
if assetManifest == nil {
|
||||
assetManifest = map[string]any{
|
||||
"manifestUrl": input.ManifestURL,
|
||||
"sourceType": input.SourceType,
|
||||
}
|
||||
}
|
||||
metadata := map[string]any{
|
||||
"bundleType": input.BundleType,
|
||||
"sourceType": input.SourceType,
|
||||
"manifestUrl": input.ManifestURL,
|
||||
"version": input.Version,
|
||||
"assetManifest": assetManifest,
|
||||
}
|
||||
status := normalizeEventCatalogStatus(input.Status)
|
||||
if strings.TrimSpace(input.Status) == "" {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
record, err := s.store.CreateContentBundle(ctx, tx, postgres.CreateContentBundleParams{
|
||||
PublicID: publicID,
|
||||
EventID: eventRecord.ID,
|
||||
Code: code,
|
||||
Name: input.Title,
|
||||
Status: status,
|
||||
IsDefault: input.IsDefault,
|
||||
EntryURL: nil,
|
||||
AssetRootURL: nil,
|
||||
MetadataJSON: mustMarshalJSONObject(metadata),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
view, err := buildAdminContentBundleView(*record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
func (s *AdminEventService) GetContentBundle(ctx context.Context, contentBundlePublicID string) (*AdminContentBundleView, error) {
|
||||
record, err := s.store.GetContentBundleByPublicID(ctx, strings.TrimSpace(contentBundlePublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "content_bundle_not_found", "content bundle not found")
|
||||
}
|
||||
view, err := buildAdminContentBundleView(*record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
func (s *AdminEventService) UpdateEventDefaults(ctx context.Context, eventPublicID string, input UpdateAdminEventDefaultsInput) (*AdminEventDetail, error) {
|
||||
record, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
|
||||
var presentationID *string
|
||||
updatePresentation := false
|
||||
if input.PresentationID != nil {
|
||||
updatePresentation = true
|
||||
trimmed := strings.TrimSpace(*input.PresentationID)
|
||||
if trimmed != "" {
|
||||
presentation, err := s.store.GetEventPresentationByPublicID(ctx, trimmed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if presentation == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "presentation_not_found", "presentation not found")
|
||||
}
|
||||
if presentation.EventID != record.ID {
|
||||
return nil, apperr.New(http.StatusConflict, "presentation_not_belong_to_event", "presentation does not belong to event")
|
||||
}
|
||||
presentationID = &presentation.ID
|
||||
}
|
||||
}
|
||||
|
||||
var contentBundleID *string
|
||||
updateContent := false
|
||||
if input.ContentBundleID != nil {
|
||||
updateContent = true
|
||||
trimmed := strings.TrimSpace(*input.ContentBundleID)
|
||||
if trimmed != "" {
|
||||
contentBundle, err := s.store.GetContentBundleByPublicID(ctx, trimmed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if contentBundle == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "content_bundle_not_found", "content bundle not found")
|
||||
}
|
||||
if contentBundle.EventID != record.ID {
|
||||
return nil, apperr.New(http.StatusConflict, "content_bundle_not_belong_to_event", "content bundle does not belong to event")
|
||||
}
|
||||
contentBundleID = &contentBundle.ID
|
||||
}
|
||||
}
|
||||
|
||||
var runtimeBindingID *string
|
||||
updateRuntime := false
|
||||
if input.RuntimeBindingID != nil {
|
||||
updateRuntime = true
|
||||
trimmed := strings.TrimSpace(*input.RuntimeBindingID)
|
||||
if trimmed != "" {
|
||||
runtimeBinding, err := s.store.GetMapRuntimeBindingByPublicID(ctx, trimmed)
|
||||
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 != record.ID {
|
||||
return nil, apperr.New(http.StatusConflict, "runtime_binding_not_belong_to_event", "runtime binding does not belong to event")
|
||||
}
|
||||
runtimeBindingID = &runtimeBinding.ID
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
if err := s.store.SetEventDefaultBindings(ctx, tx, postgres.SetEventDefaultBindingsParams{
|
||||
EventID: record.ID,
|
||||
PresentationID: presentationID,
|
||||
ContentBundleID: contentBundleID,
|
||||
RuntimeBindingID: runtimeBindingID,
|
||||
UpdatePresentation: updatePresentation,
|
||||
UpdateContent: updateContent,
|
||||
UpdateRuntime: updateRuntime,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetEventDetail(ctx, eventPublicID)
|
||||
}
|
||||
|
||||
func (s *AdminEventService) SaveEventSource(ctx context.Context, eventPublicID string, input SaveAdminEventSourceInput) (*EventConfigSourceView, error) {
|
||||
eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
|
||||
if err != nil {
|
||||
@@ -441,11 +955,160 @@ func buildAdminEventSummary(item postgres.AdminEventRecord) AdminEventSummary {
|
||||
ManifestURL: item.ManifestURL,
|
||||
ManifestChecksumSha256: item.ManifestChecksum,
|
||||
RouteCode: item.RouteCode,
|
||||
Presentation: buildPresentationSummaryFromEventRecord(&item),
|
||||
ContentBundle: buildContentBundleSummaryFromEventRecord(&item),
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
func buildPresentationSummaryFromEventRecord(item *postgres.AdminEventRecord) *PresentationSummaryView {
|
||||
if item == nil || item.CurrentPresentationID == nil {
|
||||
return nil
|
||||
}
|
||||
return &PresentationSummaryView{
|
||||
PresentationID: *item.CurrentPresentationID,
|
||||
Name: item.CurrentPresentationName,
|
||||
PresentationType: item.CurrentPresentationType,
|
||||
}
|
||||
}
|
||||
|
||||
func buildContentBundleSummaryFromEventRecord(item *postgres.AdminEventRecord) *ContentBundleSummaryView {
|
||||
if item == nil || item.CurrentContentBundleID == nil {
|
||||
return nil
|
||||
}
|
||||
return &ContentBundleSummaryView{
|
||||
ContentBundleID: *item.CurrentContentBundleID,
|
||||
Name: item.CurrentContentBundleName,
|
||||
EntryURL: item.CurrentContentEntryURL,
|
||||
AssetRootURL: item.CurrentContentAssetRootURL,
|
||||
}
|
||||
}
|
||||
|
||||
func buildRuntimeSummaryFromAdminEventRecord(item *postgres.AdminEventRecord) *RuntimeSummaryView {
|
||||
if item == nil ||
|
||||
item.CurrentRuntimeBindingID == nil ||
|
||||
item.CurrentPlaceID == nil ||
|
||||
item.CurrentMapAssetID == nil ||
|
||||
item.CurrentTileReleaseID == nil ||
|
||||
item.CurrentCourseSetID == nil ||
|
||||
item.CurrentCourseVariantID == nil {
|
||||
return nil
|
||||
}
|
||||
return &RuntimeSummaryView{
|
||||
RuntimeBindingID: *item.CurrentRuntimeBindingID,
|
||||
PlaceID: *item.CurrentPlaceID,
|
||||
MapID: *item.CurrentMapAssetID,
|
||||
TileReleaseID: *item.CurrentTileReleaseID,
|
||||
CourseSetID: *item.CurrentCourseSetID,
|
||||
CourseVariantID: *item.CurrentCourseVariantID,
|
||||
CourseVariantName: item.CurrentCourseVariantName,
|
||||
RouteCode: item.CurrentRuntimeRouteCode,
|
||||
}
|
||||
}
|
||||
|
||||
func buildAdminEventPresentationView(item postgres.EventPresentation) (AdminEventPresentationView, error) {
|
||||
schema, err := decodeJSONObject(item.SchemaJSON)
|
||||
if err != nil {
|
||||
return AdminEventPresentationView{}, err
|
||||
}
|
||||
return AdminEventPresentationView{
|
||||
ID: item.PublicID,
|
||||
EventID: item.EventPublicID,
|
||||
Code: item.Code,
|
||||
Name: item.Name,
|
||||
PresentationType: item.PresentationType,
|
||||
Status: item.Status,
|
||||
IsDefault: item.IsDefault,
|
||||
TemplateKey: readStringField(schema, "templateKey"),
|
||||
Version: readStringField(schema, "version"),
|
||||
SourceType: readStringField(schema, "sourceType"),
|
||||
SchemaURL: readStringField(schema, "schemaUrl"),
|
||||
Schema: schema,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildAdminContentBundleView(item postgres.ContentBundle) (AdminContentBundleView, error) {
|
||||
metadata, err := decodeJSONObject(item.MetadataJSON)
|
||||
if err != nil {
|
||||
return AdminContentBundleView{}, err
|
||||
}
|
||||
return AdminContentBundleView{
|
||||
ID: item.PublicID,
|
||||
EventID: item.EventPublicID,
|
||||
Code: item.Code,
|
||||
Name: item.Name,
|
||||
Status: item.Status,
|
||||
IsDefault: item.IsDefault,
|
||||
BundleType: readStringField(metadata, "bundleType"),
|
||||
Version: readStringField(metadata, "version"),
|
||||
SourceType: readStringField(metadata, "sourceType"),
|
||||
ManifestURL: readStringField(metadata, "manifestUrl"),
|
||||
AssetManifest: metadata["assetManifest"],
|
||||
EntryURL: item.EntryURL,
|
||||
AssetRootURL: item.AssetRootURL,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateImportedBundleCode(title, publicID string) string {
|
||||
var builder strings.Builder
|
||||
for _, r := range strings.ToLower(title) {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
builder.WriteRune(r)
|
||||
case r >= '0' && r <= '9':
|
||||
builder.WriteRune(r)
|
||||
case r == ' ' || r == '-' || r == '_':
|
||||
if builder.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
last := builder.String()[builder.Len()-1]
|
||||
if last != '-' {
|
||||
builder.WriteByte('-')
|
||||
}
|
||||
}
|
||||
}
|
||||
code := strings.Trim(builder.String(), "-")
|
||||
if code == "" {
|
||||
code = "bundle"
|
||||
}
|
||||
suffix := publicID
|
||||
if len(suffix) > 8 {
|
||||
suffix = suffix[len(suffix)-8:]
|
||||
}
|
||||
return code + "-" + suffix
|
||||
}
|
||||
|
||||
func generateImportedPresentationCode(title, publicID string) string {
|
||||
var builder strings.Builder
|
||||
for _, r := range strings.ToLower(title) {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
builder.WriteRune(r)
|
||||
case r >= '0' && r <= '9':
|
||||
builder.WriteRune(r)
|
||||
case r == ' ' || r == '-' || r == '_':
|
||||
if builder.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
last := builder.String()[builder.Len()-1]
|
||||
if last != '-' {
|
||||
builder.WriteByte('-')
|
||||
}
|
||||
}
|
||||
}
|
||||
code := strings.Trim(builder.String(), "-")
|
||||
if code == "" {
|
||||
code = "presentation"
|
||||
}
|
||||
suffix := publicID
|
||||
if len(suffix) > 8 {
|
||||
suffix = suffix[len(suffix)-8:]
|
||||
}
|
||||
return code + "-" + suffix
|
||||
}
|
||||
|
||||
func buildAdminAssembledSource(source map[string]any) *AdminAssembledSource {
|
||||
result := &AdminAssembledSource{}
|
||||
if refs, ok := source["refs"].(map[string]any); ok {
|
||||
@@ -474,6 +1137,26 @@ func normalizeEventCatalogStatus(value string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizePresentationType(value string) string {
|
||||
switch strings.TrimSpace(value) {
|
||||
case "card":
|
||||
return "card"
|
||||
case "detail":
|
||||
return "detail"
|
||||
case "h5":
|
||||
return "h5"
|
||||
case "result":
|
||||
return "result"
|
||||
default:
|
||||
return "generic"
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarshalJSONObject(value map[string]any) string {
|
||||
raw, _ := json.Marshal(value)
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func mergeJSONObject(target map[string]any, overrides map[string]any) {
|
||||
for key, value := range overrides {
|
||||
if valueMap, ok := value.(map[string]any); ok {
|
||||
|
||||
@@ -15,15 +15,18 @@ type AdminPipelineService struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
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 {
|
||||
@@ -38,6 +41,16 @@ 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,
|
||||
@@ -77,7 +90,18 @@ func (s *AdminPipelineService) GetEventPipeline(ctx context.Context, eventPublic
|
||||
}
|
||||
releases := make([]AdminReleaseView, 0, len(releaseRecords))
|
||||
for _, item := range releaseRecords {
|
||||
releases = append(releases, buildAdminReleaseView(item))
|
||||
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{
|
||||
@@ -94,6 +118,19 @@ func (s *AdminPipelineService) GetEventPipeline(ctx context.Context, eventPublic
|
||||
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
|
||||
@@ -107,8 +144,84 @@ func (s *AdminPipelineService) GetBuild(ctx context.Context, buildID string) (*E
|
||||
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) 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) {
|
||||
@@ -167,6 +280,9 @@ func buildAdminReleaseView(item postgres.EventRelease) AdminReleaseView {
|
||||
BuildID: item.BuildID,
|
||||
Status: item.Status,
|
||||
PublishedAt: item.PublishedAt.Format(timeRFC3339),
|
||||
Runtime: buildRuntimeSummaryFromRelease(&item),
|
||||
Presentation: buildPresentationSummaryFromRelease(&item),
|
||||
ContentBundle: buildContentBundleSummaryFromRelease(&item),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
935
backend/internal/service/admin_production_service.go
Normal file
935
backend/internal/service/admin_production_service.go
Normal file
@@ -0,0 +1,935 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
"cmr-backend/internal/platform/security"
|
||||
"cmr-backend/internal/store/postgres"
|
||||
)
|
||||
|
||||
type AdminProductionService struct {
|
||||
store *postgres.Store
|
||||
}
|
||||
|
||||
type AdminPlaceSummary struct {
|
||||
ID string `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
CoverURL *string `json:"coverUrl,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
CenterPoint map[string]any `json:"centerPoint,omitempty"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type AdminPlaceDetail struct {
|
||||
Place AdminPlaceSummary `json:"place"`
|
||||
MapAssets []AdminMapAssetSummary `json:"mapAssets"`
|
||||
}
|
||||
|
||||
type CreateAdminPlaceInput struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
CoverURL *string `json:"coverUrl,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
CenterPoint map[string]any `json:"centerPoint,omitempty"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type AdminMapAssetSummary struct {
|
||||
ID string `json:"id"`
|
||||
PlaceID string `json:"placeId"`
|
||||
LegacyMapID *string `json:"legacyMapId,omitempty"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
MapType string `json:"mapType"`
|
||||
CoverURL *string `json:"coverUrl,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CurrentTileRelease *AdminTileReleaseBrief `json:"currentTileRelease,omitempty"`
|
||||
}
|
||||
|
||||
type AdminTileReleaseBrief struct {
|
||||
ID string `json:"id"`
|
||||
VersionCode string `json:"versionCode"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type AdminMapAssetDetail struct {
|
||||
MapAsset AdminMapAssetSummary `json:"mapAsset"`
|
||||
TileReleases []AdminTileReleaseView `json:"tileReleases"`
|
||||
CourseSets []AdminCourseSetBrief `json:"courseSets"`
|
||||
}
|
||||
|
||||
type CreateAdminMapAssetInput struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
MapType string `json:"mapType"`
|
||||
LegacyMapID *string `json:"legacyMapId,omitempty"`
|
||||
CoverURL *string `json:"coverUrl,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type AdminTileReleaseView struct {
|
||||
ID string `json:"id"`
|
||||
LegacyVersionID *string `json:"legacyVersionId,omitempty"`
|
||||
VersionCode string `json:"versionCode"`
|
||||
Status string `json:"status"`
|
||||
TileBaseURL string `json:"tileBaseUrl"`
|
||||
MetaURL string `json:"metaUrl"`
|
||||
PublishedAssetRoot *string `json:"publishedAssetRoot,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
PublishedAt *time.Time `json:"publishedAt,omitempty"`
|
||||
}
|
||||
|
||||
type CreateAdminTileReleaseInput struct {
|
||||
LegacyVersionID *string `json:"legacyVersionId,omitempty"`
|
||||
VersionCode string `json:"versionCode"`
|
||||
Status string `json:"status"`
|
||||
TileBaseURL string `json:"tileBaseUrl"`
|
||||
MetaURL string `json:"metaUrl"`
|
||||
PublishedAssetRoot *string `json:"publishedAssetRoot,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
SetAsCurrent bool `json:"setAsCurrent"`
|
||||
}
|
||||
|
||||
type AdminCourseSourceSummary struct {
|
||||
ID string `json:"id"`
|
||||
LegacyVersionID *string `json:"legacyVersionId,omitempty"`
|
||||
SourceType string `json:"sourceType"`
|
||||
FileURL string `json:"fileUrl"`
|
||||
Checksum *string `json:"checksum,omitempty"`
|
||||
ParserVersion *string `json:"parserVersion,omitempty"`
|
||||
ImportStatus string `json:"importStatus"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
ImportedAt time.Time `json:"importedAt"`
|
||||
}
|
||||
|
||||
type CreateAdminCourseSourceInput struct {
|
||||
LegacyPlayfieldID *string `json:"legacyPlayfieldId,omitempty"`
|
||||
LegacyVersionID *string `json:"legacyVersionId,omitempty"`
|
||||
SourceType string `json:"sourceType"`
|
||||
FileURL string `json:"fileUrl"`
|
||||
Checksum *string `json:"checksum,omitempty"`
|
||||
ParserVersion *string `json:"parserVersion,omitempty"`
|
||||
ImportStatus string `json:"importStatus"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type AdminCourseSetBrief struct {
|
||||
ID string `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Mode string `json:"mode"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CurrentVariant *AdminCourseVariantBrief `json:"currentVariant,omitempty"`
|
||||
}
|
||||
|
||||
type AdminCourseVariantBrief struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type AdminCourseSetDetail struct {
|
||||
CourseSet AdminCourseSetBrief `json:"courseSet"`
|
||||
Variants []AdminCourseVariantView `json:"variants"`
|
||||
}
|
||||
|
||||
type CreateAdminCourseSetInput struct {
|
||||
Code string `json:"code"`
|
||||
Mode string `json:"mode"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type AdminCourseVariantView struct {
|
||||
ID string `json:"id"`
|
||||
SourceID *string `json:"sourceId,omitempty"`
|
||||
Name string `json:"name"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
Mode string `json:"mode"`
|
||||
ControlCount *int `json:"controlCount,omitempty"`
|
||||
Difficulty *string `json:"difficulty,omitempty"`
|
||||
Status string `json:"status"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
ConfigPatch map[string]any `json:"configPatch,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type CreateAdminCourseVariantInput struct {
|
||||
SourceID *string `json:"sourceId,omitempty"`
|
||||
Name string `json:"name"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
Mode string `json:"mode"`
|
||||
ControlCount *int `json:"controlCount,omitempty"`
|
||||
Difficulty *string `json:"difficulty,omitempty"`
|
||||
Status string `json:"status"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
ConfigPatch map[string]any `json:"configPatch,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type AdminRuntimeBindingSummary struct {
|
||||
ID string `json:"id"`
|
||||
EventID string `json:"eventId"`
|
||||
PlaceID string `json:"placeId"`
|
||||
MapAssetID string `json:"mapAssetId"`
|
||||
TileReleaseID string `json:"tileReleaseId"`
|
||||
CourseSetID string `json:"courseSetId"`
|
||||
CourseVariantID string `json:"courseVariantId"`
|
||||
Status string `json:"status"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
type CreateAdminRuntimeBindingInput struct {
|
||||
EventID string `json:"eventId"`
|
||||
PlaceID string `json:"placeId"`
|
||||
MapAssetID string `json:"mapAssetId"`
|
||||
TileReleaseID string `json:"tileReleaseId"`
|
||||
CourseSetID string `json:"courseSetId"`
|
||||
CourseVariantID string `json:"courseVariantId"`
|
||||
Status string `json:"status"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
func NewAdminProductionService(store *postgres.Store) *AdminProductionService {
|
||||
return &AdminProductionService{store: store}
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) ListPlaces(ctx context.Context, limit int) ([]AdminPlaceSummary, error) {
|
||||
items, err := s.store.ListPlaces(ctx, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]AdminPlaceSummary, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, buildAdminPlaceSummary(item))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) CreatePlace(ctx context.Context, input CreateAdminPlaceInput) (*AdminPlaceSummary, error) {
|
||||
input.Code = strings.TrimSpace(input.Code)
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
if input.Code == "" || input.Name == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
|
||||
}
|
||||
publicID, err := security.GeneratePublicID("place")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
item, err := s.store.CreatePlace(ctx, tx, postgres.CreatePlaceParams{
|
||||
PublicID: publicID,
|
||||
Code: input.Code,
|
||||
Name: input.Name,
|
||||
Region: trimStringPtr(input.Region),
|
||||
CoverURL: trimStringPtr(input.CoverURL),
|
||||
Description: trimStringPtr(input.Description),
|
||||
CenterPoint: input.CenterPoint,
|
||||
Status: normalizeCatalogStatus(input.Status),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := buildAdminPlaceSummary(*item)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) GetPlaceDetail(ctx context.Context, placePublicID string) (*AdminPlaceDetail, error) {
|
||||
place, err := s.store.GetPlaceByPublicID(ctx, strings.TrimSpace(placePublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if place == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "place_not_found", "place not found")
|
||||
}
|
||||
mapAssets, err := s.store.ListMapAssetsByPlaceID(ctx, place.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &AdminPlaceDetail{
|
||||
Place: buildAdminPlaceSummary(*place),
|
||||
MapAssets: make([]AdminMapAssetSummary, 0, len(mapAssets)),
|
||||
}
|
||||
for _, item := range mapAssets {
|
||||
summary, err := s.buildAdminMapAssetSummary(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.MapAssets = append(result.MapAssets, summary)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) CreateMapAsset(ctx context.Context, placePublicID string, input CreateAdminMapAssetInput) (*AdminMapAssetSummary, error) {
|
||||
place, err := s.store.GetPlaceByPublicID(ctx, strings.TrimSpace(placePublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if place == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "place_not_found", "place not found")
|
||||
}
|
||||
input.Code = strings.TrimSpace(input.Code)
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
mapType := strings.TrimSpace(input.MapType)
|
||||
if mapType == "" {
|
||||
mapType = "standard"
|
||||
}
|
||||
if input.Code == "" || input.Name == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
|
||||
}
|
||||
|
||||
var legacyMapID *string
|
||||
if input.LegacyMapID != nil && strings.TrimSpace(*input.LegacyMapID) != "" {
|
||||
legacyMap, err := s.store.GetResourceMapByPublicID(ctx, strings.TrimSpace(*input.LegacyMapID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if legacyMap == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "legacy_map_not_found", "legacy map not found")
|
||||
}
|
||||
legacyMapID = &legacyMap.ID
|
||||
}
|
||||
|
||||
publicID, err := security.GeneratePublicID("mapasset")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
item, err := s.store.CreateMapAsset(ctx, tx, postgres.CreateMapAssetParams{
|
||||
PublicID: publicID,
|
||||
PlaceID: place.ID,
|
||||
LegacyMapID: legacyMapID,
|
||||
Code: input.Code,
|
||||
Name: input.Name,
|
||||
MapType: mapType,
|
||||
CoverURL: trimStringPtr(input.CoverURL),
|
||||
Description: trimStringPtr(input.Description),
|
||||
Status: normalizeCatalogStatus(input.Status),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := s.buildAdminMapAssetSummary(ctx, *item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) GetMapAssetDetail(ctx context.Context, mapAssetPublicID string) (*AdminMapAssetDetail, error) {
|
||||
item, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(mapAssetPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "map_asset_not_found", "map asset not found")
|
||||
}
|
||||
summary, err := s.buildAdminMapAssetSummary(ctx, *item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tileReleases, err := s.store.ListTileReleasesByMapAssetID(ctx, item.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
courseSets, err := s.store.ListCourseSetsByMapAssetID(ctx, item.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &AdminMapAssetDetail{
|
||||
MapAsset: summary,
|
||||
TileReleases: make([]AdminTileReleaseView, 0, len(tileReleases)),
|
||||
CourseSets: make([]AdminCourseSetBrief, 0, len(courseSets)),
|
||||
}
|
||||
for _, release := range tileReleases {
|
||||
result.TileReleases = append(result.TileReleases, buildAdminTileReleaseView(release))
|
||||
}
|
||||
for _, courseSet := range courseSets {
|
||||
brief, err := s.buildAdminCourseSetBrief(ctx, courseSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.CourseSets = append(result.CourseSets, brief)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) CreateTileRelease(ctx context.Context, mapAssetPublicID string, input CreateAdminTileReleaseInput) (*AdminTileReleaseView, error) {
|
||||
mapAsset, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(mapAssetPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mapAsset == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "map_asset_not_found", "map asset not found")
|
||||
}
|
||||
input.VersionCode = strings.TrimSpace(input.VersionCode)
|
||||
input.TileBaseURL = strings.TrimSpace(input.TileBaseURL)
|
||||
input.MetaURL = strings.TrimSpace(input.MetaURL)
|
||||
if input.VersionCode == "" || input.TileBaseURL == "" || input.MetaURL == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "versionCode, tileBaseUrl and metaUrl are required")
|
||||
}
|
||||
|
||||
var legacyVersionID *string
|
||||
if input.LegacyVersionID != nil && strings.TrimSpace(*input.LegacyVersionID) != "" {
|
||||
if mapAsset.LegacyMapPublicID == nil || strings.TrimSpace(*mapAsset.LegacyMapPublicID) == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "legacy_map_missing", "map asset has no linked legacy map")
|
||||
}
|
||||
legacyVersion, err := s.store.GetResourceMapVersionByPublicID(ctx, *mapAsset.LegacyMapPublicID, strings.TrimSpace(*input.LegacyVersionID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if legacyVersion == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "legacy_tile_version_not_found", "legacy map version not found")
|
||||
}
|
||||
legacyVersionID = &legacyVersion.ID
|
||||
}
|
||||
|
||||
publicID, err := security.GeneratePublicID("tile")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
publishedAt := time.Now()
|
||||
release, err := s.store.CreateTileRelease(ctx, tx, postgres.CreateTileReleaseParams{
|
||||
PublicID: publicID,
|
||||
MapAssetID: mapAsset.ID,
|
||||
LegacyMapVersionID: legacyVersionID,
|
||||
VersionCode: input.VersionCode,
|
||||
Status: normalizeReleaseStatus(input.Status),
|
||||
TileBaseURL: input.TileBaseURL,
|
||||
MetaURL: input.MetaURL,
|
||||
PublishedAssetRoot: trimStringPtr(input.PublishedAssetRoot),
|
||||
MetadataJSON: input.Metadata,
|
||||
PublishedAt: &publishedAt,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.SetAsCurrent {
|
||||
if err := s.store.SetMapAssetCurrentTileRelease(ctx, tx, mapAsset.ID, release.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view := buildAdminTileReleaseView(*release)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) ListCourseSources(ctx context.Context, limit int) ([]AdminCourseSourceSummary, error) {
|
||||
items, err := s.store.ListCourseSources(ctx, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]AdminCourseSourceSummary, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, buildAdminCourseSourceSummary(item))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) CreateCourseSource(ctx context.Context, input CreateAdminCourseSourceInput) (*AdminCourseSourceSummary, error) {
|
||||
sourceType := strings.TrimSpace(input.SourceType)
|
||||
fileURL := strings.TrimSpace(input.FileURL)
|
||||
if sourceType == "" || fileURL == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "sourceType and fileUrl are required")
|
||||
}
|
||||
|
||||
var legacyPlayfieldVersionID *string
|
||||
if input.LegacyPlayfieldID != nil && input.LegacyVersionID != nil && strings.TrimSpace(*input.LegacyPlayfieldID) != "" && strings.TrimSpace(*input.LegacyVersionID) != "" {
|
||||
version, err := s.store.GetResourcePlayfieldVersionByPublicID(ctx, strings.TrimSpace(*input.LegacyPlayfieldID), strings.TrimSpace(*input.LegacyVersionID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if version == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "legacy_playfield_version_not_found", "legacy playfield version not found")
|
||||
}
|
||||
legacyPlayfieldVersionID = &version.ID
|
||||
}
|
||||
|
||||
publicID, err := security.GeneratePublicID("csrc")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
item, err := s.store.CreateCourseSource(ctx, tx, postgres.CreateCourseSourceParams{
|
||||
PublicID: publicID,
|
||||
LegacyPlayfieldVersionID: legacyPlayfieldVersionID,
|
||||
SourceType: sourceType,
|
||||
FileURL: fileURL,
|
||||
Checksum: trimStringPtr(input.Checksum),
|
||||
ParserVersion: trimStringPtr(input.ParserVersion),
|
||||
ImportStatus: normalizeCourseSourceStatus(input.ImportStatus),
|
||||
MetadataJSON: input.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := buildAdminCourseSourceSummary(*item)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) GetCourseSource(ctx context.Context, sourcePublicID string) (*AdminCourseSourceSummary, error) {
|
||||
item, err := s.store.GetCourseSourceByPublicID(ctx, strings.TrimSpace(sourcePublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "course_source_not_found", "course source not found")
|
||||
}
|
||||
result := buildAdminCourseSourceSummary(*item)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) CreateCourseSet(ctx context.Context, mapAssetPublicID string, input CreateAdminCourseSetInput) (*AdminCourseSetBrief, error) {
|
||||
mapAsset, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(mapAssetPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mapAsset == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "map_asset_not_found", "map asset not found")
|
||||
}
|
||||
input.Code = strings.TrimSpace(input.Code)
|
||||
input.Mode = strings.TrimSpace(input.Mode)
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
if input.Code == "" || input.Mode == "" || input.Name == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code, mode and name are required")
|
||||
}
|
||||
publicID, err := security.GeneratePublicID("cset")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
item, err := s.store.CreateCourseSet(ctx, tx, postgres.CreateCourseSetParams{
|
||||
PublicID: publicID,
|
||||
PlaceID: mapAsset.PlaceID,
|
||||
MapAssetID: mapAsset.ID,
|
||||
Code: input.Code,
|
||||
Mode: input.Mode,
|
||||
Name: input.Name,
|
||||
Description: trimStringPtr(input.Description),
|
||||
Status: normalizeCatalogStatus(input.Status),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
brief, err := s.buildAdminCourseSetBrief(ctx, *item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &brief, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) GetCourseSetDetail(ctx context.Context, courseSetPublicID string) (*AdminCourseSetDetail, error) {
|
||||
item, err := s.store.GetCourseSetByPublicID(ctx, strings.TrimSpace(courseSetPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "course_set_not_found", "course set not found")
|
||||
}
|
||||
brief, err := s.buildAdminCourseSetBrief(ctx, *item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
variants, err := s.store.ListCourseVariantsByCourseSetID(ctx, item.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &AdminCourseSetDetail{
|
||||
CourseSet: brief,
|
||||
Variants: make([]AdminCourseVariantView, 0, len(variants)),
|
||||
}
|
||||
for _, variant := range variants {
|
||||
result.Variants = append(result.Variants, buildAdminCourseVariantView(variant))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) CreateCourseVariant(ctx context.Context, courseSetPublicID string, input CreateAdminCourseVariantInput) (*AdminCourseVariantView, error) {
|
||||
courseSet, err := s.store.GetCourseSetByPublicID(ctx, strings.TrimSpace(courseSetPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if courseSet == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "course_set_not_found", "course set not found")
|
||||
}
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
input.Mode = strings.TrimSpace(input.Mode)
|
||||
if input.Name == "" || input.Mode == "" {
|
||||
return nil, apperr.New(http.StatusBadRequest, "invalid_params", "name and mode are required")
|
||||
}
|
||||
|
||||
var sourceID *string
|
||||
if input.SourceID != nil && strings.TrimSpace(*input.SourceID) != "" {
|
||||
source, err := s.store.GetCourseSourceByPublicID(ctx, strings.TrimSpace(*input.SourceID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if source == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "course_source_not_found", "course source not found")
|
||||
}
|
||||
sourceID = &source.ID
|
||||
}
|
||||
|
||||
publicID, err := security.GeneratePublicID("cvar")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
item, err := s.store.CreateCourseVariant(ctx, tx, postgres.CreateCourseVariantParams{
|
||||
PublicID: publicID,
|
||||
CourseSetID: courseSet.ID,
|
||||
SourceID: sourceID,
|
||||
Name: input.Name,
|
||||
RouteCode: trimStringPtr(input.RouteCode),
|
||||
Mode: input.Mode,
|
||||
ControlCount: input.ControlCount,
|
||||
Difficulty: trimStringPtr(input.Difficulty),
|
||||
Status: normalizeCatalogStatus(input.Status),
|
||||
IsDefault: input.IsDefault,
|
||||
ConfigPatch: input.ConfigPatch,
|
||||
MetadataJSON: input.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.IsDefault {
|
||||
if err := s.store.SetCourseSetCurrentVariant(ctx, tx, courseSet.ID, item.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view := buildAdminCourseVariantView(*item)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) ListRuntimeBindings(ctx context.Context, limit int) ([]AdminRuntimeBindingSummary, error) {
|
||||
items, err := s.store.ListMapRuntimeBindings(ctx, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]AdminRuntimeBindingSummary, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, buildAdminRuntimeBindingSummary(item))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) CreateRuntimeBinding(ctx context.Context, input CreateAdminRuntimeBindingInput) (*AdminRuntimeBindingSummary, error) {
|
||||
eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(input.EventID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if eventRecord == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
place, err := s.store.GetPlaceByPublicID(ctx, strings.TrimSpace(input.PlaceID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if place == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "place_not_found", "place not found")
|
||||
}
|
||||
mapAsset, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(input.MapAssetID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mapAsset == nil || mapAsset.PlaceID != place.ID {
|
||||
return nil, apperr.New(http.StatusBadRequest, "map_asset_mismatch", "map asset does not belong to place")
|
||||
}
|
||||
tileRelease, err := s.store.GetTileReleaseByPublicID(ctx, strings.TrimSpace(input.TileReleaseID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tileRelease == nil || tileRelease.MapAssetID != mapAsset.ID {
|
||||
return nil, apperr.New(http.StatusBadRequest, "tile_release_mismatch", "tile release does not belong to map asset")
|
||||
}
|
||||
courseSet, err := s.store.GetCourseSetByPublicID(ctx, strings.TrimSpace(input.CourseSetID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if courseSet == nil || courseSet.PlaceID != place.ID || courseSet.MapAssetID != mapAsset.ID {
|
||||
return nil, apperr.New(http.StatusBadRequest, "course_set_mismatch", "course set does not match place/map asset")
|
||||
}
|
||||
courseVariant, err := s.store.GetCourseVariantByPublicID(ctx, strings.TrimSpace(input.CourseVariantID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if courseVariant == nil || courseVariant.CourseSetID != courseSet.ID {
|
||||
return nil, apperr.New(http.StatusBadRequest, "course_variant_mismatch", "course variant does not belong to course set")
|
||||
}
|
||||
|
||||
publicID, err := security.GeneratePublicID("rtbind")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
item, err := s.store.CreateMapRuntimeBinding(ctx, tx, postgres.CreateMapRuntimeBindingParams{
|
||||
PublicID: publicID,
|
||||
EventID: eventRecord.ID,
|
||||
PlaceID: place.ID,
|
||||
MapAssetID: mapAsset.ID,
|
||||
TileReleaseID: tileRelease.ID,
|
||||
CourseSetID: courseSet.ID,
|
||||
CourseVariantID: courseVariant.ID,
|
||||
Status: normalizeRuntimeBindingStatus(input.Status),
|
||||
Notes: trimStringPtr(input.Notes),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
created, err := s.store.GetMapRuntimeBindingByPublicID(ctx, item.PublicID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if created == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "runtime_binding_not_found", "runtime binding not found")
|
||||
}
|
||||
result := buildAdminRuntimeBindingSummary(*created)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) GetRuntimeBinding(ctx context.Context, runtimeBindingPublicID string) (*AdminRuntimeBindingSummary, error) {
|
||||
item, err := s.store.GetMapRuntimeBindingByPublicID(ctx, strings.TrimSpace(runtimeBindingPublicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item == nil {
|
||||
return nil, apperr.New(http.StatusNotFound, "runtime_binding_not_found", "runtime binding not found")
|
||||
}
|
||||
result := buildAdminRuntimeBindingSummary(*item)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) buildAdminMapAssetSummary(ctx context.Context, item postgres.MapAsset) (AdminMapAssetSummary, error) {
|
||||
result := AdminMapAssetSummary{
|
||||
ID: item.PublicID,
|
||||
PlaceID: item.PlaceID,
|
||||
LegacyMapID: item.LegacyMapPublicID,
|
||||
Code: item.Code,
|
||||
Name: item.Name,
|
||||
MapType: item.MapType,
|
||||
CoverURL: item.CoverURL,
|
||||
Description: item.Description,
|
||||
Status: item.Status,
|
||||
}
|
||||
if item.CurrentTileReleaseID != nil {
|
||||
releases, err := s.store.ListTileReleasesByMapAssetID(ctx, item.ID)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
for _, release := range releases {
|
||||
if release.ID == *item.CurrentTileReleaseID {
|
||||
result.CurrentTileRelease = &AdminTileReleaseBrief{
|
||||
ID: release.PublicID,
|
||||
VersionCode: release.VersionCode,
|
||||
Status: release.Status,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *AdminProductionService) buildAdminCourseSetBrief(ctx context.Context, item postgres.CourseSet) (AdminCourseSetBrief, error) {
|
||||
result := AdminCourseSetBrief{
|
||||
ID: item.PublicID,
|
||||
Code: item.Code,
|
||||
Mode: item.Mode,
|
||||
Name: item.Name,
|
||||
Description: item.Description,
|
||||
Status: item.Status,
|
||||
}
|
||||
if item.CurrentVariantID != nil {
|
||||
variants, err := s.store.ListCourseVariantsByCourseSetID(ctx, item.ID)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
for _, variant := range variants {
|
||||
if variant.ID == *item.CurrentVariantID {
|
||||
result.CurrentVariant = &AdminCourseVariantBrief{
|
||||
ID: variant.PublicID,
|
||||
Name: variant.Name,
|
||||
RouteCode: variant.RouteCode,
|
||||
Status: variant.Status,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func buildAdminPlaceSummary(item postgres.Place) AdminPlaceSummary {
|
||||
return AdminPlaceSummary{
|
||||
ID: item.PublicID,
|
||||
Code: item.Code,
|
||||
Name: item.Name,
|
||||
Region: item.Region,
|
||||
CoverURL: item.CoverURL,
|
||||
Description: item.Description,
|
||||
CenterPoint: decodeJSONMap(item.CenterPoint),
|
||||
Status: item.Status,
|
||||
}
|
||||
}
|
||||
|
||||
func buildAdminTileReleaseView(item postgres.TileRelease) AdminTileReleaseView {
|
||||
return AdminTileReleaseView{
|
||||
ID: item.PublicID,
|
||||
LegacyVersionID: item.LegacyMapVersionPub,
|
||||
VersionCode: item.VersionCode,
|
||||
Status: item.Status,
|
||||
TileBaseURL: item.TileBaseURL,
|
||||
MetaURL: item.MetaURL,
|
||||
PublishedAssetRoot: item.PublishedAssetRoot,
|
||||
Metadata: decodeJSONMap(item.MetadataJSON),
|
||||
PublishedAt: item.PublishedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func buildAdminCourseSourceSummary(item postgres.CourseSource) AdminCourseSourceSummary {
|
||||
return AdminCourseSourceSummary{
|
||||
ID: item.PublicID,
|
||||
LegacyVersionID: item.LegacyPlayfieldVersionPub,
|
||||
SourceType: item.SourceType,
|
||||
FileURL: item.FileURL,
|
||||
Checksum: item.Checksum,
|
||||
ParserVersion: item.ParserVersion,
|
||||
ImportStatus: item.ImportStatus,
|
||||
Metadata: decodeJSONMap(item.MetadataJSON),
|
||||
ImportedAt: item.ImportedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func buildAdminCourseVariantView(item postgres.CourseVariant) AdminCourseVariantView {
|
||||
return AdminCourseVariantView{
|
||||
ID: item.PublicID,
|
||||
SourceID: item.SourcePublicID,
|
||||
Name: item.Name,
|
||||
RouteCode: item.RouteCode,
|
||||
Mode: item.Mode,
|
||||
ControlCount: item.ControlCount,
|
||||
Difficulty: item.Difficulty,
|
||||
Status: item.Status,
|
||||
IsDefault: item.IsDefault,
|
||||
ConfigPatch: decodeJSONMap(item.ConfigPatch),
|
||||
Metadata: decodeJSONMap(item.MetadataJSON),
|
||||
}
|
||||
}
|
||||
|
||||
func buildAdminRuntimeBindingSummary(item postgres.MapRuntimeBinding) AdminRuntimeBindingSummary {
|
||||
return AdminRuntimeBindingSummary{
|
||||
ID: item.PublicID,
|
||||
EventID: item.EventPublicID,
|
||||
PlaceID: item.PlacePublicID,
|
||||
MapAssetID: item.MapAssetPublicID,
|
||||
TileReleaseID: item.TileReleasePublicID,
|
||||
CourseSetID: item.CourseSetPublicID,
|
||||
CourseVariantID: item.CourseVariantPublicID,
|
||||
Status: item.Status,
|
||||
Notes: item.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCourseSourceStatus(value string) string {
|
||||
switch strings.TrimSpace(value) {
|
||||
case "draft":
|
||||
return "draft"
|
||||
case "parsed":
|
||||
return "parsed"
|
||||
case "failed":
|
||||
return "failed"
|
||||
case "archived":
|
||||
return "archived"
|
||||
default:
|
||||
return "imported"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRuntimeBindingStatus(value string) string {
|
||||
switch strings.TrimSpace(value) {
|
||||
case "active":
|
||||
return "active"
|
||||
case "disabled":
|
||||
return "disabled"
|
||||
case "archived":
|
||||
return "archived"
|
||||
default:
|
||||
return "draft"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeReleaseStatus(value string) string {
|
||||
switch strings.TrimSpace(value) {
|
||||
case "active":
|
||||
return "active"
|
||||
case "published":
|
||||
return "published"
|
||||
case "retired":
|
||||
return "retired"
|
||||
case "archived":
|
||||
return "archived"
|
||||
default:
|
||||
return "draft"
|
||||
}
|
||||
}
|
||||
@@ -58,10 +58,13 @@ type EventConfigBuildView struct {
|
||||
}
|
||||
|
||||
type PublishedReleaseView struct {
|
||||
EventID string `json:"eventId"`
|
||||
Release ResolvedReleaseView `json:"release"`
|
||||
ReleaseNo int `json:"releaseNo"`
|
||||
PublishedAt string `json:"publishedAt"`
|
||||
EventID string `json:"eventId"`
|
||||
Release ResolvedReleaseView `json:"release"`
|
||||
ReleaseNo int `json:"releaseNo"`
|
||||
PublishedAt string `json:"publishedAt"`
|
||||
Runtime *RuntimeSummaryView `json:"runtime,omitempty"`
|
||||
Presentation *PresentationSummaryView `json:"presentation,omitempty"`
|
||||
ContentBundle *ContentBundleSummaryView `json:"contentBundle,omitempty"`
|
||||
}
|
||||
|
||||
type ImportLocalEventConfigInput struct {
|
||||
@@ -75,7 +78,10 @@ type BuildPreviewInput struct {
|
||||
}
|
||||
|
||||
type PublishBuildInput struct {
|
||||
BuildID string `json:"buildId"`
|
||||
BuildID string `json:"buildId"`
|
||||
RuntimeBindingID string `json:"runtimeBindingId,omitempty"`
|
||||
PresentationID string `json:"presentationId,omitempty"`
|
||||
ContentBundleID string `json:"contentBundleId,omitempty"`
|
||||
}
|
||||
|
||||
func NewConfigService(store *postgres.Store, localEventDir, assetBaseURL string, publisher *assets.OSSUtilPublisher) *ConfigService {
|
||||
@@ -306,6 +312,19 @@ func (s *ConfigService) PublishBuild(ctx context.Context, input PublishBuildInpu
|
||||
return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
|
||||
}
|
||||
|
||||
runtimeBindingID, runtimeSummary, err := s.resolvePublishRuntimeBinding(ctx, event.ID, input.RuntimeBindingID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
presentationID, presentationSummary, err := s.resolvePublishPresentation(ctx, event.ID, input.PresentationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentBundleID, contentBundleSummary, err := s.resolvePublishContentBundle(ctx, event.ID, input.ContentBundleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifest, err := decodeJSONObject(buildRecord.ManifestJSON)
|
||||
if err != nil {
|
||||
return nil, apperr.New(http.StatusInternalServerError, "config_build_invalid", "stored build manifest is invalid")
|
||||
@@ -355,6 +374,9 @@ func (s *ConfigService) PublishBuild(ctx context.Context, input PublishBuildInpu
|
||||
ManifestChecksum: &checksum,
|
||||
RouteCode: routeCode,
|
||||
BuildID: &buildRecord.ID,
|
||||
RuntimeBindingID: runtimeBindingID,
|
||||
PresentationID: presentationID,
|
||||
ContentBundleID: contentBundleID,
|
||||
Status: "published",
|
||||
PayloadJSON: buildRecord.ManifestJSON,
|
||||
})
|
||||
@@ -386,11 +408,160 @@ func (s *ConfigService) PublishBuild(ctx context.Context, input PublishBuildInpu
|
||||
ManifestChecksumSha256: releaseRecord.ManifestChecksum,
|
||||
RouteCode: releaseRecord.RouteCode,
|
||||
},
|
||||
ReleaseNo: releaseRecord.ReleaseNo,
|
||||
PublishedAt: releaseRecord.PublishedAt.Format(timeRFC3339),
|
||||
ReleaseNo: releaseRecord.ReleaseNo,
|
||||
PublishedAt: releaseRecord.PublishedAt.Format(timeRFC3339),
|
||||
Runtime: runtimeSummary,
|
||||
Presentation: presentationSummary,
|
||||
ContentBundle: contentBundleSummary,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ConfigService) resolvePublishRuntimeBinding(ctx context.Context, eventID string, runtimeBindingPublicID string) (*string, *RuntimeSummaryView, error) {
|
||||
runtimeBindingPublicID = strings.TrimSpace(runtimeBindingPublicID)
|
||||
if runtimeBindingPublicID == "" {
|
||||
defaults, err := s.store.GetEventDefaultBindingsByEventID(ctx, eventID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if defaults == nil || defaults.RuntimeBindingID == nil || defaults.RuntimeBindingPublicID == nil || defaults.PlacePublicID == nil || defaults.MapAssetPublicID == nil || defaults.TileReleasePublicID == nil || defaults.CourseSetPublicID == nil || defaults.CourseVariantPublicID == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return defaults.RuntimeBindingID, &RuntimeSummaryView{
|
||||
RuntimeBindingID: *defaults.RuntimeBindingPublicID,
|
||||
PlaceID: *defaults.PlacePublicID,
|
||||
PlaceName: defaults.PlaceName,
|
||||
MapID: *defaults.MapAssetPublicID,
|
||||
MapName: defaults.MapAssetName,
|
||||
TileReleaseID: *defaults.TileReleasePublicID,
|
||||
CourseSetID: *defaults.CourseSetPublicID,
|
||||
CourseVariantID: *defaults.CourseVariantPublicID,
|
||||
CourseVariantName: defaults.CourseVariantName,
|
||||
RouteCode: defaults.RuntimeRouteCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
runtimeBinding, err := s.store.GetMapRuntimeBindingByPublicID(ctx, runtimeBindingPublicID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if runtimeBinding == nil {
|
||||
return nil, nil, apperr.New(http.StatusNotFound, "runtime_binding_not_found", "runtime binding not found")
|
||||
}
|
||||
if runtimeBinding.EventID != eventID {
|
||||
return nil, nil, apperr.New(http.StatusConflict, "runtime_binding_not_belong_to_event", "runtime binding does not belong to build event")
|
||||
}
|
||||
|
||||
return &runtimeBinding.ID, &RuntimeSummaryView{
|
||||
RuntimeBindingID: runtimeBinding.PublicID,
|
||||
PlaceID: runtimeBinding.PlacePublicID,
|
||||
MapID: runtimeBinding.MapAssetPublicID,
|
||||
TileReleaseID: runtimeBinding.TileReleasePublicID,
|
||||
CourseSetID: runtimeBinding.CourseSetPublicID,
|
||||
CourseVariantID: runtimeBinding.CourseVariantPublicID,
|
||||
RouteCode: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ConfigService) resolvePublishPresentation(ctx context.Context, eventID string, presentationPublicID string) (*string, *PresentationSummaryView, error) {
|
||||
presentationPublicID = strings.TrimSpace(presentationPublicID)
|
||||
if presentationPublicID == "" {
|
||||
defaults, err := s.store.GetEventDefaultBindingsByEventID(ctx, eventID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if defaults != nil && defaults.PresentationID != nil && defaults.PresentationPublicID != nil {
|
||||
record, err := s.store.GetEventPresentationByPublicID(ctx, *defaults.PresentationPublicID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if record != nil {
|
||||
summary, err := buildPresentationSummaryFromRecord(record)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return defaults.PresentationID, summary, nil
|
||||
}
|
||||
}
|
||||
record, err := s.store.GetDefaultEventPresentationByEventID(ctx, eventID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
summary, err := buildPresentationSummaryFromRecord(record)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &record.ID, summary, nil
|
||||
}
|
||||
record, err := s.store.GetEventPresentationByPublicID(ctx, presentationPublicID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, nil, apperr.New(http.StatusNotFound, "presentation_not_found", "presentation not found")
|
||||
}
|
||||
if record.EventID != eventID {
|
||||
return nil, nil, apperr.New(http.StatusConflict, "presentation_not_belong_to_event", "presentation does not belong to build event")
|
||||
}
|
||||
summary, err := buildPresentationSummaryFromRecord(record)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &record.ID, summary, nil
|
||||
}
|
||||
|
||||
func (s *ConfigService) resolvePublishContentBundle(ctx context.Context, eventID string, contentBundlePublicID string) (*string, *ContentBundleSummaryView, error) {
|
||||
contentBundlePublicID = strings.TrimSpace(contentBundlePublicID)
|
||||
if contentBundlePublicID == "" {
|
||||
defaults, err := s.store.GetEventDefaultBindingsByEventID(ctx, eventID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if defaults != nil && defaults.ContentBundleID != nil && defaults.ContentBundlePublicID != nil {
|
||||
record, err := s.store.GetContentBundleByPublicID(ctx, *defaults.ContentBundlePublicID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if record != nil {
|
||||
summary, err := buildContentBundleSummaryFromRecord(record)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return defaults.ContentBundleID, summary, nil
|
||||
}
|
||||
}
|
||||
record, err := s.store.GetDefaultContentBundleByEventID(ctx, eventID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
summary, err := buildContentBundleSummaryFromRecord(record)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &record.ID, summary, nil
|
||||
}
|
||||
record, err := s.store.GetContentBundleByPublicID(ctx, contentBundlePublicID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, nil, apperr.New(http.StatusNotFound, "content_bundle_not_found", "content bundle not found")
|
||||
}
|
||||
if record.EventID != eventID {
|
||||
return nil, nil, apperr.New(http.StatusConflict, "content_bundle_not_belong_to_event", "content bundle does not belong to build event")
|
||||
}
|
||||
summary, err := buildContentBundleSummaryFromRecord(record)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &record.ID, summary, nil
|
||||
}
|
||||
|
||||
func (s *ConfigService) requireEvent(ctx context.Context, eventPublicID string) (*postgres.Event, error) {
|
||||
eventPublicID = strings.TrimSpace(eventPublicID)
|
||||
if eventPublicID == "" {
|
||||
|
||||
@@ -33,8 +33,11 @@ type EventPlayResult struct {
|
||||
ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
} `json:"release,omitempty"`
|
||||
ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
|
||||
Play struct {
|
||||
ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
|
||||
Runtime *RuntimeSummaryView `json:"runtime,omitempty"`
|
||||
CurrentPresentation *PresentationSummaryView `json:"currentPresentation,omitempty"`
|
||||
CurrentContentBundle *ContentBundleSummaryView `json:"currentContentBundle,omitempty"`
|
||||
Play struct {
|
||||
AssignmentMode *string `json:"assignmentMode,omitempty"`
|
||||
CourseVariants []CourseVariantView `json:"courseVariants,omitempty"`
|
||||
CanLaunch bool `json:"canLaunch"`
|
||||
@@ -100,6 +103,19 @@ func (s *EventPlayService) GetEventPlay(ctx context.Context, input EventPlayInpu
|
||||
}
|
||||
}
|
||||
result.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
|
||||
result.Runtime = buildRuntimeSummaryFromEvent(event)
|
||||
result.CurrentPresentation = buildPresentationSummaryFromEvent(event)
|
||||
if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
|
||||
return nil, err
|
||||
} else if enrichedPresentation != nil {
|
||||
result.CurrentPresentation = enrichedPresentation
|
||||
}
|
||||
result.CurrentContentBundle = buildContentBundleSummaryFromEvent(event)
|
||||
if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
|
||||
return nil, err
|
||||
} else if enrichedBundle != nil {
|
||||
result.CurrentContentBundle = enrichedBundle
|
||||
}
|
||||
|
||||
if len(sessions) > 0 {
|
||||
recent := buildEntrySessionSummary(&sessions[0])
|
||||
|
||||
@@ -30,7 +30,10 @@ type EventDetailResult struct {
|
||||
ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
} `json:"release,omitempty"`
|
||||
ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
|
||||
ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
|
||||
Runtime *RuntimeSummaryView `json:"runtime,omitempty"`
|
||||
CurrentPresentation *PresentationSummaryView `json:"currentPresentation,omitempty"`
|
||||
CurrentContentBundle *ContentBundleSummaryView `json:"currentContentBundle,omitempty"`
|
||||
}
|
||||
|
||||
type LaunchEventInput struct {
|
||||
@@ -48,9 +51,12 @@ type LaunchEventResult struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
} `json:"event"`
|
||||
Launch struct {
|
||||
Source string `json:"source"`
|
||||
ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
|
||||
Variant *VariantBindingView `json:"variant,omitempty"`
|
||||
Source string `json:"source"`
|
||||
ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
|
||||
Variant *VariantBindingView `json:"variant,omitempty"`
|
||||
Runtime *RuntimeSummaryView `json:"runtime,omitempty"`
|
||||
Presentation *PresentationSummaryView `json:"presentation,omitempty"`
|
||||
ContentBundle *ContentBundleSummaryView `json:"contentBundle,omitempty"`
|
||||
Config struct {
|
||||
ConfigURL string `json:"configUrl"`
|
||||
ConfigLabel string `json:"configLabel"`
|
||||
@@ -110,6 +116,19 @@ func (s *EventService) GetEventDetail(ctx context.Context, eventPublicID string)
|
||||
}
|
||||
}
|
||||
result.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
|
||||
result.Runtime = buildRuntimeSummaryFromEvent(event)
|
||||
result.CurrentPresentation = buildPresentationSummaryFromEvent(event)
|
||||
if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
|
||||
return nil, err
|
||||
} else if enrichedPresentation != nil {
|
||||
result.CurrentPresentation = enrichedPresentation
|
||||
}
|
||||
result.CurrentContentBundle = buildContentBundleSummaryFromEvent(event)
|
||||
if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
|
||||
return nil, err
|
||||
} else if enrichedBundle != nil {
|
||||
result.CurrentContentBundle = enrichedBundle
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -205,6 +224,19 @@ func (s *EventService) LaunchEvent(ctx context.Context, input LaunchEventInput)
|
||||
result.Launch.Source = LaunchSourceEventCurrentRelease
|
||||
result.Launch.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
|
||||
result.Launch.Variant = variant
|
||||
result.Launch.Runtime = buildRuntimeSummaryFromEvent(event)
|
||||
result.Launch.Presentation = buildPresentationSummaryFromEvent(event)
|
||||
if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
|
||||
return nil, err
|
||||
} else if enrichedPresentation != nil {
|
||||
result.Launch.Presentation = enrichedPresentation
|
||||
}
|
||||
result.Launch.ContentBundle = buildContentBundleSummaryFromEvent(event)
|
||||
if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
|
||||
return nil, err
|
||||
} else if enrichedBundle != nil {
|
||||
result.Launch.ContentBundle = enrichedBundle
|
||||
}
|
||||
result.Launch.Config.ConfigURL = *event.ManifestURL
|
||||
result.Launch.Config.ConfigLabel = *event.ConfigLabel
|
||||
result.Launch.Config.ConfigChecksumSha256 = event.ManifestChecksum
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package service
|
||||
|
||||
import "cmr-backend/internal/store/postgres"
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"cmr-backend/internal/store/postgres"
|
||||
)
|
||||
|
||||
const (
|
||||
LaunchSourceEventCurrentRelease = "event_current_release"
|
||||
@@ -18,6 +23,36 @@ type ResolvedReleaseView struct {
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
}
|
||||
|
||||
type RuntimeSummaryView struct {
|
||||
RuntimeBindingID string `json:"runtimeBindingId"`
|
||||
PlaceID string `json:"placeId"`
|
||||
PlaceName *string `json:"placeName,omitempty"`
|
||||
MapID string `json:"mapId"`
|
||||
MapName *string `json:"mapName,omitempty"`
|
||||
TileReleaseID string `json:"tileReleaseId"`
|
||||
CourseSetID string `json:"courseSetId"`
|
||||
CourseVariantID string `json:"courseVariantId"`
|
||||
CourseVariantName *string `json:"courseVariantName,omitempty"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
}
|
||||
|
||||
type PresentationSummaryView struct {
|
||||
PresentationID string `json:"presentationId"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
PresentationType *string `json:"presentationType,omitempty"`
|
||||
TemplateKey *string `json:"templateKey,omitempty"`
|
||||
Version *string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type ContentBundleSummaryView struct {
|
||||
ContentBundleID string `json:"contentBundleId"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
BundleType *string `json:"bundleType,omitempty"`
|
||||
Version *string `json:"version,omitempty"`
|
||||
EntryURL *string `json:"entryUrl,omitempty"`
|
||||
AssetRootURL *string `json:"assetRootUrl,omitempty"`
|
||||
}
|
||||
|
||||
func buildResolvedReleaseFromEvent(event *postgres.Event, source string) *ResolvedReleaseView {
|
||||
if event == nil || event.CurrentReleasePubID == nil || event.ConfigLabel == nil || event.ManifestURL == nil {
|
||||
return nil
|
||||
@@ -35,6 +70,102 @@ func buildResolvedReleaseFromEvent(event *postgres.Event, source string) *Resolv
|
||||
}
|
||||
}
|
||||
|
||||
func buildRuntimeSummaryFromEvent(event *postgres.Event) *RuntimeSummaryView {
|
||||
if event == nil ||
|
||||
event.RuntimeBindingID == nil ||
|
||||
event.PlacePublicID == nil ||
|
||||
event.MapAssetPublicID == nil ||
|
||||
event.TileReleasePublicID == nil ||
|
||||
event.CourseSetPublicID == nil ||
|
||||
event.CourseVariantID == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &RuntimeSummaryView{
|
||||
RuntimeBindingID: *event.RuntimeBindingID,
|
||||
PlaceID: *event.PlacePublicID,
|
||||
PlaceName: event.PlaceName,
|
||||
MapID: *event.MapAssetPublicID,
|
||||
MapName: event.MapAssetName,
|
||||
TileReleaseID: *event.TileReleasePublicID,
|
||||
CourseSetID: *event.CourseSetPublicID,
|
||||
CourseVariantID: *event.CourseVariantID,
|
||||
CourseVariantName: event.CourseVariantName,
|
||||
RouteCode: firstNonNilString(event.RuntimeRouteCode, event.RouteCode),
|
||||
}
|
||||
}
|
||||
|
||||
func buildRuntimeSummaryFromRelease(release *postgres.EventRelease) *RuntimeSummaryView {
|
||||
if release == nil ||
|
||||
release.RuntimeBindingID == nil ||
|
||||
release.PlacePublicID == nil ||
|
||||
release.MapAssetPublicID == nil ||
|
||||
release.TileReleaseID == nil ||
|
||||
release.CourseSetID == nil ||
|
||||
release.CourseVariantID == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &RuntimeSummaryView{
|
||||
RuntimeBindingID: *release.RuntimeBindingID,
|
||||
PlaceID: *release.PlacePublicID,
|
||||
PlaceName: release.PlaceName,
|
||||
MapID: *release.MapAssetPublicID,
|
||||
MapName: release.MapAssetName,
|
||||
TileReleaseID: *release.TileReleaseID,
|
||||
CourseSetID: *release.CourseSetID,
|
||||
CourseVariantID: *release.CourseVariantID,
|
||||
CourseVariantName: release.CourseVariantName,
|
||||
RouteCode: firstNonNilString(release.RuntimeRouteCode, release.RouteCode),
|
||||
}
|
||||
}
|
||||
|
||||
func buildPresentationSummaryFromEvent(event *postgres.Event) *PresentationSummaryView {
|
||||
if event == nil || event.PresentationID == nil {
|
||||
return nil
|
||||
}
|
||||
return &PresentationSummaryView{
|
||||
PresentationID: *event.PresentationID,
|
||||
Name: event.PresentationName,
|
||||
PresentationType: event.PresentationType,
|
||||
}
|
||||
}
|
||||
|
||||
func buildPresentationSummaryFromRelease(release *postgres.EventRelease) *PresentationSummaryView {
|
||||
if release == nil || release.PresentationID == nil {
|
||||
return nil
|
||||
}
|
||||
return &PresentationSummaryView{
|
||||
PresentationID: *release.PresentationID,
|
||||
Name: release.PresentationName,
|
||||
PresentationType: release.PresentationType,
|
||||
}
|
||||
}
|
||||
|
||||
func buildContentBundleSummaryFromEvent(event *postgres.Event) *ContentBundleSummaryView {
|
||||
if event == nil || event.ContentBundleID == nil {
|
||||
return nil
|
||||
}
|
||||
return &ContentBundleSummaryView{
|
||||
ContentBundleID: *event.ContentBundleID,
|
||||
Name: event.ContentBundleName,
|
||||
EntryURL: event.ContentEntryURL,
|
||||
AssetRootURL: event.ContentAssetRootURL,
|
||||
}
|
||||
}
|
||||
|
||||
func buildContentBundleSummaryFromRelease(release *postgres.EventRelease) *ContentBundleSummaryView {
|
||||
if release == nil || release.ContentBundleID == nil {
|
||||
return nil
|
||||
}
|
||||
return &ContentBundleSummaryView{
|
||||
ContentBundleID: *release.ContentBundleID,
|
||||
Name: release.ContentBundleName,
|
||||
EntryURL: release.ContentEntryURL,
|
||||
AssetRootURL: release.ContentAssetURL,
|
||||
}
|
||||
}
|
||||
|
||||
func buildResolvedReleaseFromSession(session *postgres.Session, source string) *ResolvedReleaseView {
|
||||
if session == nil || session.ReleasePublicID == nil || session.ConfigLabel == nil || session.ManifestURL == nil {
|
||||
return nil
|
||||
@@ -54,3 +185,96 @@ func buildResolvedReleaseFromSession(session *postgres.Session, source string) *
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func loadPresentationSummaryByPublicID(ctx context.Context, store *postgres.Store, publicID *string) (*PresentationSummaryView, error) {
|
||||
if store == nil || publicID == nil || strings.TrimSpace(*publicID) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
record, err := store.GetEventPresentationByPublicID(ctx, strings.TrimSpace(*publicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return buildPresentationSummaryFromRecord(record)
|
||||
}
|
||||
|
||||
func loadContentBundleSummaryByPublicID(ctx context.Context, store *postgres.Store, publicID *string) (*ContentBundleSummaryView, error) {
|
||||
if store == nil || publicID == nil || strings.TrimSpace(*publicID) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
record, err := store.GetContentBundleByPublicID(ctx, strings.TrimSpace(*publicID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return buildContentBundleSummaryFromRecord(record)
|
||||
}
|
||||
|
||||
func buildPresentationSummaryFromRecord(record *postgres.EventPresentation) (*PresentationSummaryView, error) {
|
||||
if record == nil {
|
||||
return nil, nil
|
||||
}
|
||||
summary := &PresentationSummaryView{
|
||||
PresentationID: record.PublicID,
|
||||
Name: &record.Name,
|
||||
PresentationType: &record.PresentationType,
|
||||
}
|
||||
schema, err := decodeJSONObject(record.SchemaJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
summary.TemplateKey = readStringField(schema, "templateKey")
|
||||
summary.Version = readStringField(schema, "version")
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func buildContentBundleSummaryFromRecord(record *postgres.ContentBundle) (*ContentBundleSummaryView, error) {
|
||||
if record == nil {
|
||||
return nil, nil
|
||||
}
|
||||
summary := &ContentBundleSummaryView{
|
||||
ContentBundleID: record.PublicID,
|
||||
Name: &record.Name,
|
||||
EntryURL: record.EntryURL,
|
||||
AssetRootURL: record.AssetRootURL,
|
||||
}
|
||||
metadata, err := decodeJSONObject(record.MetadataJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
summary.BundleType = readStringField(metadata, "bundleType")
|
||||
summary.Version = readStringField(metadata, "version")
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func readStringField(object map[string]any, key string) *string {
|
||||
if object == nil {
|
||||
return nil
|
||||
}
|
||||
value, ok := object[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
text, ok := value.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
return &text
|
||||
}
|
||||
|
||||
func firstNonNilString(values ...*string) *string {
|
||||
for _, value := range values {
|
||||
if value != nil {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,21 +16,43 @@ type Tenant struct {
|
||||
}
|
||||
|
||||
type AdminEventRecord struct {
|
||||
ID string
|
||||
PublicID string
|
||||
TenantID *string
|
||||
TenantCode *string
|
||||
TenantName *string
|
||||
Slug string
|
||||
DisplayName string
|
||||
Summary *string
|
||||
Status string
|
||||
CurrentReleaseID *string
|
||||
CurrentReleasePubID *string
|
||||
ConfigLabel *string
|
||||
ManifestURL *string
|
||||
ManifestChecksum *string
|
||||
RouteCode *string
|
||||
ID string
|
||||
PublicID string
|
||||
TenantID *string
|
||||
TenantCode *string
|
||||
TenantName *string
|
||||
Slug string
|
||||
DisplayName string
|
||||
Summary *string
|
||||
Status string
|
||||
CurrentReleaseID *string
|
||||
CurrentReleasePubID *string
|
||||
ConfigLabel *string
|
||||
ManifestURL *string
|
||||
ManifestChecksum *string
|
||||
RouteCode *string
|
||||
PresentationID *string
|
||||
PresentationName *string
|
||||
PresentationType *string
|
||||
ContentBundleID *string
|
||||
ContentBundleName *string
|
||||
ContentEntryURL *string
|
||||
ContentAssetRootURL *string
|
||||
CurrentPresentationID *string
|
||||
CurrentPresentationName *string
|
||||
CurrentPresentationType *string
|
||||
CurrentContentBundleID *string
|
||||
CurrentContentBundleName *string
|
||||
CurrentContentEntryURL *string
|
||||
CurrentContentAssetRootURL *string
|
||||
CurrentRuntimeBindingID *string
|
||||
CurrentPlaceID *string
|
||||
CurrentMapAssetID *string
|
||||
CurrentTileReleaseID *string
|
||||
CurrentCourseSetID *string
|
||||
CurrentCourseVariantID *string
|
||||
CurrentCourseVariantName *string
|
||||
CurrentRuntimeRouteCode *string
|
||||
}
|
||||
|
||||
type CreateAdminEventParams struct {
|
||||
@@ -90,10 +112,42 @@ func (s *Store) ListAdminEvents(ctx context.Context, limit int) ([]AdminEventRec
|
||||
er.config_label,
|
||||
er.manifest_url,
|
||||
er.manifest_checksum_sha256,
|
||||
er.route_code
|
||||
er.route_code,
|
||||
ep.presentation_public_id,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
cb.content_bundle_public_id,
|
||||
cb.name,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url,
|
||||
epc.presentation_public_id,
|
||||
epc.name,
|
||||
epc.presentation_type,
|
||||
cbc.content_bundle_public_id,
|
||||
cbc.name,
|
||||
cbc.entry_url,
|
||||
cbc.asset_root_url,
|
||||
mrb.runtime_binding_public_id,
|
||||
p.place_public_id,
|
||||
ma.map_asset_public_id,
|
||||
tr.tile_release_public_id,
|
||||
cset.course_set_public_id,
|
||||
cv.course_variant_public_id,
|
||||
cv.name,
|
||||
cv.route_code
|
||||
FROM events e
|
||||
LEFT JOIN tenants t ON t.id = e.tenant_id
|
||||
LEFT JOIN event_releases er ON er.id = e.current_release_id
|
||||
LEFT JOIN event_presentations ep ON ep.id = er.presentation_id
|
||||
LEFT JOIN content_bundles cb ON cb.id = er.content_bundle_id
|
||||
LEFT JOIN event_presentations epc ON epc.id = e.current_presentation_id
|
||||
LEFT JOIN content_bundles cbc ON cbc.id = e.current_content_bundle_id
|
||||
LEFT JOIN map_runtime_bindings mrb ON mrb.id = e.current_runtime_binding_id
|
||||
LEFT JOIN places p ON p.id = mrb.place_id
|
||||
LEFT JOIN map_assets ma ON ma.id = mrb.map_asset_id
|
||||
LEFT JOIN tile_releases tr ON tr.id = mrb.tile_release_id
|
||||
LEFT JOIN course_sets cset ON cset.id = mrb.course_set_id
|
||||
LEFT JOIN course_variants cv ON cv.id = mrb.course_variant_id
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT $1
|
||||
`, limit)
|
||||
@@ -133,10 +187,42 @@ func (s *Store) GetAdminEventByPublicID(ctx context.Context, eventPublicID strin
|
||||
er.config_label,
|
||||
er.manifest_url,
|
||||
er.manifest_checksum_sha256,
|
||||
er.route_code
|
||||
er.route_code,
|
||||
ep.presentation_public_id,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
cb.content_bundle_public_id,
|
||||
cb.name,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url,
|
||||
epc.presentation_public_id,
|
||||
epc.name,
|
||||
epc.presentation_type,
|
||||
cbc.content_bundle_public_id,
|
||||
cbc.name,
|
||||
cbc.entry_url,
|
||||
cbc.asset_root_url,
|
||||
mrb.runtime_binding_public_id,
|
||||
p.place_public_id,
|
||||
ma.map_asset_public_id,
|
||||
tr.tile_release_public_id,
|
||||
cset.course_set_public_id,
|
||||
cv.course_variant_public_id,
|
||||
cv.name,
|
||||
cv.route_code
|
||||
FROM events e
|
||||
LEFT JOIN tenants t ON t.id = e.tenant_id
|
||||
LEFT JOIN event_releases er ON er.id = e.current_release_id
|
||||
LEFT JOIN event_presentations ep ON ep.id = er.presentation_id
|
||||
LEFT JOIN content_bundles cb ON cb.id = er.content_bundle_id
|
||||
LEFT JOIN event_presentations epc ON epc.id = e.current_presentation_id
|
||||
LEFT JOIN content_bundles cbc ON cbc.id = e.current_content_bundle_id
|
||||
LEFT JOIN map_runtime_bindings mrb ON mrb.id = e.current_runtime_binding_id
|
||||
LEFT JOIN places p ON p.id = mrb.place_id
|
||||
LEFT JOIN map_assets ma ON ma.id = mrb.map_asset_id
|
||||
LEFT JOIN tile_releases tr ON tr.id = mrb.tile_release_id
|
||||
LEFT JOIN course_sets cset ON cset.id = mrb.course_set_id
|
||||
LEFT JOIN course_variants cv ON cv.id = mrb.course_variant_id
|
||||
WHERE e.event_public_id = $1
|
||||
LIMIT 1
|
||||
`, eventPublicID)
|
||||
@@ -212,6 +298,28 @@ func scanAdminEvent(row pgx.Row) (*AdminEventRecord, error) {
|
||||
&item.ManifestURL,
|
||||
&item.ManifestChecksum,
|
||||
&item.RouteCode,
|
||||
&item.PresentationID,
|
||||
&item.PresentationName,
|
||||
&item.PresentationType,
|
||||
&item.ContentBundleID,
|
||||
&item.ContentBundleName,
|
||||
&item.ContentEntryURL,
|
||||
&item.ContentAssetRootURL,
|
||||
&item.CurrentPresentationID,
|
||||
&item.CurrentPresentationName,
|
||||
&item.CurrentPresentationType,
|
||||
&item.CurrentContentBundleID,
|
||||
&item.CurrentContentBundleName,
|
||||
&item.CurrentContentEntryURL,
|
||||
&item.CurrentContentAssetRootURL,
|
||||
&item.CurrentRuntimeBindingID,
|
||||
&item.CurrentPlaceID,
|
||||
&item.CurrentMapAssetID,
|
||||
&item.CurrentTileReleaseID,
|
||||
&item.CurrentCourseSetID,
|
||||
&item.CurrentCourseVariantID,
|
||||
&item.CurrentCourseVariantName,
|
||||
&item.CurrentRuntimeRouteCode,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
@@ -240,6 +348,28 @@ func scanAdminEventFromRows(rows pgx.Rows) (*AdminEventRecord, error) {
|
||||
&item.ManifestURL,
|
||||
&item.ManifestChecksum,
|
||||
&item.RouteCode,
|
||||
&item.PresentationID,
|
||||
&item.PresentationName,
|
||||
&item.PresentationType,
|
||||
&item.ContentBundleID,
|
||||
&item.ContentBundleName,
|
||||
&item.ContentEntryURL,
|
||||
&item.ContentAssetRootURL,
|
||||
&item.CurrentPresentationID,
|
||||
&item.CurrentPresentationName,
|
||||
&item.CurrentPresentationType,
|
||||
&item.CurrentContentBundleID,
|
||||
&item.CurrentContentBundleName,
|
||||
&item.CurrentContentEntryURL,
|
||||
&item.CurrentContentAssetRootURL,
|
||||
&item.CurrentRuntimeBindingID,
|
||||
&item.CurrentPlaceID,
|
||||
&item.CurrentMapAssetID,
|
||||
&item.CurrentTileReleaseID,
|
||||
&item.CurrentCourseSetID,
|
||||
&item.CurrentCourseVariantID,
|
||||
&item.CurrentCourseVariantName,
|
||||
&item.CurrentRuntimeRouteCode,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan admin event row: %w", err)
|
||||
|
||||
@@ -13,6 +13,13 @@ type DemoBootstrapSummary struct {
|
||||
SourceID string `json:"sourceId"`
|
||||
BuildID string `json:"buildId"`
|
||||
CardID string `json:"cardId"`
|
||||
PlaceID string `json:"placeId"`
|
||||
MapAssetID string `json:"mapAssetId"`
|
||||
TileReleaseID string `json:"tileReleaseId"`
|
||||
CourseSourceID string `json:"courseSourceId"`
|
||||
CourseSetID string `json:"courseSetId"`
|
||||
CourseVariantID string `json:"courseVariantId"`
|
||||
RuntimeBindingID string `json:"runtimeBindingId"`
|
||||
VariantManualEventID string `json:"variantManualEventId"`
|
||||
VariantManualRelease string `json:"variantManualReleaseId"`
|
||||
VariantManualCardID string `json:"variantManualCardId"`
|
||||
@@ -311,6 +318,156 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
||||
return nil, fmt.Errorf("ensure demo card: %w", err)
|
||||
}
|
||||
|
||||
var placeID, placePublicID string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO places (
|
||||
place_public_id, code, name, region, status
|
||||
)
|
||||
VALUES (
|
||||
'place_demo_001', 'place-demo-001', 'Demo Park', 'Shanghai', 'active'
|
||||
)
|
||||
ON CONFLICT (code) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
region = EXCLUDED.region,
|
||||
status = EXCLUDED.status
|
||||
RETURNING id, place_public_id
|
||||
`).Scan(&placeID, &placePublicID); err != nil {
|
||||
return nil, fmt.Errorf("ensure demo place: %w", err)
|
||||
}
|
||||
|
||||
var mapAssetID, mapAssetPublicID string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO map_assets (
|
||||
map_asset_public_id, place_id, code, name, map_type, status
|
||||
)
|
||||
VALUES (
|
||||
'mapasset_demo_001', $1, 'mapasset-demo-001', 'Demo Asset Map', 'standard', 'active'
|
||||
)
|
||||
ON CONFLICT (code) DO UPDATE SET
|
||||
place_id = EXCLUDED.place_id,
|
||||
name = EXCLUDED.name,
|
||||
map_type = EXCLUDED.map_type,
|
||||
status = EXCLUDED.status
|
||||
RETURNING id, map_asset_public_id
|
||||
`, placeID).Scan(&mapAssetID, &mapAssetPublicID); err != nil {
|
||||
return nil, fmt.Errorf("ensure demo map asset: %w", err)
|
||||
}
|
||||
|
||||
var tileReleaseID, tileReleasePublicID string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO tile_releases (
|
||||
tile_release_public_id, map_asset_id, version_code, status, tile_base_url, meta_url, published_at
|
||||
)
|
||||
VALUES (
|
||||
'tile_demo_001', $1, 'v2026-04-03', 'published',
|
||||
'https://example.com/tiles/demo/', 'https://example.com/tiles/demo/meta.json', NOW()
|
||||
)
|
||||
ON CONFLICT (map_asset_id, version_code) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
tile_base_url = EXCLUDED.tile_base_url,
|
||||
meta_url = EXCLUDED.meta_url,
|
||||
published_at = EXCLUDED.published_at
|
||||
RETURNING id, tile_release_public_id
|
||||
`, mapAssetID).Scan(&tileReleaseID, &tileReleasePublicID); err != nil {
|
||||
return nil, fmt.Errorf("ensure demo tile release: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE map_assets
|
||||
SET current_tile_release_id = $2
|
||||
WHERE id = $1
|
||||
`, mapAssetID, tileReleaseID); err != nil {
|
||||
return nil, fmt.Errorf("attach demo tile release: %w", err)
|
||||
}
|
||||
|
||||
var courseSourceID, courseSourcePublicID string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO course_sources (
|
||||
course_source_public_id, source_type, file_url, import_status
|
||||
)
|
||||
VALUES (
|
||||
'csource_demo_001', 'kml', 'https://example.com/course/demo.kml', 'imported'
|
||||
)
|
||||
ON CONFLICT (course_source_public_id) DO UPDATE SET
|
||||
source_type = EXCLUDED.source_type,
|
||||
file_url = EXCLUDED.file_url,
|
||||
import_status = EXCLUDED.import_status
|
||||
RETURNING id, course_source_public_id
|
||||
`).Scan(&courseSourceID, &courseSourcePublicID); err != nil {
|
||||
return nil, fmt.Errorf("ensure demo course source: %w", err)
|
||||
}
|
||||
|
||||
var courseSetID, courseSetPublicID string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO course_sets (
|
||||
course_set_public_id, place_id, map_asset_id, code, mode, name, status
|
||||
)
|
||||
VALUES (
|
||||
'cset_demo_001', $1, $2, 'cset-demo-001', 'classic-sequential', 'Demo Course Set', 'active'
|
||||
)
|
||||
ON CONFLICT (code) DO UPDATE SET
|
||||
place_id = EXCLUDED.place_id,
|
||||
map_asset_id = EXCLUDED.map_asset_id,
|
||||
mode = EXCLUDED.mode,
|
||||
name = EXCLUDED.name,
|
||||
status = EXCLUDED.status
|
||||
RETURNING id, course_set_public_id
|
||||
`, placeID, mapAssetID).Scan(&courseSetID, &courseSetPublicID); err != nil {
|
||||
return nil, fmt.Errorf("ensure demo course set: %w", err)
|
||||
}
|
||||
|
||||
var courseVariantID, courseVariantPublicID string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO course_variants (
|
||||
course_variant_public_id, course_set_id, source_id, name, route_code, mode, control_count, status, is_default
|
||||
)
|
||||
VALUES (
|
||||
'cvariant_demo_001', $1, $2, 'Demo Variant A', 'route-demo-a', 'classic-sequential', 8, 'active', true
|
||||
)
|
||||
ON CONFLICT (course_variant_public_id) DO UPDATE SET
|
||||
course_set_id = EXCLUDED.course_set_id,
|
||||
source_id = EXCLUDED.source_id,
|
||||
name = EXCLUDED.name,
|
||||
route_code = EXCLUDED.route_code,
|
||||
mode = EXCLUDED.mode,
|
||||
control_count = EXCLUDED.control_count,
|
||||
status = EXCLUDED.status,
|
||||
is_default = EXCLUDED.is_default
|
||||
RETURNING id, course_variant_public_id
|
||||
`, courseSetID, courseSourceID).Scan(&courseVariantID, &courseVariantPublicID); err != nil {
|
||||
return nil, fmt.Errorf("ensure demo course variant: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE course_sets
|
||||
SET current_variant_id = $2
|
||||
WHERE id = $1
|
||||
`, courseSetID, courseVariantID); err != nil {
|
||||
return nil, fmt.Errorf("attach demo course variant: %w", err)
|
||||
}
|
||||
|
||||
var runtimeBindingID, runtimeBindingPublicID string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO map_runtime_bindings (
|
||||
runtime_binding_public_id, event_id, place_id, map_asset_id, tile_release_id, course_set_id, course_variant_id, status, notes
|
||||
)
|
||||
VALUES (
|
||||
'runtime_demo_001', $1, $2, $3, $4, $5, $6, 'active', 'demo runtime binding'
|
||||
)
|
||||
ON CONFLICT (runtime_binding_public_id) DO UPDATE SET
|
||||
event_id = EXCLUDED.event_id,
|
||||
place_id = EXCLUDED.place_id,
|
||||
map_asset_id = EXCLUDED.map_asset_id,
|
||||
tile_release_id = EXCLUDED.tile_release_id,
|
||||
course_set_id = EXCLUDED.course_set_id,
|
||||
course_variant_id = EXCLUDED.course_variant_id,
|
||||
status = EXCLUDED.status,
|
||||
notes = EXCLUDED.notes
|
||||
RETURNING id, runtime_binding_public_id
|
||||
`, eventID, placeID, mapAssetID, tileReleaseID, courseSetID, courseVariantID).Scan(&runtimeBindingID, &runtimeBindingPublicID); err != nil {
|
||||
return nil, fmt.Errorf("ensure demo runtime binding: %w", err)
|
||||
}
|
||||
|
||||
var manualEventID string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO events (
|
||||
@@ -452,6 +609,13 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
||||
SourceID: source.ID,
|
||||
BuildID: build.ID,
|
||||
CardID: cardPublicID,
|
||||
PlaceID: placePublicID,
|
||||
MapAssetID: mapAssetPublicID,
|
||||
TileReleaseID: tileReleasePublicID,
|
||||
CourseSourceID: courseSourcePublicID,
|
||||
CourseSetID: courseSetPublicID,
|
||||
CourseVariantID: courseVariantPublicID,
|
||||
RuntimeBindingID: runtimeBindingPublicID,
|
||||
VariantManualEventID: "evt_demo_variant_manual_001",
|
||||
VariantManualRelease: manualReleaseRow.PublicID,
|
||||
VariantManualCardID: manualCardPublicID,
|
||||
|
||||
560
backend/internal/store/postgres/event_ops_store.go
Normal file
560
backend/internal/store/postgres/event_ops_store.go
Normal file
@@ -0,0 +1,560 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type EventPresentation struct {
|
||||
ID string
|
||||
PublicID string
|
||||
EventID string
|
||||
EventPublicID string
|
||||
Code string
|
||||
Name string
|
||||
PresentationType string
|
||||
Status string
|
||||
IsDefault bool
|
||||
SchemaJSON string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
type ContentBundle struct {
|
||||
ID string
|
||||
PublicID string
|
||||
EventID string
|
||||
EventPublicID string
|
||||
Code string
|
||||
Name string
|
||||
Status string
|
||||
IsDefault bool
|
||||
EntryURL *string
|
||||
AssetRootURL *string
|
||||
MetadataJSON string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
type CreateEventPresentationParams struct {
|
||||
PublicID string
|
||||
EventID string
|
||||
Code string
|
||||
Name string
|
||||
PresentationType string
|
||||
Status string
|
||||
IsDefault bool
|
||||
SchemaJSON string
|
||||
}
|
||||
|
||||
type CreateContentBundleParams struct {
|
||||
PublicID string
|
||||
EventID string
|
||||
Code string
|
||||
Name string
|
||||
Status string
|
||||
IsDefault bool
|
||||
EntryURL *string
|
||||
AssetRootURL *string
|
||||
MetadataJSON string
|
||||
}
|
||||
|
||||
type EventDefaultBindings struct {
|
||||
EventID string
|
||||
EventPublicID string
|
||||
PresentationID *string
|
||||
PresentationPublicID *string
|
||||
PresentationName *string
|
||||
PresentationType *string
|
||||
ContentBundleID *string
|
||||
ContentBundlePublicID *string
|
||||
ContentBundleName *string
|
||||
ContentEntryURL *string
|
||||
ContentAssetRootURL *string
|
||||
RuntimeBindingID *string
|
||||
RuntimeBindingPublicID *string
|
||||
PlacePublicID *string
|
||||
PlaceName *string
|
||||
MapAssetPublicID *string
|
||||
MapAssetName *string
|
||||
TileReleasePublicID *string
|
||||
CourseSetPublicID *string
|
||||
CourseVariantPublicID *string
|
||||
CourseVariantName *string
|
||||
RuntimeRouteCode *string
|
||||
}
|
||||
|
||||
type SetEventDefaultBindingsParams struct {
|
||||
EventID string
|
||||
PresentationID *string
|
||||
ContentBundleID *string
|
||||
RuntimeBindingID *string
|
||||
UpdatePresentation bool
|
||||
UpdateContent bool
|
||||
UpdateRuntime bool
|
||||
}
|
||||
|
||||
func (s *Store) ListEventPresentationsByEventID(ctx context.Context, eventID string, limit int) ([]EventPresentation, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT
|
||||
ep.id,
|
||||
ep.presentation_public_id,
|
||||
ep.event_id,
|
||||
e.event_public_id,
|
||||
ep.code,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
ep.status,
|
||||
ep.is_default,
|
||||
ep.schema_jsonb::text,
|
||||
ep.created_at::text,
|
||||
ep.updated_at::text
|
||||
FROM event_presentations ep
|
||||
JOIN events e ON e.id = ep.event_id
|
||||
WHERE ep.event_id = $1
|
||||
ORDER BY ep.is_default DESC, ep.created_at DESC
|
||||
LIMIT $2
|
||||
`, eventID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list event presentations: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []EventPresentation{}
|
||||
for rows.Next() {
|
||||
item, err := scanEventPresentationFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate event presentations: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetEventPresentationByPublicID(ctx context.Context, publicID string) (*EventPresentation, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
ep.id,
|
||||
ep.presentation_public_id,
|
||||
ep.event_id,
|
||||
e.event_public_id,
|
||||
ep.code,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
ep.status,
|
||||
ep.is_default,
|
||||
ep.schema_jsonb::text,
|
||||
ep.created_at::text,
|
||||
ep.updated_at::text
|
||||
FROM event_presentations ep
|
||||
JOIN events e ON e.id = ep.event_id
|
||||
WHERE ep.presentation_public_id = $1
|
||||
LIMIT 1
|
||||
`, publicID)
|
||||
return scanEventPresentation(row)
|
||||
}
|
||||
|
||||
func (s *Store) GetDefaultEventPresentationByEventID(ctx context.Context, eventID string) (*EventPresentation, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
ep.id,
|
||||
ep.presentation_public_id,
|
||||
ep.event_id,
|
||||
e.event_public_id,
|
||||
ep.code,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
ep.status,
|
||||
ep.is_default,
|
||||
ep.schema_jsonb::text,
|
||||
ep.created_at::text,
|
||||
ep.updated_at::text
|
||||
FROM event_presentations ep
|
||||
JOIN events e ON e.id = ep.event_id
|
||||
WHERE ep.event_id = $1
|
||||
AND ep.status = 'active'
|
||||
ORDER BY ep.is_default DESC, ep.updated_at DESC, ep.created_at DESC
|
||||
LIMIT 1
|
||||
`, eventID)
|
||||
return scanEventPresentation(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateEventPresentation(ctx context.Context, tx Tx, params CreateEventPresentationParams) (*EventPresentation, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO event_presentations (
|
||||
presentation_public_id,
|
||||
event_id,
|
||||
code,
|
||||
name,
|
||||
presentation_type,
|
||||
status,
|
||||
is_default,
|
||||
schema_jsonb
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)
|
||||
RETURNING
|
||||
id,
|
||||
presentation_public_id,
|
||||
event_id,
|
||||
code,
|
||||
name,
|
||||
presentation_type,
|
||||
status,
|
||||
is_default,
|
||||
schema_jsonb::text,
|
||||
created_at::text,
|
||||
updated_at::text
|
||||
`, params.PublicID, params.EventID, params.Code, params.Name, params.PresentationType, params.Status, params.IsDefault, params.SchemaJSON)
|
||||
|
||||
var item EventPresentation
|
||||
if err := row.Scan(
|
||||
&item.ID,
|
||||
&item.PublicID,
|
||||
&item.EventID,
|
||||
&item.Code,
|
||||
&item.Name,
|
||||
&item.PresentationType,
|
||||
&item.Status,
|
||||
&item.IsDefault,
|
||||
&item.SchemaJSON,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("create event presentation: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetEventDefaultBindingsByEventID(ctx context.Context, eventID string) (*EventDefaultBindings, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
e.id,
|
||||
e.event_public_id,
|
||||
e.current_presentation_id,
|
||||
ep.presentation_public_id,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
e.current_content_bundle_id,
|
||||
cb.content_bundle_public_id,
|
||||
cb.name,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url,
|
||||
e.current_runtime_binding_id,
|
||||
mrb.runtime_binding_public_id,
|
||||
p.place_public_id,
|
||||
p.name,
|
||||
ma.map_asset_public_id,
|
||||
ma.name,
|
||||
tr.tile_release_public_id,
|
||||
cset.course_set_public_id,
|
||||
cv.course_variant_public_id,
|
||||
cv.name,
|
||||
cv.route_code
|
||||
FROM events e
|
||||
LEFT JOIN event_presentations ep ON ep.id = e.current_presentation_id
|
||||
LEFT JOIN content_bundles cb ON cb.id = e.current_content_bundle_id
|
||||
LEFT JOIN map_runtime_bindings mrb ON mrb.id = e.current_runtime_binding_id
|
||||
LEFT JOIN places p ON p.id = mrb.place_id
|
||||
LEFT JOIN map_assets ma ON ma.id = mrb.map_asset_id
|
||||
LEFT JOIN tile_releases tr ON tr.id = mrb.tile_release_id
|
||||
LEFT JOIN course_sets cset ON cset.id = mrb.course_set_id
|
||||
LEFT JOIN course_variants cv ON cv.id = mrb.course_variant_id
|
||||
WHERE e.id = $1
|
||||
LIMIT 1
|
||||
`, eventID)
|
||||
return scanEventDefaultBindings(row)
|
||||
}
|
||||
|
||||
func (s *Store) SetEventDefaultBindings(ctx context.Context, tx Tx, params SetEventDefaultBindingsParams) error {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE events
|
||||
SET current_presentation_id = CASE WHEN $5 THEN $2 ELSE current_presentation_id END,
|
||||
current_content_bundle_id = CASE WHEN $6 THEN $3 ELSE current_content_bundle_id END,
|
||||
current_runtime_binding_id = CASE WHEN $7 THEN $4 ELSE current_runtime_binding_id END
|
||||
WHERE id = $1
|
||||
`, params.EventID, params.PresentationID, params.ContentBundleID, params.RuntimeBindingID, params.UpdatePresentation, params.UpdateContent, params.UpdateRuntime); err != nil {
|
||||
return fmt.Errorf("set event default bindings: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) ListContentBundlesByEventID(ctx context.Context, eventID string, limit int) ([]ContentBundle, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT
|
||||
cb.id,
|
||||
cb.content_bundle_public_id,
|
||||
cb.event_id,
|
||||
e.event_public_id,
|
||||
cb.code,
|
||||
cb.name,
|
||||
cb.status,
|
||||
cb.is_default,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url,
|
||||
cb.metadata_jsonb::text,
|
||||
cb.created_at::text,
|
||||
cb.updated_at::text
|
||||
FROM content_bundles cb
|
||||
JOIN events e ON e.id = cb.event_id
|
||||
WHERE cb.event_id = $1
|
||||
ORDER BY cb.is_default DESC, cb.created_at DESC
|
||||
LIMIT $2
|
||||
`, eventID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list content bundles: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []ContentBundle{}
|
||||
for rows.Next() {
|
||||
item, err := scanContentBundleFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate content bundles: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetContentBundleByPublicID(ctx context.Context, publicID string) (*ContentBundle, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
cb.id,
|
||||
cb.content_bundle_public_id,
|
||||
cb.event_id,
|
||||
e.event_public_id,
|
||||
cb.code,
|
||||
cb.name,
|
||||
cb.status,
|
||||
cb.is_default,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url,
|
||||
cb.metadata_jsonb::text,
|
||||
cb.created_at::text,
|
||||
cb.updated_at::text
|
||||
FROM content_bundles cb
|
||||
JOIN events e ON e.id = cb.event_id
|
||||
WHERE cb.content_bundle_public_id = $1
|
||||
LIMIT 1
|
||||
`, publicID)
|
||||
return scanContentBundle(row)
|
||||
}
|
||||
|
||||
func (s *Store) GetDefaultContentBundleByEventID(ctx context.Context, eventID string) (*ContentBundle, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
cb.id,
|
||||
cb.content_bundle_public_id,
|
||||
cb.event_id,
|
||||
e.event_public_id,
|
||||
cb.code,
|
||||
cb.name,
|
||||
cb.status,
|
||||
cb.is_default,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url,
|
||||
cb.metadata_jsonb::text,
|
||||
cb.created_at::text,
|
||||
cb.updated_at::text
|
||||
FROM content_bundles cb
|
||||
JOIN events e ON e.id = cb.event_id
|
||||
WHERE cb.event_id = $1
|
||||
AND cb.status = 'active'
|
||||
ORDER BY cb.is_default DESC, cb.updated_at DESC, cb.created_at DESC
|
||||
LIMIT 1
|
||||
`, eventID)
|
||||
return scanContentBundle(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateContentBundle(ctx context.Context, tx Tx, params CreateContentBundleParams) (*ContentBundle, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO content_bundles (
|
||||
content_bundle_public_id,
|
||||
event_id,
|
||||
code,
|
||||
name,
|
||||
status,
|
||||
is_default,
|
||||
entry_url,
|
||||
asset_root_url,
|
||||
metadata_jsonb
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)
|
||||
RETURNING
|
||||
id,
|
||||
content_bundle_public_id,
|
||||
event_id,
|
||||
code,
|
||||
name,
|
||||
status,
|
||||
is_default,
|
||||
entry_url,
|
||||
asset_root_url,
|
||||
metadata_jsonb::text,
|
||||
created_at::text,
|
||||
updated_at::text
|
||||
`, params.PublicID, params.EventID, params.Code, params.Name, params.Status, params.IsDefault, params.EntryURL, params.AssetRootURL, params.MetadataJSON)
|
||||
|
||||
var item ContentBundle
|
||||
if err := row.Scan(
|
||||
&item.ID,
|
||||
&item.PublicID,
|
||||
&item.EventID,
|
||||
&item.Code,
|
||||
&item.Name,
|
||||
&item.Status,
|
||||
&item.IsDefault,
|
||||
&item.EntryURL,
|
||||
&item.AssetRootURL,
|
||||
&item.MetadataJSON,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("create content bundle: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanEventPresentation(row pgx.Row) (*EventPresentation, error) {
|
||||
var item EventPresentation
|
||||
err := row.Scan(
|
||||
&item.ID,
|
||||
&item.PublicID,
|
||||
&item.EventID,
|
||||
&item.EventPublicID,
|
||||
&item.Code,
|
||||
&item.Name,
|
||||
&item.PresentationType,
|
||||
&item.Status,
|
||||
&item.IsDefault,
|
||||
&item.SchemaJSON,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan event presentation: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanEventPresentationFromRows(rows pgx.Rows) (*EventPresentation, error) {
|
||||
var item EventPresentation
|
||||
if err := rows.Scan(
|
||||
&item.ID,
|
||||
&item.PublicID,
|
||||
&item.EventID,
|
||||
&item.EventPublicID,
|
||||
&item.Code,
|
||||
&item.Name,
|
||||
&item.PresentationType,
|
||||
&item.Status,
|
||||
&item.IsDefault,
|
||||
&item.SchemaJSON,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan event presentation row: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanContentBundle(row pgx.Row) (*ContentBundle, error) {
|
||||
var item ContentBundle
|
||||
err := row.Scan(
|
||||
&item.ID,
|
||||
&item.PublicID,
|
||||
&item.EventID,
|
||||
&item.EventPublicID,
|
||||
&item.Code,
|
||||
&item.Name,
|
||||
&item.Status,
|
||||
&item.IsDefault,
|
||||
&item.EntryURL,
|
||||
&item.AssetRootURL,
|
||||
&item.MetadataJSON,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan content bundle: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanContentBundleFromRows(rows pgx.Rows) (*ContentBundle, error) {
|
||||
var item ContentBundle
|
||||
if err := rows.Scan(
|
||||
&item.ID,
|
||||
&item.PublicID,
|
||||
&item.EventID,
|
||||
&item.EventPublicID,
|
||||
&item.Code,
|
||||
&item.Name,
|
||||
&item.Status,
|
||||
&item.IsDefault,
|
||||
&item.EntryURL,
|
||||
&item.AssetRootURL,
|
||||
&item.MetadataJSON,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan content bundle row: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanEventDefaultBindings(row pgx.Row) (*EventDefaultBindings, error) {
|
||||
var item EventDefaultBindings
|
||||
err := row.Scan(
|
||||
&item.EventID,
|
||||
&item.EventPublicID,
|
||||
&item.PresentationID,
|
||||
&item.PresentationPublicID,
|
||||
&item.PresentationName,
|
||||
&item.PresentationType,
|
||||
&item.ContentBundleID,
|
||||
&item.ContentBundlePublicID,
|
||||
&item.ContentBundleName,
|
||||
&item.ContentEntryURL,
|
||||
&item.ContentAssetRootURL,
|
||||
&item.RuntimeBindingID,
|
||||
&item.RuntimeBindingPublicID,
|
||||
&item.PlacePublicID,
|
||||
&item.PlaceName,
|
||||
&item.MapAssetPublicID,
|
||||
&item.MapAssetName,
|
||||
&item.TileReleasePublicID,
|
||||
&item.CourseSetPublicID,
|
||||
&item.CourseVariantPublicID,
|
||||
&item.CourseVariantName,
|
||||
&item.RuntimeRouteCode,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan event default bindings: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
@@ -23,20 +23,54 @@ type Event struct {
|
||||
ManifestChecksum *string
|
||||
RouteCode *string
|
||||
ReleasePayloadJSON *string
|
||||
RuntimeBindingID *string
|
||||
PlacePublicID *string
|
||||
PlaceName *string
|
||||
MapAssetPublicID *string
|
||||
MapAssetName *string
|
||||
TileReleasePublicID *string
|
||||
CourseSetPublicID *string
|
||||
CourseVariantID *string
|
||||
CourseVariantName *string
|
||||
RuntimeRouteCode *string
|
||||
PresentationID *string
|
||||
PresentationName *string
|
||||
PresentationType *string
|
||||
ContentBundleID *string
|
||||
ContentBundleName *string
|
||||
ContentEntryURL *string
|
||||
ContentAssetRootURL *string
|
||||
}
|
||||
|
||||
type EventRelease struct {
|
||||
ID string
|
||||
PublicID string
|
||||
EventID string
|
||||
ReleaseNo int
|
||||
ConfigLabel string
|
||||
ManifestURL string
|
||||
ManifestChecksum *string
|
||||
RouteCode *string
|
||||
BuildID *string
|
||||
Status string
|
||||
PublishedAt time.Time
|
||||
ID string
|
||||
PublicID string
|
||||
EventID string
|
||||
ReleaseNo int
|
||||
ConfigLabel string
|
||||
ManifestURL string
|
||||
ManifestChecksum *string
|
||||
RouteCode *string
|
||||
BuildID *string
|
||||
Status string
|
||||
PublishedAt time.Time
|
||||
RuntimeBindingID *string
|
||||
PlacePublicID *string
|
||||
PlaceName *string
|
||||
MapAssetPublicID *string
|
||||
MapAssetName *string
|
||||
TileReleaseID *string
|
||||
CourseSetID *string
|
||||
CourseVariantID *string
|
||||
CourseVariantName *string
|
||||
RuntimeRouteCode *string
|
||||
PresentationID *string
|
||||
PresentationName *string
|
||||
PresentationType *string
|
||||
ContentBundleID *string
|
||||
ContentBundleName *string
|
||||
ContentEntryURL *string
|
||||
ContentAssetURL *string
|
||||
}
|
||||
|
||||
type CreateGameSessionParams struct {
|
||||
@@ -85,9 +119,34 @@ func (s *Store) GetEventByPublicID(ctx context.Context, eventPublicID string) (*
|
||||
er.manifest_url,
|
||||
er.manifest_checksum_sha256,
|
||||
er.route_code,
|
||||
er.payload_jsonb::text
|
||||
er.payload_jsonb::text,
|
||||
mrb.runtime_binding_public_id,
|
||||
p.place_public_id,
|
||||
p.name,
|
||||
ma.map_asset_public_id,
|
||||
ma.name,
|
||||
tr.tile_release_public_id,
|
||||
cset.course_set_public_id,
|
||||
cv.course_variant_public_id,
|
||||
cv.name,
|
||||
cv.route_code,
|
||||
ep.presentation_public_id,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
cb.content_bundle_public_id,
|
||||
cb.name,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url
|
||||
FROM events e
|
||||
LEFT JOIN event_releases er ON er.id = e.current_release_id
|
||||
LEFT JOIN map_runtime_bindings mrb ON mrb.id = er.runtime_binding_id
|
||||
LEFT JOIN places p ON p.id = mrb.place_id
|
||||
LEFT JOIN map_assets ma ON ma.id = mrb.map_asset_id
|
||||
LEFT JOIN tile_releases tr ON tr.id = mrb.tile_release_id
|
||||
LEFT JOIN course_sets cset ON cset.id = mrb.course_set_id
|
||||
LEFT JOIN course_variants cv ON cv.id = mrb.course_variant_id
|
||||
LEFT JOIN event_presentations ep ON ep.id = er.presentation_id
|
||||
LEFT JOIN content_bundles cb ON cb.id = er.content_bundle_id
|
||||
WHERE e.event_public_id = $1
|
||||
LIMIT 1
|
||||
`, eventPublicID)
|
||||
@@ -107,6 +166,23 @@ func (s *Store) GetEventByPublicID(ctx context.Context, eventPublicID string) (*
|
||||
&event.ManifestChecksum,
|
||||
&event.RouteCode,
|
||||
&event.ReleasePayloadJSON,
|
||||
&event.RuntimeBindingID,
|
||||
&event.PlacePublicID,
|
||||
&event.PlaceName,
|
||||
&event.MapAssetPublicID,
|
||||
&event.MapAssetName,
|
||||
&event.TileReleasePublicID,
|
||||
&event.CourseSetPublicID,
|
||||
&event.CourseVariantID,
|
||||
&event.CourseVariantName,
|
||||
&event.RuntimeRouteCode,
|
||||
&event.PresentationID,
|
||||
&event.PresentationName,
|
||||
&event.PresentationType,
|
||||
&event.ContentBundleID,
|
||||
&event.ContentBundleName,
|
||||
&event.ContentEntryURL,
|
||||
&event.ContentAssetRootURL,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
@@ -132,9 +208,34 @@ func (s *Store) GetEventByID(ctx context.Context, eventID string) (*Event, error
|
||||
er.manifest_url,
|
||||
er.manifest_checksum_sha256,
|
||||
er.route_code,
|
||||
er.payload_jsonb::text
|
||||
er.payload_jsonb::text,
|
||||
mrb.runtime_binding_public_id,
|
||||
p.place_public_id,
|
||||
p.name,
|
||||
ma.map_asset_public_id,
|
||||
ma.name,
|
||||
tr.tile_release_public_id,
|
||||
cset.course_set_public_id,
|
||||
cv.course_variant_public_id,
|
||||
cv.name,
|
||||
cv.route_code,
|
||||
ep.presentation_public_id,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
cb.content_bundle_public_id,
|
||||
cb.name,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url
|
||||
FROM events e
|
||||
LEFT JOIN event_releases er ON er.id = e.current_release_id
|
||||
LEFT JOIN map_runtime_bindings mrb ON mrb.id = er.runtime_binding_id
|
||||
LEFT JOIN places p ON p.id = mrb.place_id
|
||||
LEFT JOIN map_assets ma ON ma.id = mrb.map_asset_id
|
||||
LEFT JOIN tile_releases tr ON tr.id = mrb.tile_release_id
|
||||
LEFT JOIN course_sets cset ON cset.id = mrb.course_set_id
|
||||
LEFT JOIN course_variants cv ON cv.id = mrb.course_variant_id
|
||||
LEFT JOIN event_presentations ep ON ep.id = er.presentation_id
|
||||
LEFT JOIN content_bundles cb ON cb.id = er.content_bundle_id
|
||||
WHERE e.id = $1
|
||||
LIMIT 1
|
||||
`, eventID)
|
||||
@@ -154,6 +255,23 @@ func (s *Store) GetEventByID(ctx context.Context, eventID string) (*Event, error
|
||||
&event.ManifestChecksum,
|
||||
&event.RouteCode,
|
||||
&event.ReleasePayloadJSON,
|
||||
&event.RuntimeBindingID,
|
||||
&event.PlacePublicID,
|
||||
&event.PlaceName,
|
||||
&event.MapAssetPublicID,
|
||||
&event.MapAssetName,
|
||||
&event.TileReleasePublicID,
|
||||
&event.CourseSetPublicID,
|
||||
&event.CourseVariantID,
|
||||
&event.CourseVariantName,
|
||||
&event.RuntimeRouteCode,
|
||||
&event.PresentationID,
|
||||
&event.PresentationName,
|
||||
&event.PresentationType,
|
||||
&event.ContentBundleID,
|
||||
&event.ContentBundleName,
|
||||
&event.ContentEntryURL,
|
||||
&event.ContentAssetRootURL,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
@@ -168,7 +286,7 @@ func (s *Store) NextEventReleaseNo(ctx context.Context, eventID string) (int, er
|
||||
var next int
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT COALESCE(MAX(release_no), 0) + 1
|
||||
FROM event_releases
|
||||
FROM event_releases er
|
||||
WHERE event_id = $1
|
||||
`, eventID).Scan(&next); err != nil {
|
||||
return 0, fmt.Errorf("next event release no: %w", err)
|
||||
@@ -185,6 +303,9 @@ type CreateEventReleaseParams struct {
|
||||
ManifestChecksum *string
|
||||
RouteCode *string
|
||||
BuildID *string
|
||||
RuntimeBindingID *string
|
||||
PresentationID *string
|
||||
ContentBundleID *string
|
||||
Status string
|
||||
PayloadJSON string
|
||||
}
|
||||
@@ -200,12 +321,15 @@ func (s *Store) CreateEventRelease(ctx context.Context, tx Tx, params CreateEven
|
||||
manifest_checksum_sha256,
|
||||
route_code,
|
||||
build_id,
|
||||
runtime_binding_id,
|
||||
presentation_id,
|
||||
content_bundle_id,
|
||||
status,
|
||||
payload_jsonb
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb)
|
||||
RETURNING id, release_public_id, event_id, release_no, config_label, manifest_url, manifest_checksum_sha256, route_code, build_id, status, published_at
|
||||
`, params.PublicID, params.EventID, params.ReleaseNo, params.ConfigLabel, params.ManifestURL, params.ManifestChecksum, params.RouteCode, params.BuildID, params.Status, params.PayloadJSON)
|
||||
`, params.PublicID, params.EventID, params.ReleaseNo, params.ConfigLabel, params.ManifestURL, params.ManifestChecksum, params.RouteCode, params.BuildID, params.RuntimeBindingID, params.PresentationID, params.ContentBundleID, params.Status, params.PayloadJSON)
|
||||
|
||||
var item EventRelease
|
||||
if err := row.Scan(
|
||||
@@ -284,10 +408,46 @@ func (s *Store) ListEventReleasesByEventID(ctx context.Context, eventID string,
|
||||
limit = 20
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, release_public_id, event_id, release_no, config_label, manifest_url, manifest_checksum_sha256, route_code, build_id, status, published_at
|
||||
FROM event_releases
|
||||
WHERE event_id = $1
|
||||
ORDER BY release_no DESC
|
||||
SELECT
|
||||
er.id,
|
||||
er.release_public_id,
|
||||
er.event_id,
|
||||
er.release_no,
|
||||
er.config_label,
|
||||
er.manifest_url,
|
||||
er.manifest_checksum_sha256,
|
||||
er.route_code,
|
||||
er.build_id,
|
||||
er.status,
|
||||
er.published_at,
|
||||
mrb.runtime_binding_public_id,
|
||||
p.place_public_id,
|
||||
p.name,
|
||||
ma.map_asset_public_id,
|
||||
ma.name,
|
||||
tr.tile_release_public_id,
|
||||
cset.course_set_public_id,
|
||||
cv.course_variant_public_id,
|
||||
cv.name,
|
||||
cv.route_code,
|
||||
ep.presentation_public_id,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
cb.content_bundle_public_id,
|
||||
cb.name,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url
|
||||
FROM event_releases er
|
||||
LEFT JOIN map_runtime_bindings mrb ON mrb.id = er.runtime_binding_id
|
||||
LEFT JOIN places p ON p.id = mrb.place_id
|
||||
LEFT JOIN map_assets ma ON ma.id = mrb.map_asset_id
|
||||
LEFT JOIN tile_releases tr ON tr.id = mrb.tile_release_id
|
||||
LEFT JOIN course_sets cset ON cset.id = mrb.course_set_id
|
||||
LEFT JOIN course_variants cv ON cv.id = mrb.course_variant_id
|
||||
LEFT JOIN event_presentations ep ON ep.id = er.presentation_id
|
||||
LEFT JOIN content_bundles cb ON cb.id = er.content_bundle_id
|
||||
WHERE er.event_id = $1
|
||||
ORDER BY er.release_no DESC
|
||||
LIMIT $2
|
||||
`, eventID, limit)
|
||||
if err != nil {
|
||||
@@ -311,9 +471,45 @@ func (s *Store) ListEventReleasesByEventID(ctx context.Context, eventID string,
|
||||
|
||||
func (s *Store) GetEventReleaseByPublicID(ctx context.Context, releasePublicID string) (*EventRelease, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT id, release_public_id, event_id, release_no, config_label, manifest_url, manifest_checksum_sha256, route_code, build_id, status, published_at
|
||||
FROM event_releases
|
||||
WHERE release_public_id = $1
|
||||
SELECT
|
||||
er.id,
|
||||
er.release_public_id,
|
||||
er.event_id,
|
||||
er.release_no,
|
||||
er.config_label,
|
||||
er.manifest_url,
|
||||
er.manifest_checksum_sha256,
|
||||
er.route_code,
|
||||
er.build_id,
|
||||
er.status,
|
||||
er.published_at,
|
||||
mrb.runtime_binding_public_id,
|
||||
p.place_public_id,
|
||||
p.name,
|
||||
ma.map_asset_public_id,
|
||||
ma.name,
|
||||
tr.tile_release_public_id,
|
||||
cset.course_set_public_id,
|
||||
cv.course_variant_public_id,
|
||||
cv.name,
|
||||
cv.route_code,
|
||||
ep.presentation_public_id,
|
||||
ep.name,
|
||||
ep.presentation_type,
|
||||
cb.content_bundle_public_id,
|
||||
cb.name,
|
||||
cb.entry_url,
|
||||
cb.asset_root_url
|
||||
FROM event_releases er
|
||||
LEFT JOIN map_runtime_bindings mrb ON mrb.id = er.runtime_binding_id
|
||||
LEFT JOIN places p ON p.id = mrb.place_id
|
||||
LEFT JOIN map_assets ma ON ma.id = mrb.map_asset_id
|
||||
LEFT JOIN tile_releases tr ON tr.id = mrb.tile_release_id
|
||||
LEFT JOIN course_sets cset ON cset.id = mrb.course_set_id
|
||||
LEFT JOIN course_variants cv ON cv.id = mrb.course_variant_id
|
||||
LEFT JOIN event_presentations ep ON ep.id = er.presentation_id
|
||||
LEFT JOIN content_bundles cb ON cb.id = er.content_bundle_id
|
||||
WHERE er.release_public_id = $1
|
||||
LIMIT 1
|
||||
`, releasePublicID)
|
||||
|
||||
@@ -330,6 +526,23 @@ func (s *Store) GetEventReleaseByPublicID(ctx context.Context, releasePublicID s
|
||||
&item.BuildID,
|
||||
&item.Status,
|
||||
&item.PublishedAt,
|
||||
&item.RuntimeBindingID,
|
||||
&item.PlacePublicID,
|
||||
&item.PlaceName,
|
||||
&item.MapAssetPublicID,
|
||||
&item.MapAssetName,
|
||||
&item.TileReleaseID,
|
||||
&item.CourseSetID,
|
||||
&item.CourseVariantID,
|
||||
&item.CourseVariantName,
|
||||
&item.RuntimeRouteCode,
|
||||
&item.PresentationID,
|
||||
&item.PresentationName,
|
||||
&item.PresentationType,
|
||||
&item.ContentBundleID,
|
||||
&item.ContentBundleName,
|
||||
&item.ContentEntryURL,
|
||||
&item.ContentAssetURL,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
@@ -354,9 +567,37 @@ func scanEventReleaseFromRows(rows pgx.Rows) (*EventRelease, error) {
|
||||
&item.BuildID,
|
||||
&item.Status,
|
||||
&item.PublishedAt,
|
||||
&item.RuntimeBindingID,
|
||||
&item.PlacePublicID,
|
||||
&item.PlaceName,
|
||||
&item.MapAssetPublicID,
|
||||
&item.MapAssetName,
|
||||
&item.TileReleaseID,
|
||||
&item.CourseSetID,
|
||||
&item.CourseVariantID,
|
||||
&item.CourseVariantName,
|
||||
&item.RuntimeRouteCode,
|
||||
&item.PresentationID,
|
||||
&item.PresentationName,
|
||||
&item.PresentationType,
|
||||
&item.ContentBundleID,
|
||||
&item.ContentBundleName,
|
||||
&item.ContentEntryURL,
|
||||
&item.ContentAssetURL,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan event release row: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (s *Store) SetEventReleaseRuntimeBinding(ctx context.Context, tx Tx, releaseID string, runtimeBindingID *string) error {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE event_releases
|
||||
SET runtime_binding_id = $2
|
||||
WHERE id = $1
|
||||
`, releaseID, runtimeBindingID); err != nil {
|
||||
return fmt.Errorf("set event release runtime binding: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
822
backend/internal/store/postgres/production_store.go
Normal file
822
backend/internal/store/postgres/production_store.go
Normal file
@@ -0,0 +1,822 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type Place struct {
|
||||
ID string
|
||||
PublicID string
|
||||
Code string
|
||||
Name string
|
||||
Region *string
|
||||
CoverURL *string
|
||||
Description *string
|
||||
CenterPoint json.RawMessage
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type MapAsset struct {
|
||||
ID string
|
||||
PublicID string
|
||||
PlaceID string
|
||||
LegacyMapID *string
|
||||
LegacyMapPublicID *string
|
||||
Code string
|
||||
Name string
|
||||
MapType string
|
||||
CoverURL *string
|
||||
Description *string
|
||||
Status string
|
||||
CurrentTileReleaseID *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type TileRelease struct {
|
||||
ID string
|
||||
PublicID string
|
||||
MapAssetID string
|
||||
LegacyMapVersionID *string
|
||||
LegacyMapVersionPub *string
|
||||
VersionCode string
|
||||
Status string
|
||||
TileBaseURL string
|
||||
MetaURL string
|
||||
PublishedAssetRoot *string
|
||||
MetadataJSON json.RawMessage
|
||||
PublishedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type CourseSource struct {
|
||||
ID string
|
||||
PublicID string
|
||||
LegacyPlayfieldVersionID *string
|
||||
LegacyPlayfieldVersionPub *string
|
||||
SourceType string
|
||||
FileURL string
|
||||
Checksum *string
|
||||
ParserVersion *string
|
||||
ImportStatus string
|
||||
MetadataJSON json.RawMessage
|
||||
ImportedAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type CourseSet struct {
|
||||
ID string
|
||||
PublicID string
|
||||
PlaceID string
|
||||
MapAssetID string
|
||||
Code string
|
||||
Mode string
|
||||
Name string
|
||||
Description *string
|
||||
Status string
|
||||
CurrentVariantID *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type CourseVariant struct {
|
||||
ID string
|
||||
PublicID string
|
||||
CourseSetID string
|
||||
SourceID *string
|
||||
SourcePublicID *string
|
||||
Name string
|
||||
RouteCode *string
|
||||
Mode string
|
||||
ControlCount *int
|
||||
Difficulty *string
|
||||
Status string
|
||||
IsDefault bool
|
||||
ConfigPatch json.RawMessage
|
||||
MetadataJSON json.RawMessage
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type MapRuntimeBinding struct {
|
||||
ID string
|
||||
PublicID string
|
||||
EventID string
|
||||
EventPublicID string
|
||||
PlaceID string
|
||||
PlacePublicID string
|
||||
MapAssetID string
|
||||
MapAssetPublicID string
|
||||
TileReleaseID string
|
||||
TileReleasePublicID string
|
||||
CourseSetID string
|
||||
CourseSetPublicID string
|
||||
CourseVariantID string
|
||||
CourseVariantPublicID string
|
||||
Status string
|
||||
Notes *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type CreatePlaceParams struct {
|
||||
PublicID string
|
||||
Code string
|
||||
Name string
|
||||
Region *string
|
||||
CoverURL *string
|
||||
Description *string
|
||||
CenterPoint map[string]any
|
||||
Status string
|
||||
}
|
||||
|
||||
type CreateMapAssetParams struct {
|
||||
PublicID string
|
||||
PlaceID string
|
||||
LegacyMapID *string
|
||||
Code string
|
||||
Name string
|
||||
MapType string
|
||||
CoverURL *string
|
||||
Description *string
|
||||
Status string
|
||||
}
|
||||
|
||||
type CreateTileReleaseParams struct {
|
||||
PublicID string
|
||||
MapAssetID string
|
||||
LegacyMapVersionID *string
|
||||
VersionCode string
|
||||
Status string
|
||||
TileBaseURL string
|
||||
MetaURL string
|
||||
PublishedAssetRoot *string
|
||||
MetadataJSON map[string]any
|
||||
PublishedAt *time.Time
|
||||
}
|
||||
|
||||
type CreateCourseSourceParams struct {
|
||||
PublicID string
|
||||
LegacyPlayfieldVersionID *string
|
||||
SourceType string
|
||||
FileURL string
|
||||
Checksum *string
|
||||
ParserVersion *string
|
||||
ImportStatus string
|
||||
MetadataJSON map[string]any
|
||||
ImportedAt *time.Time
|
||||
}
|
||||
|
||||
type CreateCourseSetParams struct {
|
||||
PublicID string
|
||||
PlaceID string
|
||||
MapAssetID string
|
||||
Code string
|
||||
Mode string
|
||||
Name string
|
||||
Description *string
|
||||
Status string
|
||||
}
|
||||
|
||||
type CreateCourseVariantParams struct {
|
||||
PublicID string
|
||||
CourseSetID string
|
||||
SourceID *string
|
||||
Name string
|
||||
RouteCode *string
|
||||
Mode string
|
||||
ControlCount *int
|
||||
Difficulty *string
|
||||
Status string
|
||||
IsDefault bool
|
||||
ConfigPatch map[string]any
|
||||
MetadataJSON map[string]any
|
||||
}
|
||||
|
||||
type CreateMapRuntimeBindingParams struct {
|
||||
PublicID string
|
||||
EventID string
|
||||
PlaceID string
|
||||
MapAssetID string
|
||||
TileReleaseID string
|
||||
CourseSetID string
|
||||
CourseVariantID string
|
||||
Status string
|
||||
Notes *string
|
||||
}
|
||||
|
||||
func (s *Store) ListPlaces(ctx context.Context, limit int) ([]Place, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, place_public_id, code, name, region, cover_url, description, center_point_jsonb::text, status, created_at, updated_at
|
||||
FROM places
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1
|
||||
`, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list places: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Place{}
|
||||
for rows.Next() {
|
||||
item, err := scanPlaceFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate places: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetPlaceByPublicID(ctx context.Context, publicID string) (*Place, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT id, place_public_id, code, name, region, cover_url, description, center_point_jsonb::text, status, created_at, updated_at
|
||||
FROM places
|
||||
WHERE place_public_id = $1
|
||||
LIMIT 1
|
||||
`, publicID)
|
||||
return scanPlace(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreatePlace(ctx context.Context, tx Tx, params CreatePlaceParams) (*Place, error) {
|
||||
centerPointJSON, err := marshalJSONMap(params.CenterPoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal place center point: %w", err)
|
||||
}
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO places (place_public_id, code, name, region, cover_url, description, center_point_jsonb, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)
|
||||
RETURNING id, place_public_id, code, name, region, cover_url, description, center_point_jsonb::text, status, created_at, updated_at
|
||||
`, params.PublicID, params.Code, params.Name, params.Region, params.CoverURL, params.Description, centerPointJSON, params.Status)
|
||||
return scanPlace(row)
|
||||
}
|
||||
|
||||
func (s *Store) ListMapAssetsByPlaceID(ctx context.Context, placeID string) ([]MapAsset, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT ma.id, ma.map_asset_public_id, ma.place_id, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
|
||||
ma.cover_url, ma.description, ma.status, ma.current_tile_release_id, ma.created_at, ma.updated_at
|
||||
FROM map_assets ma
|
||||
LEFT JOIN maps lm ON lm.id = ma.legacy_map_id
|
||||
WHERE ma.place_id = $1
|
||||
ORDER BY ma.created_at DESC
|
||||
`, placeID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list map assets: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []MapAsset{}
|
||||
for rows.Next() {
|
||||
item, err := scanMapAssetFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate map assets: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetMapAssetByPublicID(ctx context.Context, publicID string) (*MapAsset, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT ma.id, ma.map_asset_public_id, ma.place_id, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
|
||||
ma.cover_url, ma.description, ma.status, ma.current_tile_release_id, ma.created_at, ma.updated_at
|
||||
FROM map_assets ma
|
||||
LEFT JOIN maps lm ON lm.id = ma.legacy_map_id
|
||||
WHERE ma.map_asset_public_id = $1
|
||||
LIMIT 1
|
||||
`, publicID)
|
||||
return scanMapAsset(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateMapAsset(ctx context.Context, tx Tx, params CreateMapAssetParams) (*MapAsset, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO map_assets (map_asset_public_id, place_id, legacy_map_id, code, name, map_type, cover_url, description, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, map_asset_public_id, place_id, legacy_map_id, NULL::text, code, name, map_type, cover_url, description, status, current_tile_release_id, created_at, updated_at
|
||||
`, params.PublicID, params.PlaceID, params.LegacyMapID, params.Code, params.Name, params.MapType, params.CoverURL, params.Description, params.Status)
|
||||
return scanMapAsset(row)
|
||||
}
|
||||
|
||||
func (s *Store) ListTileReleasesByMapAssetID(ctx context.Context, mapAssetID string) ([]TileRelease, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT tr.id, tr.tile_release_public_id, tr.map_asset_id, tr.legacy_map_version_id, mv.version_public_id,
|
||||
tr.version_code, tr.status, tr.tile_base_url, tr.meta_url, tr.published_asset_root,
|
||||
tr.metadata_jsonb::text, tr.published_at, tr.created_at, tr.updated_at
|
||||
FROM tile_releases tr
|
||||
LEFT JOIN map_versions mv ON mv.id = tr.legacy_map_version_id
|
||||
WHERE tr.map_asset_id = $1
|
||||
ORDER BY tr.created_at DESC
|
||||
`, mapAssetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list tile releases: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []TileRelease{}
|
||||
for rows.Next() {
|
||||
item, err := scanTileReleaseFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate tile releases: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTileReleaseByPublicID(ctx context.Context, publicID string) (*TileRelease, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT tr.id, tr.tile_release_public_id, tr.map_asset_id, tr.legacy_map_version_id, mv.version_public_id,
|
||||
tr.version_code, tr.status, tr.tile_base_url, tr.meta_url, tr.published_asset_root,
|
||||
tr.metadata_jsonb::text, tr.published_at, tr.created_at, tr.updated_at
|
||||
FROM tile_releases tr
|
||||
LEFT JOIN map_versions mv ON mv.id = tr.legacy_map_version_id
|
||||
WHERE tr.tile_release_public_id = $1
|
||||
LIMIT 1
|
||||
`, publicID)
|
||||
return scanTileRelease(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateTileRelease(ctx context.Context, tx Tx, params CreateTileReleaseParams) (*TileRelease, error) {
|
||||
metadataJSON, err := marshalJSONMap(params.MetadataJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal tile release metadata: %w", err)
|
||||
}
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO tile_releases (
|
||||
tile_release_public_id, map_asset_id, legacy_map_version_id, version_code, status,
|
||||
tile_base_url, meta_url, published_asset_root, metadata_jsonb, published_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)
|
||||
RETURNING id, tile_release_public_id, map_asset_id, legacy_map_version_id, NULL::text, version_code, status,
|
||||
tile_base_url, meta_url, published_asset_root, metadata_jsonb::text, published_at, created_at, updated_at
|
||||
`, params.PublicID, params.MapAssetID, params.LegacyMapVersionID, params.VersionCode, params.Status, params.TileBaseURL, params.MetaURL, params.PublishedAssetRoot, metadataJSON, params.PublishedAt)
|
||||
return scanTileRelease(row)
|
||||
}
|
||||
|
||||
func (s *Store) SetMapAssetCurrentTileRelease(ctx context.Context, tx Tx, mapAssetID, tileReleaseID string) error {
|
||||
_, err := tx.Exec(ctx, `UPDATE map_assets SET current_tile_release_id = $2 WHERE id = $1`, mapAssetID, tileReleaseID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set map asset current tile release: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) ListCourseSources(ctx context.Context, limit int) ([]CourseSource, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT cs.id, cs.course_source_public_id, cs.legacy_playfield_version_id, pv.version_public_id, cs.source_type,
|
||||
cs.file_url, cs.checksum, cs.parser_version, cs.import_status, cs.metadata_jsonb::text, cs.imported_at, cs.created_at, cs.updated_at
|
||||
FROM course_sources cs
|
||||
LEFT JOIN playfield_versions pv ON pv.id = cs.legacy_playfield_version_id
|
||||
ORDER BY cs.created_at DESC
|
||||
LIMIT $1
|
||||
`, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list course sources: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []CourseSource{}
|
||||
for rows.Next() {
|
||||
item, err := scanCourseSourceFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate course sources: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetCourseSourceByPublicID(ctx context.Context, publicID string) (*CourseSource, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT cs.id, cs.course_source_public_id, cs.legacy_playfield_version_id, pv.version_public_id, cs.source_type,
|
||||
cs.file_url, cs.checksum, cs.parser_version, cs.import_status, cs.metadata_jsonb::text, cs.imported_at, cs.created_at, cs.updated_at
|
||||
FROM course_sources cs
|
||||
LEFT JOIN playfield_versions pv ON pv.id = cs.legacy_playfield_version_id
|
||||
WHERE cs.course_source_public_id = $1
|
||||
LIMIT 1
|
||||
`, publicID)
|
||||
return scanCourseSource(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateCourseSource(ctx context.Context, tx Tx, params CreateCourseSourceParams) (*CourseSource, error) {
|
||||
metadataJSON, err := marshalJSONMap(params.MetadataJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal course source metadata: %w", err)
|
||||
}
|
||||
importedAt := time.Now()
|
||||
if params.ImportedAt != nil {
|
||||
importedAt = *params.ImportedAt
|
||||
}
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO course_sources (
|
||||
course_source_public_id, legacy_playfield_version_id, source_type, file_url, checksum,
|
||||
parser_version, import_status, metadata_jsonb, imported_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
|
||||
RETURNING id, course_source_public_id, legacy_playfield_version_id, NULL::text, source_type, file_url,
|
||||
checksum, parser_version, import_status, metadata_jsonb::text, imported_at, created_at, updated_at
|
||||
`, params.PublicID, params.LegacyPlayfieldVersionID, params.SourceType, params.FileURL, params.Checksum, params.ParserVersion, params.ImportStatus, metadataJSON, importedAt)
|
||||
return scanCourseSource(row)
|
||||
}
|
||||
|
||||
func (s *Store) ListCourseSets(ctx context.Context, limit int) ([]CourseSet, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
|
||||
FROM course_sets
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1
|
||||
`, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list course sets: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []CourseSet{}
|
||||
for rows.Next() {
|
||||
item, err := scanCourseSetFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate course sets: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListCourseSetsByMapAssetID(ctx context.Context, mapAssetID string) ([]CourseSet, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
|
||||
FROM course_sets
|
||||
WHERE map_asset_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`, mapAssetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list course sets by map asset: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []CourseSet{}
|
||||
for rows.Next() {
|
||||
item, err := scanCourseSetFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate course sets by map asset: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetCourseSetByPublicID(ctx context.Context, publicID string) (*CourseSet, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
|
||||
FROM course_sets
|
||||
WHERE course_set_public_id = $1
|
||||
LIMIT 1
|
||||
`, publicID)
|
||||
return scanCourseSet(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateCourseSet(ctx context.Context, tx Tx, params CreateCourseSetParams) (*CourseSet, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO course_sets (course_set_public_id, place_id, map_asset_id, code, mode, name, description, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
|
||||
`, params.PublicID, params.PlaceID, params.MapAssetID, params.Code, params.Mode, params.Name, params.Description, params.Status)
|
||||
return scanCourseSet(row)
|
||||
}
|
||||
|
||||
func (s *Store) ListCourseVariantsByCourseSetID(ctx context.Context, courseSetID string) ([]CourseVariant, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT cv.id, cv.course_variant_public_id, cv.course_set_id, cv.source_id, cs.course_source_public_id, cv.name, cv.route_code,
|
||||
cv.mode, cv.control_count, cv.difficulty, cv.status, cv.is_default,
|
||||
cv.config_patch_jsonb::text, cv.metadata_jsonb::text, cv.created_at, cv.updated_at
|
||||
FROM course_variants cv
|
||||
LEFT JOIN course_sources cs ON cs.id = cv.source_id
|
||||
WHERE cv.course_set_id = $1
|
||||
ORDER BY cv.created_at DESC
|
||||
`, courseSetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list course variants: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []CourseVariant{}
|
||||
for rows.Next() {
|
||||
item, err := scanCourseVariantFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate course variants: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetCourseVariantByPublicID(ctx context.Context, publicID string) (*CourseVariant, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT cv.id, cv.course_variant_public_id, cv.course_set_id, cv.source_id, cs.course_source_public_id, cv.name, cv.route_code,
|
||||
cv.mode, cv.control_count, cv.difficulty, cv.status, cv.is_default,
|
||||
cv.config_patch_jsonb::text, cv.metadata_jsonb::text, cv.created_at, cv.updated_at
|
||||
FROM course_variants cv
|
||||
LEFT JOIN course_sources cs ON cs.id = cv.source_id
|
||||
WHERE cv.course_variant_public_id = $1
|
||||
LIMIT 1
|
||||
`, publicID)
|
||||
return scanCourseVariant(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateCourseVariant(ctx context.Context, tx Tx, params CreateCourseVariantParams) (*CourseVariant, error) {
|
||||
configPatchJSON, err := marshalJSONMap(params.ConfigPatch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal course variant config patch: %w", err)
|
||||
}
|
||||
metadataJSON, err := marshalJSONMap(params.MetadataJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal course variant metadata: %w", err)
|
||||
}
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO course_variants (
|
||||
course_variant_public_id, course_set_id, source_id, name, route_code, mode, control_count,
|
||||
difficulty, status, is_default, config_patch_jsonb, metadata_jsonb
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12::jsonb)
|
||||
RETURNING id, course_variant_public_id, course_set_id, source_id, NULL::text, name, route_code, mode,
|
||||
control_count, difficulty, status, is_default, config_patch_jsonb::text, metadata_jsonb::text, created_at, updated_at
|
||||
`, params.PublicID, params.CourseSetID, params.SourceID, params.Name, params.RouteCode, params.Mode, params.ControlCount, params.Difficulty, params.Status, params.IsDefault, configPatchJSON, metadataJSON)
|
||||
return scanCourseVariant(row)
|
||||
}
|
||||
|
||||
func (s *Store) SetCourseSetCurrentVariant(ctx context.Context, tx Tx, courseSetID, variantID string) error {
|
||||
_, err := tx.Exec(ctx, `UPDATE course_sets SET current_variant_id = $2 WHERE id = $1`, courseSetID, variantID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set course set current variant: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) ListMapRuntimeBindings(ctx context.Context, limit int) ([]MapRuntimeBinding, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT mrb.id, mrb.runtime_binding_public_id, mrb.event_id, e.event_public_id, mrb.place_id, p.place_public_id,
|
||||
mrb.map_asset_id, ma.map_asset_public_id, mrb.tile_release_id, tr.tile_release_public_id,
|
||||
mrb.course_set_id, cset.course_set_public_id, mrb.course_variant_id, cv.course_variant_public_id,
|
||||
mrb.status, mrb.notes, mrb.created_at, mrb.updated_at
|
||||
FROM map_runtime_bindings mrb
|
||||
JOIN events e ON e.id = mrb.event_id
|
||||
JOIN places p ON p.id = mrb.place_id
|
||||
JOIN map_assets ma ON ma.id = mrb.map_asset_id
|
||||
JOIN tile_releases tr ON tr.id = mrb.tile_release_id
|
||||
JOIN course_sets cset ON cset.id = mrb.course_set_id
|
||||
JOIN course_variants cv ON cv.id = mrb.course_variant_id
|
||||
ORDER BY mrb.created_at DESC
|
||||
LIMIT $1
|
||||
`, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list runtime bindings: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []MapRuntimeBinding{}
|
||||
for rows.Next() {
|
||||
item, err := scanMapRuntimeBindingFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate runtime bindings: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetMapRuntimeBindingByPublicID(ctx context.Context, publicID string) (*MapRuntimeBinding, error) {
|
||||
row := s.pool.QueryRow(ctx, `
|
||||
SELECT mrb.id, mrb.runtime_binding_public_id, mrb.event_id, e.event_public_id, mrb.place_id, p.place_public_id,
|
||||
mrb.map_asset_id, ma.map_asset_public_id, mrb.tile_release_id, tr.tile_release_public_id,
|
||||
mrb.course_set_id, cset.course_set_public_id, mrb.course_variant_id, cv.course_variant_public_id,
|
||||
mrb.status, mrb.notes, mrb.created_at, mrb.updated_at
|
||||
FROM map_runtime_bindings mrb
|
||||
JOIN events e ON e.id = mrb.event_id
|
||||
JOIN places p ON p.id = mrb.place_id
|
||||
JOIN map_assets ma ON ma.id = mrb.map_asset_id
|
||||
JOIN tile_releases tr ON tr.id = mrb.tile_release_id
|
||||
JOIN course_sets cset ON cset.id = mrb.course_set_id
|
||||
JOIN course_variants cv ON cv.id = mrb.course_variant_id
|
||||
WHERE mrb.runtime_binding_public_id = $1
|
||||
LIMIT 1
|
||||
`, publicID)
|
||||
return scanMapRuntimeBinding(row)
|
||||
}
|
||||
|
||||
func (s *Store) CreateMapRuntimeBinding(ctx context.Context, tx Tx, params CreateMapRuntimeBindingParams) (*MapRuntimeBinding, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO map_runtime_bindings (
|
||||
runtime_binding_public_id, event_id, place_id, map_asset_id, tile_release_id, course_set_id, course_variant_id, status, notes
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, runtime_binding_public_id, event_id, ''::text, place_id, ''::text, map_asset_id, ''::text,
|
||||
tile_release_id, ''::text, course_set_id, ''::text, course_variant_id, ''::text,
|
||||
status, notes, created_at, updated_at
|
||||
`, params.PublicID, params.EventID, params.PlaceID, params.MapAssetID, params.TileReleaseID, params.CourseSetID, params.CourseVariantID, params.Status, params.Notes)
|
||||
return scanMapRuntimeBinding(row)
|
||||
}
|
||||
|
||||
func scanPlace(row pgx.Row) (*Place, error) {
|
||||
var item Place
|
||||
var centerPoint string
|
||||
err := row.Scan(&item.ID, &item.PublicID, &item.Code, &item.Name, &item.Region, &item.CoverURL, &item.Description, ¢erPoint, &item.Status, &item.CreatedAt, &item.UpdatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan place: %w", err)
|
||||
}
|
||||
item.CenterPoint = json.RawMessage(centerPoint)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanPlaceFromRows(rows pgx.Rows) (*Place, error) {
|
||||
var item Place
|
||||
var centerPoint string
|
||||
err := rows.Scan(&item.ID, &item.PublicID, &item.Code, &item.Name, &item.Region, &item.CoverURL, &item.Description, ¢erPoint, &item.Status, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan place row: %w", err)
|
||||
}
|
||||
item.CenterPoint = json.RawMessage(centerPoint)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanMapAsset(row pgx.Row) (*MapAsset, error) {
|
||||
var item MapAsset
|
||||
err := row.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.LegacyMapID, &item.LegacyMapPublicID, &item.Code, &item.Name, &item.MapType, &item.CoverURL, &item.Description, &item.Status, &item.CurrentTileReleaseID, &item.CreatedAt, &item.UpdatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan map asset: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanMapAssetFromRows(rows pgx.Rows) (*MapAsset, error) {
|
||||
var item MapAsset
|
||||
err := rows.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.LegacyMapID, &item.LegacyMapPublicID, &item.Code, &item.Name, &item.MapType, &item.CoverURL, &item.Description, &item.Status, &item.CurrentTileReleaseID, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan map asset row: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanTileRelease(row pgx.Row) (*TileRelease, error) {
|
||||
var item TileRelease
|
||||
var metadataJSON string
|
||||
err := row.Scan(&item.ID, &item.PublicID, &item.MapAssetID, &item.LegacyMapVersionID, &item.LegacyMapVersionPub, &item.VersionCode, &item.Status, &item.TileBaseURL, &item.MetaURL, &item.PublishedAssetRoot, &metadataJSON, &item.PublishedAt, &item.CreatedAt, &item.UpdatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan tile release: %w", err)
|
||||
}
|
||||
item.MetadataJSON = json.RawMessage(metadataJSON)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanTileReleaseFromRows(rows pgx.Rows) (*TileRelease, error) {
|
||||
var item TileRelease
|
||||
var metadataJSON string
|
||||
err := rows.Scan(&item.ID, &item.PublicID, &item.MapAssetID, &item.LegacyMapVersionID, &item.LegacyMapVersionPub, &item.VersionCode, &item.Status, &item.TileBaseURL, &item.MetaURL, &item.PublishedAssetRoot, &metadataJSON, &item.PublishedAt, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan tile release row: %w", err)
|
||||
}
|
||||
item.MetadataJSON = json.RawMessage(metadataJSON)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanCourseSource(row pgx.Row) (*CourseSource, error) {
|
||||
var item CourseSource
|
||||
var metadataJSON string
|
||||
err := row.Scan(&item.ID, &item.PublicID, &item.LegacyPlayfieldVersionID, &item.LegacyPlayfieldVersionPub, &item.SourceType, &item.FileURL, &item.Checksum, &item.ParserVersion, &item.ImportStatus, &metadataJSON, &item.ImportedAt, &item.CreatedAt, &item.UpdatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan course source: %w", err)
|
||||
}
|
||||
item.MetadataJSON = json.RawMessage(metadataJSON)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanCourseSourceFromRows(rows pgx.Rows) (*CourseSource, error) {
|
||||
var item CourseSource
|
||||
var metadataJSON string
|
||||
err := rows.Scan(&item.ID, &item.PublicID, &item.LegacyPlayfieldVersionID, &item.LegacyPlayfieldVersionPub, &item.SourceType, &item.FileURL, &item.Checksum, &item.ParserVersion, &item.ImportStatus, &metadataJSON, &item.ImportedAt, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan course source row: %w", err)
|
||||
}
|
||||
item.MetadataJSON = json.RawMessage(metadataJSON)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanCourseSet(row pgx.Row) (*CourseSet, error) {
|
||||
var item CourseSet
|
||||
err := row.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.MapAssetID, &item.Code, &item.Mode, &item.Name, &item.Description, &item.Status, &item.CurrentVariantID, &item.CreatedAt, &item.UpdatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan course set: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanCourseSetFromRows(rows pgx.Rows) (*CourseSet, error) {
|
||||
var item CourseSet
|
||||
err := rows.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.MapAssetID, &item.Code, &item.Mode, &item.Name, &item.Description, &item.Status, &item.CurrentVariantID, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan course set row: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanCourseVariant(row pgx.Row) (*CourseVariant, error) {
|
||||
var item CourseVariant
|
||||
var configPatch string
|
||||
var metadataJSON string
|
||||
err := row.Scan(&item.ID, &item.PublicID, &item.CourseSetID, &item.SourceID, &item.SourcePublicID, &item.Name, &item.RouteCode, &item.Mode, &item.ControlCount, &item.Difficulty, &item.Status, &item.IsDefault, &configPatch, &metadataJSON, &item.CreatedAt, &item.UpdatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan course variant: %w", err)
|
||||
}
|
||||
item.ConfigPatch = json.RawMessage(configPatch)
|
||||
item.MetadataJSON = json.RawMessage(metadataJSON)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanCourseVariantFromRows(rows pgx.Rows) (*CourseVariant, error) {
|
||||
var item CourseVariant
|
||||
var configPatch string
|
||||
var metadataJSON string
|
||||
err := rows.Scan(&item.ID, &item.PublicID, &item.CourseSetID, &item.SourceID, &item.SourcePublicID, &item.Name, &item.RouteCode, &item.Mode, &item.ControlCount, &item.Difficulty, &item.Status, &item.IsDefault, &configPatch, &metadataJSON, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan course variant row: %w", err)
|
||||
}
|
||||
item.ConfigPatch = json.RawMessage(configPatch)
|
||||
item.MetadataJSON = json.RawMessage(metadataJSON)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanMapRuntimeBinding(row pgx.Row) (*MapRuntimeBinding, error) {
|
||||
var item MapRuntimeBinding
|
||||
err := row.Scan(&item.ID, &item.PublicID, &item.EventID, &item.EventPublicID, &item.PlaceID, &item.PlacePublicID, &item.MapAssetID, &item.MapAssetPublicID, &item.TileReleaseID, &item.TileReleasePublicID, &item.CourseSetID, &item.CourseSetPublicID, &item.CourseVariantID, &item.CourseVariantPublicID, &item.Status, &item.Notes, &item.CreatedAt, &item.UpdatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan runtime binding: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func scanMapRuntimeBindingFromRows(rows pgx.Rows) (*MapRuntimeBinding, error) {
|
||||
var item MapRuntimeBinding
|
||||
err := rows.Scan(&item.ID, &item.PublicID, &item.EventID, &item.EventPublicID, &item.PlaceID, &item.PlacePublicID, &item.MapAssetID, &item.MapAssetPublicID, &item.TileReleaseID, &item.TileReleasePublicID, &item.CourseSetID, &item.CourseSetPublicID, &item.CourseVariantID, &item.CourseVariantPublicID, &item.Status, &item.Notes, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan runtime binding row: %w", err)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
Reference in New Issue
Block a user