Skip to content

Commit

Permalink
Set converter defaults once in Convert method (#21)
Browse files Browse the repository at this point in the history
Co-authored-by: Erik Dubbelboer <[email protected]>
  • Loading branch information
koenbollen and erikdubbelboer authored Jun 21, 2024
1 parent 289c9a4 commit 947a271
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 8 deletions.
26 changes: 18 additions & 8 deletions filter/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"sort"
"strings"
"sync"
)

var basicOperatorMap = map[string]string{
Expand All @@ -19,11 +20,12 @@ var basicOperatorMap = map[string]string{
"$regex": "~*",
}

// DefaultPlaceholderName is the default placeholder name used in the generated SQL query.
// defaultPlaceholderName is the default placeholder name used in the generated SQL query.
// This name should not be used in the database or any JSONB column. It can be changed using
// the WithPlaceholderName option.
const DefaultPlaceholderName = "__filter_placeholder"
const defaultPlaceholderName = "__filter_placeholder"

// Converter converts MongoDB filter queries to SQL conditions and values. Use [filter.NewConverter] to create a new instance.
type Converter struct {
nestedColumn string
nestedExemptions []string
Expand All @@ -33,23 +35,22 @@ type Converter struct {
}
emptyCondition string
placeholderName string

once sync.Once
}

// NewConverter creates a new Converter with optional nested JSONB field mapping.
// NewConverter creates a new [Converter] with optional nested JSONB field mapping.
//
// Note: When using github.com/lib/pq, the filter.WithArrayDriver should be set to pq.Array.
// Note: When using https://github.com/lib/pq, the [filter.WithArrayDriver] should be set to pq.Array.
func NewConverter(options ...Option) *Converter {
converter := &Converter{
emptyCondition: "FALSE",
// don't set defaults, use the once.Do in #Convert()
}
for _, option := range options {
if option != nil {
option(converter)
}
}
if converter.placeholderName == "" {
converter.placeholderName = DefaultPlaceholderName
}
return converter
}

Expand All @@ -58,6 +59,15 @@ func NewConverter(options ...Option) *Converter {
// startAtParameterIndex is the index to start the parameter numbering at.
// Passing X will make the first indexed parameter $X, the second $X+1, and so on.
func (c *Converter) Convert(query []byte, startAtParameterIndex int) (conditions string, values []any, err error) {
c.once.Do(func() {
if c.emptyCondition == "" {
c.emptyCondition = "FALSE"
}
if c.placeholderName == "" {
c.placeholderName = defaultPlaceholderName
}
})

if startAtParameterIndex < 1 {
return "", nil, fmt.Errorf("startAtParameterIndex must be greater than 0")
}
Expand Down
58 changes: 58 additions & 0 deletions filter/converter_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
package filter_test

import (
"database/sql"
"fmt"
"reflect"
"testing"

"github.com/poki/mongodb-filter-to-postgres/filter"
)

func ExampleNewConverter() {
// Remeber to use `filter.WithArrayDriver(pg.Array)` when using github.com/lib/pq
converter := filter.NewConverter(filter.WithNestedJSONB("meta", "created_at", "updated_at"))

mongoFilterQuery := `{
"name": "John",
"created_at": {
"$gte": "2020-01-01T00:00:00Z"
}
}`
conditions, values, err := converter.Convert([]byte(mongoFilterQuery), 1)
if err != nil {
// handle error
}

var db *sql.DB // setup your database connection

_, _ = db.Query("SELECT * FROM users WHERE "+conditions, values)
// SELECT * FROM users WHERE (("created_at" >= $1) AND ("meta"->>'name' = $2)), 2020-01-01T00:00:00Z, "John"
}

func TestConverter_Convert(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -402,3 +424,39 @@ func TestConverter_WithEmptyCondition(t *testing.T) {
t.Errorf("Converter.Convert() values = %v, want nil", values)
}
}

func TestConverter_NoConstructor(t *testing.T) {
c := &filter.Converter{}
conditions, values, err := c.Convert([]byte(`{"name": "John"}`), 1)
if err != nil {
t.Fatal(err)
}
if want := `("name" = $1)`; conditions != want {
t.Errorf("Converter.Convert() conditions = %v, want %v", conditions, want)
}
if !reflect.DeepEqual(values, []any{"John"}) {
t.Errorf("Converter.Convert() values = %v, want %v", values, []any{"John"})
}

conditions, values, err = c.Convert([]byte(``), 1)
if err != nil {
t.Fatal(err)
}
if want := "FALSE"; conditions != want {
t.Errorf("Converter.Convert() conditions = %v, want %v", conditions, want)
}
if len(values) != 0 {
t.Errorf("Converter.Convert() values = %v, want nil", values)
}
}

func TestConverter_CopyReference(t *testing.T) {
c := filter.Converter{}
conditions, _, err := c.Convert([]byte(``), 1)
if err != nil {
t.Fatal(err)
}
if want := "FALSE"; conditions != want {
t.Errorf("Converter.Convert() conditions = %v, want %v", conditions, want)
}
}
5 changes: 5 additions & 0 deletions filter/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// This package converts MongoDB query filters into PostgreSQL WHERE clauses.
// It's designed to be simple, secure, and free of dependencies.
//
// See: https://www.mongodb.com/docs/compass/current/query/filter
package filter

0 comments on commit 947a271

Please sign in to comment.