feat: services: crus, migrations

This commit is contained in:
2025-01-04 01:36:01 +07:00
parent 0c9f60bba5
commit 081ebf2b28
22 changed files with 533 additions and 63 deletions

BIN
internal/dal/gen_test.db Normal file

Binary file not shown.

118
internal/dal/gen_test.go Normal file
View File

@@ -0,0 +1,118 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package dal
import (
"context"
"fmt"
"reflect"
"sync"
"testing"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Input struct {
Args []interface{}
}
type Expectation struct {
Ret []interface{}
}
type TestCase struct {
Input
Expectation
}
const _gen_test_db_name = "gen_test.db"
var _gen_test_db *gorm.DB
var _gen_test_once sync.Once
func init() {
InitializeDB()
_gen_test_db.AutoMigrate(&_another{})
}
func InitializeDB() {
_gen_test_once.Do(func() {
var err error
_gen_test_db, err = gorm.Open(sqlite.Open(_gen_test_db_name), &gorm.Config{})
if err != nil {
panic(fmt.Errorf("open sqlite %q fail: %w", _gen_test_db_name, err))
}
})
}
func assert(t *testing.T, methodName string, res, exp interface{}) {
if !reflect.DeepEqual(res, exp) {
t.Errorf("%v() gotResult = %v, want %v", methodName, res, exp)
}
}
type _another struct {
ID uint64 `gorm:"primaryKey"`
}
func (*_another) TableName() string { return "another_for_unit_test" }
func Test_Available(t *testing.T) {
if !Use(_gen_test_db).Available() {
t.Errorf("query.Available() == false")
}
}
func Test_WithContext(t *testing.T) {
query := Use(_gen_test_db)
if !query.Available() {
t.Errorf("query Use(_gen_test_db) fail: query.Available() == false")
}
type Content string
var key, value Content = "gen_tag", "unit_test"
qCtx := query.WithContext(context.WithValue(context.Background(), key, value))
for _, ctx := range []context.Context{
qCtx.Post.UnderlyingDB().Statement.Context,
} {
if v := ctx.Value(key); v != value {
t.Errorf("get value from context fail, expect %q, got %q", value, v)
}
}
}
func Test_Transaction(t *testing.T) {
query := Use(_gen_test_db)
if !query.Available() {
t.Errorf("query Use(_gen_test_db) fail: query.Available() == false")
}
err := query.Transaction(func(tx *Query) error { return nil })
if err != nil {
t.Errorf("query.Transaction execute fail: %s", err)
}
tx := query.Begin()
err = tx.SavePoint("point")
if err != nil {
t.Errorf("query tx SavePoint fail: %s", err)
}
err = tx.RollbackTo("point")
if err != nil {
t.Errorf("query tx RollbackTo fail: %s", err)
}
err = tx.Commit()
if err != nil {
t.Errorf("query tx Commit fail: %s", err)
}
err = query.Begin().Rollback()
if err != nil {
t.Errorf("query tx Rollback fail: %s", err)
}
}

View File

@@ -0,0 +1,145 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package dal
import (
"app/internal/models"
"context"
"fmt"
"testing"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/gorm/clause"
)
func init() {
InitializeDB()
err := _gen_test_db.AutoMigrate(&models.Post{})
if err != nil {
fmt.Printf("Error: AutoMigrate(&models.Post{}) fail: %s", err)
}
}
func Test_postQuery(t *testing.T) {
post := newPost(_gen_test_db)
post = *post.As(post.TableName())
_do := post.WithContext(context.Background()).Debug()
primaryKey := field.NewString(post.TableName(), clause.PrimaryKey)
_, err := _do.Unscoped().Where(primaryKey.IsNotNull()).Delete()
if err != nil {
t.Error("clean table <posts> fail:", err)
return
}
_, ok := post.GetFieldByName("")
if ok {
t.Error("GetFieldByName(\"\") from post success")
}
err = _do.Create(&models.Post{})
if err != nil {
t.Error("create item in table <posts> fail:", err)
}
err = _do.Save(&models.Post{})
if err != nil {
t.Error("create item in table <posts> fail:", err)
}
err = _do.CreateInBatches([]*models.Post{{}, {}}, 10)
if err != nil {
t.Error("create item in table <posts> fail:", err)
}
_, err = _do.Select(post.ALL).Take()
if err != nil {
t.Error("Take() on table <posts> fail:", err)
}
_, err = _do.First()
if err != nil {
t.Error("First() on table <posts> fail:", err)
}
_, err = _do.Last()
if err != nil {
t.Error("First() on table <posts> fail:", err)
}
_, err = _do.Where(primaryKey.IsNotNull()).FindInBatch(10, func(tx gen.Dao, batch int) error { return nil })
if err != nil {
t.Error("FindInBatch() on table <posts> fail:", err)
}
err = _do.Where(primaryKey.IsNotNull()).FindInBatches(&[]*models.Post{}, 10, func(tx gen.Dao, batch int) error { return nil })
if err != nil {
t.Error("FindInBatches() on table <posts> fail:", err)
}
_, err = _do.Select(post.ALL).Where(primaryKey.IsNotNull()).Order(primaryKey.Desc()).Find()
if err != nil {
t.Error("Find() on table <posts> fail:", err)
}
_, err = _do.Distinct(primaryKey).Take()
if err != nil {
t.Error("select Distinct() on table <posts> fail:", err)
}
_, err = _do.Select(post.ALL).Omit(primaryKey).Take()
if err != nil {
t.Error("Omit() on table <posts> fail:", err)
}
_, err = _do.Group(primaryKey).Find()
if err != nil {
t.Error("Group() on table <posts> fail:", err)
}
_, err = _do.Scopes(func(dao gen.Dao) gen.Dao { return dao.Where(primaryKey.IsNotNull()) }).Find()
if err != nil {
t.Error("Scopes() on table <posts> fail:", err)
}
_, _, err = _do.FindByPage(0, 1)
if err != nil {
t.Error("FindByPage() on table <posts> fail:", err)
}
_, err = _do.ScanByPage(&models.Post{}, 0, 1)
if err != nil {
t.Error("ScanByPage() on table <posts> fail:", err)
}
_, err = _do.Attrs(primaryKey).Assign(primaryKey).FirstOrInit()
if err != nil {
t.Error("FirstOrInit() on table <posts> fail:", err)
}
_, err = _do.Attrs(primaryKey).Assign(primaryKey).FirstOrCreate()
if err != nil {
t.Error("FirstOrCreate() on table <posts> fail:", err)
}
var _a _another
var _aPK = field.NewString(_a.TableName(), "id")
err = _do.Join(&_a, primaryKey.EqCol(_aPK)).Scan(map[string]interface{}{})
if err != nil {
t.Error("Join() on table <posts> fail:", err)
}
err = _do.LeftJoin(&_a, primaryKey.EqCol(_aPK)).Scan(map[string]interface{}{})
if err != nil {
t.Error("LeftJoin() on table <posts> fail:", err)
}
_, err = _do.Not().Or().Clauses().Take()
if err != nil {
t.Error("Not/Or/Clauses on table <posts> fail:", err)
}
}

View File

@@ -0,0 +1,71 @@
package database
import (
"app/internal/dal"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"log"
"sync"
"time"
)
var (
db *gorm.DB
once sync.Once
)
const Path = "database.db"
func initialize() error {
var err error
db, err = gorm.Open(sqlite.Open("file:"+Path+"?_fk=1"), &gorm.Config{})
if err != nil {
return err
}
if res := db.Exec(`PRAGMA foreign_keys = ON`); res.Error != nil {
return res.Error
}
if err := limitConnectionPool(); err != nil {
return err
}
dal.SetDefault(db)
log.Println("Initialized database")
return nil
}
func limitConnectionPool() error {
sqlDB, err := db.DB()
if err != nil {
return err
}
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
return nil
}
func Shutdown() error {
db, err := db.DB()
if err != nil {
return err
}
err = db.Close()
if err != nil {
return err
}
once = sync.Once{}
return nil
}
func GetInstance() *gorm.DB {
once.Do(func() {
err := initialize()
if err != nil {
panic(err)
}
})
return db
}

View File

@@ -0,0 +1,17 @@
package dialogs
import (
"github.com/wailsapp/wails/v3/pkg/application"
)
func InfoDialog(title string, message string) {
application.InfoDialog().SetTitle(title).SetMessage(message).Show()
}
func WarningDialog(title string, message string) {
application.WarningDialog().SetTitle(title).SetMessage(message).Show()
}
func ErrorDialog(title string, message string) {
application.ErrorDialog().SetTitle(title).SetMessage(message).Show()
}

View File

@@ -6,16 +6,12 @@ import (
)
func main() {
// Initialize the generator with configuration
g := gen.NewGenerator(gen.Config{
OutPath: "../dal", // output directory, default value is ./query
Mode: gen.WithDefaultQuery | gen.WithQueryInterface | gen.WithoutContext,
FieldNullable: true,
WithUnitTest: true,
})
// Generate default DAO interface for those specified structs
g.ApplyBasic(models.Entities...)
// Execute the generator
g.Execute()
}

View File

@@ -0,0 +1,36 @@
package services
import (
"app/internal/dialogs"
"fmt"
)
func InsertDefaultData() {
insertPosts()
}
func InsertDefaultEntityData[T any](service Service[T], entities []T) {
for _, item := range entities {
createdItem, err := service.Create(item)
if err != nil {
dialogs.ErrorDialog("Ошибка при вставке данных по умолчанию", fmt.Sprintf("Произошла ошибка при вставке значения %#v: %s", createdItem, err))
}
}
}
func insertPosts() {
InsertDefaultEntityData(&PostService{}, []Post{
{
Id: 1,
Text: "Жителям Кузбасса запретили болеть.",
},
{
Id: 2,
Text: "⚡️⚡️⚡️Дома будут летать.",
},
{
Id: 3,
Text: "В Кузбассе начали строить дома выше, чтобы жители были ближе к богу и солнцу.",
},
})
}

View File

@@ -0,0 +1,116 @@
package services
import (
"app/internal/database"
"app/internal/models"
"context"
"crypto/sha256"
"fmt"
"github.com/wailsapp/wails/v3/pkg/application"
"io"
"log/slog"
"os"
)
type Migrator struct{}
var MigratorService = application.NewService(&Migrator{})
var db = database.GetInstance()
func CalculateFileSha256Checksum(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return "", err
}
return fmt.Sprintf("%x", hasher.Sum(nil)), nil
}
func dropDatabase() error {
_ = database.Shutdown()
if _, err := os.Stat(database.Path); err == nil {
err := os.Remove(database.Path)
if err != nil {
return err
}
}
return nil
}
func createDatabase(entities ...any) error {
// Close current connections and create new database
err := dropDatabase()
if err != nil {
return err
}
db = database.GetInstance()
err = db.AutoMigrate(entities...)
if err != nil {
return err
}
InsertDefaultData()
return nil
}
func Migrate(entities ...any) error {
hashBeforeMigration, err := CalculateFileSha256Checksum(database.Path)
slog.Info("Calculating hash...")
if err != nil {
return err
}
slog.Info("Apply migrations")
err = db.AutoMigrate(entities...)
if err != nil {
slog.Info("Error occurred while migrations: %s. Recreate database...", err)
if err = createDatabase(entities...); err != nil {
slog.Error("Error occurred again: %s. Panic!", err)
return err
}
} else {
slog.Info("Calculating hash after migrations...")
var hashAfterMigration string
hashAfterMigration, err = CalculateFileSha256Checksum(database.Path)
if err != nil {
slog.Error("Failed to calc hash: %s", err)
return err
}
if hashAfterMigration != hashBeforeMigration {
slog.Info("Hashes before and after migrations are different. Recreate database...")
err = createDatabase(entities...)
if err != nil {
slog.Error("Failed to create new database: %s", err)
return err
}
}
}
slog.Info("Migrations proceeded")
return nil
}
func (migrator *Migrator) OnStartup(ctx context.Context, options application.ServiceOptions) error {
return Migrate(models.Entities...)
}
func (migrator *Migrator) OnShutdown() {
err := database.Shutdown()
if err != nil {
panic(err)
}
}

View File

@@ -4,7 +4,6 @@ import (
"app/internal/dal"
"app/internal/models"
"errors"
"gorm.io/gen/field"
"gorm.io/gorm"
)

View File

@@ -0,0 +1,10 @@
package services
type Service[T any] interface {
GetAll() ([]*T, error)
GetById(id uint) (*T, error)
Create(item T) (T, error)
Update(item T) (T, error)
Delete(item T) (T, error)
Count() (int64, error)
}

View File

@@ -0,0 +1,7 @@
package services
import "github.com/wailsapp/wails/v3/pkg/application"
var ExportedServices = []application.Service{
application.NewService(&PostService{}),
}