mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(api): add admin endpoints for binding management
Implement handlers for creating, listing, and updating model bindings. Register new routes in the admin server group and add DTO definitions. Update provider handlers to trigger binding synchronization on changes to ensure upstream mappings remain current.
This commit is contained in:
@@ -167,6 +167,9 @@ func main() {
|
|||||||
adminGroup.POST("/models", handler.CreateModel)
|
adminGroup.POST("/models", handler.CreateModel)
|
||||||
adminGroup.GET("/models", handler.ListModels)
|
adminGroup.GET("/models", handler.ListModels)
|
||||||
adminGroup.PUT("/models/:id", handler.UpdateModel)
|
adminGroup.PUT("/models/:id", handler.UpdateModel)
|
||||||
|
adminGroup.POST("/bindings", handler.CreateBinding)
|
||||||
|
adminGroup.GET("/bindings", handler.ListBindings)
|
||||||
|
adminGroup.PUT("/bindings/:id", handler.UpdateBinding)
|
||||||
adminGroup.POST("/sync/snapshot", handler.SyncSnapshot)
|
adminGroup.POST("/sync/snapshot", handler.SyncSnapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
160
internal/api/binding_handler.go
Normal file
160
internal/api/binding_handler.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ez-api/ez-api/internal/dto"
|
||||||
|
"github.com/ez-api/ez-api/internal/model"
|
||||||
|
groupx "github.com/ez-api/foundation/group"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateBinding godoc
|
||||||
|
// @Summary Create a new binding
|
||||||
|
// @Description Create a new (namespace, public_model) binding to a route group and selector
|
||||||
|
// @Tags admin
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security AdminAuth
|
||||||
|
// @Param binding body dto.BindingDTO true "Binding Info"
|
||||||
|
// @Success 201 {object} model.Binding
|
||||||
|
// @Failure 400 {object} gin.H
|
||||||
|
// @Failure 500 {object} gin.H
|
||||||
|
// @Router /admin/bindings [post]
|
||||||
|
func (h *Handler) CreateBinding(c *gin.Context) {
|
||||||
|
var req dto.BindingDTO
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := strings.TrimSpace(req.Namespace)
|
||||||
|
pm := strings.TrimSpace(req.PublicModel)
|
||||||
|
if ns == "" || pm == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "namespace and public_model required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rg := groupx.Normalize(req.RouteGroup)
|
||||||
|
if strings.TrimSpace(rg) == "" {
|
||||||
|
rg = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
st := strings.TrimSpace(req.Status)
|
||||||
|
if st == "" {
|
||||||
|
st = "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
selectorType := strings.TrimSpace(req.SelectorType)
|
||||||
|
if selectorType == "" {
|
||||||
|
selectorType = "exact"
|
||||||
|
}
|
||||||
|
|
||||||
|
b := model.Binding{
|
||||||
|
Namespace: ns,
|
||||||
|
PublicModel: pm,
|
||||||
|
RouteGroup: rg,
|
||||||
|
SelectorType: selectorType,
|
||||||
|
SelectorValue: strings.TrimSpace(req.SelectorValue),
|
||||||
|
Status: st,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.Create(&b).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create binding", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.sync.SyncBindings(h.db); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync bindings", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListBindings godoc
|
||||||
|
// @Summary List bindings
|
||||||
|
// @Description List all configured bindings
|
||||||
|
// @Tags admin
|
||||||
|
// @Produce json
|
||||||
|
// @Security AdminAuth
|
||||||
|
// @Success 200 {array} model.Binding
|
||||||
|
// @Failure 500 {object} gin.H
|
||||||
|
// @Router /admin/bindings [get]
|
||||||
|
func (h *Handler) ListBindings(c *gin.Context) {
|
||||||
|
var out []model.Binding
|
||||||
|
if err := h.db.Find(&out).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list bindings", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBinding godoc
|
||||||
|
// @Summary Update a binding
|
||||||
|
// @Description Update an existing binding
|
||||||
|
// @Tags admin
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security AdminAuth
|
||||||
|
// @Param id path int true "Binding ID"
|
||||||
|
// @Param binding body dto.BindingDTO true "Binding Info"
|
||||||
|
// @Success 200 {object} model.Binding
|
||||||
|
// @Failure 400 {object} gin.H
|
||||||
|
// @Failure 404 {object} gin.H
|
||||||
|
// @Failure 500 {object} gin.H
|
||||||
|
// @Router /admin/bindings/{id} [put]
|
||||||
|
func (h *Handler) UpdateBinding(c *gin.Context) {
|
||||||
|
idParam := c.Param("id")
|
||||||
|
id, err := strconv.Atoi(idParam)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing model.Binding
|
||||||
|
if err := h.db.First(&existing, id).Error; err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "binding not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.BindingDTO
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ns := strings.TrimSpace(req.Namespace); ns != "" {
|
||||||
|
existing.Namespace = ns
|
||||||
|
}
|
||||||
|
if pm := strings.TrimSpace(req.PublicModel); pm != "" {
|
||||||
|
existing.PublicModel = pm
|
||||||
|
}
|
||||||
|
if rg := strings.TrimSpace(req.RouteGroup); rg != "" {
|
||||||
|
existing.RouteGroup = groupx.Normalize(rg)
|
||||||
|
}
|
||||||
|
if st := strings.TrimSpace(req.Status); st != "" {
|
||||||
|
existing.Status = st
|
||||||
|
}
|
||||||
|
if t := strings.TrimSpace(req.SelectorType); t != "" {
|
||||||
|
existing.SelectorType = t
|
||||||
|
}
|
||||||
|
if req.SelectorValue != "" {
|
||||||
|
existing.SelectorValue = strings.TrimSpace(req.SelectorValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.Save(&existing).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update binding", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.sync.SyncBindings(h.db); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync bindings", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, existing)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -89,6 +89,11 @@ func (h *Handler) CreateProvider(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync provider", "details": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync provider", "details": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Provider model list changes can affect binding upstream mappings; rebuild bindings snapshot.
|
||||||
|
if err := h.sync.SyncBindings(h.db); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync bindings", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, provider)
|
c.JSON(http.StatusCreated, provider)
|
||||||
}
|
}
|
||||||
@@ -196,6 +201,10 @@ func (h *Handler) UpdateProvider(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync provider", "details": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync provider", "details": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := h.sync.SyncBindings(h.db); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync bindings", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, existing)
|
c.JSON(http.StatusOK, existing)
|
||||||
}
|
}
|
||||||
|
|||||||
13
internal/dto/binding.go
Normal file
13
internal/dto/binding.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
// BindingDTO defines inbound payload for binding creation/update.
|
||||||
|
// It maps "(namespace, public_model)" to a RouteGroup and an upstream selector.
|
||||||
|
type BindingDTO struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
PublicModel string `json:"public_model"`
|
||||||
|
RouteGroup string `json:"route_group"`
|
||||||
|
SelectorType string `json:"selector_type"`
|
||||||
|
SelectorValue string `json:"selector_value"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/ez-api/ez-api/internal/model"
|
"github.com/ez-api/ez-api/internal/model"
|
||||||
groupx "github.com/ez-api/foundation/group"
|
groupx "github.com/ez-api/foundation/group"
|
||||||
"github.com/ez-api/foundation/jsoncodec"
|
"github.com/ez-api/foundation/jsoncodec"
|
||||||
|
"github.com/ez-api/foundation/routing"
|
||||||
"github.com/ez-api/foundation/tokenhash"
|
"github.com/ez-api/foundation/tokenhash"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -407,9 +408,11 @@ func (s *SyncService) writeBindingsSnapshot(ctx context.Context, pipe redis.Pipe
|
|||||||
}
|
}
|
||||||
|
|
||||||
key := ns + "." + pm
|
key := ns + "." + pm
|
||||||
if err := s.hsetJSON(ctx, "config:bindings", key, snap); err != nil {
|
payload, err := jsoncodec.Marshal(snap)
|
||||||
return err
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal config:bindings:%s: %w", key, err)
|
||||||
}
|
}
|
||||||
|
pipe.HSet(ctx, "config:bindings", key, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := map[string]string{
|
meta := map[string]string{
|
||||||
|
|||||||
Reference in New Issue
Block a user