Initial commit

This commit is contained in:
Vojtěch Káně
2020-12-03 23:07:44 +01:00
commit 28e22e3422
24 changed files with 1526 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
package schema
import (
"github.com/facebook/ent"
"github.com/facebook/ent/schema/edge"
"github.com/facebook/ent/schema/field"
"github.com/google/uuid"
)
type Answer struct {
ent.Schema
}
func (Answer) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.New()).Immutable().Unique(),
field.Time("answered").Immutable(),
}
}
func (Answer) Indexes() []ent.Index {
return []ent.Index{
}
}
func (Answer) Edges() []ent.Edge {
return []ent.Edge{
edge.From("choice", Choice.Type).
Ref("answers").
Unique().
Required(),
edge.From("answerer", Player.Type).
Ref("answers").
Unique().
Required(),
}
}
func (Answer) Config() ent.Config {
return ent.Config{
Table: "answers",
}
}

View File

@@ -0,0 +1,41 @@
package schema
import (
"github.com/facebook/ent"
"github.com/facebook/ent/schema/edge"
"github.com/facebook/ent/schema/field"
"github.com/google/uuid"
)
type Choice struct {
ent.Schema
}
func (Choice) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.New()).Immutable(),
field.Text("title").MinLen(1).MaxLen(256),
field.Bool("correct"),
}
}
func (Choice) Indexes() []ent.Index {
return []ent.Index{
}
}
func (Choice) Edges() []ent.Edge {
return []ent.Edge{
edge.From("question", Question.Type).
Ref("choices").
Unique().
Required(),
edge.To("answers", Answer.Type),
}
}
func (Choice) Config() ent.Config {
return ent.Config{
Table: "options",
}
}

View File

@@ -0,0 +1,34 @@
package schema
import (
"github.com/facebook/ent"
"github.com/facebook/ent/schema/edge"
"github.com/facebook/ent/schema/field"
"github.com/google/uuid"
)
type Game struct {
ent.Schema
}
func (Game) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.New()).Unique().Immutable(),
field.Text("name").MaxLen(64),
field.Time("created").Immutable(),
field.Text("author").MaxLen(64),
}
}
func (Game) Edges() []ent.Edge {
return []ent.Edge{
edge.To("sessions", Session.Type),
edge.To("questions", Question.Type),
}
}
func (Game) Config() ent.Config {
return ent.Config{
Table: "games",
}
}

View File

@@ -0,0 +1,45 @@
package schema
import (
"github.com/facebook/ent"
"github.com/facebook/ent/schema/edge"
"github.com/facebook/ent/schema/field"
"github.com/facebook/ent/schema/index"
"github.com/google/uuid"
"regexp"
)
type Player struct {
ent.Schema
}
func (Player) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.New()).Immutable(),
field.Text("name").MaxLen(64).MinLen(1).Match(regexp.MustCompile("(?:[a-z]|[A-Z]|_|-|.|,|[0-9])+")),
field.Time("joined").Immutable(),
field.Bool("organiser").Default(false),
}
}
func (Player) Indexes() []ent.Index {
return []ent.Index{
index.Fields("name").Edges("session").Unique(),
}
}
func (Player) Edges() []ent.Edge {
return []ent.Edge{
edge.From("session", Session.Type).
Ref("players").
Unique().
Required(),
edge.To("answers", Answer.Type),
}
}
func (Player) Config() ent.Config {
return ent.Config{
Table: "players",
}
}

View File

@@ -0,0 +1,44 @@
package schema
import (
"github.com/facebook/ent"
"github.com/facebook/ent/schema/edge"
"github.com/facebook/ent/schema/field"
"github.com/facebook/ent/schema/index"
"github.com/google/uuid"
)
type Question struct {
ent.Schema
}
func (Question) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.New()).Immutable(),
field.Text("title").MaxLen(256).MinLen(1),
field.Int("order"),
}
}
func (Question) Indexes() []ent.Index {
return []ent.Index{
index.Fields("order").Edges("game").Unique(),
}
}
func (Question) Edges() []ent.Edge {
return []ent.Edge{
edge.From("game", Game.Type).
Ref("questions").
Unique().
Required(),
edge.To("choices", Choice.Type),
edge.To("current_sessions", Session.Type),
}
}
func (Question) Config() ent.Config {
return ent.Config{
Table: "questions",
}
}

View File

@@ -0,0 +1,46 @@
package schema
import (
"github.com/facebook/ent"
"github.com/facebook/ent/schema/edge"
"github.com/facebook/ent/schema/field"
"github.com/google/uuid"
)
type Session struct {
ent.Schema
}
func (Session) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.New()).Immutable(),
field.Time("created").Immutable(),
field.Time("started").Nillable().Optional(), // TODO remove?
field.Time("current_question_until").Nillable().Optional(),
field.String("code").MinLen(6).MaxLen(6).Immutable().Unique(),
}
}
func (Session) Indexes() []ent.Index {
return []ent.Index{
}
}
func (Session) Edges() []ent.Edge {
return []ent.Edge{
edge.From("game", Game.Type).
Ref("sessions").
Unique().
Required(),
edge.To("players", Player.Type),
edge.From("current_question", Question.Type).
Ref("current_sessions").
Unique(),
}
}
func (Session) Config() ent.Config {
return ent.Config{
Table: "sessions",
}
}

168
pkg/model/model.go Normal file
View File

@@ -0,0 +1,168 @@
package model
import (
"context"
"database/sql"
"errors"
"github.com/google/uuid"
"time"
"vkane.cz/tinyquiz/pkg/model/ent"
"vkane.cz/tinyquiz/pkg/model/ent/player"
"vkane.cz/tinyquiz/pkg/model/ent/question"
"vkane.cz/tinyquiz/pkg/model/ent/session"
"vkane.cz/tinyquiz/pkg/rtcomm"
)
var NoSuchEntity = errors.New("no such entity found")
var ConstraintViolation = errors.New("constraint violation")
type Model struct {
c *ent.Client
}
func NewModel(c *ent.Client) *Model {
return &Model{c: c}
}
type Stats struct {
Games uint64
Players uint64
Sessions uint64
}
func (m *Model) GetStats(c context.Context) (Stats, error) {
var s Stats
if games, err := m.c.Game.Query().Count(c); err == nil {
s.Games = uint64(games)
} else {
return s, err
}
if players, err := m.c.Player.Query().Count(c); err == nil {
s.Players = uint64(players)
} else {
return s, err
}
if sessions, err := m.c.Session.Query().Count(c); err == nil {
s.Sessions = uint64(sessions)
} else {
return s, err
}
return s, nil
}
// returns the player's UUID if error is nil
// err = NoSuchEntity if the sessionsCode is incorrect
func (m *Model) RegisterPlayer(playerName string, sessionCode string, c context.Context) (*ent.Player, error) {
tx, err := m.c.BeginTx(c, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
})
if err != nil {
return nil, err
}
defer tx.Commit()
if s, err := tx.Session.Query().Where(session.Code(sessionCode)).Only(c); ent.IsNotFound(err) {
return nil, NoSuchEntity
} else if err != nil {
return nil, err
} else {
if p, err := tx.Player.Create().SetID(uuid.New()).SetJoined(time.Now()).SetName(playerName).SetSession(s).Save(c); err == nil {
return p, nil
} else if ent.IsConstraintError(err) {
return nil, ConstraintViolation
} else {
return nil, err
}
}
}
func (m *Model) GetPlayerWithSessionAndGame(uid uuid.UUID, c context.Context) (*ent.Player, error) {
tx, err := m.c.BeginTx(c, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
ReadOnly: true,
})
if err != nil {
return nil, err
}
defer tx.Commit()
if p, err := tx.Player.Query().Where(player.ID(uid)).WithSession(func(q *ent.SessionQuery) {
q.WithGame()
}).Only(c); err == nil {
return p, nil
} else if ent.IsNotFound(err) {
return nil, NoSuchEntity
} else {
return nil, err
}
}
func (m *Model) GetPlayersStateUpdate(sessionId uuid.UUID, c context.Context) (rtcomm.StateUpdate, error) {
tx, err := m.c.BeginTx(c, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
ReadOnly: true,
})
if err != nil {
return rtcomm.StateUpdate{}, err
}
defer tx.Commit()
if players, err := tx.Player.Query().Where(player.HasSessionWith(session.ID(sessionId))).Order(ent.Asc(player.FieldJoined)).All(c); err == nil {
var su rtcomm.StateUpdate
su.Players = make([]rtcomm.Player, 0, len(players))
for i := 0; i < len(players); i++ {
su.Players = append(su.Players, rtcomm.Player{
Organiser: players[i].Organiser,
Name: players[i].Name,
})
}
return su, nil
} else {
return rtcomm.StateUpdate{}, err
}
}
func (m *Model) GetQuestionStateUpdate(sessionId uuid.UUID, c context.Context) (rtcomm.StateUpdate, error) {
tx, err := m.c.BeginTx(c, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
ReadOnly: true,
})
if err != nil {
return rtcomm.StateUpdate{}, err
}
defer tx.Commit()
if q, err := tx.Question.Query().Where(question.HasCurrentSessionsWith(session.ID(sessionId))).WithChoices().Only(c); err == nil {
var qu rtcomm.QuestionUpdate
qu.Title = q.Title
qu.Answers = make([]rtcomm.Answer, 0, len(q.Edges.Choices))
for i := 0; i < len(q.Edges.Choices); i++ {
qu.Answers = append(qu.Answers, rtcomm.Answer{
ID: q.Edges.Choices[i].ID.String(),
Title: q.Edges.Choices[i].Title,
})
}
return rtcomm.StateUpdate{Question: &qu}, nil
} else if ent.IsNotFound(err) {
// There is simply no current question, which is not an error
return rtcomm.StateUpdate{}, nil
} else {
return rtcomm.StateUpdate{}, err
}
}
//TODO reuse transaction
func (m *Model) GetFullStateUpdate(sessionId uuid.UUID, c context.Context) (rtcomm.StateUpdate, error) {
su, err := m.GetPlayersStateUpdate(sessionId, c)
if err != nil {
return rtcomm.StateUpdate{}, err
}
if su2, err := m.GetQuestionStateUpdate(sessionId, c); err == nil {
su.Question = su2.Question
} else {
return rtcomm.StateUpdate{}, err
}
return su, nil
}

63
pkg/rtcomm/rtcomm.go Normal file
View File

@@ -0,0 +1,63 @@
package rtcomm
import (
"github.com/google/uuid"
"sync"
)
// TODO associate lock with individual clients to prevent global locking
type Clients struct {
sync.RWMutex
clients map[uuid.UUID][]chan StateUpdate
}
func NewClients() *Clients {
return &Clients{
clients: make(map[uuid.UUID][]chan StateUpdate),
}
}
func (c *Clients) AddClient(id uuid.UUID, client chan StateUpdate) {
c.Lock()
defer c.Unlock()
c.clients[id] = append(c.clients[id], client)
}
//TODO remove debug
func (c *Clients) Count() (sessions, clients uint) {
c.RLock()
defer c.RUnlock()
for _, s := range c.clients {
sessions++
clients += uint(len(s))
}
return
}
// TODO optimize
func (c *Clients) RemoveClient(id uuid.UUID, client chan StateUpdate) {
c.Lock()
defer c.Unlock()
var newClients = make([]chan StateUpdate, 0, len(c.clients[id]) - 1)
for i := 0; i < len(c.clients[id]); i++ {
if c.clients[id][i] != client {
newClients = append(newClients, c.clients[id][i])
}
}
c.clients[id] = newClients
}
func (c *Clients) SendToAll(id uuid.UUID, su StateUpdate) (sent, dropped uint) {
c.RLock()
defer c.RUnlock()
for i := 0; i < len(c.clients[id]); i++ {
select {
case c.clients[id][i] <- su:
sent++
default:
dropped++
}
}
return
}

21
pkg/rtcomm/stateUpdate.go Normal file
View File

@@ -0,0 +1,21 @@
package rtcomm
type StateUpdate struct {
Players []Player `json:"players"`
Question *QuestionUpdate `json:"question,omitempty"`
}
type Player struct {
Organiser bool `json:"organiser"`
Name string `json:"name"`
}
type QuestionUpdate struct {
Title string `json:"title"`
Answers []Answer `json:"answers"`
}
type Answer struct{
ID string `json:"id"`
Title string `json:"title"`
}