github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/couchdb/views.go (about) 1 package couchdb 2 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "net/url" 8 "reflect" 9 "time" 10 11 "github.com/cozy/cozy-stack/pkg/couchdb/mango" 12 "github.com/cozy/cozy-stack/pkg/logger" 13 "github.com/cozy/cozy-stack/pkg/prefixer" 14 "golang.org/x/sync/errgroup" 15 ) 16 17 // View is the map/reduce thing in CouchDB 18 type View struct { 19 Name string `json:"-"` 20 Doctype string `json:"-"` 21 Map interface{} `json:"map"` 22 Reduce interface{} `json:"reduce,omitempty"` 23 Options interface{} `json:"options,omitempty"` 24 } 25 26 // DesignDoc is the structure if a _design doc containing views 27 type DesignDoc struct { 28 ID string `json:"_id,omitempty"` 29 Rev string `json:"_rev,omitempty"` 30 Lang string `json:"language"` 31 Views map[string]*View `json:"views"` 32 } 33 34 // IndexCreationResponse is the response from couchdb when we create an Index 35 type IndexCreationResponse struct { 36 Result string `json:"result,omitempty"` 37 Error string `json:"error,omitempty"` 38 Reason string `json:"reason,omitempty"` 39 ID string `json:"id,omitempty"` 40 Name string `json:"name,omitempty"` 41 } 42 43 // DefineViews creates a design doc with some views 44 func DefineViews(g *errgroup.Group, db prefixer.Prefixer, views []*View) { 45 for i := range views { 46 view := views[i] 47 g.Go(func() error { 48 return DefineView(db, view) 49 }) 50 } 51 } 52 53 // DefineView ensure that a view exists, or creates it. 54 func DefineView(db prefixer.Prefixer, v *View) error { 55 id := "_design/" + v.Name 56 url := url.PathEscape(id) 57 doc := &DesignDoc{ 58 ID: id, 59 Lang: "javascript", 60 Views: map[string]*View{v.Name: v}, 61 } 62 err := makeRequest(db, v.Doctype, http.MethodPut, url, &doc, nil) 63 if IsNoDatabaseError(err) { 64 err = CreateDB(db, v.Doctype) 65 if err != nil && !IsFileExists(err) { 66 if err != nil { 67 logger.WithDomain(db.DomainName()). 68 Infof("Cannot create view %s %s: cannot create DB - %s", 69 db.DBPrefix(), v.Doctype, err) 70 } 71 return err 72 } 73 err = makeRequest(db, v.Doctype, http.MethodPut, url, &doc, nil) 74 } 75 if IsConflictError(err) { 76 var old DesignDoc 77 err = makeRequest(db, v.Doctype, http.MethodGet, url, nil, &old) 78 if err != nil { 79 if err != nil { 80 logger.WithDomain(db.DomainName()). 81 Infof("Cannot create view %s %s: conflict - %s", 82 db.DBPrefix(), v.Doctype, err) 83 } 84 return err 85 } 86 if !equalViews(&old, doc) { 87 doc.Rev = old.Rev 88 err = makeRequest(db, v.Doctype, http.MethodPut, url, &doc, nil) 89 } else { 90 err = nil 91 } 92 } 93 if err != nil { 94 logger.WithDomain(db.DomainName()). 95 Infof("Cannot create view %s %s: %s", db.DBPrefix(), v.Doctype, err) 96 } 97 return err 98 } 99 100 // UpdateIndexesAndViews creates views and indexes that are missing or not 101 // up-to-date. 102 func UpdateIndexesAndViews(db prefixer.Prefixer, indexes []*mango.Index, views []*View) error { 103 g, _ := errgroup.WithContext(context.Background()) 104 105 // Load the existing design docs 106 idsByDoctype := map[string][]string{} 107 for _, view := range views { 108 list := idsByDoctype[view.Doctype] 109 list = append(list, "_design/"+view.Name) 110 idsByDoctype[view.Doctype] = list 111 } 112 for _, index := range indexes { 113 list := idsByDoctype[index.Doctype] 114 list = append(list, "_design/"+index.Request.DDoc) 115 idsByDoctype[index.Doctype] = list 116 } 117 ddocsByDoctype := map[string][]*DesignDoc{} 118 for doctype, ids := range idsByDoctype { 119 req := &AllDocsRequest{Keys: ids, Limit: 10000} 120 results := []*DesignDoc{} 121 err := GetDesignDocs(db, doctype, req, &results) 122 if err != nil { 123 continue 124 } 125 ddocsByDoctype[doctype] = results 126 } 127 128 // Define views that don't exist 129 for i := range views { 130 view := views[i] 131 ddoc := &DesignDoc{ 132 ID: "_design/" + view.Name, 133 Lang: "javascript", 134 Views: map[string]*View{view.Name: view}, 135 } 136 exists := false 137 for _, old := range ddocsByDoctype[view.Doctype] { 138 if old != nil && equalViews(old, ddoc) { 139 exists = true 140 } 141 } 142 if exists { 143 continue 144 } 145 g.Go(func() error { 146 return DefineView(db, view) 147 }) 148 } 149 150 // Define indexes that don't exist 151 for i := range indexes { 152 index := indexes[i] 153 ddoc := &DesignDoc{ 154 ID: "_design/" + index.Request.DDoc, 155 Lang: "query", 156 } 157 exists := false 158 for _, old := range ddocsByDoctype[index.Doctype] { 159 if old == nil { 160 continue 161 } 162 name := "undefined" 163 for key := range old.Views { 164 name = key 165 } 166 mapFields := map[string]interface{}{} 167 defFields := []interface{}{} 168 for _, field := range index.Request.Index.Fields { 169 mapFields[field] = "asc" 170 defFields = append(defFields, field) 171 } 172 view := &View{ 173 Name: name, 174 Doctype: index.Doctype, 175 Map: map[string]interface{}{ 176 "fields": mapFields, 177 "partial_filter_selector": index.Request.Index.PartialFilter, 178 }, 179 Reduce: "_count", 180 Options: map[string]interface{}{ 181 "def": map[string]interface{}{ 182 "fields": defFields, 183 }, 184 }, 185 } 186 ddoc.Views = map[string]*View{name: view} 187 if equalViews(old, ddoc) { 188 exists = true 189 } 190 } 191 if exists { 192 continue 193 } 194 g.Go(func() error { 195 return DefineIndex(db, index) 196 }) 197 } 198 199 return g.Wait() 200 } 201 202 func equalViews(v1 *DesignDoc, v2 *DesignDoc) bool { 203 if v1.Lang != v2.Lang { 204 return false 205 } 206 if len(v1.Views) != len(v2.Views) { 207 return false 208 } 209 for name, view1 := range v1.Views { 210 view2, ok := v2.Views[name] 211 if !ok { 212 return false 213 } 214 if !reflect.DeepEqual(view1.Map, view2.Map) || 215 !reflect.DeepEqual(view1.Reduce, view2.Reduce) || 216 !reflect.DeepEqual(view1.Options, view2.Options) { 217 return false 218 } 219 } 220 return true 221 } 222 223 // ExecView executes the specified view function 224 func ExecView(db prefixer.Prefixer, view *View, req *ViewRequest, results interface{}) error { 225 viewurl := fmt.Sprintf("_design/%s/_view/%s", view.Name, view.Name) 226 if req.GroupLevel > 0 { 227 req.Group = true 228 } 229 v, err := req.Values() 230 if err != nil { 231 return err 232 } 233 viewurl += "?" + v.Encode() 234 if req.Keys != nil { 235 return makeRequest(db, view.Doctype, http.MethodPost, viewurl, req, &results) 236 } 237 err = makeRequest(db, view.Doctype, http.MethodGet, viewurl, nil, &results) 238 if IsInternalServerError(err) { 239 time.Sleep(1 * time.Second) 240 // Retry the error on 500, as it may be just that CouchDB is slow to build the view 241 err = makeRequest(db, view.Doctype, http.MethodGet, viewurl, nil, &results) 242 if IsInternalServerError(err) { 243 logger. 244 WithDomain(db.DomainName()). 245 WithNamespace("couchdb"). 246 WithField("critical", "true"). 247 Errorf("500 on requesting view: %s", err) 248 } 249 } 250 return err 251 } 252 253 // DefineIndexes defines a list of indexes. 254 func DefineIndexes(g *errgroup.Group, db prefixer.Prefixer, indexes []*mango.Index) { 255 for i := range indexes { 256 index := indexes[i] 257 g.Go(func() error { return DefineIndex(db, index) }) 258 } 259 } 260 261 // DefineIndex define the index on the doctype database 262 // see query package on how to define an index 263 func DefineIndex(db prefixer.Prefixer, index *mango.Index) error { 264 _, err := DefineIndexRaw(db, index.Doctype, index.Request) 265 if err != nil { 266 logger.WithDomain(db.DomainName()). 267 Infof("Cannot create index %s %s: %s", db.DBPrefix(), index.Doctype, err) 268 } 269 return err 270 } 271 272 // DefineIndexRaw defines a index 273 func DefineIndexRaw(db prefixer.Prefixer, doctype string, index interface{}) (*IndexCreationResponse, error) { 274 url := "_index" 275 response := &IndexCreationResponse{} 276 err := makeRequest(db, doctype, http.MethodPost, url, &index, &response) 277 if IsNoDatabaseError(err) { 278 if err = CreateDB(db, doctype); err != nil && !IsFileExists(err) { 279 return nil, err 280 } 281 err = makeRequest(db, doctype, http.MethodPost, url, &index, &response) 282 } 283 // XXX when creating the same index twice at the same time, CouchDB respond 284 // with a 500, so let's just retry as a work-around... 285 if IsInternalServerError(err) { 286 time.Sleep(100 * time.Millisecond) 287 err = makeRequest(db, doctype, http.MethodPost, url, &index, &response) 288 } 289 if err != nil { 290 return nil, err 291 } 292 return response, nil 293 }