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  }