diamond-orm/model.go
☙◦ The Tablet ❀ GamerGirlandCo ◦❧ 924b3fc9d2
many additions!
- add support for `autoinc` struct tag
- add Pull method to Model
- add utility func to increment a value of an unknown type by 1
- idcounter.go no longer increments the counter value (it is now up to the caller to increment the returned value themselves)
2024-09-04 19:43:12 -04:00

604 lines
14 KiB
Go

package orm
import (
"context"
"fmt"
"reflect"
"time"
"github.com/fatih/structtag"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// Model - "base" struct for all queryable models
type Model struct {
// Created time. updated/added automatically.
Created time.Time `bson:"createdAt" json:"createdAt"`
// Modified time. updated/added automatically.
Modified time.Time `bson:"updatedAt" json:"updatedAt"`
typeName string `bson:"-"`
self any `bson:"-"`
exists bool `bson:"-"`
}
func (m *Model) getCreated() time.Time {
return m.Created
}
func (m *Model) setCreated(Created time.Time) {
m.Created = Created
}
func (m *Model) getModified() time.Time {
return m.Modified
}
func (m *Model) setModified(Modified time.Time) {
m.Modified = Modified
}
// 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 {
Append(field string, a ...interface{}) error
Find(query interface{}, opts ...*options.FindOptions) (*mongo.Cursor, error)
FindAll(query interface{}, opts ...*options.FindOptions) (*Query, error)
FindByID(id interface{}) (*Query, error)
FindOne(query interface{}, options ...*options.FindOneOptions) (*Query, error)
FindPaged(query interface{}, page int64, perPage int64, options ...*options.FindOptions) (*Query, error)
getColl() *mongo.Collection
getIdxs() []*mongo.IndexModel
getParsedIdxs() map[string][]InternalIndex
Save() error
serializeToStore() any
setTypeName(str string)
getExists() bool
setExists(n bool)
setModified(Modified time.Time)
setCreated(Modified time.Time)
getModified() time.Time
getCreated() time.Time
setSelf(arg interface{})
}
func (m *Model) setTypeName(str string) {
m.typeName = str
}
func (m *Model) setSelf(arg interface{}) {
m.self = arg
}
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 {
panic(fmt.Sprintf("the model '%s' has not been registered", m.typeName))
}
return DB.Collection(ri.Collection)
}
func (m *Model) getIdxs() []*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))
}
}
return mi
}
return nil
}
func (m *Model) getParsedIdxs() map[string][]InternalIndex {
_, ri, ok := ModelRegistry.HasByName(m.typeName)
if !ok {
panic(fmt.Sprintf("model '%s' not registered", m.typeName))
}
return ri.Indexes
}
func (m *Model) Find(query interface{}, opts ...*options.FindOptions) (*mongo.Cursor, error) {
coll := m.getColl()
cursor, err := coll.Find(context.TODO(), query, opts...)
return cursor, err
}
func (m *Model) FindAll(query interface{}, opts ...*options.FindOptions) (*Query, error) {
qqn := ModelRegistry.new_(m.typeName)
qqv := reflect.New(reflect.SliceOf(reflect.TypeOf(qqn).Elem()))
qqv.Elem().Set(reflect.Zero(qqv.Elem().Type()))
qq := &Query{
Model: *m,
Collection: m.getColl(),
doc: qqv.Interface(),
Op: OP_FIND_ALL,
}
q, err := m.Find(query, opts...)
if err == nil {
rawRes := bson.A{}
err = q.All(context.TODO(), &rawRes)
if err == nil {
m.exists = true
}
qq.rawDoc = rawRes
err = q.All(context.TODO(), &qq.doc)
if err != nil {
qq.reOrganize()
err = nil
}
}
return qq, err
}
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(opts) > 0 {
opts[0].SetSkip(skipAmt).SetLimit(perPage)
} else {
opts = append(opts, options.Find().SetSkip(skipAmt).SetLimit(perPage))
}
q, err := m.FindAll(query, opts...)
q.Op = OP_FIND_PAGED
return q, err
}
func (m *Model) FindByID(id interface{}) (*Query, error) {
return m.FindOne(bson.D{{"_id", id}})
}
func (m *Model) FindOne(query interface{}, options ...*options.FindOneOptions) (*Query, error) {
coll := m.getColl()
rip := coll.FindOne(context.TODO(), query, options...)
raw := bson.M{}
err := rip.Decode(&raw)
panik(err)
m.exists = true
qq := &Query{
Collection: m.getColl(),
rawDoc: raw,
doc: ModelRegistry.new_(m.typeName),
Op: OP_FIND_ONE,
Model: *m,
}
qq.rawDoc = raw
err = rip.Decode(qq.doc)
if err != nil {
qq.reOrganize()
err = nil
}
m.self = qq.doc
return qq, err
}
func doDelete(m *Model, arg interface{}) error {
self, ok := arg.(HasID)
if !ok {
return fmt.Errorf("Object '%s' does not implement HasID", nameOf(arg))
}
c := m.getColl()
_, err := c.DeleteOne(context.TODO(), bson.M{"_id": self.Id()})
if err == nil {
m.exists = false
}
return err
}
func (m *Model) Delete() error {
var err error
val := valueOf(m.self)
if val.Kind() == reflect.Slice {
for i := 0; i < val.Len(); i++ {
cur := val.Index(i)
if err = doDelete(m, cur.Interface()); err != nil {
return err
}
}
return nil
} else {
return doDelete(m, m.self)
}
}
func incrementTagged(item interface{}) interface{} {
rv := reflect.ValueOf(item)
rt := reflect.TypeOf(item)
if rv.Kind() != reflect.Pointer {
rv = makeSettable(rv, item)
}
if rt.Kind() == reflect.Pointer {
rt = rt.Elem()
}
if rt.Kind() != reflect.Struct {
if rt.Kind() == reflect.Slice {
for i := 0; i < rv.Elem().Len(); i++ {
incrementTagged(rv.Elem().Index(i).Addr().Interface())
}
} else {
return item
}
}
for i := 0; i < rt.NumField(); i++ {
structField := rt.Field(i)
cur := rv.Elem().Field(i)
tags, err := structtag.Parse(string(structField.Tag))
if err != nil {
continue
}
incTag, err := tags.Get("autoinc")
if err != nil {
continue
}
nid := getLastInColl(incTag.Name, cur.Interface())
if cur.IsZero() {
coerced := coerceInt(reflect.ValueOf(incrementInterface(nid)), cur)
if coerced != nil {
cur.Set(reflect.ValueOf(coerced))
} else {
cur.Set(reflect.ValueOf(incrementInterface(nid)))
}
}
counterColl := DB.Collection(COUNTER_COL)
counterColl.UpdateOne(context.TODO(), bson.M{"collection": incTag.Name}, bson.M{"$set": bson.M{"collection": incTag.Name, "current": cur.Interface()}}, options.Update().SetUpsert(true))
}
return rv.Elem().Interface()
}
func incrementAll(item interface{}) {
if item == nil {
return
}
vp := reflect.ValueOf(item)
el := vp
if vp.Kind() == reflect.Pointer {
el = vp.Elem()
}
if vp.Kind() == reflect.Pointer && vp.IsNil() {
return
}
vt := el.Type()
switch el.Kind() {
case reflect.Struct:
incrementTagged(item)
for i := 0; i < el.NumField(); i++ {
fv := el.Field(i)
fst := vt.Field(i)
if !fst.IsExported() {
continue
}
incrementAll(fv.Interface())
}
case reflect.Slice:
for i := 0; i < el.Len(); i++ {
incd := incrementTagged(el.Index(i).Addr().Interface())
if reflect.ValueOf(incd).Kind() == reflect.Pointer {
el.Index(i).Set(reflect.ValueOf(incd).Elem())
} else {
el.Index(i).Set(reflect.ValueOf(incd))
}
}
}
}
func checkStruct(ref reflect.Value) error {
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!")
}
return nil
}
// 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)
selfRef := rv
rt := reflect.TypeOf(m.self)
if selfRef.Kind() == reflect.Pointer {
selfRef = selfRef.Elem()
rt = rt.Elem()
}
if err := checkStruct(selfRef); err != nil {
return err
}
_, origV, err := getNested(field, selfRef)
if err != nil {
return err
}
origRef := makeSettable(*origV, (*origV).Interface())
fv := origRef
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 {
val := reflect.ValueOf(incrementTagged(b))
fv.Set(reflect.Append(fv, val))
}
return nil
}
func (m *Model) Pull(field string, a ...any) error {
rv := reflect.ValueOf(m.self)
selfRef := rv
rt := reflect.TypeOf(m.self)
if selfRef.Kind() == reflect.Pointer {
selfRef = selfRef.Elem()
rt = rt.Elem()
}
if err := checkStruct(selfRef); err != nil {
return err
}
_, origV, err := getNested(field, selfRef)
if err != nil {
return err
}
origRef := makeSettable(*origV, (*origV).Interface())
fv := origRef
if fv.Kind() == reflect.Pointer {
fv = fv.Elem()
}
if fv.Kind() != reflect.Slice {
return fmt.Errorf("Current object is not a slice!")
}
outer:
for _, b := range a {
for i := 0; i < fv.Len(); i++ {
if reflect.DeepEqual(b, fv.Index(i).Interface()) {
fv.Set(pull(fv, i, fv.Index(i).Type()))
break outer
}
}
}
return nil
}
func doSave(c *mongo.Collection, isNew bool, arg interface{}) error {
var err error
m, ok := arg.(IModel)
if !ok {
return fmt.Errorf("type '%s' is not a model", nameOf(arg))
}
m.setSelf(m)
now := time.Now()
selfo := reflect.ValueOf(m)
vp := selfo
if vp.Kind() != reflect.Ptr {
vp = reflect.New(selfo.Type())
vp.Elem().Set(selfo)
}
var asHasId = vp.Interface().(HasID)
if isNew {
m.setCreated(now)
}
m.setModified(now)
idxs := m.getIdxs()
for _, i := range idxs {
_, err = c.Indexes().CreateOne(context.TODO(), *i)
if err != nil {
return err
}
}
if isNew {
nid := getLastInColl(c.Name(), asHasId.Id())
pnid := incrementInterface(nid)
if reflect.ValueOf(asHasId.Id()).IsZero() {
(asHasId).SetId(pnid)
}
incrementAll(asHasId)
_, err = c.InsertOne(context.TODO(), m.serializeToStore())
if err == nil {
m.setExists(true)
}
} else {
_, err = c.ReplaceOne(context.TODO(), bson.D{{Key: "_id", Value: m.(HasID).Id()}}, m.serializeToStore())
}
return err
}
func (m *Model) Save() error {
val := valueOf(m.self)
if val.Kind() == reflect.Slice {
for i := 0; i < val.Len(); i++ {
cur := val.Index(i)
if err := doSave(m.getColl(), !m.exists, cur.Interface()); err != nil {
return err
}
}
return nil
} else {
return doSave(m.getColl(), !m.exists, m.self)
}
}
func (m *Model) serializeToStore() any {
return serializeIDs((m).self)
}
func serializeIDs(input interface{}) interface{} {
vp := reflect.ValueOf(input)
mt := reflect.TypeOf(input)
var ret interface{}
if vp.Kind() != reflect.Ptr {
if vp.CanAddr() {
vp = vp.Addr()
} else {
vp = makeSettable(vp, input)
}
}
if mt.Kind() == reflect.Pointer {
mt = mt.Elem()
}
getID := func(bbb interface{}) interface{} {
mptr := reflect.ValueOf(bbb)
if mptr.Kind() != reflect.Pointer {
mptr = makeSettable(mptr, bbb)
}
ifc, ok := mptr.Interface().(HasID)
if ok {
return ifc.Id()
} else {
return nil
}
}
/*var itagged interface{}
if reflect.ValueOf(itagged).Kind() != reflect.Pointer {
itagged = incrementTagged(&input)
} else {
itagged = incrementTagged(input)
}
taggedVal := reflect.ValueOf(reflect.ValueOf(itagged).Interface()).Elem()
if vp.Kind() == reflect.Ptr {
tmp := reflect.ValueOf(taggedVal.Interface())
if tmp.Kind() == reflect.Pointer {
vp.Elem().Set(tmp.Elem())
} else {
vp.Elem().Set(tmp)
}
}*/
switch vp.Elem().Kind() {
case reflect.Struct:
ret0 := bson.M{}
for i := 0; i < vp.Elem().NumField(); i++ {
fv := vp.Elem().Field(i)
ft := mt.Field(i)
tag, err := structtag.Parse(string(ft.Tag))
panik(err)
bbson, err := tag.Get("bson")
if err != nil || bbson.Name == "-" {
continue
}
if bbson.Name == "" {
marsh, _ := bson.Marshal(fv.Interface())
unmarsh := bson.M{}
bson.Unmarshal(marsh, &unmarsh)
for k, v := range unmarsh {
ret0[k] = v
/*if t, ok := v.(primitive.DateTime); ok {
ret0
} else {
}*/
}
} else {
_, terr := tag.Get("ref")
if reflect.ValueOf(fv.Interface()).Type().Kind() != reflect.Pointer {
vp1 := reflect.New(fv.Type())
vp1.Elem().Set(reflect.ValueOf(fv.Interface()))
fv.Set(vp1.Elem())
}
if terr == nil {
ifc, ok := fv.Interface().(HasID)
if fv.Kind() == reflect.Slice {
rarr := bson.A{}
for j := 0; j < fv.Len(); j++ {
rarr = append(rarr, getID(fv.Index(j).Interface()))
}
ret0[bbson.Name] = rarr
/*ret0[bbson.Name] = serializeIDs(fv.Interface())
break*/
} else if !ok {
panic(fmt.Sprintf("referenced model slice at '%s.%s' does not implement HasID", nameOf(input), ft.Name))
} else {
if reflect.ValueOf(ifc).IsNil() {
ret0[bbson.Name] = nil
} else {
ret0[bbson.Name] = ifc.Id()
}
}
} else {
ret0[bbson.Name] = serializeIDs(fv.Interface())
}
}
ret = ret0
}
case reflect.Slice:
ret0 := bson.A{}
for i := 0; i < vp.Elem().Len(); i++ {
ret0 = append(ret0, serializeIDs(vp.Elem().Index(i).Addr().Interface()))
}
ret = ret0
default:
ret = vp.Elem().Interface()
}
return ret
}
// 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
var ok bool
n, ri, ok = ModelRegistry.HasByName(nameOf(d))
if !ok {
ModelRegistry.Model(d)
n, ri, _ = ModelRegistry.Has(d)
}
t := ri.Type
v := valueOf(d)
i := ModelRegistry.Index(n)
r := reflect.New(t)
r.Elem().Set(v)
df := r.Elem().Field(i)
dm := df.Interface().(Model)
if reflect.ValueOf(d).Kind() == reflect.Pointer {
r.Elem().Set(reflect.ValueOf(d).Elem())
} else {
r.Elem().Set(reflect.ValueOf(d))
}
dm.typeName = n
what := r.Interface()
dm.self = what
df.Set(reflect.ValueOf(dm))
return what
}
func (m *Model) PrintMe() {
fmt.Printf("My name is %s !\n", nameOf(m))
}