Initial commit

This commit is contained in:
Sebastian Frank 2018-03-20 14:16:01 +01:00
commit 91ef568a64
5 changed files with 538 additions and 0 deletions

168
crud.go Normal file
View File

@ -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
}

65
interface.go Normal file
View File

@ -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
}

169
lookup.go Normal file
View File

@ -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
}

108
setup.go Normal file
View File

@ -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
}

28
validator.go Normal file
View File

@ -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
}