25 Commits

Author SHA1 Message Date
Nikita Tokarchuk 87708fee04 Add callback support to a saving 2020-07-23 01:44:22 +02:00
Nikita Tokarchuk 06f6f48f11 Add context support to a saving 2020-07-23 01:44:22 +02:00
Nikita Tokarchuk 28be1f46b9 Add filters support for a saving 2020-07-23 01:44:22 +02:00
Nikita Tokarchuk 848703d56d Add callback support to a single deleting 2020-07-23 01:44:22 +02:00
Nikita Tokarchuk 8fcd764a8c Add context query support to a single deleting 2020-07-23 01:44:22 +02:00
Nikita Tokarchuk 50e947b203 Invoke onclose at the end of an array deleting 2020-07-23 01:44:22 +02:00
Nikita Tokarchuk 73dc4974a2 Add query context support for an array deleting 2020-07-23 01:44:22 +02:00
Nikita Tokarchuk 08397650c6 Add filters support for an array deleting 2020-07-23 01:44:22 +02:00
Nikita Tokarchuk ee4bf46662 Add callback support for a counting 2020-07-23 01:44:22 +02:00
Nikita Tokarchuk 9fb3094b87 Use reflect2 2020-07-23 00:00:53 +02:00
Nikita Tokarchuk b22b0f0919 Add query to the context 2020-07-23 00:00:53 +02:00
Nikita Tokarchuk 951e5f5bef Add onclose callback 2020-07-23 00:00:53 +02:00
Nikita Tokarchuk b796d5ac3b Make callback type 2020-07-23 00:00:53 +02:00
Nikita Tokarchuk 08c3c5b377 Add callback mechanism and implement on-decode callback 2020-07-13 16:32:48 +02:00
Nikita Tokarchuk 09fa64ab0e Return err if cannot decode array element 2020-07-13 04:09:04 +02:00
Nikita Tokarchuk c019a0ea4b Ignore unused arguments 2020-07-13 04:09:04 +02:00
Nikita Tokarchuk ee1b0e17d5 Improve panic messages 2020-07-13 04:09:04 +02:00
Nikita Tokarchuk 22a1d7033f Remove custom errors 2020-07-13 04:09:04 +02:00
Nikita Tokarchuk 1d3e29fe10 Use err type for panics 2020-07-13 04:09:04 +02:00
Nikita Tokarchuk 72e74a65b6 Use named returns for the code style consistency 2020-07-13 04:09:04 +02:00
Nikita Tokarchuk 9cf3551c20 MIT licensed 2020-06-04 21:30:19 +02:00
Nikita Tokarchuk 3035d8d571 Fix aggregation pipeline match step 2020-06-04 18:15:59 +02:00
Nikita Tokarchuk eac50d1770 Do not use unnecessary reflect 2020-06-04 18:15:35 +02:00
Nikita Tokarchuk 05ebb25e70 Use unsafe pointer in the interface struct header is more correct way 2020-06-04 18:15:08 +02:00
Nikita Tokarchuk fd53c66690 Use ordered document for index model 2020-03-25 17:40:26 +01:00
31 changed files with 354 additions and 251 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright © 2020 Nikita Tokarchuk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2
View File
@@ -3,6 +3,8 @@ module github.com/mainnika/mongox-go-driver/v2
require (
github.com/google/go-cmp v0.3.0 // indirect
github.com/klauspost/compress v1.10.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1
github.com/pkg/errors v0.9.1 // indirect
github.com/stretchr/testify v1.5.1
github.com/xdg/stringprep v1.0.0 // indirect
+7
View File
@@ -44,11 +44,17 @@ github.com/klauspost/compress v1.10.1 h1:a/QY0o9S6wCi0XhxaMX/QmusicNUqCqFugR6WKP
github.com/klauspost/compress v1.10.1/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -113,6 +119,7 @@ golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+1 -1
View File
@@ -30,6 +30,6 @@ func NewEphemeral(URI string) (db *EphemeralDatabase, err error) {
}
// Close the connection and drop database
func (e *EphemeralDatabase) Close() error {
func (e *EphemeralDatabase) Close() (err error) {
return e.Client().Database(e.Name()).Drop(e.Context())
}
+2 -2
View File
@@ -3,10 +3,10 @@ package base
import (
"fmt"
"github.com/modern-go/reflect2"
"go.mongodb.org/mongo-driver/bson/primitive"
"github.com/mainnika/mongox-go-driver/v2/mongox"
"github.com/mainnika/mongox-go-driver/v2/mongox/utils"
)
// GetID returns source document id
@@ -63,7 +63,7 @@ func getObjectOrPanic(source mongox.JSONBased) (id primitive.D) {
func getInterfaceOrPanic(source mongox.InterfaceBased) (id interface{}) {
id = source.GetID()
if !utils.IsNil(id) {
if !reflect2.IsNil(id) {
return id
}
+7 -5
View File
@@ -7,11 +7,11 @@ import (
)
// GetProtection function finds protection field in the source document otherwise returns nil
func GetProtection(source interface{}) *protection.Key {
func GetProtection(source interface{}) (key *protection.Key) {
v := reflect.ValueOf(source)
if v.Kind() != reflect.Ptr || v.IsNil() {
return nil
return
}
el := v.Elem()
@@ -25,14 +25,16 @@ func GetProtection(source interface{}) *protection.Key {
switch field.Interface().(type) {
case *protection.Key:
return field.Interface().(*protection.Key)
key = field.Interface().(*protection.Key)
case protection.Key:
ptr := field.Addr()
return ptr.Interface().(*protection.Key)
key = ptr.Interface().(*protection.Key)
default:
continue
}
return
}
return nil
return
}
+1 -1
View File
@@ -14,7 +14,7 @@ type Primary struct {
}
// GetID returns an _id
func (p *Primary) GetID() primitive.D {
func (p *Primary) GetID() (id primitive.D) {
return p.ID
}
+1 -1
View File
@@ -14,7 +14,7 @@ type Primary struct {
}
// GetID returns an _id
func (p *Primary) GetID() primitive.ObjectID {
func (p *Primary) GetID() (id primitive.ObjectID) {
return p.ID
}
+2 -1
View File
@@ -1,6 +1,7 @@
package base
import (
"fmt"
"reflect"
)
@@ -19,7 +20,7 @@ func Reset(target interface{}) {
v := reflect.ValueOf(target)
if v.Kind() != reflect.Ptr {
panic("reset target should be a pointer")
panic(fmt.Errorf("reset target should be a pointer"))
}
t := v.Elem().Type()
+1 -1
View File
@@ -12,7 +12,7 @@ type Primary struct {
}
// GetID returns an _id
func (p *Primary) GetID() string {
func (p *Primary) GetID() (id string) {
return p.ID
}
+6 -12
View File
@@ -1,32 +1,26 @@
package database
import (
"fmt"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/mainnika/mongox-go-driver/v2/mongox"
"github.com/mainnika/mongox-go-driver/v2/mongox/query"
)
// Count function counts documents in the database by query
// target is used only to get collection by tag so it'd be better to use nil ptr here
func (d *Database) Count(target interface{}, filters ...interface{}) (int64, error) {
func (d *Database) Count(target interface{}, filters ...interface{}) (result int64, err error) {
collection := d.GetCollectionOf(target)
opts := options.Count()
composed := query.Compose(filters...)
ctx := query.WithContext(d.Context(), composed)
opts.Limit = composed.Limiter()
opts.Skip = composed.Skipper()
result, err := collection.CountDocuments(d.Context(), composed.M(), opts)
if err == mongox.ErrNoDocuments {
return 0, err
}
if err != nil {
return 0, fmt.Errorf("can't decode desult: %w", err)
}
result, err = collection.CountDocuments(ctx, composed.M(), opts)
return result, nil
_ = composed.OnClose().Invoke(ctx, target)
return
}
+19 -16
View File
@@ -22,17 +22,18 @@ type Database struct {
}
// NewDatabase function creates new database instance with mongo client and empty context
func NewDatabase(client *mongox.Client, dbname string) mongox.Database {
func NewDatabase(client *mongox.Client, dbname string) (db mongox.Database) {
db := &Database{}
db.client = client
db.dbname = dbname
db = &Database{
client: client,
dbname: dbname,
}
return db
return
}
// Client function returns a mongo client
func (d *Database) Client() *mongox.Client {
func (d *Database) Client() (client *mongox.Client) {
return d.client
}
@@ -48,22 +49,24 @@ func (d *Database) Context() (ctx context.Context) {
}
// Name function returns a database name
func (d *Database) Name() string {
func (d *Database) Name() (name string) {
return d.dbname
}
// New function creates new database context with same client
func (d *Database) New(ctx context.Context) mongox.Database {
func (d *Database) New(ctx context.Context) (db mongox.Database) {
if ctx == nil {
ctx = context.Background()
}
return &Database{
db = &Database{
client: d.client,
dbname: d.dbname,
ctx: ctx,
}
return
}
// GetCollectionOf returns the collection object by the «collection» tag of the given document;
@@ -72,7 +75,7 @@ func (d *Database) New(ctx context.Context) mongox.Database {
// base.ObjectID `bson:",inline" json:",inline" collection:"foobars"`
// ...
// Will panic if there is no «collection» tag
func (d *Database) GetCollectionOf(document interface{}) *mongox.Collection {
func (d *Database) GetCollectionOf(document interface{}) (collection *mongox.Collection) {
el := reflect.TypeOf(document).Elem()
numField := el.NumField()
@@ -111,7 +114,7 @@ func (d *Database) createAggregateLoad(target interface{}, composed *query.Query
pipeline := primitive.A{}
if !composed.Empty() {
pipeline = append(pipeline, primitive.M{"$match": primitive.M{"$expr": composed.M()}})
pipeline = append(pipeline, primitive.M{"$match": composed.M()})
}
if composed.Sorter() != nil {
pipeline = append(pipeline, primitive.M{"$sort": composed.Sorter()})
@@ -137,9 +140,9 @@ func (d *Database) createAggregateLoad(target interface{}, composed *query.Query
if !ok {
continue
}
jsonTag, ok := tag.Lookup("json")
jsonTag, _ := tag.Lookup("json")
if jsonTag == "-" {
return nil, fmt.Errorf("preload private field is impossible")
panic(fmt.Errorf("preload private field is impossible"))
}
jsonData := strings.SplitN(jsonTag, ",", 2)
@@ -153,7 +156,7 @@ func (d *Database) createAggregateLoad(target interface{}, composed *query.Query
continue
}
if len(preloadData) == 1 {
panic("there is no foreign field")
panic(fmt.Errorf("there is no foreign field"))
}
localField := strings.TrimSpace(preloadData[0])
@@ -163,7 +166,7 @@ func (d *Database) createAggregateLoad(target interface{}, composed *query.Query
foreignField := strings.TrimSpace(preloadData[1])
if len(foreignField) == 0 {
panic("there is no foreign field")
panic(fmt.Errorf("there is no foreign field"))
}
preloadLimiter := 100
@@ -195,7 +198,7 @@ func (d *Database) createAggregateLoad(target interface{}, composed *query.Query
typ = typ.Elem()
}
if typ.Kind() != reflect.Ptr {
panic("preload field should have ptr type")
panic(fmt.Errorf("preload field should have ptr type"))
}
lookupCollection := d.GetCollectionOf(reflect.Zero(typ).Interface())
+12 -5
View File
@@ -8,10 +8,11 @@ import (
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/mainnika/mongox-go-driver/v2/mongox/base"
"github.com/mainnika/mongox-go-driver/v2/mongox/query"
)
// DeleteArray removes documents list from a database by their ids
func (d *Database) DeleteArray(target interface{}) error {
func (d *Database) DeleteArray(target interface{}, filters ...interface{}) (err error) {
targetV := reflect.ValueOf(target)
targetT := targetV.Type()
@@ -37,6 +38,8 @@ func (d *Database) DeleteArray(target interface{}) error {
collection := d.GetCollectionOf(zeroElem.Interface())
opts := options.Delete()
ids := primitive.A{}
composed := query.Compose(filters...)
ctx := query.WithContext(d.Context(), composed)
for i := 0; i < targetLen; i++ {
elem := targetSliceV.Index(i)
@@ -47,13 +50,17 @@ func (d *Database) DeleteArray(target interface{}) error {
return fmt.Errorf("can't delete zero elements")
}
result, err := collection.DeleteMany(d.Context(), primitive.M{"_id": primitive.M{"$in": ids}}, opts)
composed.And(primitive.M{"_id": primitive.M{"$in": ids}})
defer composed.OnClose().Invoke(ctx, target)
result, err := collection.DeleteMany(ctx, composed.M(), opts)
if err != nil {
return fmt.Errorf("can't create find and delete result: %w", err)
return
}
if result.DeletedCount != int64(targetLen) {
return fmt.Errorf("can't verify delete result: removed count mismatch %d != %d", result.DeletedCount, targetLen)
err = fmt.Errorf("can't verify delete result: removed count mismatch %d != %d", result.DeletedCount, targetLen)
}
return nil
return
}
+15 -11
View File
@@ -4,26 +4,26 @@ import (
"fmt"
"time"
"github.com/modern-go/reflect2"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/mainnika/mongox-go-driver/v2/mongox"
"github.com/mainnika/mongox-go-driver/v2/mongox/base"
"github.com/mainnika/mongox-go-driver/v2/mongox/query"
"github.com/mainnika/mongox-go-driver/v2/mongox/utils"
)
// DeleteOne removes a document from a database and then returns it into target
func (d *Database) DeleteOne(target interface{}, filters ...interface{}) error {
func (d *Database) DeleteOne(target interface{}, filters ...interface{}) (err error) {
collection := d.GetCollectionOf(target)
opts := &options.FindOneAndDeleteOptions{}
composed := query.Compose(filters...)
protected := base.GetProtection(target)
ctx := query.WithContext(d.Context(), composed)
opts.Sort = composed.Sorter()
if !utils.IsNil(target) {
if !reflect2.IsNil(target) {
composed.And(primitive.M{"_id": base.GetID(target)})
}
@@ -33,18 +33,22 @@ func (d *Database) DeleteOne(target interface{}, filters ...interface{}) error {
protected.V = time.Now().Unix()
}
result := collection.FindOneAndDelete(d.Context(), composed.M(), opts)
defer composed.OnClose().Invoke(ctx, target)
result := collection.FindOneAndDelete(ctx, composed.M(), opts)
if result.Err() != nil {
return fmt.Errorf("can't create find one and delete result: %w", result.Err())
}
err := result.Decode(target)
if err == mongox.ErrNoDocuments {
return err
}
err = result.Decode(target)
if err != nil {
return fmt.Errorf("can't decode result: %w", err)
return
}
return nil
err = composed.OnDecode().Invoke(ctx, target)
if err != nil {
return
}
return
}
+13 -11
View File
@@ -21,7 +21,7 @@ import (
// `index:"-,+foo,+-bar,unique,allowNull"` -- https://docs.mongodb.com/manual/core/index-partial
// `index:"-,unique,allowNull,expireAfter=86400"` -- https://docs.mongodb.com/manual/core/index-ttl
// `index:"-,unique,allowNull,expireAfter={{.Expire}}"` -- evaluate index as a golang template with `cfg` arguments
func (d *Database) IndexEnsure(cfg interface{}, document interface{}) error {
func (d *Database) IndexEnsure(cfg interface{}, document interface{}) (err error) {
el := reflect.ValueOf(document).Elem().Type()
numField := el.NumField()
@@ -41,14 +41,16 @@ func (d *Database) IndexEnsure(cfg interface{}, document interface{}) error {
return fmt.Errorf("bson tag is not defined for field:%v document:%v", field, document)
}
tmpBuffer := &bytes.Buffer{}
tpl, err := template.New("").Parse(indexTag)
var tmpBuffer = &bytes.Buffer{}
var tpl *template.Template
tpl, err = template.New("").Parse(indexTag)
if err != nil {
panic(fmt.Errorf("invalid prop template, %v", indexTag))
panic(fmt.Errorf("invalid prop template %v, err:%w", indexTag, err))
}
err = tpl.Execute(tmpBuffer, cfg)
if err != nil {
panic(fmt.Errorf("failed to evaluate prop template, %v", indexTag))
panic(fmt.Errorf("failed to evaluate prop template %v, err:%w", indexTag, err))
}
indexString := tmpBuffer.String()
@@ -64,15 +66,15 @@ func (d *Database) IndexEnsure(cfg interface{}, document interface{}) error {
panic(fmt.Errorf("cannot evaluate index key"))
}
index := primitive.M{key: 1}
opts := &options.IndexOptions{
Background: &f,
Unique: &f,
Name: &name,
}
index := primitive.D{{Key: key, Value: 1}}
if indexValues[0] == "-" {
index[key] = -1
index = primitive.D{{Key: key, Value: -1}}
}
for _, prop := range indexValues[1:] {
@@ -114,9 +116,9 @@ func (d *Database) IndexEnsure(cfg interface{}, document interface{}) error {
}
if compoundValue[0] == '-' {
index[compoundValue[1:]] = -1
index = append(index, primitive.E{compoundValue[1:], -1})
} else {
index[compoundValue] = 1
index = append(index, primitive.E{compoundValue, 1})
}
default:
@@ -126,9 +128,9 @@ func (d *Database) IndexEnsure(cfg interface{}, document interface{}) error {
_, err = documents.Indexes().CreateOne(d.Context(), mongo.IndexModel{Keys: index, Options: opts})
if err != nil {
return err
return
}
}
return nil
return
}
+28 -15
View File
@@ -10,7 +10,7 @@ import (
)
// LoadArray loads an array of documents from the database by query
func (d *Database) LoadArray(target interface{}, filters ...interface{}) error {
func (d *Database) LoadArray(target interface{}, filters ...interface{}) (err error) {
targetV := reflect.ValueOf(target)
targetT := targetV.Type()
@@ -34,9 +34,10 @@ func (d *Database) LoadArray(target interface{}, filters ...interface{}) error {
composed := query.Compose(filters...)
zeroElem := reflect.Zero(targetSliceElemT)
hasPreloader, _ := composed.Preloader()
ctx := query.WithContext(d.Context(), composed)
var result *mongox.Cursor
var err error
var i int
if hasPreloader {
result, err = d.createAggregateLoad(zeroElem.Interface(), composed)
@@ -44,25 +45,37 @@ func (d *Database) LoadArray(target interface{}, filters ...interface{}) error {
result, err = d.createSimpleLoad(zeroElem.Interface(), composed)
}
if err != nil {
return fmt.Errorf("can't create find result: %w", err)
err = fmt.Errorf("can't create find result: %w", err)
return
}
var i int
defer composed.OnClose().Invoke(ctx, target)
for i = 0; result.Next(ctx); {
var elem interface{}
for i = 0; result.Next(d.Context()); {
if targetSliceV.Len() == i {
elem := reflect.New(targetSliceElemT.Elem())
if err = result.Decode(elem.Interface()); err == nil {
targetSliceV = reflect.Append(targetSliceV, elem)
} else {
continue
value := reflect.New(targetSliceElemT.Elem())
err = result.Decode(value.Interface())
elem = value.Interface()
if err == nil {
targetSliceV = reflect.Append(targetSliceV, value)
}
} else {
elem := targetSliceV.Index(i).Interface()
elem = targetSliceV.Index(i).Interface()
base.Reset(elem)
if err = result.Decode(elem); err != nil {
continue
}
err = result.Decode(elem)
}
if err != nil {
_ = result.Close(ctx)
return
}
err = composed.OnDecode().Invoke(ctx, elem)
if err != nil {
_ = result.Close(ctx)
return
}
i++
@@ -71,5 +84,5 @@ func (d *Database) LoadArray(target interface{}, filters ...interface{}) error {
targetSliceV = targetSliceV.Slice(0, i)
targetV.Elem().Set(targetSliceV)
return result.Close(d.Context())
return result.Close(ctx)
}
+16 -4
View File
@@ -9,13 +9,13 @@ import (
)
// LoadOne function loads a first single target document by a query
func (d *Database) LoadOne(target interface{}, filters ...interface{}) error {
func (d *Database) LoadOne(target interface{}, filters ...interface{}) (err error) {
composed := query.Compose(append(filters, query.Limit(1))...)
hasPreloader, _ := composed.Preloader()
ctx := query.WithContext(d.Context(), composed)
var result *mongox.Cursor
var err error
if hasPreloader {
result, err = d.createAggregateLoad(target, composed)
@@ -26,7 +26,9 @@ func (d *Database) LoadOne(target interface{}, filters ...interface{}) error {
return fmt.Errorf("can't create find result: %w", err)
}
hasNext := result.Next(d.Context())
defer composed.OnClose().Invoke(ctx, target)
hasNext := result.Next(ctx)
if result.Err() != nil {
return err
}
@@ -36,5 +38,15 @@ func (d *Database) LoadOne(target interface{}, filters ...interface{}) error {
base.Reset(target)
return result.Decode(target)
err = result.Decode(target)
if err != nil {
return
}
err = composed.OnDecode().Invoke(ctx, target)
if err != nil {
return
}
return
}
+6 -5
View File
@@ -8,13 +8,13 @@ import (
)
// LoadStream function loads documents one by one into a target channel
func (d *Database) LoadStream(target interface{}, filters ...interface{}) (mongox.StreamLoader, error) {
func (d *Database) LoadStream(target interface{}, filters ...interface{}) (loader mongox.StreamLoader, err error) {
var cursor *mongox.Cursor
var err error
composed := query.Compose(filters...)
hasPreloader, _ := composed.Preloader()
ctx := query.WithContext(d.Context(), composed)
if hasPreloader {
cursor, err = d.createAggregateLoad(target, composed)
@@ -22,10 +22,11 @@ func (d *Database) LoadStream(target interface{}, filters ...interface{}) (mongo
cursor, err = d.createSimpleLoad(target, composed)
}
if err != nil {
return nil, fmt.Errorf("can't create find result: %w", err)
err = fmt.Errorf("can't create find result: %w", err)
return
}
l := &StreamLoader{cur: cursor, ctx: d.Context(), target: target}
loader = &StreamLoader{cur: cursor, ctx: ctx, target: target, query: composed}
return l, nil
return
}
+19 -4
View File
@@ -12,13 +12,16 @@ import (
)
// SaveOne saves a single source document to the database
func (d *Database) SaveOne(source interface{}) error {
func (d *Database) SaveOne(source interface{}, filters ...interface{}) (err error) {
collection := d.GetCollectionOf(source)
opts := options.FindOneAndReplace()
id := base.GetID(source)
protected := base.GetProtection(source)
composed := query.Compose(bson.M{"_id": id})
composed := query.Compose(filters...)
ctx := query.WithContext(d.Context(), composed)
composed.And(bson.M{"_id": id})
opts.SetUpsert(true)
opts.SetReturnDocument(options.After)
@@ -29,10 +32,22 @@ func (d *Database) SaveOne(source interface{}) error {
protected.V = time.Now().Unix()
}
result := collection.FindOneAndReplace(d.Context(), composed.M(), source, opts)
defer composed.OnClose().Invoke(ctx, source)
result := collection.FindOneAndReplace(ctx, composed.M(), source, opts)
if result.Err() != nil {
return result.Err()
}
return result.Decode(source)
err = result.Decode(source)
if err != nil {
return
}
err = composed.OnDecode().Invoke(ctx, source)
if err != nil {
return
}
return
}
+32 -28
View File
@@ -2,78 +2,82 @@ package database
import (
"context"
"fmt"
"github.com/mainnika/mongox-go-driver/v2/mongox"
"github.com/mainnika/mongox-go-driver/v2/mongox/base"
"github.com/mainnika/mongox-go-driver/v2/mongox/query"
)
// StreamLoader is a controller for a database cursor
type StreamLoader struct {
cur *mongox.Cursor
query *query.Query
ctx context.Context
target interface{}
}
// DecodeNext loads next documents to a target or returns an error
func (l *StreamLoader) DecodeNext() error {
func (l *StreamLoader) DecodeNext() (err error) {
hasNext := l.cur.Next(l.ctx)
if l.cur.Err() != nil {
return l.cur.Err()
}
if !hasNext {
return mongox.ErrNoDocuments
}
base.Reset(l.target)
err := l.cur.Decode(l.target)
err = l.Next()
if err != nil {
return fmt.Errorf("can't decode desult: %w", err)
return
}
return nil
err = l.Decode()
if err != nil {
return
}
return
}
// Decode function decodes the current cursor document into the target
func (l *StreamLoader) Decode() error {
func (l *StreamLoader) Decode() (err error) {
base.Reset(l.target)
err := l.cur.Decode(l.target)
err = l.cur.Decode(l.target)
if err != nil {
return fmt.Errorf("can't decode desult: %w", err)
return
}
return nil
err = l.query.OnDecode().Invoke(l.ctx, l.target)
if err != nil {
return
}
return
}
// Next loads next documents but doesn't perform decoding
func (l *StreamLoader) Next() error {
func (l *StreamLoader) Next() (err error) {
hasNext := l.cur.Next(l.ctx)
err = l.cur.Err()
if l.cur.Err() != nil {
return l.cur.Err()
if err != nil {
return
}
if !hasNext {
return mongox.ErrNoDocuments
err = mongox.ErrNoDocuments
}
return nil
return
}
func (l *StreamLoader) Cursor() *mongox.Cursor {
func (l *StreamLoader) Cursor() (cursor *mongox.Cursor) {
return l.cur
}
// Close cursor
func (l *StreamLoader) Close() error {
func (l *StreamLoader) Close() (err error) {
_ = l.query.OnClose().Invoke(l.ctx, l.target)
return l.cur.Close(l.ctx)
}
func (l *StreamLoader) Err() error {
func (l *StreamLoader) Err() (err error) {
return l.cur.Err()
}
+23 -23
View File
@@ -16,51 +16,51 @@ type (
// Database is the mongox database interface
type Database interface {
Client() *Client
Context() context.Context
Name() string
New(ctx context.Context) Database
GetCollectionOf(document interface{}) *Collection
Count(target interface{}, filters ...interface{}) (int64, error)
DeleteArray(target interface{}) error
DeleteOne(target interface{}, filters ...interface{}) error
LoadArray(target interface{}, filters ...interface{}) error
LoadOne(target interface{}, filters ...interface{}) error
LoadStream(target interface{}, filters ...interface{}) (StreamLoader, error)
SaveOne(source interface{}) error
IndexEnsure(cfg interface{}, document interface{}) error
Client() (client *Client)
Context() (context context.Context)
Name() (name string)
New(ctx context.Context) (db Database)
GetCollectionOf(document interface{}) (collection *Collection)
Count(target interface{}, filters ...interface{}) (count int64, err error)
DeleteArray(target interface{}, filters ...interface{}) (err error)
DeleteOne(target interface{}, filters ...interface{}) (err error)
LoadArray(target interface{}, filters ...interface{}) (err error)
LoadOne(target interface{}, filters ...interface{}) (err error)
LoadStream(target interface{}, filters ...interface{}) (loader StreamLoader, err error)
SaveOne(source interface{}, filters ...interface{}) (err error)
IndexEnsure(cfg interface{}, document interface{}) (err error)
}
// StreamLoader is a interface to control database cursor
type StreamLoader interface {
Cursor() *Cursor
DecodeNext() error
Decode() error
Next() error
Close() error
Err() error
Cursor() (cursor *Cursor)
DecodeNext() (err error)
Decode() (err error)
Next() (err error)
Close() (err error)
Err() (err error)
}
// OIDBased is an interface for documents that have objectId type for the _id field
type OIDBased interface {
GetID() primitive.ObjectID
GetID() (id primitive.ObjectID)
SetID(id primitive.ObjectID)
}
// StringBased is an interface for documents that have string type for the _id field
type StringBased interface {
GetID() string
GetID() (id string)
SetID(id string)
}
// JSONBased is an interface for documents that have object type for the _id field
type JSONBased interface {
GetID() primitive.D
GetID() (id primitive.D)
SetID(id primitive.D)
}
// InterfaceBased is an interface for documents that have custom declated type for the _id field
type InterfaceBased interface {
GetID() interface{}
GetID() (id interface{})
SetID(id interface{})
}
+26
View File
@@ -0,0 +1,26 @@
package query
import (
"context"
)
type Callback func(ctx context.Context, iter interface{}) (err error)
type Callbacks []Callback
type (
OnDecode Callback
OnClose Callback
)
// Invoke callbacks sequence
func (c Callbacks) Invoke(ctx context.Context, iter interface{}) (err error) {
for _, cb := range c {
err = cb(ctx, iter)
if err != nil {
return
}
}
return
}
+31 -14
View File
@@ -3,47 +3,48 @@ package query
import (
"fmt"
"github.com/modern-go/reflect2"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"github.com/mainnika/mongox-go-driver/v2/mongox/base/protection"
"github.com/mainnika/mongox-go-driver/v2/mongox/utils"
)
// Compose is a function to compose filters into a single query
func Compose(filters ...interface{}) *Query {
func Compose(filters ...interface{}) (query *Query) {
q := &Query{}
query = &Query{}
for _, f := range filters {
if !Push(q, f) {
if !Push(query, f) {
panic(fmt.Errorf("unknown filter %v", f))
}
}
return q
return
}
// Push applies single filter to a query
func Push(q *Query, f interface{}) bool {
func Push(q *Query, f interface{}) (ok bool) {
if utils.IsNil(f) {
if reflect2.IsNil(f) {
return true
}
ok := false
ok = false
ok = ok || applyBson(q, f)
ok = ok || applyLimit(q, f)
ok = ok || applySort(q, f)
ok = ok || applySkip(q, f)
ok = ok || applyProtection(q, f)
ok = ok || applyPreloader(q, f)
ok = ok || applyCallbacks(q, f)
return ok
}
// applyBson is a fallback for a custom bson.M
func applyBson(q *Query, f interface{}) bool {
func applyBson(q *Query, f interface{}) (ok bool) {
if f, ok := f.(bson.M); ok {
q.And(f)
@@ -54,7 +55,7 @@ func applyBson(q *Query, f interface{}) bool {
}
// applyLimits extends query with a limiter
func applyLimit(q *Query, f interface{}) bool {
func applyLimit(q *Query, f interface{}) (ok bool) {
if f, ok := f.(Limiter); ok {
q.limiter = f
@@ -65,7 +66,7 @@ func applyLimit(q *Query, f interface{}) bool {
}
// applySort extends query with a sort rule
func applySort(q *Query, f interface{}) bool {
func applySort(q *Query, f interface{}) (ok bool) {
if f, ok := f.(Sorter); ok {
q.sorter = f
@@ -76,7 +77,7 @@ func applySort(q *Query, f interface{}) bool {
}
// applySkip extends query with a skip number
func applySkip(q *Query, f interface{}) bool {
func applySkip(q *Query, f interface{}) (ok bool) {
if f, ok := f.(Skipper); ok {
q.skipper = f
@@ -86,7 +87,7 @@ func applySkip(q *Query, f interface{}) bool {
return false
}
func applyProtection(q *Query, f interface{}) bool {
func applyProtection(q *Query, f interface{}) (ok bool) {
var x *primitive.ObjectID
var v *int64
@@ -114,7 +115,7 @@ func applyProtection(q *Query, f interface{}) bool {
return true
}
func applyPreloader(q *Query, f interface{}) bool {
func applyPreloader(q *Query, f interface{}) (ok bool) {
if f, ok := f.(Preloader); ok {
q.preloader = f
@@ -123,3 +124,19 @@ func applyPreloader(q *Query, f interface{}) bool {
return false
}
func applyCallbacks(q *Query, f interface{}) (ok bool) {
switch cb := f.(type) {
case OnDecode:
q.ondecode = append(q.ondecode, Callback(cb))
case OnClose:
q.onclose = append(q.onclose, Callback(cb))
default:
return
}
ok = true
return
}
+19
View File
@@ -0,0 +1,19 @@
package query
import (
"context"
)
type ctxQueryKey struct{}
// GetFromContext function extracts the request data from context
func GetFromContext(ctx context.Context) (q *Query, ok bool) {
q, ok = ctx.Value(ctxQueryKey{}).(*Query)
return
}
// WithContext function creates the new context with request data
func WithContext(ctx context.Context, q *Query) (withQuery context.Context) {
withQuery = context.WithValue(ctx, ctxQueryKey{}, q)
return
}
+8 -6
View File
@@ -2,7 +2,7 @@ package query
// Limiter is a filter to limit the result
type Limiter interface {
Limit() *int64
Limit() (limit *int64)
}
// Limit is a simple implementation of the Limiter filter
@@ -11,12 +11,14 @@ type Limit int64
var _ Limiter = Limit(0)
// Limit returns a limit
func (l Limit) Limit() *int64 {
func (l Limit) Limit() (limit *int64) {
lim := int64(l)
if lim <= 0 {
return nil
if l <= 0 {
return
}
return &lim
limit = new(int64)
*limit = int64(l)
return
}
+2 -2
View File
@@ -2,7 +2,7 @@ package query
// Preloader is a filter to skip the result
type Preloader interface {
Preload() []string
Preload() (preloads []string)
}
// Preload is a simple implementation of the Skipper filter
@@ -11,6 +11,6 @@ type Preload []string
var _ Preloader = Preload{}
// Preload returns a preload list
func (l Preload) Preload() []string {
func (l Preload) Preload() (preloads []string) {
return l
}
+24 -22
View File
@@ -2,8 +2,6 @@ package query
import (
"go.mongodb.org/mongo-driver/bson"
"reflect"
)
// Query is an enchanched bson.M map
@@ -13,10 +11,12 @@ type Query struct {
sorter Sorter
skipper Skipper
preloader Preloader
ondecode Callbacks
onclose Callbacks
}
// And function pushes the elem query to the $and array of the query
func (q *Query) And(elem bson.M) *Query {
func (q *Query) And(elem bson.M) (query *Query) {
if q.m == nil {
q.m = bson.M{}
@@ -35,61 +35,63 @@ func (q *Query) And(elem bson.M) *Query {
}
// Limiter returns limiter value or nil
func (q *Query) Limiter() *int64 {
func (q *Query) Limiter() (limit *int64) {
if q.limiter == nil {
return nil
return
}
return q.limiter.Limit()
}
// Sorter is a sort rule for a query
func (q *Query) Sorter() interface{} {
func (q *Query) Sorter() (sort interface{}) {
if q.sorter == nil {
return nil
return
}
return q.sorter.Sort()
}
// Skipper is a skipper for a query
func (q *Query) Skipper() *int64 {
func (q *Query) Skipper() (skip *int64) {
if q.skipper == nil {
return nil
return
}
return q.skipper.Skip()
}
// Preloader is a preloader list for a query
func (q *Query) Preloader() (empty bool, preloader []string) {
func (q *Query) Preloader() (ok bool, preloads []string) {
if q.preloader == nil {
return false, nil
}
preloader = q.preloader.Preload()
preloads = q.preloader.Preload()
ok = len(preloads) > 0
if len(preloader) == 0 {
return false, nil
}
return
}
return true, preloader
// OnDecode callback is called after the mongo decode function
func (q *Query) OnDecode() (callbacks Callbacks) {
return q.ondecode
}
func (q *Query) OnClose() (callbacks Callbacks) {
return q.onclose
}
// Empty checks the query for any content
func (q *Query) Empty() bool {
qv := reflect.ValueOf(q.m)
keys := qv.MapKeys()
return len(keys) == 0
func (q *Query) Empty() (isEmpty bool) {
return len(q.m) == 0
}
// M returns underlying query map
func (q *Query) M() bson.M {
func (q *Query) M() (m bson.M) {
return q.m
}
+8 -6
View File
@@ -2,7 +2,7 @@ package query
// Skipper is a filter to skip the result
type Skipper interface {
Skip() *int64
Skip() (skip *int64)
}
// Skip is a simple implementation of the Skipper filter
@@ -11,12 +11,14 @@ type Skip int64
var _ Skipper = Skip(0)
// Skip returns a skip number
func (l Skip) Skip() *int64 {
func (l Skip) Skip() (skip *int64) {
lim := int64(l)
if lim <= 0 {
return nil
if l <= 0 {
return
}
return &lim
skip = new(int64)
*skip = int64(l)
return
}
+2 -2
View File
@@ -6,7 +6,7 @@ import (
// Sorter is a filter to sort the data before query
type Sorter interface {
Sort() bson.M
Sort() (sort bson.M)
}
// Sort is a simple implementations of the Sorter filter
@@ -15,6 +15,6 @@ type Sort bson.M
var _ Sorter = &Sort{}
// Sort returns a slice of fields which have to be sorted
func (f Sort) Sort() bson.M {
func (f Sort) Sort() (sort bson.M) {
return bson.M(f)
}
-21
View File
@@ -1,21 +0,0 @@
package utils
import (
"unsafe"
)
// IsNil function evaluates the interface value to nil
func IsNil(i interface{}) bool {
type iface struct {
_ *interface{}
ptr unsafe.Pointer
}
unpacked := (*iface)(unsafe.Pointer(&i))
if unpacked.ptr == nil {
return true
}
return *(*unsafe.Pointer)(unpacked.ptr) == nil
}
-32
View File
@@ -1,32 +0,0 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsNil(t *testing.T) {
testvalues := []struct {
i interface{}
isnil bool
}{
{nil, true},
{(*string)(nil), true},
{([]string)(nil), true},
{(map[string]string)(nil), true},
{(func() bool)(nil), true},
{(chan func() bool)(nil), true},
{"", true},
{0, true},
{append(([]string)(nil), ""), false},
{[]string{}, false},
{1, false},
{"1", false},
}
for _, tt := range testvalues {
assert.Equal(t, tt.isnil, IsNil(tt.i))
}
}