353 lines
10 KiB
Go
353 lines
10 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"reanimator/internal/domain"
|
|
historysvc "reanimator/internal/history"
|
|
"reanimator/internal/repository/registry"
|
|
)
|
|
|
|
type RegistryDependencies struct {
|
|
Assets *registry.AssetRepository
|
|
Components *registry.ComponentRepository
|
|
History *historysvc.Service
|
|
}
|
|
|
|
type registryHandlers struct {
|
|
deps RegistryDependencies
|
|
}
|
|
|
|
func RegisterRegistryRoutes(mux *http.ServeMux, deps RegistryDependencies) {
|
|
h := registryHandlers{deps: deps}
|
|
|
|
mux.HandleFunc("/assets", h.handleAssets)
|
|
mux.HandleFunc("/registry/assets/", h.handleAssetByID)
|
|
|
|
mux.HandleFunc("/components", h.handleComponents)
|
|
mux.HandleFunc("/registry/components/", h.handleComponentByID)
|
|
}
|
|
|
|
func (h registryHandlers) handleAssets(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
items, err := h.deps.Assets.List(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "list assets failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, items)
|
|
case http.MethodPost:
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Vendor *string `json:"vendor"`
|
|
Model *string `json:"model"`
|
|
VendorSerial string `json:"vendor_serial"`
|
|
AssetTag *string `json:"asset_tag"`
|
|
}
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
writeError(w, http.StatusBadRequest, "name is required")
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.VendorSerial) == "" {
|
|
writeError(w, http.StatusBadRequest, "vendor_serial is required")
|
|
return
|
|
}
|
|
|
|
asset := domain.Asset{
|
|
Name: strings.TrimSpace(req.Name),
|
|
Vendor: normalizeOptionalString(req.Vendor),
|
|
Model: normalizeOptionalString(req.Model),
|
|
VendorSerial: strings.TrimSpace(req.VendorSerial),
|
|
MachineTag: normalizeOptionalString(req.AssetTag),
|
|
}
|
|
|
|
item, err := h.deps.Assets.Create(r.Context(), asset)
|
|
if err != nil {
|
|
switch err {
|
|
case registry.ErrConflict:
|
|
writeError(w, http.StatusConflict, "asset conflict")
|
|
default:
|
|
writeError(w, http.StatusInternalServerError, "create asset failed")
|
|
}
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, item)
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (h registryHandlers) handleAssetByID(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseID(r.URL.Path, "/registry/assets/")
|
|
if !ok {
|
|
writeError(w, http.StatusNotFound, "asset not found")
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
item, err := h.deps.Assets.Get(r.Context(), id)
|
|
if err != nil {
|
|
switch err {
|
|
case registry.ErrNotFound:
|
|
writeError(w, http.StatusNotFound, "asset not found")
|
|
default:
|
|
writeError(w, http.StatusInternalServerError, "get asset failed")
|
|
}
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, item)
|
|
case http.MethodDelete:
|
|
result, err := h.deps.Assets.DeleteWithDetails(r.Context(), id)
|
|
if err != nil {
|
|
switch err {
|
|
case registry.ErrNotFound:
|
|
writeError(w, http.StatusNotFound, "asset not found")
|
|
default:
|
|
writeError(w, http.StatusInternalServerError, "delete asset failed")
|
|
}
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"deleted": true,
|
|
"deleted_parts": result.DeletedParts,
|
|
})
|
|
case http.MethodPut:
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Vendor *string `json:"vendor"`
|
|
Model *string `json:"model"`
|
|
VendorSerial string `json:"vendor_serial"`
|
|
AssetTag *string `json:"asset_tag"`
|
|
}
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Name) == "" {
|
|
writeError(w, http.StatusBadRequest, "name is required")
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.VendorSerial) == "" {
|
|
writeError(w, http.StatusBadRequest, "vendor_serial is required")
|
|
return
|
|
}
|
|
var item domain.Asset
|
|
var err error
|
|
if h.deps.History != nil {
|
|
patch := []historysvc.PatchOp{
|
|
{Op: "replace", Path: "/identity/name", Value: strings.TrimSpace(req.Name)},
|
|
{Op: "replace", Path: "/identity/vendor_serial", Value: strings.TrimSpace(req.VendorSerial)},
|
|
{Op: "replace", Path: "/identity/vendor", Value: valueOrNil(normalizeOptionalString(req.Vendor))},
|
|
{Op: "replace", Path: "/identity/model", Value: valueOrNil(normalizeOptionalString(req.Model))},
|
|
{Op: "replace", Path: "/identity/machine_tag", Value: valueOrNil(normalizeOptionalString(req.AssetTag))},
|
|
}
|
|
idem := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
|
|
var idemPtr *string
|
|
if idem != "" {
|
|
idemPtr = &idem
|
|
}
|
|
_, err = h.deps.History.ApplyAssetPatch(r.Context(), historysvc.ApplyPatchCommand{
|
|
EntityID: id,
|
|
ChangeType: "ASSET_REGISTRY_UPDATED",
|
|
SourceType: "user",
|
|
ActorType: "user",
|
|
IdempotencyKey: idemPtr,
|
|
Patch: patch,
|
|
})
|
|
} else {
|
|
item, err = h.deps.Assets.Update(r.Context(), domain.Asset{
|
|
ID: id,
|
|
Name: strings.TrimSpace(req.Name),
|
|
Vendor: normalizeOptionalString(req.Vendor),
|
|
Model: normalizeOptionalString(req.Model),
|
|
VendorSerial: strings.TrimSpace(req.VendorSerial),
|
|
MachineTag: normalizeOptionalString(req.AssetTag),
|
|
})
|
|
}
|
|
if err != nil {
|
|
switch err {
|
|
case registry.ErrNotFound:
|
|
writeError(w, http.StatusNotFound, "asset not found")
|
|
case registry.ErrConflict:
|
|
writeError(w, http.StatusConflict, "asset conflict")
|
|
case historysvc.ErrNotFound:
|
|
writeError(w, http.StatusNotFound, "asset not found")
|
|
case historysvc.ErrConflict:
|
|
writeError(w, http.StatusConflict, "history conflict")
|
|
default:
|
|
writeError(w, http.StatusInternalServerError, "update asset failed")
|
|
}
|
|
return
|
|
}
|
|
if h.deps.History != nil {
|
|
item, err = h.deps.Assets.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "get asset failed")
|
|
return
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, item)
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (h registryHandlers) handleComponents(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
items, err := h.deps.Components.List(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "list components failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, items)
|
|
case http.MethodPost:
|
|
var req struct {
|
|
Vendor *string `json:"vendor"`
|
|
Model *string `json:"model"`
|
|
VendorSerial string `json:"vendor_serial"`
|
|
}
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.VendorSerial) == "" {
|
|
writeError(w, http.StatusBadRequest, "vendor_serial is required")
|
|
return
|
|
}
|
|
|
|
component := domain.Component{
|
|
Vendor: req.Vendor,
|
|
Model: req.Model,
|
|
VendorSerial: strings.TrimSpace(req.VendorSerial),
|
|
}
|
|
|
|
item, err := h.deps.Components.Create(r.Context(), component)
|
|
if err != nil {
|
|
switch err {
|
|
case registry.ErrConflict:
|
|
writeError(w, http.StatusConflict, "component conflict")
|
|
default:
|
|
writeError(w, http.StatusInternalServerError, "create component failed")
|
|
}
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, item)
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (h registryHandlers) handleComponentByID(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseID(r.URL.Path, "/registry/components/")
|
|
if !ok {
|
|
writeError(w, http.StatusNotFound, "component not found")
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
item, err := h.deps.Components.Get(r.Context(), id)
|
|
if err != nil {
|
|
switch err {
|
|
case registry.ErrNotFound:
|
|
writeError(w, http.StatusNotFound, "component not found")
|
|
default:
|
|
writeError(w, http.StatusInternalServerError, "get component failed")
|
|
}
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, item)
|
|
case http.MethodPut:
|
|
var req struct {
|
|
Vendor *string `json:"vendor"`
|
|
Model *string `json:"model"`
|
|
VendorSerial string `json:"vendor_serial"`
|
|
}
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.VendorSerial) == "" {
|
|
writeError(w, http.StatusBadRequest, "vendor_serial is required")
|
|
return
|
|
}
|
|
|
|
var item domain.Component
|
|
var err error
|
|
if h.deps.History != nil {
|
|
patch := []historysvc.PatchOp{
|
|
{Op: "replace", Path: "/identity/vendor_serial", Value: strings.TrimSpace(req.VendorSerial)},
|
|
{Op: "replace", Path: "/identity/vendor", Value: valueOrNil(normalizeOptionalString(req.Vendor))},
|
|
{Op: "replace", Path: "/identity/model", Value: valueOrNil(normalizeOptionalString(req.Model))},
|
|
}
|
|
idem := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
|
|
var idemPtr *string
|
|
if idem != "" {
|
|
idemPtr = &idem
|
|
}
|
|
_, err = h.deps.History.ApplyComponentPatch(r.Context(), historysvc.ApplyPatchCommand{
|
|
EntityID: id,
|
|
ChangeType: "COMPONENT_REGISTRY_UPDATED",
|
|
SourceType: "user",
|
|
ActorType: "user",
|
|
IdempotencyKey: idemPtr,
|
|
Patch: patch,
|
|
})
|
|
if err == nil {
|
|
item, err = h.deps.Components.Get(r.Context(), id)
|
|
}
|
|
} else {
|
|
item, err = h.deps.Components.Update(r.Context(), domain.Component{
|
|
ID: id,
|
|
Vendor: normalizeOptionalString(req.Vendor),
|
|
Model: normalizeOptionalString(req.Model),
|
|
VendorSerial: strings.TrimSpace(req.VendorSerial),
|
|
})
|
|
}
|
|
if err != nil {
|
|
switch err {
|
|
case registry.ErrNotFound:
|
|
writeError(w, http.StatusNotFound, "component not found")
|
|
case registry.ErrConflict:
|
|
writeError(w, http.StatusConflict, "component conflict")
|
|
case historysvc.ErrNotFound:
|
|
writeError(w, http.StatusNotFound, "component not found")
|
|
case historysvc.ErrConflict:
|
|
writeError(w, http.StatusConflict, "history conflict")
|
|
default:
|
|
writeError(w, http.StatusInternalServerError, "update component failed")
|
|
}
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, item)
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func normalizeOptionalString(value *string) *string {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
trimmed := strings.TrimSpace(*value)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
return &trimmed
|
|
}
|
|
|
|
func valueOrNil(value *string) any {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return *value
|
|
}
|