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)