diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..dd4c951 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml new file mode 100644 index 0000000..062a2a4 --- /dev/null +++ b/.idea/material_theme_project_new.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..67ee188 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0d8a8dd --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/orm.iml b/.idea/orm.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/orm.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9092697 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "nuxt.isNuxtApp": false +} \ No newline at end of file diff --git a/indexes.go b/indexes.go index 344efcc..8443354 100644 --- a/indexes.go +++ b/indexes.go @@ -79,12 +79,12 @@ func scanIndex(src string) []InternalIndex { switch tok { case token.LBRACE: if lb { - goto panik + goto _panik } lb = true case token.RBRACE: if !lb || len(p.Fields) == 0 { - goto panik + goto _panik } lb = false case token.IDENT: @@ -100,36 +100,36 @@ func scanIndex(src string) []InternalIndex { p.appendOption(lit) case token.PERIOD: if p.getSticky() { - goto panik + goto _panik } p.setSticky(true) p.updateLastField() case token.COMMA: case token.COLON: if lb { - goto panik + goto _panik } case token.SEMICOLON: if lb { - goto panik + goto _panik } parsed = append(parsed, *p) p = &InternalIndex{} case token.EOF: if lb { - goto panik + goto _panik } return parsed default: - goto panik + goto _panik } } -panik: +_panik: panic("parsing error in index expression!") } -func BuildIndex(i InternalIndex) *mongo.IndexModel { +func buildIndex(i InternalIndex) *mongo.IndexModel { idx := &mongo.IndexModel{ Keys: i.Fields, } diff --git a/model.go b/model.go index 3b14e6f..3a28c95 100644 --- a/model.go +++ b/model.go @@ -19,20 +19,22 @@ type Model struct { Created time.Time `bson:"createdAt" json:"createdAt"` // Modified time. updated/added automatically. Modified time.Time `bson:"updatedAt" json:"updatedAt"` - typeName string `bson:"-" json:"-"` - self any `bson:"-" json:"-"` + typeName string `bson:"-"` + self any `bson:"-"` exists bool `bson:"-"` } -// HasID is a simple interface that you must implement. -// This allows for more flexibility if your ID isn't an -// ObjectID (e.g., int, uint, string...). +// HasID is a simple interface that you must implement +// in your models, using a pointer receiver. +// This allows for more flexibility in cases where +// your ID isn't an ObjectID (e.g., int, uint, string...). // // and yes, those darn ugly ObjectIDs are supported :) type HasID interface { Id() any SetId(id any) } +type HasIDSlice []HasID type IModel interface { Find(query interface{}, opts ...*options.FindOptions) (*mongo.Cursor, error) @@ -46,12 +48,21 @@ type IModel interface { Save() error serializeToStore() primitive.M setTypeName(str string) + getExists() bool + setExists(n bool) } func (m *Model) setTypeName(str string) { m.typeName = str } +func (m *Model) getExists() bool { + return m.exists +} +func (m *Model) setExists(n bool) { + m.exists = n +} + func (m *Model) getColl() *mongo.Collection { _, ri, ok := ModelRegistry.HasByName(m.typeName) if !ok { @@ -61,11 +72,11 @@ func (m *Model) getColl() *mongo.Collection { } func (m *Model) getIdxs() []*mongo.IndexModel { - mi := []*mongo.IndexModel{} + mi := make([]*mongo.IndexModel, 0) if mpi := m.getParsedIdxs(); mpi != nil { for _, v := range mpi { for _, i := range v { - mi = append(mi, BuildIndex(i)) + mi = append(mi, buildIndex(i)) } } return mi @@ -116,15 +127,17 @@ func (m *Model) FindAll(query interface{}, opts ...*options.FindOptions) (*Query return qq, err } -func (m *Model) FindPaged(query interface{}, page int64, perPage int64, options ...*options.FindOptions) (*Query, error) { +func (m *Model) FindPaged(query interface{}, page int64, perPage int64, opts ...*options.FindOptions) (*Query, error) { skipAmt := perPage * (page - 1) if skipAmt < 0 { skipAmt = 0 } - if len(options) > 0 { - options[0].SetSkip(skipAmt).SetLimit(perPage) + if len(opts) > 0 { + opts[0].SetSkip(skipAmt).SetLimit(perPage) + } else { + opts = append(opts, options.Find().SetSkip(skipAmt).SetLimit(perPage)) } - q, err := m.FindAll(query, options...) + q, err := m.FindAll(query, opts...) q.Op = OP_FIND_PAGED return q, err } @@ -133,12 +146,6 @@ func (m *Model) FindByID(id interface{}) (*Query, error) { return m.FindOne(bson.D{{"_id", id}}) } -// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 -// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 - -// ^ ^ -// ^ ^ - func (m *Model) FindOne(query interface{}, options ...*options.FindOneOptions) (*Query, error) { coll := m.getColl() rip := coll.FindOne(context.TODO(), query, options...) @@ -163,6 +170,62 @@ func (m *Model) FindOne(query interface{}, options ...*options.FindOneOptions) ( return qq, err } +func (m *Model) DeleteOne() error { + c := m.getColl() + if valueOf(m.self).Kind() == reflect.Slice { + } + id, ok := m.self.(HasID) + if !ok { + id2, ok2 := m.self.(HasIDSlice) + if !ok2 { + return fmt.Errorf("model '%s' is not registered", m.typeName) + } + _, err := c.DeleteOne(context.TODO(), bson.M{"_id": id2[0].Id()}) + return err + } else { + _, err := c.DeleteOne(context.TODO(), bson.M{"_id": id.Id()}) + return err + } +} + +// Append appends one or more items to `field`. +// will error if this Model contains a reference +// to multiple documents, or if `field` is not a +// slice. +func (m *Model) Append(field string, a ...interface{}) error { + rv := reflect.ValueOf(m.self) + ref := rv + rt := reflect.TypeOf(m.self) + if ref.Kind() == reflect.Pointer { + ref = ref.Elem() + rt = rt.Elem() + } + if ref.Kind() == reflect.Slice { + return fmt.Errorf("Cannot append to multiple documents!") + } + if ref.Kind() != reflect.Struct { + return fmt.Errorf("Current object is not a struct!") + } + _, ofv, err := getNested(field, ref) + if err != nil { + return err + } + oofv := makeSettable(*ofv, (*ofv).Interface()) + fv := oofv + if fv.Kind() == reflect.Pointer { + fv = fv.Elem() + } + if fv.Kind() != reflect.Slice { + return fmt.Errorf("Current object is not a slice!") + } + + for _, b := range a { + va := reflect.ValueOf(b) + fv.Set(reflect.Append(fv, va)) + } + return nil +} + func (m *Model) Save() error { var err error c := m.getColl() @@ -175,8 +238,8 @@ func (m *Model) Save() error { } var asHasId = vp.Interface().(HasID) (asHasId).Id() - isNew := reflect.ValueOf(asHasId.Id()).IsZero() || !m.exists - if isNew { + isNew := reflect.ValueOf(asHasId.Id()).IsZero() && !m.exists + if isNew || !m.exists { m.Created = now } m.Modified = now @@ -187,7 +250,7 @@ func (m *Model) Save() error { return err } } - if isNew { + if isNew || !m.exists { nid := getLastInColl(c.Name(), asHasId.Id()) switch pnid := nid.(type) { case uint: @@ -214,8 +277,10 @@ func (m *Model) Save() error { } m.self = asHasId - c.InsertOne(context.TODO(), m.serializeToStore()) - m.exists = true + _, err = c.InsertOne(context.TODO(), m.serializeToStore()) + if err == nil { + m.exists = true + } } else { _, err = c.ReplaceOne(context.TODO(), bson.D{{Key: "_id", Value: m.self.(HasID).Id()}}, m.serializeToStore()) } @@ -245,7 +310,7 @@ func serializeIDs(input interface{}) bson.M { for i := 0; i < mv.NumField(); i++ { fv := mv.Field(i) ft := mt.Field(i) - var dr reflect.Value = fv + var dr = fv tag, err := structtag.Parse(string(mt.Field(i).Tag)) panik(err) bbson, err := tag.Get("bson") @@ -331,8 +396,8 @@ func serializeIDSlice(input []interface{}) bson.A { return a } -// Create creates a new instance of a given model. -// returns a pointer to the newly created model. +// Create creates a new instance of a given model +// and returns a pointer to it. func Create(d any) any { var n string var ri *InternalModel diff --git a/model_test.go b/model_test.go index 142b4f2..328ff62 100644 --- a/model_test.go +++ b/model_test.go @@ -41,8 +41,6 @@ func TestPopulate(t *testing.T) { err := bandDoc.Save() assert.Equal(t, nil, err) storyDoc.Author = author - err = author.Save() - assert.Equal(t, nil, err) err = storyDoc.Save() assert.Equal(t, nil, err) assert.Greater(t, storyDoc.ID, int64(0)) @@ -50,7 +48,6 @@ func TestPopulate(t *testing.T) { smodel := Create(story{}).(*story) q, err := smodel.FindByID(storyDoc.ID) assert.Equal(t, nil, err) - //assert.Greater(t, smodel.ID, int64(0)) assert.NotPanics(t, func() { foundDoc := &story{} q.Populate("Author", "Chapters.Bands").Exec(foundDoc) @@ -107,3 +104,23 @@ func TestModel_PopulateMulti(t *testing.T) { query.Populate("Author", "Chapters.Bands").Exec(&final) assert.Greater(t, len(final), 0) } + +func TestModel_Append(t *testing.T) { + initTest() + bmodel := Create(band{}).(*band) + query, err := bmodel.FindByID(int64(1)) + assert.Equal(t, nil, err) + fin := &band{} + query.Exec(fin) + assert.Greater(t, fin.ID, int64(0)) + + err = bmodel.Append("Characters", "Robert Trujillo") + assert.Equal(t, nil, err) + err = bmodel.Save() + assert.Equal(t, nil, err) + fin = &band{} + query, _ = bmodel.FindByID(int64(1)) + query.Exec(fin) + assert.Greater(t, len(fin.Characters), 4) + +} diff --git a/query.go b/query.go index f4b1167..9a2b070 100644 --- a/query.go +++ b/query.go @@ -478,5 +478,6 @@ func (q *Query) Exec(result interface{}) { panic("Exec() has already been called!") } reflect.ValueOf(result).Elem().Set(reflect.ValueOf(q.doc).Elem()) + q.Model.self = q.doc q.done = true } diff --git a/util.go b/util.go index 2b70249..9b9804f 100644 --- a/util.go +++ b/util.go @@ -1,6 +1,10 @@ package orm -import "reflect" +import ( + "fmt" + "reflect" + "strings" +) func panik(err error) { if err != nil { @@ -68,3 +72,32 @@ func coerceInt(input reflect.Value, dst reflect.Value) interface{} { } return nil } + +func getNested(field string, value reflect.Value) (*reflect.Type, *reflect.Value, error) { + if strings.HasPrefix(field, ".") || strings.HasSuffix(field, ".") { + return nil, nil, fmt.Errorf("Malformed field name %s passed", field) + } + dots := strings.Split(field, ".") + if value.Kind() != reflect.Struct { + return nil, nil, fmt.Errorf("This value is not a struct!") + } + ref := value + if ref.Kind() == reflect.Pointer { + ref = ref.Elem() + } + fv := ref.FieldByName(dots[0]) + ft := fv.Type() + if len(dots) > 1 { + return getNested(strings.Join(dots[1:], "."), fv) + } else { + return &ft, &fv, nil + } +} +func makeSettable(rval reflect.Value, value interface{}) reflect.Value { + if !rval.CanSet() { + nv := reflect.New(rval.Type()) + nv.Elem().Set(reflect.ValueOf(value)) + return nv + } + return rval +}