Demo app (web/):
- Collections: select, rename, remove (x button per row), delete
- Features: add point (lon/lat validation), remove, list in selected collection
- QR codes for pk (private) and pb (public) keys
- Restore public key from private key
- 409 Conflict handled for already-registered login
- Title: Momswap Geo Backend Use-Cases Test
Backend:
- PATCH /v1/collections/{id} for rename
- DELETE /v1/collections/{id}
- Clearer lon/lat validation errors (-180..180, -90..90)
Client:
- updateCollection, deleteCollection, derivePublicKey
Docs:
- docs/frontend-development.md (demo app, local dev)
- README links to all docs
Made-with: Cursor
This commit is contained in:
+32
-2
@@ -244,8 +244,11 @@ func validatePoint(point store.Point) error {
|
||||
return fmt.Errorf("%w: coordinates must have lon/lat", ErrBadRequest)
|
||||
}
|
||||
lon, lat := point.Coordinates[0], point.Coordinates[1]
|
||||
if lon < -180 || lon > 180 || lat < -90 || lat > 90 {
|
||||
return fmt.Errorf("%w: invalid lon/lat bounds", ErrBadRequest)
|
||||
if lon < -180 || lon > 180 {
|
||||
return fmt.Errorf("%w: longitude must be -180 to 180, got %.2f", ErrBadRequest, lon)
|
||||
}
|
||||
if lat < -90 || lat > 90 {
|
||||
return fmt.Errorf("%w: latitude must be -90 to 90, got %.2f", ErrBadRequest, lat)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -272,6 +275,33 @@ func (s *Service) ListCollections(ownerKey string) []store.Collection {
|
||||
return s.store.ListCollectionsByOwner(ownerKey)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateCollection(ownerKey, collectionID, name string) (store.Collection, error) {
|
||||
collection, err := s.store.GetCollection(collectionID)
|
||||
if err != nil {
|
||||
return store.Collection{}, ErrCollectionMiss
|
||||
}
|
||||
if collection.OwnerKey != ownerKey {
|
||||
return store.Collection{}, ErrForbidden
|
||||
}
|
||||
if name == "" {
|
||||
return store.Collection{}, fmt.Errorf("%w: collection name required", ErrBadRequest)
|
||||
}
|
||||
collection.Name = name
|
||||
s.store.SaveCollection(collection)
|
||||
return collection, nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteCollection(ownerKey, collectionID string) error {
|
||||
collection, err := s.store.GetCollection(collectionID)
|
||||
if err != nil {
|
||||
return ErrCollectionMiss
|
||||
}
|
||||
if collection.OwnerKey != ownerKey {
|
||||
return ErrForbidden
|
||||
}
|
||||
return s.store.DeleteCollection(collectionID)
|
||||
}
|
||||
|
||||
func (s *Service) CreateFeature(ownerKey, collectionID string, geometry store.Point, properties map[string]interface{}) (store.Feature, error) {
|
||||
collection, err := s.store.GetCollection(collectionID)
|
||||
if err != nil {
|
||||
|
||||
@@ -35,6 +35,8 @@ func (a *API) Routes() http.Handler {
|
||||
|
||||
mux.HandleFunc("POST /v1/collections", a.createCollection)
|
||||
mux.HandleFunc("GET /v1/collections", a.listCollections)
|
||||
mux.HandleFunc("PATCH /v1/collections/{id}", a.updateCollection)
|
||||
mux.HandleFunc("DELETE /v1/collections/{id}", a.deleteCollection)
|
||||
mux.HandleFunc("POST /v1/collections/{id}/features", a.createFeature)
|
||||
mux.HandleFunc("GET /v1/collections/{id}/features", a.listFeatures)
|
||||
mux.HandleFunc("DELETE /v1/features/{id}", a.deleteFeature)
|
||||
@@ -272,6 +274,42 @@ func (a *API) listCollections(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"collections": a.service.ListCollections(user)})
|
||||
}
|
||||
|
||||
func (a *API) updateCollection(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := a.authUser(r)
|
||||
if err != nil {
|
||||
writeErr(w, err)
|
||||
return
|
||||
}
|
||||
collectionID := r.PathValue("id")
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
writeErr(w, app.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
collection, err := a.service.UpdateCollection(user, collectionID, req.Name)
|
||||
if err != nil {
|
||||
writeErr(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, collection)
|
||||
}
|
||||
|
||||
func (a *API) deleteCollection(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := a.authUser(r)
|
||||
if err != nil {
|
||||
writeErr(w, err)
|
||||
return
|
||||
}
|
||||
collectionID := r.PathValue("id")
|
||||
if err := a.service.DeleteCollection(user, collectionID); err != nil {
|
||||
writeErr(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (a *API) createFeature(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := a.authUser(r)
|
||||
if err != nil {
|
||||
|
||||
@@ -16,6 +16,7 @@ type Store interface {
|
||||
SaveCollection(c Collection)
|
||||
ListCollectionsByOwner(owner string) []Collection
|
||||
GetCollection(id string) (Collection, error)
|
||||
DeleteCollection(id string) error
|
||||
SaveFeature(f Feature)
|
||||
ListFeaturesByCollection(collectionID string) []Feature
|
||||
GetFeature(featureID string) (Feature, error)
|
||||
|
||||
@@ -157,6 +157,21 @@ func (s *MemoryStore) GetCollection(id string) (Collection, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) DeleteCollection(id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, ok := s.collections[id]; !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
for fid, f := range s.features {
|
||||
if f.CollectionID == id {
|
||||
delete(s.features, fid)
|
||||
}
|
||||
}
|
||||
delete(s.collections, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) SaveFeature(f Feature) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -204,6 +204,18 @@ func (s *PostgresStore) GetCollection(id string) (Collection, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *PostgresStore) DeleteCollection(id string) error {
|
||||
res, err := s.db.Exec(`DELETE FROM collections WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PostgresStore) SaveFeature(f Feature) {
|
||||
geom, _ := json.Marshal(f.Geometry)
|
||||
props, _ := json.Marshal(f.Properties)
|
||||
|
||||
Reference in New Issue
Block a user