diff --git a/.gitignore b/.gitignore
index e660fd9..412c672 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
bin/
+database.db
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.task/checksum/linux-common-generate-bindings b/.task/checksum/linux-common-generate-bindings
index 373fc77..a9207ad 100644
--- a/.task/checksum/linux-common-generate-bindings
+++ b/.task/checksum/linux-common-generate-bindings
@@ -1 +1 @@
-7fb10105afd7388bbe8ff39ed98ded4e
+273060acaa225744e95d4c8164ad961d
diff --git a/.task/checksum/linux-common-go-mod-tidy b/.task/checksum/linux-common-go-mod-tidy
index 7be50f6..de55a35 100644
--- a/.task/checksum/linux-common-go-mod-tidy
+++ b/.task/checksum/linux-common-go-mod-tidy
@@ -1 +1 @@
-15653840ace748e3e2e77eba7f8ad2aa
+ff91e2561ee95f47290ca5b41d1bc0ec
diff --git a/frontend/bindings/app/greetservice.ts b/frontend/bindings/app/greetservice.ts
deleted file mode 100644
index 27eef2b..0000000
--- a/frontend/bindings/app/greetservice.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
-// This file is automatically generated. DO NOT EDIT
-
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore: Unused imports
-import {Call as $Call, Create as $Create} from "@wailsio/runtime";
-
-export function Greet(name: string): Promise & { cancel(): void } {
- let $resultPromise = $Call.ByID(1411160069, name) as any;
- return $resultPromise;
-}
diff --git a/frontend/bindings/app/index.ts b/frontend/bindings/app/index.ts
deleted file mode 100644
index 50e3f04..0000000
--- a/frontend/bindings/app/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
-// This file is automatically generated. DO NOT EDIT
-
-import * as GreetService from "./greetservice.js";
-export {
- GreetService
-};
diff --git a/frontend/bindings/app/internal/greetservice.ts b/frontend/bindings/app/internal/greetservice.ts
deleted file mode 100644
index fe4810b..0000000
--- a/frontend/bindings/app/internal/greetservice.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
-// This file is automatically generated. DO NOT EDIT
-
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore: Unused imports
-import {Call as $Call, Create as $Create} from "@wailsio/runtime";
-
-export function Greet(name: string): Promise & { cancel(): void } {
- let $resultPromise = $Call.ByID(1954554457, name) as any;
- return $resultPromise;
-}
diff --git a/frontend/bindings/app/internal/index.ts b/frontend/bindings/app/internal/index.ts
deleted file mode 100644
index 50e3f04..0000000
--- a/frontend/bindings/app/internal/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
-// This file is automatically generated. DO NOT EDIT
-
-import * as GreetService from "./greetservice.js";
-export {
- GreetService
-};
diff --git a/frontend/bindings/app/internal/services/greetservice.ts b/frontend/bindings/app/internal/services/greetservice.ts
deleted file mode 100644
index 9e51495..0000000
--- a/frontend/bindings/app/internal/services/greetservice.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
-// This file is automatically generated. DO NOT EDIT
-
-/**
- * TODO: implement service and migrator
- * @module
- */
-
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore: Unused imports
-import {Call as $Call, Create as $Create} from "@wailsio/runtime";
-
-export function Greet(name: string): Promise & { cancel(): void } {
- let $resultPromise = $Call.ByID(4216357642, name) as any;
- return $resultPromise;
-}
diff --git a/go.mod b/go.mod
index 642cbec..b0b88d8 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ toolchain go1.23.4
require (
github.com/wailsapp/wails/v3 v3.0.0-alpha.8.3
+ gorm.io/driver/sqlite v1.5.0
gorm.io/gen v0.3.26
gorm.io/gorm v1.25.12
gorm.io/plugin/dbresolver v1.5.3
@@ -40,6 +41,7 @@ require (
github.com/lmittmann/tint v1.0.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
diff --git a/internal/dal/gen_test.db b/internal/dal/gen_test.db
new file mode 100644
index 0000000..ac0b67d
Binary files /dev/null and b/internal/dal/gen_test.db differ
diff --git a/internal/dal/gen_test.go b/internal/dal/gen_test.go
new file mode 100644
index 0000000..6e4bb46
--- /dev/null
+++ b/internal/dal/gen_test.go
@@ -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)
+ }
+}
diff --git a/internal/dal/posts.gen_test.go b/internal/dal/posts.gen_test.go
new file mode 100644
index 0000000..ed31c2e
--- /dev/null
+++ b/internal/dal/posts.gen_test.go
@@ -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 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 fail:", err)
+ }
+
+ err = _do.Save(&models.Post{})
+ if err != nil {
+ t.Error("create item in table fail:", err)
+ }
+
+ err = _do.CreateInBatches([]*models.Post{{}, {}}, 10)
+ if err != nil {
+ t.Error("create item in table fail:", err)
+ }
+
+ _, err = _do.Select(post.ALL).Take()
+ if err != nil {
+ t.Error("Take() on table fail:", err)
+ }
+
+ _, err = _do.First()
+ if err != nil {
+ t.Error("First() on table fail:", err)
+ }
+
+ _, err = _do.Last()
+ if err != nil {
+ t.Error("First() on table 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 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 fail:", err)
+ }
+
+ _, err = _do.Select(post.ALL).Where(primaryKey.IsNotNull()).Order(primaryKey.Desc()).Find()
+ if err != nil {
+ t.Error("Find() on table fail:", err)
+ }
+
+ _, err = _do.Distinct(primaryKey).Take()
+ if err != nil {
+ t.Error("select Distinct() on table fail:", err)
+ }
+
+ _, err = _do.Select(post.ALL).Omit(primaryKey).Take()
+ if err != nil {
+ t.Error("Omit() on table fail:", err)
+ }
+
+ _, err = _do.Group(primaryKey).Find()
+ if err != nil {
+ t.Error("Group() on table 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 fail:", err)
+ }
+
+ _, _, err = _do.FindByPage(0, 1)
+ if err != nil {
+ t.Error("FindByPage() on table fail:", err)
+ }
+
+ _, err = _do.ScanByPage(&models.Post{}, 0, 1)
+ if err != nil {
+ t.Error("ScanByPage() on table fail:", err)
+ }
+
+ _, err = _do.Attrs(primaryKey).Assign(primaryKey).FirstOrInit()
+ if err != nil {
+ t.Error("FirstOrInit() on table fail:", err)
+ }
+
+ _, err = _do.Attrs(primaryKey).Assign(primaryKey).FirstOrCreate()
+ if err != nil {
+ t.Error("FirstOrCreate() on table 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 fail:", err)
+ }
+
+ err = _do.LeftJoin(&_a, primaryKey.EqCol(_aPK)).Scan(map[string]interface{}{})
+ if err != nil {
+ t.Error("LeftJoin() on table fail:", err)
+ }
+
+ _, err = _do.Not().Or().Clauses().Take()
+ if err != nil {
+ t.Error("Not/Or/Clauses on table fail:", err)
+ }
+}
diff --git a/internal/database/database.go b/internal/database/database.go
new file mode 100644
index 0000000..346ff95
--- /dev/null
+++ b/internal/database/database.go
@@ -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
+}
diff --git a/internal/dialogs/dialogs.go b/internal/dialogs/dialogs.go
new file mode 100644
index 0000000..c843a48
--- /dev/null
+++ b/internal/dialogs/dialogs.go
@@ -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()
+}
diff --git a/internal/gen/gen.go b/internal/gen/gen.go
index 9637f5e..1f59ae7 100644
--- a/internal/gen/gen.go
+++ b/internal/gen/gen.go
@@ -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()
}
diff --git a/internal/services/default_data.go b/internal/services/default_data.go
new file mode 100644
index 0000000..0072e94
--- /dev/null
+++ b/internal/services/default_data.go
@@ -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: "В Кузбассе начали строить дома выше, чтобы жители были ближе к богу и солнцу.",
+ },
+ })
+}
diff --git a/internal/services/migrator.go b/internal/services/migrator.go
new file mode 100644
index 0000000..b1589e2
--- /dev/null
+++ b/internal/services/migrator.go
@@ -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)
+ }
+}
diff --git a/internal/services/post_service_crud.go b/internal/services/post_service_crud.go
index 8430919..2bee674 100644
--- a/internal/services/post_service_crud.go
+++ b/internal/services/post_service_crud.go
@@ -4,7 +4,6 @@ import (
"app/internal/dal"
"app/internal/models"
"errors"
-
"gorm.io/gen/field"
"gorm.io/gorm"
)
diff --git a/internal/services/service.go b/internal/services/service.go
new file mode 100644
index 0000000..071953a
--- /dev/null
+++ b/internal/services/service.go
@@ -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)
+}
diff --git a/internal/services/services.go b/internal/services/services.go
new file mode 100644
index 0000000..5beb015
--- /dev/null
+++ b/internal/services/services.go
@@ -0,0 +1,7 @@
+package services
+
+import "github.com/wailsapp/wails/v3/pkg/application"
+
+var ExportedServices = []application.Service{
+ application.NewService(&PostService{}),
+}
diff --git a/main.go b/main.go
index 89140a0..0830b1a 100644
--- a/main.go
+++ b/main.go
@@ -14,9 +14,7 @@ func main() {
app := application.New(application.Options{
Name: "nto_starterkit",
Description: "A demo of using raw HTML & CSS",
- Services: []application.Service{
- application.NewService(&services.PostService{}),
- },
+ Services: append([]application.Service{services.MigratorService}, services.ExportedServices...),
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},