Files
persist/persist.go
2026-04-07 15:43:01 -04:00

316 lines
6.1 KiB
Go

// Package persist offers a convinient way to interact with the database. It takes into consideration multitenancy.
package persist
import (
"log"
"context"
"fmt"
"errors"
"reflect"
"github.com/go-viper/mapstructure/v2"
"go.mongodb.org/mongo-driver/v2/bson"
"git.gsuntres.com/general/commons"
"git.gsuntres.com/general/mongo"
"git.gsuntres.com/general/structful"
)
type PersistProps struct {
MongoSysDb string
}
// Persist is the root object for accessing data.
type Persist struct {
// IPersist
SysDb string
}
var fields []reflect.StructField
var sysDb string
func NewPersist(props *PersistProps) *Persist {
p := &Persist{
}
p.SysDb = props.MongoSysDb
return p
}
var persistOriginal *Persist
var deferedFuncs map[string]any = make(map[string]any, 0)
var persist any
type InitReport struct {
Fields []string
}
func (report *InitReport) AddField(field string) {
report.Fields = append(report.Fields, field)
}
type InitProps struct {
MongoSysDb string
}
// Init runs on boot, reads system_collections from structful and registers them. No changes after boot are going to be applied.
func Init(props *InitProps) {
var persistProps PersistProps
commons.StructToStruct(props, &persistProps)
persist = NewPersist(&persistProps)
sysDb = props.MongoSysDb
persistOriginal = NewPersist(&persistProps)
// Load structful
s := structful.Current()
filter := map[string]any{}
scol, err := s.FilterByGroup("system_collections", filter)
if err != nil {
log.Fatalf("Failed to get system collections: %v", err)
}
// TODO(me): WIP
// Build a new struct similar to Persist
origType := reflect.TypeOf(persist)
if origType.Kind() == reflect.Ptr {
origType = origType.Elem()
}
// 1. Copy all existing fields
for i := 0; i < origType.NumField(); i++ {
// log.Printf("Add existing field: %v", origType.Field(i))
fields = append(fields, origType.Field(i))
}
client := mongo.GetMongoClient()
report := &InitReport{}
// Take system_collection from structful and build the calls.
for _, col := range scol {
BuildInsertOne(col, report)
BuildFindOne(col, report)
BuildFind(col, report)
BuildGetOne(col, report)
BuildDeleteOne(col, report)
client.AddDefinition(col)
}
log.Printf("Fields Registered: %v", report.Fields)
// 3. Create the new struct type
newStructType := reflect.StructOf(fields)
// 4. Create a new instance
newStruct := reflect.New(newStructType).Elem()
for _, field := range fields {
// log.Printf("FIELD %s", field.Name)
if actualFunc, ok := deferedFuncs[field.Name]; ok {
f := newStruct.FieldByName(field.Name)
f.Set(reflect.ValueOf(actualFunc))
}
}
persist = newStruct.Addr().Interface()
}
func GetCurrent() any {
return persist
}
func Orig() *Persist {
return persistOriginal
}
var ErrCall error = errors.New("failed to call")
func Call(p any, name string, args... any) ([]reflect.Value, error) {
v := reflect.ValueOf(p)
// 1. Make sure it's the right type
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
// 2. Get field
fn := v.FieldByName(name)
// 3. Validate
if !fn.IsValid() {
log.Printf("Failed to find %s field", name)
return nil, ErrCall
}
if fn.Kind() != reflect.Func {
log.Printf("%s is not a function", name)
return nil, ErrCall
}
if fn.IsNil() {
log.Printf("%s is nil", name)
return nil, ErrCall
}
// 4. Prepare arguments
vArgs := make([]reflect.Value, len(args))
for i, arg := range args {
vArgs[i] = reflect.ValueOf(arg)
}
// 5. Call and return results
return fn.Call(vArgs), nil
}
const SAVE_USER_DATA = `
{
"type": "object",
"properties": {
"Fullname": {
"type": "string"
},
"Firstname": {
"type": "string"
},
"Username": {
"type": "string",
"minLength": 3
},
"Password": {
"type": "string",
"minLength": 1
},
"Email": {
"type": "string"
}
},
"required": ["Username", "Password"]
}
`
// SaveUser will encrypt the password before saving the user.
func (p *Persist) SaveUser(ctx context.Context, data *User) (*User, error) {
if valid := commons.Validate(SAVE_USER_DATA, data); valid != nil {
return nil, errors.New(fmt.Sprintf("%s", valid.Error()))
}
data.EncryptPassword()
client := mongo.GetMongoClient()
dataNew, err := client.InsertOneFromStruct(ctx, p.SysDb, USER_COLLECTION, data)
if err != nil {
return nil, err
}
d, err := bson.Marshal(dataNew)
if err != nil {
return nil, err
}
// Unmarshal into struct
var u User
err = bson.Unmarshal(d, &u)
if err != nil {
return nil, err
}
return &u, nil
}
func (p *Persist) CheckUser(ctx context.Context, usernameOrEmail string, password string) (*User, error) {
if len(usernameOrEmail) < 3 {
return nil, errors.New("username or email too short")
}
client := mongo.GetMongoClient()
filter := bson.M{
"$or": bson.A{
bson.M{"username": usernameOrEmail},
bson.M{"email": usernameOrEmail},
},
}
found, err := client.FindOne(ctx, p.SysDb, USER_COLLECTION, filter)
if err != nil {
return nil, err
}
var user User
if err := mongo.ToStruct(found, &user); err != nil {
return nil, err
}
if !user.CheckPassword(password) {
return nil, errors.New("invalid credentials")
}
return &user, nil
}
const SAVE_ACCOUNT_DATA = `
{
"type": "object",
"properties": {
"Code": {
"type": "string",
"minLength": 6
},
"Owner": {
"type": "string",
"minLength": 6
}
},
"required": ["Code", "Owner"]
}
`
func (p *Persist) SaveAccount(ctx context.Context, data *Account) (*Account, error) {
if valid := commons.Validate(SAVE_ACCOUNT_DATA, data); valid != nil {
return nil, errors.New(fmt.Sprintf("%s", valid.Error()))
}
client := mongo.GetMongoClient()
dataNew, err := client.InsertOneFromStruct(ctx, p.SysDb, ACCOUNT_COLLECTION, data)
if err != nil {
return nil, err
}
var o Account
mapstructure.Decode(dataNew, &o)
return &o, nil
}
func (p *Persist) GetAccountByCode(ctx context.Context, code string) (*Account, error) {
client := mongo.GetMongoClient()
filter := bson.M{"code": code}
found, err := client.FindOne(ctx, p.SysDb, ACCOUNT_COLLECTION, filter)
if err != nil {
return nil, err
}
var acc Account
if err := mongo.ToStruct(found, &acc); err != nil {
return nil, err
}
return &acc, nil
}