/* requests with raw string bodies: - curl -X PUT http://db/path/to/key/_val -d 'some value' and complete JSON objects - curl -X PUT http://db/path -d '{"to": {"_val": "nothing here", "key": "some value"}}' -H 'content-type: application/json' While setting the raw string body of a path will only update that path and do not change others, a full JSON request will replace all keys under the specified path. PUT is idempotent. The "_val" key is optional when setting, but can be used to set values right to the key to which they refer. It is sometimes needed, like in this example, here "path/to" had some children values to be set, but also needed a value of its own. Other use of the "_val" key is to set a value to null strictly, because setting the nude key to null will delete it instead of setting it. */ func Put(w http.ResponseWriter, r *http.Request) { ctx := getContext(r) if ctx.lastKey == "_security" { WriteSecurity(w, r) return } var err error var rev string if ctx.exists && ctx.currentRev != ctx.providedRev { log.WithFields(log.Fields{ "given": ctx.providedRev, "current": ctx.currentRev, }).Debug("rev mismatch.") res := responses.ConflictError() w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } if ctx.lastKey == "_val" || ctx.jsonBody == nil { // updating the value at only this specific path, as raw bytes rev, err = db.SaveValueAt(ctx.path, db.ToLevel(ctx.body)) } else if ctx.jsonBody != nil { // saving a JSON tree, as its structure demands, expanding to subpaths and so on if ctx.lastKey[0] == '_' { log.WithFields(log.Fields{ "path": ctx.path, "lastkey": ctx.lastKey, }).Debug("couldn't update special key.") res := responses.BadRequest("you can't update special keys") w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } if ctx.localDoc { rev, err = db.SaveLocalDocAt(ctx.path, ctx.jsonBody) } else { rev, err = db.ReplaceTreeAt(ctx.path, ctx.jsonBody, false) } } else { err = errors.New("value received at " + ctx.path + " is not JSON: " + string(ctx.body)) } if err != nil { log.Debug("couldn't save value: ", err) res := responses.UnknownError(err.Error()) w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } w.WriteHeader(201) w.Header().Add("Content-Type", "application/json") json.NewEncoder(w).Encode(responses.Success{true, ctx.lastKey, rev}) }
/* should accept PATCH requests with JSON objects: `curl -X PATCH http://db/path -d '{"to": {"key": "some value"}}' -H 'content-type: application/json'` this will not replace all values under /path, but only modify the values which the JSON object refers to. */ func Patch(w http.ResponseWriter, r *http.Request) { ctx := getContext(r) var err error var rev string if ctx.lastKey[0] == '_' { res := responses.BadRequest("you can't update special keys") w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } if !ctx.exists { res := responses.NotFound() w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } if ctx.currentRev != ctx.providedRev { log.WithFields(log.Fields{ "given": ctx.providedRev, "current": ctx.currentRev, }).Debug("rev mismatch.") res := responses.ConflictError() w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } // update the tree as the JSON structure demands rev, err = db.SaveTreeAt(ctx.path, ctx.jsonBody) if err != nil { log.Debug("couldn't save value: ", err) res := responses.UnknownError(err.Error()) w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } w.WriteHeader(200) w.Header().Add("Content-Type", "application/json") json.NewEncoder(w).Encode(responses.Success{true, ctx.lastKey, rev}) }
func Delete(w http.ResponseWriter, r *http.Request) { ctx := getContext(r) var err error var rev string if !ctx.exists { res := responses.NotFound() w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } if ctx.lastKey[0] == '_' { res := responses.BadRequest("you can't delete special keys") w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } if ctx.currentRev != "" && ctx.currentRev != ctx.providedRev { log.WithFields(log.Fields{ "given": ctx.providedRev, "current": ctx.currentRev, }).Debug("rev mismatch.") res := responses.ConflictError() w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } rev, err = db.DeleteAt(ctx.path) if err != nil { log.Error("couldn't delete key at ", ctx.path, ": ", err) res := responses.NotFound() w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } w.WriteHeader(200) w.Header().Add("Content-Type", "application/json") json.NewEncoder(w).Encode(responses.Success{true, ctx.lastKey, rev}) }
func TestCouchDBSpecialEndpoints(t *testing.T) { g := Goblin(t) RegisterFailHandler(func(m string, _ ...int) { g.Fail(m) }) g.Describe("couchdb db special endpoints", func() { g.BeforeEach(func() { rec = httptest.NewRecorder() server = handle.BuildHandler() }) g.Before(func() { db.Erase() db.Start() populateDB() }) g.After(func() { db.End() }) var rev string var oldrev string var id string g.It("_all_docs for a sub db", func() { r, _ = http.NewRequest("GET", "/vehicles/_all_docs", nil) server.ServeHTTP(rec, r) var res responses.AllDocs json.Unmarshal(rec.Body.Bytes(), &res) Expect(res.Rows).To(HaveLen(3)) Expect(res.Rows[2].Id).To(Equal(res.Rows[2].Key)) rev, _ := res.Rows[2].Value.(map[string]interface{})["rev"] Expect(rev).To(HavePrefix("1-")) keys := []string{res.Rows[0].Id, res.Rows[1].Key, res.Rows[2].Id} Expect(keys).To(ConsistOf("airplane", "boat", "car")) }) g.It("_all_docs with selected keys", func() { r, _ = http.NewRequest("GET", "/vehicles/_all_docs?keys=%5B%22airplane%22,%22boat%22%5D", nil) server.ServeHTTP(rec, r) var res responses.AllDocs json.Unmarshal(rec.Body.Bytes(), &res) Expect(res.Rows).To(HaveLen(2)) keys := []string{res.Rows[0].Id, res.Rows[1].Key} Expect(keys).To(ConsistOf("airplane", "boat")) }) g.It("all_docs with include_docs -- for another sub db", func() { r, _ = http.NewRequest("GET", "/vehicles/airplane/_all_docs?include_docs=true", nil) server.ServeHTTP(rec, r) var res responses.AllDocs json.Unmarshal(rec.Body.Bytes(), &res) Expect(res.Rows).To(HaveLen(3)) docid, _ := res.Rows[1].Doc["_id"] Expect(res.Rows[1].Key).To(Equal(docid)) rev, _ := res.Rows[1].Value.(map[string]interface{})["rev"] docrev, _ := res.Rows[1].Doc["_rev"] Expect(rev).To(Equal(docrev)) keys := []string{res.Rows[0].Id, res.Rows[1].Key, res.Rows[2].Id} sort.Strings(keys) Expect(keys).To(Equal([]string{"air", "land", "water"})) docs := map[string]interface{}{ res.Rows[0].Key: res.Rows[0].Doc, res.Rows[1].Key: res.Rows[1].Doc, res.Rows[2].Key: res.Rows[2].Doc, } Expect(docs).To(HaveKey("air")) Expect(res.Rows[0].Doc).To(HaveKey("_rev")) Expect(res.Rows[0].Doc).To(HaveKeyWithValue("_id", res.Rows[0].Id)) }) g.It("_bulk_get", func() { r, _ = http.NewRequest("POST", "/vehicles/_bulk_get", bytes.NewReader([]byte(`{ "docs": [ {"id": "nonexisting-doc"}, {"id": "car"}, {"_id": "airplane"} ] }`))) server.ServeHTTP(rec, r) var res responses.BulkGet json.Unmarshal(rec.Body.Bytes(), &res) Expect(res.Results[0].Docs[0].Ok).To(BeNil()) Expect(res.Results[0].Docs[0].Error).ToNot(BeNil()) Expect(res.Results[1].Docs[0].Ok).ToNot(BeNil()) Expect(res.Results[1].Docs[0].Error).To(BeNil()) doc := *res.Results[1].Docs[0].Ok id, _ := doc["_id"] irev, _ := doc["_rev"] rev = irev.(string) water, _ := doc["water"] Expect(id.(string)).To(Equal("car")) Expect(res.Results[1].Id).To(Equal(id)) Expect(water).To(BeEquivalentTo(value(false))) Expect(res.Results[2].Docs[0].Ok).To(BeNil()) Expect(res.Results[0].Docs[0].Error).ToNot(BeNil()) }) g.It("_bulk_docs", func() { r, _ = http.NewRequest("POST", "/vehicles/_bulk_docs", bytes.NewReader([]byte(`{ "docs": [ {"everywhere": true}, {"_id": "car", "_rev": "`+rev+`", "space": false, "land": true}, {"_id": "airplane", "nowhere": false}, {"_id": "_local/.abchtru", "replication+data": "k"}, {"_id": "empty-doc"}, {"_id": "doc-with-a-rev-already-set", "_rev": "4-sa98hsa3i4", "val": 33} ] }`))) server.ServeHTTP(rec, r) Expect(rec.Code).To(Equal(201)) var res []responses.BulkDocsResult json.Unmarshal(rec.Body.Bytes(), &res) Expect(res).To(HaveLen(6)) Expect(res[0].Error).To(Equal("")) Expect(res[0].Ok).To(Equal(true)) Expect(res[0].Rev).To(HavePrefix("1-")) id = res[0].Id Expect(res[1].Id).To(Equal("car")) prevn, _ := strconv.Atoi(strings.Split(rev, "-")[0]) Expect(res[1].Rev).To(HavePrefix(fmt.Sprintf("%d-", prevn+1))) oldrev = rev rev = res[1].Rev cfe := responses.ConflictError() Expect(res[2].Error).To(Equal(cfe.Error)) Expect(res[3].Id).To(Equal("_local/.abchtru")) Expect(res[3].Ok).To(Equal(true)) Expect(res[4].Ok).To(Equal(true)) Expect(res[4].Rev).To(HavePrefix("1-")) Expect(res[5].Ok).To(Equal(true)) Expect(res[5].Rev).To(HavePrefix("5-")) }) g.It("_bulk_docs with new_edits=false", func() { r, _ = http.NewRequest("POST", "/animals/_bulk_docs", bytes.NewReader([]byte(`{ "docs": [ {"_id": "0", "_rev": "34-83fsop4", "name": "albatroz"}, {"_id": "1", "_rev": "0-a0a0a0a0", "name": "puppy"}, {"_id": "2"} ], "new_edits": false }`))) server.ServeHTTP(rec, r) Expect(rec.Code).To(Equal(201)) var res []responses.BulkDocsResult json.Unmarshal(rec.Body.Bytes(), &res) Expect(res).To(HaveLen(3)) Expect(res[0].Ok).To(Equal(true)) Expect(res[1].Ok).To(Equal(true)) Expect(res[2].Ok).To(Equal(false)) Expect(db.GetRev("/animals/0")).To(BeEquivalentTo("34-83fsop4")) Expect(db.GetRev("/animals/1")).ToNot(BeEquivalentTo("0-a0a0a0a0")) Expect(db.GetValueAt("/animals/0/name")).To(BeEquivalentTo(`"albatroz"`)) Expect(db.GetValueAt("/animals/1/name")).To(BeEquivalentTo(`"dog"`)) }) g.It("should have the correct docs saved", func() { Expect(db.GetValueAt("/vehicles/" + id + "/everywhere")).To(BeEquivalentTo("true")) Expect(db.GetLocalDocJsonAt("/vehicles/_local/.abchtru")).To(MatchJSON(`{ "_id": "_local/.abchtru", "_rev": "0-1", "replication+data": "k" }`)) }) g.It("shouldn't show _local docs on _all_docs", func() { r, _ = http.NewRequest("GET", "/vehicles/_all_docs", nil) server.ServeHTTP(rec, r) var res responses.AllDocs json.Unmarshal(rec.Body.Bytes(), &res) Expect(res.Rows).To(HaveLen(5)) }) g.It("_revs_diff", func() { r, _ = http.NewRequest("POST", "/vehicles/_revs_diff", bytes.NewReader([]byte(`{ "everywhere": ["2-invalidrev"], "car": ["`+oldrev+`", "`+rev+`", "1-invalidrev"], "airplane": ["1-nonexisting"] }`))) server.ServeHTTP(rec, r) Expect(rec.Code).To(Equal(200)) var res map[string]responses.RevsDiffResult json.Unmarshal(rec.Body.Bytes(), &res) everywhere, _ := res["everywhere"] car, _ := res["car"] airplane, _ := res["airplane"] Expect(everywhere.Missing).To(Equal([]string{"2-invalidrev"})) Expect(car.Missing).To(Equal([]string{oldrev, "1-invalidrev"})) Expect(airplane.Missing).To(Equal([]string{"1-nonexisting"})) }) }) }
// this method only exists for compatibility with PouchDB and should not be used elsewhere. func BulkDocs(w http.ResponseWriter, r *http.Request) { ctx := getContext(r) var idocs interface{} var iid interface{} var irev interface{} var id string var rev string var ok bool if idocs, ok = ctx.jsonBody["docs"]; !ok { res := responses.BadRequest("You're supposed to send an array of docs to input on the database, and you didn't.") w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } /* options */ new_edits := true if inew_edits, ok := ctx.jsonBody["new_edits"]; ok { new_edits = inew_edits.(bool) } path := db.CleanPath(ctx.path) docs := idocs.([]interface{}) res := make([]responses.BulkDocsResult, len(docs)) var err error if new_edits { /* procedure for normal document saving */ for i, idoc := range docs { var newrev string localDoc := false doc := idoc.(map[string]interface{}) if iid, ok = doc["_id"]; ok { id = iid.(string) if strings.HasPrefix(id, "_local/") { localDoc = true } if irev, ok = doc["_rev"]; ok { rev = irev.(string) } else { rev = "" } } else { id = db.Random(5) } if !localDoc { // procedure for normal documents // check rev matching: if iid != nil /* when iid is nil that means the doc had no _id, so we don't have to check. */ { currentRev := db.GetRev(path + "/" + db.EscapeKey(id)) if string(currentRev) != rev && !bytes.HasPrefix(currentRev, []byte{'0', '-'}) { // rev is not matching, don't input this document e := responses.ConflictError() res[i] = responses.BulkDocsResult{ Id: id, Error: e.Error, Reason: e.Reason, } continue } } // proceed to write the document. newrev, err = db.ReplaceTreeAt(path+"/"+db.EscapeKey(id), doc, false) if err != nil { e := responses.UnknownError() res[i] = responses.BulkDocsResult{ Id: id, Error: e.Error, Reason: e.Reason, } continue } } else { // procedure for local documents // check rev matching: currentRev := db.GetLocalDocRev(path + "/" + id) if currentRev != "0-0" && currentRev != rev { // rev is not matching, don't input this document e := responses.ConflictError() res[i] = responses.BulkDocsResult{ Id: id, Error: e.Error, Reason: e.Reason, } continue } // proceed to write the document. newrev, err = db.SaveLocalDocAt(path+"/"+id, doc) if err != nil { e := responses.UnknownError() res[i] = responses.BulkDocsResult{ Id: id, Error: e.Error, Reason: e.Reason, } continue } } res[i] = responses.BulkDocsResult{ Id: id, Ok: true, Rev: newrev, } } } else { /* procedure for replication-type _bulk_docs (new_edits=false)*/ for i, idoc := range docs { id = "" rev = "" doc := idoc.(map[string]interface{}) if iid, ok = doc["_id"]; ok { id = iid.(string) if irev, ok = doc["_rev"]; ok { rev = irev.(string) } } if id == "" || rev == "" { e := responses.UnknownError() res[i] = responses.BulkDocsResult{ Id: id, Error: e.Error, Reason: e.Reason, } continue } // proceed to write currentRev := db.GetRev(path + "/" + db.EscapeKey(id)) if rev < string(currentRev) { // will write only the rev if it is not the winning rev db.AcknowledgeRevFor(path+"/"+db.EscapeKey(id), rev) } else { // otherwise will write the whole doc normally (replacing the current) rev, err = db.ReplaceTreeAt(path+"/"+db.EscapeKey(id), doc, true) } // handle write error if err != nil { e := responses.UnknownError() res[i] = responses.BulkDocsResult{ Id: id, Error: e.Error, Reason: e.Reason, } continue } res[i] = responses.BulkDocsResult{ Id: id, Ok: true, Rev: rev, } } } w.WriteHeader(201) json.NewEncoder(w).Encode(res) }