☙◦ The Tablet ❀ GamerGirlandCo ◦❧
924b3fc9d2
- 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)
604 lines
14 KiB
Go
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))
|
|
}
|