eintopf.info@v0.13.16/service/place/place.go (about) 1 // Copyright (C) 2022 The Eintopf authors 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <https://www.gnu.org/licenses/>. 15 16 package place 17 18 import ( 19 "context" 20 "fmt" 21 "strings" 22 23 "eintopf.info/internal/crud" 24 "eintopf.info/internal/xerror" 25 "eintopf.info/service/auth" 26 ) 27 28 // Place defines a place entity 29 type NewPlace struct { 30 Published bool `json:"published"` 31 Name string `json:"name"` 32 Address string `json:"address"` 33 Lat float64 `json:"lat"` 34 Lng float64 `json:"lng"` 35 Link string `json:"link"` 36 Email string `json:"email"` 37 Description string `json:"description"` 38 Image string `json:"image"` 39 OwnedBy []string `json:"ownedBy" db:"owned_by"` 40 } 41 42 // IsOwned returns true if the id is in the OwnedBy field. 43 func (p *NewPlace) IsOwned(id string) bool { 44 owned := false 45 for _, owner := range p.OwnedBy { 46 if owner == id { 47 owned = true 48 } 49 } 50 return owned 51 } 52 53 // Place defines a place entity 54 // It implements indexo.Coppyable. 55 type Place struct { 56 ID string `json:"id" db:"id"` 57 Deactivated bool `json:"deactivated" db:"deactivated"` 58 Published bool `json:"published" db:"published"` 59 Name string `json:"name" db:"name"` 60 Description string `json:"description" db:"description"` 61 Email string `json:"email" db:"email"` 62 Image string `json:"image" db:"image"` 63 Link string `json:"link" db:"link"` 64 Address string `json:"address" db:"address"` 65 Lat float64 `json:"lat" db:"lat"` 66 Lng float64 `json:"lng" db:"lng"` 67 OwnedBy []string `json:"ownedBy" db:"owned_by"` 68 } 69 70 func (p Place) Identifier() string { return p.ID } 71 72 func PlaceFromNewPlace(newPlace *NewPlace, id string) *Place { 73 return &Place{ 74 ID: id, 75 Deactivated: false, 76 Name: newPlace.Name, 77 Address: newPlace.Address, 78 Lat: newPlace.Lat, 79 Lng: newPlace.Lng, 80 Link: newPlace.Link, 81 Email: newPlace.Email, 82 Description: newPlace.Description, 83 Published: newPlace.Published, 84 Image: newPlace.Image, 85 OwnedBy: newPlace.OwnedBy, 86 } 87 } 88 89 // Indexable indicates wether a place should be added to the search index. 90 func (p *Place) Indexable() bool { 91 return !p.Deactivated 92 } 93 94 // Listable indicates wether a place should be shown on the list page. 95 func (p *Place) Listable() bool { 96 return p.Published 97 } 98 99 // IsOwned returns true if the id is in the OwnedBy field. 100 func (p *Place) IsOwned(id string) bool { 101 owned := false 102 for _, owner := range p.OwnedBy { 103 if owner == id { 104 owned = true 105 } 106 } 107 return owned 108 } 109 110 func (p *Place) Coordinates() (float64, float64) { 111 return p.Lat, p.Lng 112 } 113 114 // Service defines the crud service to manage places. 115 // -go:generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/place Service --output=../../internal/mock/place_service.go --package=mock --mock-name=PlaceService 116 type Service interface { 117 Storer 118 } 119 120 // SortOrder defines the order of sorting. 121 type SortOrder string 122 123 // Possible values for SortOrder. 124 const ( 125 OrderAsc = SortOrder("ASC") 126 OrderDesc = SortOrder("DESC") 127 ) 128 129 // FindFilters defines the possible filters for the find method. 130 type FindFilters struct { 131 ID *string `json:"id"` 132 NotID *string `json:"notID"` 133 Deactivated *bool `json:"deactivated"` 134 Published *bool `json:"published"` 135 Name *string `json:"name"` 136 LikeName *string `json:"likeName"` 137 Description *string `json:"description"` 138 OwnedBy []string `json:"ownedBy"` 139 } 140 141 // Storer defines a service for CRUD operations on the event model. 142 // -go:generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/place Storer --output=../../internal/mock/place_store.go --package=mock --mock-name=PlaceStore 143 type Storer interface { 144 Create(ctx context.Context, newPlace *NewPlace) (*Place, error) 145 Update(ctx context.Context, place *Place) (*Place, error) 146 Delete(ctx context.Context, id string) error 147 FindByID(ctx context.Context, id string) (*Place, error) 148 Find(ctx context.Context, params *crud.FindParams[FindFilters]) ([]*Place, int, error) 149 } 150 151 // NewService returns a new place service. 152 func NewService(store Storer) Service { 153 return &service{store: store} 154 } 155 156 type service struct { 157 store Storer 158 } 159 160 var ErrNameAlreadyExists = xerror.BadInputError{Err: fmt.Errorf("name already exists")} 161 162 // Create makes sure the loggedin user is in the owned field. 163 func (s *service) Create(ctx context.Context, newPlace *NewPlace) (*Place, error) { 164 if s.placeNameExists(ctx, newPlace.Name, "") { 165 return nil, ErrNameAlreadyExists 166 } 167 id, err := auth.UserIDFromContext(ctx) 168 if err != nil { 169 return nil, err 170 } 171 if !newPlace.IsOwned(id) { 172 newPlace.OwnedBy = append(newPlace.OwnedBy, id) 173 } 174 newPlace.Name = strings.TrimSpace(newPlace.Name) 175 return s.store.Create(ctx, newPlace) 176 } 177 178 func (s *service) Update(ctx context.Context, place *Place) (*Place, error) { 179 if s.placeNameExists(ctx, place.Name, place.ID) { 180 return nil, ErrNameAlreadyExists 181 } 182 return s.store.Update(ctx, place) 183 } 184 185 func (s *service) Delete(ctx context.Context, id string) error { 186 return s.store.Delete(ctx, id) 187 } 188 189 func (s *service) FindByID(ctx context.Context, id string) (*Place, error) { 190 return s.store.FindByID(ctx, id) 191 } 192 193 // Find adds a special filter value "self" for the OwnedBy field. If it is set 194 // the value gets replaced with the id of the loggedin user. 195 func (s *service) Find(ctx context.Context, params *crud.FindParams[FindFilters]) ([]*Place, int, error) { 196 if params != nil && params.Filters != nil && params.Filters.OwnedBy != nil { 197 if len(params.Filters.OwnedBy) == 1 && params.Filters.OwnedBy[0] == "self" { 198 id, err := auth.UserIDFromContext(ctx) 199 if err == nil { 200 params.Filters.OwnedBy = []string{id} 201 } 202 } 203 } 204 return s.store.Find(ctx, params) 205 } 206 207 func (s *service) placeNameExists(ctx context.Context, name string, id string) bool { 208 existingPlaces, _, err := s.store.Find(auth.ContextWithRole(ctx, auth.RoleInternal), &crud.FindParams[FindFilters]{ 209 Filters: &FindFilters{Name: &name, NotID: &id}, 210 }) 211 if err != nil || len(existingPlaces) > 0 { 212 return true 213 } 214 return false 215 }