From 8c2ae33e5dfe8ffd5e1d060143af585e24884aa8 Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Sun, 4 Feb 2024 21:39:53 +0100 Subject: [PATCH 1/3] add _local_docs support --- x/server/db.go | 30 ++++++++-- x/server/db_test.go | 134 +++++++++++++++++++++++++++++++++++++++++++- x/server/server.go | 12 ++-- 3 files changed, 165 insertions(+), 11 deletions(-) diff --git a/x/server/db.go b/x/server/db.go index bc5622989..e30969762 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,21 @@ 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) (string, bool) { + parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") + if parts[len(parts)-1] == "queries" { + return parts[len(parts)-2], true + } + return parts[len(parts)-1], false +} + +func (s *Server) query() httpe.HandlerWithError { return httpe.HandlerWithErrorFunc(func(w http.ResponseWriter, r *http.Request) error { + 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 +256,16 @@ 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 + switch view { + case "_all_docs": + viewFunc = s.client.DB(db).AllDocs + case "_local_docs": + viewFunc = s.client.DB(db).LocalDocs + default: + return &internal.Error{Status: http.StatusNotFound, Message: fmt.Sprintf("kivik: view %q not found", view)} + } + 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..949fbe1ee 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,135 @@ 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) +} diff --git a/x/server/server.go b/x/server/server.go index cda6f6eda..5db5ec327 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("/_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.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("/_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())) From b072272eb34cc75b85d4aebcda93593b4b21218c Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Sun, 4 Feb 2024 21:41:59 +0100 Subject: [PATCH 2/3] Add _design_docs support --- x/server/db.go | 2 + x/server/db_test.go | 132 ++++++++++++++++++++++++++++++++++++++++++++ x/server/server.go | 6 +- 3 files changed, 137 insertions(+), 3 deletions(-) diff --git a/x/server/db.go b/x/server/db.go index e30969762..b68a65664 100644 --- a/x/server/db.go +++ b/x/server/db.go @@ -262,6 +262,8 @@ func (s *Server) query() httpe.HandlerWithError { 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)} } diff --git a/x/server/db_test.go b/x/server/db_test.go index 949fbe1ee..d8cbd09fb 100644 --- a/x/server/db_test.go +++ b/x/server/db_test.go @@ -384,3 +384,135 @@ func Test_localDocs(t *testing.T) { 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) +} diff --git a/x/server/server.go b/x/server/server.go index 5db5ec327..0347d13ca 100644 --- a/x/server/server.go +++ b/x/server/server.go @@ -153,9 +153,9 @@ func (s *Server) routes(mux *chi.Mux) { 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.notImplemented())) - member.Post("/_design_docs", e(s.notImplemented())) - member.Post("/_design_docs/queries", e(s.notImplemented())) + 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())) From ca39d3da0e2c9f306a3fedb94a6443cbfe516214 Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Sun, 4 Feb 2024 21:50:15 +0100 Subject: [PATCH 3/3] Add support for standard views --- x/server/db.go | 37 ++++++++----- x/server/db_test.go | 132 ++++++++++++++++++++++++++++++++++++++++++++ x/server/server.go | 6 +- 3 files changed, 159 insertions(+), 16 deletions(-) diff --git a/x/server/db.go b/x/server/db.go index b68a65664..41bcea0af 100644 --- a/x/server/db.go +++ b/x/server/db.go @@ -230,17 +230,22 @@ loop: // whichView returns `_all_docs`, `_local_docs`, of `_design_docs`, and whether // the path ends with /queries. -func whichView(r *http.Request) (string, bool) { +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" { - return parts[len(parts)-2], true + isQuery = true + parts = parts[:len(parts)-1] } - return parts[len(parts)-1], false + 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 { - view, isQueries := whichView(r) + ddoc, view, isQueries := whichView(r) req := map[string]interface{}{} if isQueries { var jsonReq struct { @@ -257,15 +262,21 @@ func (s *Server) query() httpe.HandlerWithError { } db := chi.URLParam(r, "db") var viewFunc func(context.Context, ...kivik.Option) *kivik.ResultSet - 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)} + 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() diff --git a/x/server/db_test.go b/x/server/db_test.go index d8cbd09fb..f819198dd 100644 --- a/x/server/db_test.go +++ b/x/server/db_test.go @@ -516,3 +516,135 @@ func Test_designDocs(t *testing.T) { 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 0347d13ca..10af7ce7d 100644 --- a/x/server/server.go +++ b/x/server/server.go @@ -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()))