feat: initial commit

This commit is contained in:
2025-06-13 16:55:46 +02:00
parent 1fb753469e
commit 85f0bc1024
23 changed files with 11758 additions and 45 deletions

View File

@ -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
View 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
View 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
}

View File

@ -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
View 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
}