api.spaceplanner.app

Spaceplanner API
git clone git://jacobedwards.org/api.spaceplanner.app
Log | Files | Refs

commit 59bf1940dc5d523335e62b534092eb4d71740b22
parent 907da057dcee96aa2e342a85d4ca602f1109ffdb
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Fri,  4 Oct 2024 15:55:04 -0700

Use IDs to reference floorplans

While I like referencing things by name, using an ID will prevent
some unwelcome behavior for free (say, when the name of a floorplan
gets changed while you're editing it.)

A basic permission system was also half-implemented so the user
doesn't have to be passed around so much which has the added benifit
of providing a more helpful error than "no results found" on lack
of permissions.

Diffstat:
Mcmd/api/floorplans.go | 64+++++++++++++++++++++++++++++++++++++++++++++-------------------
Minternal/backend/floorplan.go | 45++++++++++++++++++++++++++++++++-------------
Minternal/backend/floorplan_data.go | 217++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Minternal/backend/id.go | 6++++++
Ainternal/backend/permissions.go | 41+++++++++++++++++++++++++++++++++++++++++
5 files changed, 261 insertions(+), 112 deletions(-)

diff --git a/cmd/api/floorplans.go b/cmd/api/floorplans.go @@ -18,6 +18,11 @@ type Point struct { Y *int `json:"y" binding:"required"` } +type FloorplanURI struct { + User string `uri:"user" binding:"required"` + Floorplan backend.ObjectID `uri:"floorplan" binding:"required"` +} + func (e *Env) FurnitureTypes(c *gin.Context) { types, err := e.backend.FurnitureTypes(nil) if err != nil { @@ -45,17 +50,20 @@ func (e *Env) CreateFloorplan(c *gin.Context) { } func (e *Env) UpdateFloorplan(c *gin.Context) { + var uri FloorplanURI var req SettableFloorplan + if err := c.ShouldBindUri(&uri); err != nil { + RespondError(c, 400, err.Error()) + return + } if err := c.ShouldBind(&req); err != nil { RespondError(c, 400, "%s", err.Error()) return } - user := c.Param("user") - name := c.Param("floorplan") - fp, err := e.backend.UpdateFloorplan(nil, user, name, - &backend.Floorplan{User: user, Name: req.Name, Address: req.Address, Synopsis: req.Synopsis}) + fp, err := e.backend.UpdateFloorplan(nil, uri.User, uri.Floorplan, + &backend.Floorplan{User: uri.User, Name: req.Name, Address: req.Address, Synopsis: req.Synopsis}) if err != nil { RespondError(c, 500, "Unable to update floorplan") } else { @@ -64,10 +72,14 @@ func (e *Env) UpdateFloorplan(c *gin.Context) { } func (e *Env) DeleteFloorplan(c *gin.Context) { - user := c.Param("user") - floorplan := c.Param("floorplan") + var uri FloorplanURI - fp, err := e.backend.DeleteFloorplan(nil, user, floorplan) + if err := c.ShouldBindUri(&uri); err != nil { + RespondError(c, 400, err.Error()) + return + } + + fp, err := e.backend.DeleteFloorplan(nil, uri.User, uri.Floorplan) if err != nil { RespondError(c, 400, "%s", err.Error()) } else { @@ -87,10 +99,14 @@ func (e *Env) GetFloorplans(c *gin.Context) { } func (e *Env) GetFloorplan(c *gin.Context) { - user := c.Param("user") - name := c.Param("floorplan") + var uri FloorplanURI - fp, err := e.backend.GetFloorplan(nil, user, name) + if err := c.ShouldBindUri(&uri); err != nil { + RespondError(c, 400, err.Error()) + return + } + + fp, err := e.backend.GetFloorplan(nil, uri.User, uri.Floorplan) if err != nil { RespondError(c, 400, "%s", err.Error()) } else { @@ -99,10 +115,14 @@ func (e *Env) GetFloorplan(c *gin.Context) { } func (e *Env) GetFloorplanData(c *gin.Context) { - user := c.Param("user") - floorplan := c.Param("floorplan") + var uri FloorplanURI + + if err := c.ShouldBindUri(&uri); err != nil { + RespondError(c, 400, err.Error()) + return + } - data, err := e.backend.GetFloorplanData(nil, user, floorplan) + data, err := e.backend.GetFloorplanData(nil, uri.User, uri.Floorplan) if err != nil { RespondError(c, 400, "%s", err.Error()) } else { @@ -111,16 +131,19 @@ func (e *Env) GetFloorplanData(c *gin.Context) { } func (e *Env) PatchFloorplanData(c *gin.Context) { + var uri FloorplanURI var patch []backend.Patch - user := c.Param("user") - floorplan := c.Param("floorplan") + if err := c.ShouldBindUri(&uri); err != nil { + RespondError(c, 400, err.Error()) + return + } if err := c.ShouldBind(&patch); err != nil { RespondError(c, 400, "%s: Unable to get patch", err.Error()) return } - data, err := e.backend.PatchFloorplanData(nil, user, floorplan, patch) + data, err := e.backend.PatchFloorplanData(nil, uri.User, uri.Floorplan, patch) if err != nil { RespondError(c, 400, "%s: Unable to patch floorplan", err.Error()) } else { @@ -129,16 +152,19 @@ func (e *Env) PatchFloorplanData(c *gin.Context) { } func (e *Env) ReplaceFloorplanData(c *gin.Context) { + var uri FloorplanURI var data *backend.FloorplanData - user := c.Param("user") - floorplan := c.Param("floorplan") + if err := c.ShouldBindUri(&uri); err != nil { + RespondError(c, 400, err.Error()) + return + } if err := c.ShouldBind(&data); err != nil { RespondError(c, 400, "%s: Couldn't get floorplan data", err.Error()) return } - newdata, err := e.backend.ReplaceFloorplanData(nil, user, floorplan, data) + newdata, err := e.backend.ReplaceFloorplanData(nil, uri.User, uri.Floorplan, data) if err != nil { RespondError(c, 500, "%s: Unable to update floorplan data", err.Error()) return diff --git a/internal/backend/floorplan.go b/internal/backend/floorplan.go @@ -2,11 +2,12 @@ package backend import ( "database/sql" + "errors" "time" ) type Floorplan struct { - id int + ID ObjectID `json:"id"` User string `json:"user"` Name string `json:"name"` Address *string `json:"address"` @@ -17,7 +18,7 @@ type Floorplan struct { func (e *Env) CreateFloorplan(tx *sql.Tx, template *Floorplan) (*Floorplan, error) { stmt, err := e.CacheTxStmt(tx, "create_floorplan", `INSERT INTO spaceplanner.floorplans (owner, name, address, synopsis) - VALUES ($1, $2, $3, $4) RETURNING *`) + VALUES ($1, $2, $3, $4) RETURNING id, owner, name, address, synopsis, updated, created`) if err != nil { return nil, err } @@ -25,27 +26,37 @@ func (e *Env) CreateFloorplan(tx *sql.Tx, template *Floorplan) (*Floorplan, erro return scanFloorplan(stmt.QueryRow(template.User, template.Name, template.Address, template.Synopsis)) } -func (e *Env) UpdateFloorplan(tx *sql.Tx, user string, name string, updated *Floorplan) (*Floorplan, error) { +func (e *Env) UpdateFloorplan(tx *sql.Tx, user string, id ObjectID, updated *Floorplan) (*Floorplan, error) { + if id.Type != IDTypeFloorplan { + return nil, errors.New("Expected floorplan ID") + } + stmt, err := e.CacheTxStmt(tx, "update_floorplan", `UPDATE spaceplanner.floorplans SET (name, address, synopsis) = - ($3, $4, $5) WHERE owner = $1 AND name = $2 RETURNING *`) + ($3, $4, $5) WHERE owner = $1 AND id = $2 RETURNING id, owner, name, address, synopsis, updated, created`) if err != nil { return nil, err } - return scanFloorplan(stmt.QueryRow(user, name, updated.Name, updated.Address, updated.Synopsis)) + return scanFloorplan(stmt.QueryRow(user, id.Seq, updated.Name, updated.Address, updated.Synopsis)) } -func (e *Env) GetFloorplan(tx *sql.Tx, user string, name string) (*Floorplan, error) { - stmt, err := e.CacheTxStmt(tx, "get_floorplan", "SELECT * FROM spaceplanner.floorplans WHERE owner = $1 AND name = $2") +func (e *Env) GetFloorplan(tx *sql.Tx, user string, id ObjectID) (*Floorplan, error) { + stmt, err := e.CacheTxStmt(tx, "get_floorplan", `SELECT id, owner, name, address, synopsis, updated, created + FROM spaceplanner.floorplans + WHERE owner = $1 AND id = $2`) if err != nil { return nil, err } - return scanFloorplan(stmt.QueryRow(user, name)) + if id.Type != IDTypeFloorplan { + return nil, errors.New("Expected floorplan ID") + } + return scanFloorplan(stmt.QueryRow(user, id.Seq)) } func (e *Env) GetFloorplans(tx *sql.Tx, user string) ([]*Floorplan, error) { - stmt, err := e.CacheTxStmt(tx, "get_floorplans", `SELECT * FROM spaceplanner.floorplans WHERE owner = $1 + stmt, err := e.CacheTxStmt(tx, "get_floorplans", `SELECT id, owner, name, address, synopsis, updated, created + FROM spaceplanner.floorplans WHERE owner = $1 ORDER BY updated DESC, created DESC`) if err != nil { return nil, err @@ -60,17 +71,25 @@ func (e *Env) GetFloorplans(tx *sql.Tx, user string) ([]*Floorplan, error) { return collectRows(rows, scanFloorplan) } -func (e *Env) DeleteFloorplan(tx *sql.Tx, user string, name string) (*Floorplan, error) { - stmt, err := e.CacheTxStmt(tx, "del_floorplan", "DELETE FROM spaceplanner.floorplans WHERE owner = $1 AND name = $2 RETURNING *") +func (e *Env) DeleteFloorplan(tx *sql.Tx, user string, id ObjectID) (*Floorplan, error) { + if id.Type != IDTypeFloorplan { + return nil, errors.New("Expected floorplan ID") + } + + stmt, err := e.CacheTxStmt(tx, "del_floorplan", `DELETE FROM spaceplanner.floorplans + WHERE owner = $1 AND id = $2 + RETURNING id, owner, name, address, synopsis, updated, created`) if err != nil { return nil, err } - return scanFloorplan(stmt.QueryRow(user, name)) + return scanFloorplan(stmt.QueryRow(user, id.Seq)) } func scanFloorplan(row Scanner) (*Floorplan, error) { var f Floorplan - err := row.Scan(&f.id, &f.User, &f.Name, &f.Address, &f.Synopsis, &f.Updated, &f.Created) + var id int64 + err := row.Scan(&id, &f.User, &f.Name, &f.Address, &f.Synopsis, &f.Updated, &f.Created) + f.ID = makeID(IDTypeFloorplan, id) return &f, err } diff --git a/internal/backend/floorplan_data.go b/internal/backend/floorplan_data.go @@ -78,9 +78,9 @@ type DBObject interface { * NOTE: these all take user and floorplan arguments solely for authorization, but * I want a cleaner method */ - Create(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) - Update(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) - Delete(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) + Create(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) + Update(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) + Delete(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) } type FurnitureType struct { @@ -166,26 +166,38 @@ func (e *Env) FurnitureTypes(tx *sql.Tx) (map[string]FurnitureType, error) { return data, nil } -func (e *Env) GetFloorplanData(tx *sql.Tx, user string, floorplan string) (FloorplanData, error) { +func (e *Env) GetFloorplanData(tx *sql.Tx, user string, floorplan ObjectID) (FloorplanData, error) { var data FloorplanData var err error - data.Points, err = e.getFloorplanPoints(tx, user, floorplan) + if floorplan.Type != IDTypeFloorplan { + return data, errors.New("Expected floorplan id") + } + + a, err := e.userFloorplanAccess(tx, user, floorplan) + if err != nil { + return FloorplanData{}, err + } + if !a.Read() { + return FloorplanData{}, errors.New("You do not have read permission on this resource") + } + + data.Points, err = e.getFloorplanPoints(tx, floorplan) if err != nil { return data, err } - data.Pointmaps, err = e.getFloorplanPointMaps(tx, user, floorplan) + data.Pointmaps, err = e.getFloorplanPointMaps(tx, floorplan) if err != nil { return data, err } - data.Furniture, err = e.getFloorplanFurnitureDefs(tx, user, floorplan) + data.Furniture, err = e.getFloorplanFurnitureDefs(tx, floorplan) if err != nil { return data, err } - data.FurnitureMaps, err = e.getFloorplanFurnitureMaps(tx, user, floorplan) + data.FurnitureMaps, err = e.getFloorplanFurnitureMaps(tx, floorplan) if err != nil { return data, err } @@ -193,15 +205,15 @@ func (e *Env) GetFloorplanData(tx *sql.Tx, user string, floorplan string) (Floor return data, nil } -func (e *Env) getFloorplanPoints(tx *sql.Tx, user string, floorplan string) (map[ObjectID]Point, error) { +func (e *Env) getFloorplanPoints(tx *sql.Tx, floorplan ObjectID) (map[ObjectID]Point, error) { stmt, err := e.CacheTxStmt(tx, "get_points", - `SELECT id, x, y FROM spaceplanner.floorplan_points WHERE - floorplan = spaceplanner.floorplan_id($1, $2)`) + `SELECT id, x, y FROM spaceplanner.floorplan_points + WHERE floorplan = $1`) if err != nil { return nil, err } - rows, err := stmt.Query(user, floorplan) + rows, err := stmt.Query(floorplan.Seq) if err != nil { return nil, err } @@ -214,15 +226,15 @@ func (e *Env) getFloorplanPoints(tx *sql.Tx, user string, floorplan string) (map return mapArray(points, mapPoint) } -func (e *Env) getFloorplanPointMaps(tx *sql.Tx, user string, floorplan string) (map[ObjectID]PointMap, error) { +func (e *Env) getFloorplanPointMaps(tx *sql.Tx, floorplan ObjectID) (map[ObjectID]PointMap, error) { stmt, err := e.CacheTxStmt(tx, "get_pointmaps", `SELECT id, type, a, b, door_swing FROM spaceplanner.floorplan_pointmaps WHERE - floorplan = spaceplanner.floorplan_id($1, $2)`) + floorplan = $1`) if err != nil { return nil, err } - rows, err := stmt.Query(user, floorplan) + rows, err := stmt.Query(floorplan.Seq) if err != nil { return nil, err } @@ -234,16 +246,16 @@ func (e *Env) getFloorplanPointMaps(tx *sql.Tx, user string, floorplan string) ( return mapArray(pointmaps, mapPointMap) } -func (e *Env) getFloorplanFurnitureDefs(tx *sql.Tx, user string, floorplan string) (map[ObjectID]Furniture, error) { +func (e *Env) getFloorplanFurnitureDefs(tx *sql.Tx, floorplan ObjectID) (map[ObjectID]Furniture, error) { defsStmt, err := e.CacheTxStmt(tx, "furniture_defs", `SELECT id, type, name, width, depth FROM spaceplanner.furniture - WHERE floorplan = spaceplanner.floorplan_id($1, $2)`) + WHERE floorplan = $1`) if err != nil { return nil, err } - rows, err := defsStmt.Query(user, floorplan) + rows, err := defsStmt.Query(floorplan.Seq) if err != nil { return nil, err } @@ -258,20 +270,20 @@ func (e *Env) getFloorplanFurnitureDefs(tx *sql.Tx, user string, floorplan strin } -func (e *Env) getFloorplanFurnitureMaps(tx *sql.Tx, user string, floorplan string) (map[ObjectID]FurnitureMap, error) { +func (e *Env) getFloorplanFurnitureMaps(tx *sql.Tx, floorplan ObjectID) (map[ObjectID]FurnitureMap, error) { mapsStmt, err := e.CacheTxStmt(tx, "furniture_maps", `SELECT id, furniture_id, layout, x, y, angle FROM spaceplanner.furniture_maps WHERE furniture_id IN ( SELECT id FROM spaceplanner.furniture - WHERE floorplan = spaceplanner.floorplan_id($1, $2) + WHERE floorplan = $1 )`) if err != nil { return nil, err } - rows, err := mapsStmt.Query(user, floorplan) + rows, err := mapsStmt.Query(floorplan.Seq) if err != nil { return nil, err } @@ -284,7 +296,11 @@ func (e *Env) getFloorplanFurnitureMaps(tx *sql.Tx, user string, floorplan strin return mapArray(maps, mapFurnitureMap) } -func (e *Env) ReplaceFloorplanData(tx *sql.Tx, user string, floorplan string, data *FloorplanData) (FloorplanData, error) { +func (e *Env) ReplaceFloorplanData(tx *sql.Tx, user string, floorplan ObjectID, data *FloorplanData) (FloorplanData, error) { + if floorplan.Type != IDTypeFloorplan { + return FloorplanData{}, errors.New("Expected floorplan id") + } + mytx := false if (tx == nil) { mytx = true @@ -333,9 +349,12 @@ func (e *Env) ReplaceFloorplanData(tx *sql.Tx, user string, floorplan string, da return newdata, nil } -func (e *Env) DeleteFloorplanData(tx *sql.Tx, user string, floorplan string) error { - var mytx bool +func (e *Env) DeleteFloorplanData(tx *sql.Tx, user string, floorplan ObjectID) error { + if floorplan.Type != IDTypeFloorplan { + return errors.New("Expected floorplan id") + } + var mytx bool if tx == nil { mytx = true var err error @@ -345,26 +364,34 @@ func (e *Env) DeleteFloorplanData(tx *sql.Tx, user string, floorplan string) err } defer tx.Rollback() } - + + a, err := e.userFloorplanAccess(tx, user, floorplan) + if err != nil { + return err + } + if !a.Write() { + return errors.New("You do not have write permission on this resource") + } + delPnt, err := e.CacheTxStmt(tx, "del_floorplan_pnt", `DELETE FROM spaceplanner.floorplan_points - WHERE floorplan = spaceplanner.floorplan_id($1, $2)`) + WHERE floorplan = $1)`) if err != nil { return err } delFur, err := e.CacheTxStmt(tx, "del_floorplan_fur", `DELETE FROM spaceplanner.furniture - WHERE floorplan = spaceplanner.floorplan_id($1, $2)`) + WHERE floorplan = $1`) if err != nil { return err } - _, err = delPnt.Exec(user, floorplan) + _, err = delPnt.Exec(floorplan.Seq) if err != nil { return err } - _, err = delFur.Exec(user, floorplan) + _, err = delFur.Exec(floorplan.Seq) if err != nil { return err } @@ -375,10 +402,14 @@ func (e *Env) DeleteFloorplanData(tx *sql.Tx, user string, floorplan string) err return nil } -func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan string, patches []Patch) (FloorplanData, error) { +func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan ObjectID, patches []Patch) (FloorplanData, error) { var err error mytx := false - + + if floorplan.Type != IDTypeFloorplan { + return FloorplanData{}, errors.New("Expected floorplan id") + } + if (tx == nil) { tx, err = e.DB.Begin() if err != nil { @@ -388,6 +419,14 @@ func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan string, patc mytx = true } + a, err := e.userFloorplanAccess(tx, user, floorplan) + if err != nil { + return FloorplanData{}, err + } + if !a.Write() { + return FloorplanData{}, errors.New("You do not have write permission on this resource") + } + newIDs := make(map[ObjectID]ObjectID) data := FloorplanData{} @@ -409,7 +448,7 @@ func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan string, patc } if (id.Type == IDTypePoint) { - point, err := applyPatch[Point](e, tx, user, floorplan, &patch, id, newIDs) + point, err := applyPatch[Point](e, tx, floorplan, &patch, id, newIDs) if err != nil { return data, id.Error("", err) } @@ -417,7 +456,7 @@ func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan string, patc data.Points[point.id] = point } } else if (id.Type == IDTypePointMap) { - pointmap, err := applyPatch[PointMap](e, tx, user, floorplan, &patch, id, newIDs) + pointmap, err := applyPatch[PointMap](e, tx, floorplan, &patch, id, newIDs) if err != nil { return data, id.Error("", err) } @@ -425,7 +464,7 @@ func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan string, patc data.Pointmaps[pointmap.id] = pointmap } } else if (id.Type == IDTypeFurniture) { - def, err := applyPatch[Furniture](e, tx, user, floorplan, &patch, id, newIDs) + def, err := applyPatch[Furniture](e, tx, floorplan, &patch, id, newIDs) if err != nil { return data, id.Error("", err) } @@ -433,7 +472,7 @@ func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan string, patc data.Furniture[def.id] = def } } else if (id.Type == IDTypeFurnitureMap) { - fm, err := applyPatch[FurnitureMap](e, tx, user, floorplan, &patch, id, newIDs) + fm, err := applyPatch[FurnitureMap](e, tx, floorplan, &patch, id, newIDs) if err != nil { return data, id.Error("", err) } @@ -455,7 +494,25 @@ func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan string, patc return data, nil } -func applyPatch[T DBObject](e *Env, tx *sql.Tx, user, floorplan string, patch *Patch, id ObjectID, +func (e *Env) userFloorplanAccess(tx *sql.Tx, user string, floorplan ObjectID) (AccessPermission, error) { + checkPerms, err := e.CacheTxStmt(tx, "check_perms", + "SELECT owner FROM spaceplanner.floorplans WHERE id = $1") + if err != nil { + return NoAccess, err + } + + var owner string + r := checkPerms.QueryRow(floorplan.Seq) + if err := r.Scan(&owner); err != nil { + return NoAccess, err + } + if owner == user { + return ReadAccess | WriteAccess, nil + } + return NoAccess, nil +} + +func applyPatch[T DBObject](e *Env, tx *sql.Tx, floorplan ObjectID, patch *Patch, id ObjectID, newIDs map[ObjectID]ObjectID) (T, error) { inputID := id id, exists := newIDs[inputID] @@ -479,11 +536,11 @@ func applyPatch[T DBObject](e *Env, tx *sql.Tx, user, floorplan string, patch *P var dbo DBObject switch patch.Op { case "new": - dbo, err = thing.Create(e, tx, user, floorplan) + dbo, err = thing.Create(e, tx, floorplan) case "replace": - dbo, err = thing.Update(e, tx, user, floorplan) + dbo, err = thing.Update(e, tx, floorplan) case "remove": - dbo, err = thing.Delete(e, tx, user, floorplan) + dbo, err = thing.Delete(e, tx, floorplan) default: return thing, inputID.Error("Unsupported operation", nil) } @@ -515,151 +572,151 @@ func remarshal[T any](value interface{}, result *T) (error) { * these functions return an interface instead of their value * Not sure exactly how this is suppost to be done. */ -func (p Point) Create(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) { +func (p Point) Create(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { stmt, err := e.CacheTxStmt(tx, "add_point",`INSERT INTO spaceplanner.floorplan_points (floorplan, x, y) - VALUES (spaceplanner.floorplan_id($1, $2), $3, $4) RETURNING id, x, y`) + VALUES ($1, $2, $3) RETURNING id, x, y`) if err != nil { return Point{}, err } - return scanPoint(stmt.QueryRow(user, floorplan, p.X, p.Y)) + return scanPoint(stmt.QueryRow(floorplan.Seq, p.X, p.Y)) } -func (p Point) Update(e *Env, tx *sql.Tx, user string, floorplan string) (DBObject, error) { - stmt, err := e.CacheTxStmt(tx, "repl_point", `UPDATE spaceplanner.floorplan_points SET (x, y) = ($4, $5) - WHERE floorplan = spaceplanner.floorplan_id($1, $2) AND id = $3 RETURNING id, x, y`) +func (p Point) Update(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { + stmt, err := e.CacheTxStmt(tx, "repl_point", `UPDATE spaceplanner.floorplan_points SET (x, y) = ($3, $4) + WHERE floorplan = $1 AND id = $2 RETURNING id, x, y`) if err != nil { return Point{}, err } - return scanPoint(stmt.QueryRow(user, floorplan, p.id.Seq, p.X, p.Y)) + return scanPoint(stmt.QueryRow(floorplan.Seq, p.id.Seq, p.X, p.Y)) } -func (p Point) Delete(e *Env, tx *sql.Tx, user string, floorplan string) (DBObject, error) { - return e.DeletePoint(tx, user, floorplan, p.id) +func (p Point) Delete(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { + return e.DeletePoint(tx, floorplan, p.id) } -func (e *Env) DeletePoint(tx *sql.Tx, user string, floorplan string, id ObjectID) (Point, error) { +func (e *Env) DeletePoint(tx *sql.Tx, floorplan, id ObjectID) (Point, error) { stmt, err := e.CacheTxStmt(tx, "dele_point", - `DELETE FROM spaceplanner.floorplan_points WHERE floorplan = spaceplanner.floorplan_id($1, $2) AND id = $3 + `DELETE FROM spaceplanner.floorplan_points WHERE floorplan = $1 AND id = $2 RETURNING id, x, y`) if err != nil { return Point{}, err } - return scanPoint(stmt.QueryRow(user, floorplan, id.Seq)) + return scanPoint(stmt.QueryRow(floorplan.Seq, id.Seq)) } -func (pm PointMap) Create(e *Env, tx *sql.Tx, user string, floorplan string) (DBObject, error) { +func (pm PointMap) Create(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { stmt, err := e.CacheTxStmt(tx, "add_pointmap", `INSERT INTO spaceplanner.floorplan_pointmaps (floorplan, type, a, b, door_swing) VALUES ( - spaceplanner.floorplan_id($1, $2), $3, $4, $5, $6 + $1, $2, $3, $4, $5 ) RETURNING id, type, a, b, door_swing`) if err != nil { return PointMap{}, err } - return scanPointMap(stmt.QueryRow(user, floorplan, pm.Type, pm.A.Seq, pm.B.Seq, pm.DoorSwing)) + return scanPointMap(stmt.QueryRow(floorplan.Seq, pm.Type, pm.A.Seq, pm.B.Seq, pm.DoorSwing)) } -func (pm PointMap) Update(e *Env, tx *sql.Tx, user string, floorplan string) (DBObject, error) { +func (pm PointMap) Update(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { stmt, err := e.CacheTxStmt(tx, "repl_pointmap", `UPDATE spaceplanner.floorplan_pointmaps SET (type, a, b, door_swing) = - ($4, $5, $6, $7) WHERE floorplan = spaceplanner.floorplan_id($1, $2) AND id = $3 + ($3, $4, $5, $6) WHERE floorplan = $1 AND id = $2 RETURNING id, type, a, b, door_swing`) if err != nil { return PointMap{}, err } - return scanPointMap(stmt.QueryRow(user, floorplan, pm.id.Seq, pm.Type, pm.A.Seq, pm.B.Seq, pm.DoorSwing)) + return scanPointMap(stmt.QueryRow(floorplan.Seq, pm.id.Seq, pm.Type, pm.A.Seq, pm.B.Seq, pm.DoorSwing)) } -func (pm PointMap) Delete(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) { - return e.DeletePointMap(tx, user, floorplan, pm.id) +func (pm PointMap) Delete(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { + return e.DeletePointMap(tx, floorplan, pm.id) } -func (e *Env) DeletePointMap(tx *sql.Tx, user string, floorplan string, id ObjectID) (PointMap, error) { +func (e *Env) DeletePointMap(tx *sql.Tx, floorplan, id ObjectID) (PointMap, error) { stmt, err := e.CacheTxStmt(tx, "dele_pointmap", `DELETE FROM spaceplanner.floorplan_pointmaps - WHERE floorplan = spaceplanner.floorplan_id($1, $2) AND id = $3 + WHERE floorplan = $1 AND id = $2 RETURNING id, type, a, b, door_swing`) if err != nil { return PointMap{}, err } - return scanPointMap(stmt.QueryRow(user, floorplan, id.Seq)) + return scanPointMap(stmt.QueryRow(floorplan.Seq, id.Seq)) } -func (f Furniture) Create(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) { +func (f Furniture) Create(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { ins, err := e.CacheTxStmt(tx, "add_furn", `INSERT INTO spaceplanner.furniture (floorplan, type, name, width, depth) - VALUES (spaceplanner.floorplan_id($1, $2), $3, $4, $5, $6) + VALUES ($1, $2, $3, $4, $5) RETURNING id, type, name, width, depth`) if err != nil { return f, err } - return scanFurniture(ins.QueryRow(user, floorplan, f.Type, f.Name, f.Width, f.Depth)) + return scanFurniture(ins.QueryRow(floorplan.Seq, f.Type, f.Name, f.Width, f.Depth)) } -func (f Furniture) Update(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) { +func (f Furniture) Update(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { update, err := e.CacheTxStmt(tx, "update_furn", `UPDATE spaceplanner.furniture SET (type, name, width, depth) = - ($4, $5, $6, $7) WHERE floorplan = spaceplanner.floorplan_id($1, $2) AND id = $3 + ($3, $4, $5, $6) WHERE floorplan = $1 AND id = $2 RETURNING id, type, name, width, depth`) if err != nil { return f, err } - return scanFurniture(update.QueryRow(user, floorplan, f.id.Seq, f.Type, f.Name, f.Width, f.Depth)) + return scanFurniture(update.QueryRow(floorplan.Seq, f.id.Seq, f.Type, f.Name, f.Width, f.Depth)) } -func (f Furniture) Delete(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) { +func (f Furniture) Delete(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { del, err := e.CacheTxStmt(tx, "dele_furn", `DELETE FROM spaceplanner.furniture - WHERE floorplan = spaceplanner.floorplan_id($1, $2) AND id = $3 + WHERE floorplan = $1 AND id = $2 RETURNING id, type, name, width, depth`) if err != nil { return f, err } - return scanFurniture(del.QueryRow(user, floorplan, f.id.Seq)) + return scanFurniture(del.QueryRow(floorplan.Seq, f.id.Seq)) } -func (f FurnitureMap) Create(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) { +func (f FurnitureMap) Create(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { ins, err := e.CacheTxStmt(tx, "add_furnmap", `INSERT INTO spaceplanner.furniture_maps (floorplan, furniture_id, layout, x, y, angle) - VALUES (spaceplanner.floorplan_id($1, $2), $3, $4, $5, $6, $7) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, furniture_id, layout, x, y, angle`) if err != nil { return f, err } - return scanFurnitureMap(ins.QueryRow(user, floorplan, f.FurnitureID.Seq, f.Layout, f.X, f.Y, f.Angle)) + return scanFurnitureMap(ins.QueryRow(floorplan.Seq, f.FurnitureID.Seq, f.Layout, f.X, f.Y, f.Angle)) } -func (fm FurnitureMap) Update(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) { +func (fm FurnitureMap) Update(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { update, err := e.CacheTxStmt(tx, "update_furnmap", `UPDATE spaceplanner.furniture_maps SET (furniture_id, layout, x, y, angle) = - ($4, $5, $6, $7, $8) WHERE floorplan = spaceplanner.floorplan_id($1, $2) AND id = $3 + ($3, $4, $5, $6, $7) WHERE floorplan = $1 AND id = $2 RETURNING id, furniture_id, layout, x, y, angle`) if err != nil { return fm, err } - return scanFurnitureMap(update.QueryRow(user, floorplan, fm.id.Seq, fm.FurnitureID.Seq, fm.Layout, fm.X, fm.Y, fm.Angle)) + return scanFurnitureMap(update.QueryRow(floorplan.Seq, fm.id.Seq, fm.FurnitureID.Seq, fm.Layout, fm.X, fm.Y, fm.Angle)) } -func (f FurnitureMap) Delete(e *Env, tx *sql.Tx, user, floorplan string) (DBObject, error) { +func (f FurnitureMap) Delete(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) { del, err := e.CacheTxStmt(tx, "dele_furnmap", `DELETE FROM spaceplanner.furniture_maps - WHERE floorplan = spaceplanner.floorplan_id($1, $2) AND id = $3 + WHERE floorplan = $1 AND id = $2 RETURNING id, furniture_id, layout, x, y, angle`) if err != nil { return f, err } - return scanFurnitureMap(del.QueryRow(user, floorplan, f.id.Seq)) + return scanFurnitureMap(del.QueryRow(floorplan.Seq, f.id.Seq)) } func (f Furniture) ID() ObjectID { diff --git a/internal/backend/id.go b/internal/backend/id.go @@ -16,6 +16,7 @@ type ObjectID struct { } var ( + IDTypeFloorplan ObjectType = "flp" IDTypePoint ObjectType = "pnt" IDTypePointMap ObjectType = "pntmap" IDTypeFurniture ObjectType = "fur" @@ -26,6 +27,7 @@ var objectTypes []ObjectType func init() { objectTypes = []ObjectType{ + IDTypeFloorplan, IDTypePoint, IDTypePointMap, IDTypeFurniture, @@ -95,3 +97,7 @@ func (id *ObjectID) UnmarshalText(b []byte) error { *id = d return err } + +func (id *ObjectID) UnmarshalParam(p string) error { + return id.UnmarshalText([]byte(p)) +} diff --git a/internal/backend/permissions.go b/internal/backend/permissions.go @@ -0,0 +1,41 @@ +package backend + +/* + * In the future this will hold something like this + * + * type Permission { + * Name string + * Allowed bool + * } + * + * for whatever permissions might need to be enforced, such as + * service levels. + */ + +type unsignedNumeric interface { + uint8 | uint16 | uint32 | uint64 | uint +} + +var ( + NoAccess AccessPermission = 0 + ReadAccess AccessPermission = 1 + WriteAccess AccessPermission = 2 +) + +type AccessPermission uint8 + +func (p AccessPermission) Read() bool { + return (p & ReadAccess) != 0 +} + +func (p AccessPermission) Write() bool { + return (p & WriteAccess) != 0 +} + +func setBit(mask uint8, bit uint8, set bool) uint8 { + if set { + return mask | bit + } else { + return mask & ^bit + } +}