eintopf.info@v0.13.16/service/group/group.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 group 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 // Group defines a group entity 29 type NewGroup struct { 30 Published bool `json:"published"` 31 Name string `json:"name"` 32 Link string `json:"link"` 33 Email string `json:"email"` 34 Description string `json:"description"` 35 Image string `json:"image"` 36 OwnedBy []string `json:"ownedBy" db:"owned_by"` 37 } 38 39 // IsOwned returns true if the id is in the OwnedBy field. 40 func (g *NewGroup) IsOwned(id string) bool { 41 owned := false 42 for _, owner := range g.OwnedBy { 43 if owner == id { 44 owned = true 45 } 46 } 47 return owned 48 } 49 50 // Group defines a group entity 51 // It implements indexo.Coppyable. 52 type Group struct { 53 ID string `json:"id" db:"id"` 54 Deactivated bool `json:"deactivated" db:"deactivated"` 55 Published bool `json:"published" db:"published"` 56 Name string `json:"name" db:"name"` 57 Link string `json:"link" db:"link"` 58 Email string `json:"email" db:"email"` 59 Description string `json:"description" db:"description"` 60 Image string `json:"image" db:"image"` 61 OwnedBy []string `json:"ownedBy" db:"owned_by"` 62 } 63 64 func (g Group) Identifier() string { return g.ID } 65 66 func GroupFromNewGroup(newGroup *NewGroup, id string) *Group { 67 return &Group{ 68 ID: id, 69 Deactivated: false, 70 Name: newGroup.Name, 71 Link: newGroup.Link, 72 Email: newGroup.Email, 73 Description: newGroup.Description, 74 OwnedBy: newGroup.OwnedBy, 75 Image: newGroup.Image, 76 Published: newGroup.Published, 77 } 78 } 79 80 // Indexable indicates wether the group should be added to the search index. 81 func (g *Group) Indexable() bool { 82 return !g.Deactivated 83 } 84 85 // Listed indicates wether the group should be shown on the list page. 86 func (g *Group) Listable() bool { 87 return g.Published 88 } 89 90 // IsOwned returns true if the id is in the OwnedBy field. 91 func (g *Group) IsOwned(id string) bool { 92 owned := false 93 for _, owner := range g.OwnedBy { 94 if owner == id { 95 owned = true 96 } 97 } 98 return owned 99 } 100 101 // Service defines the crud service to manage groups. 102 // -go:generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/group Service --output=../../internal/mock/group_service.go --package=mock --mock-name=GroupService 103 type Service interface { 104 Storer 105 } 106 107 // SortOrder defines the order of sorting. 108 type SortOrder string 109 110 // Possible values for SortOrder. 111 const ( 112 OrderAsc = SortOrder("ASC") 113 OrderDesc = SortOrder("DESC") 114 ) 115 116 // FindFilters defines the possible filters for the find method. 117 type FindFilters struct { 118 ID *string `json:"id"` 119 NotID *string `json:"not_id"` 120 Deactivated *bool `json:"deactivated"` 121 Published *bool `json:"published"` 122 Name *string `json:"name"` 123 LikeName *string `json:"likeName"` 124 Link *string `json:"link"` 125 Email *string `json:"email"` 126 Description *string `json:"description"` 127 OwnedBy []string `json:"ownedBy"` 128 } 129 130 // Storer defines a service for CRUD operations on the event model. 131 // -go:generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/group Storer --output=../../internal/mock/group_store.go --package=mock --mock-name=GroupStore 132 type Storer interface { 133 Create(ctx context.Context, newGroup *NewGroup) (*Group, error) 134 Update(ctx context.Context, group *Group) (*Group, error) 135 Delete(ctx context.Context, id string) error 136 FindByID(ctx context.Context, id string) (*Group, error) 137 Find(ctx context.Context, params *crud.FindParams[FindFilters]) ([]*Group, int, error) 138 } 139 140 // NewService returns a new group service. 141 func NewService(store Storer) *service { 142 return &service{store: store} 143 } 144 145 type service struct { 146 store Storer 147 } 148 149 var ErrNameAlreadyExists = xerror.BadInputError{Err: fmt.Errorf("name already exists")} 150 151 // Create makes sure the loggedin user is in the owned field. 152 func (s *service) Create(ctx context.Context, newGroup *NewGroup) (*Group, error) { 153 if s.groupNameExists(ctx, newGroup.Name, "") { 154 return nil, ErrNameAlreadyExists 155 } 156 id, err := auth.UserIDFromContext(ctx) 157 if err != nil { 158 return nil, err 159 } 160 if !newGroup.IsOwned(id) { 161 newGroup.OwnedBy = append(newGroup.OwnedBy, id) 162 } 163 newGroup.Name = strings.TrimSpace(newGroup.Name) 164 return s.store.Create(ctx, newGroup) 165 } 166 167 func (s *service) Update(ctx context.Context, group *Group) (*Group, error) { 168 if s.groupNameExists(ctx, group.Name, group.ID) { 169 return nil, ErrNameAlreadyExists 170 } 171 return s.store.Update(ctx, group) 172 } 173 174 func (s *service) Delete(ctx context.Context, id string) error { 175 return s.store.Delete(ctx, id) 176 } 177 178 func (s *service) FindByID(ctx context.Context, id string) (*Group, error) { 179 return s.store.FindByID(ctx, id) 180 } 181 182 // Find adds a special filter value "self" for the OwnedBy field. If it is set 183 // the value gets replaced with the id of the loggedin user. 184 func (s *service) Find(ctx context.Context, params *crud.FindParams[FindFilters]) ([]*Group, int, error) { 185 if params != nil && params.Filters != nil && params.Filters.OwnedBy != nil { 186 if len(params.Filters.OwnedBy) == 1 && params.Filters.OwnedBy[0] == "self" { 187 id, err := auth.UserIDFromContext(ctx) 188 if err == nil { 189 params.Filters.OwnedBy = []string{id} 190 } 191 } 192 } 193 return s.store.Find(ctx, params) 194 } 195 196 func (s *service) groupNameExists(ctx context.Context, name string, id string) bool { 197 existingGroups, _, err := s.store.Find(auth.ContextWithRole(ctx, auth.RoleInternal), &crud.FindParams[FindFilters]{ 198 Filters: &FindFilters{Name: &name, NotID: &id}, 199 }) 200 if err != nil || len(existingGroups) > 0 { 201 return true 202 } 203 return false 204 } 205 206 func (s *service) GroupIDsByOwners(ctx context.Context, ownedBy []string) ([]string, error) { 207 208 groups, _, err := s.store.Find(auth.ContextWithRole(ctx, auth.RoleInternal), &crud.FindParams[FindFilters]{ 209 Filters: &FindFilters{OwnedBy: ownedBy}, 210 }) 211 if err != nil { 212 return nil, err 213 } 214 ids := []string{} 215 for _, g := range groups { 216 ids = append(ids, g.ID) 217 } 218 return ids, nil 219 }