feat: initial commit
This commit is contained in:
@ -1,15 +1,61 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var models = []any{
|
||||
&Player{},
|
||||
&QuizCategory{},
|
||||
&QuizEntry{},
|
||||
&QuizTurn{},
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
gorm.Model
|
||||
|
||||
Name string
|
||||
|
||||
UserID string `gorm:"index"`
|
||||
UserProvider string `gorm:"index"`
|
||||
|
||||
Score int
|
||||
|
||||
PlayedAt time.Time
|
||||
}
|
||||
|
||||
type QuizTurn struct {
|
||||
gorm.Model
|
||||
|
||||
StartedAt time.Time `gorm:"index"`
|
||||
EndedAt time.Time `gorm:"index"`
|
||||
|
||||
Entries []*QuizEntry `gorm:"many2many:quiz_turn_entries;"`
|
||||
}
|
||||
|
||||
type QuizCategory struct {
|
||||
gorm.Model
|
||||
|
||||
Name string `gorm:"index"`
|
||||
Theme string `gorm:"index"`
|
||||
Description string
|
||||
}
|
||||
|
||||
type QuizEntry struct {
|
||||
gorm.Model
|
||||
|
||||
Category *QuizCategory
|
||||
CategoryID uint
|
||||
|
||||
Provider string `gorm:"index"`
|
||||
ProviderID string `gorm:"index"`
|
||||
|
||||
Question string
|
||||
Propositions datatypes.JSONSlice[string]
|
||||
Answer string
|
||||
Level uint `gorm:"index"`
|
||||
Anecdote string
|
||||
}
|
||||
|
28
internal/store/player.go
Normal file
28
internal/store/player.go
Normal file
@ -0,0 +1,28 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *Store) UpsertPlayer(ctx context.Context, player *Player) error {
|
||||
return errors.WithStack(s.Do(ctx, func(db *gorm.DB) error {
|
||||
var existing *Player
|
||||
err := db.Find(&existing, "user_id = ? and user_provider = ?", player.UserID, player.UserProvider).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
player.Model = existing.Model
|
||||
}
|
||||
|
||||
if err := db.Save(player).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}))
|
||||
}
|
114
internal/store/quiz.go
Normal file
114
internal/store/quiz.go
Normal file
@ -0,0 +1,114 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *Store) UpsertQuizCategory(ctx context.Context, category *QuizCategory) error {
|
||||
return errors.WithStack(s.Do(ctx, func(db *gorm.DB) error {
|
||||
var existing *QuizCategory
|
||||
err := db.Find(&existing, "name = ? and theme = ?", category.Name, category.Theme).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
category.Model = existing.Model
|
||||
}
|
||||
|
||||
if err := db.Save(category).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *Store) UpsertQuizEntry(ctx context.Context, entry *QuizEntry) error {
|
||||
return errors.WithStack(s.Do(ctx, func(db *gorm.DB) error {
|
||||
var existing *QuizEntry
|
||||
err := db.Find(&existing, "provider = ? and provider_id = ?", entry.Provider, entry.ProviderID).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
entry.Model = existing.Model
|
||||
}
|
||||
|
||||
if err := db.Save(entry).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *Store) GetQuizTurn(ctx context.Context, playInterval time.Duration) (*QuizTurn, error) {
|
||||
var quizTurn *QuizTurn
|
||||
err := s.Tx(ctx, func(tx *gorm.DB) error {
|
||||
now := time.Now().UTC()
|
||||
|
||||
err := tx.Model(&quizTurn).Preload("Entries").Preload("Entries.Category").Order("ended_at DESC").First(&quizTurn, "ended_at >= ?", now).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if quizTurn != nil && quizTurn.ID != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
quizTurn = &QuizTurn{
|
||||
StartedAt: now.Round(time.Hour),
|
||||
EndedAt: now.Add(playInterval).Round(time.Hour),
|
||||
}
|
||||
|
||||
alreadyUsed := make([]uint, 0)
|
||||
err = tx.Table("quiz_turn_entries").Pluck("quiz_entry_id", &alreadyUsed).Error
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
query := tx.Model(&QuizEntry{})
|
||||
|
||||
if len(alreadyUsed) > 0 {
|
||||
query = query.Where("id NOT IN ?", alreadyUsed)
|
||||
}
|
||||
|
||||
var entryIDs []uint
|
||||
err = query.Pluck("id", &entryIDs).Error
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for range 3 {
|
||||
index := rand.IntN(len(entryIDs))
|
||||
quizTurn.Entries = append(quizTurn.Entries, &QuizEntry{
|
||||
Model: gorm.Model{
|
||||
ID: entryIDs[index],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if err := tx.Save(quizTurn).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
err = tx.Model(&QuizTurn{}).Preload("Entries").Preload("Entries.Category").First(&quizTurn).Error
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return quizTurn, nil
|
||||
}
|
@ -2,6 +2,7 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@ -31,6 +32,21 @@ func (s *Store) Do(ctx context.Context, fn func(db *gorm.DB) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) Tx(ctx context.Context, fn func(db *gorm.DB) error, opts ...*sql.TxOptions) error {
|
||||
return errors.WithStack(s.Do(ctx, func(db *gorm.DB) error {
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := fn(tx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return nil
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
func createGetDatabase(db *gorm.DB) func(ctx context.Context) (*gorm.DB, error) {
|
||||
var (
|
||||
migrateOnce sync.Once
|
||||
|
27
internal/store/types.go
Normal file
27
internal/store/types.go
Normal file
@ -0,0 +1,27 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type StringSlice []string
|
||||
|
||||
func (s StringSlice) Scan(src any) error {
|
||||
bytes, ok := src.([]byte)
|
||||
if !ok {
|
||||
return errors.New("src value cannot cast to []byte")
|
||||
}
|
||||
|
||||
s = strings.Split(string(bytes), "|")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s StringSlice) Value() (driver.Value, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return strings.Join(s, "|"), nil
|
||||
}
|
Reference in New Issue
Block a user