func TestLocalDocs(t *testing.T) { g := Goblin(t) RegisterFailHandler(func(m string, _ ...int) { g.Fail(m) }) g.Describe("local docs", func() { g.Before(func() { db.Erase() db.Start() }) g.After(func() { db.End() }) g.It("should save a _local doc", func() { Expect(db.SaveLocalDocAt("/banana/_local/w3hnks8hr", map[string]interface{}{ "j": 23, "history": []map[string]interface{}{ map[string]interface{}{"a": "b"}, }, })).To(Equal("0-1")) Expect(db.GetLocalDocJsonAt("/banana/_local/w3hnks8hr")).To(MatchJSON(`{ "_id": "_local/w3hnks8hr", "_rev": "0-1", "j": 23, "history": [{"a": "b"}] }`)) }) g.It("should update a _local doc", func() { Expect(db.SaveLocalDocAt("/banana/_local/w3hnks8hr", map[string]interface{}{ "j": 77, "history": []map[string]interface{}{ map[string]interface{}{"a": "b"}, map[string]interface{}{"c": "d"}, }, "_rev": "0-1", "_id": "_local/w3hnks8hr", })).To(Equal("0-2")) Expect(db.GetLocalDocJsonAt("/banana/_local/w3hnks8hr")).To(MatchJSON(`{ "_id": "_local/w3hnks8hr", "_rev": "0-2", "j": 77, "history": [{"a": "b"}, {"c": "d"}] }`)) }) g.It("should get the _local doc rev", func() { Expect(db.GetLocalDocRev("/banana/_local/w3hnks8hr")).To(BeEquivalentTo("0-2")) }) }) }
// 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) }
func setCommonVariables(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := getContext(r) /* when the path is in the format /nanana/nanana/_val we reply with the single value for that path, otherwise assume the whole tree is being requested. */ ctx.path = r.URL.Path ctx.wantsTree = true ctx.localDoc = false ctx.wantsDatabaseInfo = false entityPath := ctx.path pathKeys := db.SplitKeys(ctx.path) nkeys := len(pathKeys) ctx.lastKey = pathKeys[nkeys-1] if nkeys > 1 && pathKeys[nkeys-2] == "_local" { // workaround for couchdb-like _local/blablabla docs // we use a different sublevel for local docs so we must // acknowledge that somehow. ctx.lastKey = db.UnescapeKey(db.JoinKeys(pathKeys[nkeys-2:])) ctx.localDoc = true } else if ctx.lastKey == "" { // this means the request was made with an ending slash (for example: // https://my.summadb.com/path/to/here/), so it wants couchdb-like information // for the referred sub-database, and not the document at the referred path. // to get information on the document at the referred path the request must be // made without the trailing slash (or with a special header, for the root document // and other scenarios in which the user does not have control over the presence // of the ending slash). ctx.wantsDatabaseInfo = true if ctx.path == "/" { ctx.path = "" ctx.lastKey = "" entityPath = "" } } else { if ctx.lastKey[0] == '_' { ctx.wantsTree = false entityPath = db.JoinKeys(pathKeys[:nkeys-1]) if ctx.lastKey == "_val" { ctx.path = db.JoinKeys(pathKeys[:nkeys-1]) ctx.lastKey = pathKeys[nkeys-1] } } } ctx.currentRev = "" ctx.deleted = false qrev := r.URL.Query().Get("rev") hrev := r.Header.Get("If-Match") drev := "" ctx.providedRev = hrev // will be "" if there's no header rev // fetching current rev if !ctx.localDoc { // procedure for normal documents specialKeys, err := db.GetSpecialKeysAt(entityPath) if err == nil { ctx.currentRev = specialKeys.Rev ctx.deleted = specialKeys.Deleted } } else { // procedure for local documents ctx.currentRev = db.GetLocalDocRev(ctx.path) } // body parsing if r.Method[0] == 'P' { // PUT, PATCH, POST /* filter body size */ b, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) ctx.body = b if err != nil { res := responses.BadRequest("request body too large") w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } if ctx.lastKey != "_val" { err = json.Unmarshal(ctx.body, &ctx.jsonBody) if err != nil { res := responses.BadRequest("invalid JSON sent as JSON") w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } } } revfail := false // rev checking if qrev != "" { if hrev != "" && qrev != hrev { revfail = true } else { ctx.providedRev = qrev } } drev = "" if drevi, ok := ctx.jsonBody["_rev"]; ok { drev = drevi.(string) } if drev != "" { if qrev != "" && qrev != drev { revfail = true } else if hrev != "" && hrev != drev { revfail = true } else if qrev != "" && hrev != "" && qrev != hrev { revfail = true } else { ctx.providedRev = drev } } if revfail { res := responses.BadRequest("different rev values were sent") w.WriteHeader(res.Code) json.NewEncoder(w).Encode(res) return } ctx.exists = ctx.currentRev != "" && !ctx.deleted context.Set(r, k, ctx) next.ServeHTTP(w, r) }) }