diff --git a/models/models.go b/models/models.go index b51f373..6f7c689 100644 --- a/models/models.go +++ b/models/models.go @@ -5,6 +5,7 @@ import geojson "github.com/paulmach/go.geojson" // core objects type UserID int +type FeatureID int type User struct { ID UserID `json:"id"` @@ -12,7 +13,7 @@ type User struct { } type Feature struct { - ID int `json:"id"` + ID FeatureID `json:"id"` OwnerID *UserID `json:"owner_id"` Name string `json:"name"` Description *string `json:"description"` @@ -23,9 +24,9 @@ type Feature struct { // transport objects type Update struct { - Create []Feature `json:"create"` - Update []Feature `json:"update"` - Delete []UserID `json:"delete"` + Create []Feature `json:"create"` + Update []Feature `json:"update"` + Delete []FeatureID `json:"delete"` } type HandshakeChallenge struct { diff --git a/server/handling.go b/server/handling.go index 5de39e7..c6e5a28 100644 --- a/server/handling.go +++ b/server/handling.go @@ -105,7 +105,7 @@ func (s *Server) handlePOSTHandshake(gc *gin.Context) { var hs models.HandshakeChallenge err := gc.ShouldBindJSON(&hs) if err != nil { - gc.String(http.StatusBadRequest, "malformed handshake challenge") + gc.String(http.StatusBadRequest, fmt.Sprintf("malformed handshake challenge: %v", err)) return } @@ -140,7 +140,19 @@ func (s *Server) handleGETData(gc *gin.Context) { } func (s *Server) handlePOSTData(gc *gin.Context) { - panic("not implemented") + var data models.Update + err := gc.ShouldBindJSON(&data) + if err != nil { + gc.String(http.StatusBadRequest, fmt.Sprintf("malformed data: %v", err)) + return + } + + err = s.update(data) + if err != nil { + internalError(gc, fmt.Errorf("failed to update data: %w", err)) + return + } + gc.Status(http.StatusNoContent) } func (s *Server) handleGETDataPeople(gc *gin.Context) { diff --git a/server/initdb.sql b/server/initdb.sql index 9c3ce5f..36fbc02 100644 --- a/server/initdb.sql +++ b/server/initdb.sql @@ -8,9 +8,10 @@ INSERT INTO users(id, name) VALUES(0, 'system'); DROP TABLE IF EXISTS features; CREATE TABLE IF NOT EXISTS features ( id integer PRIMARY KEY AUTOINCREMENT, - owner_id integer REFERENCES users(id) ON DELETE CASCADE, + owner_id integer, name text NOT NULL, description text, category text, - geom text NOT NULL + geom text NOT NULL, + FOREIGN KEY(owner_id) REFERENCES users(id) ON DELETE CASCADE ); diff --git a/server/server.go b/server/server.go index d9cd98a..9ffcb67 100644 --- a/server/server.go +++ b/server/server.go @@ -78,6 +78,15 @@ func (s *Server) Run(ctx context.Context) { s.log.Info("Server exitting.") } +func (s *Server) getDbConn() *sqlite.Conn { + conn := s.dbpool.Get(s.ctx) + _, err := conn.Prep("PRAGMA foreign_keys = ON").Step() + if err != nil { + panic(err) + } + return conn +} + func (s *Server) setupDB(reinit bool) { dbpool, err := sqlitex.Open(fmt.Sprintf("file:%s", s.config.DbPath), 0, 10) if err != nil { @@ -86,7 +95,7 @@ func (s *Server) setupDB(reinit bool) { s.dbpool = dbpool if reinit { - conn := s.dbpool.Get(s.ctx) + conn := s.getDbConn() defer s.dbpool.Put(conn) err = sqlitex.ExecScript(conn, initDB) @@ -97,7 +106,7 @@ func (s *Server) setupDB(reinit bool) { } func (s *Server) setupTiles() { - tsRootURL, err := url.Parse("/tileserver") + tsRootURL, err := url.Parse(URITileserverRoot) if err != nil { s.log.WithError(err).Fatal("Failed to parse tileserver root URL.") } @@ -119,7 +128,7 @@ func (s *Server) setupTiles() { } func (s *Server) handshake(hc models.HandshakeChallenge) (models.UserID, error) { - conn := s.dbpool.Get(s.ctx) + conn := s.getDbConn() defer s.dbpool.Put(conn) userID, err := func() (uid int, err error) { @@ -162,7 +171,7 @@ func (s *Server) handshake(hc models.HandshakeChallenge) (models.UserID, error) } func (s *Server) getData() (models.Data, error) { - conn := s.dbpool.Get(s.ctx) + conn := s.getDbConn() defer s.dbpool.Put(conn) return func() (data models.Data, err error) { @@ -184,9 +193,42 @@ func (s *Server) getData() (models.Data, error) { }() } +func (s *Server) update(data models.Update) error { + conn := s.getDbConn() + defer s.dbpool.Put(conn) + + return func() (err error) { + defer sqlitex.Save(conn)(&err) + + if data.Create != nil { + err = s.addFeatures(conn, data.Create) + if err != nil { + err = fmt.Errorf("failed to add features: %w", err) + return + } + } + if data.Update != nil { + err = s.updateFeatures(conn, data.Update) + if err != nil { + err = fmt.Errorf("failed to update features: %w", err) + return + } + } + if data.Delete != nil { + err = s.deleteFeatures(conn, data.Delete) + if err != nil { + err = fmt.Errorf("failed to delete features: %w", err) + return + } + } + + return nil + }() +} + func (s *Server) getPeople(conn *sqlite.Conn) ([]models.User, error) { if conn == nil { - conn = s.dbpool.Get(s.ctx) + conn = s.getDbConn() defer s.dbpool.Put(conn) } @@ -206,7 +248,7 @@ func (s *Server) getPeople(conn *sqlite.Conn) ([]models.User, error) { func (s *Server) getFeatures(conn *sqlite.Conn) ([]models.Feature, error) { if conn == nil { - conn = s.dbpool.Get(s.ctx) + conn = s.getDbConn() defer s.dbpool.Put(conn) } @@ -235,7 +277,7 @@ func (s *Server) getFeatures(conn *sqlite.Conn) ([]models.Feature, error) { } feature := models.Feature{ - ID: id, + ID: models.FeatureID(id), OwnerID: (*models.UserID)(ownerID), Name: name, Description: description, @@ -251,3 +293,125 @@ func (s *Server) getFeatures(conn *sqlite.Conn) ([]models.Feature, error) { } return features, nil } + +func (s *Server) addFeatures(conn *sqlite.Conn, features []models.Feature) error { + stmt, err := conn.Prepare("insert into features(owner_id, name, description, category, geom) values(?, ?, ?, ?, ?)") + if err != nil { + return fmt.Errorf("failed to prepare statement: %w", err) + } + defer stmt.Finalize() + + for _, feature := range features { + err := stmt.Reset() + if err != nil { + return fmt.Errorf("failed to reset prepared statement: %w", err) + } + err = stmt.ClearBindings() + if err != nil { + return fmt.Errorf("failed to clear bindings of prepared statement: %w", err) + } + + if feature.OwnerID == nil { + stmt.BindNull(1) + } else { + stmt.BindInt64(1, int64(*feature.OwnerID)) + } + stmt.BindText(2, feature.Name) + if feature.Description == nil { + stmt.BindNull(3) + } else { + stmt.BindText(3, *feature.Description) + } + if feature.Category == nil { + stmt.BindNull(4) + } else { + stmt.BindText(4, *feature.Category) + } + geomBytes, err := json.Marshal(feature.Geometry) + if err != nil { + return fmt.Errorf("failed to marshal geometry of feature ID %d: %w", feature.ID, err) + } + stmt.BindText(5, string(geomBytes)) + + _, err = stmt.Step() + if err != nil { + return fmt.Errorf("failed to evaluate prepared statement: %w", err) + } + } + return nil +} + +func (s *Server) updateFeatures(conn *sqlite.Conn, features []models.Feature) error { + stmt, err := conn.Prepare("update features set owner_id = ?, name = ?, description = ?, category = ?, geom = ? where id = ?") + if err != nil { + return fmt.Errorf("failed to prepare statement: %w", err) + } + defer stmt.Finalize() + + for _, feature := range features { + err := stmt.Reset() + if err != nil { + return fmt.Errorf("failed to reset prepared statement: %w", err) + } + err = stmt.ClearBindings() + if err != nil { + return fmt.Errorf("failed to clear bindings of prepared statement: %w", err) + } + + if feature.OwnerID == nil { + stmt.BindNull(1) + } else { + stmt.BindInt64(1, int64(*feature.OwnerID)) + } + stmt.BindText(2, feature.Name) + if feature.Description == nil { + stmt.BindNull(3) + } else { + stmt.BindText(3, *feature.Description) + } + if feature.Category == nil { + stmt.BindNull(4) + } else { + stmt.BindText(4, *feature.Category) + } + geomBytes, err := json.Marshal(feature.Geometry) + if err != nil { + return fmt.Errorf("failed to marshal geometry of feature ID %d: %w", feature.ID, err) + } + stmt.BindText(5, string(geomBytes)) + stmt.BindInt64(6, int64(feature.ID)) + + _, err = stmt.Step() + if err != nil { + return fmt.Errorf("failed to evaluate prepared statement: %w", err) + } + } + return nil +} + +func (s *Server) deleteFeatures(conn *sqlite.Conn, featureIDs []models.FeatureID) error { + stmt, err := conn.Prepare("delete from features where id = ?") + if err != nil { + return fmt.Errorf("failed to prepare statement: %w", err) + } + defer stmt.Finalize() + + for _, featureID := range featureIDs { + err := stmt.Reset() + if err != nil { + return fmt.Errorf("failed to reset prepared statement: %w", err) + } + err = stmt.ClearBindings() + if err != nil { + return fmt.Errorf("failed to clear bindings of prepared statement: %w", err) + } + + stmt.BindInt64(1, int64(featureID)) + + _, err = stmt.Step() + if err != nil { + return fmt.Errorf("failed to evaluate prepared statement: %w", err) + } + } + return nil +} diff --git a/server/uris.go b/server/uris.go index c01ab2a..56aac00 100644 --- a/server/uris.go +++ b/server/uris.go @@ -1,14 +1,15 @@ package server const ( - URIPing = "/ping" - URIHardFail = "/hard-fail" - URISoftFail = "/soft-fail" - URITilepack = "/tilepack" - URIHandshake = "/handshake" - URIData = "/data" - URIDataPeople = "/data/people" - URIDataExtra = "/data/extra" - URIDataFeatures = "/data/features" - URITileserver = "/tileserver/*x" + URIPing = "/ping" + URIHardFail = "/hard-fail" + URISoftFail = "/soft-fail" + URITilepack = "/tilepack" + URIHandshake = "/handshake" + URIData = "/data" + URIDataPeople = "/data/people" + URIDataExtra = "/data/extra" + URIDataFeatures = "/data/features" + URITileserverRoot = "/tileserver" + URITileserver = URITileserverRoot + "/*x" )