Skip to content

Commit

Permalink
Add Support for Mongo (#18)
Browse files Browse the repository at this point in the history
* Resolves #17 - Add support for Mongo

* Update README.md

Co-authored-by: dazadep <62723515+dazadep@users.noreply.github.com>

---------

Co-authored-by: dazadep <62723515+dazadep@users.noreply.github.com>
  • Loading branch information
steve-r-west and dazadep authored Jun 15, 2023
1 parent c3639d7 commit fe2d29e
Show file tree
Hide file tree
Showing 6 changed files with 553 additions and 28 deletions.
110 changes: 108 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ import epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/ep
import epsearchast_v3_gorm "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/gorm"
import "gorm.io/gorm"

func Example(ast *epsearchast_v3.AstNode, query *gorm.DB) error {
func Example(ast *epsearchast_v3.AstNode, query *gorm.DB, tenantBoundaryId string) error {
var err error

// Not Shown: Validation
Expand All @@ -137,6 +137,9 @@ func Example(ast *epsearchast_v3.AstNode, query *gorm.DB) error {
if err != nil {
return err
}

// Don't forget to add additional filters
query.Where("tenant_boundary_id = ?", tenantBoundaryId)

// Don't forget to expand the Args argument with ...
query.Where(sq.Clause, sq.Args...)
Expand Down Expand Up @@ -166,7 +169,7 @@ import epsearchast_v3_gorm "github.com/elasticpath/epcc-search-ast-helper/extern
import "gorm.io/gorm"


func Example(ast *epsearchast_v3.AstNode, query *gorm.DB) error {
func Example(ast *epsearchast_v3.AstNode, query *gorm.DB, tenantBoundaryId string) error {
var err error

// Not Shown: Validation
Expand All @@ -180,6 +183,9 @@ func Example(ast *epsearchast_v3.AstNode, query *gorm.DB) error {
return err
}

// Don't forget to add additional filters
query.Where("tenant_boundary_id = ?", tenantBoundaryId)

// Don't forget to expand the Args argument with ...
query.Where(sq.Clause, sq.Args...)
}
Expand Down Expand Up @@ -209,6 +215,106 @@ func (l *CustomQueryBuilder) VisitEq(first, second string) (*epsearchast_v3_gorm
}
```

#### Mongo

The following examples shows how to generate a Mongo Query with this library.

```go
package example

import (
"context"
epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3"
epsearchast_v3_mongo "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/mongo"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)

func Example(ast *epsearchast_v3.AstNode, collection *mongo.Collection, tenantBoundaryQuery bson.M) (*mongo.Cursor, error) {
// Not Shown: Validation

// Create query builder
var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{}

// Create Query Object
queryObj, err := epsearchast_v3.SemanticReduceAst(ast, qb)

if err != nil {
return nil, err
}

mongoQuery := bson.D{
{"$and",
bson.A{
tenantBoundaryQuery,
queryObj,
},
}}


return collection.Find(context.TODO(), mongoQuery)
}
```


##### Limitations

1. The Mongo Query builder is designed to produce filter compatible with the [filter argument in a Query](https://www.mongodb.com/docs/drivers/go/current/fundamentals/crud/read-operations/query-document/#specify-a-query), if a field in the API is a projection that requires computation via the aggregation pipeline, then we would likely need code changes to support that.

##### Advanced Customization

In some cases you may want to change the behaviour of the generated Mongo, the following example shows how to do that in this case we want to change emails because
we store them only in lower case in the db.

```go
package example

import (
"context"
epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3"
epsearchast_v3_mongo "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/mongo"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"strings"
)

func Example(ast *epsearchast_v3.AstNode, collection *mongo.Collection, tenantBoundaryQuery *bson.M) (*mongo.Cursor, error) {
// Not Shown: Validation

// Create query builder
var qb epsearchast_v3.SemanticReducer[bson.D] = &LowerCaseEmail{}

// Create Query Object
queryObj, err := epsearchast_v3.SemanticReduceAst(ast, qb)

if err != nil {
return nil, err
}

mongoQuery := bson.D{
{"$and",
bson.A{
tenantBoundaryQuery,
queryObj,
},
}}

return collection.Find(context.TODO(), mongoQuery)
}

type LowerCaseEmail struct {
epsearchast_v3_mongo.DefaultMongoQueryBuilder
}

func (l *LowerCaseEmail) VisitEq(first, second string) (*bson.D, error) {
if first == "email" {
return &bson.D{{first, bson.D{{"$eq", strings.ToLower(second)}}}}, nil
} else {
return DefaultMongoQueryBuilder.VisitEq(l.DefaultMongoQueryBuilder, first, second)
}
}
```


### FAQ

Expand Down
37 changes: 12 additions & 25 deletions external/epsearchast/v3/gorm/gorm_query_builder_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package epsearchast_v3_gorm

import (
"encoding/json"
"fmt"
epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -39,14 +38,12 @@ func TestSimpleBinaryOperatorFiltersGeneratesCorrectWhereClause(t *testing.T) {
}`, binOp.AstOp)

astNode, err := epsearchast_v3.GetAst(jsonTxt)

err = json.Unmarshal([]byte(jsonTxt), astNode)
require.NoError(t, err)

var sr epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{}
var qb epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{}

// Execute SUT
query, err := epsearchast_v3.SemanticReduceAst(astNode, sr)
query, err := epsearchast_v3.SemanticReduceAst(astNode, qb)

// Verification

Expand All @@ -71,14 +68,12 @@ func TestSimpleVariableOperatorFiltersGeneratesCorrectWhereClause(t *testing.T)
}`, varOp.AstOp)

astNode, err := epsearchast_v3.GetAst(jsonTxt)

err = json.Unmarshal([]byte(jsonTxt), astNode)
require.NoError(t, err)

var sr epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{}
var qb epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{}

// Execute SUT
query, err := epsearchast_v3.SemanticReduceAst(astNode, sr)
query, err := epsearchast_v3.SemanticReduceAst(astNode, qb)

// Verification

Expand All @@ -102,14 +97,12 @@ func TestLikeFilterWildCards(t *testing.T) {
}`, astLiteral)

astNode, err := epsearchast_v3.GetAst(jsonTxt)

err = json.Unmarshal([]byte(jsonTxt), astNode)
require.NoError(t, err)

var sr epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{}
var qb epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{}

// Execute SUT
query, err := epsearchast_v3.SemanticReduceAst(astNode, sr)
query, err := epsearchast_v3.SemanticReduceAst(astNode, qb)

// Verification

Expand Down Expand Up @@ -146,14 +139,12 @@ func TestSimpleRecursiveStructure(t *testing.T) {
`

astNode, err := epsearchast_v3.GetAst(jsonTxt)

err = json.Unmarshal([]byte(jsonTxt), astNode)
require.NoError(t, err)

var sr epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{}
var qb epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{}

// Execute SUT
query, err := epsearchast_v3.SemanticReduceAst(astNode, sr)
query, err := epsearchast_v3.SemanticReduceAst(astNode, qb)

// Verification

Expand Down Expand Up @@ -183,14 +174,12 @@ func TestSimpleRecursiveWithStringOverrideStruct(t *testing.T) {
`

astNode, err := epsearchast_v3.GetAst(jsonTxt)

err = json.Unmarshal([]byte(jsonTxt), astNode)
require.NoError(t, err)

var sr epsearchast_v3.SemanticReducer[SubQuery] = &LowerCaseEmail{}
var qb epsearchast_v3.SemanticReducer[SubQuery] = &LowerCaseEmail{}

// Execute SUT
query, err := epsearchast_v3.SemanticReduceAst(astNode, sr)
query, err := epsearchast_v3.SemanticReduceAst(astNode, qb)

// Verification

Expand Down Expand Up @@ -219,14 +208,12 @@ func TestSimpleRecursiveWithIntFieldStruct(t *testing.T) {
`

astNode, err := epsearchast_v3.GetAst(jsonTxt)

err = json.Unmarshal([]byte(jsonTxt), astNode)
require.NoError(t, err)

var sr epsearchast_v3.SemanticReducer[SubQuery] = &IntFieldQueryBuilder{}
var qb epsearchast_v3.SemanticReducer[SubQuery] = &IntFieldQueryBuilder{}

// Execute SUT
query, err := epsearchast_v3.SemanticReduceAst(astNode, sr)
query, err := epsearchast_v3.SemanticReduceAst(astNode, qb)

// Verification

Expand Down
81 changes: 81 additions & 0 deletions external/epsearchast/v3/mongo/mongo_query_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package epsearchast_v3_mongo

import (
epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3"
"regexp"
"strings"
)
import "go.mongodb.org/mongo-driver/bson"

type DefaultMongoQueryBuilder struct{}

var _ epsearchast_v3.SemanticReducer[bson.D] = (*DefaultMongoQueryBuilder)(nil)

func (d DefaultMongoQueryBuilder) PostVisitAnd(rs []*bson.D) (*bson.D, error) {
// https://www.mongodb.com/docs/manual/reference/operator/query/and/
return &bson.D{
{"$and",
rs,
},
}, nil
}

func (d DefaultMongoQueryBuilder) VisitIn(args ...string) (*bson.D, error) {
// https://www.mongodb.com/docs/manual/reference/operator/query/in/
return &bson.D{{args[0], bson.D{{"$in", args[1:]}}}}, nil
}

func (d DefaultMongoQueryBuilder) VisitEq(first, second string) (*bson.D, error) {
// https://www.mongodb.com/docs/manual/reference/operator/query/eq/#std-label-eq-usage-examples
// This is equivalent to { key: value } but makes for easier tests.
return &bson.D{{first, bson.D{{"$eq", second}}}}, nil
}

func (d DefaultMongoQueryBuilder) VisitLe(first, second string) (*bson.D, error) {
// https://www.mongodb.com/docs/manual/reference/operator/query/lte/
return &bson.D{{first, bson.D{{"$lte", second}}}}, nil
}

func (d DefaultMongoQueryBuilder) VisitLt(first, second string) (*bson.D, error) {
// https://www.mongodb.com/docs/manual/reference/operator/query/lt/
return &bson.D{{first, bson.D{{"$lt", second}}}}, nil
}

func (d DefaultMongoQueryBuilder) VisitGe(first, second string) (*bson.D, error) {
// https://www.mongodb.com/docs/manual/reference/operator/query/gte/
return &bson.D{{first, bson.D{{"$gte", second}}}}, nil
}

func (d DefaultMongoQueryBuilder) VisitGt(first, second string) (*bson.D, error) {
// https://www.mongodb.com/docs/manual/reference/operator/query/gt/
return &bson.D{{first, bson.D{{"$gt", second}}}}, nil
}

func (d DefaultMongoQueryBuilder) VisitLike(first, second string) (*bson.D, error) {
return &bson.D{{first, bson.D{{"$regex", d.ProcessLikeWildcards(second)}}}}, nil
}

func (d DefaultMongoQueryBuilder) ProcessLikeWildcards(valString string) string {
if valString == "*" {
return "^.*$"
}

var startsWithStar = strings.HasPrefix(valString, "*")
var endsWithStar = strings.HasSuffix(valString, "*")
if startsWithStar {
valString = valString[1:]
}
if endsWithStar {
valString = valString[:len(valString)-1]
}

valString = regexp.QuoteMeta(valString)

if startsWithStar {
valString = ".*" + valString
}
if endsWithStar {
valString += ".*"
}
return "^" + valString + "$"
}
Loading

0 comments on commit fe2d29e

Please sign in to comment.