package couchbase import ( "encoding/json" "errors" "fmt" "io/ioutil" "math/rand" "net/http" "net/url" "time" ) // ViewRow represents a single result from a view. // // Doc is present only if include_docs was set on the request. type ViewRow struct { ID string Key interface{} Value interface{} Doc *interface{} } // A ViewError is a node-specific error indicating a partial failure // within a view result. type ViewError struct { From string Reason string } func (ve ViewError) Error() string { return "Node: " + ve.From + ", reason: " + ve.Reason } // ViewResult holds the entire result set from a view request, // including the rows and the errors. type ViewResult struct { TotalRows int `json:"total_rows"` Rows []ViewRow Errors []ViewError } func (b *Bucket) randomBaseURL() (*url.URL, error) { nodes := b.HealthyNodes() if len(nodes) == 0 { return nil, errors.New("no available couch rest URLs") } nodeNo := rand.Intn(len(nodes)) node := nodes[nodeNo] b.RLock() name := b.Name pool := b.pool b.RUnlock() u, err := ParseURL(node.CouchAPIBase) if err != nil { return nil, fmt.Errorf("config error: Bucket %q node #%d CouchAPIBase=%q: %v", name, nodeNo, node.CouchAPIBase, err) } else if pool != nil { u.User = pool.client.BaseURL.User } return u, err } const START_NODE_ID = -1 func (b *Bucket) randomNextURL(lastNode int) (*url.URL, int, error) { nodes := b.HealthyNodes() if len(nodes) == 0 { return nil, -1, errors.New("no available couch rest URLs") } var nodeNo int if lastNode == START_NODE_ID || lastNode >= len(nodes) { // randomly select a node if the value of lastNode is invalid nodeNo = rand.Intn(len(nodes)) } else { // wrap around the node list nodeNo = (lastNode + 1) % len(nodes) } b.RLock() name := b.Name pool := b.pool b.RUnlock() node := nodes[nodeNo] u, err := ParseURL(node.CouchAPIBase) if err != nil { return nil, -1, fmt.Errorf("config error: Bucket %q node #%d CouchAPIBase=%q: %v", name, nodeNo, node.CouchAPIBase, err) } else if pool != nil { u.User = pool.client.BaseURL.User } return u, nodeNo, err } // DocID is the document ID type for the startkey_docid parameter in // views. type DocID string func qParam(k, v string) string { format := `"%s"` switch k { case "startkey_docid", "endkey_docid", "stale": format = "%s" } return fmt.Sprintf(format, v) } // ViewURL constructs a URL for a view with the given ddoc, view name, // and parameters. func (b *Bucket) ViewURL(ddoc, name string, params map[string]interface{}) (string, error) { u, err := b.randomBaseURL() if err != nil { return "", err } values := url.Values{} for k, v := range params { switch t := v.(type) { case DocID: values[k] = []string{string(t)} case string: values[k] = []string{qParam(k, t)} case int: values[k] = []string{fmt.Sprintf(`%d`, t)} case bool: values[k] = []string{fmt.Sprintf(`%v`, t)} default: b, err := json.Marshal(v) if err != nil { return "", fmt.Errorf("unsupported value-type %T in Query, "+ "json encoder said %v", t, err) } values[k] = []string{fmt.Sprintf(`%v`, string(b))} } } if ddoc == "" && name == "_all_docs" { u.Path = fmt.Sprintf("/%s/_all_docs", b.GetName()) } else { u.Path = fmt.Sprintf("/%s/_design/%s/_view/%s", b.GetName(), ddoc, name) } u.RawQuery = values.Encode() return u.String(), nil } // ViewCallback is called for each view invocation. var ViewCallback func(ddoc, name string, start time.Time, err error) // ViewCustom performs a view request that can map row values to a // custom type. // // See the source to View for an example usage. func (b *Bucket) ViewCustom(ddoc, name string, params map[string]interface{}, vres interface{}) (err error) { if SlowServerCallWarningThreshold > 0 { defer slowLog(time.Now(), "call to ViewCustom(%q, %q)", ddoc, name) } if ViewCallback != nil { defer func(t time.Time) { ViewCallback(ddoc, name, t, err) }(time.Now()) } u, err := b.ViewURL(ddoc, name, params) if err != nil { return err } req, err := http.NewRequest("GET", u, nil) if err != nil { return err } ah := b.authHandler(false /* bucket not yet locked */) maybeAddAuth(req, ah) res, err := doHTTPRequest(req) if err != nil { return fmt.Errorf("error starting view req at %v: %v", u, err) } defer res.Body.Close() if res.StatusCode != 200 { bod := make([]byte, 512) l, _ := res.Body.Read(bod) return fmt.Errorf("error executing view req at %v: %v - %s", u, res.Status, bod[:l]) } body, err := ioutil.ReadAll(res.Body) if err := json.Unmarshal(body, vres); err != nil { return nil } return nil } // View executes a view. // // The ddoc parameter is just the bare name of your design doc without // the "_design/" prefix. // // Parameters are string keys with values that correspond to couchbase // view parameters. Primitive should work fairly naturally (booleans, // ints, strings, etc...) and other values will attempt to be JSON // marshaled (useful for array indexing on on view keys, for example). // // Example: // // res, err := couchbase.View("myddoc", "myview", map[string]interface{}{ // "group_level": 2, // "startkey_docid": []interface{}{"thing"}, // "endkey_docid": []interface{}{"thing", map[string]string{}}, // "stale": false, // }) func (b *Bucket) View(ddoc, name string, params map[string]interface{}) (ViewResult, error) { vres := ViewResult{} if err := b.ViewCustom(ddoc, name, params, &vres); err != nil { //error in accessing views. Retry once after a bucket refresh b.Refresh() return vres, b.ViewCustom(ddoc, name, params, &vres) } else { return vres, nil } }