Extract leaf package

This commit is contained in:
Anton Zadvorny 2024-01-07 08:40:41 +03:00
commit 2dd1fb91c7
11 changed files with 664 additions and 0 deletions

52
.golangci.yml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}