github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/couchdb/couchdb.go (about)

     1  package couchdb
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"reflect"
    11  	"strings"
    12  	"time"
    13  
    14  	build "github.com/cozy/cozy-stack/pkg/config"
    15  	"github.com/cozy/cozy-stack/pkg/config/config"
    16  	"github.com/cozy/cozy-stack/pkg/consts"
    17  	"github.com/cozy/cozy-stack/pkg/couchdb/mango"
    18  	"github.com/cozy/cozy-stack/pkg/logger"
    19  	"github.com/cozy/cozy-stack/pkg/prefixer"
    20  	"github.com/cozy/cozy-stack/pkg/realtime"
    21  	"github.com/labstack/echo/v4"
    22  )
    23  
    24  // MaxString is the unicode character "\uFFFF", useful in query as
    25  // a upperbound for string.
    26  const MaxString = mango.MaxString
    27  
    28  // SelectorReferencedBy is the string constant for the references in a JSON
    29  // document.
    30  const SelectorReferencedBy = "referenced_by"
    31  
    32  // Doc is the interface that encapsulate a couchdb document, of any
    33  // serializable type. This interface defines method to set and get the
    34  // ID of the document.
    35  type Doc interface {
    36  	ID() string
    37  	Rev() string
    38  	DocType() string
    39  	Clone() Doc
    40  
    41  	SetID(id string)
    42  	SetRev(rev string)
    43  }
    44  
    45  // RTEvent published a realtime event for a couchDB change
    46  func RTEvent(db prefixer.Prefixer, verb string, doc, oldDoc Doc) {
    47  	if err := runHooks(db, verb, doc, oldDoc); err != nil {
    48  		logger.WithDomain(db.DomainName()).WithNamespace("couchdb").
    49  			Errorf("error in hooks on %s %s %v\n", verb, doc.DocType(), err)
    50  	}
    51  	docClone := doc.Clone()
    52  	go realtime.GetHub().Publish(db, verb, docClone, oldDoc)
    53  }
    54  
    55  // JSONDoc is a map representing a simple json object that implements
    56  // the Doc interface.
    57  type JSONDoc struct {
    58  	M    map[string]interface{}
    59  	Type string
    60  }
    61  
    62  // ID returns the identifier field of the document
    63  //
    64  //	"io.cozy.event/123abc123" == doc.ID()
    65  func (j *JSONDoc) ID() string {
    66  	id, ok := j.M["_id"].(string)
    67  	if ok {
    68  		return id
    69  	}
    70  	return ""
    71  }
    72  
    73  // Rev returns the revision field of the document
    74  //
    75  //	"3-1234def1234" == doc.Rev()
    76  func (j *JSONDoc) Rev() string {
    77  	rev, ok := j.M["_rev"].(string)
    78  	if ok {
    79  		return rev
    80  	}
    81  	return ""
    82  }
    83  
    84  // DocType returns the document type of the document
    85  //
    86  //	"io.cozy.event" == doc.Doctype()
    87  func (j *JSONDoc) DocType() string {
    88  	return j.Type
    89  }
    90  
    91  // SetID is used to set the identifier of the document
    92  func (j *JSONDoc) SetID(id string) {
    93  	if id == "" {
    94  		delete(j.M, "_id")
    95  	} else {
    96  		j.M["_id"] = id
    97  	}
    98  }
    99  
   100  // SetRev is used to set the revision of the document
   101  func (j *JSONDoc) SetRev(rev string) {
   102  	if rev == "" {
   103  		delete(j.M, "_rev")
   104  	} else {
   105  		j.M["_rev"] = rev
   106  	}
   107  }
   108  
   109  // Clone is used to create a copy of the document
   110  func (j *JSONDoc) Clone() Doc {
   111  	cloned := JSONDoc{Type: j.Type}
   112  	cloned.M = deepClone(j.M)
   113  	return &cloned
   114  }
   115  
   116  func deepClone(m map[string]interface{}) map[string]interface{} {
   117  	clone := make(map[string]interface{}, len(m))
   118  	for k, v := range m {
   119  		if vv, ok := v.(map[string]interface{}); ok {
   120  			clone[k] = deepClone(vv)
   121  		} else if vv, ok := v.([]interface{}); ok {
   122  			clone[k] = deepCloneSlice(vv)
   123  		} else {
   124  			clone[k] = v
   125  		}
   126  	}
   127  	return clone
   128  }
   129  
   130  func deepCloneSlice(s []interface{}) []interface{} {
   131  	clone := make([]interface{}, len(s))
   132  	for i, v := range s {
   133  		if vv, ok := v.(map[string]interface{}); ok {
   134  			clone[i] = deepClone(vv)
   135  		} else if vv, ok := v.([]interface{}); ok {
   136  			clone[i] = deepCloneSlice(vv)
   137  		} else {
   138  			clone[i] = v
   139  		}
   140  	}
   141  	return clone
   142  }
   143  
   144  // MarshalJSON implements json.Marshaller by proxying to internal map
   145  func (j *JSONDoc) MarshalJSON() ([]byte, error) {
   146  	return json.Marshal(j.M)
   147  }
   148  
   149  // UnmarshalJSON implements json.Unmarshaller by proxying to internal map
   150  func (j *JSONDoc) UnmarshalJSON(bytes []byte) error {
   151  	err := json.Unmarshal(bytes, &j.M)
   152  	if err != nil {
   153  		return err
   154  	}
   155  	doctype, ok := j.M["_type"].(string)
   156  	if ok {
   157  		j.Type = doctype
   158  	}
   159  	delete(j.M, "_type")
   160  	return nil
   161  }
   162  
   163  // ToMapWithType returns the JSONDoc internal map including its DocType
   164  // its used in request response.
   165  func (j *JSONDoc) ToMapWithType() map[string]interface{} {
   166  	j.M["_type"] = j.DocType()
   167  	return j.M
   168  }
   169  
   170  // Get returns the value of one of the db fields
   171  func (j *JSONDoc) Get(key string) interface{} {
   172  	return j.M[key]
   173  }
   174  
   175  // Fetch implements permission.Fetcher on JSONDoc.
   176  //
   177  // The `referenced_by` selector is a special case: the `values` field of such
   178  // rule has the format "doctype/id" and it cannot directly be compared to the
   179  // same field of a JSONDoc since, in the latter, the format is:
   180  // "referenced_by": [
   181  //
   182  //	{"type": "doctype1", "id": "id1"},
   183  //	{"type": "doctype2", "id": "id2"},
   184  //
   185  // ]
   186  func (j *JSONDoc) Fetch(field string) []string {
   187  	if field == SelectorReferencedBy {
   188  		rawReferences := j.Get(field)
   189  		references, ok := rawReferences.([]interface{})
   190  		if !ok {
   191  			return nil
   192  		}
   193  
   194  		var values []string
   195  		for _, reference := range references {
   196  			if ref, ok := reference.(map[string]interface{}); ok {
   197  				values = append(values, fmt.Sprintf("%s/%s", ref["type"], ref["id"]))
   198  			}
   199  		}
   200  		return values
   201  	}
   202  
   203  	return []string{fmt.Sprintf("%v", j.Get(field))}
   204  }
   205  
   206  func unescapeCouchdbName(name string) string {
   207  	return strings.ReplaceAll(name, "-", ".")
   208  }
   209  
   210  // EscapeCouchdbName can be used to build the name of a database from the
   211  // instance prefix and doctype.
   212  func EscapeCouchdbName(name string) string {
   213  	name = strings.ReplaceAll(name, ".", "-")
   214  	name = strings.ReplaceAll(name, ":", "-")
   215  	return strings.ToLower(name)
   216  }
   217  
   218  func makeDBName(db prefixer.Prefixer, doctype string) string {
   219  	dbname := EscapeCouchdbName(db.DBPrefix() + "/" + doctype)
   220  	return url.PathEscape(dbname)
   221  }
   222  
   223  func dbNameHasPrefix(dbname, dbprefix string) (bool, string) {
   224  	dbprefix = EscapeCouchdbName(dbprefix + "/")
   225  	if !strings.HasPrefix(dbname, dbprefix) {
   226  		return false, ""
   227  	}
   228  	return true, strings.Replace(dbname, dbprefix, "", 1)
   229  }
   230  
   231  func buildCouchRequest(db prefixer.Prefixer, doctype, method, path string, reqjson []byte, headers map[string]string) (*http.Request, error) {
   232  	couch := config.CouchCluster(db.DBCluster())
   233  	if doctype != "" {
   234  		path = makeDBName(db, doctype) + "/" + path
   235  	}
   236  	req, err := http.NewRequest(
   237  		method,
   238  		couch.URL.String()+path,
   239  		bytes.NewReader(reqjson),
   240  	)
   241  	// Possible err = wrong method, unparsable url
   242  	if err != nil {
   243  		return nil, newRequestError(err)
   244  	}
   245  	req.Header.Add(echo.HeaderAccept, echo.MIMEApplicationJSON)
   246  	if len(reqjson) > 0 {
   247  		req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationJSON)
   248  	}
   249  	for k, v := range headers {
   250  		req.Header.Add(k, v)
   251  	}
   252  	if auth := couch.Auth; auth != nil {
   253  		if p, ok := auth.Password(); ok {
   254  			req.SetBasicAuth(auth.Username(), p)
   255  		}
   256  	}
   257  	return req, nil
   258  }
   259  
   260  func handleResponseError(db prefixer.Prefixer, resp *http.Response) error {
   261  	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
   262  		return nil
   263  	}
   264  	log := logger.WithDomain(db.DomainName()).WithNamespace("couchdb")
   265  	body, err := io.ReadAll(resp.Body)
   266  	if err != nil {
   267  		err = newIOReadError(err)
   268  		log.Error(err.Error())
   269  	} else {
   270  		err = newCouchdbError(resp.StatusCode, body)
   271  		if isBadArgError(err) {
   272  			log.Error(err.Error())
   273  		} else {
   274  			log.Debug(err.Error())
   275  		}
   276  	}
   277  	return err
   278  }
   279  
   280  func makeRequest(db prefixer.Prefixer, doctype, method, path string, reqbody interface{}, resbody interface{}) error {
   281  	var err error
   282  	var reqjson []byte
   283  
   284  	if reqbody != nil {
   285  		reqjson, err = json.Marshal(reqbody)
   286  		if err != nil {
   287  			return err
   288  		}
   289  	}
   290  	log := logger.WithDomain(db.DomainName()).WithNamespace("couchdb")
   291  
   292  	// We do not log the account doctype to avoid printing account informations
   293  	// in the log files.
   294  	logDebug := doctype != consts.Accounts && log.IsDebug()
   295  
   296  	if logDebug {
   297  		log.Debugf("request: %s %s %s", method, path, string(bytes.TrimSpace(reqjson)))
   298  	}
   299  	req, err := buildCouchRequest(db, doctype, method, path, reqjson, nil)
   300  	if err != nil {
   301  		log.Error(err.Error())
   302  		return err
   303  	}
   304  
   305  	start := time.Now()
   306  	resp, err := config.CouchClient().Do(req)
   307  	elapsed := time.Since(start)
   308  	// Possible err = mostly connection failure
   309  	if err != nil {
   310  		err = newConnectionError(err)
   311  		log.Error(err.Error())
   312  		return err
   313  	}
   314  	defer resp.Body.Close()
   315  
   316  	if elapsed.Seconds() >= 10 {
   317  		log.Infof("slow request on %s %s (%s)", method, path, elapsed)
   318  	}
   319  
   320  	err = handleResponseError(db, resp)
   321  	if err != nil {
   322  		return err
   323  	}
   324  	if resbody == nil {
   325  		// Flush the body, so that the connection can be reused by keep-alive
   326  		_, _ = io.Copy(io.Discard, resp.Body)
   327  		return nil
   328  	}
   329  
   330  	if logDebug {
   331  		var data []byte
   332  		data, err = io.ReadAll(resp.Body)
   333  		if err != nil {
   334  			return err
   335  		}
   336  		log.Debugf("response: %s", string(bytes.TrimSpace(data)))
   337  		err = json.Unmarshal(data, &resbody)
   338  	} else {
   339  		err = json.NewDecoder(resp.Body).Decode(&resbody)
   340  	}
   341  
   342  	return err
   343  }
   344  
   345  // Compact asks CouchDB to compact a database.
   346  func Compact(db prefixer.Prefixer, doctype string) error {
   347  	// CouchDB requires a Content-Type: application/json header
   348  	body := map[string]interface{}{}
   349  	return makeRequest(db, doctype, http.MethodPost, "_compact", body, nil)
   350  }
   351  
   352  // DBStatus responds with informations on the database: size, number of
   353  // documents, sequence numbers, etc.
   354  func DBStatus(db prefixer.Prefixer, doctype string) (*DBStatusResponse, error) {
   355  	var out DBStatusResponse
   356  	return &out, makeRequest(db, doctype, http.MethodGet, "", nil, &out)
   357  }
   358  
   359  func allDbs(db prefixer.Prefixer) ([]string, error) {
   360  	var dbs []string
   361  	prefix := EscapeCouchdbName(db.DBPrefix())
   362  	u := fmt.Sprintf(`_all_dbs?start_key="%s"&end_key="%s"`, prefix+"/", prefix+"0")
   363  	if err := makeRequest(db, "", http.MethodGet, u, nil, &dbs); err != nil {
   364  		return nil, err
   365  	}
   366  	return dbs, nil
   367  }
   368  
   369  // AllDoctypes returns a list of all the doctypes that have a database
   370  // on a given instance
   371  func AllDoctypes(db prefixer.Prefixer) ([]string, error) {
   372  	dbs, err := allDbs(db)
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	prefix := EscapeCouchdbName(db.DBPrefix())
   377  	var doctypes []string
   378  	for _, dbname := range dbs {
   379  		parts := strings.Split(dbname, "/")
   380  		if len(parts) == 2 && parts[0] == prefix {
   381  			doctype := unescapeCouchdbName(parts[1])
   382  			doctypes = append(doctypes, doctype)
   383  		}
   384  	}
   385  	return doctypes, nil
   386  }
   387  
   388  // GetDoc fetches a document by its docType and id
   389  // It fills with out by json.Unmarshal-ing
   390  func GetDoc(db prefixer.Prefixer, doctype, id string, out Doc) error {
   391  	var err error
   392  	id, err = validateDocID(id)
   393  	if err != nil {
   394  		return err
   395  	}
   396  	if id == "" {
   397  		return fmt.Errorf("Missing ID for GetDoc")
   398  	}
   399  	return makeRequest(db, doctype, http.MethodGet, url.PathEscape(id), nil, out)
   400  }
   401  
   402  // GetDocRev fetch a document by its docType and ID on a specific revision, out
   403  // is filled with the document by json.Unmarshal-ing
   404  func GetDocRev(db prefixer.Prefixer, doctype, id, rev string, out Doc) error {
   405  	var err error
   406  	id, err = validateDocID(id)
   407  	if err != nil {
   408  		return err
   409  	}
   410  	if id == "" {
   411  		return fmt.Errorf("Missing ID for GetDoc")
   412  	}
   413  	url := url.PathEscape(id) + "?rev=" + url.QueryEscape(rev)
   414  	return makeRequest(db, doctype, http.MethodGet, url, nil, out)
   415  }
   416  
   417  // GetDocWithRevs fetches a document by its docType and ID.
   418  // out is filled with the document by json.Unmarshal-ing and contains the list
   419  // of all revisions
   420  func GetDocWithRevs(db prefixer.Prefixer, doctype, id string, out Doc) error {
   421  	var err error
   422  	id, err = validateDocID(id)
   423  	if err != nil {
   424  		return err
   425  	}
   426  	if id == "" {
   427  		return fmt.Errorf("Missing ID for GetDoc")
   428  	}
   429  	url := url.PathEscape(id) + "?revs=true"
   430  	return makeRequest(db, doctype, http.MethodGet, url, nil, out)
   431  }
   432  
   433  // EnsureDBExist creates the database for the doctype if it doesn't exist
   434  func EnsureDBExist(db prefixer.Prefixer, doctype string) error {
   435  	_, err := DBStatus(db, doctype)
   436  	if IsNoDatabaseError(err) {
   437  		_ = CreateDB(db, doctype)
   438  		_, err = DBStatus(db, doctype)
   439  	}
   440  	return err
   441  }
   442  
   443  // CreateDB creates the necessary database for a doctype
   444  func CreateDB(db prefixer.Prefixer, doctype string) error {
   445  	// XXX On dev release of the stack, we force some parameters at the
   446  	// creation of a database. It helps CouchDB to have more acceptable
   447  	// performances inside Docker. Those parameters are not suitable for
   448  	// production, and we must not override the CouchDB configuration.
   449  	query := ""
   450  	if build.IsDevRelease() {
   451  		query = "?q=1&n=1"
   452  	}
   453  	if err := makeRequest(db, doctype, http.MethodPut, query, nil, nil); err != nil {
   454  		return err
   455  	}
   456  
   457  	// We may need to recreate indexes for a database that was deleted manually in CouchDB
   458  	for _, index := range Indexes {
   459  		if index.Doctype == doctype {
   460  			_ = DefineIndex(db, index)
   461  		}
   462  	}
   463  	return nil
   464  }
   465  
   466  // DeleteDB destroy the database for a doctype
   467  func DeleteDB(db prefixer.Prefixer, doctype string) error {
   468  	return makeRequest(db, doctype, http.MethodDelete, "", nil, nil)
   469  }
   470  
   471  // DeleteAllDBs will remove all the couchdb doctype databases for
   472  // a couchdb.DB.
   473  func DeleteAllDBs(db prefixer.Prefixer) error {
   474  	dbprefix := db.DBPrefix()
   475  	if dbprefix == "" {
   476  		return fmt.Errorf("You need to provide a valid database")
   477  	}
   478  
   479  	dbsList, err := allDbs(db)
   480  	if err != nil {
   481  		return err
   482  	}
   483  
   484  	for _, doctypedb := range dbsList {
   485  		hasPrefix, doctype := dbNameHasPrefix(doctypedb, dbprefix)
   486  		if !hasPrefix {
   487  			continue
   488  		}
   489  		if err = DeleteDB(db, doctype); err != nil {
   490  			return err
   491  		}
   492  	}
   493  
   494  	return nil
   495  }
   496  
   497  // ResetDB destroy and recreate the database for a doctype
   498  func ResetDB(db prefixer.Prefixer, doctype string) error {
   499  	err := DeleteDB(db, doctype)
   500  	if err != nil && !IsNoDatabaseError(err) {
   501  		return err
   502  	}
   503  	return CreateDB(db, doctype)
   504  }
   505  
   506  // DeleteDoc deletes a struct implementing the couchb.Doc interface
   507  // If the document's current rev does not match the one passed,
   508  // a CouchdbError(409 conflict) will be returned.
   509  // The document's SetRev will be called with tombstone revision
   510  func DeleteDoc(db prefixer.Prefixer, doc Doc) error {
   511  	id, err := validateDocID(doc.ID())
   512  	if err != nil {
   513  		return err
   514  	}
   515  	if id == "" {
   516  		return fmt.Errorf("Missing ID for DeleteDoc")
   517  	}
   518  	old := doc.Clone()
   519  
   520  	// XXX Specific log for the deletion of an account, to help monitor this
   521  	// metric.
   522  	if doc.DocType() == consts.Accounts {
   523  		logger.WithDomain(db.DomainName()).
   524  			WithFields(logger.Fields{
   525  				"log_id":      "account_delete",
   526  				"account_id":  doc.ID(),
   527  				"account_rev": doc.Rev(),
   528  				"nspace":      "couchb",
   529  			}).
   530  			Infof("Deleting account %s", doc.ID())
   531  	}
   532  
   533  	var res UpdateResponse
   534  	url := url.PathEscape(id) + "?rev=" + url.QueryEscape(doc.Rev())
   535  	err = makeRequest(db, doc.DocType(), http.MethodDelete, url, nil, &res)
   536  	if err != nil {
   537  		return err
   538  	}
   539  	doc.SetRev(res.Rev)
   540  	RTEvent(db, realtime.EventDelete, doc, old)
   541  	return nil
   542  }
   543  
   544  // NewEmptyObjectOfSameType takes an object and returns a new object of the
   545  // same type. For example, if NewEmptyObjectOfSameType is called with a pointer
   546  // to a JSONDoc, it will return a pointer to an empty JSONDoc (and not a nil
   547  // pointer).
   548  func NewEmptyObjectOfSameType(obj interface{}) interface{} {
   549  	typ := reflect.TypeOf(obj)
   550  	if typ.Kind() == reflect.Ptr {
   551  		typ = typ.Elem()
   552  	}
   553  	value := reflect.New(typ)
   554  	return value.Interface()
   555  }
   556  
   557  // UpdateDoc update a document. The document ID and Rev should be filled.
   558  // The doc SetRev function will be called with the new rev.
   559  func UpdateDoc(db prefixer.Prefixer, doc Doc) error {
   560  	id, err := validateDocID(doc.ID())
   561  	if err != nil {
   562  		return err
   563  	}
   564  	doctype := doc.DocType()
   565  	if doctype == "" {
   566  		return fmt.Errorf("UpdateDoc: doctype is missing")
   567  	}
   568  	if id == "" {
   569  		return fmt.Errorf("UpdateDoc: id is missing")
   570  	}
   571  	if doc.Rev() == "" {
   572  		return fmt.Errorf("UpdateDoc: rev is missing")
   573  	}
   574  
   575  	url := url.PathEscape(id)
   576  	// The old doc is requested to be emitted thought RTEvent.
   577  	// This is useful to keep track of the modifications for the triggers.
   578  	oldDoc := NewEmptyObjectOfSameType(doc).(Doc)
   579  	err = makeRequest(db, doctype, http.MethodGet, url, nil, oldDoc)
   580  	if err != nil {
   581  		return err
   582  	}
   583  	var res UpdateResponse
   584  	err = makeRequest(db, doctype, http.MethodPut, url, doc, &res)
   585  	if err != nil {
   586  		return err
   587  	}
   588  	doc.SetRev(res.Rev)
   589  	RTEvent(db, realtime.EventUpdate, doc, oldDoc)
   590  	return nil
   591  }
   592  
   593  // UpdateDocWithOld updates a document, like UpdateDoc. The difference is that
   594  // if we already have oldDoc there is no need to refetch it from database.
   595  func UpdateDocWithOld(db prefixer.Prefixer, doc, oldDoc Doc) error {
   596  	id, err := validateDocID(doc.ID())
   597  	if err != nil {
   598  		return err
   599  	}
   600  	doctype := doc.DocType()
   601  	if doctype == "" {
   602  		return fmt.Errorf("UpdateDocWithOld: doctype is missing")
   603  	}
   604  	if id == "" {
   605  		return fmt.Errorf("UpdateDocWithOld: id is missing")
   606  	}
   607  	if doc.Rev() == "" {
   608  		return fmt.Errorf("UpdateDocWithOld: rev is missing")
   609  	}
   610  
   611  	url := url.PathEscape(id)
   612  	var res UpdateResponse
   613  	err = makeRequest(db, doctype, http.MethodPut, url, doc, &res)
   614  	if err != nil {
   615  		return err
   616  	}
   617  	doc.SetRev(res.Rev)
   618  	RTEvent(db, realtime.EventUpdate, doc, oldDoc)
   619  	return nil
   620  }
   621  
   622  // CreateNamedDoc persist a document with an ID.
   623  // if the document already exist, it will return a 409 error.
   624  // The document ID should be fillled.
   625  // The doc SetRev function will be called with the new rev.
   626  func CreateNamedDoc(db prefixer.Prefixer, doc Doc) error {
   627  	id, err := validateDocID(doc.ID())
   628  	if err != nil {
   629  		return err
   630  	}
   631  	doctype := doc.DocType()
   632  	if doctype == "" {
   633  		return fmt.Errorf("CreateNamedDoc: doctype is missing")
   634  	}
   635  	if id == "" {
   636  		return fmt.Errorf("CreateNamedDoc: id is missing")
   637  	}
   638  	if doc.Rev() != "" {
   639  		return fmt.Errorf("CreateNamedDoc: no rev should be given")
   640  	}
   641  
   642  	var res UpdateResponse
   643  	err = makeRequest(db, doctype, http.MethodPut, url.PathEscape(id), doc, &res)
   644  	if err != nil {
   645  		return err
   646  	}
   647  	doc.SetRev(res.Rev)
   648  	RTEvent(db, realtime.EventCreate, doc, nil)
   649  	return nil
   650  }
   651  
   652  // CreateNamedDocWithDB is equivalent to CreateNamedDoc but creates the database
   653  // if it does not exist
   654  func CreateNamedDocWithDB(db prefixer.Prefixer, doc Doc) error {
   655  	err := CreateNamedDoc(db, doc)
   656  	if IsNoDatabaseError(err) {
   657  		err = CreateDB(db, doc.DocType())
   658  		if err != nil {
   659  			return err
   660  		}
   661  		return CreateNamedDoc(db, doc)
   662  	}
   663  	return err
   664  }
   665  
   666  // Upsert create the doc or update it if it already exists.
   667  func Upsert(db prefixer.Prefixer, doc Doc) error {
   668  	id, err := validateDocID(doc.ID())
   669  	if err != nil {
   670  		return err
   671  	}
   672  
   673  	var old JSONDoc
   674  	err = GetDoc(db, doc.DocType(), id, &old)
   675  	if IsNoDatabaseError(err) {
   676  		err = CreateDB(db, doc.DocType())
   677  		if err != nil {
   678  			return err
   679  		}
   680  		return CreateNamedDoc(db, doc)
   681  	}
   682  	if IsNotFoundError(err) {
   683  		return CreateNamedDoc(db, doc)
   684  	}
   685  	if err != nil {
   686  		return err
   687  	}
   688  
   689  	doc.SetRev(old.Rev())
   690  	return UpdateDoc(db, doc)
   691  }
   692  
   693  func createDocOrDB(db prefixer.Prefixer, doc Doc, response interface{}) error {
   694  	doctype := doc.DocType()
   695  	err := makeRequest(db, doctype, http.MethodPost, "", doc, response)
   696  	if err == nil || !IsNoDatabaseError(err) {
   697  		return err
   698  	}
   699  	err = CreateDB(db, doctype)
   700  	if err == nil || IsFileExists(err) {
   701  		err = makeRequest(db, doctype, http.MethodPost, "", doc, response)
   702  	}
   703  	return err
   704  }
   705  
   706  // CreateDoc is used to persist the given document in the couchdb
   707  // database. The document's SetRev and SetID function will be called
   708  // with the document's new ID and Rev.
   709  // This function creates a database if this is the first document of its type
   710  func CreateDoc(db prefixer.Prefixer, doc Doc) error {
   711  	var res *UpdateResponse
   712  
   713  	if doc.ID() != "" {
   714  		return newDefinedIDError()
   715  	}
   716  
   717  	err := createDocOrDB(db, doc, &res)
   718  	if err != nil {
   719  		return err
   720  	} else if !res.Ok {
   721  		return fmt.Errorf("CouchDB replied with 200 ok=false")
   722  	}
   723  
   724  	doc.SetID(res.ID)
   725  	doc.SetRev(res.Rev)
   726  	RTEvent(db, realtime.EventCreate, doc, nil)
   727  	return nil
   728  }
   729  
   730  // Copy copies an existing doc to a specified destination
   731  func Copy(db prefixer.Prefixer, doctype, path, destination string) (map[string]interface{}, error) {
   732  	headers := map[string]string{"Destination": destination}
   733  	// COPY is not a standard HTTP method
   734  	req, err := buildCouchRequest(db, doctype, "COPY", path, nil, headers)
   735  	if err != nil {
   736  		return nil, err
   737  	}
   738  	resp, err := config.CouchClient().Do(req)
   739  	if err != nil {
   740  		return nil, err
   741  	}
   742  	defer resp.Body.Close()
   743  	err = handleResponseError(db, resp)
   744  	if err != nil {
   745  		return nil, err
   746  	}
   747  	var results map[string]interface{}
   748  	err = json.NewDecoder(resp.Body).Decode(&results)
   749  	return results, err
   750  }
   751  
   752  // FindDocs returns all documents matching the passed FindRequest
   753  // documents will be unmarshalled in the provided results slice.
   754  func FindDocs(db prefixer.Prefixer, doctype string, req *FindRequest, results interface{}) error {
   755  	_, err := FindDocsRaw(db, doctype, req, results)
   756  	return err
   757  }
   758  
   759  // FindDocsUnoptimized allows search on non-indexed fields.
   760  // /!\ Use with care
   761  func FindDocsUnoptimized(db prefixer.Prefixer, doctype string, req *FindRequest, results interface{}) error {
   762  	_, err := findDocsRaw(db, doctype, req, results, true)
   763  	return err
   764  }
   765  
   766  func findDocsRaw(db prefixer.Prefixer, doctype string, req interface{}, results interface{}, ignoreUnoptimized bool) (*FindResponse, error) {
   767  	url := "_find"
   768  	// prepare a structure to receive the results
   769  	var response FindResponse
   770  	err := makeRequest(db, doctype, http.MethodPost, url, &req, &response)
   771  	if err != nil {
   772  		if isIndexError(err) {
   773  			jsonReq, errm := json.Marshal(req)
   774  			if errm != nil {
   775  				return nil, err
   776  			}
   777  			errc := err.(*Error)
   778  			errc.Reason += fmt.Sprintf(" (original req: %s)", string(jsonReq))
   779  			return nil, errc
   780  		}
   781  		return nil, err
   782  	}
   783  	if !ignoreUnoptimized && strings.Contains(response.Warning, "matching index found") {
   784  		// Developers should not rely on fullscan with no index.
   785  		return nil, unoptimalError()
   786  	}
   787  	if response.Bookmark == "nil" {
   788  		// CouchDB surprisingly returns "nil" when there is no doc
   789  		response.Bookmark = ""
   790  	}
   791  	return &response, json.Unmarshal(response.Docs, results)
   792  }
   793  
   794  // FindDocsRaw find documents
   795  func FindDocsRaw(db prefixer.Prefixer, doctype string, req interface{}, results interface{}) (*FindResponse, error) {
   796  	return findDocsRaw(db, doctype, req, results, false)
   797  }
   798  
   799  // NormalDocs returns all the documents from a database, with pagination, but
   800  // it excludes the design docs.
   801  func NormalDocs(db prefixer.Prefixer, doctype string, skip, limit int, bookmark string, executionStats bool) (*NormalDocsResponse, error) {
   802  	var findRes struct {
   803  		Docs           []json.RawMessage `json:"docs"`
   804  		Bookmark       string            `json:"bookmark"`
   805  		ExecutionStats *ExecutionStats   `json:"execution_stats,omitempty"`
   806  	}
   807  	req := FindRequest{
   808  		Selector:       mango.Gte("_id", nil),
   809  		Limit:          limit,
   810  		ExecutionStats: executionStats,
   811  	}
   812  	// Both bookmark and skip can be used for pagination, but bookmark is more efficient.
   813  	// See https://docs.couchdb.org/en/latest/api/database/find.html#pagination
   814  	if bookmark != "" {
   815  		req.Bookmark = bookmark
   816  	} else {
   817  		req.Skip = skip
   818  	}
   819  	err := makeRequest(db, doctype, http.MethodPost, "_find", &req, &findRes)
   820  	if err != nil {
   821  		return nil, err
   822  	}
   823  	res := NormalDocsResponse{
   824  		Rows:           findRes.Docs,
   825  		ExecutionStats: findRes.ExecutionStats,
   826  	}
   827  	if bookmark == "" && len(res.Rows) < limit {
   828  		res.Total = skip + len(res.Rows)
   829  	} else {
   830  		total, err := CountNormalDocs(db, doctype)
   831  		if err != nil {
   832  			return nil, err
   833  		}
   834  		res.Total = total
   835  	}
   836  	res.Bookmark = findRes.Bookmark
   837  	if res.Bookmark == "nil" {
   838  		// CouchDB surprisingly returns "nil" when there is no doc
   839  		res.Bookmark = ""
   840  	}
   841  	return &res, nil
   842  }
   843  
   844  func validateDocID(id string) (string, error) {
   845  	if len(id) > 0 && id[0] == '_' {
   846  		return "", newBadIDError(id)
   847  	}
   848  	return id, nil
   849  }
   850  
   851  // UpdateResponse is the response from couchdb when updating documents
   852  type UpdateResponse struct {
   853  	ID     string `json:"id"`
   854  	Rev    string `json:"rev"`
   855  	Ok     bool   `json:"ok"`
   856  	Error  string `json:"error"`
   857  	Reason string `json:"reason"`
   858  }
   859  
   860  // FindResponse is the response from couchdb on a find request
   861  type FindResponse struct {
   862  	Warning        string          `json:"warning"`
   863  	Bookmark       string          `json:"bookmark"`
   864  	Docs           json.RawMessage `json:"docs"`
   865  	ExecutionStats *ExecutionStats `json:"execution_stats,omitempty"`
   866  }
   867  
   868  // ExecutionStats is returned by CouchDB on _find queries
   869  type ExecutionStats struct {
   870  	TotalKeysExamined       int     `json:"total_keys_examined,omitempty"`
   871  	TotalDocsExamined       int     `json:"total_docs_examined,omitempty"`
   872  	TotalQuorumDocsExamined int     `json:"total_quorum_docs_examined,omitempty"`
   873  	ResultsReturned         int     `json:"results_returned,omitempty"`
   874  	ExecutionTimeMs         float32 `json:"execution_time_ms,omitempty"`
   875  }
   876  
   877  // FindRequest is used to build a find request
   878  type FindRequest struct {
   879  	Selector       mango.Filter `json:"selector"`
   880  	UseIndex       string       `json:"use_index,omitempty"`
   881  	Bookmark       string       `json:"bookmark,omitempty"`
   882  	Limit          int          `json:"limit,omitempty"`
   883  	Skip           int          `json:"skip,omitempty"`
   884  	Sort           mango.SortBy `json:"sort,omitempty"`
   885  	Fields         []string     `json:"fields,omitempty"`
   886  	Conflicts      bool         `json:"conflicts,omitempty"`
   887  	ExecutionStats bool         `json:"execution_stats,omitempty"`
   888  }
   889  
   890  // ViewRequest are all params that can be passed to a view
   891  // It can be encoded either as a POST-json or a GET-url.
   892  type ViewRequest struct {
   893  	Key      interface{} `json:"key,omitempty" url:"key,omitempty"`
   894  	StartKey interface{} `json:"start_key,omitempty" url:"start_key,omitempty"`
   895  	EndKey   interface{} `json:"end_key,omitempty" url:"end_key,omitempty"`
   896  
   897  	StartKeyDocID string `json:"startkey_docid,omitempty" url:"startkey_docid,omitempty"`
   898  	EndKeyDocID   string `json:"endkey_docid,omitempty" url:"endkey_docid,omitempty"`
   899  
   900  	// Keys cannot be used in url mode
   901  	Keys []interface{} `json:"keys,omitempty" url:"-"`
   902  
   903  	Limit       int  `json:"limit,omitempty" url:"limit,omitempty"`
   904  	Skip        int  `json:"skip,omitempty" url:"skip,omitempty"`
   905  	Descending  bool `json:"descending,omitempty" url:"descending,omitempty"`
   906  	IncludeDocs bool `json:"include_docs,omitempty" url:"include_docs,omitempty"`
   907  
   908  	InclusiveEnd bool `json:"inclusive_end,omitempty" url:"inclusive_end,omitempty"`
   909  
   910  	Reduce     bool `json:"reduce" url:"reduce"`
   911  	Group      bool `json:"group" url:"group"`
   912  	GroupLevel int  `json:"group_level,omitempty" url:"group_level,omitempty"`
   913  }
   914  
   915  // ViewResponseRow is a row in a ViewResponse
   916  type ViewResponseRow struct {
   917  	ID    string          `json:"id"`
   918  	Key   interface{}     `json:"key"`
   919  	Value interface{}     `json:"value"`
   920  	Doc   json.RawMessage `json:"doc"`
   921  }
   922  
   923  // ViewResponse is the response we receive when executing a view
   924  type ViewResponse struct {
   925  	Total  int                `json:"total_rows"`
   926  	Offset int                `json:"offset,omitempty"`
   927  	Rows   []*ViewResponseRow `json:"rows"`
   928  }
   929  
   930  // DBStatusResponse is the response from DBStatus
   931  type DBStatusResponse struct {
   932  	DBName    string `json:"db_name"`
   933  	UpdateSeq string `json:"update_seq"`
   934  	Sizes     struct {
   935  		File     int `json:"file"`
   936  		External int `json:"external"`
   937  		Active   int `json:"active"`
   938  	} `json:"sizes"`
   939  	PurgeSeq interface{} `json:"purge_seq"` // Was an int before CouchDB 2.3, and a string since then
   940  	Other    struct {
   941  		DataSize int `json:"data_size"`
   942  	} `json:"other"`
   943  	DocDelCount       int    `json:"doc_del_count"`
   944  	DocCount          int    `json:"doc_count"`
   945  	DiskSize          int    `json:"disk_size"`
   946  	DiskFormatVersion int    `json:"disk_format_version"`
   947  	DataSize          int    `json:"data_size"`
   948  	CompactRunning    bool   `json:"compact_running"`
   949  	InstanceStartTime string `json:"instance_start_time"`
   950  }
   951  
   952  // NormalDocsResponse is the response the stack send for _normal_docs queries
   953  type NormalDocsResponse struct {
   954  	Total          int               `json:"total_rows"`
   955  	Rows           []json.RawMessage `json:"rows"`
   956  	Bookmark       string            `json:"bookmark"`
   957  	ExecutionStats *ExecutionStats   `json:"execution_stats,omitempty"`
   958  }
   959  
   960  var _ realtime.Doc = (*JSONDoc)(nil)