From f0bd6ba53a18082c7cde1501a118dbede1248d17 Mon Sep 17 00:00:00 2001 From: Quaternions Date: Tue, 26 Nov 2024 13:36:40 -0800 Subject: [PATCH] copy some gorm from data-service --- internal/controller/maps.go | 132 ++++++++++++++++++++++ internal/controller/users.go | 117 +++++++++++++++++++ internal/datastore/datastore.go | 94 +++++++++++++++ internal/datastore/filter.go | 28 +++++ internal/datastore/gormstore/db.go | 48 ++++++++ internal/datastore/gormstore/gormstore.go | 44 ++++++++ internal/datastore/gormstore/maps.go | 72 ++++++++++++ internal/datastore/gormstore/users.go | 72 ++++++++++++ internal/model/map.go | 11 ++ internal/model/page.go | 6 + internal/model/user.go | 7 ++ 11 files changed, 631 insertions(+) create mode 100644 internal/controller/maps.go create mode 100644 internal/controller/users.go create mode 100644 internal/datastore/datastore.go create mode 100644 internal/datastore/filter.go create mode 100644 internal/datastore/gormstore/db.go create mode 100644 internal/datastore/gormstore/gormstore.go create mode 100644 internal/datastore/gormstore/maps.go create mode 100644 internal/datastore/gormstore/users.go create mode 100644 internal/model/map.go create mode 100644 internal/model/page.go create mode 100644 internal/model/user.go diff --git a/internal/controller/maps.go b/internal/controller/maps.go new file mode 100644 index 0000000..9484943 --- /dev/null +++ b/internal/controller/maps.go @@ -0,0 +1,132 @@ +package controller + +import ( + "context" + "fmt" + "git.itzana.me/strafesnet/data-service/internal/datastore" + "git.itzana.me/strafesnet/data-service/internal/model" + "git.itzana.me/strafesnet/go-grpc/maps" + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "time" +) + +type Maps struct { + *maps.UnimplementedMapsServiceServer + Store datastore.Datastore +} + +func (m Maps) Get(ctx context.Context, message *maps.IdMessage) (*maps.MapResponse, error) { + item, err := m.Store.Maps().Get(ctx, message.GetID()) + if err != nil { + if err == datastore.ErrNotExist { + return nil, status.Error(codes.NotFound, "map does not exit") + } + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get map:").Error()) + } + + return &maps.MapResponse{ + ID: item.ID, + DisplayName: item.DisplayName, + Creator: item.Creator, + GameID: item.GameID, + Date: item.Date.Unix(), + }, nil +} + +func (m Maps) GetList(ctx context.Context, list *maps.IdList) (*maps.MapList, error) { + items, err := m.Store.Maps().GetList(ctx, list.ID) + if err != nil { + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get maps").Error()) + } + + var resp maps.MapList + for i := 0; i < len(items); i++ { + resp.Maps = append(resp.Maps, &maps.MapResponse{ + ID: items[i].ID, + DisplayName: items[i].DisplayName, + Creator: items[i].Creator, + GameID: items[i].GameID, + Date: items[i].Date.Unix(), + }) + } + + return &resp, nil +} + +func (m Maps) Update(ctx context.Context, request *maps.MapRequest) (*maps.NullResponse, error) { + updates := datastore.Optional() + updates.AddNotNil("display_name", request.DisplayName) + updates.AddNotNil("creator", request.Creator) + updates.AddNotNil("game_id", request.GameID) + if request.Date != nil { + updates.AddNotNil("date", time.Unix(request.GetDate(), 0)) + } + + if err := m.Store.Maps().Update(ctx, request.GetID(), updates); err != nil { + if err == datastore.ErrNotExist { + return nil, status.Error(codes.NotFound, "map does not exit") + } + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to update map:").Error()) + } + + return &maps.NullResponse{}, nil +} + +func (m Maps) Create(ctx context.Context, request *maps.MapRequest) (*maps.IdMessage, error) { + item, err := m.Store.Maps().Create(ctx, model.Map{ + ID: request.GetID(), + DisplayName: request.GetDisplayName(), + Creator: request.GetCreator(), + GameID: request.GetGameID(), + Date: time.Unix(request.GetDate(), 0), + }) + if err != nil { + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to create map:").Error()) + } + + return &maps.IdMessage{ID: item.ID}, nil +} + +func (m Maps) Delete(ctx context.Context, message *maps.IdMessage) (*maps.NullResponse, error) { + if err := m.Store.Maps().Delete(ctx, message.GetID()); err != nil { + if err == datastore.ErrNotExist { + return nil, status.Error(codes.NotFound, "map does not exit") + } + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to delete map:").Error()) + } + + return &maps.NullResponse{}, nil +} + +func (m Maps) List(ctx context.Context, request *maps.ListRequest) (*maps.MapList, error) { + filter := datastore.Optional() + fmt.Println(request) + if request.Filter != nil { + filter.AddNotNil("display_name", request.GetFilter().DisplayName) + filter.AddNotNil("creator", request.GetFilter().Creator) + filter.AddNotNil("game_id", request.GetFilter().GameID) + } + + items, err := m.Store.Maps().List(ctx, filter, model.Page{ + Number: request.GetPage().GetNumber(), + Size: request.GetPage().GetSize(), + }) + if err != nil { + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get maps:").Error()) + } + + var resp maps.MapList + for i := 0; i < len(items); i++ { + resp.Maps = append(resp.Maps, &maps.MapResponse{ + ID: items[i].ID, + DisplayName: items[i].DisplayName, + Creator: items[i].Creator, + GameID: items[i].GameID, + Date: items[i].Date.Unix(), + }) + } + + return &resp, nil +} diff --git a/internal/controller/users.go b/internal/controller/users.go new file mode 100644 index 0000000..8ceaf2f --- /dev/null +++ b/internal/controller/users.go @@ -0,0 +1,117 @@ +package controller + +import ( + "context" + "git.itzana.me/strafesnet/data-service/internal/datastore" + "git.itzana.me/strafesnet/data-service/internal/model" + "git.itzana.me/strafesnet/go-grpc/users" + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type Users struct { + *users.UnimplementedUsersServiceServer + Store datastore.Datastore +} + +func (u Users) Get(ctx context.Context, request *users.IdMessage) (*users.UserResponse, error) { + ur, err := u.Store.Users().Get(ctx, request.ID) + if err != nil { + if err == datastore.ErrNotExist { + return nil, status.Error(codes.NotFound, err.Error()) + } + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get user").Error()) + } + + return &users.UserResponse{ + ID: ur.ID, + Username: ur.Username, + StateID: ur.StateID, + }, nil +} + +func (u Users) GetList(ctx context.Context, list *users.IdList) (*users.UserList, error) { + uList, err := u.Store.Users().GetList(ctx, list.ID) + if err != nil { + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get users").Error()) + } + + var resp users.UserList + for i := 0; i < len(uList); i++ { + resp.Users = append(resp.Users, &users.UserResponse{ + ID: uList[i].ID, + Username: uList[i].Username, + StateID: uList[i].StateID, + }) + } + + return &resp, nil +} + +func (u Users) Update(ctx context.Context, request *users.UserRequest) (*users.NullResponse, error) { + updates := datastore.Optional() + updates.AddNotNil("state_id", request.StateID) + updates.AddNotNil("username", request.Username) + + if err := u.Store.Users().Update(ctx, request.GetID(), updates); err != nil { + if err == datastore.ErrNotExist { + return nil, status.Error(codes.NotFound, err.Error()) + } + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to update user").Error()) + } + + return &users.NullResponse{}, nil +} + +func (u Users) Create(ctx context.Context, request *users.UserRequest) (*users.IdMessage, error) { + us, err := u.Store.Users().Create(ctx, model.User{ + ID: request.GetID(), + Username: request.GetUsername(), + StateID: request.GetStateID(), + }) + if err != nil { + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to create user").Error()) + } + + return &users.IdMessage{ID: us.ID}, nil +} + +func (u Users) Delete(ctx context.Context, request *users.IdMessage) (*users.NullResponse, error) { + if err := u.Store.Users().Delete(ctx, request.GetID()); err != nil { + if err == datastore.ErrNotExist { + return nil, status.Error(codes.NotFound, err.Error()) + } + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to delete user").Error()) + } + + return &users.NullResponse{}, nil +} + +func (u Users) List(ctx context.Context, request *users.ListRequest) (*users.UserList, error) { + filters := datastore.Optional() + if request.Filter != nil { + filters.AddNotNil("id", request.GetFilter().ID) + filters.AddNotNil("state_id", request.GetFilter().StateID) + filters.AddNotNil("username", request.GetFilter().Username) + } + + uList, err := u.Store.Users().List(ctx, filters, model.Page{ + Number: request.GetPage().GetNumber(), + Size: request.GetPage().GetSize(), + }) + if err != nil { + return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get filtered users").Error()) + } + + var uResp users.UserList + for i := 0; i < len(uList); i++ { + uResp.Users = append(uResp.Users, &users.UserResponse{ + ID: uList[i].ID, + Username: uList[i].Username, + StateID: uList[i].StateID, + }) + } + + return &uResp, nil +} diff --git a/internal/datastore/datastore.go b/internal/datastore/datastore.go new file mode 100644 index 0000000..09df5d3 --- /dev/null +++ b/internal/datastore/datastore.go @@ -0,0 +1,94 @@ +package datastore + +import ( + "context" + "errors" + "time" + + "git.itzana.me/strafesnet/data-service/internal/model" +) + +var ( + ErrNotExist = errors.New("resource does not exist") +) + +type Datastore interface { + Times() Times + Users() Users + Bots() Bots + Maps() Maps + Events() Events + Servers() Servers + Transactions() Transactions + Ranks() Ranks +} + +type Times interface { + Get(ctx context.Context, id int64) (model.Time, error) + Create(ctx context.Context, time model.Time) (model.Time, error) + Update(ctx context.Context, id int64, values OptionalMap) error + Delete(ctx context.Context, id int64) error + List(ctx context.Context, filters OptionalMap, blacklisted bool, page model.Page, sort uint32) (int64, []model.Time, error) + Rank(ctx context.Context, id int64) (int64, error) + DistinctStylePairs(ctx context.Context) ([]model.Time, error) +} + +type Users interface { + Get(ctx context.Context, id int64) (model.User, error) + GetList(ctx context.Context, id []int64) ([]model.User, error) + Create(ctx context.Context, user model.User) (model.User, error) + Update(ctx context.Context, id int64, values OptionalMap) error + Delete(ctx context.Context, id int64) error + List(ctx context.Context, filters OptionalMap, page model.Page) ([]model.User, error) +} + +type Bots interface { + Get(ctx context.Context, id int64) (model.Bot, error) + GetList(ctx context.Context, id []int64) ([]model.Bot, error) + Create(ctx context.Context, bot model.Bot) (model.Bot, error) + Update(ctx context.Context, id int64, values OptionalMap) error + Delete(ctx context.Context, id int64) error + List(ctx context.Context, filters OptionalMap, page model.Page) ([]model.Bot, error) +} + +type Maps interface { + Get(ctx context.Context, id int64) (model.Map, error) + GetList(ctx context.Context, id []int64) ([]model.Map, error) + Create(ctx context.Context, time model.Map) (model.Map, error) + Update(ctx context.Context, id int64, values OptionalMap) error + Delete(ctx context.Context, id int64) error + List(ctx context.Context, filters OptionalMap, page model.Page) ([]model.Map, error) +} + +type Events interface { + Latest(ctx context.Context, date int64, page model.Page) ([]model.Event, error) + Create(ctx context.Context, event model.Event) (model.Event, error) + Clean(ctx context.Context) error +} + +type Servers interface { + Get(ctx context.Context, id string) (model.Server, error) + Create(ctx context.Context, server model.Server) (model.Server, error) + Update(ctx context.Context, id string, values OptionalMap) error + Delete(ctx context.Context, id string) error + DeleteByLastUpdated(ctx context.Context, date time.Time) error + List(ctx context.Context, filters OptionalMap, page model.Page) ([]model.Server, error) +} + +type Transactions interface { + Balance(ctx context.Context, user int64) (int64, error) + Get(ctx context.Context, id string) (model.Transaction, error) + Create(ctx context.Context, transaction model.Transaction) (model.Transaction, error) + Update(ctx context.Context, id string, values OptionalMap) error + Delete(ctx context.Context, id string) error + List(ctx context.Context, filters OptionalMap, page model.Page) ([]model.Transaction, error) +} + +type Ranks interface { + Delete(ctx context.Context, id int64) error + Get(ctx context.Context, user int64, style, game, mode int32, state []int32) (model.Rank, error) + List(ctx context.Context, style, game, mode int32, sort int64, state []int32, page model.Page) ([]model.Rank, error) + UpdateRankCalc(ctx context.Context) error + UpdateAll(ctx context.Context, style, game, mode int32) error + UpdateUsers(ctx context.Context, style, game, mode int32, users []int) error +} diff --git a/internal/datastore/filter.go b/internal/datastore/filter.go new file mode 100644 index 0000000..43f0d67 --- /dev/null +++ b/internal/datastore/filter.go @@ -0,0 +1,28 @@ +package datastore + +import "reflect" + +type OptionalMap struct { + filter map[string]interface{} +} + +func Optional() OptionalMap { + return OptionalMap{filter: map[string]interface{}{}} +} + +func (q OptionalMap) Add(column string, value interface{}) OptionalMap { + q.filter[column] = value + return q +} + +func (q OptionalMap) AddNotNil(column string, value interface{}) OptionalMap { + if value == nil || (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) { + return q + } + q.Add(column, value) + return q +} + +func (q OptionalMap) Map() map[string]interface{} { + return q.filter +} diff --git a/internal/datastore/gormstore/db.go b/internal/datastore/gormstore/db.go new file mode 100644 index 0000000..ed36f6b --- /dev/null +++ b/internal/datastore/gormstore/db.go @@ -0,0 +1,48 @@ +package gormstore + +import ( + "fmt" + "os" + "time" + + "git.itzana.me/strafesnet/data-service/internal/datastore" + "git.itzana.me/strafesnet/data-service/internal/model" + "git.itzana.me/strafesnet/utils/logger" + "github.com/eko/gocache/lib/v4/cache" + gocache_store "github.com/eko/gocache/store/go_cache/v4" + gocache "github.com/patrickmn/go-cache" + log "github.com/sirupsen/logrus" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +func New(migrate bool) (datastore.Datastore, error) { + db, err := gorm.Open(postgres.Open(fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s", os.Getenv("PG_HOST"), os.Getenv("PG_USER"), os.Getenv("PG_PASS"), os.Getenv("PG_DB"), os.Getenv("PG_PORT"))), &gorm.Config{ + Logger: logger.New()}) + if err != nil { + log.WithFields(log.Fields{ + "PG_USER": os.Getenv("PG_USER"), + "PG_HOST": os.Getenv("PG_HOST"), + "PG_PORT": os.Getenv("PG_PORT"), + "PG_DB": os.Getenv("PG_DB"), + "error": err, + }).Errorln("failed to connect to database") + return nil, err + } + + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + sqlDB.SetMaxIdleConns(10) + sqlDB.SetMaxOpenConns(25) + + if migrate { + if err := db.AutoMigrate(&model.Time{}, &model.User{}, &model.Bot{}, &model.Map{}, &model.Event{}, &model.Server{}, &model.Transaction{}, &model.Rank{}, &model.RankCalc{}); err != nil { + log.WithField("error", err).Errorln("database migration failed") + return nil, err + } + } + + return &Gormstore{db, cache.New[[]byte](gocache_store.NewGoCache(gocache.New(5*time.Minute, 10*time.Minute)))}, nil +} diff --git a/internal/datastore/gormstore/gormstore.go b/internal/datastore/gormstore/gormstore.go new file mode 100644 index 0000000..0e495c3 --- /dev/null +++ b/internal/datastore/gormstore/gormstore.go @@ -0,0 +1,44 @@ +package gormstore + +import ( + "git.itzana.me/strafesnet/data-service/internal/datastore" + "github.com/eko/gocache/lib/v4/cache" + "gorm.io/gorm" +) + +type Gormstore struct { + db *gorm.DB + cache *cache.Cache[[]byte] +} + +func (g Gormstore) Times() datastore.Times { + return &Times{db: g.db} +} + +func (g Gormstore) Users() datastore.Users { + return &Users{db: g.db} +} + +func (g Gormstore) Bots() datastore.Bots { + return &Bots{db: g.db} +} + +func (g Gormstore) Maps() datastore.Maps { + return &Maps{db: g.db} +} + +func (g Gormstore) Events() datastore.Events { + return &Events{db: g.db} +} + +func (g Gormstore) Servers() datastore.Servers { + return &Servers{db: g.db} +} + +func (g Gormstore) Transactions() datastore.Transactions { + return &Transactions{db: g.db} +} + +func (g Gormstore) Ranks() datastore.Ranks { + return &Ranks{db: g.db, cache: g.cache} +} diff --git a/internal/datastore/gormstore/maps.go b/internal/datastore/gormstore/maps.go new file mode 100644 index 0000000..baeaeaf --- /dev/null +++ b/internal/datastore/gormstore/maps.go @@ -0,0 +1,72 @@ +package gormstore + +import ( + "context" + "git.itzana.me/strafesnet/data-service/internal/datastore" + "git.itzana.me/strafesnet/data-service/internal/model" + "gorm.io/gorm" +) + +type Maps struct { + db *gorm.DB +} + +func (m Maps) Get(ctx context.Context, id int64) (model.Map, error) { + var smap model.Map + if err := m.db.WithContext(ctx).First(&smap, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return smap, datastore.ErrNotExist + } + return smap, err + } + + return smap, nil +} + +func (m Maps) GetList(ctx context.Context, id []int64) ([]model.Map, error) { + var mapList []model.Map + if err := m.db.WithContext(ctx).Find(&mapList, "id IN ?", id).Error; err != nil { + return mapList, err + } + + return mapList, nil +} + +func (m Maps) Create(ctx context.Context, smap model.Map) (model.Map, error) { + if err := m.db.WithContext(ctx).Create(&smap).Error; err != nil { + return smap, err + } + + return smap, nil +} + +func (m Maps) Update(ctx context.Context, id int64, values datastore.OptionalMap) error { + if err := m.db.WithContext(ctx).Model(&model.Map{}).Where("id = ?", id).Updates(values.Map()).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return datastore.ErrNotExist + } + return err + } + + return nil +} + +func (m Maps) Delete(ctx context.Context, id int64) error { + if err := m.db.WithContext(ctx).Delete(&model.Map{}, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return datastore.ErrNotExist + } + return err + } + + return nil +} + +func (m Maps) List(ctx context.Context, filters datastore.OptionalMap, page model.Page) ([]model.Map, error) { + var maps []model.Map + if err := m.db.WithContext(ctx).Where(filters.Map()).Offset(int((page.Number - 1) * page.Size)).Limit(int(page.Size)).Find(&maps).Error; err != nil { + return nil, err + } + + return maps, nil +} diff --git a/internal/datastore/gormstore/users.go b/internal/datastore/gormstore/users.go new file mode 100644 index 0000000..9a3897c --- /dev/null +++ b/internal/datastore/gormstore/users.go @@ -0,0 +1,72 @@ +package gormstore + +import ( + "context" + "git.itzana.me/strafesnet/data-service/internal/datastore" + "git.itzana.me/strafesnet/data-service/internal/model" + "gorm.io/gorm" +) + +type Users struct { + db *gorm.DB +} + +func (u Users) Get(ctx context.Context, id int64) (model.User, error) { + var user model.User + if err := u.db.WithContext(ctx).First(&user, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return user, datastore.ErrNotExist + } + return user, err + } + + return user, nil +} + +func (u Users) GetList(ctx context.Context, id []int64) ([]model.User, error) { + var user []model.User + if err := u.db.WithContext(ctx).Find(&user, "id IN ?", id).Error; err != nil { + return user, err + } + + return user, nil +} + +func (u Users) Create(ctx context.Context, user model.User) (model.User, error) { + if err := u.db.WithContext(ctx).Create(&user).Error; err != nil { + return user, err + } + + return user, nil +} + +func (u Users) Update(ctx context.Context, id int64, values datastore.OptionalMap) error { + if err := u.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).Updates(values.Map()).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return datastore.ErrNotExist + } + return err + } + + return nil +} + +func (u Users) Delete(ctx context.Context, id int64) error { + if err := u.db.WithContext(ctx).Delete(&model.User{}, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return datastore.ErrNotExist + } + return err + } + + return nil +} + +func (u Users) List(ctx context.Context, filters datastore.OptionalMap, page model.Page) ([]model.User, error) { + var users []model.User + if err := u.db.WithContext(ctx).Where(filters.Map()).Offset(int((page.Number - 1) * page.Size)).Limit(int(page.Size)).Find(&users).Error; err != nil { + return nil, err + } + + return users, nil +} diff --git a/internal/model/map.go b/internal/model/map.go new file mode 100644 index 0000000..a96728b --- /dev/null +++ b/internal/model/map.go @@ -0,0 +1,11 @@ +package model + +import "time" + +type Map struct { + ID int64 + DisplayName string + Creator string + GameID int32 + Date time.Time +} diff --git a/internal/model/page.go b/internal/model/page.go new file mode 100644 index 0000000..37ff8be --- /dev/null +++ b/internal/model/page.go @@ -0,0 +1,6 @@ +package model + +type Page struct { + Number int32 + Size int32 +} diff --git a/internal/model/user.go b/internal/model/user.go new file mode 100644 index 0000000..ba6d80b --- /dev/null +++ b/internal/model/user.go @@ -0,0 +1,7 @@ +package model + +type User struct { + ID int64 + Username string `gorm:"not null"` + StateID int32 `gorm:"not null;default:0"` +}