Skip to content

Commit 8b8b03d

Browse files
authored
CBG-4369 optionally return CV on rest API (#7203)
* CBG-4369 optionally return CV on rest API * pass lint * Switch to show_cv parameter and add bulk_get test cases
1 parent 058d42e commit 8b8b03d

File tree

10 files changed

+266
-60
lines changed

10 files changed

+266
-60
lines changed

channels/log_entry.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ package channels
1414

1515
import (
1616
"fmt"
17+
"strconv"
1718
"time"
1819

1920
"github.com/couchbase/sync_gateway/base"
@@ -126,3 +127,9 @@ func (rv *RevAndVersion) UnmarshalJSON(data []byte) error {
126127
return fmt.Errorf("unrecognized JSON format for RevAndVersion: %s", data)
127128
}
128129
}
130+
131+
// CV returns ver@src in big endian format 1@cbl for CBL format.
132+
func (rv RevAndVersion) CV() string {
133+
// this should match db.Version.String()
134+
return strconv.FormatUint(base.HexCasToUint64(rv.CurrentVersion), 16) + "@" + rv.CurrentSource
135+
}

db/crud.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -287,27 +287,40 @@ func (db *DatabaseCollectionWithUser) Get1xRevBody(ctx context.Context, docid, r
287287
maxHistory = math.MaxInt32
288288
}
289289

290-
return db.Get1xRevBodyWithHistory(ctx, docid, revid, maxHistory, nil, attachmentsSince, false)
290+
return db.Get1xRevBodyWithHistory(ctx, docid, revid, Get1xRevBodyOptions{
291+
MaxHistory: maxHistory,
292+
HistoryFrom: nil,
293+
AttachmentsSince: attachmentsSince,
294+
ShowExp: false,
295+
})
296+
}
297+
298+
type Get1xRevBodyOptions struct {
299+
MaxHistory int
300+
HistoryFrom []string
301+
AttachmentsSince []string
302+
ShowExp bool
303+
ShowCV bool
291304
}
292305

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

300313
// RequestedHistory is the _revisions returned in the body. Avoids mutating revision.History, in case it's needed
301314
// during attachment processing below
302315
requestedHistory := rev.History
303-
if maxHistory == 0 {
316+
if opts.MaxHistory == 0 {
304317
requestedHistory = nil
305318
}
306319
if requestedHistory != nil {
307-
_, requestedHistory = trimEncodedRevisionsToAncestor(ctx, requestedHistory, historyFrom, maxHistory)
320+
_, requestedHistory = trimEncodedRevisionsToAncestor(ctx, requestedHistory, opts.HistoryFrom, opts.MaxHistory)
308321
}
309322

310-
return rev.Mutable1xBody(ctx, db, requestedHistory, attachmentsSince, showExp)
323+
return rev.Mutable1xBody(ctx, db, requestedHistory, opts.AttachmentsSince, opts.ShowExp, opts.ShowCV)
311324
}
312325

313326
// Underlying revision retrieval used by Get1xRevBody, Get1xRevBodyWithHistory, GetRevCopy.

db/database.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,7 @@ type IDRevAndSequence struct {
952952
DocID string
953953
RevID string
954954
Sequence uint64
955+
CV string
955956
}
956957

957958
// The ForEachDocID options for limiting query results
@@ -987,13 +988,15 @@ func (c *DatabaseCollection) processForEachDocIDResults(ctx context.Context, cal
987988
var found bool
988989
var docid, revid string
989990
var seq uint64
991+
var cv string
990992
var channels []string
991993
if c.useViews() {
992994
var viewRow AllDocsViewQueryRow
993995
found = results.Next(ctx, &viewRow)
994996
if found {
995997
docid = viewRow.Key
996998
revid = viewRow.Value.RevID.RevTreeID
999+
cv = viewRow.Value.RevID.CV()
9971000
seq = viewRow.Value.Sequence
9981001
channels = viewRow.Value.Channels
9991002
}
@@ -1002,6 +1005,7 @@ func (c *DatabaseCollection) processForEachDocIDResults(ctx context.Context, cal
10021005
if found {
10031006
docid = queryRow.Id
10041007
revid = queryRow.RevID.RevTreeID
1008+
cv = queryRow.RevID.CV()
10051009
seq = queryRow.Sequence
10061010
channels = make([]string, 0)
10071011
// Query returns all channels, but we only want to return active channels
@@ -1016,7 +1020,7 @@ func (c *DatabaseCollection) processForEachDocIDResults(ctx context.Context, cal
10161020
break
10171021
}
10181022

1019-
if ok, err := callback(IDRevAndSequence{docid, revid, seq}, channels); ok {
1023+
if ok, err := callback(IDRevAndSequence{DocID: docid, RevID: revid, Sequence: seq, CV: cv}, channels); ok {
10201024
count++
10211025
} else if err != nil {
10221026
return err

db/hybrid_logical_vector.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func ParseVersion(versionString string) (version Version, err error) {
139139
return version, nil
140140
}
141141

142-
// String returns a version/sourceID pair in CBL string format
142+
// 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.
143143
func (v Version) String() string {
144144
return strconv.FormatUint(v.Value, 16) + "@" + v.SourceID
145145
}

db/revision_cache_interface.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ func (rev *DocumentRevision) Inject1xBodyProperties(ctx context.Context, db *Dat
270270

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

294+
if showCV && rev.CV != nil {
295+
b["_cv"] = rev.CV.String()
296+
}
297+
294298
if rev.Deleted {
295299
b[BodyDeleted] = true
296300
}

rest/access_test.go

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ import (
2525
"github.com/stretchr/testify/require"
2626
)
2727

28+
type allDocsResponse struct {
29+
TotalRows int `json:"total_rows"`
30+
Offset int `json:"offset"`
31+
Rows []allDocsRow `json:"rows"`
32+
}
33+
2834
func TestPublicChanGuestAccess(t *testing.T) {
2935
rt := NewRestTester(t,
3036
&RestTesterConfig{
@@ -70,17 +76,6 @@ func TestStarAccess(t *testing.T) {
7076

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

73-
type allDocsRow struct {
74-
ID string `json:"id"`
75-
Key string `json:"key"`
76-
Value struct {
77-
Rev string `json:"rev"`
78-
Channels []string `json:"channels,omitempty"`
79-
Access map[string]base.Set `json:"access,omitempty"` // for admins only
80-
} `json:"value"`
81-
Doc db.Body `json:"doc,omitempty"`
82-
Error string `json:"error"`
83-
}
8479
var allDocsResult struct {
8580
TotalRows int `json:"total_rows"`
8681
Offset int `json:"offset"`
@@ -552,23 +547,6 @@ func TestAllDocsAccessControl(t *testing.T) {
552547
rt := NewRestTester(t, &RestTesterConfig{SyncFn: channels.DocChannelsSyncFunction})
553548
defer rt.Close()
554549

555-
type allDocsRow struct {
556-
ID string `json:"id"`
557-
Key string `json:"key"`
558-
Value struct {
559-
Rev string `json:"rev"`
560-
Channels []string `json:"channels,omitempty"`
561-
Access map[string]base.Set `json:"access,omitempty"` // for admins only
562-
} `json:"value"`
563-
Doc db.Body `json:"doc,omitempty"`
564-
Error string `json:"error"`
565-
}
566-
type allDocsResponse struct {
567-
TotalRows int `json:"total_rows"`
568-
Offset int `json:"offset"`
569-
Rows []allDocsRow `json:"rows"`
570-
}
571-
572550
// Create some docs:
573551
a := auth.NewAuthenticator(rt.MetadataStore(), nil, rt.GetDatabase().AuthenticatorOptions(rt.Context()))
574552
a.Collections = rt.GetDatabase().CollectionNames
@@ -708,13 +686,13 @@ func TestAllDocsAccessControl(t *testing.T) {
708686
assert.Equal(t, []string{"Cinemax"}, allDocsResult.Rows[0].Value.Channels)
709687
assert.Equal(t, "doc1", allDocsResult.Rows[1].Key)
710688
assert.Equal(t, "forbidden", allDocsResult.Rows[1].Error)
711-
assert.Equal(t, "", allDocsResult.Rows[1].Value.Rev)
689+
assert.Nil(t, allDocsResult.Rows[1].Value)
712690
assert.Equal(t, "doc3", allDocsResult.Rows[2].ID)
713691
assert.Equal(t, []string{"Cinemax"}, allDocsResult.Rows[2].Value.Channels)
714692
assert.Equal(t, "1-20912648f85f2bbabefb0993ddd37b41", allDocsResult.Rows[2].Value.Rev)
715693
assert.Equal(t, "b0gus", allDocsResult.Rows[3].Key)
716694
assert.Equal(t, "not_found", allDocsResult.Rows[3].Error)
717-
assert.Equal(t, "", allDocsResult.Rows[3].Value.Rev)
695+
assert.Nil(t, allDocsResult.Rows[3].Value)
718696

719697
// Check GET to _all_docs with keys parameter:
720698
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")
@@ -1178,3 +1156,43 @@ func TestPublicChannel(t *testing.T) {
11781156
response = rt.SendUserRequest("GET", "/{{.keyspace}}/privateDoc", "", "user1")
11791157
RequireStatus(t, response, 403)
11801158
}
1159+
1160+
func TestAllDocsCV(t *testing.T) {
1161+
rt := NewRestTesterPersistentConfig(t)
1162+
defer rt.Close()
1163+
1164+
const docID = "foo"
1165+
docVersion := rt.PutDocDirectly(docID, db.Body{"foo": "bar"})
1166+
1167+
testCases := []struct {
1168+
name string
1169+
url string
1170+
output string
1171+
}{
1172+
{
1173+
name: "no query string",
1174+
url: "/{{.keyspace}}/_all_docs",
1175+
output: fmt.Sprintf(`{
1176+
"total_rows": 1,
1177+
"update_seq": 1,
1178+
"rows": [{"key": "%s", "id": "%s", "value": {"rev": "%s"}}]
1179+
}`, docID, docID, docVersion.RevTreeID),
1180+
},
1181+
{
1182+
name: "cvs=true",
1183+
url: "/{{.keyspace}}/_all_docs?show_cv=true",
1184+
output: fmt.Sprintf(`{
1185+
"total_rows": 1,
1186+
"update_seq": 1,
1187+
"rows": [{"key": "%s", "id": "%s", "value": {"rev": "%s", "cv": "%s"}}]
1188+
}`, docID, docID, docVersion.RevTreeID, docVersion.CV.String()),
1189+
},
1190+
}
1191+
for _, testCase := range testCases {
1192+
t.Run(testCase.name, func(t *testing.T) {
1193+
response := rt.SendAdminRequest(http.MethodGet, testCase.url, "")
1194+
RequireStatus(t, response, http.StatusOK)
1195+
require.JSONEq(t, testCase.output, response.Body.String())
1196+
})
1197+
}
1198+
}

rest/api_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2717,7 +2717,7 @@ func TestNullDocHandlingForMutable1xBody(t *testing.T) {
27172717

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

2720-
body, err := documentRev.Mutable1xBody(ctx, collection, nil, nil, false)
2720+
body, err := documentRev.Mutable1xBody(ctx, collection, nil, nil, false, false)
27212721
require.Error(t, err)
27222722
require.Nil(t, body)
27232723
assert.Contains(t, err.Error(), "null doc body for doc")

rest/bulk_api.go

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,33 @@ import (
2525
"github.com/couchbase/sync_gateway/db"
2626
)
2727

28+
// allDocsRowValue is a struct that represents possible values returned in a document from /ks/_all_docs endpoint
29+
type allDocsRowValue struct {
30+
Rev string `json:"rev"`
31+
CV string `json:"cv,omitempty"`
32+
Channels []string `json:"channels,omitempty"`
33+
Access map[string]base.Set `json:"access,omitempty"` // for admins only
34+
}
35+
36+
// allDocsRow is a struct that represents a linefrom /ks/_all_docs endpoint
37+
type allDocsRow struct {
38+
Key string `json:"key"`
39+
ID string `json:"id,omitempty"`
40+
Value *allDocsRowValue `json:"value,omitempty"`
41+
Doc json.RawMessage `json:"doc,omitempty"`
42+
UpdateSeq uint64 `json:"update_seq,omitempty"`
43+
Error string `json:"error,omitempty"`
44+
Status int `json:"status,omitempty"`
45+
}
46+
2847
// HTTP handler for _all_docs
2948
func (h *handler) handleAllDocs() error {
3049
// http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API
3150
includeDocs := h.getBoolQuery("include_docs")
3251
includeChannels := h.getBoolQuery("channels")
3352
includeAccess := h.getBoolQuery("access") && h.user == nil
3453
includeRevs := h.getBoolQuery("revs")
54+
includeCVs := h.getBoolQuery("show_cv")
3555
includeSeqs := h.getBoolQuery("update_seq")
3656

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

102-
type allDocsRowValue struct {
103-
Rev string `json:"rev"`
104-
Channels []string `json:"channels,omitempty"`
105-
Access map[string]base.Set `json:"access,omitempty"` // for admins only
106-
}
107-
type allDocsRow struct {
108-
Key string `json:"key"`
109-
ID string `json:"id,omitempty"`
110-
Value *allDocsRowValue `json:"value,omitempty"`
111-
Doc json.RawMessage `json:"doc,omitempty"`
112-
UpdateSeq uint64 `json:"update_seq,omitempty"`
113-
Error string `json:"error,omitempty"`
114-
Status int `json:"status,omitempty"`
115-
}
116-
117122
// Subroutine that creates a response row for a document:
118123
totalRows := 0
119124
createRow := func(doc db.IDRevAndSequence, channels []string) *allDocsRow {
@@ -169,6 +174,9 @@ func (h *handler) handleAllDocs() error {
169174
if includeChannels {
170175
row.Value.Channels = channels
171176
}
177+
if includeCVs {
178+
row.Value.CV = doc.CV
179+
}
172180
return row
173181
}
174182

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

365374
includeAttachments := h.getBoolQuery("attachments")
366375
showExp := h.getBoolQuery("show_exp")
376+
showCV := h.getBoolQuery("show_cv")
367377

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

442452
if err == nil {
443-
body, err = h.collection.Get1xRevBodyWithHistory(h.ctx(), docid, revid, docRevsLimit, revsFrom, attsSince, showExp)
453+
body, err = h.collection.Get1xRevBodyWithHistory(h.ctx(), docid, revid, db.Get1xRevBodyOptions{
454+
MaxHistory: docRevsLimit,
455+
HistoryFrom: revsFrom,
456+
AttachmentsSince: attsSince,
457+
ShowExp: showExp,
458+
ShowCV: showCV})
444459
}
445460

446461
if err != nil {

0 commit comments

Comments
 (0)