diff --git a/filter/converter.go b/filter/converter.go index d18d60d..8f3bec6 100644 --- a/filter/converter.go +++ b/filter/converter.go @@ -7,6 +7,7 @@ import ( "fmt" "sort" "strings" + "sync" ) var basicOperatorMap = map[string]string{ @@ -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 @@ -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 } @@ -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") } diff --git a/filter/converter_test.go b/filter/converter_test.go index 58a6e05..4dfba5a 100644 --- a/filter/converter_test.go +++ b/filter/converter_test.go @@ -1,6 +1,7 @@ package filter_test import ( + "database/sql" "fmt" "reflect" "testing" @@ -8,6 +9,27 @@ import ( "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 @@ -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) + } +} diff --git a/filter/doc.go b/filter/doc.go new file mode 100644 index 0000000..7da9556 --- /dev/null +++ b/filter/doc.go @@ -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