Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CBG-4369 optionally return CV on rest API #7203

Merged
merged 3 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions channels/log_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package channels

import (
"fmt"
"strconv"
"time"

"github.com/couchbase/sync_gateway/base"
Expand Down Expand Up @@ -126,3 +127,9 @@ func (rv *RevAndVersion) UnmarshalJSON(data []byte) error {
return fmt.Errorf("unrecognized JSON format for RevAndVersion: %s", data)
}
}

// CV returns ver@src in big endian format 1@cbl for CBL format.
func (rv RevAndVersion) CV() string {
// this should match db.Version.String()
return strconv.FormatUint(base.HexCasToUint64(rv.CurrentVersion), 16) + "@" + rv.CurrentSource
}
25 changes: 19 additions & 6 deletions db/crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,27 +287,40 @@ func (db *DatabaseCollectionWithUser) Get1xRevBody(ctx context.Context, docid, r
maxHistory = math.MaxInt32
}

return db.Get1xRevBodyWithHistory(ctx, docid, revid, maxHistory, nil, attachmentsSince, false)
return db.Get1xRevBodyWithHistory(ctx, docid, revid, Get1xRevBodyOptions{
MaxHistory: maxHistory,
HistoryFrom: nil,
AttachmentsSince: attachmentsSince,
ShowExp: false,
})
}

type Get1xRevBodyOptions struct {
MaxHistory int
HistoryFrom []string
AttachmentsSince []string
ShowExp bool
ShowCV bool
}

// Retrieves rev with request history specified as collection of revids (historyFrom)
func (db *DatabaseCollectionWithUser) Get1xRevBodyWithHistory(ctx context.Context, docid, revid string, maxHistory int, historyFrom []string, attachmentsSince []string, showExp bool) (Body, error) {
rev, err := db.getRev(ctx, docid, revid, maxHistory, historyFrom)
func (db *DatabaseCollectionWithUser) Get1xRevBodyWithHistory(ctx context.Context, docid, revtreeid string, opts Get1xRevBodyOptions) (Body, error) {
rev, err := db.getRev(ctx, docid, revtreeid, opts.MaxHistory, opts.HistoryFrom)
if err != nil {
return nil, err
}

// RequestedHistory is the _revisions returned in the body. Avoids mutating revision.History, in case it's needed
// during attachment processing below
requestedHistory := rev.History
if maxHistory == 0 {
if opts.MaxHistory == 0 {
requestedHistory = nil
}
if requestedHistory != nil {
_, requestedHistory = trimEncodedRevisionsToAncestor(ctx, requestedHistory, historyFrom, maxHistory)
_, requestedHistory = trimEncodedRevisionsToAncestor(ctx, requestedHistory, opts.HistoryFrom, opts.MaxHistory)
}

return rev.Mutable1xBody(ctx, db, requestedHistory, attachmentsSince, showExp)
return rev.Mutable1xBody(ctx, db, requestedHistory, opts.AttachmentsSince, opts.ShowExp, opts.ShowCV)
}

// Underlying revision retrieval used by Get1xRevBody, Get1xRevBodyWithHistory, GetRevCopy.
Expand Down
6 changes: 5 additions & 1 deletion db/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,7 @@ type IDRevAndSequence struct {
DocID string
RevID string
Sequence uint64
CV string
}

// The ForEachDocID options for limiting query results
Expand Down Expand Up @@ -987,13 +988,15 @@ func (c *DatabaseCollection) processForEachDocIDResults(ctx context.Context, cal
var found bool
var docid, revid string
var seq uint64
var cv string
var channels []string
if c.useViews() {
var viewRow AllDocsViewQueryRow
found = results.Next(ctx, &viewRow)
if found {
docid = viewRow.Key
revid = viewRow.Value.RevID.RevTreeID
cv = viewRow.Value.RevID.CV()
seq = viewRow.Value.Sequence
channels = viewRow.Value.Channels
}
Expand All @@ -1002,6 +1005,7 @@ func (c *DatabaseCollection) processForEachDocIDResults(ctx context.Context, cal
if found {
docid = queryRow.Id
revid = queryRow.RevID.RevTreeID
cv = queryRow.RevID.CV()
seq = queryRow.Sequence
channels = make([]string, 0)
// Query returns all channels, but we only want to return active channels
Expand All @@ -1016,7 +1020,7 @@ func (c *DatabaseCollection) processForEachDocIDResults(ctx context.Context, cal
break
}

if ok, err := callback(IDRevAndSequence{docid, revid, seq}, channels); ok {
if ok, err := callback(IDRevAndSequence{DocID: docid, RevID: revid, Sequence: seq, CV: cv}, channels); ok {
count++
} else if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion db/hybrid_logical_vector.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func ParseVersion(versionString string) (version Version, err error) {
return version, nil
}

// String returns a version/sourceID pair in CBL string format
// String returns a version/sourceID pair in CBL string format. This does not match the format serialized on CBS, which will be in 0x0 format.
func (v Version) String() string {
return strconv.FormatUint(v.Value, 16) + "@" + v.SourceID
}
Expand Down
6 changes: 5 additions & 1 deletion db/revision_cache_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func (rev *DocumentRevision) Inject1xBodyProperties(ctx context.Context, db *Dat

// Mutable1xBody returns a copy of the given document revision as a 1.x style body (with special properties)
// Callers are free to modify this body without affecting the document revision.
func (rev *DocumentRevision) Mutable1xBody(ctx context.Context, db *DatabaseCollectionWithUser, requestedHistory Revisions, attachmentsSince []string, showExp bool) (b Body, err error) {
func (rev *DocumentRevision) Mutable1xBody(ctx context.Context, db *DatabaseCollectionWithUser, requestedHistory Revisions, attachmentsSince []string, showExp bool, showCV bool) (b Body, err error) {
b, err = rev.Body()
if err != nil {
return nil, err
Expand All @@ -283,6 +283,10 @@ func (rev *DocumentRevision) Mutable1xBody(ctx context.Context, db *DatabaseColl
b[BodyExpiry] = rev.Expiry.Format(time.RFC3339)
}

if showCV && rev.CV != nil {
b["_cv"] = rev.CV.String()
}

if rev.Deleted {
b[BodyDeleted] = true
}
Expand Down
78 changes: 48 additions & 30 deletions rest/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ import (
"github.com/stretchr/testify/require"
)

type allDocsResponse struct {
TotalRows int `json:"total_rows"`
Offset int `json:"offset"`
Rows []allDocsRow `json:"rows"`
}

func TestPublicChanGuestAccess(t *testing.T) {
rt := NewRestTester(t,
&RestTesterConfig{
Expand Down Expand Up @@ -70,17 +76,6 @@ func TestStarAccess(t *testing.T) {

base.SetUpTestLogging(t, base.LevelDebug, base.KeyChanges)

type allDocsRow struct {
ID string `json:"id"`
Key string `json:"key"`
Value struct {
Rev string `json:"rev"`
Channels []string `json:"channels,omitempty"`
Access map[string]base.Set `json:"access,omitempty"` // for admins only
} `json:"value"`
Doc db.Body `json:"doc,omitempty"`
Error string `json:"error"`
}
var allDocsResult struct {
TotalRows int `json:"total_rows"`
Offset int `json:"offset"`
Expand Down Expand Up @@ -552,23 +547,6 @@ func TestAllDocsAccessControl(t *testing.T) {
rt := NewRestTester(t, &RestTesterConfig{SyncFn: channels.DocChannelsSyncFunction})
defer rt.Close()

type allDocsRow struct {
ID string `json:"id"`
Key string `json:"key"`
Value struct {
Rev string `json:"rev"`
Channels []string `json:"channels,omitempty"`
Access map[string]base.Set `json:"access,omitempty"` // for admins only
} `json:"value"`
Doc db.Body `json:"doc,omitempty"`
Error string `json:"error"`
}
type allDocsResponse struct {
TotalRows int `json:"total_rows"`
Offset int `json:"offset"`
Rows []allDocsRow `json:"rows"`
}

// Create some docs:
a := auth.NewAuthenticator(rt.MetadataStore(), nil, rt.GetDatabase().AuthenticatorOptions(rt.Context()))
a.Collections = rt.GetDatabase().CollectionNames
Expand Down Expand Up @@ -708,13 +686,13 @@ func TestAllDocsAccessControl(t *testing.T) {
assert.Equal(t, []string{"Cinemax"}, allDocsResult.Rows[0].Value.Channels)
assert.Equal(t, "doc1", allDocsResult.Rows[1].Key)
assert.Equal(t, "forbidden", allDocsResult.Rows[1].Error)
assert.Equal(t, "", allDocsResult.Rows[1].Value.Rev)
assert.Nil(t, allDocsResult.Rows[1].Value)
assert.Equal(t, "doc3", allDocsResult.Rows[2].ID)
assert.Equal(t, []string{"Cinemax"}, allDocsResult.Rows[2].Value.Channels)
assert.Equal(t, "1-20912648f85f2bbabefb0993ddd37b41", allDocsResult.Rows[2].Value.Rev)
assert.Equal(t, "b0gus", allDocsResult.Rows[3].Key)
assert.Equal(t, "not_found", allDocsResult.Rows[3].Error)
assert.Equal(t, "", allDocsResult.Rows[3].Value.Rev)
assert.Nil(t, allDocsResult.Rows[3].Value)

// Check GET to _all_docs with keys parameter:
response = rt.SendUserRequest(http.MethodGet, "/{{.keyspace}}/_all_docs?channels=true&keys=%5B%22doc4%22%2C%22doc1%22%2C%22doc3%22%2C%22b0gus%22%5D", "", "alice")
Expand Down Expand Up @@ -1178,3 +1156,43 @@ func TestPublicChannel(t *testing.T) {
response = rt.SendUserRequest("GET", "/{{.keyspace}}/privateDoc", "", "user1")
RequireStatus(t, response, 403)
}

func TestAllDocsCV(t *testing.T) {
rt := NewRestTesterPersistentConfig(t)
defer rt.Close()

const docID = "foo"
docVersion := rt.PutDocDirectly(docID, db.Body{"foo": "bar"})

testCases := []struct {
name string
url string
output string
}{
{
name: "no query string",
url: "/{{.keyspace}}/_all_docs",
output: fmt.Sprintf(`{
"total_rows": 1,
"update_seq": 1,
"rows": [{"key": "%s", "id": "%s", "value": {"rev": "%s"}}]
}`, docID, docID, docVersion.RevTreeID),
},
{
name: "cvs=true",
url: "/{{.keyspace}}/_all_docs?show_cv=true",
output: fmt.Sprintf(`{
"total_rows": 1,
"update_seq": 1,
"rows": [{"key": "%s", "id": "%s", "value": {"rev": "%s", "cv": "%s"}}]
}`, docID, docID, docVersion.RevTreeID, docVersion.CV.String()),
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
response := rt.SendAdminRequest(http.MethodGet, testCase.url, "")
RequireStatus(t, response, http.StatusOK)
require.JSONEq(t, testCase.output, response.Body.String())
})
}
}
2 changes: 1 addition & 1 deletion rest/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2717,7 +2717,7 @@ func TestNullDocHandlingForMutable1xBody(t *testing.T) {

documentRev := db.DocumentRevision{DocID: "doc1", BodyBytes: []byte("null")}

body, err := documentRev.Mutable1xBody(ctx, collection, nil, nil, false)
body, err := documentRev.Mutable1xBody(ctx, collection, nil, nil, false, false)
require.Error(t, err)
require.Nil(t, body)
assert.Contains(t, err.Error(), "null doc body for doc")
Expand Down
49 changes: 32 additions & 17 deletions rest/bulk_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,33 @@ import (
"github.com/couchbase/sync_gateway/db"
)

// allDocsRowValue is a struct that represents possible values returned in a document from /ks/_all_docs endpoint
type allDocsRowValue struct {
Rev string `json:"rev"`
CV string `json:"cv,omitempty"`
Channels []string `json:"channels,omitempty"`
Access map[string]base.Set `json:"access,omitempty"` // for admins only
}

// allDocsRow is a struct that represents a linefrom /ks/_all_docs endpoint
type allDocsRow struct {
Key string `json:"key"`
ID string `json:"id,omitempty"`
Value *allDocsRowValue `json:"value,omitempty"`
Doc json.RawMessage `json:"doc,omitempty"`
UpdateSeq uint64 `json:"update_seq,omitempty"`
Error string `json:"error,omitempty"`
Status int `json:"status,omitempty"`
}

// HTTP handler for _all_docs
func (h *handler) handleAllDocs() error {
// http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API
includeDocs := h.getBoolQuery("include_docs")
includeChannels := h.getBoolQuery("channels")
includeAccess := h.getBoolQuery("access") && h.user == nil
includeRevs := h.getBoolQuery("revs")
includeCVs := h.getBoolQuery("show_cv")
includeSeqs := h.getBoolQuery("update_seq")

// Get the doc IDs if this is a POST request:
Expand Down Expand Up @@ -99,21 +119,6 @@ func (h *handler) handleAllDocs() error {
return result
}

type allDocsRowValue struct {
Rev string `json:"rev"`
Channels []string `json:"channels,omitempty"`
Access map[string]base.Set `json:"access,omitempty"` // for admins only
}
type allDocsRow struct {
Key string `json:"key"`
ID string `json:"id,omitempty"`
Value *allDocsRowValue `json:"value,omitempty"`
Doc json.RawMessage `json:"doc,omitempty"`
UpdateSeq uint64 `json:"update_seq,omitempty"`
Error string `json:"error,omitempty"`
Status int `json:"status,omitempty"`
}

// Subroutine that creates a response row for a document:
totalRows := 0
createRow := func(doc db.IDRevAndSequence, channels []string) *allDocsRow {
Expand Down Expand Up @@ -169,6 +174,9 @@ func (h *handler) handleAllDocs() error {
if includeChannels {
row.Value.Channels = channels
}
if includeCVs {
row.Value.CV = doc.CV
}
return row
}

Expand Down Expand Up @@ -220,7 +228,8 @@ func (h *handler) handleAllDocs() error {
if explicitDocIDs != nil {
count := uint64(0)
for _, docID := range explicitDocIDs {
_, _ = writeDoc(db.IDRevAndSequence{DocID: docID, RevID: "", Sequence: 0}, nil)
// no revtreeid or cv if explicitDocIDs are specified
_, _ = writeDoc(db.IDRevAndSequence{DocID: docID, RevID: "", Sequence: 0, CV: ""}, nil)
count++
if options.Limit > 0 && count == options.Limit {
break
Expand Down Expand Up @@ -364,6 +373,7 @@ func (h *handler) handleBulkGet() error {

includeAttachments := h.getBoolQuery("attachments")
showExp := h.getBoolQuery("show_exp")
showCV := h.getBoolQuery("show_cv")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be consistent with the query parameter used to include cv in the responses. I'm fine with cvs=true like you've got in the all docs case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did show_cv somewhat arbitrarily becuase I thought it made it clearer that it is an output parameter, especially when rev or revs can be input parameters to some functions.

Also cvs reminds me of a US pharmacy or the legacy version control system. I am happy to switch to cvs though.


showRevs := h.getBoolQuery("revs")
globalRevsLimit := int(h.getIntQuery("revs_limit", math.MaxInt32))
Expand Down Expand Up @@ -440,7 +450,12 @@ func (h *handler) handleBulkGet() error {
}

if err == nil {
body, err = h.collection.Get1xRevBodyWithHistory(h.ctx(), docid, revid, docRevsLimit, revsFrom, attsSince, showExp)
body, err = h.collection.Get1xRevBodyWithHistory(h.ctx(), docid, revid, db.Get1xRevBodyOptions{
MaxHistory: docRevsLimit,
HistoryFrom: revsFrom,
AttachmentsSince: attsSince,
ShowExp: showExp,
ShowCV: showCV})
}

if err != nil {
Expand Down
Loading
Loading