Skip to content

Commit

Permalink
feat: use PUT api to update a room and a new PUT api to assign device…
Browse files Browse the repository at this point in the history
… to home+room #8

Signed-off-by: Stefano Cappa <[email protected]>
  • Loading branch information
Ks89 committed Mar 5, 2023
1 parent ba47146 commit 00e7d77
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 60 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
indent_style = space
indent_style = tab
indent_size = 2
166 changes: 166 additions & 0 deletions api/assigndevice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package api

import (
"api-server/models"
"api-server/utils"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"
"golang.org/x/net/context"
"net/http"
"time"
)

type AssignDeviceReq struct {
HomeId string `json:"homeId" validate:"required"`
RoomId string `json:"roomId" validate:"required"`
}

type AssignDevice struct {
collectionProfiles *mongo.Collection
collectionHomes *mongo.Collection
ctx context.Context
logger *zap.SugaredLogger
validate *validator.Validate
}

func NewAssignDevice(ctx context.Context, logger *zap.SugaredLogger, collectionProfiles *mongo.Collection, collectionHomes *mongo.Collection, validate *validator.Validate) *AssignDevice {
return &AssignDevice{
collectionProfiles: collectionProfiles,
collectionHomes: collectionHomes,
ctx: ctx,
logger: logger,
validate: validate,
}
}

func (handler *AssignDevice) PutAssignDeviceToHomeRoom(c *gin.Context) {
handler.logger.Info("REST - PUT - PutAssignDeviceToHomeRoom called")

deviceId, errId := primitive.ObjectIDFromHex(c.Param("id"))
if errId != nil {
handler.logger.Error("REST - PUT - PutAssignDeviceToHomeRoom - wrong format of device 'id' path param")
c.JSON(http.StatusBadRequest, gin.H{"error": "wrong format of device 'id' path param"})
return
}

var assignDeviceReq AssignDeviceReq
if err := c.ShouldBindJSON(&assignDeviceReq); err != nil {
handler.logger.Error("REST - PUT - PutAssignDeviceToHomeRoom - Cannot bind request body", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"})
return
}
if err := handler.validate.Struct(assignDeviceReq); err != nil {
handler.logger.Errorf("REST - PUT - PutAssignDeviceToHomeRoom - request body is not valid, err %#v", err)
var errFields = utils.GetErrorMessage(err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body, these fields are not valid:" + errFields})
return
}
homeObjId, errHomeObjId := primitive.ObjectIDFromHex(assignDeviceReq.HomeId)
roomObjId, errRoomObjId := primitive.ObjectIDFromHex(assignDeviceReq.RoomId)
if errHomeObjId != nil || errRoomObjId != nil {
handler.logger.Error("REST - PUT - PutAssignDeviceToHomeRoom - wrong format of one of the values in body")
c.JSON(http.StatusBadRequest, gin.H{"error": "wrong format of one of the values in body"})
return
}

// retrieve current profile object from session
session := sessions.Default(c)
profileSession, err := utils.GetProfileFromSession(&session)
if err != nil {
handler.logger.Error("REST - GET - PutAssignDeviceToHomeRoom - cannot find profile in session")
c.JSON(http.StatusUnauthorized, gin.H{"error": "cannot find profile in session"})
return
}
// get the profile from db
var profile models.Profile
err = handler.collectionProfiles.FindOne(handler.ctx, bson.M{
"_id": profileSession.ID,
}).Decode(&profile)
if err != nil {
handler.logger.Error("REST - GET - PutAssignDeviceToHomeRoom - cannot find profile in db")
c.JSON(http.StatusUnauthorized, gin.H{"error": "cannot find profile"})
return
}

// 1. profile must be the owner of device with id = `deviceId`
if _, found := utils.Find(profile.Devices, deviceId); !found {
handler.logger.Errorf("REST - GET - PutAssignDeviceToHomeRoom - profile must be the owner of device with id = '%s'", deviceId)
c.JSON(http.StatusUnauthorized, gin.H{"error": "you are not the owner of this device id = " + deviceId.Hex()})
return
}

// 2. profile must be the owner of home with id = `assignDeviceReq.HomeId`
if _, found := utils.Find(profile.Homes, homeObjId); !found {
handler.logger.Errorf("REST - GET - PutAssignDeviceToHomeRoom - profile must be the owner of home with id = '%s'", assignDeviceReq.HomeId)
c.JSON(http.StatusUnauthorized, gin.H{"error": "you are not the owner of home id = " + assignDeviceReq.HomeId})
return
}

// 3. `assignDeviceReq.RoomId` must be a room of home with id = `assignDeviceReq.HomeId`
var home models.Home
err = handler.collectionHomes.FindOne(handler.ctx, bson.M{
"_id": homeObjId,
}).Decode(&home)
if err != nil {
handler.logger.Errorf("REST - PUT - PutAssignDeviceToHomeRoom - cannot find home with id = '%s'", assignDeviceReq.HomeId)
c.JSON(http.StatusNotFound, gin.H{"error": "Cannot find home id = " + assignDeviceReq.HomeId})
return
}
// `roomID` must be a room of `home`
var roomFound bool
for _, val := range home.Rooms {
if val.ID == roomObjId {
roomFound = true
}
}
if !roomFound {
handler.logger.Errorf("REST - PUT - PutAssignDeviceToHomeRoom - cannot find room with id = '%s'", assignDeviceReq.RoomId)
c.JSON(http.StatusNotFound, gin.H{"error": "Cannot find room id = " + assignDeviceReq.RoomId})
return
}

// 4. remove device with id = `deviceId` from all rooms of profile's homes
filterProfileHomes := bson.M{"_id": bson.M{"$in": profile.Homes}} // filter homes owned by the profile
updateClean := bson.M{
"$pull": bson.M{
// using the `all positional operator` https://www.mongodb.com/docs/manual/reference/operator/update/positional-all/
"rooms.$[].devices": deviceId,
},
}
_, errClean := handler.collectionHomes.UpdateMany(handler.ctx, filterProfileHomes, updateClean)
if errClean != nil {
handler.logger.Errorf("REST - DELETE - PutAssignDeviceToHomeRoom - cannot remove device from all rooms %#v", errClean)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Cannot assign device to home and room"})
return
}

// 5. assign device with id = `deviceId` to room with id = `assignDeviceReq.RoomId` of home with id = `assignDeviceReq.HomeId`
filterHome := bson.D{bson.E{Key: "_id", Value: homeObjId}}
arrayFiltersRoom := options.ArrayFilters{Filters: bson.A{bson.M{"x._id": roomObjId}}}
opts := options.UpdateOptions{
ArrayFilters: &arrayFiltersRoom,
}
update := bson.M{
"$push": bson.M{
"rooms.$[x].devices": deviceId,
},
"$set": bson.M{
"rooms.$[x].modifiedAt": time.Now(),
},
// TODO I should update `modifiedAt` of both `home` and `room` documents
}
_, errUpdate := handler.collectionHomes.UpdateOne(handler.ctx, filterHome, update, &opts)
if errUpdate != nil {
handler.logger.Errorf("REST - PUT - PutAssignDeviceToHomeRoom - Cannot assign device to room in DB, errUpdate = %#v", errUpdate)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Cannot assign device to room"})
return
}

c.JSON(http.StatusOK, gin.H{"message": "Room has been updated"})
}
87 changes: 43 additions & 44 deletions api/homes.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package api

import (
"api-server/models"
"api-server/utils"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"
"golang.org/x/net/context"
"net/http"
"time"
"api-server/models"
"api-server/utils"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"
"golang.org/x/net/context"
"net/http"
"time"
)

type HomeNewReq struct {
Expand All @@ -28,14 +28,15 @@ type HomeUpdateReq struct {
}

type RoomNewReq struct {
Name string `json:"name" validate:"required,min=1,max=50"`
Floor int `json:"floor" validate:"required,min=-50,max=300"`
Name string `json:"name" validate:"required,min=1,max=50"`
// cannot use 'required' because I should be able to set floor=0. It isn't a problem, because the default value is 0 :)
Floor int `json:"floor" validate:"min=-50,max=300"`
}

type RoomUpdateReq struct {
Name string `json:"name" validate:"required,min=1,max=50"`
Floor int `json:"floor" validate:"required,min=-50,max=300"`
Devices []primitive.ObjectID `json:"devices" bson:"devices,omitempty"`
Name string `json:"name" validate:"required,min=1,max=50"`
// cannot use 'required' because I should be able to set floor=0. It isn't a problem, because the default value is 0 :)
Floor int `json:"floor" validate:"min=-50,max=300"`
}

type Homes struct {
Expand Down Expand Up @@ -473,8 +474,8 @@ func (handler *Homes) PostRoom(c *gin.Context) {
func (handler *Homes) PutRoom(c *gin.Context) {
handler.logger.Info("REST - PUT - PutRoom called")

objectId, errId := primitive.ObjectIDFromHex(c.Param("id"))
objectRid, errRid := primitive.ObjectIDFromHex(c.Param("rid"))
homeId, errId := primitive.ObjectIDFromHex(c.Param("id"))
roomId, errRid := primitive.ObjectIDFromHex(c.Param("rid"))
if errId != nil || errRid != nil {
handler.logger.Error("REST - PUT - PutRoom - wrong format of one of the path params")
c.JSON(http.StatusBadRequest, gin.H{"error": "wrong format of one of the path params"})
Expand All @@ -487,51 +488,50 @@ func (handler *Homes) PutRoom(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"})
return
}

err := handler.validate.Struct(updateRoom)
if err != nil {
if err := handler.validate.Struct(updateRoom); err != nil {
handler.logger.Errorf("REST - PUT - PutRoom - request body is not valid, err %#v", err)
var errFields = utils.GetErrorMessage(err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body, these fields are not valid:" + errFields})
return
}

// you can update a home only if you are the owner of that home
session := sessions.Default(c)
isOwned := handler.isHomeOwnedBy(session, homeId)

if !isOwned {
handler.logger.Error("REST - PUT - PutRoom - Cannot update a room in an home that is not in session profile")
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot update a room in an home that is not in your profile"})
return
}

// get Home
var home models.Home
err = handler.collection.FindOne(handler.ctx, bson.M{
"_id": objectId,
err := handler.collection.FindOne(handler.ctx, bson.M{
"_id": homeId,
}).Decode(&home)
if err != nil {
handler.logger.Error("REST - PUT - PutRoom - Cannot find rooms of the home with that id")
c.JSON(http.StatusNotFound, gin.H{"error": "Cannot find rooms for that house"})
return
}

// search if room is in rooms array
// `roomID` must be a room of `home`
var roomFound bool
for _, val := range home.Rooms {
if val.ID == objectRid {
if val.ID == roomId {
roomFound = true
}
}
if !roomFound {
handler.logger.Errorf("REST - PUT - PutRoom - Cannot find room with id: %v", objectRid)
handler.logger.Errorf("REST - PUT - PutRoom - Cannot find room with id: %v", roomId)
c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
return
}

// you can update a home only if you are the owner of that home
session := sessions.Default(c)
isOwned := handler.isHomeOwnedBy(session, objectId)

if !isOwned {
handler.logger.Error("REST - PUT - PutRoom - Cannot update a room in an home that is not in session profile")
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot update a room in an home that is not in your profile"})
return
}

// update room
filter := bson.D{primitive.E{Key: "_id", Value: objectId}}
arrayFilters := options.ArrayFilters{Filters: bson.A{bson.M{"x._id": objectRid}}}
filter := bson.D{primitive.E{Key: "_id", Value: homeId}}
arrayFilters := options.ArrayFilters{Filters: bson.A{bson.M{"x._id": roomId}}}
upsert := true
opts := options.UpdateOptions{
ArrayFilters: &arrayFilters,
Expand All @@ -541,13 +541,12 @@ func (handler *Homes) PutRoom(c *gin.Context) {
"$set": bson.M{
"rooms.$[x].name": updateRoom.Name,
"rooms.$[x].floor": updateRoom.Floor,
"rooms.$[x].devices": updateRoom.Devices,
"rooms.$[x].modifiedAt": time.Now(),
},
}
_, err2 := handler.collection.UpdateOne(handler.ctx, filter, update, &opts)
if err2 != nil {
handler.logger.Error("REST - PUT - PutRoom - Cannot update a room in DB")
_, errUpdate := handler.collection.UpdateOne(handler.ctx, filter, update, &opts)
if errUpdate != nil {
handler.logger.Errorf("REST - PUT - PutRoom - Cannot update a room in DB, errUpdate = %#v", errUpdate)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Cannot update room"})
return
}
Expand Down
3 changes: 3 additions & 0 deletions initialization/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var OauthGithub *api.Github
var auth *api.Auth
var homes *api.Homes
var devices *api.Devices
var assignDevices *api.AssignDevice
var devicesValues *api.DevicesValues
var profiles *api.Profiles
var register *api.Register
Expand Down Expand Up @@ -115,6 +116,7 @@ func RegisterRoutes(router *gin.Engine, ctx context.Context, logger *zap.Sugared
auth = api.NewAuth(ctx, logger, collProfiles)
homes = api.NewHomes(ctx, logger, collHomes, collProfiles, validate)
devices = api.NewDevices(ctx, logger, collDevices, collProfiles, collHomes)
assignDevices = api.NewAssignDevice(ctx, logger, collProfiles, collHomes, validate)
devicesValues = api.NewDevicesValues(ctx, logger, collDevices, collProfiles, collHomes, validate)
profiles = api.NewProfiles(ctx, logger, collProfiles)
register = api.NewRegister(ctx, logger, collDevices, collProfiles, validate)
Expand Down Expand Up @@ -149,6 +151,7 @@ func RegisterRoutes(router *gin.Engine, ctx context.Context, logger *zap.Sugared
private.POST("/profiles/:id/tokens", profiles.PostProfilesToken)

private.GET("/devices", devices.GetDevices)
private.PUT("/devices/:id", assignDevices.PutAssignDeviceToHomeRoom)
private.DELETE("/devices/:id", devices.DeleteDevice)

private.GET("/devices/:id/values", devicesValues.GetValuesDevice)
Expand Down
5 changes: 2 additions & 3 deletions integration_tests/homes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,8 @@ var _ = Describe("Homes", func() {
When("profile owns an home", func() {
It("should update an existing room of the home", func() {
room1Upd := api.RoomUpdateReq{
Name: "room1-upd",
Floor: 1,
Devices: []primitive.ObjectID{},
Name: "room1-upd",
Floor: 0,
}
var roomBuf bytes.Buffer
err := json.NewEncoder(&roomBuf).Encode(room1Upd)
Expand Down
24 changes: 12 additions & 12 deletions utils/slice_utils.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package utils

func MapSlice[T any, M any](a []T, f func(T) M) []M {
n := make([]M, len(a))
for i, e := range a {
n[i] = f(e)
}
return n
n := make([]M, len(a))
for i, e := range a {
n[i] = f(e)
}
return n
}

func Find(slice []string, val string) (int, bool) {
for i, item := range slice {
if item == val {
return i, true
}
}
return -1, false
func Find[T comparable](slice []T, val T) (int, bool) {
for i, item := range slice {
if item == val {
return i, true
}
}
return -1, false
}

0 comments on commit 00e7d77

Please sign in to comment.