Skip to content

Commit 3b21b1b

Browse files
committed
merge CBG-4411
1 parent d5f6e1f commit 3b21b1b

21 files changed

+257
-135
lines changed

base/error.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ var (
8484
// ErrInvalidJSON is returned when the JSON being unmarshalled cannot be parsed.
8585
ErrInvalidJSON = HTTPErrorf(http.StatusBadRequest, "Invalid JSON")
8686

87+
ErrSyncFnDryRun = &sgError{"Error returned from Sync Function:"}
8788
ErrImportDryRun = &sgError{"Error occured during import dry run: "}
8889
)
8990

db/blip.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ package db
1212

1313
import (
1414
"context"
15+
"fmt"
1516
"regexp"
1617
"strings"
1718

@@ -84,11 +85,13 @@ func defaultBlipLogger(ctx context.Context) blip.LogFn {
8485
}
8586

8687
// blipRevMessageProperties returns a set of BLIP message properties for the given parameters.
87-
func blipRevMessageProperties(revisionHistory []string, deleted bool, seq SequenceID, replacedRevID string, revTreeProperty []string) blip.Properties {
88+
func blipRevMessageProperties(revisionHistory []string, deleted bool, seq SequenceID, replacedRevID string, revTreeProperty []string) (blip.Properties, error) {
8889
properties := make(blip.Properties)
8990

90-
// TODO: Assert? db.SequenceID.MarshalJSON can never error
91-
seqJSON, _ := base.JSONMarshal(seq)
91+
seqJSON, err := base.JSONMarshal(seq)
92+
if err != nil {
93+
return nil, fmt.Errorf("could not marshal sequence %v: %v", seq, err)
94+
}
9295
properties[RevMessageSequence] = string(seqJSON)
9396

9497
if len(revisionHistory) > 0 {
@@ -107,7 +110,7 @@ func blipRevMessageProperties(revisionHistory []string, deleted bool, seq Sequen
107110
properties[RevMessageReplacedRev] = replacedRevID
108111
}
109112

110-
return properties
113+
return properties, nil
111114
}
112115

113116
// Returns true if this attachment is worth trying to compress.

db/blip_handler.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,10 @@ func (bsc *BlipSyncContext) sendRevAsDelta(ctx context.Context, sender *blip.Sen
937937
revTreeProperty = append(revTreeProperty, toHistory(redactedRev.History, knownRevs, maxHistory)...)
938938
}
939939

940-
properties := blipRevMessageProperties(history, redactedRev.Deleted, seq, "", revTreeProperty)
940+
properties, err := blipRevMessageProperties(history, redactedRev.Deleted, seq, "", revTreeProperty)
941+
if err != nil {
942+
return err
943+
}
941944
return bsc.sendRevisionWithProperties(ctx, sender, docID, revID, collectionIdx, redactedRev.BodyBytes, nil, properties, seq, nil)
942945
}
943946

db/blip_sync_context.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,10 @@ func (bsc *BlipSyncContext) sendDelta(ctx context.Context, sender *blip.Sender,
597597
revTreeProperty = append(revTreeProperty, revDelta.ToRevID)
598598
revTreeProperty = append(revTreeProperty, revDelta.RevisionHistory...)
599599
}
600-
properties := blipRevMessageProperties(history, revDelta.ToDeleted, seq, "", revTreeProperty)
600+
properties, err := blipRevMessageProperties(history, revDelta.ToDeleted, seq, "", revTreeProperty)
601+
if err != nil {
602+
return err
603+
}
601604
properties[RevMessageDeltaSrc] = deltaSrcRevID
602605

603606
if bsc.useHLV() {
@@ -629,7 +632,10 @@ func (bsc *BlipSyncContext) sendNoRev(sender *blip.Sender, docID, revID string,
629632
if bsc.activeCBMobileSubprotocol <= CBMobileReplicationV2 && bsc.clientType == BLIPClientTypeSGR2 {
630633
noRevRq.SetSeq(seq)
631634
} else {
632-
noRevRq.SetSequence(seq)
635+
err := noRevRq.SetSequence(seq)
636+
if err != nil {
637+
return err
638+
}
633639
}
634640

635641
status, reason := base.ErrorAsHTTPStatus(err)
@@ -785,7 +791,10 @@ func (bsc *BlipSyncContext) sendRevision(ctx context.Context, sender *blip.Sende
785791
revTreeHistoryProperty = append(revTreeHistoryProperty, toHistory(docRev.History, knownRevs, maxHistory)...)
786792
}
787793

788-
properties := blipRevMessageProperties(history, docRev.Deleted, seq, replacedRevID, revTreeHistoryProperty)
794+
properties, err := blipRevMessageProperties(history, docRev.Deleted, seq, replacedRevID, revTreeHistoryProperty)
795+
if err != nil {
796+
return err
797+
}
789798
if base.LogDebugEnabled(ctx, base.KeySync) {
790799
replacedRevMsg := ""
791800
if replacedRevID != "" {

db/blip_sync_messages.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,13 @@ func (nrm *noRevMessage) SetSeq(seq SequenceID) {
508508
nrm.Properties[NorevMessageSeq] = seq.String()
509509
}
510510

511-
func (nrm *noRevMessage) SetSequence(sequence SequenceID) {
512-
nrm.Properties[NorevMessageSequence] = sequence.String()
511+
func (nrm *noRevMessage) SetSequence(sequence SequenceID) error {
512+
s, err := base.JSONMarshal(sequence)
513+
if err != nil {
514+
return err
515+
}
516+
nrm.Properties[NorevMessageSequence] = string(s)
517+
return nil
513518
}
514519

515520
func (nrm *noRevMessage) SetReason(reason string) {

db/blip_sync_messages_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2025-Present Couchbase, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License included
4+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
5+
// in that file, in accordance with the Business Source License, use of this
6+
// software will be governed by the Apache License, Version 2.0, included in
7+
// the file licenses/APL2.txt.
8+
9+
package db
10+
11+
import (
12+
"fmt"
13+
"testing"
14+
15+
"github.com/couchbase/sync_gateway/base"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func TestBlipSequenceProperty(t *testing.T) {
20+
testCases := []struct {
21+
name string
22+
seq SequenceID
23+
expectedString string
24+
expectedISGRV2String string
25+
}{
26+
{
27+
name: "Simple SequenceID",
28+
seq: SequenceID{Seq: 1},
29+
expectedString: `1`,
30+
expectedISGRV2String: `1`,
31+
},
32+
{
33+
name: "compound sequenceID",
34+
seq: SequenceID{LowSeq: 5, TriggeredBy: 10, Seq: 15},
35+
expectedString: `"5:10:15"`,
36+
expectedISGRV2String: `5:10:15`,
37+
},
38+
{
39+
name: "TriggeredBy only",
40+
seq: SequenceID{TriggeredBy: 20, Seq: 25},
41+
expectedString: `"20:25"`,
42+
expectedISGRV2String: `20:25`,
43+
},
44+
{
45+
name: "LowSeq and Seq",
46+
seq: SequenceID{LowSeq: 30, Seq: 35},
47+
expectedString: `"30::35"`,
48+
expectedISGRV2String: `30::35`,
49+
},
50+
}
51+
for _, tc := range testCases {
52+
t.Run(tc.name, func(t *testing.T) {
53+
// test norev sequence property
54+
norevMessage := NewNoRevMessage()
55+
require.NoError(t, norevMessage.SetSequence(tc.seq))
56+
require.Equal(t, tc.expectedString, norevMessage.Properties[NorevMessageSequence])
57+
// this string is only used by ISGR when CB_Mobile < 3
58+
norevMessage.SetSeq(tc.seq)
59+
require.Equal(t, tc.expectedISGRV2String, norevMessage.Properties[NorevMessageSeq])
60+
61+
// test rev sequence property
62+
properties, err := blipRevMessageProperties(nil, false, tc.seq, "", nil)
63+
require.NoError(t, err)
64+
require.Equal(t, tc.expectedString, properties[RevMessageSequence])
65+
changeEntry := &ChangeEntry{
66+
Seq: tc.seq,
67+
ID: "docID",
68+
}
69+
// changes message format
70+
ctx := base.TestCtx(t)
71+
bh := &blipHandler{
72+
loggingCtx: ctx,
73+
BlipSyncContext: &BlipSyncContext{},
74+
}
75+
changeRow := bh.buildChangesRow(changeEntry, ChangeByVersionType{"rev": "1-abc"})
76+
rowString, err := base.JSONMarshal(changeRow)
77+
require.NoError(t, err)
78+
require.Equal(t, fmt.Sprintf(`[%s,"docID","1-abc"]`, tc.expectedString), string(rowString))
79+
})
80+
}
81+
82+
}

db/crud.go

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,36 +1697,15 @@ func (db *DatabaseCollectionWithUser) PutExistingRevWithBody(ctx context.Context
16971697
// SyncFnDryrun Runs a document through the sync function and returns expiry, channels doc was placed in, access map for users, roles, handler errors and sync fn exceptions.
16981698
// If a docID is a non empty string, the document will be fetched from the bucket, otherwise the body will be used. If both are specified, this function returns an error.
16991699
// The first error return value represents an error that occurs before the sync function is run. The second error return value represents an exception from the sync function.
1700-
func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, body Body, docID, syncFn string) (*channels.ChannelMapperOutput, error, error) {
1701-
doc := &Document{
1702-
ID: docID,
1703-
_body: body,
1704-
}
1705-
oldDoc := doc
1706-
if docID != "" {
1707-
if docInBucket, err := db.GetDocument(ctx, docID, DocUnmarshalAll); err == nil {
1708-
oldDoc = docInBucket
1709-
if len(doc._body) == 0 {
1710-
body = oldDoc.Body(ctx)
1711-
doc._body = body
1712-
// If no body is given, use doc in bucket as doc with no old doc
1713-
oldDoc._body = nil
1714-
}
1715-
doc._body[BodyRev] = oldDoc.SyncData.GetRevTreeID()
1716-
} else {
1717-
return nil, err, nil
1718-
}
1719-
} else {
1720-
oldDoc._body = nil
1721-
}
1700+
func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, oldDoc *Document, body Body, docID, syncFn string) (*channels.ChannelMapperOutput, error) {
17221701

17231702
delete(body, BodyId)
17241703

17251704
// Get the revision ID to match, and the new generation number:
17261705
matchRev, _ := body[BodyRev].(string)
17271706
generation, _ := ParseRevID(ctx, matchRev)
17281707
if generation < 0 {
1729-
return nil, base.HTTPErrorf(http.StatusBadRequest, "Invalid revision ID"), nil
1708+
return nil, base.HTTPErrorf(http.StatusBadRequest, "Invalid revision ID")
17301709
}
17311710
generation++
17321711

@@ -1742,12 +1721,12 @@ func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, body Bod
17421721

17431722
err := validateAPIDocUpdate(body)
17441723
if err != nil {
1745-
return nil, err, nil
1724+
return nil, err
17461725
}
17471726
bodyWithoutInternalProps, wasStripped := StripInternalProperties(body)
17481727
canonicalBytesForRevID, err := base.JSONMarshalCanonical(bodyWithoutInternalProps)
17491728
if err != nil {
1750-
return nil, err, nil
1729+
return nil, err
17511730
}
17521731

17531732
// We needed to keep _deleted around in the body until we generated a rev ID, but now we can ditch it.
@@ -1770,30 +1749,34 @@ func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, body Bod
17701749
mutableBody, metaMap, _, err := db.prepareSyncFn(oldDoc, newDoc)
17711750
if err != nil {
17721751
base.InfofCtx(ctx, base.KeyDiagnostic, "Failed to prepare to run sync function: %v", err)
1773-
return nil, err, nil
1752+
return nil, err
17741753
}
17751754

17761755
syncOptions, err := MakeUserCtx(db.user, db.ScopeName, db.Name)
17771756
if err != nil {
1778-
return nil, err, nil
1757+
return nil, err
17791758
}
17801759
var output *channels.ChannelMapperOutput
1760+
var syncErr error
17811761
if syncFn == "" {
17821762
output, err = db.ChannelMapper.MapToChannelsAndAccess(ctx, mutableBody, string(oldDoc._rawBody), metaMap, syncOptions)
1763+
if err != nil {
1764+
syncErr = fmt.Errorf("%s%s", base.ErrSyncFnDryRun, err)
1765+
}
17831766
} else {
17841767
jsTimeout := time.Duration(base.DefaultJavascriptTimeoutSecs) * time.Second
17851768
syncRunner, err := channels.NewSyncRunner(ctx, syncFn, jsTimeout)
17861769
if err != nil {
1787-
return nil, fmt.Errorf("failed to create sync runner: %v", err), nil
1770+
return nil, fmt.Errorf("failed to create sync runner: %v", err)
17881771
}
17891772
jsOutput, err := syncRunner.Call(ctx, mutableBody, string(oldDoc._rawBody), metaMap, syncOptions)
17901773
if err != nil {
1791-
return nil, err, nil
1774+
syncErr = fmt.Errorf("%s%s", base.ErrSyncFnDryRun, err)
17921775
}
17931776
output = jsOutput.(*channels.ChannelMapperOutput)
17941777
}
17951778

1796-
return output, nil, err
1779+
return output, syncErr
17971780
}
17981781

17991782
// revTreeConflictCheck checks for conflicts in the rev tree history and returns the parent revid, currentRevIndex

docs/api/paths/diagnostic/keyspace-sync.yaml

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,39 @@
77
# the file licenses/APL2.txt.
88
parameters:
99
- $ref: ../../components/parameters.yaml#/keyspace
10-
get:
10+
post:
1111
summary: Run a doc body through the sync function and return sync data.
1212
description: |-
13-
Run a document body through the sync function and return document sync data.
13+
Runs a document body through the sync function and returns document sync
14+
data. If no custom sync function is provided in the request body, the
15+
default or user-defined sync function for the collection is used.
16+
| Document | DocID | Behaviour |
17+
| -------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
18+
| Yes | No | The document passed will be considered as newDoc and oldDoc will be empty |
19+
| Yes | Yes | The document passed in the body will be newDoc and DocID will be read from the bucket/collection and will be passed as the oldDoc. If DocID doesn't exist, then oldDoc will be empty |
20+
| No | No | Will throw an error |
21+
| No | Yes | The docID will be passed in as the newDoc and oldDoc will be empty. If the document is not found, an error will be returned |
22+
1423
* Sync Gateway Application Read Only
1524
requestBody:
1625
content:
1726
application/json:
1827
schema:
19-
$ref: ../../components/schemas.yaml#/Document
28+
type: object
29+
properties:
30+
sync_function:
31+
description: |-
32+
A JavaScript function that defines custom access, channel, and
33+
validation logic for documents. This function will be evaluated
34+
by the Sync Gateway to determine document routing, access
35+
grants, and validation outcomes during synchronization.
36+
type: string
37+
example: |-
38+
function (doc, oldDoc) {
39+
channel(doc.channels);
40+
}
41+
doc:
42+
$ref: ../../components/schemas.yaml#/Document
2043
responses:
2144
'200':
2245
description: Document Processed by sync function successfully

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ require (
3737
golang.org/x/crypto v0.45.0
3838
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476
3939
golang.org/x/net v0.47.0
40-
golang.org/x/oauth2 v0.33.0
40+
golang.org/x/oauth2 v0.34.0
4141
gopkg.in/natefinch/lumberjack.v2 v2.2.1
4242
)
4343

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
270270
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
271271
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
272272
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
273-
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
274-
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
273+
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
274+
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
275275
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
276276
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
277277
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

0 commit comments

Comments
 (0)