Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
25 changes: 25 additions & 0 deletions base/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,28 @@ func (e *SyncFnDryRunError) Unwrap() error {
}
return e.Err
}

const importFilterErrorPrefix = "Error returned from Import Filter"

// ImportFilterError is returned when the import filter dry run returns an error.
// It wraps the original error for errors.Is and the type supports errors.As
type ImportFilterDryRunError struct {
Err error
}

func (e *ImportFilterDryRunError) Error() string {
if e == nil {
return importFilterErrorPrefix
}
if e.Err == nil {
return importFilterErrorPrefix
}
return importFilterErrorPrefix + ": " + e.Err.Error()
}

func (e *ImportFilterDryRunError) Unwrap() error {
if e == nil {
return nil
}
return e.Err
}
47 changes: 35 additions & 12 deletions db/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,20 +545,43 @@ func (i *ImportFilterFunction) EvaluateFunction(ctx context.Context, doc Body, d
return false, errors.New("Import filter function returned non-boolean value.")
}
}
func (db *DatabaseCollectionWithUser) ImportFilterDryRun(ctx context.Context, doc Body, docid string) (bool, error) {

importFilter := db.importFilter()
if docid != "" {
docInBucket, err := db.GetDocument(ctx, docid, DocUnmarshalAll)
if err == nil {
if doc == nil {
doc = docInBucket.Body(ctx)

// ImportFilterDryRun Runs a document through the import filter and returns a boolean and error
func (db *DatabaseCollectionWithUser) ImportFilterDryRun(ctx context.Context, doc Body, importFn string) (bool, error) {

var shouldImport bool
if importFn == "" {
importFilter := db.importFilter()
if importFilter == nil {
return false, base.HTTPErrorf(http.StatusBadRequest, "No import filter specified")
}
output, err := importFilter.EvaluateFunction(ctx, doc, true)
if err != nil {
return false, &base.ImportFilterDryRunError{Err: err}
}
shouldImport = output
Copy link
Member

Choose a reason for hiding this comment

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

If we return output here, we can avoid the nested } else { block below since we're returning early.

This is more idiomatic Go than writing if else chains and deep nesting.

} else {
jsTimeout := time.Duration(base.DefaultJavascriptTimeoutSecs) * time.Second
importRunner, err := newImportFilterRunner(ctx, importFn, jsTimeout)
if err != nil {
return false, errors.New("failed to create import filter runner: " + err.Error())
}
importOuput, err := importRunner.Call(ctx, doc)
switch result := importOuput.(type) {
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

Corrected spelling of 'importOuput' to 'importOutput'.

Suggested change
importOuput, err := importRunner.Call(ctx, doc)
switch result := importOuput.(type) {
importOutput, err := importRunner.Call(ctx, doc)
switch result := importOutput.(type) {

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

👍

case bool:
shouldImport = result
break
case string:
boolResult, err := strconv.ParseBool(result)
if err != nil {
return false, err
}
} else {
return false, err
shouldImport = boolResult
break
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The break statements are unnecessary in a switch statement in Go. The break is implicit at the end of each case unless a fallthrough is explicitly used.

Suggested change
break
case string:
boolResult, err := strconv.ParseBool(result)
if err != nil {
return false, err
}
} else {
return false, err
shouldImport = boolResult
break
case string:
boolResult, err := strconv.ParseBool(result)
if err != nil {
return false, err
}
shouldImport = boolResult

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

👍

Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The break statements are unnecessary in a switch statement in Go. The break is implicit at the end of each case unless a fallthrough is explicitly used.

Suggested change
break
case string:
boolResult, err := strconv.ParseBool(result)
if err != nil {
return false, err
}
} else {
return false, err
shouldImport = boolResult
break
case string:
boolResult, err := strconv.ParseBool(result)
if err != nil {
return false, err
}
shouldImport = boolResult

Copilot uses AI. Check for mistakes.
default:
return false, &base.ImportFilterDryRunError{Err: err}
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

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

The error err may be nil at this point since it's only assigned in the string case. This would result in wrapping a nil error. Consider returning an explicit error message indicating that the import filter returned a non-boolean value.

Suggested change
return false, &base.ImportFilterDryRunError{Err: err}
wrappedErr := err
if wrappedErr == nil {
wrappedErr = errors.New("import filter function returned non-boolean value")
}
return false, &base.ImportFilterDryRunError{Err: wrappedErr}

Copilot uses AI. Check for mistakes.
}
}
shouldImport, err := importFilter.EvaluateFunction(ctx, doc, true)

return shouldImport, err
return shouldImport, nil
}
21 changes: 18 additions & 3 deletions docs/api/paths/diagnostic/keyspace-import_filter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,31 @@
# the file licenses/APL2.txt.
parameters:
- $ref: ../../components/parameters.yaml#/keyspace
get:
- $ref: ../../components/parameters.yaml#/doc_id
post:
summary: Run a doc body through the Import filter and return results.
description: |-
Run a document body through the import filter and return whether its imported or not, and any error messages.
Runs a document body through the import filter and return whether its
imported or not, and any error messages. If no custom import filter is
provided in the request body, the default or user-defined import filter
is used.

* Sync Gateway Application Read Only
requestBody:
content:
application/json:
schema:
$ref: ../../components/schemas.yaml#/Document
type: object
properties:
import_filter:
description: |-
A JavaScript function that all imported documents in the
default scope and collection are ran through in order to filter
out what to import and what not to import.
type: string
example: 'function(doc) { if (doc.type != ''mobile'') { return false; } return true; }'
doc:
$ref: ../../components/schemas.yaml#/Document
responses:
'200':
description: Document Processed by import filter successfully
Expand Down
33 changes: 26 additions & 7 deletions rest/diagnostic_doc_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ package rest

import (
"errors"
"fmt"
"net/http"

"github.com/couchbase/sync_gateway/auth"
Expand All @@ -39,6 +38,11 @@ type SyncFnDryRunPayload struct {
Doc db.Body `json:"doc,omitempty"`
}

type ImportFilterDryRunPayload struct {
Function string `json:"import_filter"`
Doc db.Body `json:"doc,omitempty"`
}

func populateDocChannelInfo(doc db.Document) map[string][]auth.GrantHistorySequencePair {
resp := make(map[string][]auth.GrantHistorySequencePair, len(doc.Channels))

Expand Down Expand Up @@ -178,19 +182,34 @@ func (h *handler) handleSyncFnDryRun() error {
func (h *handler) handleImportFilterDryRun() error {
docid := h.getQuery("doc_id")

body, err := h.readDocument()
var importFilterPayload ImportFilterDryRunPayload
err := h.readJSONInto(&importFilterPayload)
if err != nil {
if docid == "" {
return fmt.Errorf("Error reading body: %s, no doc id provided for dry run", err)
}
return base.HTTPErrorf(http.StatusBadRequest, "Error reading import filter payload: %v", err)
}

if docid != "" && body != nil {
// Cannot pass both doc_id and body in the request body
if len(importFilterPayload.Doc) > 0 && docid != "" {
return base.HTTPErrorf(http.StatusBadRequest, "doc body and doc id provided. Please provide either the body or a doc id for the import filter dry run")
}
shouldImport, err := h.collection.ImportFilterDryRun(h.ctx(), body, docid)

var doc db.Body
if docid != "" {
docInBucket, err := h.collection.GetDocument(h.ctx(), docid, db.DocUnmarshalSync)
if err != nil {
return err
}
doc = docInBucket.Body(h.ctx())
} else {
doc = importFilterPayload.Doc
}
shouldImport, err := h.collection.ImportFilterDryRun(h.ctx(), doc, importFilterPayload.Function)
errorMsg := ""
if err != nil {
var importFilterDryRunErr *base.ImportFilterDryRunError
if !errors.As(err, &importFilterDryRunErr) {
return err
}
errorMsg = err.Error()
}
resp := ImportFilterDryRun{
Expand Down
Loading