From 081ebf2b28eaeecc60b12182b7f2ed0c94d7f0f0 Mon Sep 17 00:00:00 2001 From: gogacoder Date: Sat, 4 Jan 2025 01:36:01 +0700 Subject: [PATCH] feat: services: crus, migrations --- .gitignore | 1 + .idea/vcs.xml | 6 + .task/checksum/linux-common-generate-bindings | 2 +- .task/checksum/linux-common-go-mod-tidy | 2 +- frontend/bindings/app/greetservice.ts | 11 -- frontend/bindings/app/index.ts | 7 - .../bindings/app/internal/greetservice.ts | 11 -- frontend/bindings/app/internal/index.ts | 7 - .../app/internal/services/greetservice.ts | 16 -- go.mod | 2 + internal/dal/gen_test.db | Bin 0 -> 12288 bytes internal/dal/gen_test.go | 118 ++++++++++++++ internal/dal/posts.gen_test.go | 145 ++++++++++++++++++ internal/database/database.go | 71 +++++++++ internal/dialogs/dialogs.go | 17 ++ internal/gen/gen.go | 6 +- internal/services/default_data.go | 36 +++++ internal/services/migrator.go | 116 ++++++++++++++ internal/services/post_service_crud.go | 1 - internal/services/service.go | 10 ++ internal/services/services.go | 7 + main.go | 4 +- 22 files changed, 533 insertions(+), 63 deletions(-) create mode 100644 .idea/vcs.xml delete mode 100644 frontend/bindings/app/greetservice.ts delete mode 100644 frontend/bindings/app/index.ts delete mode 100644 frontend/bindings/app/internal/greetservice.ts delete mode 100644 frontend/bindings/app/internal/index.ts delete mode 100644 frontend/bindings/app/internal/services/greetservice.ts create mode 100644 internal/dal/gen_test.db create mode 100644 internal/dal/gen_test.go create mode 100644 internal/dal/posts.gen_test.go create mode 100644 internal/database/database.go create mode 100644 internal/dialogs/dialogs.go create mode 100644 internal/services/default_data.go create mode 100644 internal/services/migrator.go create mode 100644 internal/services/service.go create mode 100644 internal/services/services.go 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 0000000000000000000000000000000000000000..ac0b67d47652549879a544b69d2cb6f5c3b3feff GIT binary patch literal 12288 zcmeI#Jxjzu5C-7=$Z^%NS_Q#kEAim4vAEVKDZ~@bculHo#8m^DNHT(rMgFgijg5_y z$bm}iRo;PpGf9?gc)B@1TNLE&RaM{RWcS5}kWw62D})&F(!AECxCz!be3hU2pW2bw z%eE){R+$}PS`HKhAOHafKmY;|fB*y_009U<;MWSIY8-~4Y&R*txb^o{Lrow0D2bz0 zY**3YBDSu7=IpL3E}bn(^4GqeIr5L>Y?rfTa(o&ksXd9)k6qL0lN^Whxoo%kPv>Pt zH@?nz9?u@if->?A4L(#~j~?9f{Z|H#?`Crz`cV*o00bZa0SG_<0uX=z1Rwwb2>fG# wZvGF 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), },