Enable moving own features on MapLibre and switch to raster tiles.
CI / test (push) Successful in 4s
CI / test (push) Successful in 4s
Add feature geometry PATCH API support and update MapLibre demo to use OSM raster tiles, load all public/owned features, and let logged-in users drag their own feature markers to persist new positions. Made-with: Cursor
This commit is contained in:
@@ -443,6 +443,23 @@ func (s *Service) DeleteFeature(ownerKey, featureID string) error {
|
||||
return s.store.DeleteFeature(featureID)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateFeatureGeometry(ownerKey, featureID string, geometry store.Point) (store.Feature, error) {
|
||||
feature, err := s.store.GetFeature(featureID)
|
||||
if err != nil {
|
||||
return store.Feature{}, ErrFeatureMiss
|
||||
}
|
||||
if feature.OwnerKey != ownerKey {
|
||||
return store.Feature{}, ErrForbidden
|
||||
}
|
||||
if err := validatePoint(geometry); err != nil {
|
||||
return store.Feature{}, err
|
||||
}
|
||||
feature.Geometry = geometry
|
||||
feature.UpdatedAt = time.Now().UTC()
|
||||
s.store.SaveFeature(feature)
|
||||
return feature, nil
|
||||
}
|
||||
|
||||
type CreateAssetInput struct {
|
||||
FeatureID string
|
||||
Checksum string
|
||||
|
||||
@@ -308,6 +308,28 @@ func TestCollectionOwnershipIsolation(t *testing.T) {
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("expected 403, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
featureID := createFeatureData["id"].(string)
|
||||
|
||||
patchOwnResp, patchOwnData := patchJSON(t, client, server.URL+"/v1/features/"+featureID, map[string]interface{}{
|
||||
"geometry": map[string]interface{}{
|
||||
"type": "Point",
|
||||
"coordinates": []float64{-16.6299, 28.4639, 11},
|
||||
},
|
||||
}, user1Token)
|
||||
if patchOwnResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("owner patch feature status=%d body=%v", patchOwnResp.StatusCode, patchOwnData)
|
||||
}
|
||||
|
||||
patchOtherResp, patchOtherData := patchJSON(t, client, server.URL+"/v1/features/"+featureID, map[string]interface{}{
|
||||
"geometry": map[string]interface{}{
|
||||
"type": "Point",
|
||||
"coordinates": []float64{-16.6301, 28.4641, 12},
|
||||
},
|
||||
}, user2Token)
|
||||
if patchOtherResp.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("non-owner patch feature status=%d body=%v", patchOtherResp.StatusCode, patchOtherData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssetLifecycleAndVisibility(t *testing.T) {
|
||||
|
||||
@@ -42,6 +42,7 @@ func (a *API) Routes() http.Handler {
|
||||
mux.HandleFunc("POST /v1/collections/{id}/features", a.createFeature)
|
||||
mux.HandleFunc("GET /v1/collections/{id}/features", a.listFeatures)
|
||||
mux.HandleFunc("GET /v1/features/public", a.listPublicFeatures)
|
||||
mux.HandleFunc("PATCH /v1/features/{id}", a.patchFeature)
|
||||
mux.HandleFunc("DELETE /v1/features/{id}", a.deleteFeature)
|
||||
mux.HandleFunc("POST /v1/assets", a.createAsset)
|
||||
mux.HandleFunc("PATCH /v1/assets/{id}", a.patchAsset)
|
||||
@@ -393,6 +394,28 @@ func (a *API) deleteFeature(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (a *API) patchFeature(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := a.authUser(r)
|
||||
if err != nil {
|
||||
writeErr(w, err)
|
||||
return
|
||||
}
|
||||
featureID := r.PathValue("id")
|
||||
var req struct {
|
||||
Geometry store.Point `json:"geometry"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
writeErr(w, app.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
feature, err := a.service.UpdateFeatureGeometry(user, featureID, req.Geometry)
|
||||
if err != nil {
|
||||
writeErr(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, feature)
|
||||
}
|
||||
|
||||
func (a *API) createAsset(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := a.authUser(r)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user