add a new Append method to Model, handle misc errors, unexport some funcs + methods

This commit is contained in:
parent f46aab2f5f
commit 823b3b0d79
Signed by: tablet
GPG Key ID: 924A5F6AF051E87C
13 changed files with 226 additions and 36 deletions

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Default" />
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="1fe2f249:191aa6d861d:-7ffe" />
</MTProjectMetadataState>
</option>
<option name="titleBarState">
<MTProjectTitleBarConfigState>
<option name="overrideColor" value="false" />
</MTProjectTitleBarConfigState>
</option>
</component>
</project>

18
.idea/misc.xml Normal file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="excludedPrintfFunctions.xml">
<option name="excludedDescriptors">
<set>
<PrintfFuncDescriptorState>
<option name="importPath" value="fmt" />
<option name="name" value="Errorf" />
</PrintfFuncDescriptorState>
</set>
</option>
<option name="excludedNames">
<set>
<option value="Errorf" />
</set>
</option>
</component>
</project>

8
.idea/modules.xml Normal file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/orm.iml" filepath="$PROJECT_DIR$/.idea/orm.iml" />
</modules>
</component>
</project>

9
.idea/orm.iml Normal file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml Normal file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

3
.vscode/settings.json vendored Normal file

@ -0,0 +1,3 @@
{
"nuxt.isNuxtApp": false
}

@ -79,12 +79,12 @@ func scanIndex(src string) []InternalIndex {
switch tok { switch tok {
case token.LBRACE: case token.LBRACE:
if lb { if lb {
goto panik goto _panik
} }
lb = true lb = true
case token.RBRACE: case token.RBRACE:
if !lb || len(p.Fields) == 0 { if !lb || len(p.Fields) == 0 {
goto panik goto _panik
} }
lb = false lb = false
case token.IDENT: case token.IDENT:
@ -100,36 +100,36 @@ func scanIndex(src string) []InternalIndex {
p.appendOption(lit) p.appendOption(lit)
case token.PERIOD: case token.PERIOD:
if p.getSticky() { if p.getSticky() {
goto panik goto _panik
} }
p.setSticky(true) p.setSticky(true)
p.updateLastField() p.updateLastField()
case token.COMMA: case token.COMMA:
case token.COLON: case token.COLON:
if lb { if lb {
goto panik goto _panik
} }
case token.SEMICOLON: case token.SEMICOLON:
if lb { if lb {
goto panik goto _panik
} }
parsed = append(parsed, *p) parsed = append(parsed, *p)
p = &InternalIndex{} p = &InternalIndex{}
case token.EOF: case token.EOF:
if lb { if lb {
goto panik goto _panik
} }
return parsed return parsed
default: default:
goto panik goto _panik
} }
} }
panik: _panik:
panic("parsing error in index expression!") panic("parsing error in index expression!")
} }
func BuildIndex(i InternalIndex) *mongo.IndexModel { func buildIndex(i InternalIndex) *mongo.IndexModel {
idx := &mongo.IndexModel{ idx := &mongo.IndexModel{
Keys: i.Fields, Keys: i.Fields,
} }

114
model.go

@ -19,20 +19,22 @@ type Model struct {
Created time.Time `bson:"createdAt" json:"createdAt"` Created time.Time `bson:"createdAt" json:"createdAt"`
// Modified time. updated/added automatically. // Modified time. updated/added automatically.
Modified time.Time `bson:"updatedAt" json:"updatedAt"` Modified time.Time `bson:"updatedAt" json:"updatedAt"`
typeName string `bson:"-" json:"-"` typeName string `bson:"-"`
self any `bson:"-" json:"-"` self any `bson:"-"`
exists bool `bson:"-"` exists bool `bson:"-"`
} }
// HasID is a simple interface that you must implement. // HasID is a simple interface that you must implement
// This allows for more flexibility if your ID isn't an // in your models, using a pointer receiver.
// ObjectID (e.g., int, uint, string...). // 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 :) // and yes, those darn ugly ObjectIDs are supported :)
type HasID interface { type HasID interface {
Id() any Id() any
SetId(id any) SetId(id any)
} }
type HasIDSlice []HasID
type IModel interface { type IModel interface {
Find(query interface{}, opts ...*options.FindOptions) (*mongo.Cursor, error) Find(query interface{}, opts ...*options.FindOptions) (*mongo.Cursor, error)
@ -46,12 +48,21 @@ type IModel interface {
Save() error Save() error
serializeToStore() primitive.M serializeToStore() primitive.M
setTypeName(str string) setTypeName(str string)
getExists() bool
setExists(n bool)
} }
func (m *Model) setTypeName(str string) { func (m *Model) setTypeName(str string) {
m.typeName = str 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 { func (m *Model) getColl() *mongo.Collection {
_, ri, ok := ModelRegistry.HasByName(m.typeName) _, ri, ok := ModelRegistry.HasByName(m.typeName)
if !ok { if !ok {
@ -61,11 +72,11 @@ func (m *Model) getColl() *mongo.Collection {
} }
func (m *Model) getIdxs() []*mongo.IndexModel { func (m *Model) getIdxs() []*mongo.IndexModel {
mi := []*mongo.IndexModel{} mi := make([]*mongo.IndexModel, 0)
if mpi := m.getParsedIdxs(); mpi != nil { if mpi := m.getParsedIdxs(); mpi != nil {
for _, v := range mpi { for _, v := range mpi {
for _, i := range v { for _, i := range v {
mi = append(mi, BuildIndex(i)) mi = append(mi, buildIndex(i))
} }
} }
return mi return mi
@ -116,15 +127,17 @@ func (m *Model) FindAll(query interface{}, opts ...*options.FindOptions) (*Query
return qq, err 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) skipAmt := perPage * (page - 1)
if skipAmt < 0 { if skipAmt < 0 {
skipAmt = 0 skipAmt = 0
} }
if len(options) > 0 { if len(opts) > 0 {
options[0].SetSkip(skipAmt).SetLimit(perPage) 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 q.Op = OP_FIND_PAGED
return q, err return q, err
} }
@ -133,12 +146,6 @@ func (m *Model) FindByID(id interface{}) (*Query, error) {
return m.FindOne(bson.D{{"_id", id}}) 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) { func (m *Model) FindOne(query interface{}, options ...*options.FindOneOptions) (*Query, error) {
coll := m.getColl() coll := m.getColl()
rip := coll.FindOne(context.TODO(), query, options...) rip := coll.FindOne(context.TODO(), query, options...)
@ -163,6 +170,61 @@ func (m *Model) FindOne(query interface{}, options ...*options.FindOneOptions) (
return qq, err 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
}
_, 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 { func (m *Model) Save() error {
var err error var err error
c := m.getColl() c := m.getColl()
@ -175,8 +237,8 @@ func (m *Model) Save() error {
} }
var asHasId = vp.Interface().(HasID) var asHasId = vp.Interface().(HasID)
(asHasId).Id() (asHasId).Id()
isNew := reflect.ValueOf(asHasId.Id()).IsZero() || !m.exists isNew := reflect.ValueOf(asHasId.Id()).IsZero() && !m.exists
if isNew { if isNew || !m.exists {
m.Created = now m.Created = now
} }
m.Modified = now m.Modified = now
@ -187,7 +249,7 @@ func (m *Model) Save() error {
return err return err
} }
} }
if isNew { if isNew || !m.exists {
nid := getLastInColl(c.Name(), asHasId.Id()) nid := getLastInColl(c.Name(), asHasId.Id())
switch pnid := nid.(type) { switch pnid := nid.(type) {
case uint: case uint:
@ -214,8 +276,10 @@ func (m *Model) Save() error {
} }
m.self = asHasId m.self = asHasId
c.InsertOne(context.TODO(), m.serializeToStore()) _, err = c.InsertOne(context.TODO(), m.serializeToStore())
m.exists = true if err == nil {
m.exists = true
}
} else { } else {
_, err = c.ReplaceOne(context.TODO(), bson.D{{Key: "_id", Value: m.self.(HasID).Id()}}, m.serializeToStore()) _, err = c.ReplaceOne(context.TODO(), bson.D{{Key: "_id", Value: m.self.(HasID).Id()}}, m.serializeToStore())
} }
@ -245,7 +309,7 @@ func serializeIDs(input interface{}) bson.M {
for i := 0; i < mv.NumField(); i++ { for i := 0; i < mv.NumField(); i++ {
fv := mv.Field(i) fv := mv.Field(i)
ft := mt.Field(i) ft := mt.Field(i)
var dr reflect.Value = fv var dr = fv
tag, err := structtag.Parse(string(mt.Field(i).Tag)) tag, err := structtag.Parse(string(mt.Field(i).Tag))
panik(err) panik(err)
bbson, err := tag.Get("bson") bbson, err := tag.Get("bson")
@ -331,8 +395,8 @@ func serializeIDSlice(input []interface{}) bson.A {
return a return a
} }
// Create creates a new instance of a given model. // Create creates a new instance of a given model
// returns a pointer to the newly created model. // and returns a pointer to it.
func Create(d any) any { func Create(d any) any {
var n string var n string
var ri *InternalModel var ri *InternalModel

@ -41,7 +41,6 @@ func TestPopulate(t *testing.T) {
err := bandDoc.Save() err := bandDoc.Save()
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
storyDoc.Author = author storyDoc.Author = author
err = author.Save()
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
err = storyDoc.Save() err = storyDoc.Save()
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
@ -107,3 +106,23 @@ func TestModel_PopulateMulti(t *testing.T) {
query.Populate("Author", "Chapters.Bands").Exec(&final) query.Populate("Author", "Chapters.Bands").Exec(&final)
assert.Greater(t, len(final), 0) 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)
}

@ -478,5 +478,6 @@ func (q *Query) Exec(result interface{}) {
panic("Exec() has already been called!") panic("Exec() has already been called!")
} }
reflect.ValueOf(result).Elem().Set(reflect.ValueOf(q.doc).Elem()) reflect.ValueOf(result).Elem().Set(reflect.ValueOf(q.doc).Elem())
q.Model.self = q.doc
q.done = true q.done = true
} }

35
util.go

@ -1,6 +1,10 @@
package orm package orm
import "reflect" import (
"fmt"
"reflect"
"strings"
)
func panik(err error) { func panik(err error) {
if err != nil { if err != nil {
@ -68,3 +72,32 @@ func coerceInt(input reflect.Value, dst reflect.Value) interface{} {
} }
return nil 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
}