From 91ef568a64d3648fd19e45760386ac782adfb773 Mon Sep 17 00:00:00 2001 From: Sebastian Frank Date: Tue, 20 Mar 2018 14:16:01 +0100 Subject: [PATCH] Initial commit --- crud.go | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++ interface.go | 65 ++++++++++++++++++++ lookup.go | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++ setup.go | 108 ++++++++++++++++++++++++++++++++ validator.go | 28 +++++++++ 5 files changed, 538 insertions(+) create mode 100644 crud.go create mode 100644 interface.go create mode 100644 lookup.go create mode 100644 setup.go create mode 100644 validator.go diff --git a/crud.go b/crud.go new file mode 100644 index 0000000..039bca5 --- /dev/null +++ b/crud.go @@ -0,0 +1,168 @@ +package mgocrud + +import ( + "fmt" + "reflect" + "strings" + "time" + + "github.com/davecgh/go-spew/spew" + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// CreateDocument creates a document from specified model +func CreateDocument(db *mgo.Database, m ModelInterface) error { + m.PrepareInsert() + + c := db.C(GetCollectionName(m)) + err := c.Insert(m) + + return err +} + +// ReadDocument gets one document via its id +func ReadDocument(db *mgo.Database, m ModelInterface, selector bson.M) error { + c := db.C(GetCollectionName(m)) + + q := c.FindId(m.GetID()) + if selector != nil { + q = q.Select(selector) + } + err := q.One(m) + + return err +} + +// PipelineModifierFunction is a function to modify mongodb query +type PipelineModifierFunction func(pipeline []bson.M) []bson.M + +// ReadCollection gets the filtered collection of the model +func ReadCollection(db *mgo.Database, results interface{}, filter bson.M, selector bson.M, offset int, limit int, sort []string, pipelineModifier PipelineModifierFunction) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + + // get pointer to model (element of slice in results) to get collection name + m := reflect.New(reflect.TypeOf( + reflect.Indirect(reflect.ValueOf(results)).Interface(), // get indirection of slice pointer + ).Elem()).Interface().(ModelInterface) // it must be a ModelInterface here + + c := db.C(GetCollectionName(m)) + + if pipelineModifier != nil { + // search via pipeline + + pipeline := []bson.M{} + + if filter != nil { + pipeline = append(pipeline, bson.M{ + "$match": filter, + }) + } + + if len(sort) > 0 { + sortM := bson.M{} + for _, s := range sort { + if strings.HasPrefix(s, "-") { + s = s[1:] + sortM[s] = -1 + } else { + sortM[s] = 1 + } + } + spew.Dump(sortM) + pipeline = append(pipeline, bson.M{ + "$sort": sortM, + }) + } + + if offset > 0 { + pipeline = append(pipeline, bson.M{ + "$skip": offset, + }) + } + if limit > 0 { + pipeline = append(pipeline, bson.M{ + "$limit": limit, + }) + } + if selector != nil { + pipeline = append(pipeline, bson.M{ + "$project": selector, + }) + } + + if pipelineModifier != nil { + pipeline = pipelineModifier(pipeline) + } + + q := c.Pipe(pipeline).AllowDiskUse().Iter() + err = q.All(results) + } else { + // search without pipe is faster + + q := c.Find(filter) + if selector != nil { + q = q.Select(selector) + } + if len(sort) > 0 { + q = q.Sort(sort...) + } + if offset > 0 { + q = q.Skip(offset) + } + if limit > 0 { + q = q.Limit(limit) + } + + err = q.All(results) + } + + return err +} + +// ReadCollectionCount gets the count of elements in filtered collection +func ReadCollectionCount(db *mgo.Database, m ModelInterface, filter bson.M) (count int, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + + c := db.C(GetCollectionName(m)) + return c.Find(filter).Count() +} + +// UpdateDocument updates a document from specified model +func UpdateDocument(db *mgo.Database, m ModelInterface, changes bson.M) error { + m.PrepareUpdate() + changes["updateTime"] = time.Now() + + c := db.C(GetCollectionName(m)) + err := c.UpdateId(m.GetID(), bson.M{"$set": changes}) + + return err +} + +// UpsertDocument updates a document from specified model or inserts it, of not found +func UpsertDocument(db *mgo.Database, m ModelInterface, changes bson.M) error { + m.PrepareUpdate() + changes["updateTime"] = time.Now() + + c := db.C(GetCollectionName(m)) + _, err := c.Upsert(m, bson.M{"$set": changes}) + + return err +} + +// DeleteDocument deletes one document via its id +func DeleteDocument(db *mgo.Database, m ModelInterface) error { + c := db.C(GetCollectionName(m)) + + err := c.RemoveId(m.GetID()) + + return err +} diff --git a/interface.go b/interface.go new file mode 100644 index 0000000..593777b --- /dev/null +++ b/interface.go @@ -0,0 +1,65 @@ +package mgocrud + +import ( + "reflect" + "strings" + "time" + + "gopkg.in/mgo.v2/bson" +) + +// ModelInterface is interface for mgo crud operations +type ModelInterface interface { + PrepareInsert() + PrepareUpdate() + GetID() *bson.ObjectId + SetID(id *bson.ObjectId) + GetIdentSelector() (selector bson.M) + SetIdent() +} + +// Model is model with default fields +type Model struct { + ID *bson.ObjectId `json:"id,omitempty" bson:"_id" mapstructure:"id"` + InsertTime *time.Time `json:"insertTime,omitempty" bson:"insertTime,omitempty" mapstructure:"Datum"` + UpdateTime *time.Time `json:"updateTime,omitempty" bson:"updateTime,omitempty" mapstructure:"Datum"` + Ident []string `json:"ident,omitempty" bson:"-" mapstructure:"-"` +} + +// GetCollectionName gets name from type +func GetCollectionName(m ModelInterface) string { + if i, ok := m.(interface { + GetCollectionName() string + }); ok { + return i.GetCollectionName() + } + return strings.ToLower( + reflect.TypeOf( + reflect.Indirect(reflect.ValueOf(m)).Interface(), + ).Name()) +} + +// PrepareInsert creates new bson id and insert time +func (m *Model) PrepareInsert() { + id := bson.NewObjectId() + m.ID = &id + now := time.Now() + m.InsertTime = &now + m.UpdateTime = &now +} + +// PrepareUpdate updates UpdateTime +func (m *Model) PrepareUpdate() { + now := time.Now() + m.UpdateTime = &now +} + +// GetID gets object id +func (m *Model) GetID() *bson.ObjectId { + return m.ID +} + +// SetID sets object id +func (m *Model) SetID(id *bson.ObjectId) { + m.ID = id +} diff --git a/lookup.go b/lookup.go new file mode 100644 index 0000000..dfc5f4d --- /dev/null +++ b/lookup.go @@ -0,0 +1,169 @@ +package mgocrud + +import ( + "fmt" + "reflect" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// Lookup extends results with data for inline structs +func Lookup(db *mgo.Database, structField string, results interface{}, selector bson.M) error { + + t := reflect.TypeOf(results) + v := reflect.ValueOf(results) + if t.Kind() == reflect.Ptr { + t = t.Elem() + v = v.Elem() + } + + isSlice := false + elIsPtr := false + if t.Kind() == reflect.Slice { + t = t.Elem() + if t.Kind() == reflect.Ptr { + t = t.Elem() + elIsPtr = true + } + isSlice = true + } + + if t.Kind() != reflect.Struct { + return fmt.Errorf("%+v is not a struct", t) + } + + field, found := t.FieldByName(structField) + if !found { + return fmt.Errorf("struct field %s not found in struct %+v", structField, t) + } + fieldType := field.Type + fieldIsPtr := false + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + fieldIsPtr = true + } + + fieldID, found := t.FieldByName(structField + "ID") + if !found { + return fmt.Errorf("struct field %s not found in struct %+v", structField+"ID", t) + } + idType := fieldID.Type + idIsPtr := false + if idType.Kind() == reflect.Ptr { + idType = idType.Elem() + idIsPtr = true + } + + if idType != reflect.TypeOf(bson.ObjectId("")) { + return fmt.Errorf("field %s in struct %+v is not bson.ObjectId", structField+"ID", t) + } + + objectIDs := make(map[bson.ObjectId]interface{}) + + // make slice for single elements to use generic code + if !isSlice { + tmpSlice := []interface{}{ + results, + } + v = reflect.ValueOf(tmpSlice) + if reflect.TypeOf(results).Kind() == reflect.Ptr { + elIsPtr = true + } + } + + l := v.Len() + for i := 0; i < l; i++ { + elV := v.Index(i) + if elIsPtr { + elV = elV.Elem() + } + if elV.Kind() == reflect.Ptr { + // needed if tmpSlice -> dont know why double ptr resolv + elV = elV.Elem() + } + lookupID := elV.FieldByName(fieldID.Name) + if idIsPtr { + lookupID = lookupID.Elem() + } + objectIDs[lookupID.Interface().(bson.ObjectId)] = nil + } + + lArr := len(objectIDs) + if lArr <= 0 { + // no entries to map + return nil + } + sArr := make([]bson.M, lArr, lArr) + aI := 0 + for sID := range objectIDs { + sArr[aI] = bson.M{ + "_id": sID, + } + aI++ + } + sQuery := bson.M{ + "$or": sArr, + } + + slice := reflect.MakeSlice(reflect.SliceOf(fieldType), 0, 0) + x := reflect.New(slice.Type()) + x.Elem().Set(slice) + objectResults := x.Interface() + objectSlice := x.Elem() + + objectIDIsPtr := false + objectIDField, found := fieldType.FieldByName("ID") + objectIDType := objectIDField.Type + if found && objectIDType.Kind() == reflect.Ptr { + objectIDType = objectIDType.Elem() + objectIDIsPtr = true + } + if !found || objectIDType != reflect.TypeOf(bson.ObjectId("")) { + return fmt.Errorf("ID type in objects struct %+v is not bson.ObjectId", fieldType) + } + + err := ReadCollection(db, objectResults, sQuery, selector, 0, 0, nil, nil) + if err != nil { + panic(err) + } + + // map IDs to object for better resolving + oLen := objectSlice.Len() + for i := 0; i < oLen; i++ { + oID := objectSlice.Index(i).FieldByName("ID") + if objectIDIsPtr { + oID = oID.Elem() + } + objectIDs[oID.Interface().(bson.ObjectId)] = objectSlice.Index(i).Addr().Interface() + } + + for i := 0; i < l; i++ { + elV := v.Index(i) + if elIsPtr { + elV = elV.Elem() + } + if elV.Kind() == reflect.Ptr { + // needed if tmpSlice -> dont know why double ptr resolv + elV = elV.Elem() + } + + oID := elV.FieldByName(structField + "ID") + if idIsPtr { + oID = oID.Elem() + } + objectID := oID.Interface().(bson.ObjectId) + object := objectIDs[objectID] + + field := elV.FieldByName(structField) + if fieldIsPtr { + field.Set(reflect.ValueOf(object)) + } else { + field.Set(reflect.ValueOf(object).Elem()) + } + + } + + return nil + +} diff --git a/setup.go b/setup.go new file mode 100644 index 0000000..2a58a4a --- /dev/null +++ b/setup.go @@ -0,0 +1,108 @@ +package mgocrud + +import ( + "fmt" + "reflect" + "strings" + + mgo "gopkg.in/mgo.v2" +) + +// EnsureIndex ensured mongodb index reflecting model struct index tag +func EnsureIndex(db *mgo.Database, m ModelInterface) error { + col := db.C(GetCollectionName(m)) + + mType := reflect.TypeOf(m) + + textFields := []string{} + + var _indexWalk func(t reflect.Type, fieldbase string) error + _indexWalk = func(t reflect.Type, fieldbase string) error { + + for i := 0; i < 2; i++ { + if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice { + t = t.Elem() + } + } + + if t.Kind() == reflect.Struct { + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + indexTag := field.Tag.Get("index") + + bsonField := strings.Split(field.Tag.Get("bson"), ",")[0] + if bsonField == "" { + bsonField = strings.ToLower(field.Name) + } + + if bsonField != "-" { + if indexTag != "" { + index := mgo.Index{ + Background: false, + Sparse: false, + Unique: false, + } + for _, indexEl := range strings.Split(indexTag, ",") { + switch { + case indexEl == "single": + index.Key = []string{fieldbase + bsonField} + case indexEl == "unique": + index.Unique = true + case indexEl == "sparse": + index.Sparse = true + case indexEl == "background": + index.Background = true + case indexEl == "text": + textFields = append(textFields, "$text:"+fieldbase+bsonField) + default: + return fmt.Errorf("invalid index tag for field %s in model %+v", bsonField, t) + } + + } + if len(index.Key) > 0 { + // fmt.Println(bsonField, index) + fmt.Printf("ensure index on collection %s for field %s\n", GetCollectionName(m), bsonField) + err := col.EnsureIndex(index) + if err != nil { + return err + } + } + } + + fBase := bsonField + "." + if field.Anonymous { + fBase = "" + } + err := _indexWalk(field.Type, fBase) + if err != nil { + return err + } + } + } + } + + return nil + } + + err := _indexWalk(mType, "") + if err != nil { + return err + } + + if len(textFields) > 0 { + // fmt.Println("$text", textFields) + fmt.Printf("ensure text index on collection %s for fields %v\n", GetCollectionName(m), textFields) + err := col.EnsureIndex(mgo.Index{ + Name: "textindex", + Key: textFields, + DefaultLanguage: "german", + Background: false, + }) + if err != nil { + return err + } + } + + return nil +} diff --git a/validator.go b/validator.go new file mode 100644 index 0000000..174cc7c --- /dev/null +++ b/validator.go @@ -0,0 +1,28 @@ +package mgocrud + +import ( + validator "gopkg.in/go-playground/validator.v8" + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// ValidateObject validates object via validator tag and custom method +func ValidateObject(db *mgo.Database, m ModelInterface, changes bson.M) error { + // first validate via struct tag + validator := validator.New(&validator.Config{ + TagName: "validator", + }) + err := validator.Struct(m) + if err != nil { + return err + } + + // next execute custom model validator if exists + if i, ok := m.(interface { + Validate(db *mgo.Database, changes bson.M) error + }); ok { + return i.Validate(db, changes) + } + + return nil +}