diff --git a/x/server/db.go b/x/server/db.go index bc5622989..41bcea0af 100644 --- a/x/server/db.go +++ b/x/server/db.go @@ -16,16 +16,18 @@ package server import ( + "context" "encoding/json" "fmt" "net/http" - "path/filepath" "strconv" + "strings" "time" "github.com/go-chi/chi/v5" "gitlab.com/flimzy/httpe" + "github.com/go-kivik/kivik/v4" "github.com/go-kivik/kivik/v4/driver" "github.com/go-kivik/kivik/v4/internal" ) @@ -226,10 +228,26 @@ loop: return updates.Err() } -func (s *Server) allDocs() httpe.HandlerWithError { +// whichView returns `_all_docs`, `_local_docs`, of `_design_docs`, and whether +// the path ends with /queries. +func whichView(r *http.Request) (ddoc, view string, isQueries bool) { + parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") + var isQuery bool + if parts[len(parts)-1] == "queries" { + isQuery = true + parts = parts[:len(parts)-1] + } + if parts[1] == "_design" { + return parts[2], parts[4], isQuery + } + return "", parts[len(parts)-1], isQuery +} + +func (s *Server) query() httpe.HandlerWithError { return httpe.HandlerWithErrorFunc(func(w http.ResponseWriter, r *http.Request) error { + ddoc, view, isQueries := whichView(r) req := map[string]interface{}{} - if _, last := filepath.Split(r.URL.Path); last == "queries" { + if isQueries { var jsonReq struct { Queries []map[string]interface{} `json:"queries"` } @@ -243,7 +261,24 @@ func (s *Server) allDocs() httpe.HandlerWithError { } } db := chi.URLParam(r, "db") - rows := s.client.DB(db).AllDocs(r.Context(), options(r)) + var viewFunc func(context.Context, ...kivik.Option) *kivik.ResultSet + if ddoc == "" { + switch view { + case "_all_docs": + viewFunc = s.client.DB(db).AllDocs + case "_local_docs": + viewFunc = s.client.DB(db).LocalDocs + case "_design_docs": + viewFunc = s.client.DB(db).DesignDocs + default: + return &internal.Error{Status: http.StatusNotFound, Message: fmt.Sprintf("kivik: view %q not found", view)} + } + } else { + viewFunc = func(ctx context.Context, opts ...kivik.Option) *kivik.ResultSet { + return s.client.DB(db).Query(ctx, ddoc, view, options(r)) + } + } + rows := viewFunc(r.Context(), options(r)) defer rows.Close() if err := rows.Err(); err != nil { diff --git a/x/server/db_test.go b/x/server/db_test.go index 518af89d8..f819198dd 100644 --- a/x/server/db_test.go +++ b/x/server/db_test.go @@ -163,7 +163,7 @@ func Test_allDocs(t *testing.T) { }, }, { - name: "multi queries defaults", + name: "multi queries", authUser: userAdmin, method: http.MethodPost, path: "/db1/_all_docs/queries", @@ -252,3 +252,399 @@ func Test_allDocs(t *testing.T) { tests.Run(t) } + +func Test_localDocs(t *testing.T) { + tests := serverTests{ + { + name: "GET defaults", + authUser: userAdmin, + method: http.MethodGet, + path: "/db1/_local_docs", + client: func() *kivik.Client { + client, mock, err := mockdb.New() + if err != nil { + t.Fatal(err) + } + db := mock.NewDB() + mock.ExpectDB().WillReturn(db) + db.ExpectSecurity().WillReturn(&driver.Security{}) + mock.ExpectDB().WillReturn(db) + db.ExpectLocalDocs().WillReturn(mockdb.NewRows(). + AddRow(&driver.Row{ + ID: "foo", + Key: []byte(`"foo"`), + Value: strings.NewReader(`{"rev": "1-beea34a62a215ab051862d1e5d93162e"}`), + }). + TotalRows(99), + ) + return client + }(), + wantStatus: http.StatusOK, + wantJSON: map[string]interface{}{ + "offset": 0, + "rows": []interface{}{ + map[string]interface{}{ + "id": "foo", + "key": "foo", + "value": map[string]interface{}{ + "rev": "1-beea34a62a215ab051862d1e5d93162e", + }, + }, + }, + "total_rows": 99, + }, + }, + { + name: "multi queries", + authUser: userAdmin, + method: http.MethodPost, + path: "/db1/_local_docs/queries", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader(`{"queries": [{"keys": ["foo", "bar"]}]}`), + client: func() *kivik.Client { + client, mock, err := mockdb.New() + if err != nil { + t.Fatal(err) + } + db := mock.NewDB() + mock.ExpectDB().WillReturn(db) + db.ExpectSecurity().WillReturn(&driver.Security{}) + mock.ExpectDB().WillReturn(db) + db.ExpectLocalDocs().WillReturn(mockdb.NewRows(). + AddRow(&driver.Row{ + ID: "foo", + Key: []byte(`"foo"`), + Value: strings.NewReader(`{"rev": "1-beea34a62a215ab051862d1e5d93162e"}`), + }). + TotalRows(99), + ) + return client + }(), + wantStatus: http.StatusOK, + wantJSON: map[string]interface{}{ + "offset": 0, + "rows": []interface{}{ + map[string]interface{}{ + "id": "foo", + "key": "foo", + "value": map[string]interface{}{ + "rev": "1-beea34a62a215ab051862d1e5d93162e", + }, + }, + }, + "total_rows": 99, + }, + }, + { + name: "POST _local_docs", + authUser: userAdmin, + method: http.MethodPost, + path: "/db1/_local_docs", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader(`{"keys": ["foo", "bar"]}`), + client: func() *kivik.Client { + client, mock, err := mockdb.New() + if err != nil { + t.Fatal(err) + } + db := mock.NewDB() + mock.ExpectDB().WillReturn(db) + db.ExpectSecurity().WillReturn(&driver.Security{}) + mock.ExpectDB().WillReturn(db) + db.ExpectLocalDocs().WillReturn(mockdb.NewRows(). + AddRow(&driver.Row{ + ID: "foo", + Key: []byte(`"foo"`), + Value: strings.NewReader(`{"rev": "1-beea34a62a215ab051862d1e5d93162e"}`), + }). + TotalRows(99), + ) + return client + }(), + wantStatus: http.StatusOK, + wantJSON: map[string]interface{}{ + "offset": 0, + "rows": []interface{}{ + map[string]interface{}{ + "id": "foo", + "key": "foo", + "value": map[string]interface{}{ + "rev": "1-beea34a62a215ab051862d1e5d93162e", + }, + }, + }, + "total_rows": 99, + }, + }, + } + + tests.Run(t) +} + +func Test_designDocs(t *testing.T) { + tests := serverTests{ + { + name: "GET defaults", + authUser: userAdmin, + method: http.MethodGet, + path: "/db1/_design_docs", + client: func() *kivik.Client { + client, mock, err := mockdb.New() + if err != nil { + t.Fatal(err) + } + db := mock.NewDB() + mock.ExpectDB().WillReturn(db) + db.ExpectSecurity().WillReturn(&driver.Security{}) + mock.ExpectDB().WillReturn(db) + db.ExpectDesignDocs().WillReturn(mockdb.NewRows(). + AddRow(&driver.Row{ + ID: "foo", + Key: []byte(`"foo"`), + Value: strings.NewReader(`{"rev": "1-beea34a62a215ab051862d1e5d93162e"}`), + }). + TotalRows(99), + ) + return client + }(), + wantStatus: http.StatusOK, + wantJSON: map[string]interface{}{ + "offset": 0, + "rows": []interface{}{ + map[string]interface{}{ + "id": "foo", + "key": "foo", + "value": map[string]interface{}{ + "rev": "1-beea34a62a215ab051862d1e5d93162e", + }, + }, + }, + "total_rows": 99, + }, + }, + { + name: "multi queries", + authUser: userAdmin, + method: http.MethodPost, + path: "/db1/_design_docs/queries", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader(`{"queries": [{"keys": ["foo", "bar"]}]}`), + client: func() *kivik.Client { + client, mock, err := mockdb.New() + if err != nil { + t.Fatal(err) + } + db := mock.NewDB() + mock.ExpectDB().WillReturn(db) + db.ExpectSecurity().WillReturn(&driver.Security{}) + mock.ExpectDB().WillReturn(db) + db.ExpectDesignDocs().WillReturn(mockdb.NewRows(). + AddRow(&driver.Row{ + ID: "foo", + Key: []byte(`"foo"`), + Value: strings.NewReader(`{"rev": "1-beea34a62a215ab051862d1e5d93162e"}`), + }). + TotalRows(99), + ) + return client + }(), + wantStatus: http.StatusOK, + wantJSON: map[string]interface{}{ + "offset": 0, + "rows": []interface{}{ + map[string]interface{}{ + "id": "foo", + "key": "foo", + "value": map[string]interface{}{ + "rev": "1-beea34a62a215ab051862d1e5d93162e", + }, + }, + }, + "total_rows": 99, + }, + }, + { + name: "POST _design_docs", + authUser: userAdmin, + method: http.MethodPost, + path: "/db1/_design_docs", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader(`{"keys": ["foo", "bar"]}`), + client: func() *kivik.Client { + client, mock, err := mockdb.New() + if err != nil { + t.Fatal(err) + } + db := mock.NewDB() + mock.ExpectDB().WillReturn(db) + db.ExpectSecurity().WillReturn(&driver.Security{}) + mock.ExpectDB().WillReturn(db) + db.ExpectDesignDocs().WillReturn(mockdb.NewRows(). + AddRow(&driver.Row{ + ID: "foo", + Key: []byte(`"foo"`), + Value: strings.NewReader(`{"rev": "1-beea34a62a215ab051862d1e5d93162e"}`), + }). + TotalRows(99), + ) + return client + }(), + wantStatus: http.StatusOK, + wantJSON: map[string]interface{}{ + "offset": 0, + "rows": []interface{}{ + map[string]interface{}{ + "id": "foo", + "key": "foo", + "value": map[string]interface{}{ + "rev": "1-beea34a62a215ab051862d1e5d93162e", + }, + }, + }, + "total_rows": 99, + }, + }, + } + + tests.Run(t) +} + +func Test_queryView(t *testing.T) { + tests := serverTests{ + { + name: "GET defaults", + authUser: userAdmin, + method: http.MethodGet, + path: "/db1/_design/foo/_view/bar", + client: func() *kivik.Client { + client, mock, err := mockdb.New() + if err != nil { + t.Fatal(err) + } + db := mock.NewDB() + mock.ExpectDB().WillReturn(db) + db.ExpectSecurity().WillReturn(&driver.Security{}) + mock.ExpectDB().WillReturn(db) + db.ExpectQuery().WillReturn(mockdb.NewRows(). + AddRow(&driver.Row{ + ID: "foo", + Key: []byte(`"foo"`), + Value: strings.NewReader(`{"rev": "1-beea34a62a215ab051862d1e5d93162e"}`), + }). + TotalRows(99), + ) + return client + }(), + wantStatus: http.StatusOK, + wantJSON: map[string]interface{}{ + "offset": 0, + "rows": []interface{}{ + map[string]interface{}{ + "id": "foo", + "key": "foo", + "value": map[string]interface{}{ + "rev": "1-beea34a62a215ab051862d1e5d93162e", + }, + }, + }, + "total_rows": 99, + }, + }, + { + name: "multi queries", + authUser: userAdmin, + method: http.MethodPost, + path: "/db1/_design/foo/_view/bar/queries", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader(`{"queries": [{"keys": ["foo", "bar"]}]}`), + client: func() *kivik.Client { + client, mock, err := mockdb.New() + if err != nil { + t.Fatal(err) + } + db := mock.NewDB() + mock.ExpectDB().WillReturn(db) + db.ExpectSecurity().WillReturn(&driver.Security{}) + mock.ExpectDB().WillReturn(db) + db.ExpectQuery().WillReturn(mockdb.NewRows(). + AddRow(&driver.Row{ + ID: "foo", + Key: []byte(`"foo"`), + Value: strings.NewReader(`{"rev": "1-beea34a62a215ab051862d1e5d93162e"}`), + }). + TotalRows(99), + ) + return client + }(), + wantStatus: http.StatusOK, + wantJSON: map[string]interface{}{ + "offset": 0, + "rows": []interface{}{ + map[string]interface{}{ + "id": "foo", + "key": "foo", + "value": map[string]interface{}{ + "rev": "1-beea34a62a215ab051862d1e5d93162e", + }, + }, + }, + "total_rows": 99, + }, + }, + { + name: "POST _design/foo/_view/bar", + authUser: userAdmin, + method: http.MethodPost, + path: "/db1/_design/foo/_view/bar", + headers: map[string]string{ + "Content-Type": "application/json", + }, + body: strings.NewReader(`{"keys": ["foo", "bar"]}`), + client: func() *kivik.Client { + client, mock, err := mockdb.New() + if err != nil { + t.Fatal(err) + } + db := mock.NewDB() + mock.ExpectDB().WillReturn(db) + db.ExpectSecurity().WillReturn(&driver.Security{}) + mock.ExpectDB().WillReturn(db) + db.ExpectQuery().WillReturn(mockdb.NewRows(). + AddRow(&driver.Row{ + ID: "foo", + Key: []byte(`"foo"`), + Value: strings.NewReader(`{"rev": "1-beea34a62a215ab051862d1e5d93162e"}`), + }). + TotalRows(99), + ) + return client + }(), + wantStatus: http.StatusOK, + wantJSON: map[string]interface{}{ + "offset": 0, + "rows": []interface{}{ + map[string]interface{}{ + "id": "foo", + "key": "foo", + "value": map[string]interface{}{ + "rev": "1-beea34a62a215ab051862d1e5d93162e", + }, + }, + }, + "total_rows": 99, + }, + }, + } + + tests.Run(t) +} diff --git a/x/server/server.go b/x/server/server.go index cda6f6eda..10af7ce7d 100644 --- a/x/server/server.go +++ b/x/server/server.go @@ -150,15 +150,15 @@ func (s *Server) routes(mux *chi.Mux) { member.Get("/", e(s.db())) admin.Put("/", e(s.createDB())) admin.Delete("/", e(s.deleteDB())) - member.Get("/_all_docs", e(s.allDocs())) - member.Post("/_all_docs/queries", e(s.allDocs())) - member.Post("/_all_docs", e(s.allDocs())) - member.Get("/_design_docs", e(s.notImplemented())) - member.Post("/_design_docs", e(s.notImplemented())) - member.Post("/_design_docs/queries", e(s.notImplemented())) - member.Get("/_local_docs", e(s.notImplemented())) - member.Post("/_local_docs", e(s.notImplemented())) - member.Post("/_local_docs/queries", e(s.notImplemented())) + member.Get("/_all_docs", e(s.query())) + member.Post("/_all_docs/queries", e(s.query())) + member.Post("/_all_docs", e(s.query())) + member.Get("/_design_docs", e(s.query())) + member.Post("/_design_docs", e(s.query())) + member.Post("/_design_docs/queries", e(s.query())) + member.Get("/_local_docs", e(s.query())) + member.Post("/_local_docs", e(s.query())) + member.Post("/_local_docs/queries", e(s.query())) member.Post("/_bulk_get", e(s.notImplemented())) member.Post("/_bulk_docs", e(s.notImplemented())) member.Post("/_find", e(s.notImplemented())) @@ -204,10 +204,10 @@ func (s *Server) routes(mux *chi.Mux) { member.Get("/_design/{ddoc}/{attname}", e(s.notImplemented())) dbAdmin.Put("/_design/{ddoc}/{attname}", e(s.notImplemented())) dbAdmin.Delete("/_design/{ddoc}/{attname}", e(s.notImplemented())) - member.Get("/_design/{ddoc}/_view/{view}", e(s.notImplemented())) + member.Get("/_design/{ddoc}/_view/{view}", e(s.query())) member.Get("/_design/{ddoc}/_info", e(s.notImplemented())) - member.Post("/_design/{ddoc}/_view/{view}", e(s.notImplemented())) - member.Post("/_design/{ddoc}/_view/{view}/queries", e(s.notImplemented())) + member.Post("/_design/{ddoc}/_view/{view}", e(s.query())) + member.Post("/_design/{ddoc}/_view/{view}/queries", e(s.query())) member.Get("/_design/{ddoc}/_search/{index}", e(s.notImplemented())) member.Get("/_design/{ddoc}/_search_info/{index}", e(s.notImplemented())) member.Post("/_design/{ddoc}/_update/{func}", e(s.notImplemented()))