Extract leaf package
This commit is contained in:
commit
2dd1fb91c7
52
.golangci.yml
Normal file
52
.golangci.yml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
output:
|
||||||
|
format: tab
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
govet:
|
||||||
|
check-shadowing: true
|
||||||
|
golint:
|
||||||
|
min-confidence: 0.1
|
||||||
|
maligned:
|
||||||
|
suggest-new: true
|
||||||
|
goconst:
|
||||||
|
min-len: 2
|
||||||
|
min-occurrences: 2
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
lll:
|
||||||
|
line-length: 140
|
||||||
|
gocritic:
|
||||||
|
enabled-tags:
|
||||||
|
- performance
|
||||||
|
- style
|
||||||
|
- experimental
|
||||||
|
disabled-checks:
|
||||||
|
- unnamedResult
|
||||||
|
- paramTypeCombine
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- megacheck
|
||||||
|
- revive
|
||||||
|
- govet
|
||||||
|
- unconvert
|
||||||
|
- megacheck
|
||||||
|
- unused
|
||||||
|
- gas
|
||||||
|
- gocyclo
|
||||||
|
- dupl
|
||||||
|
- misspell
|
||||||
|
- unparam
|
||||||
|
- typecheck
|
||||||
|
- ineffassign
|
||||||
|
- stylecheck
|
||||||
|
- gochecknoinits
|
||||||
|
- exportloopref
|
||||||
|
- gocritic
|
||||||
|
- nakedret
|
||||||
|
- gosimple
|
||||||
|
- prealloc
|
||||||
|
fast: false
|
||||||
|
disable-all: true
|
37
document.go
Normal file
37
document.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package leaf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Document interface {
|
||||||
|
ID() primitive.ObjectID
|
||||||
|
SetID(id primitive.ObjectID)
|
||||||
|
|
||||||
|
SetCreatedAt(ts time.Time)
|
||||||
|
SetUpdatedAt(ts time.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Base struct {
|
||||||
|
ObjectID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty" example:"507f1f77bcf86cd799439011"`
|
||||||
|
CreatedAt time.Time `json:"created_at" bson:"created_at" format:"dateTime" example:"2020-01-01T00:00:00Z"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" bson:"updated_at" format:"dateTime" example:"2020-01-01T00:00:00Z"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Base) ID() primitive.ObjectID {
|
||||||
|
return s.ObjectID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Base) SetID(id primitive.ObjectID) {
|
||||||
|
s.ObjectID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Base) SetCreatedAt(ts time.Time) {
|
||||||
|
s.CreatedAt = ts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Base) SetUpdatedAt(ts time.Time) {
|
||||||
|
s.UpdatedAt = ts
|
||||||
|
}
|
51
document_test.go
Normal file
51
document_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package leaf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBaseInitialization(t *testing.T) {
|
||||||
|
id := primitive.NewObjectID()
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
base := Base{}
|
||||||
|
base.SetID(id)
|
||||||
|
base.SetCreatedAt(now)
|
||||||
|
base.SetUpdatedAt(now)
|
||||||
|
|
||||||
|
assert.Equal(t, id, base.ID())
|
||||||
|
assert.Equal(t, id, base.ObjectID)
|
||||||
|
assert.True(t, base.CreatedAt.Equal(now))
|
||||||
|
assert.True(t, base.UpdatedAt.Equal(now))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseJSONMarshalling(t *testing.T) {
|
||||||
|
base := Base{}
|
||||||
|
base.SetID(primitive.NewObjectID())
|
||||||
|
base.SetCreatedAt(time.Now())
|
||||||
|
base.SetUpdatedAt(time.Now())
|
||||||
|
|
||||||
|
data, err := json.Marshal(base)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, json.Valid(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseBSONMarshalling(t *testing.T) {
|
||||||
|
base := Base{}
|
||||||
|
base.SetID(primitive.NewObjectID())
|
||||||
|
base.SetCreatedAt(time.Now())
|
||||||
|
base.SetUpdatedAt(time.Now())
|
||||||
|
|
||||||
|
data, err := bson.Marshal(base)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var m bson.M
|
||||||
|
err = bson.Unmarshal(data, &m)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
84
filter.go
Normal file
84
filter.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package leaf
|
||||||
|
|
||||||
|
var (
|
||||||
|
FilterAll = FilterDocument{}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OperatorEq Operator = "$eq"
|
||||||
|
OperatorNe Operator = "$ne"
|
||||||
|
OperatorGt Operator = "$gt"
|
||||||
|
OperatorGte Operator = "$gte"
|
||||||
|
OperatorLt Operator = "$lt"
|
||||||
|
OperatorLte Operator = "$lte"
|
||||||
|
OperatorIn Operator = "$in"
|
||||||
|
OperatorNin Operator = "$nin"
|
||||||
|
OperatorExists Operator = "$exists"
|
||||||
|
OperatorRegex Operator = "$regex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type M map[Operator]any
|
||||||
|
type Operator string
|
||||||
|
type FilterDocument map[string]M
|
||||||
|
|
||||||
|
func Eq[T any](field string, value T) FilterDocument {
|
||||||
|
return createSelector(field, OperatorEq, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ne[T any](field string, value T) FilterDocument {
|
||||||
|
return createSelector(field, OperatorNe, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Gt[T any](field string, value T) FilterDocument {
|
||||||
|
return createSelector(field, OperatorGt, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Gte[T any](field string, value T) FilterDocument {
|
||||||
|
return createSelector(field, OperatorGte, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Lt[T any](field string, value T) FilterDocument {
|
||||||
|
return createSelector(field, OperatorLt, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Lte[T any](field string, value T) FilterDocument {
|
||||||
|
return createSelector(field, OperatorLte, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func In[T any](field string, value []T) FilterDocument {
|
||||||
|
return createSelector(field, OperatorIn, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Nin[T any](field string, value []T) FilterDocument {
|
||||||
|
return createSelector(field, OperatorNin, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Exists(field string, value bool) FilterDocument {
|
||||||
|
return createSelector(field, OperatorExists, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Regex(field string, value string) FilterDocument {
|
||||||
|
return createSelector(field, OperatorRegex, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSelector(field string, operator Operator, value any) FilterDocument {
|
||||||
|
return FilterDocument{field: M{operator: value}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Filter(docs ...FilterDocument) FilterDocument {
|
||||||
|
query := FilterDocument{}
|
||||||
|
for _, doc := range docs {
|
||||||
|
for field, operators := range doc {
|
||||||
|
if _, ok := query[field]; !ok {
|
||||||
|
query[field] = operators
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for op, value := range operators {
|
||||||
|
query[field][op] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
56
filter_test.go
Normal file
56
filter_test.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package leaf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFilter(t *testing.T) {
|
||||||
|
t.Run("merge two filter documents", func(t *testing.T) {
|
||||||
|
doc1 := Filter(
|
||||||
|
Gt("field1", 1),
|
||||||
|
Lt("field2", 10),
|
||||||
|
)
|
||||||
|
|
||||||
|
doc2 := Filter(
|
||||||
|
Ne("field1", 2),
|
||||||
|
Eq("field3", "test"),
|
||||||
|
)
|
||||||
|
|
||||||
|
expected := FilterDocument{
|
||||||
|
"field1": {"$gt": 1, "$ne": 2},
|
||||||
|
"field2": {"$lt": 10},
|
||||||
|
"field3": {"$eq": "test"},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, Filter(doc1, doc2))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("merge three filter documents", func(t *testing.T) {
|
||||||
|
doc1 := Filter(
|
||||||
|
Gt("field1", 1),
|
||||||
|
Lt("field2", 10),
|
||||||
|
)
|
||||||
|
|
||||||
|
doc2 := Filter(
|
||||||
|
Ne("field1", 2),
|
||||||
|
Eq("field3", "test"),
|
||||||
|
)
|
||||||
|
|
||||||
|
doc3 := Filter(
|
||||||
|
Gt("field1", 5),
|
||||||
|
Ne("field2", 5),
|
||||||
|
Eq("field4", "hello"),
|
||||||
|
)
|
||||||
|
|
||||||
|
expected := FilterDocument{
|
||||||
|
"field1": {"$gt": 5, "$ne": 2},
|
||||||
|
"field2": {"$lt": 10, "$ne": 5},
|
||||||
|
"field3": {"$eq": "test"},
|
||||||
|
"field4": {"$eq": "hello"},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, Filter(doc1, doc2, doc3))
|
||||||
|
})
|
||||||
|
}
|
28
go.mod
Normal file
28
go.mod
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
module go.pkg.cx/leaf
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/rs/zerolog v1.31.0
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
|
go.mongodb.org/mongo-driver v1.13.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
|
github.com/klauspost/compress v1.13.6 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||||
|
golang.org/x/sys v0.12.0 // indirect
|
||||||
|
golang.org/x/text v0.7.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
77
go.sum
Normal file
77
go.sum
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
|
||||||
|
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
|
||||||
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||||
|
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
|
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||||
|
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
|
||||||
|
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
98
index.go
Normal file
98
index.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package leaf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
driver "go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureIndexes(ctx context.Context, collection *driver.Collection, models []driver.IndexModel, createIndexes bool, logger *zerolog.Logger) error {
|
||||||
|
existing, err := indexNames(ctx, collection)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
indexes := filterIndexes(models, existing, logger)
|
||||||
|
if len(indexes) < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !createIndexes {
|
||||||
|
for _, im := range indexes {
|
||||||
|
logger.Warn().Str("name", *im.Options.Name).Msg("index declared but not present")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = collection.Indexes().CreateMany(ctx, indexes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, im := range indexes {
|
||||||
|
logger.Info().Str("name", *im.Options.Name).Msg("index created")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexNames(ctx context.Context, collection *driver.Collection) (map[string]struct{}, error) {
|
||||||
|
cursor, err := collection.Indexes().List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
names := make(map[string]struct{})
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var idx bson.M
|
||||||
|
if err := cursor.Decode(&idx); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name, ok := idx["name"].(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
names[name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterIndexes(models []driver.IndexModel, names map[string]struct{}, logger *zerolog.Logger) []driver.IndexModel {
|
||||||
|
var filtered []driver.IndexModel
|
||||||
|
for _, im := range models {
|
||||||
|
if im.Options.Name == nil {
|
||||||
|
logger.Warn().Interface("keys", im.Keys).Msg("must specify index name explicitly")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := names[*im.Options.Name]; !ok {
|
||||||
|
filtered = append(filtered, im)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func withTimestampIndexes(models []driver.IndexModel) []driver.IndexModel {
|
||||||
|
tsModels := []driver.IndexModel{
|
||||||
|
{
|
||||||
|
Keys: bson.D{primitive.E{Key: "created_at", Value: 1}},
|
||||||
|
Options: options.Index().SetName("created_at_1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Keys: bson.D{primitive.E{Key: "updated_at", Value: 1}},
|
||||||
|
Options: options.Index().SetName("updated_at_1"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(models, tsModels...)
|
||||||
|
}
|
23
objectid.go
Normal file
23
objectid.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package leaf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ObjectIDFromHex(hex string) primitive.ObjectID {
|
||||||
|
id, err := primitive.ObjectIDFromHex(hex)
|
||||||
|
if err != nil {
|
||||||
|
return primitive.NilObjectID
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func ObjectIDFromHexSlice(hexSlice []string) []primitive.ObjectID {
|
||||||
|
ids := make([]primitive.ObjectID, len(hexSlice))
|
||||||
|
for i := range hexSlice {
|
||||||
|
ids[i] = ObjectIDFromHex(hexSlice[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids
|
||||||
|
}
|
40
objectid_test.go
Normal file
40
objectid_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package leaf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestObjectIDFromHex(t *testing.T) {
|
||||||
|
t.Run("valid hex", func(t *testing.T) {
|
||||||
|
hex := "507f1f77bcf86cd799439011"
|
||||||
|
id, err := primitive.ObjectIDFromHex(hex)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, id, ObjectIDFromHex(hex))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid hex", func(t *testing.T) {
|
||||||
|
hex := "invalidhex"
|
||||||
|
assert.Equal(t, primitive.NilObjectID, ObjectIDFromHex(hex))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObjectIDFromHexSlice(t *testing.T) {
|
||||||
|
t.Run("valid hex slice", func(t *testing.T) {
|
||||||
|
hex := []string{"507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"}
|
||||||
|
ids := make([]primitive.ObjectID, len(hex))
|
||||||
|
for i := range hex {
|
||||||
|
ids[i], _ = primitive.ObjectIDFromHex(hex[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, ids, ObjectIDFromHexSlice(hex))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid hex slice", func(t *testing.T) {
|
||||||
|
hex := []string{"invalidhex", "invalidhex"}
|
||||||
|
assert.Equal(t, []primitive.ObjectID{primitive.NilObjectID, primitive.NilObjectID}, ObjectIDFromHexSlice(hex))
|
||||||
|
})
|
||||||
|
}
|
118
repository.go
Normal file
118
repository.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package leaf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
driver "go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RepositoryInterface[T Document] interface {
|
||||||
|
Find(ctx context.Context, filter interface{}, opts ...*options.FindOptions) ([]T, error)
|
||||||
|
FindOne(ctx context.Context, filter interface{}, opts ...*options.FindOneOptions) (T, error)
|
||||||
|
FindByID(ctx context.Context, id string, opts ...*options.FindOneOptions) (T, error)
|
||||||
|
|
||||||
|
Create(ctx context.Context, data T, opts ...*options.InsertOneOptions) (T, error)
|
||||||
|
|
||||||
|
Update(ctx context.Context, data T, opts ...*options.UpdateOptions) (T, error)
|
||||||
|
UpdateOne(ctx context.Context, filter interface{}, data T, opts ...*options.UpdateOptions) (T, error)
|
||||||
|
|
||||||
|
Delete(ctx context.Context, data T, opts ...*options.DeleteOptions) error
|
||||||
|
DeleteOne(ctx context.Context, filter interface{}, opts ...*options.DeleteOptions) error
|
||||||
|
|
||||||
|
CountDocuments(ctx context.Context, filter interface{}, opts ...*options.CountOptions) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Repository[T Document] struct {
|
||||||
|
collection *driver.Collection
|
||||||
|
indexes []driver.IndexModel
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository[T Document](collection *driver.Collection, indexes []driver.IndexModel) *Repository[T] {
|
||||||
|
return &Repository[T]{collection: collection, indexes: withTimestampIndexes(indexes)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Repository[T]) Find(ctx context.Context, filter interface{}, opts ...*options.FindOptions) ([]T, error) {
|
||||||
|
cursor, err := s.collection.Find(ctx, filter, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, repositoryError(s.collection, "Find", err)
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
var data []T
|
||||||
|
if err := cursor.All(ctx, &data); err != nil {
|
||||||
|
return nil, repositoryError(s.collection, "Find", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Repository[T]) FindOne(ctx context.Context, filter interface{}, opts ...*options.FindOneOptions) (T, error) {
|
||||||
|
var data T
|
||||||
|
if err := s.collection.FindOne(ctx, filter, opts...).Decode(&data); err != nil {
|
||||||
|
return data, repositoryError(s.collection, "FindOne", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Repository[T]) FindByID(ctx context.Context, id string, opts ...*options.FindOneOptions) (T, error) {
|
||||||
|
return s.FindOne(ctx, bson.M{"_id": ObjectIDFromHex(id)}, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Repository[T]) Create(ctx context.Context, data T, opts ...*options.InsertOneOptions) (T, error) {
|
||||||
|
data.SetCreatedAt(time.Now().UTC())
|
||||||
|
data.SetUpdatedAt(time.Now().UTC())
|
||||||
|
|
||||||
|
res, err := s.collection.InsertOne(ctx, data, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return data, repositoryError(s.collection, "Create", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.SetID(res.InsertedID.(primitive.ObjectID))
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Repository[T]) Update(ctx context.Context, data T, opts ...*options.UpdateOptions) (T, error) {
|
||||||
|
return s.UpdateOne(ctx, bson.M{"_id": data.ID()}, data, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Repository[T]) UpdateOne(ctx context.Context, filter interface{}, data T, opts ...*options.UpdateOptions) (T, error) {
|
||||||
|
data.SetUpdatedAt(time.Now().UTC())
|
||||||
|
|
||||||
|
if _, err := s.collection.UpdateOne(ctx, filter, bson.M{"$set": data}, opts...); err != nil {
|
||||||
|
return data, repositoryError(s.collection, "UpdateOne", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Repository[T]) Delete(ctx context.Context, data T, opts ...*options.DeleteOptions) error {
|
||||||
|
return s.DeleteOne(ctx, bson.M{"_id": data.ID()}, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Repository[T]) DeleteOne(ctx context.Context, filter interface{}, opts ...*options.DeleteOptions) error {
|
||||||
|
if _, err := s.collection.DeleteOne(ctx, filter, opts...); err != nil {
|
||||||
|
return repositoryError(s.collection, "DeleteOne", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Repository[T]) CountDocuments(ctx context.Context, filter interface{}, opts ...*options.CountOptions) (int, error) {
|
||||||
|
count, err := s.collection.CountDocuments(ctx, filter, opts...)
|
||||||
|
return int(count), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Repository[T]) EnsureIndexes(ctx context.Context, createIndexes bool, logger *zerolog.Logger) error {
|
||||||
|
return ensureIndexes(ctx, s.collection, s.indexes, createIndexes, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func repositoryError(collection *driver.Collection, op string, err error) error {
|
||||||
|
return fmt.Errorf("%s.%s: %v", collection.Name(), op, err)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user