Initial commit
This commit is contained in:
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Allowlisting gitignore template for GO projects prevents us
|
||||||
|
# from adding various unwanted local files, such as generated
|
||||||
|
# files, developer configurations or IDE-specific files etc.
|
||||||
|
#
|
||||||
|
# Recommended: Go.AllowList.gitignore
|
||||||
|
|
||||||
|
# Ignore everything
|
||||||
|
*
|
||||||
|
|
||||||
|
# But these files...
|
||||||
|
!.gitignore
|
||||||
|
|
||||||
|
!*.go
|
||||||
|
!go.sum
|
||||||
|
!go.mod
|
||||||
|
|
||||||
|
!README.md
|
||||||
|
!LICENSE
|
||||||
|
|
||||||
|
!Makefile
|
||||||
|
|
||||||
|
!*.sh
|
||||||
|
!*.md
|
||||||
|
|
||||||
|
# ...even if they are in subdirectories
|
||||||
|
!*/
|
||||||
33
encrypt.go
Normal file
33
encrypt.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package persist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
// "unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncryptPassword will encrypt the password and replace the old one. Will return the encrypted password.
|
||||||
|
func (user *User) EncryptPassword() string {
|
||||||
|
if user.Password == "" {
|
||||||
|
log.Println("Nothing to encrypt.")
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
hpass, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Password = string(hpass)
|
||||||
|
|
||||||
|
return user.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPassword returns true if passwords match.
|
||||||
|
func (user *User) CheckPassword(password string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||||
|
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
41
encrypt_test.go
Normal file
41
encrypt_test.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package persist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUser_EncryptPassword(t *testing.T) {
|
||||||
|
u := &User{
|
||||||
|
Password: "1234",
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted := u.EncryptPassword()
|
||||||
|
|
||||||
|
if u.Password != encrypted {
|
||||||
|
t.Fatal("Failed to encrypt password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUser_CheckPassword(t *testing.T) {
|
||||||
|
u := &User{
|
||||||
|
Password: "1234",
|
||||||
|
}
|
||||||
|
|
||||||
|
u.EncryptPassword()
|
||||||
|
|
||||||
|
if !u.CheckPassword("1234") {
|
||||||
|
t.Fatal("Failed to check password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUser_CheckPassword_FalsePositive(t *testing.T) {
|
||||||
|
u := &User{
|
||||||
|
Password: "1234",
|
||||||
|
}
|
||||||
|
|
||||||
|
u.EncryptPassword()
|
||||||
|
|
||||||
|
if u.CheckPassword("!234") {
|
||||||
|
t.Fatal("Should have not accepted password")
|
||||||
|
}
|
||||||
|
}
|
||||||
299
persist.go
Normal file
299
persist.go
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
// Package persist offers a convinient way to interact with the database. It takes into consideration multitenancy.
|
||||||
|
package persist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"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
|
||||||
|
|
||||||
|
var caseString cases.Caser = cases.Title(language.English)
|
||||||
|
|
||||||
|
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.CopyToStruct(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()
|
||||||
|
// Take system_collection from structful and build the calls.
|
||||||
|
for _, col := range scol {
|
||||||
|
BuildInsertOne(col)
|
||||||
|
BuildFindOne(col)
|
||||||
|
BuildFind(col)
|
||||||
|
BuildGetOne(col)
|
||||||
|
BuildDeleteOne(col)
|
||||||
|
|
||||||
|
client.AddDefinition(col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func Call(p any, name string, args... any) []reflect.Value {
|
||||||
|
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.Fatalf("Failed to find %s field", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fn.Kind() != reflect.Func {
|
||||||
|
log.Fatalf("%s is not a function", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fn.IsNil() {
|
||||||
|
log.Fatalf("%s is nil", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
54
persist_deleteone.go
Normal file
54
persist_deleteone.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package persist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"git.gsuntres.com/general/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildDeleteOne(col map[string]any) {
|
||||||
|
name := col["_name"].(string)
|
||||||
|
singular := col["singular"].(string)
|
||||||
|
|
||||||
|
// prepare input arguments and return results
|
||||||
|
in := []reflect.Type{
|
||||||
|
reflect.TypeOf((*context.Context)(nil)).Elem(),
|
||||||
|
reflect.TypeOf((*map[string]any)(nil)).Elem(),
|
||||||
|
}
|
||||||
|
out := []reflect.Type{
|
||||||
|
reflect.TypeOf((*error)(nil)).Elem(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// create function signature
|
||||||
|
variadic := false
|
||||||
|
funcType := reflect.FuncOf(in, out, variadic)
|
||||||
|
deleteOneName := fmt.Sprintf("%s%s", "Delete", caseString.String(singular))
|
||||||
|
fields = append(fields, reflect.StructField{
|
||||||
|
Name: deleteOneName,
|
||||||
|
Type: funcType,
|
||||||
|
})
|
||||||
|
|
||||||
|
isSystem := false
|
||||||
|
if v, ok := col["system"]; ok {
|
||||||
|
isSystem = v.(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
mc := mongo.GetMongoClient()
|
||||||
|
|
||||||
|
// we defer function's implementation until we create the actual struct
|
||||||
|
deferedFuncs[deleteOneName] = func(ctx context.Context, filter map[string]any) error {
|
||||||
|
db := "__undefined__"
|
||||||
|
if isSystem {
|
||||||
|
db = sysDb
|
||||||
|
} else {
|
||||||
|
account := ctx.Value("account").(string)
|
||||||
|
if account != "" {
|
||||||
|
db = mc.GetName(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mc.DeleteOne(ctx, db, name, filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
persist_find.go
Normal file
58
persist_find.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package persist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
|
||||||
|
"git.gsuntres.com/general/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildFind(col map[string]any) {
|
||||||
|
name := col["_name"].(string)
|
||||||
|
plural := col["plural"].(string)
|
||||||
|
|
||||||
|
// prepare input arguments and return results
|
||||||
|
in := []reflect.Type{
|
||||||
|
reflect.TypeOf((*context.Context)(nil)).Elem(),
|
||||||
|
reflect.TypeOf((*map[string]any)(nil)).Elem(),
|
||||||
|
reflect.TypeOf((*int64)(nil)).Elem(),
|
||||||
|
}
|
||||||
|
out := []reflect.Type{
|
||||||
|
reflect.TypeOf(bson.M{}),
|
||||||
|
reflect.TypeOf((*error)(nil)).Elem(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// create function signature
|
||||||
|
variadic := false
|
||||||
|
funcType := reflect.FuncOf(in, out, variadic)
|
||||||
|
funcName := fmt.Sprintf("%s%s", "Find", caseString.String(plural))
|
||||||
|
fields = append(fields, reflect.StructField{
|
||||||
|
Name: funcName,
|
||||||
|
Type: funcType,
|
||||||
|
})
|
||||||
|
|
||||||
|
isSystem := false
|
||||||
|
if v, ok := col["system"]; ok {
|
||||||
|
isSystem = v.(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
mc := mongo.GetMongoClient()
|
||||||
|
|
||||||
|
// we defer function's implementation until we create the actual struct
|
||||||
|
deferedFuncs[funcName] = func(ctx context.Context, filter map[string]any, limit int64) (bson.M, error) {
|
||||||
|
db := "__undefined__"
|
||||||
|
if isSystem {
|
||||||
|
db = sysDb
|
||||||
|
} else {
|
||||||
|
account := ctx.Value("account").(string)
|
||||||
|
if account != "" {
|
||||||
|
db = mc.GetName(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mc.Find(ctx, db, name, filter, limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
57
persist_findone.go
Normal file
57
persist_findone.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package persist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
|
||||||
|
"git.gsuntres.com/general/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildFindOne(col map[string]any) {
|
||||||
|
name := col["_name"].(string)
|
||||||
|
singular := col["singular"].(string)
|
||||||
|
|
||||||
|
// prepare input arguments and return results
|
||||||
|
in := []reflect.Type{
|
||||||
|
reflect.TypeOf((*context.Context)(nil)).Elem(),
|
||||||
|
reflect.TypeOf((*map[string]any)(nil)).Elem(),
|
||||||
|
}
|
||||||
|
out := []reflect.Type{
|
||||||
|
reflect.TypeOf(bson.M{}),
|
||||||
|
reflect.TypeOf((*error)(nil)).Elem(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// create function signature
|
||||||
|
variadic := false
|
||||||
|
funcType := reflect.FuncOf(in, out, variadic)
|
||||||
|
funcName := fmt.Sprintf("%s%s", "Find", caseString.String(singular))
|
||||||
|
fields = append(fields, reflect.StructField{
|
||||||
|
Name: funcName,
|
||||||
|
Type: funcType,
|
||||||
|
})
|
||||||
|
|
||||||
|
isSystem := false
|
||||||
|
if v, ok := col["system"]; ok {
|
||||||
|
isSystem = v.(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
mc := mongo.GetMongoClient()
|
||||||
|
|
||||||
|
// we defer function's implementation until we create the actual struct
|
||||||
|
deferedFuncs[funcName] = func(ctx context.Context, filter map[string]any) (bson.M, error) {
|
||||||
|
db := "__undefined__"
|
||||||
|
if isSystem {
|
||||||
|
db = sysDb
|
||||||
|
} else {
|
||||||
|
account := ctx.Value("account").(string)
|
||||||
|
if account != "" {
|
||||||
|
db = mc.GetName(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mc.FindOne(ctx, db, name, filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
57
persist_getone.go
Normal file
57
persist_getone.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package persist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
|
||||||
|
"git.gsuntres.com/general/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildGetOne(col map[string]any) {
|
||||||
|
name := col["_name"].(string)
|
||||||
|
singular := col["singular"].(string)
|
||||||
|
|
||||||
|
// prepare input arguments and return results
|
||||||
|
in := []reflect.Type{
|
||||||
|
reflect.TypeOf((*context.Context)(nil)).Elem(),
|
||||||
|
reflect.TypeOf((*map[string]any)(nil)).Elem(),
|
||||||
|
}
|
||||||
|
out := []reflect.Type{
|
||||||
|
reflect.TypeOf(bson.M{}),
|
||||||
|
reflect.TypeOf((*error)(nil)).Elem(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// create function signature
|
||||||
|
variadic := false
|
||||||
|
funcType := reflect.FuncOf(in, out, variadic)
|
||||||
|
funcName := fmt.Sprintf("%s%s", "Get", caseString.String(singular))
|
||||||
|
fields = append(fields, reflect.StructField{
|
||||||
|
Name: funcName,
|
||||||
|
Type: funcType,
|
||||||
|
})
|
||||||
|
|
||||||
|
isSystem := false
|
||||||
|
if v, ok := col["system"]; ok {
|
||||||
|
isSystem = v.(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
mc := mongo.GetMongoClient()
|
||||||
|
|
||||||
|
// we defer function's implementation until we create the actual struct
|
||||||
|
deferedFuncs[funcName] = func(ctx context.Context, data map[string]any) (bson.M, error) {
|
||||||
|
db := "__undefined__"
|
||||||
|
if isSystem {
|
||||||
|
db = sysDb
|
||||||
|
} else {
|
||||||
|
account := ctx.Value("account").(string)
|
||||||
|
if account != "" {
|
||||||
|
db = mc.GetName(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mc.GetOne(ctx, db, name, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
57
persist_insertone.go
Normal file
57
persist_insertone.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package persist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
|
||||||
|
"git.gsuntres.com/general/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildInsertOne(col map[string]any) {
|
||||||
|
name := col["_name"].(string)
|
||||||
|
singular := col["singular"].(string)
|
||||||
|
|
||||||
|
// prepare input arguments and return results
|
||||||
|
in := []reflect.Type{
|
||||||
|
reflect.TypeOf((*context.Context)(nil)).Elem(),
|
||||||
|
reflect.TypeOf((*map[string]any)(nil)).Elem(),
|
||||||
|
}
|
||||||
|
out := []reflect.Type{
|
||||||
|
reflect.TypeOf(bson.M{}),
|
||||||
|
reflect.TypeOf((*error)(nil)).Elem(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// create function signature
|
||||||
|
variadic := false
|
||||||
|
funcType := reflect.FuncOf(in, out, variadic)
|
||||||
|
insertOneName := fmt.Sprintf("%s%s", "Insert", caseString.String(singular))
|
||||||
|
fields = append(fields, reflect.StructField{
|
||||||
|
Name: insertOneName,
|
||||||
|
Type: funcType,
|
||||||
|
})
|
||||||
|
|
||||||
|
isSystem := false
|
||||||
|
if v, ok := col["system"]; ok {
|
||||||
|
isSystem = v.(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
mc := mongo.GetMongoClient()
|
||||||
|
|
||||||
|
// we defer function's implementation until we create the actual struct
|
||||||
|
deferedFuncs[insertOneName] = func(ctx context.Context, data map[string]any) (bson.M, error) {
|
||||||
|
db := "__undefined__"
|
||||||
|
if isSystem {
|
||||||
|
db = sysDb
|
||||||
|
} else {
|
||||||
|
account := ctx.Value("account").(string)
|
||||||
|
if account != "" {
|
||||||
|
db = mc.GetName(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mc.InsertOne(ctx, db, name, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
194
persist_test.go
Normal file
194
persist_test.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package persist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/testcontainers/testcontainers-go"
|
||||||
|
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
|
||||||
|
"git.gsuntres.com/general/mongo"
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
if os.Getenv("RUN_INTEGRATION") == "" {
|
||||||
|
fmt.Println("Skipping package tests: RUN_INTEGRATION is missing")
|
||||||
|
os.Exit(0) // Exit with success, but no tests ran
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
user := "admin"
|
||||||
|
pass := "1"
|
||||||
|
|
||||||
|
// 1. Setup: Start the MongoDB container
|
||||||
|
mongoContainer, err := mongodb.Run(ctx, "mongo:8",
|
||||||
|
testcontainers.WithEnv(map[string]string{
|
||||||
|
"MONGO_INITDB_ROOT_USERNAME": user,
|
||||||
|
"MONGO_INITDB_ROOT_PASSWORD": pass,
|
||||||
|
}),)
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to start container")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get the connection string dynamically
|
||||||
|
endpoint, _ := mongoContainer.ConnectionString(ctx)
|
||||||
|
|
||||||
|
mongo.Start(&mongo.MongoStartProps{
|
||||||
|
MongoUri: endpoint,
|
||||||
|
MongoUser: user,
|
||||||
|
MongoPass: pass,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. Run tests
|
||||||
|
code := m.Run()
|
||||||
|
|
||||||
|
// 4. Teardown: Clean up resources
|
||||||
|
mongo.Stop()
|
||||||
|
_ = testcontainers.TerminateContainer(mongoContainer)
|
||||||
|
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveUser_Valid(t *testing.T) {
|
||||||
|
p := NewPersist(&PersistProps{
|
||||||
|
MongoSysDb: "test_boxtep_sys",
|
||||||
|
})
|
||||||
|
|
||||||
|
data := &User{
|
||||||
|
Fullname: "King Long Too",
|
||||||
|
Firstname: "Too",
|
||||||
|
Username: "toolong",
|
||||||
|
Password: "mypass",
|
||||||
|
Email: "too@long.test",
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := p.SaveUser(context.TODO(), data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Should have saved user %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Id = user.Id
|
||||||
|
|
||||||
|
dataMap, _ := mongo.ToMap(data)
|
||||||
|
userMap, _ := mongo.ToMap(user)
|
||||||
|
|
||||||
|
mongo.AssertSubset(t, dataMap, userMap, "Should have been equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveUser_RequireUsername(t *testing.T) {
|
||||||
|
p := NewPersist(&PersistProps{
|
||||||
|
MongoSysDb: "test_boxtep_sys",
|
||||||
|
})
|
||||||
|
|
||||||
|
data := &User{
|
||||||
|
Fullname: "King Long Too",
|
||||||
|
Firstname: "Too",
|
||||||
|
Password: "mypass",
|
||||||
|
Email: "too@long.test",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.SaveUser(context.TODO(), data)
|
||||||
|
|
||||||
|
if err == nil || err.Error() != `properties/Username: value "" too short for "minLength" argument 3` {
|
||||||
|
t.Fatal("Should require username")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveUser_RequirePassword(t *testing.T) {
|
||||||
|
p := NewPersist(&PersistProps{
|
||||||
|
MongoSysDb: "test_boxtep_sys",
|
||||||
|
})
|
||||||
|
|
||||||
|
data := &User{
|
||||||
|
Fullname: "King Long Too",
|
||||||
|
Firstname: "Too",
|
||||||
|
Username: "toolong",
|
||||||
|
Email: "too@long.test",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.SaveUser(context.TODO(), data)
|
||||||
|
|
||||||
|
if err == nil || err.Error() != `properties/Password: value "" too short for "minLength" argument 1` {
|
||||||
|
t.Fatal("Should require password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveUser_UsesSysDb(t *testing.T) {
|
||||||
|
db := "test_boxtep_sys"
|
||||||
|
p := NewPersist(&PersistProps{
|
||||||
|
MongoSysDb: db,
|
||||||
|
})
|
||||||
|
|
||||||
|
data := &User{
|
||||||
|
Fullname: "King Long Too",
|
||||||
|
Firstname: "Too",
|
||||||
|
Username: "toolong",
|
||||||
|
Password: "1",
|
||||||
|
Email: "too@long.test",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.SaveUser(context.TODO(), data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Should have saved user %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user is saved
|
||||||
|
client := mongo.GetMongoClient()
|
||||||
|
|
||||||
|
collection := client.GetCollection(db, USER_COLLECTION)
|
||||||
|
|
||||||
|
var found bson.M
|
||||||
|
filter := bson.M{"username": "toolong"}
|
||||||
|
if err := collection.FindOne(context.Background(), filter).Decode(&found); err != nil {
|
||||||
|
t.Fatalf("Should have found user %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(found, "_id")
|
||||||
|
delete(found, "password")
|
||||||
|
delete(found, "created_at")
|
||||||
|
delete(found, "updated_at")
|
||||||
|
|
||||||
|
d, _ := mongo.ToMap(data)
|
||||||
|
|
||||||
|
mongo.AssertSubset(t, found, d, "Should have been equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthUser_UsingUsername(t *testing.T) {
|
||||||
|
db := "test_boxtep_sys"
|
||||||
|
p := NewPersist(&PersistProps{
|
||||||
|
MongoSysDb: db,
|
||||||
|
})
|
||||||
|
|
||||||
|
data := &User{
|
||||||
|
Fullname: "King Long Too",
|
||||||
|
Firstname: "Too",
|
||||||
|
Username: "toolong_test1",
|
||||||
|
Password: "1",
|
||||||
|
Email: "too@long.test",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.SaveUser(context.TODO(), data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Should have saved user %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := p.CheckUser(context.TODO(), "toolong_test1", "1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Should have checked %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d, _ := mongo.ToMap(data)
|
||||||
|
u, _ := mongo.ToMap(user)
|
||||||
|
|
||||||
|
delete(u, "_id")
|
||||||
|
|
||||||
|
mongo.AssertSubset(t, u, d, "Wrong user")
|
||||||
|
}
|
||||||
23
type.go
Normal file
23
type.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package persist
|
||||||
|
|
||||||
|
const USER_COLLECTION = "user"
|
||||||
|
|
||||||
|
// User can use [User.EncryptPassword] to encrypt the password
|
||||||
|
type User struct {
|
||||||
|
Id string `bson:"_id" json:"id"`
|
||||||
|
Fullname string `bson:"fullname" json:"fullname"`
|
||||||
|
Firstname string `bson:"firstname" json:"firstname"`
|
||||||
|
Username string `bson:"username" json:"username"`
|
||||||
|
Password string `bson:"password" json:"password"`
|
||||||
|
Email string `bson:"email" json:"email"`
|
||||||
|
Roles []string `bson:"roles" json:"roles"`
|
||||||
|
Credentials map[string]any `bson:"credentials" json:"credentials"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACCOUNT_COLLECTION = "account"
|
||||||
|
// Deprecate: use core.BusinessAccount
|
||||||
|
type Account struct {
|
||||||
|
Id string `bson:"_id" json:"id"`
|
||||||
|
Code string `bson:"code" json:"code"`
|
||||||
|
Owner string `bson:"owner" json:"owner"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user