Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set converter defaults once in Convert method #21

Merged
merged 2 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading