diff --git a/common/roles.go b/common/roles.go index 6a00533..31c2e0a 100644 --- a/common/roles.go +++ b/common/roles.go @@ -20,6 +20,7 @@ const ModelSimulator = ModelName("simulator") const ModelVisualization = ModelName("visualization") const ModelWidget = ModelName("widget") const ModelSimulationModel = ModelName("simulationmodel") +const ModelSignal = ModelName("signal") type CRUD string @@ -52,6 +53,7 @@ var Roles = RoleActions{ ModelSimulator: crud, ModelWidget: crud, ModelVisualization: crud, + ModelSignal: crud, }, "User": { ModelUser: _ru_, @@ -60,6 +62,7 @@ var Roles = RoleActions{ ModelSimulator: _r__, ModelWidget: crud, ModelVisualization: crud, + ModelSignal: crud, }, "Guest": { ModelSimulation: _r__, @@ -68,6 +71,7 @@ var Roles = RoleActions{ ModelWidget: _r__, ModelSimulator: _r__, ModelUser: _ru_, + ModelSignal: _r__, }, } diff --git a/routes/signal/signalEndpoints.go b/routes/signal/signalEndpoints.go new file mode 100644 index 0000000..d676b1f --- /dev/null +++ b/routes/signal/signalEndpoints.go @@ -0,0 +1,193 @@ +package signal + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/common" + "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/routes/simulationmodel" +) + +func RegisterSignalEndpoints(r *gin.RouterGroup) { + r.GET("", getSignals) + r.POST("", addSignal) + r.PUT("/:signalID", updateSignal) + r.GET("/:signalID", getSignal) + r.DELETE("/:signalID", deleteSignal) +} + +// getSignals godoc +// @Summary Get all signals of one direction +// @ID getSignals +// @Produce json +// @Tags signals +// @Param direction query string true "Direction of signal (in or out)" +// @Param direction modelID string true "Model ID of signals to be obtained" +// @Success 200 {array} common.Signal "Requested signals." +// @Failure 401 "Unauthorized Access" +// @Failure 403 "Access forbidden." +// @Failure 404 "Not found" +// @Failure 500 "Internal server error" +// @Router /signals [get] +func getSignals(c *gin.Context) { + + ok, m := simulationmodel.CheckPermissions(c, common.Read, "query", -1) + if !ok { + return + } + + var mapping string + direction := c.Request.URL.Query().Get("direction") + if direction == "in" { + mapping = "InputMapping" + } else if direction == "out" { + mapping = "OutputMapping" + } else { + errormsg := "Bad request. Direction has to be in or out" + c.JSON(http.StatusBadRequest, gin.H{ + "error": errormsg, + }) + return + } + + db := common.GetDB() + var sigs []common.Signal + err := db.Order("ID asc").Model(m).Where("Direction = ?", direction).Related(&sigs, mapping).Error + if common.ProvideErrorResponse(c, err) { + return + } + + serializer := common.SignalsSerializer{c, sigs} + c.JSON(http.StatusOK, gin.H{ + "signals": serializer.Response(), + }) +} + +// AddSignal godoc +// @Summary Add a signal to a signal mapping of a model +// @ID AddSignal +// @Accept json +// @Produce json +// @Tags signals +// @Param inputSignal body common.Signal true "A signal to be added to the model incl. direction and model ID to which signal shall be added" +// @Success 200 "OK." +// @Failure 401 "Unauthorized Access" +// @Failure 403 "Access forbidden." +// @Failure 404 "Not found" +// @Failure 500 "Internal server error" +// @Router /signals [post] +func addSignal(c *gin.Context) { + + var newSignal Signal + err := c.BindJSON(&newSignal) + if err != nil { + errormsg := "Bad request. Error binding form data to JSON: " + err.Error() + c.JSON(http.StatusBadRequest, gin.H{ + "error": errormsg, + }) + return + } + + ok, _ := simulationmodel.CheckPermissions(c, common.Update, "body", int(newSignal.SimulationModelID)) + if !ok { + return + } + + // Add signal to model + err = newSignal.addToSimulationModel() + if common.ProvideErrorResponse(c, err) == false { + c.JSON(http.StatusOK, gin.H{ + "message": "OK.", + }) + } +} + +// updateSignal godoc +// @Summary Update a signal +// @ID updateSignal +// @Tags signals +// @Produce json +// @Success 200 "OK." +// @Failure 401 "Unauthorized Access" +// @Failure 403 "Access forbidden." +// @Failure 404 "Not found" +// @Failure 500 "Internal server error" +// @Param signalID path int true "ID of signal to be updated" +// @Router /signals/{signalID} [put] +func updateSignal(c *gin.Context) { + ok, sig := checkPermissions(c, common.Delete) + if !ok { + return + } + + var modifiedSignal Signal + err := c.BindJSON(&modifiedSignal) + if err != nil { + errormsg := "Bad request. Error binding form data to JSON: " + err.Error() + c.JSON(http.StatusBadRequest, gin.H{ + "error": errormsg, + }) + return + } + + err = sig.update(modifiedSignal) + if common.ProvideErrorResponse(c, err) == false { + c.JSON(http.StatusOK, gin.H{ + "message": "OK.", + }) + } + +} + +// getSignal godoc +// @Summary Get a signal +// @ID getSignal +// @Tags signals +// @Produce json +// @Success 200 "OK." +// @Failure 401 "Unauthorized Access" +// @Failure 403 "Access forbidden." +// @Failure 404 "Not found" +// @Failure 500 "Internal server error" +// @Param signalID path int true "ID of signal to be obtained" +// @Router /signals/{signalID} [get] +func getSignal(c *gin.Context) { + ok, sig := checkPermissions(c, common.Delete) + if !ok { + return + } + + serializer := common.SignalSerializer{c, sig.Signal} + c.JSON(http.StatusOK, gin.H{ + "signal": serializer.Response(), + }) +} + +// deleteSignal godoc +// @Summary Delete a signal +// @ID deleteSignal +// @Tags signals +// @Produce json +// @Success 200 "OK." +// @Failure 401 "Unauthorized Access" +// @Failure 403 "Access forbidden." +// @Failure 404 "Not found" +// @Failure 500 "Internal server error" +// @Param signalID path int true "ID of signal to be deleted" +// @Router /signals/{signalID} [delete] +func deleteSignal(c *gin.Context) { + + ok, sig := checkPermissions(c, common.Delete) + if !ok { + return + } + + err := sig.delete() + if common.ProvideErrorResponse(c, err) == false { + c.JSON(http.StatusOK, gin.H{ + "message": "OK.", + }) + } + +} diff --git a/routes/signal/signalMethods.go b/routes/signal/signalMethods.go new file mode 100644 index 0000000..ffab8ff --- /dev/null +++ b/routes/signal/signalMethods.go @@ -0,0 +1,112 @@ +package signal + +import ( + "fmt" + + "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/common" + "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/routes/simulationmodel" +) + +type Signal struct { + common.Signal +} + +func (s *Signal) save() error { + db := common.GetDB() + err := db.Create(s).Error + return err +} + +func (s *Signal) byID(id uint) error { + db := common.GetDB() + err := db.Find(s, id).Error + if err != nil { + return fmt.Errorf("Signal with id=%v does not exist", id) + } + return nil +} + +func (s *Signal) addToSimulationModel() error { + db := common.GetDB() + var m simulationmodel.SimulationModel + err := m.ByID(s.SimulationModelID) + if err != nil { + return err + } + + // save signal to DB + err = s.save() + if err != nil { + return err + } + + // associate signal with simulation model in correct direction + if s.Direction == "in" { + err = db.Model(&m).Association("InputMapping").Append(s).Error + if err != nil { + return err + } + + // adapt length of mapping + m.InputLength = db.Model(m).Where("Direction = ?", "in").Association("InputMapping").Count() + err = m.Update(m) + } else { + err = db.Model(&m).Association("OutputMapping").Append(s).Error + if err != nil { + return err + } + + // adapt length of mapping + m.OutputLength = db.Model(m).Where("Direction = ?", "out").Association("OutputMapping").Count() + err = m.Update(m) + } + return err +} + +func (s *Signal) update(modifiedSignal Signal) error { + db := common.GetDB() + + err := db.Model(s).Updates(map[string]interface{}{ + "Name": modifiedSignal.Name, + "Unit": modifiedSignal.Unit, + "Index": modifiedSignal.Index, + }).Error + + return err + +} + +func (s *Signal) delete() error { + + db := common.GetDB() + var m simulationmodel.SimulationModel + err := m.ByID(s.SimulationModelID) + if err != nil { + return err + } + + // remove association between Signal and SimulationModel + // Signal itself is not deleted from DB, it remains as "dangling" + if s.Direction == "in" { + err = db.Model(&m).Association("InputMapping").Delete(s).Error + if err != nil { + return err + } + + // Reduce length of mapping by 1 + m.InputLength-- + err = m.Update(m) + + } else { + err = db.Model(&m).Association("OutputMapping").Delete(s).Error + if err != nil { + return err + } + + // Reduce length of mapping by 1 + m.OutputLength-- + err = m.Update(m) + } + + return err +} diff --git a/routes/signal/signalMiddleware.go b/routes/signal/signalMiddleware.go new file mode 100644 index 0000000..404b5bc --- /dev/null +++ b/routes/signal/signalMiddleware.go @@ -0,0 +1,44 @@ +package signal + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/common" + "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/routes/simulationmodel" +) + +func checkPermissions(c *gin.Context, operation common.CRUD) (bool, Signal) { + + var sig Signal + + err := common.ValidateRole(c, common.ModelSignal, operation) + if err != nil { + c.JSON(http.StatusUnprocessableEntity, "Access denied (role validation failed).") + return false, sig + } + + signalID, err := strconv.Atoi(c.Param("signalID")) + if err != nil { + errormsg := fmt.Sprintf("Bad request. No or incorrect format of signalID path parameter") + c.JSON(http.StatusBadRequest, gin.H{ + "error": errormsg, + }) + return false, sig + } + + err = sig.byID(uint(signalID)) + if common.ProvideErrorResponse(c, err) { + return false, sig + } + + ok, _ := simulationmodel.CheckPermissions(c, operation, "body", int(sig.SimulationModelID)) + if !ok { + return false, sig + } + + return true, sig +} diff --git a/routes/simulationmodel/simulationmodelEndpoints.go b/routes/simulationmodel/simulationmodelEndpoints.go index 0fe9193..a5cd4cd 100644 --- a/routes/simulationmodel/simulationmodelEndpoints.go +++ b/routes/simulationmodel/simulationmodelEndpoints.go @@ -15,9 +15,6 @@ func RegisterSimulationModelEndpoints(r *gin.RouterGroup) { r.PUT("/:modelID", updateSimulationModel) r.GET("/:modelID", getSimulationModel) r.DELETE("/:modelID", deleteSimulationModel) - r.GET("/:modelID/signals", getSignals) - r.PUT("/:modelID/signals", addSignal) - r.DELETE("/:modelID/signals", deleteSignals) } // getSimulationModels godoc @@ -106,7 +103,7 @@ func addSimulationModel(c *gin.Context) { // @Router /models/{modelID} [put] func updateSimulationModel(c *gin.Context) { - ok, m := checkPermissions(c, common.Update) + ok, m := CheckPermissions(c, common.Update, "path", -1) if !ok { return } @@ -121,7 +118,7 @@ func updateSimulationModel(c *gin.Context) { return } - err = m.update(modifiedModel) + err = m.Update(modifiedModel) if common.ProvideErrorResponse(c, err) == false { c.JSON(http.StatusOK, gin.H{ "message": "OK.", @@ -143,7 +140,7 @@ func updateSimulationModel(c *gin.Context) { // @Router /models/{modelID} [get] func getSimulationModel(c *gin.Context) { - ok, m := checkPermissions(c, common.Read) + ok, m := CheckPermissions(c, common.Read, "path", -1) if !ok { return } @@ -168,7 +165,7 @@ func getSimulationModel(c *gin.Context) { // @Router /models/{modelID} [delete] func deleteSimulationModel(c *gin.Context) { - ok, m := checkPermissions(c, common.Delete) + ok, m := CheckPermissions(c, common.Delete, "path", -1) if !ok { return } @@ -182,126 +179,3 @@ func deleteSimulationModel(c *gin.Context) { "message": "OK.", }) } - -// getSignals godoc -// @Summary Get all signals of one direction -// @ID getSignals -// @Produce json -// @Tags models -// @Param direction query string true "Direction of signal (in or out)" -// @Success 200 {array} common.Signal "Requested signals." -// @Failure 401 "Unauthorized Access" -// @Failure 403 "Access forbidden." -// @Failure 404 "Not found" -// @Failure 500 "Internal server error" -// @Router /models/{modelID}/signals [get] -func getSignals(c *gin.Context) { - - ok, m := checkPermissions(c, common.Read) - if !ok { - return - } - - var mapping string - direction := c.Request.URL.Query().Get("direction") - if direction == "in" { - mapping = "InputMapping" - } else if direction == "out" { - mapping = "OutputMapping" - } else { - errormsg := "Bad request. Direction has to be in or out" - c.JSON(http.StatusBadRequest, gin.H{ - "error": errormsg, - }) - return - } - - db := common.GetDB() - var sigs []common.Signal - err := db.Order("ID asc").Model(m).Where("Direction = ?", direction).Related(&sigs, mapping).Error - if common.ProvideErrorResponse(c, err) { - return - } - - serializer := common.SignalsSerializer{c, sigs} - c.JSON(http.StatusOK, gin.H{ - "signals": serializer.Response(), - }) -} - -// AddSignal godoc -// @Summary Add a signal to a signal mapping of a model -// @ID AddSignal -// @Accept json -// @Produce json -// @Tags models -// @Param inputSignal body common.Signal true "A signal to be added to the model incl. direction" -// @Success 200 "OK." -// @Failure 401 "Unauthorized Access" -// @Failure 403 "Access forbidden." -// @Failure 404 "Not found" -// @Failure 500 "Internal server error" -// @Router /models/{modelID}/signals [put] -func addSignal(c *gin.Context) { - - ok, m := checkPermissions(c, common.Update) - if !ok { - return - } - - var sig common.Signal - err := c.BindJSON(&sig) - if err != nil { - errormsg := "Bad request. Error binding form data to JSON: " + err.Error() - c.JSON(http.StatusBadRequest, gin.H{ - "error": errormsg, - }) - return - } - - // Add signal to model - err = m.addSignal(sig) - if common.ProvideErrorResponse(c, err) == false { - c.JSON(http.StatusOK, gin.H{ - "message": "OK.", - }) - } -} - -// deleteSignals godoc -// @Summary Delete all signals of a direction -// @ID deleteSignals -// @Tags models -// @Produce json -// @Success 200 "OK." -// @Failure 401 "Unauthorized Access" -// @Failure 403 "Access forbidden." -// @Failure 404 "Not found" -// @Failure 500 "Internal server error" -// @Param modelID path int true "Model ID" -// @Param direction query string true "Direction of signals to delete (in or out)" -// @Router /models/{modelID}/signals [delete] -func deleteSignals(c *gin.Context) { - - ok, m := checkPermissions(c, common.Update) - if !ok { - return - } - - direction := c.Request.URL.Query().Get("direction") - if !(direction == "out") && !(direction == "in") { - errormsg := "Bad request. Direction has to be in or out" - c.JSON(http.StatusBadRequest, gin.H{ - "error": errormsg, - }) - return - } - - err := m.deleteSignals(direction) - if common.ProvideErrorResponse(c, err) == false { - c.JSON(http.StatusOK, gin.H{ - "message": "OK.", - }) - } - -} diff --git a/routes/simulationmodel/simulationmodelMethods.go b/routes/simulationmodel/simulationmodelMethods.go index c86ee82..a7ee89d 100644 --- a/routes/simulationmodel/simulationmodelMethods.go +++ b/routes/simulationmodel/simulationmodelMethods.go @@ -52,7 +52,7 @@ func (m *SimulationModel) addToSimulation() error { return err } -func (m *SimulationModel) update(modifiedSimulationModel SimulationModel) error { +func (m *SimulationModel) Update(modifiedSimulationModel SimulationModel) error { db := common.GetDB() if m.SimulatorID != modifiedSimulationModel.SimulatorID { @@ -66,15 +66,13 @@ func (m *SimulationModel) update(modifiedSimulationModel SimulationModel) error } - err := db.Model(m).Updates(map[string]interface{}{"Name": modifiedSimulationModel.Name, + err := db.Model(m).Updates(map[string]interface{}{ + "Name": modifiedSimulationModel.Name, "OutputLength": modifiedSimulationModel.OutputLength, "InputLength": modifiedSimulationModel.InputLength, "StartParameters": modifiedSimulationModel.StartParameters, "SimulatorID": modifiedSimulationModel.SimulatorID, }).Error - if err != nil { - return err - } return err } @@ -94,75 +92,3 @@ func (m *SimulationModel) delete() error { return err } - -func (m *SimulationModel) addSignal(signal common.Signal) error { - - db := common.GetDB() - var err error - - if signal.Direction == "in" { - err = db.Model(m).Association("InputMapping").Append(signal).Error - if err != nil { - return err - } - // adapt length of mapping - m.InputLength = db.Model(m).Where("Direction = ?", "in").Association("InputMapping").Count() - err = m.update(*m) - - } else { - err = db.Model(m).Association("OutputMapping").Append(signal).Error - if err != nil { - return err - } - - // adapt length of mapping - m.OutputLength = db.Model(m).Where("Direction = ?", "out").Association("OutputMapping").Count() - err = m.update(*m) - - } - - return err -} - -func (m *SimulationModel) deleteSignals(direction string) error { - - db := common.GetDB() - var err error - - var columnName string - - if direction == "in" { - columnName = "InputMapping" - - } else { - columnName = "OutputMapping" - } - - var signals []common.Signal - err = db.Order("ID asc").Model(m).Where("Direction = ?", direction).Related(&signals, columnName).Error - if err != nil { - return err - } - - // remove association to each signal and delete each signal from db - for _, sig := range signals { - err = db.Model(m).Association(columnName).Delete(sig).Error - if err != nil { - return err - } - err = db.Delete(sig).Error - if err != nil { - return err - } - } - - // set length of mapping to 0 - if columnName == "InputMapping" { - m.InputLength = 0 - } else { - m.OutputLength = 0 - } - err = m.update(*m) - - return err -} diff --git a/routes/simulationmodel/simulationmodelMiddleware.go b/routes/simulationmodel/simulationmodelMiddleware.go index 62cb6e9..b0df013 100644 --- a/routes/simulationmodel/simulationmodelMiddleware.go +++ b/routes/simulationmodel/simulationmodelMiddleware.go @@ -11,7 +11,7 @@ import ( "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/routes/simulation" ) -func checkPermissions(c *gin.Context, operation common.CRUD) (bool, SimulationModel) { +func CheckPermissions(c *gin.Context, operation common.CRUD, modelIDSource string, modelIDBody int) (bool, SimulationModel) { var m SimulationModel @@ -21,14 +21,27 @@ func checkPermissions(c *gin.Context, operation common.CRUD) (bool, SimulationMo return false, m } - modelID, err := strconv.Atoi(c.Param("modelID")) - - if err != nil { - errormsg := fmt.Sprintf("Bad request. No or incorrect format of model ID in path") - c.JSON(http.StatusBadRequest, gin.H{ - "error": errormsg, - }) - return false, m + var modelID int + if modelIDSource == "path" { + modelID, err = strconv.Atoi(c.Param("modelID")) + if err != nil { + errormsg := fmt.Sprintf("Bad request. No or incorrect format of modelID path parameter") + c.JSON(http.StatusBadRequest, gin.H{ + "error": errormsg, + }) + return false, m + } + } else if modelIDSource == "query" { + modelID, err = strconv.Atoi(c.Request.URL.Query().Get("modelID")) + if err != nil { + errormsg := fmt.Sprintf("Bad request. No or incorrect format of modelID query parameter") + c.JSON(http.StatusBadRequest, gin.H{ + "error": errormsg, + }) + return false, m + } + } else if modelIDSource == "body" { + modelID = modelIDBody } err = m.ByID(uint(modelID)) diff --git a/start.go b/start.go index 9e6cd6c..a8702d6 100644 --- a/start.go +++ b/start.go @@ -2,6 +2,8 @@ package main import ( "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/routes/file" + "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/routes/signal" + "github.com/gin-gonic/gin" "github.com/swaggo/gin-swagger" "github.com/swaggo/gin-swagger/swaggerFiles" @@ -52,6 +54,7 @@ func main() { simulation.RegisterSimulationEndpoints(api.Group("/simulations")) simulationmodel.RegisterSimulationModelEndpoints(api.Group("/models")) + signal.RegisterSignalEndpoints(api.Group("/signals")) visualization.RegisterVisualizationEndpoints(api.Group("/visualizations")) widget.RegisterWidgetEndpoints(api.Group("/widgets")) file.RegisterFileEndpoints(api.Group("/files"))