api.spaceplanner.app

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

floorplan_data.go (24800B)


      1 package backend
      2 
      3 import (
      4 	"database/sql"
      5 	"encoding/json"
      6 	"errors"
      7 	"fmt"
      8 	"strings"
      9 )
     10 
     11 type FloorplanData struct {
     12 	Points map[ObjectID]Point `json:"points"`
     13 	Pointmaps map[ObjectID]PointMap `json:"pointmaps"`
     14 
     15 	// I would like to have this in a FurnitureData struct,
     16 	// but don't want to rework the client to accept that
     17 	// at the moment.
     18 	Furniture map[ObjectID]Furniture `json:"furniture"`
     19 	FurnitureMaps map[ObjectID]FurnitureMap `json:"furniture_maps"`
     20 }
     21 
     22 /*
     23  * NOTE: The id fields are not sent in response because it is encoded
     24  * in the data structure. That is, they're in a map keyed by their ID
     25  * (as seen above.) It's done this way so that the client can update
     26  * individual objects with JSON Patch.
     27  */
     28 type Point struct {
     29 	id ObjectID `json:"id"`
     30 	OldID *ObjectID `json:"old_id,omitempty"`
     31 	X int `json:"x" binding:"required"`
     32 	Y int `json:"y" binding:"required"`
     33 }
     34 
     35 type PointMap struct {
     36 	id ObjectID
     37 	OldID *ObjectID `json:"old_id,omitempty"`
     38 	Type string `json:"type" binding:"required"`
     39 	A ObjectID `json:"a" binding:"required"`
     40 	B ObjectID `json:"b" binding:"required"`
     41 	DoorSwing *string `json:"door_swing,omitempty"`
     42 }
     43 
     44 // NOTE: I'd like to allow every value to be omitted so
     45 // long as it doesn't violate database rules, but without
     46 // setting to null.
     47 type Furniture struct {
     48 	id ObjectID
     49 	OldID *ObjectID `json:"old_id,omitempty"`
     50 	Type string `json:"type" binding:"required"`
     51 	UserType *string `json:"user_type,omitempty"`
     52 	Style *string `json:"style,omitempty"`
     53 	Name *string `json:"name"`
     54 	Width int `json:"width" binding:"required"`
     55 	Depth int `json:"depth" binding:"required"`
     56 }
     57 
     58 type FurnitureMap struct {
     59 	id ObjectID
     60 	OldID *ObjectID `json:"old_id,omitempty"`
     61 	FurnitureID ObjectID `json:"furniture_id" binding:"required"`
     62 	Layout string `json:"layout" binding:"required"`
     63 	X int `json:"x" binding:"required"`
     64 	Y int `json:"y" binding:"required"`
     65 	Angle int `json:"angle" binding:"required"`
     66 }
     67 
     68 type PatchError struct {
     69 	ID *ObjectID
     70 	Context string
     71 	Reason error
     72 }
     73 
     74 type DBObject interface {
     75 	ID() ObjectID
     76 	updateIDs(ObjectID, map[ObjectID]ObjectID) DBObject
     77 	SetOldID(ObjectID) DBObject
     78 
     79 	/*
     80 	 * NOTE: these all take user and floorplan arguments solely for authorization, but
     81 	 * I want a cleaner method
     82 	 */
     83 	Create(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error)
     84 	Update(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error)
     85 	Delete(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error)
     86 }
     87 
     88 type FurnitureType struct {
     89 	Varieties map[string]FurnitureVariety `json:"varieties,omit_empty"`
     90 	Class *string `json:"class,omit_empty"`
     91 	Styles *[]string `json:"styles,omit_empty"`
     92 }
     93 
     94 type FurnitureVariety struct {
     95 	Width int `json:"width"`
     96 	Depth int `json:"depth"`
     97 }
     98 
     99 type Mappable interface {
    100 	Key() any
    101 }
    102 
    103 
    104 var tables map[ObjectType]string
    105 
    106 func init() {
    107 	tables = map[ObjectType]string{
    108 		IDTypePoint: "points",
    109 		IDTypePointMap: "pointmaps",
    110 		IDTypeFurniture: "furniture",
    111 		IDTypeFurnitureMap: "furniture_maps",
    112 	}
    113 }
    114 
    115 func (e *Env) FurnitureTypes(tx *sql.Tx) (map[string]*FurnitureType, error) {
    116 	types, err := e.CacheTxStmt(tx, "furn_types",
    117 		`SELECT name, class FROM spaceplanner.furniture_types`)
    118 	if err != nil {
    119 		return nil, err
    120 	}
    121 
    122 	vars, err := e.CacheTxStmt(tx, "furn_vars",
    123 		`SELECT type, name, width, depth FROM spaceplanner.furniture_varieties`)
    124 	if err != nil {
    125 		return nil, err
    126 	}
    127 
    128 	styles, err := e.CacheTxStmt(tx, "furn_styles",
    129 		`SELECT style FROM spaceplanner.furniture_styles WHERE type = $1`)
    130 	if err != nil {
    131 		return nil, err
    132 	}
    133 
    134 	data := make(map[string]*FurnitureType)
    135 
    136 	rows, err := types.Query()
    137 	if err != nil {
    138 		return nil, err
    139 	}
    140 	// Is this evaluating the expression at execution or now?
    141 	// that would change whether this should be run.
    142 	defer rows.Close()
    143 	for rows.Next() {
    144 		var key string
    145 		var class *string
    146 		if err := rows.Scan(&key, &class); err != nil {
    147 			return nil, err
    148 		}
    149 		d := FurnitureType{}
    150 		if class != nil {
    151 			d.Class = class
    152 		}
    153 		data[key] = &d
    154 	}
    155 
    156 	rows, err = vars.Query()
    157 	if err != nil {
    158 		return nil, err
    159 	}
    160 	defer rows.Close()
    161 	for rows.Next() {
    162 		var tn, vn string
    163 		var v FurnitureVariety
    164 		if err := rows.Scan(&tn, &vn, &v.Width, &v.Depth); err != nil {
    165 			return nil, err
    166 		}
    167 
    168 		d := data[tn]
    169 		if d.Varieties == nil {
    170 			d.Varieties = make(map[string]FurnitureVariety)
    171 		}
    172 		d.Varieties[vn] = v
    173 	}
    174 	rows.Close()
    175 
    176 	for k := range data {
    177 		a := make([]string, 0, 4)
    178 		kstyles, err := styles.Query(k)
    179 		if err != nil {
    180 			return nil, err
    181 		}
    182 
    183 		defer kstyles.Close()
    184 		for kstyles.Next() {
    185 			var s string
    186 			if err := kstyles.Scan(&s); err != nil {
    187 				return nil, err
    188 			}
    189 			a = append(a, s)
    190 		}
    191 		kstyles.Close()
    192 
    193 		if len(a) > 0 {
    194 			d := data[k]
    195 			d.Styles = &a
    196 		}
    197 	}
    198 
    199 	return data, nil
    200 }
    201 
    202 func (e *Env) PointmapTypes() ([]string, error) {
    203 	r, err := e.DB.Query("SELECT name FROM spaceplanner.pointmap_types")
    204 	if err != nil {
    205 		return nil, err
    206 	}
    207 	defer r.Close()
    208 
    209 	a := make([]string, 0, 8)
    210 	for r.Next() {
    211 		var name string
    212 		if err := r.Scan(&name); err != nil {
    213 			return nil, err
    214 		}
    215 		a = append(a, name)
    216 	}
    217 	if err := r.Err(); err != nil {
    218 		return nil, err
    219 	}
    220 
    221 	return a, nil
    222 }
    223 
    224 func (e *Env) GetFloorplanData(tx *sql.Tx, user string, floorplan ObjectID) (FloorplanData, error) {
    225 	var data FloorplanData
    226 	var err error
    227 
    228 	if floorplan.Type != IDTypeFloorplan {
    229 		return data, errors.New("Expected floorplan id")
    230 	}
    231 
    232 	a, err := e.userFloorplanAccess(tx, user, floorplan)
    233 	if err != nil {
    234 		return FloorplanData{}, err
    235 	}
    236 	if !a.Read() {
    237 		return FloorplanData{}, errors.New("You do not have read permission on this resource")
    238 	}
    239 
    240 	data.Points, err = e.getFloorplanPoints(tx, floorplan)
    241 	if err != nil {
    242 		return data, err
    243 	}
    244 
    245 	data.Pointmaps, err = e.getFloorplanPointMaps(tx, floorplan)
    246 	if err != nil {
    247 		return data, err
    248 	}
    249 
    250 	data.Furniture, err = e.getFloorplanFurnitureDefs(tx, floorplan)
    251 	if err != nil {
    252 		return data, err
    253 	}
    254 
    255 	data.FurnitureMaps, err = e.getFloorplanFurnitureMaps(tx, floorplan)
    256 	if err != nil {
    257 		return data, err
    258 	}
    259 
    260 	return data, nil
    261 }
    262 
    263 func (e *Env) getFloorplanPoints(tx *sql.Tx, floorplan ObjectID) (map[ObjectID]Point, error) {
    264 	stmt, err := e.CacheTxStmt(tx, "get_points",
    265 		`SELECT id, x, y FROM spaceplanner.floorplan_points
    266 		WHERE floorplan = $1`)
    267 	if err != nil {
    268 		return nil, err
    269 	}
    270 
    271 	rows, err := stmt.Query(floorplan.Seq)
    272 	if err != nil {
    273 		return nil, err
    274 	}
    275 	defer rows.Close()
    276 
    277 	points, err := collectRows(rows, scanPoint)
    278 	if err != nil {
    279 		return nil, err
    280 	}
    281 	return mapArray(points, mapPoint)
    282 }
    283 
    284 func (e *Env) getFloorplanPointMaps(tx *sql.Tx, floorplan ObjectID) (map[ObjectID]PointMap, error) {
    285 	stmt, err := e.CacheTxStmt(tx, "get_pointmaps",
    286 		`SELECT id, type, a, b, door_swing FROM spaceplanner.floorplan_pointmaps WHERE
    287 		 	floorplan = $1`)
    288 	if err != nil {
    289 		return nil, err
    290 	}
    291 
    292 	rows, err := stmt.Query(floorplan.Seq)
    293 	if err != nil {
    294 		return nil, err
    295 	}
    296 	defer rows.Close()
    297 	pointmaps, err := collectRows(rows, scanPointMap)
    298 	if err != nil {
    299 		return nil, err
    300 	}
    301 	return mapArray(pointmaps, mapPointMap)
    302 }
    303 
    304 func (e *Env) getFloorplanFurnitureDefs(tx *sql.Tx, floorplan ObjectID) (map[ObjectID]Furniture, error) {
    305 	defsStmt, err := e.CacheTxStmt(tx, "furniture_defs",
    306 		`SELECT id, type, user_type, style, name, width, depth
    307 		FROM spaceplanner.furniture
    308 		WHERE floorplan = $1`)
    309 	if err != nil {
    310 		return nil, err
    311 	}
    312 
    313 	rows, err := defsStmt.Query(floorplan.Seq)
    314 	if err != nil {
    315 		return nil, err
    316 	}
    317 	defer rows.Close()
    318 
    319 	defs, err := collectRows(rows, scanFurniture)
    320 	if err != nil {
    321 		return nil, err
    322 	}
    323 
    324 	return mapArray(defs, mapFurniture)
    325 
    326 }
    327 
    328 func (e *Env) getFloorplanFurnitureMaps(tx *sql.Tx, floorplan ObjectID) (map[ObjectID]FurnitureMap, error) {
    329 	mapsStmt, err := e.CacheTxStmt(tx, "furniture_maps",
    330 		`SELECT id, furniture_id, layout, x, y, angle
    331 		FROM spaceplanner.furniture_maps
    332 		WHERE furniture_id IN (
    333 			SELECT id
    334 			FROM spaceplanner.furniture
    335 			WHERE floorplan = $1
    336 		)`)
    337 	if err != nil {
    338 		return nil, err
    339 	}
    340 
    341 	rows, err := mapsStmt.Query(floorplan.Seq)
    342 	if err != nil {
    343 		return nil, err
    344 	}
    345 	defer rows.Close()
    346 
    347 	maps, err := collectRows(rows, scanFurnitureMap)
    348 	if err != nil {
    349 		return nil, err
    350 	}
    351 	return mapArray(maps, mapFurnitureMap)
    352 }
    353 
    354 func (e *Env) ReplaceFloorplanData(tx *sql.Tx, user string, floorplan ObjectID, data *FloorplanData) (FloorplanData, error) {
    355 	if floorplan.Type != IDTypeFloorplan {
    356 		return FloorplanData{}, errors.New("Expected floorplan id")
    357 	}
    358 
    359 	mytx := false
    360 	if (tx == nil) {
    361 		mytx = true 
    362 		var err error 
    363 		tx, err = e.DB.Begin()
    364 		if err != nil {
    365 			return FloorplanData{}, err
    366 		}
    367 		defer tx.Rollback()
    368 	}
    369 	createPatch := func(id ObjectID, value any) Patch {
    370 		return Patch{
    371 			Op: PatchNew,
    372 			Path: id.Path(),
    373 			Value: value,
    374 		}
    375 	}
    376 
    377 	if err := e.DeleteFloorplanData(tx, user, floorplan); err != nil {
    378 		return FloorplanData{}, err
    379 	}
    380 
    381 	patches := make([]Patch, 0)
    382 	for id, v := range data.Points {
    383 		patches = append(patches, createPatch(id, v))
    384 	}
    385 	for id, v := range data.Pointmaps {
    386 		patches = append(patches, createPatch(id, v))
    387 	}
    388 	for id, v := range data.Furniture {
    389 		patches = append(patches, createPatch(id, v))
    390 	}
    391 	for id, v := range data.FurnitureMaps {
    392 		patches = append(patches, createPatch(id, v))
    393 	}
    394 
    395 	newdata, err := e.PatchFloorplanData(tx, user, floorplan, patches)
    396 	if err != nil {
    397 		return FloorplanData{}, err
    398 	}
    399 	if (mytx) {
    400 		if err := tx.Commit(); err != nil {
    401 			return FloorplanData{}, err
    402 		}
    403 	}
    404 	return newdata, nil
    405 }
    406 
    407 func (e *Env) DeleteFloorplanData(tx *sql.Tx, user string, floorplan ObjectID) error {
    408 	if floorplan.Type != IDTypeFloorplan {
    409 		return errors.New("Expected floorplan id")
    410 	}
    411 
    412 	var mytx bool
    413 	if tx == nil {
    414 		mytx = true
    415 		var err error
    416 		tx, err = e.DB.Begin()
    417 		if err != nil {
    418 			return err
    419 		}
    420 		defer tx.Rollback()
    421 	}
    422 
    423 	a, err := e.userFloorplanAccess(tx, user, floorplan)
    424 	if err != nil {
    425 		return err
    426 	}
    427 	if !a.Write() {
    428 		return errors.New("You do not have write permission on this resource")
    429 	}
    430 
    431 	delPnt, err := e.CacheTxStmt(tx, "del_floorplan_pnt",
    432 		`DELETE FROM spaceplanner.floorplan_points
    433 		 WHERE floorplan = $1`)
    434 	if err != nil {
    435 		return err
    436 	}
    437 
    438 	delFur, err := e.CacheTxStmt(tx, "del_floorplan_fur",
    439 		`DELETE FROM spaceplanner.furniture
    440 		 WHERE floorplan = $1`)
    441 	if err != nil {
    442 		return err
    443 	}
    444 
    445 	_, err = delPnt.Exec(floorplan.Seq)
    446 	if err != nil {
    447 		return err
    448 	}
    449 	_, err = delFur.Exec(floorplan.Seq)
    450 	if err != nil {
    451 		return err
    452 	}
    453 
    454 	if (mytx) {
    455 		return tx.Commit()
    456 	}
    457 	return nil
    458 }
    459 
    460 func (e *Env) PatchFloorplanData(tx *sql.Tx, user string, floorplan ObjectID, patches []Patch) (FloorplanData, error) {
    461 	var err error
    462 	mytx := false
    463 
    464 	if floorplan.Type != IDTypeFloorplan {
    465 		return FloorplanData{}, errors.New("Expected floorplan id")
    466 	}
    467 
    468 	if (tx == nil) {
    469 		tx, err = e.DB.Begin()
    470 		if err != nil {
    471 			return FloorplanData{}, err
    472 		}
    473 		defer tx.Rollback()
    474 		mytx = true
    475 	}
    476 
    477 	a, err := e.userFloorplanAccess(tx, user, floorplan)
    478 	if err != nil {
    479 		return FloorplanData{}, err
    480 	}
    481 	if !a.Write() {
    482 		return FloorplanData{}, errors.New("You do not have write permission on this resource")
    483 	}
    484 
    485 	newIDs := make(map[ObjectID]ObjectID)
    486 
    487 	data := FloorplanData{}
    488 	data.Points = make(map[ObjectID]Point)
    489 	data.Pointmaps = make(map[ObjectID]PointMap)
    490 	data.Furniture = make(map[ObjectID]Furniture)
    491 	data.FurnitureMaps = make(map[ObjectID]FurnitureMap)
    492 
    493 	// Allowed operations are new, replace, and delete
    494 	// (new is an extention that ensures path's don't exist before creation)
    495 	for _, patch := range patches {
    496 		id, err := parsePath(patch.Path)
    497 		if err != nil {
    498 			return data, patchError(nil, patch.Path, err)
    499 		}
    500 
    501 		if (patch.Op == PatchNew || patch.Op == PatchReplace) && patch.Value == nil {
    502 			return data, id.Error("Requires value", nil)
    503 		}
    504 
    505 		if (id.Type == IDTypePoint) {
    506 			point, err := applyPatch[Point](e, tx, floorplan, &patch, id, newIDs)
    507 			if err != nil {
    508 				return data, id.Error("", err)
    509 			}
    510 			if patch.Op != PatchRemove {
    511 				data.Points[point.id] = point
    512 			}
    513 		} else if (id.Type == IDTypePointMap) {
    514 			pointmap, err := applyPatch[PointMap](e, tx, floorplan, &patch, id, newIDs)
    515 			if err != nil {
    516 				return data, id.Error("", err)
    517 			}
    518 			if patch.Op != PatchRemove {
    519 				data.Pointmaps[pointmap.id] = pointmap
    520 			}
    521 		} else if (id.Type == IDTypeFurniture) {
    522 			def, err := applyPatch[Furniture](e, tx, floorplan, &patch, id, newIDs)
    523 			if err != nil {
    524 				return data, id.Error("", err)
    525 			}
    526 			if patch.Op != PatchRemove {
    527 				data.Furniture[def.id] = def
    528 			}
    529 		} else if (id.Type == IDTypeFurnitureMap) {
    530 			fm, err := applyPatch[FurnitureMap](e, tx, floorplan, &patch, id, newIDs)
    531 			if err != nil {
    532 				return data, id.Error("", err)
    533 			}
    534 			if patch.Op != PatchRemove {
    535 				data.FurnitureMaps[fm.id] = fm
    536 			}
    537 		} else {
    538 			return data, id.Error("Path does not exist", nil)
    539 		}
    540 	}
    541 
    542 	if (mytx) {
    543 		err = tx.Commit()
    544 		if err != nil {
    545 			return data, patchError(nil, "Unable to commit patch", err)
    546 		}
    547 	}
    548 
    549 	return data, nil
    550 }
    551 
    552 func (e *Env) userFloorplanAccess(tx *sql.Tx, user string, floorplan ObjectID) (AccessPermission, error) {
    553 	checkPerms, err := e.CacheTxStmt(tx, "check_perms",
    554 		"SELECT owner FROM spaceplanner.floorplans WHERE id = $1")
    555 	if err != nil {
    556 		return NoAccess, err
    557 	}
    558 
    559 	var owner string
    560 	r := checkPerms.QueryRow(floorplan.Seq)
    561 	if err := r.Scan(&owner); err != nil {
    562 		return NoAccess, err
    563 	}
    564 	if owner == user {
    565 		return ReadAccess | WriteAccess, nil
    566 	}
    567 	return NoAccess, nil
    568 }
    569 
    570 func applyPatch[T DBObject](e *Env, tx *sql.Tx, floorplan ObjectID, patch *Patch, id ObjectID,
    571     newIDs map[ObjectID]ObjectID) (T, error) {
    572 	inputID := id
    573 	id, exists := newIDs[inputID]
    574 	if !exists {
    575 		id = inputID
    576 	}
    577 
    578 	var thing T
    579 	if err := remarshal(patch.Value, &thing); err != nil {
    580 		return thing, inputID.Error("Invalid object", err)
    581 	}
    582 
    583 	// Could do with adding a id.Type check
    584 
    585 	// Tried to figure out the correct solution to this, but
    586 	// can't figure it out right now. I'll come back to it later
    587 	// but for now a few type assertions
    588 	thing = thing.updateIDs(id, newIDs).(T)
    589 
    590 	var err error
    591 	var dbo DBObject
    592 	switch patch.Op {
    593 	case PatchNew:
    594 		dbo, err = thing.Create(e, tx, floorplan)
    595 	case PatchReplace:
    596 		dbo, err = thing.Update(e, tx, floorplan)
    597 	case PatchRemove:
    598 		dbo, err = thing.Delete(e, tx, floorplan)
    599 	default:
    600 		return thing, inputID.Error("Unsupported operation", nil)
    601 	}
    602 
    603 	if err != nil {
    604 		return thing, inputID.Error("Unable to perform " + patch.Op + " operation", err)
    605 	}
    606 
    607 	thing = dbo.(T)
    608 	id = thing.ID()
    609 	if inputID == id {
    610 		return thing, nil
    611 	}
    612 
    613 	newIDs[inputID] = id
    614 	return thing.SetOldID(inputID).(T), nil
    615 }
    616 
    617 func remarshal[T any](value interface{}, result *T) (error) {
    618 	s, err := json.Marshal(value)
    619 	if err != nil {
    620 		return err
    621 	}
    622 	return json.Unmarshal(s, result)
    623 }
    624 
    625 /*
    626  * NOTE: Tried getting this right for a while, settled on having
    627  * these functions return an interface instead of their value
    628  * Not sure exactly how this is suppost to be done.
    629  */
    630 func (p Point) Create(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    631 	stmt, err := e.CacheTxStmt(tx, "add_point",`INSERT INTO spaceplanner.floorplan_points (floorplan, x, y)
    632 		VALUES ($1, $2, $3) RETURNING id, x, y`)
    633 	if err != nil {
    634 		return Point{}, err
    635 	}
    636 
    637 	return scanPoint(stmt.QueryRow(floorplan.Seq, p.X, p.Y))
    638 }
    639 
    640 func (p Point) Update(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    641 	stmt, err := e.CacheTxStmt(tx, "repl_point", `UPDATE spaceplanner.floorplan_points SET (x, y) = ($3, $4)
    642 		WHERE floorplan = $1 AND id = $2 RETURNING id, x, y`)
    643 	if err != nil {
    644 		return Point{}, err
    645 	}
    646 
    647 	return scanPoint(stmt.QueryRow(floorplan.Seq, p.id.Seq, p.X, p.Y))
    648 }
    649 
    650 func (p Point) Delete(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    651 	return e.DeletePoint(tx, floorplan, p.id)
    652 }
    653 
    654 func (e *Env) DeletePoint(tx *sql.Tx, floorplan, id ObjectID) (Point, error) {
    655 	stmt, err := e.CacheTxStmt(tx, "dele_point",
    656 		`DELETE FROM spaceplanner.floorplan_points WHERE floorplan = $1 AND id = $2
    657 		RETURNING id, x, y`)
    658 	if err != nil {
    659 		return Point{}, err
    660 	}
    661 
    662 	return scanPoint(stmt.QueryRow(floorplan.Seq, id.Seq))
    663 }
    664 
    665 func (pm PointMap) Create(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    666 	stmt, err := e.CacheTxStmt(tx, "add_pointmap",
    667 		`INSERT INTO spaceplanner.floorplan_pointmaps (floorplan, type, a, b, door_swing) VALUES (
    668 			$1, $2, $3, $4, $5
    669 		) RETURNING id, type, a, b, door_swing`)
    670 	if err != nil {
    671 		return PointMap{}, err
    672 	}
    673 
    674 	return scanPointMap(stmt.QueryRow(floorplan.Seq, pm.Type, pm.A.Seq, pm.B.Seq, pm.DoorSwing))
    675 }
    676 
    677 func (pm PointMap) Update(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    678 	stmt, err := e.CacheTxStmt(tx, "repl_pointmap",
    679 		`UPDATE spaceplanner.floorplan_pointmaps SET (type, a, b, door_swing) =
    680 			($3, $4, $5, $6) WHERE floorplan = $1 AND id = $2
    681 			RETURNING id, type, a, b, door_swing`)
    682 	if err != nil {
    683 		return PointMap{}, err
    684 	}
    685 
    686 	return scanPointMap(stmt.QueryRow(floorplan.Seq, pm.id.Seq, pm.Type, pm.A.Seq, pm.B.Seq, pm.DoorSwing))
    687 }
    688 
    689 func (pm PointMap) Delete(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    690 	return e.DeletePointMap(tx, floorplan, pm.id)
    691 }
    692 
    693 func (e *Env) DeletePointMap(tx *sql.Tx, floorplan, id ObjectID) (PointMap, error) {
    694 	stmt, err := e.CacheTxStmt(tx, "dele_pointmap",
    695 		`DELETE FROM spaceplanner.floorplan_pointmaps
    696 			WHERE floorplan = $1 AND id = $2
    697 			RETURNING id, type, a, b, door_swing`)
    698 	if err != nil {
    699 		return PointMap{}, err
    700 	}
    701 
    702 	return scanPointMap(stmt.QueryRow(floorplan.Seq, id.Seq))
    703 }
    704 
    705 func (f Furniture) Create(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    706 	ins, err := e.CacheTxStmt(tx, "add_furn",
    707 		`INSERT INTO spaceplanner.furniture (floorplan, type, user_type, style, name, width, depth)
    708 			VALUES ($1, $2, $3, $4, $5, $6, $7)
    709 			RETURNING id, type, user_type, style, name, width, depth`)
    710 	if err != nil {
    711 		return f, err
    712 	}
    713 
    714 	return scanFurniture(ins.QueryRow(floorplan.Seq, f.Type, f.UserType, f.Style, f.Name, f.Width, f.Depth))
    715 }
    716 
    717 func (f Furniture) Update(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    718 	update, err := e.CacheTxStmt(tx, "update_furn",
    719 		`UPDATE spaceplanner.furniture SET (type, user_type, style, name, width, depth) =
    720 			($3, $4, $5, $6, $7, $8) WHERE floorplan = $1 AND id = $2
    721 			RETURNING id, type, user_type, style, name, width, depth`)
    722 	if err != nil {
    723 		return f, err
    724 	}
    725 
    726 	return scanFurniture(update.QueryRow(floorplan.Seq, f.id.Seq, f.Type, f.UserType, f.Style, f.Name, f.Width, f.Depth))
    727 }
    728 
    729 func (f Furniture) Delete(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    730 	del, err := e.CacheTxStmt(tx, "dele_furn",
    731 		`DELETE FROM spaceplanner.furniture
    732 			WHERE floorplan = $1 AND id = $2
    733 			RETURNING id, type, user_type, style, name, width, depth`)
    734 	if err != nil {
    735 		return f, err
    736 	}
    737 
    738 	return scanFurniture(del.QueryRow(floorplan.Seq, f.id.Seq))
    739 }
    740 
    741 func (f FurnitureMap) Create(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    742 	ins, err := e.CacheTxStmt(tx, "add_furnmap",
    743 		`INSERT INTO spaceplanner.furniture_maps (floorplan, furniture_id, layout, x, y, angle)
    744 			VALUES ($1, $2, $3, $4, $5, $6)
    745 			RETURNING id, furniture_id, layout, x, y, angle`)
    746 	if err != nil {
    747 		return f, err
    748 	}
    749 
    750 	return scanFurnitureMap(ins.QueryRow(floorplan.Seq, f.FurnitureID.Seq, f.Layout, f.X, f.Y, f.Angle))
    751 }
    752 
    753 func (fm FurnitureMap) Update(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    754 	update, err := e.CacheTxStmt(tx, "update_furnmap",
    755 		`UPDATE spaceplanner.furniture_maps SET (furniture_id, layout, x, y, angle) =
    756 			($3, $4, $5, $6, $7) WHERE floorplan = $1 AND id = $2
    757 			RETURNING id, furniture_id, layout, x, y, angle`)
    758 	if err != nil {
    759 		return fm, err
    760 	}
    761 
    762 	return scanFurnitureMap(update.QueryRow(floorplan.Seq, fm.id.Seq, fm.FurnitureID.Seq, fm.Layout, fm.X, fm.Y, fm.Angle))
    763 }
    764 
    765 func (f FurnitureMap) Delete(e *Env, tx *sql.Tx, floorplan ObjectID) (DBObject, error) {
    766 	del, err := e.CacheTxStmt(tx, "dele_furnmap",
    767 		`DELETE FROM spaceplanner.furniture_maps
    768 			WHERE floorplan = $1 AND id = $2
    769 			RETURNING id, furniture_id, layout, x, y, angle`)
    770 	if err != nil {
    771 		return f, err
    772 	}
    773 
    774 	return scanFurnitureMap(del.QueryRow(floorplan.Seq, f.id.Seq))
    775 }
    776 
    777 func (f Furniture) ID() ObjectID {
    778 	return f.id
    779 }
    780 
    781 func (f Furniture) updateIDs(id ObjectID, newIDs map[ObjectID]ObjectID) DBObject {
    782 	if n, exists := newIDs[id]; exists {
    783 		f.id = n
    784 	} else {
    785 		f.id = id
    786 	}
    787 	return f
    788 }
    789 
    790 func (f Furniture) SetOldID(id ObjectID) DBObject {
    791 	if id != f.id {
    792 		f.OldID = &id
    793 	}
    794 	return f
    795 }
    796 
    797 func (fm FurnitureMap) ID() ObjectID {
    798 	return fm.id
    799 }
    800 
    801 func (fm FurnitureMap) updateIDs(id ObjectID, newIDs map[ObjectID]ObjectID) DBObject {
    802 	if n, exists := newIDs[id]; exists {
    803 		fm.id = n
    804 	} else {
    805 		fm.id = id
    806 	}
    807 	if n, exists := newIDs[fm.FurnitureID]; exists {
    808 		fm.FurnitureID = n
    809 	}
    810 	return fm
    811 }
    812 
    813 func (fm FurnitureMap) SetOldID(id ObjectID) DBObject {
    814 	if id != fm.id {
    815 		fm.OldID = &id
    816 	}
    817 	return fm
    818 }
    819 
    820 func (p Point) ID() ObjectID {
    821 	return p.id
    822 }
    823 
    824 func (p Point) updateIDs(id ObjectID, newIDs map[ObjectID]ObjectID) DBObject {
    825 	if n, exists := newIDs[id]; exists {
    826 		p.id = n
    827 	} else {
    828 		p.id = id
    829 	}
    830 	return p
    831 }
    832 
    833 func (p Point) SetOldID(id ObjectID) DBObject {
    834 	if id != p.id {
    835 		p.OldID = &id
    836 	}
    837 	return p
    838 }
    839 
    840 func (m PointMap) ID() ObjectID {
    841 	return m.id
    842 }
    843 
    844 func (m PointMap) updateIDs(id ObjectID, newIDs map[ObjectID]ObjectID) DBObject {
    845 	if n, exists := newIDs[id]; exists {
    846 		m.id = n
    847 	} else {
    848 		m.id = id
    849 	}
    850 	if n, exists := newIDs[m.A]; exists {
    851 		m.A = n
    852 	}
    853 	if n, exists := newIDs[m.B]; exists {
    854 		m.B = n
    855 	}
    856 	return m
    857 }
    858 
    859 func (m PointMap) SetOldID(id ObjectID) DBObject {
    860 	if id != m.id {
    861 		m.OldID = &id
    862 	}
    863 	return m
    864 }
    865 
    866 /*
    867  * Parse patch path's of the form
    868  * 	/table/id
    869  */
    870 func parsePath(p string) (ObjectID, error) {
    871 	parts := strings.Split(p, "/")
    872 	if parts[0] != "" {
    873 		return ObjectID{}, errors.New(p + ": Requires absolute path (start with /)")
    874 	}
    875 	if len(parts) < 3 {
    876 		return ObjectID{}, errors.New(p + ": Object does not exist")
    877 	}
    878 
    879 	id, err := ParseObjectID(parts[len(parts) - 1])
    880 	if err != nil {
    881 		return ObjectID{}, err
    882 	}
    883 
    884 	parts = parts[1:len(parts) - 1]
    885 	typ, err := pathIDType(strings.Join(parts, "/"))
    886 	if err != nil {
    887 		return ObjectID{}, err
    888 	}
    889 
    890 	if typ != id.Type {
    891 		return ObjectID{}, errors.New("Invalid ID for that path")
    892 	}
    893 	return id, nil
    894 }
    895 
    896 func pathIDType(p string) (ObjectType, error) {
    897 	switch p {
    898 	case "points":
    899 		return IDTypePoint, nil
    900 	case "pointmaps":
    901 		return IDTypePointMap, nil
    902 	case "furniture":
    903 		return IDTypeFurniture, nil
    904 	case "furniture_maps":
    905 		return IDTypeFurnitureMap, nil
    906 	}
    907 	return "", errors.New(p + ": Invalid path prefix")
    908 }
    909 
    910 func scanPoint(s Scanner) (Point, error) {
    911 	var p Point
    912 	var id int64
    913 
    914 	err := s.Scan(&id, &p.X, &p.Y)
    915 	if err != nil {
    916 		return p, err
    917 	}
    918 	p.id = makeID(IDTypePoint, id)
    919 	return p, nil
    920 }
    921 
    922 func scanPointMap(s Scanner) (PointMap, error) {
    923 	var pm PointMap
    924 	var id, a, b int64
    925 
    926 	err := s.Scan(&id, &pm.Type, &a, &b, &pm.DoorSwing)
    927 	if err != nil {
    928 		return pm, err
    929 	}
    930 	pm.id = makeID(IDTypePointMap, id)
    931 	pm.A = makeID(IDTypePoint, a)
    932 	pm.B = makeID(IDTypePoint, b)
    933 	return pm, nil
    934 }
    935 
    936 func scanFurniture(row Scanner) (Furniture, error) {
    937 	var f Furniture
    938 	var id int64
    939 
    940 	err := row.Scan(&id, &f.Type, &f.UserType, &f.Style, &f.Name, &f.Width, &f.Depth)
    941 	if err != nil {
    942 		return f, err
    943 	}
    944 	f.id = makeID(IDTypeFurniture, id)
    945 	return f, nil
    946 }
    947 
    948 func scanFurnitureMap(row Scanner) (FurnitureMap, error) {
    949 	var m FurnitureMap
    950 	var id int64
    951 	var fid int64
    952 
    953 	err := row.Scan(&id, &fid, &m.Layout, &m.X, &m.Y, &m.Angle)
    954 	if err != nil {
    955 		return m, err
    956 	}
    957 	m.id = makeID(IDTypeFurnitureMap, id)
    958 	m.FurnitureID = makeID(IDTypeFurniture, fid)
    959 	return m, nil
    960 }
    961 
    962 func mapArray[K comparable, V any](array []V, mapper func(V) (K, error)) (map[K]V, error) {
    963 	m := make(map[K]V, len(array))
    964 
    965 	for _, v := range array {
    966 		key, err := mapper(v)
    967 		if err != nil {
    968 			return nil, err
    969 		}
    970 		m[key] = v
    971 	}
    972 	return m, nil
    973 }
    974 
    975 func mapPoint(p Point) (ObjectID, error) {
    976 	return p.id, nil
    977 }
    978 
    979 func mapPointMap(pm PointMap) (ObjectID, error) {
    980 	return pm.id, nil
    981 }
    982 
    983 func mapFurniture(f Furniture) (ObjectID, error) {
    984 	return f.id, nil
    985 }
    986 
    987 func mapFurnitureMap(m FurnitureMap) (ObjectID, error) {
    988 	return m.id, nil
    989 }
    990 
    991 func (o ObjectID) Path() string {
    992 	/* Type should be verified, so this shouldn't panic */
    993 	return fmt.Sprintf("/%s/%s", tables[o.Type], o)
    994 }
    995 
    996 func (id ObjectID) Error(context string, reason error) PatchError {
    997 	return patchError(&id, context, reason)
    998 }
    999 
   1000 func patchError(id *ObjectID, context string, reason error) PatchError {
   1001 	if id != nil {
   1002 		// Copy
   1003 		d := *id
   1004 		id = &d
   1005 	}
   1006 	return PatchError{ID: id, Context: context, Reason: reason}
   1007 }
   1008 
   1009 func (e PatchError) Error() string {
   1010 	var s string
   1011 
   1012 	if e.ID != nil {
   1013 		s = fmt.Sprintf("%s", e.ID)
   1014 	}
   1015 	if len(e.Context) > 0 {
   1016 		s = fmt.Sprintf("%s: %s", s, e.Context)
   1017 	}
   1018 	if e.Reason != nil {
   1019 		s = fmt.Sprintf("%s: %s", s, e.Reason.Error())
   1020 	}
   1021 	return s
   1022 }