2024-09-01 16:17:48 -04:00
|
|
|
package orm
|
|
|
|
|
|
|
|
import (
|
|
|
|
context2 "context"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"reflect"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
"github.com/fatih/structtag"
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
|
|
"go.mongodb.org/mongo-driver/mongo"
|
|
|
|
"go.mongodb.org/mongo-driver/mongo/options"
|
|
|
|
"golang.org/x/net/context"
|
|
|
|
)
|
|
|
|
|
|
|
|
// InternalModel, as the name suggests, is used
|
|
|
|
// internally by the model registry
|
|
|
|
type InternalModel struct {
|
|
|
|
Idx int
|
|
|
|
Type reflect.Type
|
|
|
|
Collection string
|
|
|
|
References map[string]Reference
|
|
|
|
Indexes map[string][]InternalIndex
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reference stores a typed document reference
|
|
|
|
type Reference struct {
|
|
|
|
// owning model name
|
|
|
|
Model string
|
|
|
|
// the name of the struct field
|
|
|
|
FieldName string
|
|
|
|
// index of field in owning struct
|
|
|
|
Idx int
|
|
|
|
// the type of the referenced object
|
|
|
|
HydratedType reflect.Type
|
|
|
|
|
|
|
|
// field kind (struct, slice, ...)
|
|
|
|
Kind reflect.Kind
|
|
|
|
|
|
|
|
Exists bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type TModelRegistry map[string]*InternalModel
|
|
|
|
|
|
|
|
// ModelRegistry - the ModelRegistry stores a map containing
|
|
|
|
// pointers to InternalModel instances, keyed by an associated
|
|
|
|
// model name
|
|
|
|
var ModelRegistry = make(TModelRegistry, 0)
|
|
|
|
|
|
|
|
// DB - The mongodb database handle
|
|
|
|
var DB *mongo.Database
|
|
|
|
|
|
|
|
// DBClient - The mongodb client
|
|
|
|
var DBClient *mongo.Client
|
|
|
|
|
|
|
|
// NextStringID - Override this function with your own
|
|
|
|
// string ID generator!
|
|
|
|
var NextStringID func() string
|
|
|
|
|
|
|
|
var mutex sync.Mutex
|
|
|
|
|
|
|
|
func getRawTypeFromTag(tagOpt string, slice bool) reflect.Type {
|
|
|
|
var t reflect.Type
|
|
|
|
switch strings.ToLower(tagOpt) {
|
|
|
|
case "int":
|
|
|
|
var v int64 = 0
|
|
|
|
t = reflect.TypeOf(v)
|
|
|
|
case "uint":
|
|
|
|
var v uint = 0
|
|
|
|
t = reflect.TypeOf(v)
|
|
|
|
case "string":
|
2024-09-05 17:53:04 -04:00
|
|
|
var v = "0"
|
2024-09-01 16:17:48 -04:00
|
|
|
t = reflect.TypeOf(v)
|
|
|
|
|
|
|
|
}
|
|
|
|
if slice {
|
|
|
|
return reflect.SliceOf(t)
|
|
|
|
}
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeRef(idx int, modelName string, fieldName string, ht reflect.Type) Reference {
|
|
|
|
if modelName != "" {
|
|
|
|
if ModelRegistry.Index(modelName) != -1 {
|
|
|
|
return Reference{
|
|
|
|
Idx: idx,
|
|
|
|
Model: modelName,
|
|
|
|
HydratedType: ht,
|
|
|
|
Kind: ht.Kind(),
|
|
|
|
Exists: true,
|
|
|
|
FieldName: fieldName,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Reference{
|
|
|
|
Idx: idx,
|
|
|
|
Model: modelName,
|
|
|
|
FieldName: fieldName,
|
|
|
|
HydratedType: ht,
|
|
|
|
Kind: ht.Kind(),
|
|
|
|
Exists: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
panic("model name was empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseTags(t reflect.Type, v reflect.Value) (map[string][]InternalIndex, map[string]Reference, string) {
|
|
|
|
coll := ""
|
|
|
|
refs := make(map[string]Reference, 0)
|
|
|
|
idcs := make(map[string][]InternalIndex, 0)
|
|
|
|
|
|
|
|
for i := 0; i < v.NumField(); i++ {
|
|
|
|
sft := t.Field(i)
|
|
|
|
ft := sft.Type
|
|
|
|
tags, err := structtag.Parse(string(sft.Tag))
|
|
|
|
panik(err)
|
|
|
|
shouldContinue := true
|
|
|
|
for {
|
|
|
|
if !shouldContinue {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
switch ft.Kind() {
|
|
|
|
case reflect.Slice:
|
|
|
|
ft = ft.Elem()
|
|
|
|
if _, ok := tags.Get("ref"); ok != nil {
|
|
|
|
if ft.Kind() == reflect.Struct {
|
|
|
|
ii2, rr2, _ := parseTags(ft, reflect.New(ft).Elem())
|
|
|
|
for k, v := range ii2 {
|
|
|
|
idcs[sft.Name+"."+k] = v
|
|
|
|
}
|
|
|
|
for k, v := range rr2 {
|
|
|
|
refs[sft.Name+"."+k] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
case reflect.Pointer:
|
|
|
|
ft = ft.Elem()
|
|
|
|
fallthrough
|
|
|
|
case reflect.Struct:
|
|
|
|
if ft.ConvertibleTo(reflect.TypeOf(Model{})) {
|
|
|
|
collTag, err := tags.Get("coll")
|
|
|
|
panik(err)
|
|
|
|
coll = collTag.Name
|
|
|
|
idxTag, err := tags.Get("idx")
|
|
|
|
if err == nil {
|
|
|
|
idcs[sft.Type.Name()] = scanIndex(idxTag.Value())
|
|
|
|
}
|
|
|
|
shouldContinue = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if refTag, ok := tags.Get("ref"); ok == nil {
|
|
|
|
// ref:"ModelName,refType"
|
|
|
|
/* if len(refTag.Options) < 1 {
|
|
|
|
panic("no raw type provided for ref")
|
|
|
|
} */
|
|
|
|
sname := sft.Name + "@" + refTag.Name
|
|
|
|
refs[sname] = makeRef(i, refTag.Name, sft.Name, sft.Type)
|
|
|
|
}
|
|
|
|
fallthrough
|
|
|
|
default:
|
|
|
|
idxTag, err := tags.Get("idx")
|
|
|
|
if err == nil {
|
|
|
|
idcs[sft.Name] = scanIndex(idxTag.Value())
|
|
|
|
}
|
|
|
|
shouldContinue = false
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return idcs, refs, coll
|
|
|
|
}
|
|
|
|
|
|
|
|
// Has returns the model typename and InternalModel instance corresponding
|
|
|
|
// to the argument passed, as well as a boolean indicating whether it
|
|
|
|
// was found. otherwise returns `"", nil, false`
|
|
|
|
func (r TModelRegistry) Has(i interface{}) (string, *InternalModel, bool) {
|
|
|
|
t := reflect.TypeOf(i)
|
|
|
|
if t.Kind() == reflect.Ptr {
|
|
|
|
t = t.Elem()
|
|
|
|
}
|
|
|
|
n := t.Name()
|
|
|
|
if rT, ok := ModelRegistry[n]; ok {
|
|
|
|
return n, rT, true
|
|
|
|
}
|
|
|
|
return "", nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
// HasByName functions almost identically to Has,
|
|
|
|
// except that it takes a string as its argument.
|
|
|
|
func (t TModelRegistry) HasByName(n string) (string, *InternalModel, bool) {
|
|
|
|
if t, ok := ModelRegistry[n]; ok {
|
|
|
|
return n, t, true
|
|
|
|
}
|
|
|
|
return "", nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Index returns the index at which the Model struct is embedded
|
|
|
|
func (r TModelRegistry) Index(n string) int {
|
|
|
|
if v, ok := ModelRegistry[n]; ok {
|
|
|
|
return v.Idx
|
|
|
|
}
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t TModelRegistry) new_(n string) interface{} {
|
2024-09-05 17:53:04 -04:00
|
|
|
if name, m, ok := ModelRegistry.HasByName(n); ok {
|
2024-09-01 16:17:48 -04:00
|
|
|
v := reflect.New(m.Type)
|
|
|
|
df := v.Elem().Field(m.Idx)
|
|
|
|
d := df.Interface().(Model)
|
2024-09-05 17:53:04 -04:00
|
|
|
d.typeName = name
|
2024-09-01 16:17:48 -04:00
|
|
|
df.Set(reflect.ValueOf(d))
|
|
|
|
return v.Interface()
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Model registers models in the ModelRegistry, where
|
|
|
|
// they can be accessed via a model's struct name
|
|
|
|
func (r TModelRegistry) Model(mdl ...any) {
|
|
|
|
defer mutex.Unlock()
|
|
|
|
mutex.Lock()
|
|
|
|
|
|
|
|
for _, m := range mdl {
|
|
|
|
t := reflect.TypeOf(m)
|
|
|
|
v := reflect.ValueOf(m)
|
|
|
|
vp := v
|
|
|
|
if vp.Kind() != reflect.Ptr {
|
|
|
|
vp = reflect.New(v.Type())
|
|
|
|
vp.Elem().Set(v)
|
|
|
|
}
|
|
|
|
id, ok := vp.Interface().(HasID)
|
|
|
|
if !ok {
|
|
|
|
panic(fmt.Sprintf("you MUST implement the HasID interface!!! skipping...\n"))
|
|
|
|
}
|
|
|
|
switch (id).Id().(type) {
|
|
|
|
case int, int64, int32, string, primitive.ObjectID, uint, uint32, uint64:
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
log.Printf("invalid ID type specified!!! skipping...\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
if t.Kind() == reflect.Ptr {
|
|
|
|
t = reflect.Indirect(reflect.ValueOf(m)).Type()
|
|
|
|
v = reflect.ValueOf(m).Elem()
|
|
|
|
}
|
|
|
|
n := t.Name()
|
|
|
|
if t.Kind() != reflect.Struct {
|
|
|
|
panic(fmt.Sprintf("Only structs can be passed to this function, silly! (passed type: %s)", n))
|
|
|
|
}
|
|
|
|
idx := -1
|
|
|
|
for i := 0; i < v.NumField(); i++ {
|
|
|
|
ft := t.Field(i)
|
|
|
|
if (ft.Type.ConvertibleTo(reflect.TypeOf(Model{}))) {
|
|
|
|
idx = i
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if idx < 0 {
|
|
|
|
panic("A model must embed the Model struct!")
|
|
|
|
}
|
|
|
|
inds, refs, coll := parseTags(t, v)
|
|
|
|
if coll == "" {
|
|
|
|
panic(fmt.Sprintf("a model needs to be given a collection name! (passed type: %s)", n))
|
|
|
|
}
|
|
|
|
ModelRegistry[n] = &InternalModel{
|
|
|
|
Idx: idx,
|
|
|
|
Type: t,
|
|
|
|
Collection: coll,
|
|
|
|
Indexes: inds,
|
|
|
|
References: refs,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for k, v := range ModelRegistry {
|
|
|
|
for k2, v2 := range v.References {
|
|
|
|
if !v2.Exists {
|
|
|
|
if _, ok := ModelRegistry[v2.FieldName]; ok {
|
|
|
|
tmp := ModelRegistry[k].References[k2]
|
|
|
|
ModelRegistry[k].References[k2] = Reference{
|
|
|
|
Model: k,
|
|
|
|
Idx: tmp.Idx,
|
|
|
|
FieldName: tmp.FieldName,
|
|
|
|
Kind: tmp.Kind,
|
|
|
|
HydratedType: tmp.HydratedType,
|
|
|
|
Exists: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func innerWatch(coll *mongo.Collection) {
|
|
|
|
sspipeline := mongo.Pipeline{
|
|
|
|
bson.D{{"$match", bson.D{{"$or",
|
|
|
|
bson.A{
|
|
|
|
bson.D{{
|
|
|
|
"operationType", "insert",
|
|
|
|
}},
|
|
|
|
bson.D{{"operationType", "update"}},
|
|
|
|
},
|
|
|
|
}},
|
|
|
|
}},
|
|
|
|
}
|
|
|
|
|
|
|
|
stream, err := coll.Watch(context.TODO(), sspipeline, options.ChangeStream().SetFullDocument(options.UpdateLookup).SetFullDocumentBeforeChange(options.WhenAvailable))
|
|
|
|
panik(err)
|
|
|
|
defer func(stream *mongo.ChangeStream, ctx context2.Context) {
|
|
|
|
err := stream.Close(ctx)
|
|
|
|
panik(err)
|
|
|
|
}(stream, context.TODO())
|
|
|
|
for stream.Next(context.TODO()) {
|
|
|
|
var data bson.M
|
|
|
|
if err := stream.Decode(&data); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var uid = data["documentKey"].(bson.M)["_id"]
|
|
|
|
|
|
|
|
if data["operationType"] == "insert" {
|
|
|
|
counterColl := DB.Collection(COUNTER_COL)
|
|
|
|
counterColl.UpdateOne(context.TODO(), bson.M{"collection": coll.Name()}, bson.M{"$set": bson.M{
|
|
|
|
"current": uid,
|
|
|
|
}}, options.Update().SetUpsert(true))
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("%v\n", data)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func Connect(uri string, dbName string) {
|
|
|
|
cli, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal("failed to open database")
|
|
|
|
}
|
|
|
|
panik(err)
|
|
|
|
DB = cli.Database(dbName)
|
|
|
|
colls, err := DB.ListCollectionNames(context.TODO(), bson.M{"name": bson.M{"$ne": COUNTER_COL}}, options.ListCollections().SetNameOnly(true))
|
|
|
|
|
|
|
|
for _, c := range colls {
|
|
|
|
if c == COUNTER_COL {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
go innerWatch(DB.Collection(c))
|
|
|
|
}
|
|
|
|
|
|
|
|
DBClient = cli
|
|
|
|
}
|