Skip to content

Commit 45d420c

Browse files
committed
Merge branch 'CBG-4411' into CBG-4412
2 parents 362cc0b + 68a452c commit 45d420c

File tree

5 files changed

+156
-117
lines changed

5 files changed

+156
-117
lines changed

base/error.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,6 @@ var (
8383

8484
// ErrInvalidJSON is returned when the JSON being unmarshalled cannot be parsed.
8585
ErrInvalidJSON = HTTPErrorf(http.StatusBadRequest, "Invalid JSON")
86-
87-
// ErrSyncFnDryRun is returned when the Sync Function Dry Run returns an error while computing the access
88-
ErrSyncFnDryRun = &sgError{"Error returned from Sync Function:"}
89-
90-
// ErrImportDryRun is returned when the Import Filter Dry Run returns an error when trying to import a document
91-
ErrImportDryRun = &sgError{"Error occured during import dry run: "}
9286
)
9387

9488
func (e *sgError) Error() string {
@@ -367,3 +361,28 @@ func (me *MultiError) ErrorOrNil() error {
367361
}
368362
return me
369363
}
364+
365+
const syncFnDryRunErrorPrefix = "Error returned from Sync Function"
366+
367+
// SyncFnDryRunError is returned when the sync function dry run returns an error.
368+
// It wraps the original error for errors.Is and the type supports errors.As
369+
type SyncFnDryRunError struct {
370+
Err error
371+
}
372+
373+
func (e *SyncFnDryRunError) Error() string {
374+
if e == nil {
375+
return syncFnDryRunErrorPrefix
376+
}
377+
if e.Err == nil {
378+
return syncFnDryRunErrorPrefix
379+
}
380+
return syncFnDryRunErrorPrefix + ": " + e.Err.Error()
381+
}
382+
383+
func (e *SyncFnDryRunError) Unwrap() error {
384+
if e == nil {
385+
return nil
386+
}
387+
return e.Err
388+
}

db/crud.go

Lines changed: 7 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,58 +1694,11 @@ func (db *DatabaseCollectionWithUser) PutExistingRevWithBody(ctx context.Context
16941694

16951695
}
16961696

1697-
// 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.
1698-
// 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.
1699-
// 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, oldDoc *Document, body Body, docID, syncFn string) (*channels.ChannelMapperOutput, error) {
1697+
// SyncFnDryrun Runs the given document body through a sync function and returns expiry, channels doc was placed in,
1698+
// access map for users, roles, handler errors and sync fn exceptions.
1699+
// If syncFn is provided, it will be used instead of the one configured on the database.
1700+
func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, newDoc, oldDoc *Document, syncFn string) (*channels.ChannelMapperOutput, error) {
17011701

1702-
delete(body, BodyId)
1703-
1704-
// Get the revision ID to match, and the new generation number:
1705-
matchRev, _ := body[BodyRev].(string)
1706-
generation, _ := ParseRevID(ctx, matchRev)
1707-
if generation < 0 {
1708-
return nil, base.HTTPErrorf(http.StatusBadRequest, "Invalid revision ID")
1709-
}
1710-
generation++
1711-
1712-
// Create newDoc which will be used to pass around Body
1713-
newDoc := &Document{
1714-
ID: docID,
1715-
}
1716-
// Pull out attachments
1717-
newDoc.SetAttachments(GetBodyAttachments(body))
1718-
delete(body, BodyAttachments)
1719-
1720-
delete(body, BodyRevisions)
1721-
1722-
err := validateAPIDocUpdate(body)
1723-
if err != nil {
1724-
return nil, err
1725-
}
1726-
bodyWithoutInternalProps, wasStripped := StripInternalProperties(body)
1727-
canonicalBytesForRevID, err := base.JSONMarshalCanonical(bodyWithoutInternalProps)
1728-
if err != nil {
1729-
return nil, err
1730-
}
1731-
1732-
// We needed to keep _deleted around in the body until we generated a rev ID, but now we can ditch it.
1733-
_, isDeleted := body[BodyDeleted]
1734-
if isDeleted {
1735-
delete(body, BodyDeleted)
1736-
}
1737-
1738-
// and now we can finally update the newDoc body to be without any special properties
1739-
newDoc.UpdateBody(body)
1740-
1741-
// If no special properties were stripped and document wasn't deleted, the canonical bytes represent the current
1742-
// body. In this scenario, store canonical bytes as newDoc._rawBody
1743-
if !wasStripped && !isDeleted {
1744-
newDoc._rawBody = canonicalBytesForRevID
1745-
}
1746-
1747-
newRev := CreateRevIDWithBytes(generation, matchRev, canonicalBytesForRevID)
1748-
newDoc.RevID = newRev
17491702
mutableBody, metaMap, _, err := db.prepareSyncFn(oldDoc, newDoc)
17501703
if err != nil {
17511704
base.InfofCtx(ctx, base.KeyDiagnostic, "Failed to prepare to run sync function: %v", err)
@@ -1761,7 +1714,7 @@ func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, oldDoc *
17611714
if syncFn == "" {
17621715
output, err = db.ChannelMapper.MapToChannelsAndAccess(ctx, mutableBody, string(oldDoc._rawBody), metaMap, syncOptions)
17631716
if err != nil {
1764-
syncErr = fmt.Errorf("%s%s", base.ErrSyncFnDryRun, err)
1717+
return nil, &base.SyncFnDryRunError{Err: err}
17651718
}
17661719
} else {
17671720
jsTimeout := time.Duration(base.DefaultJavascriptTimeoutSecs) * time.Second
@@ -1771,7 +1724,8 @@ func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, oldDoc *
17711724
}
17721725
jsOutput, err := syncRunner.Call(ctx, mutableBody, string(oldDoc._rawBody), metaMap, syncOptions)
17731726
if err != nil {
1774-
syncErr = fmt.Errorf("%s%s", base.ErrSyncFnDryRun, err)
1727+
1728+
return nil, fmt.Errorf("failed to create sync runner: %v", err)
17751729
}
17761730
output = jsOutput.(*channels.ChannelMapperOutput)
17771731
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# the file licenses/APL2.txt.
88
parameters:
99
- $ref: ../../components/parameters.yaml#/keyspace
10+
- $ref: ../../components/parameters.yaml#/doc_id
1011
post:
1112
summary: Run a doc body through the sync function and return sync data.
1213
description: |-

rest/diagnostic_doc_api.go

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ licenses/APL2.txt.
1111
package rest
1212

1313
import (
14-
"mime"
14+
"errors"
15+
"fmt"
1516
"net/http"
16-
"strings"
1717

1818
"github.com/couchbase/sync_gateway/auth"
1919
"github.com/couchbase/sync_gateway/base"
@@ -80,20 +80,19 @@ func (h *handler) handleGetDocChannels() error {
8080
// If docid is specified and the document does not exist in the bucket, it will return error
8181
func (h *handler) handleSyncFnDryRun() error {
8282
docid := h.getQuery("doc_id")
83-
contentType, _, _ := mime.ParseMediaType(h.rq.Header.Get("Content-Type"))
84-
85-
if contentType != "application/json" && contentType != "" {
86-
return base.HTTPErrorf(http.StatusUnsupportedMediaType, "Invalid Content-Type header: %s. Needs to be empty or application/json", contentType)
87-
}
8883

8984
var syncDryRunPayload SyncFnDryRunPayload
9085
err := h.readJSONInto(&syncDryRunPayload)
9186
// Only require a valid JSON payload if docid is not provided.
9287
// If docid is provided, the sync function will use the document from the bucket, and the payload is optional.
93-
if err != nil && docid == "" {
88+
if err != nil {
9489
return base.HTTPErrorf(http.StatusBadRequest, "Error reading sync function payload: %v", err)
9590
}
9691

92+
if syncDryRunPayload.Doc == nil && docid == "" {
93+
return base.HTTPErrorf(http.StatusBadRequest, "no docid or document provided")
94+
}
95+
9796
oldDoc := &db.Document{ID: docid}
9897
oldDoc.UpdateBody(syncDryRunPayload.Doc)
9998
if docid != "" {
@@ -110,17 +109,64 @@ func (h *handler) handleSyncFnDryRun() error {
110109
oldDoc.UpdateBody(nil)
111110
}
112111

113-
output, err := h.collection.SyncFnDryrun(h.ctx(), oldDoc, syncDryRunPayload.Doc, docid, syncDryRunPayload.Function)
112+
delete(syncDryRunPayload.Doc, db.BodyId)
113+
114+
// Get the revision ID to match, and the new generation number:
115+
matchRev, _ := syncDryRunPayload.Doc[db.BodyRev].(string)
116+
generation, _ := db.ParseRevID(h.ctx(), matchRev)
117+
if generation < 0 {
118+
return base.HTTPErrorf(http.StatusBadRequest, "Invalid revision ID")
119+
}
120+
generation++
121+
122+
// Create newDoc which will be used to pass around Body
123+
newDoc := &db.Document{
124+
ID: docid,
125+
}
126+
// Pull attachments
127+
newDoc.SetAttachments(db.GetBodyAttachments(syncDryRunPayload.Doc))
128+
delete(syncDryRunPayload.Doc, db.BodyAttachments)
129+
delete(syncDryRunPayload.Doc, db.BodyRevisions)
130+
131+
if _, ok := syncDryRunPayload.Doc[base.SyncPropertyName]; ok {
132+
return base.HTTPErrorf(http.StatusBadRequest, "document-top level property '_sync' is a reserved internal property")
133+
}
134+
135+
db.StripInternalProperties(syncDryRunPayload.Doc)
136+
137+
// We needed to keep _deleted around in the body until we generate rev ID, but now it can be removed
138+
_, isDeleted := syncDryRunPayload.Doc[db.BodyDeleted]
139+
if isDeleted {
140+
delete(syncDryRunPayload.Doc, db.BodyDeleted)
141+
}
142+
143+
//update the newDoc body to be without any special properties
144+
newDoc.UpdateBody(syncDryRunPayload.Doc)
145+
146+
rawDocBytes, err := newDoc.BodyBytes(h.ctx())
114147
if err != nil {
115-
if strings.Contains(err.Error(), base.ErrSyncFnDryRun.Error()) {
116-
errMsg := strings.ReplaceAll(err.Error(), base.ErrSyncFnDryRun.Error(), "")
117-
resp := SyncFnDryRun{
118-
Exception: errMsg,
119-
}
120-
h.writeJSON(resp)
121-
return nil
148+
return base.HTTPErrorf(http.StatusBadRequest, "Error marshalling document: %v", err)
149+
}
150+
151+
newRev := db.CreateRevIDWithBytes(generation, matchRev, rawDocBytes)
152+
newDoc.RevID = newRev
153+
154+
output, err := h.collection.SyncFnDryrun(h.ctx(), newDoc, oldDoc, syncDryRunPayload.Function)
155+
if err != nil {
156+
var syncFnDryRunErr *base.SyncFnDryRunError
157+
if !errors.As(err, &syncFnDryRunErr) {
158+
return err
122159
}
123-
return err
160+
161+
errMsg := syncFnDryRunErr.Error()
162+
if syncFnDryRunErr.Unwrap() != nil {
163+
errMsg = syncFnDryRunErr.Err.Error()
164+
}
165+
resp := SyncFnDryRun{
166+
Exception: errMsg,
167+
}
168+
h.writeJSON(resp)
169+
return nil
124170
}
125171
errorMsg := ""
126172
if output.Rejection != nil {

0 commit comments

Comments
 (0)