From 385c3f8a1543b4916a9518fcf919e533093710b9 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Sat, 23 Mar 2024 11:14:53 +0900 Subject: [PATCH 01/26] =?UTF-8?q?feat:=20Inverse=E3=82=A8=E3=83=83?= =?UTF-8?q?=E3=82=B8=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/domain/repository/ent/client.go | 16 +++ .../domain/repository/ent/migrate/schema.go | 4 +- .../domain/repository/ent/mutation.go | 127 +++++++++++++----- .../domain/repository/ent/runtime.go | 16 ++- .../domain/repository/ent/schema/score.go | 15 ++- .../domain/repository/ent/schema/user.go | 4 +- typing-server/domain/repository/ent/score.go | 37 ++++- .../domain/repository/ent/score/score.go | 32 ++++- .../domain/repository/ent/score/where.go | 42 ++++-- .../domain/repository/ent/score_create.go | 48 ++++++- .../domain/repository/ent/score_query.go | 88 +++++++++++- .../domain/repository/ent/score_update.go | 127 ++++++++++++++---- typing-server/domain/repository/ent/user.go | 16 +-- .../domain/repository/ent/user/user.go | 12 +- .../domain/repository/ent/user/where.go | 56 ++++---- .../domain/repository/ent/user_create.go | 12 +- .../domain/repository/ent/user_query.go | 4 +- .../domain/repository/ent/user_update.go | 24 ++-- 18 files changed, 534 insertions(+), 146 deletions(-) diff --git a/typing-server/domain/repository/ent/client.go b/typing-server/domain/repository/ent/client.go index 8b9be77..7df8704 100644 --- a/typing-server/domain/repository/ent/client.go +++ b/typing-server/domain/repository/ent/client.go @@ -316,6 +316,22 @@ func (c *ScoreClient) GetX(ctx context.Context, id uuid.UUID) *Score { return obj } +// QueryUser queries the user edge of a Score. +func (c *ScoreClient) QueryUser(s *Score) *UserQuery { + query := (&UserClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := s.ID + step := sqlgraph.NewStep( + sqlgraph.From(score.Table, score.FieldID, id), + sqlgraph.To(user.Table, user.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, score.UserTable, score.UserColumn), + ) + fromV = sqlgraph.Neighbors(s.driver.Dialect(), step) + return fromV, nil + } + return query +} + // Hooks returns the client hooks. func (c *ScoreClient) Hooks() []Hook { return c.hooks.Score diff --git a/typing-server/domain/repository/ent/migrate/schema.go b/typing-server/domain/repository/ent/migrate/schema.go index d0fa83f..2a4d8a0 100644 --- a/typing-server/domain/repository/ent/migrate/schema.go +++ b/typing-server/domain/repository/ent/migrate/schema.go @@ -14,7 +14,7 @@ var ( {Name: "keystrokes", Type: field.TypeInt}, {Name: "accuracy", Type: field.TypeFloat64}, {Name: "created_at", Type: field.TypeTime}, - {Name: "user_scores", Type: field.TypeUUID, Nullable: true}, + {Name: "user_scores", Type: field.TypeUUID}, } // ScoresTable holds the schema information for the "scores" table. ScoresTable = &schema.Table{ @@ -26,7 +26,7 @@ var ( Symbol: "scores_users_scores", Columns: []*schema.Column{ScoresColumns[4]}, RefColumns: []*schema.Column{UsersColumns[0]}, - OnDelete: schema.SetNull, + OnDelete: schema.NoAction, }, }, } diff --git a/typing-server/domain/repository/ent/mutation.go b/typing-server/domain/repository/ent/mutation.go index 1f505a9..99be27b 100644 --- a/typing-server/domain/repository/ent/mutation.go +++ b/typing-server/domain/repository/ent/mutation.go @@ -40,8 +40,10 @@ type ScoreMutation struct { addkeystrokes *int accuracy *float64 addaccuracy *float64 - createdAt *time.Time + created_at *time.Time clearedFields map[string]struct{} + user *uuid.UUID + cleareduser bool done bool oldValue func(context.Context) (*Score, error) predicates []predicate.Score @@ -263,21 +265,21 @@ func (m *ScoreMutation) ResetAccuracy() { m.addaccuracy = nil } -// SetCreatedAt sets the "createdAt" field. +// SetCreatedAt sets the "created_at" field. func (m *ScoreMutation) SetCreatedAt(t time.Time) { - m.createdAt = &t + m.created_at = &t } -// CreatedAt returns the value of the "createdAt" field in the mutation. +// CreatedAt returns the value of the "created_at" field in the mutation. func (m *ScoreMutation) CreatedAt() (r time.Time, exists bool) { - v := m.createdAt + v := m.created_at if v == nil { return } return *v, true } -// OldCreatedAt returns the old "createdAt" field's value of the Score entity. +// OldCreatedAt returns the old "created_at" field's value of the Score entity. // If the Score object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. func (m *ScoreMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) { @@ -294,9 +296,48 @@ func (m *ScoreMutation) OldCreatedAt(ctx context.Context) (v time.Time, err erro return oldValue.CreatedAt, nil } -// ResetCreatedAt resets all changes to the "createdAt" field. +// ResetCreatedAt resets all changes to the "created_at" field. func (m *ScoreMutation) ResetCreatedAt() { - m.createdAt = nil + m.created_at = nil +} + +// SetUserID sets the "user" edge to the User entity by id. +func (m *ScoreMutation) SetUserID(id uuid.UUID) { + m.user = &id +} + +// ClearUser clears the "user" edge to the User entity. +func (m *ScoreMutation) ClearUser() { + m.cleareduser = true +} + +// UserCleared reports if the "user" edge to the User entity was cleared. +func (m *ScoreMutation) UserCleared() bool { + return m.cleareduser +} + +// UserID returns the "user" edge ID in the mutation. +func (m *ScoreMutation) UserID() (id uuid.UUID, exists bool) { + if m.user != nil { + return *m.user, true + } + return +} + +// UserIDs returns the "user" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// UserID instead. It exists only for internal usage by the builders. +func (m *ScoreMutation) UserIDs() (ids []uuid.UUID) { + if id := m.user; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetUser resets all changes to the "user" edge. +func (m *ScoreMutation) ResetUser() { + m.user = nil + m.cleareduser = false } // Where appends a list predicates to the ScoreMutation builder. @@ -340,7 +381,7 @@ func (m *ScoreMutation) Fields() []string { if m.accuracy != nil { fields = append(fields, score.FieldAccuracy) } - if m.createdAt != nil { + if m.created_at != nil { fields = append(fields, score.FieldCreatedAt) } return fields @@ -493,19 +534,28 @@ func (m *ScoreMutation) ResetField(name string) error { // AddedEdges returns all edge names that were set/added in this mutation. func (m *ScoreMutation) AddedEdges() []string { - edges := make([]string, 0, 0) + edges := make([]string, 0, 1) + if m.user != nil { + edges = append(edges, score.EdgeUser) + } return edges } // AddedIDs returns all IDs (to other nodes) that were added for the given edge // name in this mutation. func (m *ScoreMutation) AddedIDs(name string) []ent.Value { + switch name { + case score.EdgeUser: + if id := m.user; id != nil { + return []ent.Value{*id} + } + } return nil } // RemovedEdges returns all edge names that were removed in this mutation. func (m *ScoreMutation) RemovedEdges() []string { - edges := make([]string, 0, 0) + edges := make([]string, 0, 1) return edges } @@ -517,25 +567,42 @@ func (m *ScoreMutation) RemovedIDs(name string) []ent.Value { // ClearedEdges returns all edge names that were cleared in this mutation. func (m *ScoreMutation) ClearedEdges() []string { - edges := make([]string, 0, 0) + edges := make([]string, 0, 1) + if m.cleareduser { + edges = append(edges, score.EdgeUser) + } return edges } // EdgeCleared returns a boolean which indicates if the edge with the given name // was cleared in this mutation. func (m *ScoreMutation) EdgeCleared(name string) bool { + switch name { + case score.EdgeUser: + return m.cleareduser + } return false } // ClearEdge clears the value of the edge with the given name. It returns an error // if that edge is not defined in the schema. func (m *ScoreMutation) ClearEdge(name string) error { + switch name { + case score.EdgeUser: + m.ClearUser() + return nil + } return fmt.Errorf("unknown Score unique edge %s", name) } // ResetEdge resets all changes to the edge with the given name in this mutation. // It returns an error if the edge is not defined in the schema. func (m *ScoreMutation) ResetEdge(name string) error { + switch name { + case score.EdgeUser: + m.ResetUser() + return nil + } return fmt.Errorf("unknown Score edge %s", name) } @@ -545,8 +612,8 @@ type UserMutation struct { op Op typ string id *uuid.UUID - _StudentNumber *string - _HandleName *string + student_number *string + handle_name *string created_at *time.Time updated_at *time.Time clearedFields map[string]struct{} @@ -662,21 +729,21 @@ func (m *UserMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { } } -// SetStudentNumber sets the "StudentNumber" field. +// SetStudentNumber sets the "student_number" field. func (m *UserMutation) SetStudentNumber(s string) { - m._StudentNumber = &s + m.student_number = &s } -// StudentNumber returns the value of the "StudentNumber" field in the mutation. +// StudentNumber returns the value of the "student_number" field in the mutation. func (m *UserMutation) StudentNumber() (r string, exists bool) { - v := m._StudentNumber + v := m.student_number if v == nil { return } return *v, true } -// OldStudentNumber returns the old "StudentNumber" field's value of the User entity. +// OldStudentNumber returns the old "student_number" field's value of the User entity. // If the User object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. func (m *UserMutation) OldStudentNumber(ctx context.Context) (v string, err error) { @@ -693,26 +760,26 @@ func (m *UserMutation) OldStudentNumber(ctx context.Context) (v string, err erro return oldValue.StudentNumber, nil } -// ResetStudentNumber resets all changes to the "StudentNumber" field. +// ResetStudentNumber resets all changes to the "student_number" field. func (m *UserMutation) ResetStudentNumber() { - m._StudentNumber = nil + m.student_number = nil } -// SetHandleName sets the "HandleName" field. +// SetHandleName sets the "handle_name" field. func (m *UserMutation) SetHandleName(s string) { - m._HandleName = &s + m.handle_name = &s } -// HandleName returns the value of the "HandleName" field in the mutation. +// HandleName returns the value of the "handle_name" field in the mutation. func (m *UserMutation) HandleName() (r string, exists bool) { - v := m._HandleName + v := m.handle_name if v == nil { return } return *v, true } -// OldHandleName returns the old "HandleName" field's value of the User entity. +// OldHandleName returns the old "handle_name" field's value of the User entity. // If the User object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. func (m *UserMutation) OldHandleName(ctx context.Context) (v string, err error) { @@ -729,9 +796,9 @@ func (m *UserMutation) OldHandleName(ctx context.Context) (v string, err error) return oldValue.HandleName, nil } -// ResetHandleName resets all changes to the "HandleName" field. +// ResetHandleName resets all changes to the "handle_name" field. func (m *UserMutation) ResetHandleName() { - m._HandleName = nil + m.handle_name = nil } // SetCreatedAt sets the "created_at" field. @@ -895,10 +962,10 @@ func (m *UserMutation) Type() string { // AddedFields(). func (m *UserMutation) Fields() []string { fields := make([]string, 0, 4) - if m._StudentNumber != nil { + if m.student_number != nil { fields = append(fields, user.FieldStudentNumber) } - if m._HandleName != nil { + if m.handle_name != nil { fields = append(fields, user.FieldHandleName) } if m.created_at != nil { diff --git a/typing-server/domain/repository/ent/runtime.go b/typing-server/domain/repository/ent/runtime.go index 7ac9c43..5485c0b 100644 --- a/typing-server/domain/repository/ent/runtime.go +++ b/typing-server/domain/repository/ent/runtime.go @@ -17,28 +17,32 @@ import ( func init() { scoreFields := schema.Score{}.Fields() _ = scoreFields + // scoreDescCreatedAt is the schema descriptor for created_at field. + scoreDescCreatedAt := scoreFields[3].Descriptor() + // score.DefaultCreatedAt holds the default value on creation for the created_at field. + score.DefaultCreatedAt = scoreDescCreatedAt.Default.(func() time.Time) // scoreDescID is the schema descriptor for id field. scoreDescID := scoreFields[0].Descriptor() // score.DefaultID holds the default value on creation for the id field. score.DefaultID = scoreDescID.Default.(func() uuid.UUID) userFields := schema.User{}.Fields() _ = userFields - // userDescStudentNumber is the schema descriptor for StudentNumber field. + // userDescStudentNumber is the schema descriptor for student_number field. userDescStudentNumber := userFields[1].Descriptor() - // user.StudentNumberValidator is a validator for the "StudentNumber" field. It is called by the builders before save. + // user.StudentNumberValidator is a validator for the "student_number" field. It is called by the builders before save. user.StudentNumberValidator = userDescStudentNumber.Validators[0].(func(string) error) - // userDescHandleName is the schema descriptor for HandleName field. + // userDescHandleName is the schema descriptor for handle_name field. userDescHandleName := userFields[2].Descriptor() - // user.HandleNameValidator is a validator for the "HandleName" field. It is called by the builders before save. + // user.HandleNameValidator is a validator for the "handle_name" field. It is called by the builders before save. user.HandleNameValidator = func() func(string) error { validators := userDescHandleName.Validators fns := [...]func(string) error{ validators[0].(func(string) error), validators[1].(func(string) error), } - return func(_HandleName string) error { + return func(handle_name string) error { for _, fn := range fns { - if err := fn(_HandleName); err != nil { + if err := fn(handle_name); err != nil { return err } } diff --git a/typing-server/domain/repository/ent/schema/score.go b/typing-server/domain/repository/ent/schema/score.go index b01a954..fd29131 100644 --- a/typing-server/domain/repository/ent/schema/score.go +++ b/typing-server/domain/repository/ent/schema/score.go @@ -1,7 +1,10 @@ package schema import ( + "time" + "entgo.io/ent" + "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" "github.com/google/uuid" ) @@ -16,6 +19,16 @@ func (Score) Fields() []ent.Field { field.UUID("id", uuid.UUID{}).Default(uuid.New).StorageKey("id").Unique(), field.Int("keystrokes"), field.Float("accuracy"), - field.Time("createdAt"), + field.Time("created_at").Immutable().Default(time.Now)} +} + +// Edges of the Score. +func (Score) Edges() []ent.Edge { + return []ent.Edge{ + //ScoreとUserの関係をInverseで持たせる。ScoreはUserを必須とする + edge.From("user", User.Type). + Ref("scores"). + Unique(). + Required(), } } diff --git a/typing-server/domain/repository/ent/schema/user.go b/typing-server/domain/repository/ent/schema/user.go index b4a949d..6856ef6 100644 --- a/typing-server/domain/repository/ent/schema/user.go +++ b/typing-server/domain/repository/ent/schema/user.go @@ -19,9 +19,9 @@ func (User) Fields() []ent.Field { //カラムとしてvarcher型(255)のMailAdress,varcher型(255)のHandleName,varcher型(255)のName,varcher型(255)のHashedPassword,enum型のDepartment,datetime型のCreatedAt,datetime型のUpdatedAtを持たせる return []ent.Field{ field.UUID("id", uuid.UUID{}).Default(uuid.New).StorageKey("id").Unique(), - field.String("StudentNumber").NotEmpty().Unique(), + field.String("student_number").NotEmpty().Unique(), // field.String("MailAdress").NotEmpty().MaxLen(255), - field.String("HandleName").NotEmpty().MaxLen(36), + field.String("handle_name").NotEmpty().MaxLen(36), // field.String("Name").NotEmpty().MaxLen(36), // field.String("HashedPassword").NotEmpty().MaxLen(255), // field.Enum("Department").Values("CS", "BI", "IA"), diff --git a/typing-server/domain/repository/ent/score.go b/typing-server/domain/repository/ent/score.go index 927b8b1..f3f9eed 100644 --- a/typing-server/domain/repository/ent/score.go +++ b/typing-server/domain/repository/ent/score.go @@ -11,6 +11,7 @@ import ( "entgo.io/ent/dialect/sql" "github.com/google/uuid" "github.com/su-its/typing/typing-server/domain/repository/ent/score" + "github.com/su-its/typing/typing-server/domain/repository/ent/user" ) // Score is the model entity for the Score schema. @@ -22,12 +23,35 @@ type Score struct { Keystrokes int `json:"keystrokes,omitempty"` // Accuracy holds the value of the "accuracy" field. Accuracy float64 `json:"accuracy,omitempty"` - // CreatedAt holds the value of the "createdAt" field. - CreatedAt time.Time `json:"createdAt,omitempty"` + // CreatedAt holds the value of the "created_at" field. + CreatedAt time.Time `json:"created_at,omitempty"` + // Edges holds the relations/edges for other nodes in the graph. + // The values are being populated by the ScoreQuery when eager-loading is set. + Edges ScoreEdges `json:"edges"` user_scores *uuid.UUID selectValues sql.SelectValues } +// ScoreEdges holds the relations/edges for other nodes in the graph. +type ScoreEdges struct { + // User holds the value of the user edge. + User *User `json:"user,omitempty"` + // loadedTypes holds the information for reporting if a + // type was loaded (or requested) in eager-loading or not. + loadedTypes [1]bool +} + +// UserOrErr returns the User value or an error if the edge +// was not loaded in eager-loading, or loaded but was not found. +func (e ScoreEdges) UserOrErr() (*User, error) { + if e.User != nil { + return e.User, nil + } else if e.loadedTypes[0] { + return nil, &NotFoundError{label: user.Label} + } + return nil, &NotLoadedError{edge: "user"} +} + // scanValues returns the types for scanning values from sql.Rows. func (*Score) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) @@ -78,7 +102,7 @@ func (s *Score) assignValues(columns []string, values []any) error { } case score.FieldCreatedAt: if value, ok := values[i].(*sql.NullTime); !ok { - return fmt.Errorf("unexpected type %T for field createdAt", values[i]) + return fmt.Errorf("unexpected type %T for field created_at", values[i]) } else if value.Valid { s.CreatedAt = value.Time } @@ -102,6 +126,11 @@ func (s *Score) Value(name string) (ent.Value, error) { return s.selectValues.Get(name) } +// QueryUser queries the "user" edge of the Score entity. +func (s *Score) QueryUser() *UserQuery { + return NewScoreClient(s.config).QueryUser(s) +} + // Update returns a builder for updating this Score. // Note that you need to call Score.Unwrap() before calling this method if this Score // was returned from a transaction, and the transaction was committed or rolled back. @@ -131,7 +160,7 @@ func (s *Score) String() string { builder.WriteString("accuracy=") builder.WriteString(fmt.Sprintf("%v", s.Accuracy)) builder.WriteString(", ") - builder.WriteString("createdAt=") + builder.WriteString("created_at=") builder.WriteString(s.CreatedAt.Format(time.ANSIC)) builder.WriteByte(')') return builder.String() diff --git a/typing-server/domain/repository/ent/score/score.go b/typing-server/domain/repository/ent/score/score.go index a880202..b0182c8 100644 --- a/typing-server/domain/repository/ent/score/score.go +++ b/typing-server/domain/repository/ent/score/score.go @@ -3,7 +3,10 @@ package score import ( + "time" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" "github.com/google/uuid" ) @@ -16,10 +19,19 @@ const ( FieldKeystrokes = "keystrokes" // FieldAccuracy holds the string denoting the accuracy field in the database. FieldAccuracy = "accuracy" - // FieldCreatedAt holds the string denoting the createdat field in the database. + // FieldCreatedAt holds the string denoting the created_at field in the database. FieldCreatedAt = "created_at" + // EdgeUser holds the string denoting the user edge name in mutations. + EdgeUser = "user" // Table holds the table name of the score in the database. Table = "scores" + // UserTable is the table that holds the user relation/edge. + UserTable = "scores" + // UserInverseTable is the table name for the User entity. + // It exists in this package in order to avoid circular dependency with the "user" package. + UserInverseTable = "users" + // UserColumn is the table column denoting the user relation/edge. + UserColumn = "user_scores" ) // Columns holds all SQL columns for score fields. @@ -52,6 +64,8 @@ func ValidColumn(column string) bool { } var ( + // DefaultCreatedAt holds the default value on creation for the "created_at" field. + DefaultCreatedAt func() time.Time // DefaultID holds the default value on creation for the "id" field. DefaultID func() uuid.UUID ) @@ -74,7 +88,21 @@ func ByAccuracy(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldAccuracy, opts...).ToFunc() } -// ByCreatedAt orders the results by the createdAt field. +// ByCreatedAt orders the results by the created_at field. func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() } + +// ByUserField orders the results by user field. +func ByUserField(field string, opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newUserStep(), sql.OrderByField(field, opts...)) + } +} +func newUserStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(UserInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, UserTable, UserColumn), + ) +} diff --git a/typing-server/domain/repository/ent/score/where.go b/typing-server/domain/repository/ent/score/where.go index 6d1618d..057f02c 100644 --- a/typing-server/domain/repository/ent/score/where.go +++ b/typing-server/domain/repository/ent/score/where.go @@ -6,6 +6,7 @@ import ( "time" "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" "github.com/google/uuid" "github.com/su-its/typing/typing-server/domain/repository/ent/predicate" ) @@ -65,7 +66,7 @@ func Accuracy(v float64) predicate.Score { return predicate.Score(sql.FieldEQ(FieldAccuracy, v)) } -// CreatedAt applies equality check predicate on the "createdAt" field. It's identical to CreatedAtEQ. +// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. func CreatedAt(v time.Time) predicate.Score { return predicate.Score(sql.FieldEQ(FieldCreatedAt, v)) } @@ -150,46 +151,69 @@ func AccuracyLTE(v float64) predicate.Score { return predicate.Score(sql.FieldLTE(FieldAccuracy, v)) } -// CreatedAtEQ applies the EQ predicate on the "createdAt" field. +// CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v time.Time) predicate.Score { return predicate.Score(sql.FieldEQ(FieldCreatedAt, v)) } -// CreatedAtNEQ applies the NEQ predicate on the "createdAt" field. +// CreatedAtNEQ applies the NEQ predicate on the "created_at" field. func CreatedAtNEQ(v time.Time) predicate.Score { return predicate.Score(sql.FieldNEQ(FieldCreatedAt, v)) } -// CreatedAtIn applies the In predicate on the "createdAt" field. +// CreatedAtIn applies the In predicate on the "created_at" field. func CreatedAtIn(vs ...time.Time) predicate.Score { return predicate.Score(sql.FieldIn(FieldCreatedAt, vs...)) } -// CreatedAtNotIn applies the NotIn predicate on the "createdAt" field. +// CreatedAtNotIn applies the NotIn predicate on the "created_at" field. func CreatedAtNotIn(vs ...time.Time) predicate.Score { return predicate.Score(sql.FieldNotIn(FieldCreatedAt, vs...)) } -// CreatedAtGT applies the GT predicate on the "createdAt" field. +// CreatedAtGT applies the GT predicate on the "created_at" field. func CreatedAtGT(v time.Time) predicate.Score { return predicate.Score(sql.FieldGT(FieldCreatedAt, v)) } -// CreatedAtGTE applies the GTE predicate on the "createdAt" field. +// CreatedAtGTE applies the GTE predicate on the "created_at" field. func CreatedAtGTE(v time.Time) predicate.Score { return predicate.Score(sql.FieldGTE(FieldCreatedAt, v)) } -// CreatedAtLT applies the LT predicate on the "createdAt" field. +// CreatedAtLT applies the LT predicate on the "created_at" field. func CreatedAtLT(v time.Time) predicate.Score { return predicate.Score(sql.FieldLT(FieldCreatedAt, v)) } -// CreatedAtLTE applies the LTE predicate on the "createdAt" field. +// CreatedAtLTE applies the LTE predicate on the "created_at" field. func CreatedAtLTE(v time.Time) predicate.Score { return predicate.Score(sql.FieldLTE(FieldCreatedAt, v)) } +// HasUser applies the HasEdge predicate on the "user" edge. +func HasUser() predicate.Score { + return predicate.Score(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, UserTable, UserColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasUserWith applies the HasEdge predicate on the "user" edge with a given conditions (other predicates). +func HasUserWith(preds ...predicate.User) predicate.Score { + return predicate.Score(func(s *sql.Selector) { + step := newUserStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.Score) predicate.Score { return predicate.Score(sql.AndPredicates(predicates...)) diff --git a/typing-server/domain/repository/ent/score_create.go b/typing-server/domain/repository/ent/score_create.go index 71b4edb..f4963e2 100644 --- a/typing-server/domain/repository/ent/score_create.go +++ b/typing-server/domain/repository/ent/score_create.go @@ -12,6 +12,7 @@ import ( "entgo.io/ent/schema/field" "github.com/google/uuid" "github.com/su-its/typing/typing-server/domain/repository/ent/score" + "github.com/su-its/typing/typing-server/domain/repository/ent/user" ) // ScoreCreate is the builder for creating a Score entity. @@ -33,12 +34,20 @@ func (sc *ScoreCreate) SetAccuracy(f float64) *ScoreCreate { return sc } -// SetCreatedAt sets the "createdAt" field. +// SetCreatedAt sets the "created_at" field. func (sc *ScoreCreate) SetCreatedAt(t time.Time) *ScoreCreate { sc.mutation.SetCreatedAt(t) return sc } +// SetNillableCreatedAt sets the "created_at" field if the given value is not nil. +func (sc *ScoreCreate) SetNillableCreatedAt(t *time.Time) *ScoreCreate { + if t != nil { + sc.SetCreatedAt(*t) + } + return sc +} + // SetID sets the "id" field. func (sc *ScoreCreate) SetID(u uuid.UUID) *ScoreCreate { sc.mutation.SetID(u) @@ -53,6 +62,17 @@ func (sc *ScoreCreate) SetNillableID(u *uuid.UUID) *ScoreCreate { return sc } +// SetUserID sets the "user" edge to the User entity by ID. +func (sc *ScoreCreate) SetUserID(id uuid.UUID) *ScoreCreate { + sc.mutation.SetUserID(id) + return sc +} + +// SetUser sets the "user" edge to the User entity. +func (sc *ScoreCreate) SetUser(u *User) *ScoreCreate { + return sc.SetUserID(u.ID) +} + // Mutation returns the ScoreMutation object of the builder. func (sc *ScoreCreate) Mutation() *ScoreMutation { return sc.mutation @@ -88,6 +108,10 @@ func (sc *ScoreCreate) ExecX(ctx context.Context) { // defaults sets the default values of the builder before save. func (sc *ScoreCreate) defaults() { + if _, ok := sc.mutation.CreatedAt(); !ok { + v := score.DefaultCreatedAt() + sc.mutation.SetCreatedAt(v) + } if _, ok := sc.mutation.ID(); !ok { v := score.DefaultID() sc.mutation.SetID(v) @@ -103,7 +127,10 @@ func (sc *ScoreCreate) check() error { return &ValidationError{Name: "accuracy", err: errors.New(`ent: missing required field "Score.accuracy"`)} } if _, ok := sc.mutation.CreatedAt(); !ok { - return &ValidationError{Name: "createdAt", err: errors.New(`ent: missing required field "Score.createdAt"`)} + return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "Score.created_at"`)} + } + if _, ok := sc.mutation.UserID(); !ok { + return &ValidationError{Name: "user", err: errors.New(`ent: missing required edge "Score.user"`)} } return nil } @@ -152,6 +179,23 @@ func (sc *ScoreCreate) createSpec() (*Score, *sqlgraph.CreateSpec) { _spec.SetField(score.FieldCreatedAt, field.TypeTime, value) _node.CreatedAt = value } + if nodes := sc.mutation.UserIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: score.UserTable, + Columns: []string{score.UserColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _node.user_scores = &nodes[0] + _spec.Edges = append(_spec.Edges, edge) + } return _node, _spec } diff --git a/typing-server/domain/repository/ent/score_query.go b/typing-server/domain/repository/ent/score_query.go index f22149e..b1dcf1b 100644 --- a/typing-server/domain/repository/ent/score_query.go +++ b/typing-server/domain/repository/ent/score_query.go @@ -13,6 +13,7 @@ import ( "github.com/google/uuid" "github.com/su-its/typing/typing-server/domain/repository/ent/predicate" "github.com/su-its/typing/typing-server/domain/repository/ent/score" + "github.com/su-its/typing/typing-server/domain/repository/ent/user" ) // ScoreQuery is the builder for querying Score entities. @@ -22,6 +23,7 @@ type ScoreQuery struct { order []score.OrderOption inters []Interceptor predicates []predicate.Score + withUser *UserQuery withFKs bool // intermediate query (i.e. traversal path). sql *sql.Selector @@ -59,6 +61,28 @@ func (sq *ScoreQuery) Order(o ...score.OrderOption) *ScoreQuery { return sq } +// QueryUser chains the current query on the "user" edge. +func (sq *ScoreQuery) QueryUser() *UserQuery { + query := (&UserClient{config: sq.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := sq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := sq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(score.Table, score.FieldID, selector), + sqlgraph.To(user.Table, user.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, score.UserTable, score.UserColumn), + ) + fromU = sqlgraph.SetNeighbors(sq.driver.Dialect(), step) + return fromU, nil + } + return query +} + // First returns the first Score entity from the query. // Returns a *NotFoundError when no Score was found. func (sq *ScoreQuery) First(ctx context.Context) (*Score, error) { @@ -251,12 +275,24 @@ func (sq *ScoreQuery) Clone() *ScoreQuery { order: append([]score.OrderOption{}, sq.order...), inters: append([]Interceptor{}, sq.inters...), predicates: append([]predicate.Score{}, sq.predicates...), + withUser: sq.withUser.Clone(), // clone intermediate query. sql: sq.sql.Clone(), path: sq.path, } } +// WithUser tells the query-builder to eager-load the nodes that are connected to +// the "user" edge. The optional arguments are used to configure the query builder of the edge. +func (sq *ScoreQuery) WithUser(opts ...func(*UserQuery)) *ScoreQuery { + query := (&UserClient{config: sq.config}).Query() + for _, opt := range opts { + opt(query) + } + sq.withUser = query + return sq +} + // GroupBy is used to group vertices by one or more fields/columns. // It is often used with aggregate functions, like: count, max, mean, min, sum. // @@ -333,10 +369,16 @@ func (sq *ScoreQuery) prepareQuery(ctx context.Context) error { func (sq *ScoreQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Score, error) { var ( - nodes = []*Score{} - withFKs = sq.withFKs - _spec = sq.querySpec() + nodes = []*Score{} + withFKs = sq.withFKs + _spec = sq.querySpec() + loadedTypes = [1]bool{ + sq.withUser != nil, + } ) + if sq.withUser != nil { + withFKs = true + } if withFKs { _spec.Node.Columns = append(_spec.Node.Columns, score.ForeignKeys...) } @@ -346,6 +388,7 @@ func (sq *ScoreQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Score, _spec.Assign = func(columns []string, values []any) error { node := &Score{config: sq.config} nodes = append(nodes, node) + node.Edges.loadedTypes = loadedTypes return node.assignValues(columns, values) } for i := range hooks { @@ -357,9 +400,48 @@ func (sq *ScoreQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Score, if len(nodes) == 0 { return nodes, nil } + if query := sq.withUser; query != nil { + if err := sq.loadUser(ctx, query, nodes, nil, + func(n *Score, e *User) { n.Edges.User = e }); err != nil { + return nil, err + } + } return nodes, nil } +func (sq *ScoreQuery) loadUser(ctx context.Context, query *UserQuery, nodes []*Score, init func(*Score), assign func(*Score, *User)) error { + ids := make([]uuid.UUID, 0, len(nodes)) + nodeids := make(map[uuid.UUID][]*Score) + for i := range nodes { + if nodes[i].user_scores == nil { + continue + } + fk := *nodes[i].user_scores + if _, ok := nodeids[fk]; !ok { + ids = append(ids, fk) + } + nodeids[fk] = append(nodeids[fk], nodes[i]) + } + if len(ids) == 0 { + return nil + } + query.Where(user.IDIn(ids...)) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nodeids[n.ID] + if !ok { + return fmt.Errorf(`unexpected foreign-key "user_scores" returned %v`, n.ID) + } + for i := range nodes { + assign(nodes[i], n) + } + } + return nil +} + func (sq *ScoreQuery) sqlCount(ctx context.Context) (int, error) { _spec := sq.querySpec() _spec.Node.Columns = sq.ctx.Fields diff --git a/typing-server/domain/repository/ent/score_update.go b/typing-server/domain/repository/ent/score_update.go index 26be882..b58ca66 100644 --- a/typing-server/domain/repository/ent/score_update.go +++ b/typing-server/domain/repository/ent/score_update.go @@ -6,13 +6,14 @@ import ( "context" "errors" "fmt" - "time" "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" + "github.com/google/uuid" "github.com/su-its/typing/typing-server/domain/repository/ent/predicate" "github.com/su-its/typing/typing-server/domain/repository/ent/score" + "github.com/su-its/typing/typing-server/domain/repository/ent/user" ) // ScoreUpdate is the builder for updating Score entities. @@ -70,18 +71,15 @@ func (su *ScoreUpdate) AddAccuracy(f float64) *ScoreUpdate { return su } -// SetCreatedAt sets the "createdAt" field. -func (su *ScoreUpdate) SetCreatedAt(t time.Time) *ScoreUpdate { - su.mutation.SetCreatedAt(t) +// SetUserID sets the "user" edge to the User entity by ID. +func (su *ScoreUpdate) SetUserID(id uuid.UUID) *ScoreUpdate { + su.mutation.SetUserID(id) return su } -// SetNillableCreatedAt sets the "createdAt" field if the given value is not nil. -func (su *ScoreUpdate) SetNillableCreatedAt(t *time.Time) *ScoreUpdate { - if t != nil { - su.SetCreatedAt(*t) - } - return su +// SetUser sets the "user" edge to the User entity. +func (su *ScoreUpdate) SetUser(u *User) *ScoreUpdate { + return su.SetUserID(u.ID) } // Mutation returns the ScoreMutation object of the builder. @@ -89,6 +87,12 @@ func (su *ScoreUpdate) Mutation() *ScoreMutation { return su.mutation } +// ClearUser clears the "user" edge to the User entity. +func (su *ScoreUpdate) ClearUser() *ScoreUpdate { + su.mutation.ClearUser() + return su +} + // Save executes the query and returns the number of nodes affected by the update operation. func (su *ScoreUpdate) Save(ctx context.Context) (int, error) { return withHooks(ctx, su.sqlSave, su.mutation, su.hooks) @@ -116,7 +120,18 @@ func (su *ScoreUpdate) ExecX(ctx context.Context) { } } +// check runs all checks and user-defined validators on the builder. +func (su *ScoreUpdate) check() error { + if _, ok := su.mutation.UserID(); su.mutation.UserCleared() && !ok { + return errors.New(`ent: clearing a required unique edge "Score.user"`) + } + return nil +} + func (su *ScoreUpdate) sqlSave(ctx context.Context) (n int, err error) { + if err := su.check(); err != nil { + return n, err + } _spec := sqlgraph.NewUpdateSpec(score.Table, score.Columns, sqlgraph.NewFieldSpec(score.FieldID, field.TypeUUID)) if ps := su.mutation.predicates; len(ps) > 0 { _spec.Predicate = func(selector *sql.Selector) { @@ -137,8 +152,34 @@ func (su *ScoreUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := su.mutation.AddedAccuracy(); ok { _spec.AddField(score.FieldAccuracy, field.TypeFloat64, value) } - if value, ok := su.mutation.CreatedAt(); ok { - _spec.SetField(score.FieldCreatedAt, field.TypeTime, value) + if su.mutation.UserCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: score.UserTable, + Columns: []string{score.UserColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := su.mutation.UserIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: score.UserTable, + Columns: []string{score.UserColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) } if n, err = sqlgraph.UpdateNodes(ctx, su.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { @@ -202,18 +243,15 @@ func (suo *ScoreUpdateOne) AddAccuracy(f float64) *ScoreUpdateOne { return suo } -// SetCreatedAt sets the "createdAt" field. -func (suo *ScoreUpdateOne) SetCreatedAt(t time.Time) *ScoreUpdateOne { - suo.mutation.SetCreatedAt(t) +// SetUserID sets the "user" edge to the User entity by ID. +func (suo *ScoreUpdateOne) SetUserID(id uuid.UUID) *ScoreUpdateOne { + suo.mutation.SetUserID(id) return suo } -// SetNillableCreatedAt sets the "createdAt" field if the given value is not nil. -func (suo *ScoreUpdateOne) SetNillableCreatedAt(t *time.Time) *ScoreUpdateOne { - if t != nil { - suo.SetCreatedAt(*t) - } - return suo +// SetUser sets the "user" edge to the User entity. +func (suo *ScoreUpdateOne) SetUser(u *User) *ScoreUpdateOne { + return suo.SetUserID(u.ID) } // Mutation returns the ScoreMutation object of the builder. @@ -221,6 +259,12 @@ func (suo *ScoreUpdateOne) Mutation() *ScoreMutation { return suo.mutation } +// ClearUser clears the "user" edge to the User entity. +func (suo *ScoreUpdateOne) ClearUser() *ScoreUpdateOne { + suo.mutation.ClearUser() + return suo +} + // Where appends a list predicates to the ScoreUpdate builder. func (suo *ScoreUpdateOne) Where(ps ...predicate.Score) *ScoreUpdateOne { suo.mutation.Where(ps...) @@ -261,7 +305,18 @@ func (suo *ScoreUpdateOne) ExecX(ctx context.Context) { } } +// check runs all checks and user-defined validators on the builder. +func (suo *ScoreUpdateOne) check() error { + if _, ok := suo.mutation.UserID(); suo.mutation.UserCleared() && !ok { + return errors.New(`ent: clearing a required unique edge "Score.user"`) + } + return nil +} + func (suo *ScoreUpdateOne) sqlSave(ctx context.Context) (_node *Score, err error) { + if err := suo.check(); err != nil { + return _node, err + } _spec := sqlgraph.NewUpdateSpec(score.Table, score.Columns, sqlgraph.NewFieldSpec(score.FieldID, field.TypeUUID)) id, ok := suo.mutation.ID() if !ok { @@ -299,8 +354,34 @@ func (suo *ScoreUpdateOne) sqlSave(ctx context.Context) (_node *Score, err error if value, ok := suo.mutation.AddedAccuracy(); ok { _spec.AddField(score.FieldAccuracy, field.TypeFloat64, value) } - if value, ok := suo.mutation.CreatedAt(); ok { - _spec.SetField(score.FieldCreatedAt, field.TypeTime, value) + if suo.mutation.UserCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: score.UserTable, + Columns: []string{score.UserColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := suo.mutation.UserIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: score.UserTable, + Columns: []string{score.UserColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) } _node = &Score{config: suo.config} _spec.Assign = _node.assignValues diff --git a/typing-server/domain/repository/ent/user.go b/typing-server/domain/repository/ent/user.go index 23e39d5..48edffc 100644 --- a/typing-server/domain/repository/ent/user.go +++ b/typing-server/domain/repository/ent/user.go @@ -18,10 +18,10 @@ type User struct { config `json:"-"` // ID of the ent. ID uuid.UUID `json:"id,omitempty"` - // StudentNumber holds the value of the "StudentNumber" field. - StudentNumber string `json:"StudentNumber,omitempty"` - // HandleName holds the value of the "HandleName" field. - HandleName string `json:"HandleName,omitempty"` + // StudentNumber holds the value of the "student_number" field. + StudentNumber string `json:"student_number,omitempty"` + // HandleName holds the value of the "handle_name" field. + HandleName string `json:"handle_name,omitempty"` // CreatedAt holds the value of the "created_at" field. CreatedAt time.Time `json:"created_at,omitempty"` // UpdatedAt holds the value of the "updated_at" field. @@ -84,13 +84,13 @@ func (u *User) assignValues(columns []string, values []any) error { } case user.FieldStudentNumber: if value, ok := values[i].(*sql.NullString); !ok { - return fmt.Errorf("unexpected type %T for field StudentNumber", values[i]) + return fmt.Errorf("unexpected type %T for field student_number", values[i]) } else if value.Valid { u.StudentNumber = value.String } case user.FieldHandleName: if value, ok := values[i].(*sql.NullString); !ok { - return fmt.Errorf("unexpected type %T for field HandleName", values[i]) + return fmt.Errorf("unexpected type %T for field handle_name", values[i]) } else if value.Valid { u.HandleName = value.String } @@ -147,10 +147,10 @@ func (u *User) String() string { var builder strings.Builder builder.WriteString("User(") builder.WriteString(fmt.Sprintf("id=%v, ", u.ID)) - builder.WriteString("StudentNumber=") + builder.WriteString("student_number=") builder.WriteString(u.StudentNumber) builder.WriteString(", ") - builder.WriteString("HandleName=") + builder.WriteString("handle_name=") builder.WriteString(u.HandleName) builder.WriteString(", ") builder.WriteString("created_at=") diff --git a/typing-server/domain/repository/ent/user/user.go b/typing-server/domain/repository/ent/user/user.go index 0cf6fe4..46991e3 100644 --- a/typing-server/domain/repository/ent/user/user.go +++ b/typing-server/domain/repository/ent/user/user.go @@ -15,9 +15,9 @@ const ( Label = "user" // FieldID holds the string denoting the id field in the database. FieldID = "id" - // FieldStudentNumber holds the string denoting the studentnumber field in the database. + // FieldStudentNumber holds the string denoting the student_number field in the database. FieldStudentNumber = "student_number" - // FieldHandleName holds the string denoting the handlename field in the database. + // FieldHandleName holds the string denoting the handle_name field in the database. FieldHandleName = "handle_name" // FieldCreatedAt holds the string denoting the created_at field in the database. FieldCreatedAt = "created_at" @@ -56,9 +56,9 @@ func ValidColumn(column string) bool { } var ( - // StudentNumberValidator is a validator for the "StudentNumber" field. It is called by the builders before save. + // StudentNumberValidator is a validator for the "student_number" field. It is called by the builders before save. StudentNumberValidator func(string) error - // HandleNameValidator is a validator for the "HandleName" field. It is called by the builders before save. + // HandleNameValidator is a validator for the "handle_name" field. It is called by the builders before save. HandleNameValidator func(string) error // DefaultCreatedAt holds the default value on creation for the "created_at" field. DefaultCreatedAt func() time.Time @@ -78,12 +78,12 @@ func ByID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldID, opts...).ToFunc() } -// ByStudentNumber orders the results by the StudentNumber field. +// ByStudentNumber orders the results by the student_number field. func ByStudentNumber(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldStudentNumber, opts...).ToFunc() } -// ByHandleName orders the results by the HandleName field. +// ByHandleName orders the results by the handle_name field. func ByHandleName(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldHandleName, opts...).ToFunc() } diff --git a/typing-server/domain/repository/ent/user/where.go b/typing-server/domain/repository/ent/user/where.go index 0653f73..1cb262d 100644 --- a/typing-server/domain/repository/ent/user/where.go +++ b/typing-server/domain/repository/ent/user/where.go @@ -56,12 +56,12 @@ func IDLTE(id uuid.UUID) predicate.User { return predicate.User(sql.FieldLTE(FieldID, id)) } -// StudentNumber applies equality check predicate on the "StudentNumber" field. It's identical to StudentNumberEQ. +// StudentNumber applies equality check predicate on the "student_number" field. It's identical to StudentNumberEQ. func StudentNumber(v string) predicate.User { return predicate.User(sql.FieldEQ(FieldStudentNumber, v)) } -// HandleName applies equality check predicate on the "HandleName" field. It's identical to HandleNameEQ. +// HandleName applies equality check predicate on the "handle_name" field. It's identical to HandleNameEQ. func HandleName(v string) predicate.User { return predicate.User(sql.FieldEQ(FieldHandleName, v)) } @@ -76,132 +76,132 @@ func UpdatedAt(v time.Time) predicate.User { return predicate.User(sql.FieldEQ(FieldUpdatedAt, v)) } -// StudentNumberEQ applies the EQ predicate on the "StudentNumber" field. +// StudentNumberEQ applies the EQ predicate on the "student_number" field. func StudentNumberEQ(v string) predicate.User { return predicate.User(sql.FieldEQ(FieldStudentNumber, v)) } -// StudentNumberNEQ applies the NEQ predicate on the "StudentNumber" field. +// StudentNumberNEQ applies the NEQ predicate on the "student_number" field. func StudentNumberNEQ(v string) predicate.User { return predicate.User(sql.FieldNEQ(FieldStudentNumber, v)) } -// StudentNumberIn applies the In predicate on the "StudentNumber" field. +// StudentNumberIn applies the In predicate on the "student_number" field. func StudentNumberIn(vs ...string) predicate.User { return predicate.User(sql.FieldIn(FieldStudentNumber, vs...)) } -// StudentNumberNotIn applies the NotIn predicate on the "StudentNumber" field. +// StudentNumberNotIn applies the NotIn predicate on the "student_number" field. func StudentNumberNotIn(vs ...string) predicate.User { return predicate.User(sql.FieldNotIn(FieldStudentNumber, vs...)) } -// StudentNumberGT applies the GT predicate on the "StudentNumber" field. +// StudentNumberGT applies the GT predicate on the "student_number" field. func StudentNumberGT(v string) predicate.User { return predicate.User(sql.FieldGT(FieldStudentNumber, v)) } -// StudentNumberGTE applies the GTE predicate on the "StudentNumber" field. +// StudentNumberGTE applies the GTE predicate on the "student_number" field. func StudentNumberGTE(v string) predicate.User { return predicate.User(sql.FieldGTE(FieldStudentNumber, v)) } -// StudentNumberLT applies the LT predicate on the "StudentNumber" field. +// StudentNumberLT applies the LT predicate on the "student_number" field. func StudentNumberLT(v string) predicate.User { return predicate.User(sql.FieldLT(FieldStudentNumber, v)) } -// StudentNumberLTE applies the LTE predicate on the "StudentNumber" field. +// StudentNumberLTE applies the LTE predicate on the "student_number" field. func StudentNumberLTE(v string) predicate.User { return predicate.User(sql.FieldLTE(FieldStudentNumber, v)) } -// StudentNumberContains applies the Contains predicate on the "StudentNumber" field. +// StudentNumberContains applies the Contains predicate on the "student_number" field. func StudentNumberContains(v string) predicate.User { return predicate.User(sql.FieldContains(FieldStudentNumber, v)) } -// StudentNumberHasPrefix applies the HasPrefix predicate on the "StudentNumber" field. +// StudentNumberHasPrefix applies the HasPrefix predicate on the "student_number" field. func StudentNumberHasPrefix(v string) predicate.User { return predicate.User(sql.FieldHasPrefix(FieldStudentNumber, v)) } -// StudentNumberHasSuffix applies the HasSuffix predicate on the "StudentNumber" field. +// StudentNumberHasSuffix applies the HasSuffix predicate on the "student_number" field. func StudentNumberHasSuffix(v string) predicate.User { return predicate.User(sql.FieldHasSuffix(FieldStudentNumber, v)) } -// StudentNumberEqualFold applies the EqualFold predicate on the "StudentNumber" field. +// StudentNumberEqualFold applies the EqualFold predicate on the "student_number" field. func StudentNumberEqualFold(v string) predicate.User { return predicate.User(sql.FieldEqualFold(FieldStudentNumber, v)) } -// StudentNumberContainsFold applies the ContainsFold predicate on the "StudentNumber" field. +// StudentNumberContainsFold applies the ContainsFold predicate on the "student_number" field. func StudentNumberContainsFold(v string) predicate.User { return predicate.User(sql.FieldContainsFold(FieldStudentNumber, v)) } -// HandleNameEQ applies the EQ predicate on the "HandleName" field. +// HandleNameEQ applies the EQ predicate on the "handle_name" field. func HandleNameEQ(v string) predicate.User { return predicate.User(sql.FieldEQ(FieldHandleName, v)) } -// HandleNameNEQ applies the NEQ predicate on the "HandleName" field. +// HandleNameNEQ applies the NEQ predicate on the "handle_name" field. func HandleNameNEQ(v string) predicate.User { return predicate.User(sql.FieldNEQ(FieldHandleName, v)) } -// HandleNameIn applies the In predicate on the "HandleName" field. +// HandleNameIn applies the In predicate on the "handle_name" field. func HandleNameIn(vs ...string) predicate.User { return predicate.User(sql.FieldIn(FieldHandleName, vs...)) } -// HandleNameNotIn applies the NotIn predicate on the "HandleName" field. +// HandleNameNotIn applies the NotIn predicate on the "handle_name" field. func HandleNameNotIn(vs ...string) predicate.User { return predicate.User(sql.FieldNotIn(FieldHandleName, vs...)) } -// HandleNameGT applies the GT predicate on the "HandleName" field. +// HandleNameGT applies the GT predicate on the "handle_name" field. func HandleNameGT(v string) predicate.User { return predicate.User(sql.FieldGT(FieldHandleName, v)) } -// HandleNameGTE applies the GTE predicate on the "HandleName" field. +// HandleNameGTE applies the GTE predicate on the "handle_name" field. func HandleNameGTE(v string) predicate.User { return predicate.User(sql.FieldGTE(FieldHandleName, v)) } -// HandleNameLT applies the LT predicate on the "HandleName" field. +// HandleNameLT applies the LT predicate on the "handle_name" field. func HandleNameLT(v string) predicate.User { return predicate.User(sql.FieldLT(FieldHandleName, v)) } -// HandleNameLTE applies the LTE predicate on the "HandleName" field. +// HandleNameLTE applies the LTE predicate on the "handle_name" field. func HandleNameLTE(v string) predicate.User { return predicate.User(sql.FieldLTE(FieldHandleName, v)) } -// HandleNameContains applies the Contains predicate on the "HandleName" field. +// HandleNameContains applies the Contains predicate on the "handle_name" field. func HandleNameContains(v string) predicate.User { return predicate.User(sql.FieldContains(FieldHandleName, v)) } -// HandleNameHasPrefix applies the HasPrefix predicate on the "HandleName" field. +// HandleNameHasPrefix applies the HasPrefix predicate on the "handle_name" field. func HandleNameHasPrefix(v string) predicate.User { return predicate.User(sql.FieldHasPrefix(FieldHandleName, v)) } -// HandleNameHasSuffix applies the HasSuffix predicate on the "HandleName" field. +// HandleNameHasSuffix applies the HasSuffix predicate on the "handle_name" field. func HandleNameHasSuffix(v string) predicate.User { return predicate.User(sql.FieldHasSuffix(FieldHandleName, v)) } -// HandleNameEqualFold applies the EqualFold predicate on the "HandleName" field. +// HandleNameEqualFold applies the EqualFold predicate on the "handle_name" field. func HandleNameEqualFold(v string) predicate.User { return predicate.User(sql.FieldEqualFold(FieldHandleName, v)) } -// HandleNameContainsFold applies the ContainsFold predicate on the "HandleName" field. +// HandleNameContainsFold applies the ContainsFold predicate on the "handle_name" field. func HandleNameContainsFold(v string) predicate.User { return predicate.User(sql.FieldContainsFold(FieldHandleName, v)) } diff --git a/typing-server/domain/repository/ent/user_create.go b/typing-server/domain/repository/ent/user_create.go index c2864ac..edae558 100644 --- a/typing-server/domain/repository/ent/user_create.go +++ b/typing-server/domain/repository/ent/user_create.go @@ -22,13 +22,13 @@ type UserCreate struct { hooks []Hook } -// SetStudentNumber sets the "StudentNumber" field. +// SetStudentNumber sets the "student_number" field. func (uc *UserCreate) SetStudentNumber(s string) *UserCreate { uc.mutation.SetStudentNumber(s) return uc } -// SetHandleName sets the "HandleName" field. +// SetHandleName sets the "handle_name" field. func (uc *UserCreate) SetHandleName(s string) *UserCreate { uc.mutation.SetHandleName(s) return uc @@ -143,19 +143,19 @@ func (uc *UserCreate) defaults() { // check runs all checks and user-defined validators on the builder. func (uc *UserCreate) check() error { if _, ok := uc.mutation.StudentNumber(); !ok { - return &ValidationError{Name: "StudentNumber", err: errors.New(`ent: missing required field "User.StudentNumber"`)} + return &ValidationError{Name: "student_number", err: errors.New(`ent: missing required field "User.student_number"`)} } if v, ok := uc.mutation.StudentNumber(); ok { if err := user.StudentNumberValidator(v); err != nil { - return &ValidationError{Name: "StudentNumber", err: fmt.Errorf(`ent: validator failed for field "User.StudentNumber": %w`, err)} + return &ValidationError{Name: "student_number", err: fmt.Errorf(`ent: validator failed for field "User.student_number": %w`, err)} } } if _, ok := uc.mutation.HandleName(); !ok { - return &ValidationError{Name: "HandleName", err: errors.New(`ent: missing required field "User.HandleName"`)} + return &ValidationError{Name: "handle_name", err: errors.New(`ent: missing required field "User.handle_name"`)} } if v, ok := uc.mutation.HandleName(); ok { if err := user.HandleNameValidator(v); err != nil { - return &ValidationError{Name: "HandleName", err: fmt.Errorf(`ent: validator failed for field "User.HandleName": %w`, err)} + return &ValidationError{Name: "handle_name", err: fmt.Errorf(`ent: validator failed for field "User.handle_name": %w`, err)} } } if _, ok := uc.mutation.CreatedAt(); !ok { diff --git a/typing-server/domain/repository/ent/user_query.go b/typing-server/domain/repository/ent/user_query.go index c88c6f5..a5b89b7 100644 --- a/typing-server/domain/repository/ent/user_query.go +++ b/typing-server/domain/repository/ent/user_query.go @@ -299,7 +299,7 @@ func (uq *UserQuery) WithScores(opts ...func(*ScoreQuery)) *UserQuery { // Example: // // var v []struct { -// StudentNumber string `json:"StudentNumber,omitempty"` +// StudentNumber string `json:"student_number,omitempty"` // Count int `json:"count,omitempty"` // } // @@ -322,7 +322,7 @@ func (uq *UserQuery) GroupBy(field string, fields ...string) *UserGroupBy { // Example: // // var v []struct { -// StudentNumber string `json:"StudentNumber,omitempty"` +// StudentNumber string `json:"student_number,omitempty"` // } // // client.User.Query(). diff --git a/typing-server/domain/repository/ent/user_update.go b/typing-server/domain/repository/ent/user_update.go index 98f1d92..68c90b6 100644 --- a/typing-server/domain/repository/ent/user_update.go +++ b/typing-server/domain/repository/ent/user_update.go @@ -30,13 +30,13 @@ func (uu *UserUpdate) Where(ps ...predicate.User) *UserUpdate { return uu } -// SetStudentNumber sets the "StudentNumber" field. +// SetStudentNumber sets the "student_number" field. func (uu *UserUpdate) SetStudentNumber(s string) *UserUpdate { uu.mutation.SetStudentNumber(s) return uu } -// SetNillableStudentNumber sets the "StudentNumber" field if the given value is not nil. +// SetNillableStudentNumber sets the "student_number" field if the given value is not nil. func (uu *UserUpdate) SetNillableStudentNumber(s *string) *UserUpdate { if s != nil { uu.SetStudentNumber(*s) @@ -44,13 +44,13 @@ func (uu *UserUpdate) SetNillableStudentNumber(s *string) *UserUpdate { return uu } -// SetHandleName sets the "HandleName" field. +// SetHandleName sets the "handle_name" field. func (uu *UserUpdate) SetHandleName(s string) *UserUpdate { uu.mutation.SetHandleName(s) return uu } -// SetNillableHandleName sets the "HandleName" field if the given value is not nil. +// SetNillableHandleName sets the "handle_name" field if the given value is not nil. func (uu *UserUpdate) SetNillableHandleName(s *string) *UserUpdate { if s != nil { uu.SetHandleName(*s) @@ -145,12 +145,12 @@ func (uu *UserUpdate) defaults() { func (uu *UserUpdate) check() error { if v, ok := uu.mutation.StudentNumber(); ok { if err := user.StudentNumberValidator(v); err != nil { - return &ValidationError{Name: "StudentNumber", err: fmt.Errorf(`ent: validator failed for field "User.StudentNumber": %w`, err)} + return &ValidationError{Name: "student_number", err: fmt.Errorf(`ent: validator failed for field "User.student_number": %w`, err)} } } if v, ok := uu.mutation.HandleName(); ok { if err := user.HandleNameValidator(v); err != nil { - return &ValidationError{Name: "HandleName", err: fmt.Errorf(`ent: validator failed for field "User.HandleName": %w`, err)} + return &ValidationError{Name: "handle_name", err: fmt.Errorf(`ent: validator failed for field "User.handle_name": %w`, err)} } } return nil @@ -242,13 +242,13 @@ type UserUpdateOne struct { mutation *UserMutation } -// SetStudentNumber sets the "StudentNumber" field. +// SetStudentNumber sets the "student_number" field. func (uuo *UserUpdateOne) SetStudentNumber(s string) *UserUpdateOne { uuo.mutation.SetStudentNumber(s) return uuo } -// SetNillableStudentNumber sets the "StudentNumber" field if the given value is not nil. +// SetNillableStudentNumber sets the "student_number" field if the given value is not nil. func (uuo *UserUpdateOne) SetNillableStudentNumber(s *string) *UserUpdateOne { if s != nil { uuo.SetStudentNumber(*s) @@ -256,13 +256,13 @@ func (uuo *UserUpdateOne) SetNillableStudentNumber(s *string) *UserUpdateOne { return uuo } -// SetHandleName sets the "HandleName" field. +// SetHandleName sets the "handle_name" field. func (uuo *UserUpdateOne) SetHandleName(s string) *UserUpdateOne { uuo.mutation.SetHandleName(s) return uuo } -// SetNillableHandleName sets the "HandleName" field if the given value is not nil. +// SetNillableHandleName sets the "handle_name" field if the given value is not nil. func (uuo *UserUpdateOne) SetNillableHandleName(s *string) *UserUpdateOne { if s != nil { uuo.SetHandleName(*s) @@ -370,12 +370,12 @@ func (uuo *UserUpdateOne) defaults() { func (uuo *UserUpdateOne) check() error { if v, ok := uuo.mutation.StudentNumber(); ok { if err := user.StudentNumberValidator(v); err != nil { - return &ValidationError{Name: "StudentNumber", err: fmt.Errorf(`ent: validator failed for field "User.StudentNumber": %w`, err)} + return &ValidationError{Name: "student_number", err: fmt.Errorf(`ent: validator failed for field "User.student_number": %w`, err)} } } if v, ok := uuo.mutation.HandleName(); ok { if err := user.HandleNameValidator(v); err != nil { - return &ValidationError{Name: "HandleName", err: fmt.Errorf(`ent: validator failed for field "User.HandleName": %w`, err)} + return &ValidationError{Name: "handle_name", err: fmt.Errorf(`ent: validator failed for field "User.handle_name": %w`, err)} } } return nil From 015909b25acf3bc39e7b196257037039865072b2 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Sat, 23 Mar 2024 11:15:36 +0900 Subject: [PATCH 02/26] =?UTF-8?q?WIP:=20api=E4=BD=9C=E3=81=A3=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/handler/score.go | 57 +++++++++++++++++++++++++++ typing-server/api/handler/user.go | 27 +++++++++++++ typing-server/api/presenter/server.go | 11 ------ typing-server/api/repository/score.go | 44 +++++++++++++++++++++ typing-server/api/repository/user.go | 25 ++++++++++++ typing-server/api/router/router.go | 22 +++++++++++ typing-server/api/service/score.go | 26 ++++++++++++ typing-server/api/service/user.go | 17 ++++++++ typing-server/domain/model/score.go | 12 ++++++ 9 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 typing-server/api/handler/score.go create mode 100644 typing-server/api/handler/user.go delete mode 100644 typing-server/api/presenter/server.go create mode 100644 typing-server/api/repository/score.go create mode 100644 typing-server/api/repository/user.go create mode 100644 typing-server/api/router/router.go create mode 100644 typing-server/api/service/score.go create mode 100644 typing-server/api/service/user.go create mode 100644 typing-server/domain/model/score.go diff --git a/typing-server/api/handler/score.go b/typing-server/api/handler/score.go new file mode 100644 index 0000000..37da3fc --- /dev/null +++ b/typing-server/api/handler/score.go @@ -0,0 +1,57 @@ +package handler + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/su-its/typing/typing-server/api/service" + "github.com/su-its/typing/typing-server/domain/repository/ent" +) + +func GetScoresRanking(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + sortBy := r.URL.Query().Get("sort_by") + if sortBy == "" { + sortBy = "keystrokes" + } + + startStr := r.URL.Query().Get("start") + start, err := strconv.Atoi(startStr) + if err != nil { + start = 1 + } + + limitStr := r.URL.Query().Get("limit") + limit, err := strconv.Atoi(limitStr) + if err != nil { + limit = 50 + } + + rankings, err := service.GetScoresRanking(ctx, sortBy, start, limit) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(rankings) +} + +func PostScore(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var score ent.Score + if err := json.NewDecoder(r.Body).Decode(&score); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := service.CreateScore(ctx, &score); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) +} diff --git a/typing-server/api/handler/user.go b/typing-server/api/handler/user.go new file mode 100644 index 0000000..4b5b8f2 --- /dev/null +++ b/typing-server/api/handler/user.go @@ -0,0 +1,27 @@ +package handler + +import ( + "encoding/json" + "net/http" + + "github.com/su-its/typing/typing-server/api/service" +) + +func GetUsers(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + studentNumber := r.URL.Query().Get("student_number") + if studentNumber == "" { + http.Error(w, "student_number is required", http.StatusBadRequest) + return + } + + user, err := service.GetUserByStudentNumber(ctx, studentNumber) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) +} diff --git a/typing-server/api/presenter/server.go b/typing-server/api/presenter/server.go deleted file mode 100644 index e921a4e..0000000 --- a/typing-server/api/presenter/server.go +++ /dev/null @@ -1,11 +0,0 @@ -package presenter - -import ( - "net/http" - - "github.com/su-its/typing/typing-server/api/controller/system" -) - -func RegisterRoutes() { - http.HandleFunc("/health", system.HealthCheck) -} diff --git a/typing-server/api/repository/score.go b/typing-server/api/repository/score.go new file mode 100644 index 0000000..2a1e1de --- /dev/null +++ b/typing-server/api/repository/score.go @@ -0,0 +1,44 @@ +package repository + +import ( + "context" + + "github.com/su-its/typing/typing-server/domain/model" + "github.com/su-its/typing/typing-server/domain/repository/ent" +) + +func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { + client := ent.FromContext(ctx) + + var rankings []*model.ScoreRanking + + err := client.Score.Query(). + WithUser(). + Order(ent.Desc(sortBy)). + Limit(limit). + Offset(start-1). + Select("id, keystrokes, accuracy, created_at"). + Scan(ctx, func(s *ent.Score) { + rankings = append(rankings, &model.ScoreRanking{ + Rank: start + len(rankings), + UserID: s.ID.String(), + Username: s.Edges.User.HandleName, + Score: s.Keystrokes, + Accuracy: s.Accuracy, + CreatedAt: s.CreatedAt, + }) + }) + return rankings, err +} + +func CreateScore(ctx context.Context, score *ent.Score) error { + client := ent.FromContext(ctx) + + _, err := client.Score.Create(). + SetKeystrokes(score.Keystrokes). + SetAccuracy(score.Accuracy). + SetUser(score.Edges.User). + Save(ctx) + + return err +} diff --git a/typing-server/api/repository/user.go b/typing-server/api/repository/user.go new file mode 100644 index 0000000..fedc20b --- /dev/null +++ b/typing-server/api/repository/user.go @@ -0,0 +1,25 @@ +package repository + +import ( + "context" + + "github.com/su-its/typing/typing-server/domain/repository/ent" + "github.com/su-its/typing/typing-server/domain/repository/ent/user" +) + +func GetUserByStudentNumber(ctx context.Context, studentNumber string) (*ent.User, error) { + client := ent.FromContext(ctx) + + entUser, err := client.User.Query(). + Where(user.StudentNumber(studentNumber)). + Only(ctx) + if err != nil { + return nil, err + } + + return &ent.User{ + ID: entUser.ID, + StudentNumber: entUser.StudentNumber, + HandleName: entUser.HandleName, + }, nil +} diff --git a/typing-server/api/router/router.go b/typing-server/api/router/router.go new file mode 100644 index 0000000..2f213cd --- /dev/null +++ b/typing-server/api/router/router.go @@ -0,0 +1,22 @@ +package router + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/su-its/typing/typing-server/api/controller/system" + "github.com/su-its/typing/typing-server/api/handler" +) + +func SetupRouter() http.Handler { + r := chi.NewRouter() + + r.Get("/health", system.HealthCheck) + + r.Get("/users", handler.GetUsers) + + r.Get("/scores/ranking", handler.GetScoresRanking) + r.Post("/scores", handler.PostScore) + + return r +} diff --git a/typing-server/api/service/score.go b/typing-server/api/service/score.go new file mode 100644 index 0000000..3267cf1 --- /dev/null +++ b/typing-server/api/service/score.go @@ -0,0 +1,26 @@ +package service + +import ( + "context" + + "github.com/su-its/typing/typing-server/api/repository" + "github.com/su-its/typing/typing-server/domain/model" + "github.com/su-its/typing/typing-server/domain/repository/ent" +) + +func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { + rankings, err := repository.GetScoresRanking(ctx, sortBy, start, limit) + if err != nil { + return nil, err + } + + return rankings, nil +} + +func CreateScore(ctx context.Context, score *ent.Score) error { + if err := repository.CreateScore(ctx, score); err != nil { + return err + } + + return nil +} diff --git a/typing-server/api/service/user.go b/typing-server/api/service/user.go new file mode 100644 index 0000000..10d449a --- /dev/null +++ b/typing-server/api/service/user.go @@ -0,0 +1,17 @@ +package service + +import ( + "context" + + "github.com/su-its/typing/typing-server/api/repository" + "github.com/su-its/typing/typing-server/domain/repository/ent" +) + +func GetUserByStudentNumber(ctx context.Context, studentNumber string) (*ent.User, error) { + user, err := repository.GetUserByStudentNumber(ctx, studentNumber) + if err != nil { + return nil, err + } + + return user, nil +} diff --git a/typing-server/domain/model/score.go b/typing-server/domain/model/score.go new file mode 100644 index 0000000..3e31347 --- /dev/null +++ b/typing-server/domain/model/score.go @@ -0,0 +1,12 @@ +package model + +import "time" + +type ScoreRanking struct { + Rank int `json:"rank"` + UserID string `json:"user_id"` + Username string `json:"username"` + Score int `json:"score"` + Accuracy float64 `json:"accuracy"` + CreatedAt time.Time `json:"created_at"` +} From 34551a04beab58d0914b2b29fe25d2272c963d0f Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Sat, 23 Mar 2024 15:43:48 +0900 Subject: [PATCH 03/26] =?UTF-8?q?chore:=20=E4=BE=9D=E5=AD=98=E9=96=A2?= =?UTF-8?q?=E4=BF=82=E3=82=92=E6=9B=B4=E6=96=B0=E3=81=97=E3=81=BE=E3=81=97?= =?UTF-8?q?=E3=81=9F:=20go-chi/chi/v5=E3=82=92v5.0.12=E3=81=AB=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/go.mod | 1 + typing-server/go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/typing-server/go.mod b/typing-server/go.mod index 4d5c904..754da99 100644 --- a/typing-server/go.mod +++ b/typing-server/go.mod @@ -4,6 +4,7 @@ go 1.22.0 require ( entgo.io/ent v0.13.1 + github.com/go-chi/chi/v5 v5.0.12 github.com/go-sql-driver/mysql v1.7.0 github.com/google/uuid v1.6.0 ) diff --git a/typing-server/go.sum b/typing-server/go.sum index a1a1a0e..5ba8169 100644 --- a/typing-server/go.sum +++ b/typing-server/go.sum @@ -10,6 +10,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= From d02645bb33559096e97e883ecc1bbc53d29218b3 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Sat, 23 Mar 2024 16:31:18 +0900 Subject: [PATCH 04/26] =?UTF-8?q?refactor:=20=E3=83=98=E3=83=AB=E3=82=B9?= =?UTF-8?q?=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E9=96=A2=E9=80=A3=E3=81=AE?= =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=89=E3=81=AE=E4=BD=8D=E7=BD=AE=E3=82=92?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/controller/system/health.go | 20 ------------------- typing-server/api/handler/health.go | 14 +++++++++++++ typing-server/api/router/router.go | 3 +-- 3 files changed, 15 insertions(+), 22 deletions(-) delete mode 100644 typing-server/api/controller/system/health.go create mode 100644 typing-server/api/handler/health.go diff --git a/typing-server/api/controller/system/health.go b/typing-server/api/controller/system/health.go deleted file mode 100644 index 937ac73..0000000 --- a/typing-server/api/controller/system/health.go +++ /dev/null @@ -1,20 +0,0 @@ -package system - -import ( - "log/slog" - "net/http" -) - -// HealthCheck はヘルスチェックのためのハンドラー関数です。 -func HealthCheck(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, err := w.Write([]byte("API is running")) - if err != nil { - // エラーログを記録し、処理を終了します。 - // 実際には、この時点でレスポンスヘッダーやボディがクライアントに送信されている可能性が高いため、 - // http.Errorを呼び出すことは推奨されません。 - // 代わりに、ログに記録するなどのサーバー側での対応が適切です。 - slog.Error("failed to write response: %v", err) - } -} - diff --git a/typing-server/api/handler/health.go b/typing-server/api/handler/health.go new file mode 100644 index 0000000..357ddc2 --- /dev/null +++ b/typing-server/api/handler/health.go @@ -0,0 +1,14 @@ +package handler + +import ( + "log/slog" + "net/http" +) + +func HealthCheck(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("API is running")) + if err != nil { + slog.Error("failed to write response: %v", err) + } +} diff --git a/typing-server/api/router/router.go b/typing-server/api/router/router.go index 2f213cd..c44dee9 100644 --- a/typing-server/api/router/router.go +++ b/typing-server/api/router/router.go @@ -4,14 +4,13 @@ import ( "net/http" "github.com/go-chi/chi/v5" - "github.com/su-its/typing/typing-server/api/controller/system" "github.com/su-its/typing/typing-server/api/handler" ) func SetupRouter() http.Handler { r := chi.NewRouter() - r.Get("/health", system.HealthCheck) + r.Get("/health", handler.HealthCheck) r.Get("/users", handler.GetUsers) From 62b04c9c335dbd8b65abb98ececda3d9d0884fd0 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Sun, 24 Mar 2024 23:34:16 +0900 Subject: [PATCH 05/26] =?UTF-8?q?feat:=20=E3=83=A6=E3=83=BC=E3=82=B6?= =?UTF-8?q?=E3=83=BC=E6=83=85=E5=A0=B1=E5=8F=96=E5=BE=97API=E3=81=AE?= =?UTF-8?q?=E3=83=91=E3=83=A9=E3=83=A1=E3=83=BC=E3=82=BF=E3=82=92=E5=A4=89?= =?UTF-8?q?=E6=9B=B4=E3=81=97=E3=80=81=E3=82=B9=E3=82=B3=E3=82=A2=E3=83=A9?= =?UTF-8?q?=E3=83=B3=E3=82=AD=E3=83=B3=E3=82=B0API=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/openapi.yaml | 196 +++++++++++-------------------------- 1 file changed, 56 insertions(+), 140 deletions(-) diff --git a/typing-server/openapi.yaml b/typing-server/openapi.yaml index 94e9455..0cf174c 100644 --- a/typing-server/openapi.yaml +++ b/typing-server/openapi.yaml @@ -9,153 +9,80 @@ servers: description: 本番環境 paths: /users: - post: - summary: 新しいユーザーを作成 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - MailAdress: - type: string - maxLength: 255 - HandleName: - type: string - maxLength: 36 - Name: - type: string - maxLength: 36 - HashedPassword: - type: string - maxLength: 255 - Department: - type: string - enum: [CS, BI, IA] - responses: - '201': - description: ユーザー作成成功 - '400': - description: 不正なリクエスト - /users/{userId}: get: summary: ユーザー情報を取得 parameters: - - in: path - name: userId - required: true + - in: query + name: student_number schema: type: string + description: 学生番号 + required: true responses: - '200': + "200": description: ユーザー情報取得成功 content: application/json: schema: - $ref: '#/components/schemas/User' - '404': + $ref: "#/components/schemas/User" + "404": description: ユーザーが見つからない - /users/{userId}/scores: - post: - summary: ユーザーのスコアを登録 - parameters: - - in: path - name: userId - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - keystrokes: - type: integer - accuracy: - type: number - format: float - score: - type: number - format: float - startedAt: - type: string - format: date-time - endedAt: - type: string - format: date-time - responses: - '201': - description: スコア登録成功 - '400': - description: 不正なリクエスト - /users/{userId}/scores/{scoreId}: + + /scores/ranking: get: - summary: 特定のスコア情報の取得 + summary: スコアランキングを取得 parameters: - - in: path - name: userId - required: true + - in: query + name: sort_by schema: type: string - - in: path - name: scoreId - required: true + enum: [keystrokes, accuracy] + description: ソート対象のカラム + - in: query + name: start schema: - type: string + type: integer + description: ランキングの開始位置 responses: - '200': - description: スコア情報の取得成功 + "200": + description: スコアランキング取得成功 content: application/json: schema: - $ref: '#/components/schemas/Score' - '404': - description: スコアが見つからない + type: array + items: + $ref: "#/components/schemas/ScoreRanking" - put: - summary: ユーザーのスコアを更新 + /scores: + post: + summary: スコアを登録 parameters: - - in: path - name: userId - required: true + - in: query + name: user_id schema: type: string - - in: path - name: scoreId + format: uuid required: true + description: ユーザーID + - in: query + name: keystrokes schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - keystrokes: - type: integer - accuracy: - type: number - format: float - score: - type: number - format: float - startedAt: - type: string - format: date-time - endedAt: - type: string - format: date-time + type: integer + required: true + description: キーストローク数 + - in: query + name: accuracy + schema: + type: number + format: float + required: true + description: 正確性 responses: - '200': - description: スコアの更新成功 - '400': + "201": + description: スコア登録成功 + "400": description: 不正なリクエスト - '404': - description: スコアが見つからない + components: schemas: User: @@ -164,40 +91,29 @@ components: id: type: string format: uuid - MailAdress: + student_number: type: string - HandleName: + handle_name: type: string - Name: - type: string - HashedPassword: - type: string - Department: - type: string - enum: [CS, BI, IA] created_at: type: string format: date-time updated_at: type: string format: date-time - Score: + + ScoreRanking: type: object properties: - id: - type: string - format: uuid - keystrokes: + rank: type: integer - accuracy: - type: number - format: float + user: + $ref: "#/components/schemas/User" score: + type: integer + accuracy: type: number format: float - startedAt: - type: string - format: date-time - endedAt: + created_at: type: string format: date-time From cd7a3b3589d84e1a3f05771162144db4f30f72ba Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Mon, 25 Mar 2024 00:40:33 +0900 Subject: [PATCH 06/26] =?UTF-8?q?docs:=20=E3=83=87=E3=82=A3=E3=83=AC?= =?UTF-8?q?=E3=82=AF=E3=83=88=E3=83=AA=E6=88=A6=E7=95=A5=E3=81=AB=E9=96=A2?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/docs/directory-strategy.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 typing-server/docs/directory-strategy.md diff --git a/typing-server/docs/directory-strategy.md b/typing-server/docs/directory-strategy.md new file mode 100644 index 0000000..7fd7d26 --- /dev/null +++ b/typing-server/docs/directory-strategy.md @@ -0,0 +1,20 @@ +. + +├── api +│ ├── cmd - アプリケーションのエントリーポイントとなるmainパッケージを格納するディレクトリ +│ ├── handler - APIのエンドポイントハンドラーを実装するパッケージを格納するディレクトリ +│ ├── repository - データ永続化層とのインターフェースを定義するパッケージを格納するディレクトリ +│ ├── router - APIルーティングを定義するパッケージを格納するディレクトリ +│ └── service - アプリケーションのビジネスロジックを実装するパッケージを格納するディレクトリ +├── domain +│ ├── model +│ └── repository - データ永続化層の実装を格納するディレクトリ +│ └── ent +│ └── schema - データベーススキーマの定義を格納するディレクトリ +├── Dockerfile +├── Dockerfile.dev +├── docker-compose.dev.yml +├── docker-compose.yml +├── go.mod +├── go.sum +└── openapi.yaml \ No newline at end of file From 5c36ba3e7847f40ff2edce8b1d138ba74c947b26 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Mon, 25 Mar 2024 00:59:17 +0900 Subject: [PATCH 07/26] =?UTF-8?q?docs:=20=E5=9E=8B=E3=81=AE=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/openapi.yaml | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/typing-server/openapi.yaml b/typing-server/openapi.yaml index 0cf174c..28a1f8d 100644 --- a/typing-server/openapi.yaml +++ b/typing-server/openapi.yaml @@ -90,7 +90,6 @@ components: properties: id: type: string - format: uuid student_number: type: string handle_name: @@ -102,14 +101,12 @@ components: type: string format: date-time - ScoreRanking: + Score: type: object properties: - rank: - type: integer - user: - $ref: "#/components/schemas/User" - score: + id: + type: string + keystrokes: type: integer accuracy: type: number @@ -117,3 +114,13 @@ components: created_at: type: string format: date-time + user: + $ref: "#/components/schemas/User" + + ScoreRanking: + type: object + properties: + rank: + type: integer + score: + $ref: "#/components/schemas/Score" From 82f7a323425902aad5935cb7d42aef7932ebd88b Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Mon, 25 Mar 2024 00:59:34 +0900 Subject: [PATCH 08/26] =?UTF-8?q?feat:=20=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/domain/model/model.go | 24 ++++++++++++++++++++++++ typing-server/domain/model/score.go | 12 ------------ 2 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 typing-server/domain/model/model.go delete mode 100644 typing-server/domain/model/score.go diff --git a/typing-server/domain/model/model.go b/typing-server/domain/model/model.go new file mode 100644 index 0000000..c176f7a --- /dev/null +++ b/typing-server/domain/model/model.go @@ -0,0 +1,24 @@ +package model + +import "time" + +type ScoreRanking struct { + Rank int `json:"rank"` + Score Score `json:"score"` +} + +type User struct { + ID string `json:"id"` + StudentNumber string `json:"student_number"` + HandleName string `json:"handle_name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type Score struct { + ID string `json:"id"` + Keystrokes int `json:"keystrokes"` + Accuracy float64 `json:"accuracy"` + CreatedAt time.Time `json:"created_at"` + User User `json:"user"` +} diff --git a/typing-server/domain/model/score.go b/typing-server/domain/model/score.go deleted file mode 100644 index 3e31347..0000000 --- a/typing-server/domain/model/score.go +++ /dev/null @@ -1,12 +0,0 @@ -package model - -import "time" - -type ScoreRanking struct { - Rank int `json:"rank"` - UserID string `json:"user_id"` - Username string `json:"username"` - Score int `json:"score"` - Accuracy float64 `json:"accuracy"` - CreatedAt time.Time `json:"created_at"` -} From e88b7ecfd4d156f808ad49fe4dc8ca51fb8821d0 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Mon, 25 Mar 2024 01:00:05 +0900 Subject: [PATCH 09/26] =?UTF-8?q?feat:=20openAPI=E3=81=AE=E4=BB=95?= =?UTF-8?q?=E6=A7=98=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6API?= =?UTF-8?q?=E3=82=92=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/handler/score.go | 33 ++++++++++++-------- typing-server/api/repository/score.go | 44 ++++++++++++++++++--------- typing-server/api/service/score.go | 10 +++--- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/typing-server/api/handler/score.go b/typing-server/api/handler/score.go index 37da3fc..005fb32 100644 --- a/typing-server/api/handler/score.go +++ b/typing-server/api/handler/score.go @@ -5,8 +5,8 @@ import ( "net/http" "strconv" + "github.com/google/uuid" "github.com/su-its/typing/typing-server/api/service" - "github.com/su-its/typing/typing-server/domain/repository/ent" ) func GetScoresRanking(w http.ResponseWriter, r *http.Request) { @@ -23,13 +23,7 @@ func GetScoresRanking(w http.ResponseWriter, r *http.Request) { start = 1 } - limitStr := r.URL.Query().Get("limit") - limit, err := strconv.Atoi(limitStr) - if err != nil { - limit = 50 - } - - rankings, err := service.GetScoresRanking(ctx, sortBy, start, limit) + rankings, err := service.GetScoresRanking(ctx, sortBy, start) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -42,13 +36,28 @@ func GetScoresRanking(w http.ResponseWriter, r *http.Request) { func PostScore(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - var score ent.Score - if err := json.NewDecoder(r.Body).Decode(&score); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + userIDStr := r.URL.Query().Get("user_id") + userID, err := uuid.Parse(userIDStr) + if err != nil { + http.Error(w, "Invalid user_id", http.StatusBadRequest) + return + } + + keystrokesStr := r.URL.Query().Get("keystrokes") + keystrokes, err := strconv.Atoi(keystrokesStr) + if err != nil { + http.Error(w, "Invalid keystrokes", http.StatusBadRequest) + return + } + + accuracyStr := r.URL.Query().Get("accuracy") + accuracy, err := strconv.ParseFloat(accuracyStr, 64) + if err != nil { + http.Error(w, "Invalid accuracy", http.StatusBadRequest) return } - if err := service.CreateScore(ctx, &score); err != nil { + if err := service.CreateScore(ctx, userID, keystrokes, accuracy); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/typing-server/api/repository/score.go b/typing-server/api/repository/score.go index 2a1e1de..db3b0d3 100644 --- a/typing-server/api/repository/score.go +++ b/typing-server/api/repository/score.go @@ -3,11 +3,12 @@ package repository import ( "context" + "github.com/google/uuid" "github.com/su-its/typing/typing-server/domain/model" "github.com/su-its/typing/typing-server/domain/repository/ent" ) -func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { +func GetScoresRanking(ctx context.Context, sortBy string, start int) ([]*model.ScoreRanking, error) { client := ent.FromContext(ctx) var rankings []*model.ScoreRanking @@ -15,29 +16,44 @@ func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]* err := client.Score.Query(). WithUser(). Order(ent.Desc(sortBy)). - Limit(limit). + Limit(50). Offset(start-1). Select("id, keystrokes, accuracy, created_at"). Scan(ctx, func(s *ent.Score) { - rankings = append(rankings, &model.ScoreRanking{ - Rank: start + len(rankings), - UserID: s.ID.String(), - Username: s.Edges.User.HandleName, - Score: s.Keystrokes, - Accuracy: s.Accuracy, - CreatedAt: s.CreatedAt, - }) + user := &model.User{ + ID: s.Edges.User.ID.String(), + StudentNumber: s.Edges.User.StudentNumber, + HandleName: s.Edges.User.HandleName, + CreatedAt: s.Edges.User.CreatedAt, + UpdatedAt: s.Edges.User.UpdatedAt, + } + + score := &model.Score{ + ID: s.ID.String(), + Keystrokes: s.Keystrokes, + Accuracy: s.Accuracy, + CreatedAt: s.CreatedAt, + User: *user, + } + + ranking := &model.ScoreRanking{ + Rank: start + len(rankings), + Score: *score, + } + + rankings = append(rankings, ranking) }) + return rankings, err } -func CreateScore(ctx context.Context, score *ent.Score) error { +func CreateScore(ctx context.Context, userID uuid.UUID, keystrokes int, accuracy float64) error { client := ent.FromContext(ctx) _, err := client.Score.Create(). - SetKeystrokes(score.Keystrokes). - SetAccuracy(score.Accuracy). - SetUser(score.Edges.User). + SetKeystrokes(keystrokes). + SetAccuracy(float64(keystrokes)). + SetUserID(userID). Save(ctx) return err diff --git a/typing-server/api/service/score.go b/typing-server/api/service/score.go index 3267cf1..605d81b 100644 --- a/typing-server/api/service/score.go +++ b/typing-server/api/service/score.go @@ -3,13 +3,13 @@ package service import ( "context" + "github.com/google/uuid" "github.com/su-its/typing/typing-server/api/repository" "github.com/su-its/typing/typing-server/domain/model" - "github.com/su-its/typing/typing-server/domain/repository/ent" ) -func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { - rankings, err := repository.GetScoresRanking(ctx, sortBy, start, limit) +func GetScoresRanking(ctx context.Context, sortBy string, start int) ([]*model.ScoreRanking, error) { + rankings, err := repository.GetScoresRanking(ctx, sortBy, start) if err != nil { return nil, err } @@ -17,8 +17,8 @@ func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]* return rankings, nil } -func CreateScore(ctx context.Context, score *ent.Score) error { - if err := repository.CreateScore(ctx, score); err != nil { +func CreateScore(ctx context.Context, userID uuid.UUID, keystrokes int, accuracy float64) error { + if err := repository.CreateScore(ctx, userID, keystrokes, accuracy); err != nil { return err } From 6638bdd8ef100545cddd09cbc1131b787f0f9727 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Mon, 25 Mar 2024 01:03:39 +0900 Subject: [PATCH 10/26] =?UTF-8?q?fix:=20API=E3=83=8F=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=83=A9=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=8F=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/handler/score.go | 6 +++++- typing-server/api/handler/user.go | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/typing-server/api/handler/score.go b/typing-server/api/handler/score.go index 005fb32..179da15 100644 --- a/typing-server/api/handler/score.go +++ b/typing-server/api/handler/score.go @@ -30,7 +30,11 @@ func GetScoresRanking(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(rankings) + err = json.NewEncoder(w).Encode(rankings) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } func PostScore(w http.ResponseWriter, r *http.Request) { diff --git a/typing-server/api/handler/user.go b/typing-server/api/handler/user.go index 4b5b8f2..8631892 100644 --- a/typing-server/api/handler/user.go +++ b/typing-server/api/handler/user.go @@ -23,5 +23,9 @@ func GetUsers(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(user) + err = json.NewEncoder(w).Encode(user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } From 9cf1eedd56ea36178c425748603b515beda9398e Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Mon, 25 Mar 2024 23:08:22 +0900 Subject: [PATCH 11/26] =?UTF-8?q?docs:=20openAPI=E3=82=92=E6=94=B9?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/openapi.yaml | 52 +++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/typing-server/openapi.yaml b/typing-server/openapi.yaml index 28a1f8d..21807c6 100644 --- a/typing-server/openapi.yaml +++ b/typing-server/openapi.yaml @@ -20,13 +20,13 @@ paths: required: true responses: "200": - description: ユーザー情報取得成功 + description: ユーザー情報を返します。 content: application/json: schema: $ref: "#/components/schemas/User" "404": - description: ユーザーが見つからない + description: ユーザーが見つかりません。 /scores/ranking: get: @@ -45,43 +45,43 @@ paths: description: ランキングの開始位置 responses: "200": - description: スコアランキング取得成功 + description: スコアランキングを返します。 content: application/json: schema: type: array items: $ref: "#/components/schemas/ScoreRanking" + "400": + description: 不正なリクエストです。 /scores: post: summary: スコアを登録 - parameters: - - in: query - name: user_id - schema: - type: string - format: uuid - required: true - description: ユーザーID - - in: query - name: keystrokes - schema: - type: integer - required: true - description: キーストローク数 - - in: query - name: accuracy - schema: - type: number - format: float - required: true - description: 正確性 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + user_id: + type: string + format: uuid + keystrokes: + type: integer + accuracy: + type: number + format: float + required: + - user_id + - keystrokes + - accuracy responses: "201": - description: スコア登録成功 + description: スコアが正常に登録されました。 "400": - description: 不正なリクエスト + description: 不正なリクエストです。 components: schemas: From 2f1cb5acf813e5fa6aac1b91bdcbfc5f9a9131cb Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Tue, 26 Mar 2024 08:56:16 +0900 Subject: [PATCH 12/26] =?UTF-8?q?feat:=20=E3=83=A9=E3=83=B3=E3=82=AD?= =?UTF-8?q?=E3=83=B3=E3=82=B0=E3=81=AE=E5=8F=96=E5=BE=97=E4=BB=B6=E6=95=B0?= =?UTF-8?q?=E3=81=AE=E3=82=AF=E3=82=A8=E3=83=AA=E3=83=91=E3=83=A9=E3=83=A1?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/openapi.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/typing-server/openapi.yaml b/typing-server/openapi.yaml index 21807c6..79238f5 100644 --- a/typing-server/openapi.yaml +++ b/typing-server/openapi.yaml @@ -43,6 +43,11 @@ paths: schema: type: integer description: ランキングの開始位置 + - in: query + name: limit + schema: + type: integer + description: ランキングの取得件数 responses: "200": description: スコアランキングを返します。 From ac3b5ada0505cb52841d8f6d38c146166248b042 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Tue, 26 Mar 2024 09:13:18 +0900 Subject: [PATCH 13/26] =?UTF-8?q?feat:=20GetScoresRanking=E9=96=A2?= =?UTF-8?q?=E6=95=B0=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/handler/score.go | 8 ++- typing-server/api/repository/score.go | 87 +++++++++++++++++---------- typing-server/api/service/score.go | 4 +- 3 files changed, 64 insertions(+), 35 deletions(-) diff --git a/typing-server/api/handler/score.go b/typing-server/api/handler/score.go index 179da15..7f27e7b 100644 --- a/typing-server/api/handler/score.go +++ b/typing-server/api/handler/score.go @@ -23,7 +23,13 @@ func GetScoresRanking(w http.ResponseWriter, r *http.Request) { start = 1 } - rankings, err := service.GetScoresRanking(ctx, sortBy, start) + limitStr := r.URL.Query().Get("limit") + limit, err := strconv.Atoi(limitStr) + if err != nil { + limit = 10 + } + + rankings, err := service.GetScoresRanking(ctx, sortBy, start, limit) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/typing-server/api/repository/score.go b/typing-server/api/repository/score.go index db3b0d3..ce57ed3 100644 --- a/typing-server/api/repository/score.go +++ b/typing-server/api/repository/score.go @@ -2,49 +2,72 @@ package repository import ( "context" + "fmt" "github.com/google/uuid" "github.com/su-its/typing/typing-server/domain/model" "github.com/su-its/typing/typing-server/domain/repository/ent" ) -func GetScoresRanking(ctx context.Context, sortBy string, start int) ([]*model.ScoreRanking, error) { +func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { client := ent.FromContext(ctx) - var rankings []*model.ScoreRanking - - err := client.Score.Query(). + scores, err := client.Score.Query(). WithUser(). Order(ent.Desc(sortBy)). - Limit(50). - Offset(start-1). + Limit(limit). + Offset(start - 1). Select("id, keystrokes, accuracy, created_at"). - Scan(ctx, func(s *ent.Score) { - user := &model.User{ - ID: s.Edges.User.ID.String(), - StudentNumber: s.Edges.User.StudentNumber, - HandleName: s.Edges.User.HandleName, - CreatedAt: s.Edges.User.CreatedAt, - UpdatedAt: s.Edges.User.UpdatedAt, - } - - score := &model.Score{ - ID: s.ID.String(), - Keystrokes: s.Keystrokes, - Accuracy: s.Accuracy, - CreatedAt: s.CreatedAt, - User: *user, - } - - ranking := &model.ScoreRanking{ - Rank: start + len(rankings), - Score: *score, - } - - rankings = append(rankings, ranking) - }) - - return rankings, err + All(ctx) + if err != nil { + return nil, err + } + + var rankings []*model.ScoreRanking + var prevScore float64 + var rank int + + for i, s := range scores { + user := &model.User{ + ID: s.Edges.User.ID.String(), + StudentNumber: s.Edges.User.StudentNumber, + HandleName: s.Edges.User.HandleName, + CreatedAt: s.Edges.User.CreatedAt, + UpdatedAt: s.Edges.User.UpdatedAt, + } + + score := &model.Score{ + ID: s.ID.String(), + Keystrokes: s.Keystrokes, + Accuracy: s.Accuracy, + CreatedAt: s.CreatedAt, + User: *user, + } + + var currentScore float64 + switch sortBy { + case "accuracy": + currentScore = s.Accuracy + case "keystrokes": + currentScore = float64(s.Keystrokes) + default: + return nil, fmt.Errorf("invalid sort by parameter: %s", sortBy) + } + + if i == 0 || currentScore != prevScore { + rank = start + i + } + prevScore = currentScore + + ranking := &model.ScoreRanking{ + Rank: rank, + Score: *score, + } + + rankings = append(rankings, ranking) + } + + return rankings, nil } func CreateScore(ctx context.Context, userID uuid.UUID, keystrokes int, accuracy float64) error { diff --git a/typing-server/api/service/score.go b/typing-server/api/service/score.go index 605d81b..9614f8d 100644 --- a/typing-server/api/service/score.go +++ b/typing-server/api/service/score.go @@ -8,8 +8,8 @@ import ( "github.com/su-its/typing/typing-server/domain/model" ) -func GetScoresRanking(ctx context.Context, sortBy string, start int) ([]*model.ScoreRanking, error) { - rankings, err := repository.GetScoresRanking(ctx, sortBy, start) +func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { + rankings, err := repository.GetScoresRanking(ctx, sortBy, start, limit) if err != nil { return nil, err } From 1ba5d501671e0621f2b646ab8d767771afa82250 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Tue, 26 Mar 2024 16:03:35 +0900 Subject: [PATCH 14/26] docs: Add descriptions for user_id, keystrokes, and accuracy fields in openapi.yaml --- typing-server/openapi.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/typing-server/openapi.yaml b/typing-server/openapi.yaml index 79238f5..37114bb 100644 --- a/typing-server/openapi.yaml +++ b/typing-server/openapi.yaml @@ -73,11 +73,14 @@ paths: user_id: type: string format: uuid + description: ユーザーID keystrokes: type: integer + description: キーストローク数 accuracy: type: number format: float + description: 正確性(ミスタイプ数/ストローク数) required: - user_id - keystrokes From e4ae07572e39a2228c8313d5b0d20418f78f5292 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Wed, 27 Mar 2024 10:59:26 +0900 Subject: [PATCH 15/26] =?UTF-8?q?fix:=20=E3=82=B9=E3=82=B3=E3=82=A2?= =?UTF-8?q?=E3=81=AE=E3=83=A9=E3=83=B3=E3=82=AD=E3=83=B3=E3=82=B0=E3=82=AF?= =?UTF-8?q?=E3=82=A8=E3=83=AA=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/repository/score.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/typing-server/api/repository/score.go b/typing-server/api/repository/score.go index ce57ed3..e107008 100644 --- a/typing-server/api/repository/score.go +++ b/typing-server/api/repository/score.go @@ -7,17 +7,27 @@ import ( "github.com/google/uuid" "github.com/su-its/typing/typing-server/domain/model" "github.com/su-its/typing/typing-server/domain/repository/ent" + "github.com/su-its/typing/typing-server/domain/repository/ent/score" ) func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { client := ent.FromContext(ctx) + // ! このクエリは要チェック scores, err := client.Score.Query(). WithUser(). + Where( + score.And( + score.KeystrokesGTE(120), + score.AccuracyGTE(95.0), + ), + ). Order(ent.Desc(sortBy)). Limit(limit). - Offset(start - 1). - Select("id, keystrokes, accuracy, created_at"). + Offset(start-1). + Select("user_id"). + Unique(true). + Select("id", "keystrokes", "accuracy", "created_at", "user_id"). All(ctx) if err != nil { return nil, err From d02e5c0a76c94fc64b9a3d0dbcd56cb9bcecac80 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Wed, 27 Mar 2024 13:19:30 +0900 Subject: [PATCH 16/26] =?UTF-8?q?fix:=20=E3=83=AB=E3=83=BC=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=83=B3=E3=82=B0=E3=81=99=E3=82=8B=E3=81=AE=E5=BF=98?= =?UTF-8?q?=E3=82=8C=E3=81=A6=E3=81=9F=E7=AC=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/cmd/main.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/typing-server/api/cmd/main.go b/typing-server/api/cmd/main.go index e592914..e0ca3d0 100644 --- a/typing-server/api/cmd/main.go +++ b/typing-server/api/cmd/main.go @@ -14,6 +14,7 @@ import ( "time" "github.com/go-sql-driver/mysql" + "github.com/su-its/typing/typing-server/api/router" "github.com/su-its/typing/typing-server/domain/repository/ent" "github.com/su-its/typing/typing-server/domain/repository/ent/user" ) @@ -86,7 +87,14 @@ func main() { go func() { defer wg.Done() // 関数終了時にWaitGroupをデクリメント // サーバーの設定 - server := &http.Server{Addr: ":8080"} + // ルーティングの設定 + r := router.SetupRouter() + + // サーバーの設定 + server := &http.Server{ + Addr: ":8080", + Handler: r, + } // 非同期でサーバーを開始 go func() { logger.Info("server is running at Addr :8080") From 2ff47ef35fa18708cdd3f2ecef54ec40b6691807 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Wed, 27 Mar 2024 14:01:00 +0900 Subject: [PATCH 17/26] =?UTF-8?q?feat:=20cors=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/router/router.go | 13 ++++++++++++- typing-server/go.mod | 1 + typing-server/go.sum | 2 ++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/typing-server/api/router/router.go b/typing-server/api/router/router.go index c44dee9..7dba68a 100644 --- a/typing-server/api/router/router.go +++ b/typing-server/api/router/router.go @@ -4,15 +4,26 @@ import ( "net/http" "github.com/go-chi/chi/v5" + "github.com/rs/cors" "github.com/su-its/typing/typing-server/api/handler" ) func SetupRouter() http.Handler { r := chi.NewRouter() + // CORSの設定 + cors := cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, // 許可するオリジンを指定 + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + MaxAge: 300, // Preflightリクエストの結果をキャッシュする時間(秒) + }) + r.Use(cors.Handler) + r.Get("/health", handler.HealthCheck) - r.Get("/users", handler.GetUsers) + r.Get("/users", handler.GetUser) r.Get("/scores/ranking", handler.GetScoresRanking) r.Post("/scores", handler.PostScore) diff --git a/typing-server/go.mod b/typing-server/go.mod index 754da99..6c8123c 100644 --- a/typing-server/go.mod +++ b/typing-server/go.mod @@ -17,6 +17,7 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/hcl/v2 v2.19.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/rs/cors v1.10.1 github.com/zclconf/go-cty v1.14.2 // indirect golang.org/x/mod v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/typing-server/go.sum b/typing-server/go.sum index 5ba8169..8138d96 100644 --- a/typing-server/go.sum +++ b/typing-server/go.sum @@ -36,6 +36,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= From 3ded660067c09d4f053096ac58305ed17fe85a9c Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Wed, 27 Mar 2024 14:01:18 +0900 Subject: [PATCH 18/26] =?UTF-8?q?docs:=20=E3=83=98=E3=83=AB=E3=82=B9?= =?UTF-8?q?=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/openapi.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/typing-server/openapi.yaml b/typing-server/openapi.yaml index 37114bb..cd436d8 100644 --- a/typing-server/openapi.yaml +++ b/typing-server/openapi.yaml @@ -8,6 +8,12 @@ servers: - url: https://typemaster.com description: 本番環境 paths: + /health: + get: + summary: サーバーの状態を取得 + responses: + "200": + description: サーバーが正常に稼働しています。 /users: get: summary: ユーザー情報を取得 From 75569eb97e4dd6d620c1387e5bd3e9737e3b0c15 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Wed, 27 Mar 2024 14:01:37 +0900 Subject: [PATCH 19/26] =?UTF-8?q?fix:=20entCleint=E3=81=8C=E6=B8=A1?= =?UTF-8?q?=E3=81=9B=E3=81=A6=E3=81=84=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/cmd/main.go | 2 ++ typing-server/api/handler/options.go | 9 +++++++++ typing-server/api/handler/score.go | 4 ++-- typing-server/api/handler/user.go | 4 ++-- typing-server/api/repository/score.go | 10 +++------- typing-server/api/repository/user.go | 6 ++---- typing-server/api/service/score.go | 9 +++++---- typing-server/api/service/user.go | 4 ++-- 8 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 typing-server/api/handler/options.go diff --git a/typing-server/api/cmd/main.go b/typing-server/api/cmd/main.go index e0ca3d0..a34062f 100644 --- a/typing-server/api/cmd/main.go +++ b/typing-server/api/cmd/main.go @@ -14,6 +14,7 @@ import ( "time" "github.com/go-sql-driver/mysql" + "github.com/su-its/typing/typing-server/api/handler" "github.com/su-its/typing/typing-server/api/router" "github.com/su-its/typing/typing-server/domain/repository/ent" "github.com/su-its/typing/typing-server/domain/repository/ent/user" @@ -55,6 +56,7 @@ func main() { return } defer entClient.Close() + handler.SetEntClient(entClient) logger.Info("ent client is opened") // スキーマの作成 diff --git a/typing-server/api/handler/options.go b/typing-server/api/handler/options.go new file mode 100644 index 0000000..0dc6867 --- /dev/null +++ b/typing-server/api/handler/options.go @@ -0,0 +1,9 @@ +package handler + +import "github.com/su-its/typing/typing-server/domain/repository/ent" + +var entClient *ent.Client + +func SetEntClient(client *ent.Client) { + entClient = client +} diff --git a/typing-server/api/handler/score.go b/typing-server/api/handler/score.go index 7f27e7b..b061ab3 100644 --- a/typing-server/api/handler/score.go +++ b/typing-server/api/handler/score.go @@ -29,7 +29,7 @@ func GetScoresRanking(w http.ResponseWriter, r *http.Request) { limit = 10 } - rankings, err := service.GetScoresRanking(ctx, sortBy, start, limit) + rankings, err := service.GetScoresRanking(ctx, entClient, sortBy, start, limit) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -67,7 +67,7 @@ func PostScore(w http.ResponseWriter, r *http.Request) { return } - if err := service.CreateScore(ctx, userID, keystrokes, accuracy); err != nil { + if err := service.CreateScore(ctx, entClient, userID, keystrokes, accuracy); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/typing-server/api/handler/user.go b/typing-server/api/handler/user.go index 8631892..2fb0c88 100644 --- a/typing-server/api/handler/user.go +++ b/typing-server/api/handler/user.go @@ -7,7 +7,7 @@ import ( "github.com/su-its/typing/typing-server/api/service" ) -func GetUsers(w http.ResponseWriter, r *http.Request) { +func GetUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() studentNumber := r.URL.Query().Get("student_number") @@ -16,7 +16,7 @@ func GetUsers(w http.ResponseWriter, r *http.Request) { return } - user, err := service.GetUserByStudentNumber(ctx, studentNumber) + user, err := service.GetUserByStudentNumber(ctx, entClient, studentNumber) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/typing-server/api/repository/score.go b/typing-server/api/repository/score.go index e107008..17e5dee 100644 --- a/typing-server/api/repository/score.go +++ b/typing-server/api/repository/score.go @@ -10,9 +10,7 @@ import ( "github.com/su-its/typing/typing-server/domain/repository/ent/score" ) -func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { - client := ent.FromContext(ctx) - +func GetScoresRanking(ctx context.Context, client *ent.Client, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { // ! このクエリは要チェック scores, err := client.Score.Query(). WithUser(). @@ -24,7 +22,7 @@ func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]* ). Order(ent.Desc(sortBy)). Limit(limit). - Offset(start-1). + Offset(start-1). Select("user_id"). Unique(true). Select("id", "keystrokes", "accuracy", "created_at", "user_id"). @@ -80,9 +78,7 @@ func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]* return rankings, nil } -func CreateScore(ctx context.Context, userID uuid.UUID, keystrokes int, accuracy float64) error { - client := ent.FromContext(ctx) - +func CreateScore(ctx context.Context, client *ent.Client, userID uuid.UUID, keystrokes int, accuracy float64) error { _, err := client.Score.Create(). SetKeystrokes(keystrokes). SetAccuracy(float64(keystrokes)). diff --git a/typing-server/api/repository/user.go b/typing-server/api/repository/user.go index fedc20b..8629f46 100644 --- a/typing-server/api/repository/user.go +++ b/typing-server/api/repository/user.go @@ -7,11 +7,9 @@ import ( "github.com/su-its/typing/typing-server/domain/repository/ent/user" ) -func GetUserByStudentNumber(ctx context.Context, studentNumber string) (*ent.User, error) { - client := ent.FromContext(ctx) - +func GetUserByStudentNumber(ctx context.Context, client *ent.Client, studentNumber string) (*ent.User, error) { entUser, err := client.User.Query(). - Where(user.StudentNumber(studentNumber)). + Where(user.StudentNumberEQ(studentNumber)). Only(ctx) if err != nil { return nil, err diff --git a/typing-server/api/service/score.go b/typing-server/api/service/score.go index 9614f8d..15025a8 100644 --- a/typing-server/api/service/score.go +++ b/typing-server/api/service/score.go @@ -6,10 +6,11 @@ import ( "github.com/google/uuid" "github.com/su-its/typing/typing-server/api/repository" "github.com/su-its/typing/typing-server/domain/model" + "github.com/su-its/typing/typing-server/domain/repository/ent" ) -func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { - rankings, err := repository.GetScoresRanking(ctx, sortBy, start, limit) +func GetScoresRanking(ctx context.Context, client *ent.Client, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { + rankings, err := repository.GetScoresRanking(ctx, client, sortBy, start, limit) if err != nil { return nil, err } @@ -17,8 +18,8 @@ func GetScoresRanking(ctx context.Context, sortBy string, start, limit int) ([]* return rankings, nil } -func CreateScore(ctx context.Context, userID uuid.UUID, keystrokes int, accuracy float64) error { - if err := repository.CreateScore(ctx, userID, keystrokes, accuracy); err != nil { +func CreateScore(ctx context.Context, client *ent.Client, userID uuid.UUID, keystrokes int, accuracy float64) error { + if err := repository.CreateScore(ctx, client, userID, keystrokes, accuracy); err != nil { return err } diff --git a/typing-server/api/service/user.go b/typing-server/api/service/user.go index 10d449a..6c95f3f 100644 --- a/typing-server/api/service/user.go +++ b/typing-server/api/service/user.go @@ -7,8 +7,8 @@ import ( "github.com/su-its/typing/typing-server/domain/repository/ent" ) -func GetUserByStudentNumber(ctx context.Context, studentNumber string) (*ent.User, error) { - user, err := repository.GetUserByStudentNumber(ctx, studentNumber) +func GetUserByStudentNumber(ctx context.Context, client *ent.Client, studentNumber string) (*ent.User, error) { + user, err := repository.GetUserByStudentNumber(ctx, client, studentNumber) if err != nil { return nil, err } From 2a521cc91d61eb269de1a67d5e7a398540143bd2 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Wed, 27 Mar 2024 19:34:37 +0900 Subject: [PATCH 20/26] =?UTF-8?q?docs:=20=E3=83=A9=E3=83=B3=E3=82=AD?= =?UTF-8?q?=E3=83=B3=E3=82=B0=E3=81=AE=E9=96=8B=E5=A7=8B=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E3=81=AE=E8=AA=AC=E6=98=8E=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/openapi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typing-server/openapi.yaml b/typing-server/openapi.yaml index cd436d8..6f155d5 100644 --- a/typing-server/openapi.yaml +++ b/typing-server/openapi.yaml @@ -48,7 +48,7 @@ paths: name: start schema: type: integer - description: ランキングの開始位置 + description: ランキングの開始位置(x位 | x>0) - in: query name: limit schema: From 407c9d3192850d4ae7cb8c447fe1bbc3478607df Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Wed, 27 Mar 2024 19:36:57 +0900 Subject: [PATCH 21/26] =?UTF-8?q?feat:=20score=E3=82=A8=E3=83=B3=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=83=86=E3=82=A3=E3=81=AB=E3=81=9D=E3=82=8C=E3=81=9E?= =?UTF-8?q?=E3=82=8C=E3=81=AE=E6=8C=87=E6=A8=99=E3=81=8CUser=E3=81=AB?= =?UTF-8?q?=E3=81=A8=E3=81=A3=E3=81=A6=E6=9C=80=E5=A4=A7=E3=81=AE=E3=82=82?= =?UTF-8?q?=E3=81=AE=E3=81=8B=E3=81=A9=E3=81=86=E3=81=8B=E3=82=92=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E3=81=99=E3=82=8BField=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/domain/model/model.go | 1 + .../domain/repository/ent/migrate/schema.go | 6 +- .../domain/repository/ent/mutation.go | 248 ++++++++++++++++-- .../domain/repository/ent/runtime.go | 2 +- .../domain/repository/ent/schema/score.go | 4 + typing-server/domain/repository/ent/score.go | 47 +++- .../domain/repository/ent/score/score.go | 37 ++- .../domain/repository/ent/score/where.go | 75 ++++++ .../domain/repository/ent/score_create.go | 53 +++- .../domain/repository/ent/score_query.go | 26 +- .../domain/repository/ent/score_update.go | 132 +++++++++- .../domain/repository/ent/user/user.go | 2 +- .../domain/repository/ent/user_query.go | 13 +- 13 files changed, 553 insertions(+), 93 deletions(-) diff --git a/typing-server/domain/model/model.go b/typing-server/domain/model/model.go index c176f7a..0239d0f 100644 --- a/typing-server/domain/model/model.go +++ b/typing-server/domain/model/model.go @@ -17,6 +17,7 @@ type User struct { type Score struct { ID string `json:"id"` + UserID string `json:"user_id"` Keystrokes int `json:"keystrokes"` Accuracy float64 `json:"accuracy"` CreatedAt time.Time `json:"created_at"` diff --git a/typing-server/domain/repository/ent/migrate/schema.go b/typing-server/domain/repository/ent/migrate/schema.go index 2a4d8a0..00f4c94 100644 --- a/typing-server/domain/repository/ent/migrate/schema.go +++ b/typing-server/domain/repository/ent/migrate/schema.go @@ -13,8 +13,10 @@ var ( {Name: "id", Type: field.TypeUUID, Unique: true}, {Name: "keystrokes", Type: field.TypeInt}, {Name: "accuracy", Type: field.TypeFloat64}, + {Name: "is_max_keystrokes", Type: field.TypeBool, Nullable: true}, + {Name: "is_max_accuracy", Type: field.TypeBool, Nullable: true}, {Name: "created_at", Type: field.TypeTime}, - {Name: "user_scores", Type: field.TypeUUID}, + {Name: "user_id", Type: field.TypeUUID}, } // ScoresTable holds the schema information for the "scores" table. ScoresTable = &schema.Table{ @@ -24,7 +26,7 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "scores_users_scores", - Columns: []*schema.Column{ScoresColumns[4]}, + Columns: []*schema.Column{ScoresColumns[6]}, RefColumns: []*schema.Column{UsersColumns[0]}, OnDelete: schema.NoAction, }, diff --git a/typing-server/domain/repository/ent/mutation.go b/typing-server/domain/repository/ent/mutation.go index 99be27b..7fb5d40 100644 --- a/typing-server/domain/repository/ent/mutation.go +++ b/typing-server/domain/repository/ent/mutation.go @@ -33,20 +33,22 @@ const ( // ScoreMutation represents an operation that mutates the Score nodes in the graph. type ScoreMutation struct { config - op Op - typ string - id *uuid.UUID - keystrokes *int - addkeystrokes *int - accuracy *float64 - addaccuracy *float64 - created_at *time.Time - clearedFields map[string]struct{} - user *uuid.UUID - cleareduser bool - done bool - oldValue func(context.Context) (*Score, error) - predicates []predicate.Score + op Op + typ string + id *uuid.UUID + keystrokes *int + addkeystrokes *int + accuracy *float64 + addaccuracy *float64 + is_max_keystrokes *bool + is_max_accuracy *bool + created_at *time.Time + clearedFields map[string]struct{} + user *uuid.UUID + cleareduser bool + done bool + oldValue func(context.Context) (*Score, error) + predicates []predicate.Score } var _ ent.Mutation = (*ScoreMutation)(nil) @@ -153,6 +155,42 @@ func (m *ScoreMutation) IDs(ctx context.Context) ([]uuid.UUID, error) { } } +// SetUserID sets the "user_id" field. +func (m *ScoreMutation) SetUserID(u uuid.UUID) { + m.user = &u +} + +// UserID returns the value of the "user_id" field in the mutation. +func (m *ScoreMutation) UserID() (r uuid.UUID, exists bool) { + v := m.user + if v == nil { + return + } + return *v, true +} + +// OldUserID returns the old "user_id" field's value of the Score entity. +// If the Score object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScoreMutation) OldUserID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUserID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUserID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUserID: %w", err) + } + return oldValue.UserID, nil +} + +// ResetUserID resets all changes to the "user_id" field. +func (m *ScoreMutation) ResetUserID() { + m.user = nil +} + // SetKeystrokes sets the "keystrokes" field. func (m *ScoreMutation) SetKeystrokes(i int) { m.keystrokes = &i @@ -265,6 +303,104 @@ func (m *ScoreMutation) ResetAccuracy() { m.addaccuracy = nil } +// SetIsMaxKeystrokes sets the "is_max_keystrokes" field. +func (m *ScoreMutation) SetIsMaxKeystrokes(b bool) { + m.is_max_keystrokes = &b +} + +// IsMaxKeystrokes returns the value of the "is_max_keystrokes" field in the mutation. +func (m *ScoreMutation) IsMaxKeystrokes() (r bool, exists bool) { + v := m.is_max_keystrokes + if v == nil { + return + } + return *v, true +} + +// OldIsMaxKeystrokes returns the old "is_max_keystrokes" field's value of the Score entity. +// If the Score object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScoreMutation) OldIsMaxKeystrokes(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldIsMaxKeystrokes is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldIsMaxKeystrokes requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldIsMaxKeystrokes: %w", err) + } + return oldValue.IsMaxKeystrokes, nil +} + +// ClearIsMaxKeystrokes clears the value of the "is_max_keystrokes" field. +func (m *ScoreMutation) ClearIsMaxKeystrokes() { + m.is_max_keystrokes = nil + m.clearedFields[score.FieldIsMaxKeystrokes] = struct{}{} +} + +// IsMaxKeystrokesCleared returns if the "is_max_keystrokes" field was cleared in this mutation. +func (m *ScoreMutation) IsMaxKeystrokesCleared() bool { + _, ok := m.clearedFields[score.FieldIsMaxKeystrokes] + return ok +} + +// ResetIsMaxKeystrokes resets all changes to the "is_max_keystrokes" field. +func (m *ScoreMutation) ResetIsMaxKeystrokes() { + m.is_max_keystrokes = nil + delete(m.clearedFields, score.FieldIsMaxKeystrokes) +} + +// SetIsMaxAccuracy sets the "is_max_accuracy" field. +func (m *ScoreMutation) SetIsMaxAccuracy(b bool) { + m.is_max_accuracy = &b +} + +// IsMaxAccuracy returns the value of the "is_max_accuracy" field in the mutation. +func (m *ScoreMutation) IsMaxAccuracy() (r bool, exists bool) { + v := m.is_max_accuracy + if v == nil { + return + } + return *v, true +} + +// OldIsMaxAccuracy returns the old "is_max_accuracy" field's value of the Score entity. +// If the Score object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ScoreMutation) OldIsMaxAccuracy(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldIsMaxAccuracy is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldIsMaxAccuracy requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldIsMaxAccuracy: %w", err) + } + return oldValue.IsMaxAccuracy, nil +} + +// ClearIsMaxAccuracy clears the value of the "is_max_accuracy" field. +func (m *ScoreMutation) ClearIsMaxAccuracy() { + m.is_max_accuracy = nil + m.clearedFields[score.FieldIsMaxAccuracy] = struct{}{} +} + +// IsMaxAccuracyCleared returns if the "is_max_accuracy" field was cleared in this mutation. +func (m *ScoreMutation) IsMaxAccuracyCleared() bool { + _, ok := m.clearedFields[score.FieldIsMaxAccuracy] + return ok +} + +// ResetIsMaxAccuracy resets all changes to the "is_max_accuracy" field. +func (m *ScoreMutation) ResetIsMaxAccuracy() { + m.is_max_accuracy = nil + delete(m.clearedFields, score.FieldIsMaxAccuracy) +} + // SetCreatedAt sets the "created_at" field. func (m *ScoreMutation) SetCreatedAt(t time.Time) { m.created_at = &t @@ -301,14 +437,10 @@ func (m *ScoreMutation) ResetCreatedAt() { m.created_at = nil } -// SetUserID sets the "user" edge to the User entity by id. -func (m *ScoreMutation) SetUserID(id uuid.UUID) { - m.user = &id -} - // ClearUser clears the "user" edge to the User entity. func (m *ScoreMutation) ClearUser() { m.cleareduser = true + m.clearedFields[score.FieldUserID] = struct{}{} } // UserCleared reports if the "user" edge to the User entity was cleared. @@ -316,14 +448,6 @@ func (m *ScoreMutation) UserCleared() bool { return m.cleareduser } -// UserID returns the "user" edge ID in the mutation. -func (m *ScoreMutation) UserID() (id uuid.UUID, exists bool) { - if m.user != nil { - return *m.user, true - } - return -} - // UserIDs returns the "user" edge IDs in the mutation. // Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use // UserID instead. It exists only for internal usage by the builders. @@ -374,13 +498,22 @@ func (m *ScoreMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ScoreMutation) Fields() []string { - fields := make([]string, 0, 3) + fields := make([]string, 0, 6) + if m.user != nil { + fields = append(fields, score.FieldUserID) + } if m.keystrokes != nil { fields = append(fields, score.FieldKeystrokes) } if m.accuracy != nil { fields = append(fields, score.FieldAccuracy) } + if m.is_max_keystrokes != nil { + fields = append(fields, score.FieldIsMaxKeystrokes) + } + if m.is_max_accuracy != nil { + fields = append(fields, score.FieldIsMaxAccuracy) + } if m.created_at != nil { fields = append(fields, score.FieldCreatedAt) } @@ -392,10 +525,16 @@ func (m *ScoreMutation) Fields() []string { // schema. func (m *ScoreMutation) Field(name string) (ent.Value, bool) { switch name { + case score.FieldUserID: + return m.UserID() case score.FieldKeystrokes: return m.Keystrokes() case score.FieldAccuracy: return m.Accuracy() + case score.FieldIsMaxKeystrokes: + return m.IsMaxKeystrokes() + case score.FieldIsMaxAccuracy: + return m.IsMaxAccuracy() case score.FieldCreatedAt: return m.CreatedAt() } @@ -407,10 +546,16 @@ func (m *ScoreMutation) Field(name string) (ent.Value, bool) { // database failed. func (m *ScoreMutation) OldField(ctx context.Context, name string) (ent.Value, error) { switch name { + case score.FieldUserID: + return m.OldUserID(ctx) case score.FieldKeystrokes: return m.OldKeystrokes(ctx) case score.FieldAccuracy: return m.OldAccuracy(ctx) + case score.FieldIsMaxKeystrokes: + return m.OldIsMaxKeystrokes(ctx) + case score.FieldIsMaxAccuracy: + return m.OldIsMaxAccuracy(ctx) case score.FieldCreatedAt: return m.OldCreatedAt(ctx) } @@ -422,6 +567,13 @@ func (m *ScoreMutation) OldField(ctx context.Context, name string) (ent.Value, e // type. func (m *ScoreMutation) SetField(name string, value ent.Value) error { switch name { + case score.FieldUserID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUserID(v) + return nil case score.FieldKeystrokes: v, ok := value.(int) if !ok { @@ -436,6 +588,20 @@ func (m *ScoreMutation) SetField(name string, value ent.Value) error { } m.SetAccuracy(v) return nil + case score.FieldIsMaxKeystrokes: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetIsMaxKeystrokes(v) + return nil + case score.FieldIsMaxAccuracy: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetIsMaxAccuracy(v) + return nil case score.FieldCreatedAt: v, ok := value.(time.Time) if !ok { @@ -499,7 +665,14 @@ func (m *ScoreMutation) AddField(name string, value ent.Value) error { // ClearedFields returns all nullable fields that were cleared during this // mutation. func (m *ScoreMutation) ClearedFields() []string { - return nil + var fields []string + if m.FieldCleared(score.FieldIsMaxKeystrokes) { + fields = append(fields, score.FieldIsMaxKeystrokes) + } + if m.FieldCleared(score.FieldIsMaxAccuracy) { + fields = append(fields, score.FieldIsMaxAccuracy) + } + return fields } // FieldCleared returns a boolean indicating if a field with the given name was @@ -512,6 +685,14 @@ func (m *ScoreMutation) FieldCleared(name string) bool { // ClearField clears the value of the field with the given name. It returns an // error if the field is not defined in the schema. func (m *ScoreMutation) ClearField(name string) error { + switch name { + case score.FieldIsMaxKeystrokes: + m.ClearIsMaxKeystrokes() + return nil + case score.FieldIsMaxAccuracy: + m.ClearIsMaxAccuracy() + return nil + } return fmt.Errorf("unknown Score nullable field %s", name) } @@ -519,12 +700,21 @@ func (m *ScoreMutation) ClearField(name string) error { // It returns an error if the field is not defined in the schema. func (m *ScoreMutation) ResetField(name string) error { switch name { + case score.FieldUserID: + m.ResetUserID() + return nil case score.FieldKeystrokes: m.ResetKeystrokes() return nil case score.FieldAccuracy: m.ResetAccuracy() return nil + case score.FieldIsMaxKeystrokes: + m.ResetIsMaxKeystrokes() + return nil + case score.FieldIsMaxAccuracy: + m.ResetIsMaxAccuracy() + return nil case score.FieldCreatedAt: m.ResetCreatedAt() return nil diff --git a/typing-server/domain/repository/ent/runtime.go b/typing-server/domain/repository/ent/runtime.go index 5485c0b..7f65234 100644 --- a/typing-server/domain/repository/ent/runtime.go +++ b/typing-server/domain/repository/ent/runtime.go @@ -18,7 +18,7 @@ func init() { scoreFields := schema.Score{}.Fields() _ = scoreFields // scoreDescCreatedAt is the schema descriptor for created_at field. - scoreDescCreatedAt := scoreFields[3].Descriptor() + scoreDescCreatedAt := scoreFields[6].Descriptor() // score.DefaultCreatedAt holds the default value on creation for the created_at field. score.DefaultCreatedAt = scoreDescCreatedAt.Default.(func() time.Time) // scoreDescID is the schema descriptor for id field. diff --git a/typing-server/domain/repository/ent/schema/score.go b/typing-server/domain/repository/ent/schema/score.go index fd29131..9fd0671 100644 --- a/typing-server/domain/repository/ent/schema/score.go +++ b/typing-server/domain/repository/ent/schema/score.go @@ -17,8 +17,11 @@ func (Score) Fields() []ent.Field { //カラムとしてint型のkeystrokes,float型のaccuracy,float型のscore,datetime型のstartedAt,datetime型のendedAtを持たせる return []ent.Field{ field.UUID("id", uuid.UUID{}).Default(uuid.New).StorageKey("id").Unique(), + field.UUID("user_id", uuid.UUID{}).StorageKey("user_id").Unique(), field.Int("keystrokes"), field.Float("accuracy"), + field.Bool("is_max_keystrokes").Optional().Comment("条件を満たす結果のうち、Userのkeystrokesが最大のもの"), + field.Bool("is_max_accuracy").Optional().Comment("条件を満たす結果のうち、Userのaccuracyが最大のもの"), field.Time("created_at").Immutable().Default(time.Now)} } @@ -29,6 +32,7 @@ func (Score) Edges() []ent.Edge { edge.From("user", User.Type). Ref("scores"). Unique(). + Field("user_id"). // ここでリンクするカラムを指定 Required(), } } diff --git a/typing-server/domain/repository/ent/score.go b/typing-server/domain/repository/ent/score.go index f3f9eed..e8bda7d 100644 --- a/typing-server/domain/repository/ent/score.go +++ b/typing-server/domain/repository/ent/score.go @@ -19,16 +19,21 @@ type Score struct { config `json:"-"` // ID of the ent. ID uuid.UUID `json:"id,omitempty"` + // UserID holds the value of the "user_id" field. + UserID uuid.UUID `json:"user_id,omitempty"` // Keystrokes holds the value of the "keystrokes" field. Keystrokes int `json:"keystrokes,omitempty"` // Accuracy holds the value of the "accuracy" field. Accuracy float64 `json:"accuracy,omitempty"` + // 条件を満たす結果のうち、Userのkeystrokesが最大のもの + IsMaxKeystrokes bool `json:"is_max_keystrokes,omitempty"` + // 条件を満たす結果のうち、Userのaccuracyが最大のもの + IsMaxAccuracy bool `json:"is_max_accuracy,omitempty"` // CreatedAt holds the value of the "created_at" field. CreatedAt time.Time `json:"created_at,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the ScoreQuery when eager-loading is set. Edges ScoreEdges `json:"edges"` - user_scores *uuid.UUID selectValues sql.SelectValues } @@ -57,16 +62,16 @@ func (*Score) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { + case score.FieldIsMaxKeystrokes, score.FieldIsMaxAccuracy: + values[i] = new(sql.NullBool) case score.FieldAccuracy: values[i] = new(sql.NullFloat64) case score.FieldKeystrokes: values[i] = new(sql.NullInt64) case score.FieldCreatedAt: values[i] = new(sql.NullTime) - case score.FieldID: + case score.FieldID, score.FieldUserID: values[i] = new(uuid.UUID) - case score.ForeignKeys[0]: // user_scores - values[i] = &sql.NullScanner{S: new(uuid.UUID)} default: values[i] = new(sql.UnknownType) } @@ -88,6 +93,12 @@ func (s *Score) assignValues(columns []string, values []any) error { } else if value != nil { s.ID = *value } + case score.FieldUserID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field user_id", values[i]) + } else if value != nil { + s.UserID = *value + } case score.FieldKeystrokes: if value, ok := values[i].(*sql.NullInt64); !ok { return fmt.Errorf("unexpected type %T for field keystrokes", values[i]) @@ -100,19 +111,24 @@ func (s *Score) assignValues(columns []string, values []any) error { } else if value.Valid { s.Accuracy = value.Float64 } + case score.FieldIsMaxKeystrokes: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field is_max_keystrokes", values[i]) + } else if value.Valid { + s.IsMaxKeystrokes = value.Bool + } + case score.FieldIsMaxAccuracy: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field is_max_accuracy", values[i]) + } else if value.Valid { + s.IsMaxAccuracy = value.Bool + } case score.FieldCreatedAt: if value, ok := values[i].(*sql.NullTime); !ok { return fmt.Errorf("unexpected type %T for field created_at", values[i]) } else if value.Valid { s.CreatedAt = value.Time } - case score.ForeignKeys[0]: - if value, ok := values[i].(*sql.NullScanner); !ok { - return fmt.Errorf("unexpected type %T for field user_scores", values[i]) - } else if value.Valid { - s.user_scores = new(uuid.UUID) - *s.user_scores = *value.S.(*uuid.UUID) - } default: s.selectValues.Set(columns[i], values[i]) } @@ -154,12 +170,21 @@ func (s *Score) String() string { var builder strings.Builder builder.WriteString("Score(") builder.WriteString(fmt.Sprintf("id=%v, ", s.ID)) + builder.WriteString("user_id=") + builder.WriteString(fmt.Sprintf("%v", s.UserID)) + builder.WriteString(", ") builder.WriteString("keystrokes=") builder.WriteString(fmt.Sprintf("%v", s.Keystrokes)) builder.WriteString(", ") builder.WriteString("accuracy=") builder.WriteString(fmt.Sprintf("%v", s.Accuracy)) builder.WriteString(", ") + builder.WriteString("is_max_keystrokes=") + builder.WriteString(fmt.Sprintf("%v", s.IsMaxKeystrokes)) + builder.WriteString(", ") + builder.WriteString("is_max_accuracy=") + builder.WriteString(fmt.Sprintf("%v", s.IsMaxAccuracy)) + builder.WriteString(", ") builder.WriteString("created_at=") builder.WriteString(s.CreatedAt.Format(time.ANSIC)) builder.WriteByte(')') diff --git a/typing-server/domain/repository/ent/score/score.go b/typing-server/domain/repository/ent/score/score.go index b0182c8..ae516f1 100644 --- a/typing-server/domain/repository/ent/score/score.go +++ b/typing-server/domain/repository/ent/score/score.go @@ -15,10 +15,16 @@ const ( Label = "score" // FieldID holds the string denoting the id field in the database. FieldID = "id" + // FieldUserID holds the string denoting the user_id field in the database. + FieldUserID = "user_id" // FieldKeystrokes holds the string denoting the keystrokes field in the database. FieldKeystrokes = "keystrokes" // FieldAccuracy holds the string denoting the accuracy field in the database. FieldAccuracy = "accuracy" + // FieldIsMaxKeystrokes holds the string denoting the is_max_keystrokes field in the database. + FieldIsMaxKeystrokes = "is_max_keystrokes" + // FieldIsMaxAccuracy holds the string denoting the is_max_accuracy field in the database. + FieldIsMaxAccuracy = "is_max_accuracy" // FieldCreatedAt holds the string denoting the created_at field in the database. FieldCreatedAt = "created_at" // EdgeUser holds the string denoting the user edge name in mutations. @@ -31,23 +37,20 @@ const ( // It exists in this package in order to avoid circular dependency with the "user" package. UserInverseTable = "users" // UserColumn is the table column denoting the user relation/edge. - UserColumn = "user_scores" + UserColumn = "user_id" ) // Columns holds all SQL columns for score fields. var Columns = []string{ FieldID, + FieldUserID, FieldKeystrokes, FieldAccuracy, + FieldIsMaxKeystrokes, + FieldIsMaxAccuracy, FieldCreatedAt, } -// ForeignKeys holds the SQL foreign-keys that are owned by the "scores" -// table and are not defined as standalone fields in the schema. -var ForeignKeys = []string{ - "user_scores", -} - // ValidColumn reports if the column name is valid (part of the table columns). func ValidColumn(column string) bool { for i := range Columns { @@ -55,11 +58,6 @@ func ValidColumn(column string) bool { return true } } - for i := range ForeignKeys { - if column == ForeignKeys[i] { - return true - } - } return false } @@ -78,6 +76,11 @@ func ByID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldID, opts...).ToFunc() } +// ByUserID orders the results by the user_id field. +func ByUserID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUserID, opts...).ToFunc() +} + // ByKeystrokes orders the results by the keystrokes field. func ByKeystrokes(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldKeystrokes, opts...).ToFunc() @@ -88,6 +91,16 @@ func ByAccuracy(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldAccuracy, opts...).ToFunc() } +// ByIsMaxKeystrokes orders the results by the is_max_keystrokes field. +func ByIsMaxKeystrokes(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldIsMaxKeystrokes, opts...).ToFunc() +} + +// ByIsMaxAccuracy orders the results by the is_max_accuracy field. +func ByIsMaxAccuracy(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldIsMaxAccuracy, opts...).ToFunc() +} + // ByCreatedAt orders the results by the created_at field. func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() diff --git a/typing-server/domain/repository/ent/score/where.go b/typing-server/domain/repository/ent/score/where.go index 057f02c..cfb9cf1 100644 --- a/typing-server/domain/repository/ent/score/where.go +++ b/typing-server/domain/repository/ent/score/where.go @@ -56,6 +56,11 @@ func IDLTE(id uuid.UUID) predicate.Score { return predicate.Score(sql.FieldLTE(FieldID, id)) } +// UserID applies equality check predicate on the "user_id" field. It's identical to UserIDEQ. +func UserID(v uuid.UUID) predicate.Score { + return predicate.Score(sql.FieldEQ(FieldUserID, v)) +} + // Keystrokes applies equality check predicate on the "keystrokes" field. It's identical to KeystrokesEQ. func Keystrokes(v int) predicate.Score { return predicate.Score(sql.FieldEQ(FieldKeystrokes, v)) @@ -66,11 +71,41 @@ func Accuracy(v float64) predicate.Score { return predicate.Score(sql.FieldEQ(FieldAccuracy, v)) } +// IsMaxKeystrokes applies equality check predicate on the "is_max_keystrokes" field. It's identical to IsMaxKeystrokesEQ. +func IsMaxKeystrokes(v bool) predicate.Score { + return predicate.Score(sql.FieldEQ(FieldIsMaxKeystrokes, v)) +} + +// IsMaxAccuracy applies equality check predicate on the "is_max_accuracy" field. It's identical to IsMaxAccuracyEQ. +func IsMaxAccuracy(v bool) predicate.Score { + return predicate.Score(sql.FieldEQ(FieldIsMaxAccuracy, v)) +} + // CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. func CreatedAt(v time.Time) predicate.Score { return predicate.Score(sql.FieldEQ(FieldCreatedAt, v)) } +// UserIDEQ applies the EQ predicate on the "user_id" field. +func UserIDEQ(v uuid.UUID) predicate.Score { + return predicate.Score(sql.FieldEQ(FieldUserID, v)) +} + +// UserIDNEQ applies the NEQ predicate on the "user_id" field. +func UserIDNEQ(v uuid.UUID) predicate.Score { + return predicate.Score(sql.FieldNEQ(FieldUserID, v)) +} + +// UserIDIn applies the In predicate on the "user_id" field. +func UserIDIn(vs ...uuid.UUID) predicate.Score { + return predicate.Score(sql.FieldIn(FieldUserID, vs...)) +} + +// UserIDNotIn applies the NotIn predicate on the "user_id" field. +func UserIDNotIn(vs ...uuid.UUID) predicate.Score { + return predicate.Score(sql.FieldNotIn(FieldUserID, vs...)) +} + // KeystrokesEQ applies the EQ predicate on the "keystrokes" field. func KeystrokesEQ(v int) predicate.Score { return predicate.Score(sql.FieldEQ(FieldKeystrokes, v)) @@ -151,6 +186,46 @@ func AccuracyLTE(v float64) predicate.Score { return predicate.Score(sql.FieldLTE(FieldAccuracy, v)) } +// IsMaxKeystrokesEQ applies the EQ predicate on the "is_max_keystrokes" field. +func IsMaxKeystrokesEQ(v bool) predicate.Score { + return predicate.Score(sql.FieldEQ(FieldIsMaxKeystrokes, v)) +} + +// IsMaxKeystrokesNEQ applies the NEQ predicate on the "is_max_keystrokes" field. +func IsMaxKeystrokesNEQ(v bool) predicate.Score { + return predicate.Score(sql.FieldNEQ(FieldIsMaxKeystrokes, v)) +} + +// IsMaxKeystrokesIsNil applies the IsNil predicate on the "is_max_keystrokes" field. +func IsMaxKeystrokesIsNil() predicate.Score { + return predicate.Score(sql.FieldIsNull(FieldIsMaxKeystrokes)) +} + +// IsMaxKeystrokesNotNil applies the NotNil predicate on the "is_max_keystrokes" field. +func IsMaxKeystrokesNotNil() predicate.Score { + return predicate.Score(sql.FieldNotNull(FieldIsMaxKeystrokes)) +} + +// IsMaxAccuracyEQ applies the EQ predicate on the "is_max_accuracy" field. +func IsMaxAccuracyEQ(v bool) predicate.Score { + return predicate.Score(sql.FieldEQ(FieldIsMaxAccuracy, v)) +} + +// IsMaxAccuracyNEQ applies the NEQ predicate on the "is_max_accuracy" field. +func IsMaxAccuracyNEQ(v bool) predicate.Score { + return predicate.Score(sql.FieldNEQ(FieldIsMaxAccuracy, v)) +} + +// IsMaxAccuracyIsNil applies the IsNil predicate on the "is_max_accuracy" field. +func IsMaxAccuracyIsNil() predicate.Score { + return predicate.Score(sql.FieldIsNull(FieldIsMaxAccuracy)) +} + +// IsMaxAccuracyNotNil applies the NotNil predicate on the "is_max_accuracy" field. +func IsMaxAccuracyNotNil() predicate.Score { + return predicate.Score(sql.FieldNotNull(FieldIsMaxAccuracy)) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v time.Time) predicate.Score { return predicate.Score(sql.FieldEQ(FieldCreatedAt, v)) diff --git a/typing-server/domain/repository/ent/score_create.go b/typing-server/domain/repository/ent/score_create.go index f4963e2..cac89fa 100644 --- a/typing-server/domain/repository/ent/score_create.go +++ b/typing-server/domain/repository/ent/score_create.go @@ -22,6 +22,12 @@ type ScoreCreate struct { hooks []Hook } +// SetUserID sets the "user_id" field. +func (sc *ScoreCreate) SetUserID(u uuid.UUID) *ScoreCreate { + sc.mutation.SetUserID(u) + return sc +} + // SetKeystrokes sets the "keystrokes" field. func (sc *ScoreCreate) SetKeystrokes(i int) *ScoreCreate { sc.mutation.SetKeystrokes(i) @@ -34,6 +40,34 @@ func (sc *ScoreCreate) SetAccuracy(f float64) *ScoreCreate { return sc } +// SetIsMaxKeystrokes sets the "is_max_keystrokes" field. +func (sc *ScoreCreate) SetIsMaxKeystrokes(b bool) *ScoreCreate { + sc.mutation.SetIsMaxKeystrokes(b) + return sc +} + +// SetNillableIsMaxKeystrokes sets the "is_max_keystrokes" field if the given value is not nil. +func (sc *ScoreCreate) SetNillableIsMaxKeystrokes(b *bool) *ScoreCreate { + if b != nil { + sc.SetIsMaxKeystrokes(*b) + } + return sc +} + +// SetIsMaxAccuracy sets the "is_max_accuracy" field. +func (sc *ScoreCreate) SetIsMaxAccuracy(b bool) *ScoreCreate { + sc.mutation.SetIsMaxAccuracy(b) + return sc +} + +// SetNillableIsMaxAccuracy sets the "is_max_accuracy" field if the given value is not nil. +func (sc *ScoreCreate) SetNillableIsMaxAccuracy(b *bool) *ScoreCreate { + if b != nil { + sc.SetIsMaxAccuracy(*b) + } + return sc +} + // SetCreatedAt sets the "created_at" field. func (sc *ScoreCreate) SetCreatedAt(t time.Time) *ScoreCreate { sc.mutation.SetCreatedAt(t) @@ -62,12 +96,6 @@ func (sc *ScoreCreate) SetNillableID(u *uuid.UUID) *ScoreCreate { return sc } -// SetUserID sets the "user" edge to the User entity by ID. -func (sc *ScoreCreate) SetUserID(id uuid.UUID) *ScoreCreate { - sc.mutation.SetUserID(id) - return sc -} - // SetUser sets the "user" edge to the User entity. func (sc *ScoreCreate) SetUser(u *User) *ScoreCreate { return sc.SetUserID(u.ID) @@ -120,6 +148,9 @@ func (sc *ScoreCreate) defaults() { // check runs all checks and user-defined validators on the builder. func (sc *ScoreCreate) check() error { + if _, ok := sc.mutation.UserID(); !ok { + return &ValidationError{Name: "user_id", err: errors.New(`ent: missing required field "Score.user_id"`)} + } if _, ok := sc.mutation.Keystrokes(); !ok { return &ValidationError{Name: "keystrokes", err: errors.New(`ent: missing required field "Score.keystrokes"`)} } @@ -175,6 +206,14 @@ func (sc *ScoreCreate) createSpec() (*Score, *sqlgraph.CreateSpec) { _spec.SetField(score.FieldAccuracy, field.TypeFloat64, value) _node.Accuracy = value } + if value, ok := sc.mutation.IsMaxKeystrokes(); ok { + _spec.SetField(score.FieldIsMaxKeystrokes, field.TypeBool, value) + _node.IsMaxKeystrokes = value + } + if value, ok := sc.mutation.IsMaxAccuracy(); ok { + _spec.SetField(score.FieldIsMaxAccuracy, field.TypeBool, value) + _node.IsMaxAccuracy = value + } if value, ok := sc.mutation.CreatedAt(); ok { _spec.SetField(score.FieldCreatedAt, field.TypeTime, value) _node.CreatedAt = value @@ -193,7 +232,7 @@ func (sc *ScoreCreate) createSpec() (*Score, *sqlgraph.CreateSpec) { for _, k := range nodes { edge.Target.Nodes = append(edge.Target.Nodes, k) } - _node.user_scores = &nodes[0] + _node.UserID = nodes[0] _spec.Edges = append(_spec.Edges, edge) } return _node, _spec diff --git a/typing-server/domain/repository/ent/score_query.go b/typing-server/domain/repository/ent/score_query.go index b1dcf1b..84ad5b8 100644 --- a/typing-server/domain/repository/ent/score_query.go +++ b/typing-server/domain/repository/ent/score_query.go @@ -24,7 +24,6 @@ type ScoreQuery struct { inters []Interceptor predicates []predicate.Score withUser *UserQuery - withFKs bool // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -299,12 +298,12 @@ func (sq *ScoreQuery) WithUser(opts ...func(*UserQuery)) *ScoreQuery { // Example: // // var v []struct { -// Keystrokes int `json:"keystrokes,omitempty"` +// UserID uuid.UUID `json:"user_id,omitempty"` // Count int `json:"count,omitempty"` // } // // client.Score.Query(). -// GroupBy(score.FieldKeystrokes). +// GroupBy(score.FieldUserID). // Aggregate(ent.Count()). // Scan(ctx, &v) func (sq *ScoreQuery) GroupBy(field string, fields ...string) *ScoreGroupBy { @@ -322,11 +321,11 @@ func (sq *ScoreQuery) GroupBy(field string, fields ...string) *ScoreGroupBy { // Example: // // var v []struct { -// Keystrokes int `json:"keystrokes,omitempty"` +// UserID uuid.UUID `json:"user_id,omitempty"` // } // // client.Score.Query(). -// Select(score.FieldKeystrokes). +// Select(score.FieldUserID). // Scan(ctx, &v) func (sq *ScoreQuery) Select(fields ...string) *ScoreSelect { sq.ctx.Fields = append(sq.ctx.Fields, fields...) @@ -370,18 +369,11 @@ func (sq *ScoreQuery) prepareQuery(ctx context.Context) error { func (sq *ScoreQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Score, error) { var ( nodes = []*Score{} - withFKs = sq.withFKs _spec = sq.querySpec() loadedTypes = [1]bool{ sq.withUser != nil, } ) - if sq.withUser != nil { - withFKs = true - } - if withFKs { - _spec.Node.Columns = append(_spec.Node.Columns, score.ForeignKeys...) - } _spec.ScanValues = func(columns []string) ([]any, error) { return (*Score).scanValues(nil, columns) } @@ -413,10 +405,7 @@ func (sq *ScoreQuery) loadUser(ctx context.Context, query *UserQuery, nodes []*S ids := make([]uuid.UUID, 0, len(nodes)) nodeids := make(map[uuid.UUID][]*Score) for i := range nodes { - if nodes[i].user_scores == nil { - continue - } - fk := *nodes[i].user_scores + fk := nodes[i].UserID if _, ok := nodeids[fk]; !ok { ids = append(ids, fk) } @@ -433,7 +422,7 @@ func (sq *ScoreQuery) loadUser(ctx context.Context, query *UserQuery, nodes []*S for _, n := range neighbors { nodes, ok := nodeids[n.ID] if !ok { - return fmt.Errorf(`unexpected foreign-key "user_scores" returned %v`, n.ID) + return fmt.Errorf(`unexpected foreign-key "user_id" returned %v`, n.ID) } for i := range nodes { assign(nodes[i], n) @@ -467,6 +456,9 @@ func (sq *ScoreQuery) querySpec() *sqlgraph.QuerySpec { _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) } } + if sq.withUser != nil { + _spec.Node.AddColumnOnce(score.FieldUserID) + } } if ps := sq.predicates; len(ps) > 0 { _spec.Predicate = func(selector *sql.Selector) { diff --git a/typing-server/domain/repository/ent/score_update.go b/typing-server/domain/repository/ent/score_update.go index b58ca66..689c555 100644 --- a/typing-server/domain/repository/ent/score_update.go +++ b/typing-server/domain/repository/ent/score_update.go @@ -29,6 +29,20 @@ func (su *ScoreUpdate) Where(ps ...predicate.Score) *ScoreUpdate { return su } +// SetUserID sets the "user_id" field. +func (su *ScoreUpdate) SetUserID(u uuid.UUID) *ScoreUpdate { + su.mutation.SetUserID(u) + return su +} + +// SetNillableUserID sets the "user_id" field if the given value is not nil. +func (su *ScoreUpdate) SetNillableUserID(u *uuid.UUID) *ScoreUpdate { + if u != nil { + su.SetUserID(*u) + } + return su +} + // SetKeystrokes sets the "keystrokes" field. func (su *ScoreUpdate) SetKeystrokes(i int) *ScoreUpdate { su.mutation.ResetKeystrokes() @@ -71,9 +85,43 @@ func (su *ScoreUpdate) AddAccuracy(f float64) *ScoreUpdate { return su } -// SetUserID sets the "user" edge to the User entity by ID. -func (su *ScoreUpdate) SetUserID(id uuid.UUID) *ScoreUpdate { - su.mutation.SetUserID(id) +// SetIsMaxKeystrokes sets the "is_max_keystrokes" field. +func (su *ScoreUpdate) SetIsMaxKeystrokes(b bool) *ScoreUpdate { + su.mutation.SetIsMaxKeystrokes(b) + return su +} + +// SetNillableIsMaxKeystrokes sets the "is_max_keystrokes" field if the given value is not nil. +func (su *ScoreUpdate) SetNillableIsMaxKeystrokes(b *bool) *ScoreUpdate { + if b != nil { + su.SetIsMaxKeystrokes(*b) + } + return su +} + +// ClearIsMaxKeystrokes clears the value of the "is_max_keystrokes" field. +func (su *ScoreUpdate) ClearIsMaxKeystrokes() *ScoreUpdate { + su.mutation.ClearIsMaxKeystrokes() + return su +} + +// SetIsMaxAccuracy sets the "is_max_accuracy" field. +func (su *ScoreUpdate) SetIsMaxAccuracy(b bool) *ScoreUpdate { + su.mutation.SetIsMaxAccuracy(b) + return su +} + +// SetNillableIsMaxAccuracy sets the "is_max_accuracy" field if the given value is not nil. +func (su *ScoreUpdate) SetNillableIsMaxAccuracy(b *bool) *ScoreUpdate { + if b != nil { + su.SetIsMaxAccuracy(*b) + } + return su +} + +// ClearIsMaxAccuracy clears the value of the "is_max_accuracy" field. +func (su *ScoreUpdate) ClearIsMaxAccuracy() *ScoreUpdate { + su.mutation.ClearIsMaxAccuracy() return su } @@ -152,6 +200,18 @@ func (su *ScoreUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := su.mutation.AddedAccuracy(); ok { _spec.AddField(score.FieldAccuracy, field.TypeFloat64, value) } + if value, ok := su.mutation.IsMaxKeystrokes(); ok { + _spec.SetField(score.FieldIsMaxKeystrokes, field.TypeBool, value) + } + if su.mutation.IsMaxKeystrokesCleared() { + _spec.ClearField(score.FieldIsMaxKeystrokes, field.TypeBool) + } + if value, ok := su.mutation.IsMaxAccuracy(); ok { + _spec.SetField(score.FieldIsMaxAccuracy, field.TypeBool, value) + } + if su.mutation.IsMaxAccuracyCleared() { + _spec.ClearField(score.FieldIsMaxAccuracy, field.TypeBool) + } if su.mutation.UserCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -201,6 +261,20 @@ type ScoreUpdateOne struct { mutation *ScoreMutation } +// SetUserID sets the "user_id" field. +func (suo *ScoreUpdateOne) SetUserID(u uuid.UUID) *ScoreUpdateOne { + suo.mutation.SetUserID(u) + return suo +} + +// SetNillableUserID sets the "user_id" field if the given value is not nil. +func (suo *ScoreUpdateOne) SetNillableUserID(u *uuid.UUID) *ScoreUpdateOne { + if u != nil { + suo.SetUserID(*u) + } + return suo +} + // SetKeystrokes sets the "keystrokes" field. func (suo *ScoreUpdateOne) SetKeystrokes(i int) *ScoreUpdateOne { suo.mutation.ResetKeystrokes() @@ -243,9 +317,43 @@ func (suo *ScoreUpdateOne) AddAccuracy(f float64) *ScoreUpdateOne { return suo } -// SetUserID sets the "user" edge to the User entity by ID. -func (suo *ScoreUpdateOne) SetUserID(id uuid.UUID) *ScoreUpdateOne { - suo.mutation.SetUserID(id) +// SetIsMaxKeystrokes sets the "is_max_keystrokes" field. +func (suo *ScoreUpdateOne) SetIsMaxKeystrokes(b bool) *ScoreUpdateOne { + suo.mutation.SetIsMaxKeystrokes(b) + return suo +} + +// SetNillableIsMaxKeystrokes sets the "is_max_keystrokes" field if the given value is not nil. +func (suo *ScoreUpdateOne) SetNillableIsMaxKeystrokes(b *bool) *ScoreUpdateOne { + if b != nil { + suo.SetIsMaxKeystrokes(*b) + } + return suo +} + +// ClearIsMaxKeystrokes clears the value of the "is_max_keystrokes" field. +func (suo *ScoreUpdateOne) ClearIsMaxKeystrokes() *ScoreUpdateOne { + suo.mutation.ClearIsMaxKeystrokes() + return suo +} + +// SetIsMaxAccuracy sets the "is_max_accuracy" field. +func (suo *ScoreUpdateOne) SetIsMaxAccuracy(b bool) *ScoreUpdateOne { + suo.mutation.SetIsMaxAccuracy(b) + return suo +} + +// SetNillableIsMaxAccuracy sets the "is_max_accuracy" field if the given value is not nil. +func (suo *ScoreUpdateOne) SetNillableIsMaxAccuracy(b *bool) *ScoreUpdateOne { + if b != nil { + suo.SetIsMaxAccuracy(*b) + } + return suo +} + +// ClearIsMaxAccuracy clears the value of the "is_max_accuracy" field. +func (suo *ScoreUpdateOne) ClearIsMaxAccuracy() *ScoreUpdateOne { + suo.mutation.ClearIsMaxAccuracy() return suo } @@ -354,6 +462,18 @@ func (suo *ScoreUpdateOne) sqlSave(ctx context.Context) (_node *Score, err error if value, ok := suo.mutation.AddedAccuracy(); ok { _spec.AddField(score.FieldAccuracy, field.TypeFloat64, value) } + if value, ok := suo.mutation.IsMaxKeystrokes(); ok { + _spec.SetField(score.FieldIsMaxKeystrokes, field.TypeBool, value) + } + if suo.mutation.IsMaxKeystrokesCleared() { + _spec.ClearField(score.FieldIsMaxKeystrokes, field.TypeBool) + } + if value, ok := suo.mutation.IsMaxAccuracy(); ok { + _spec.SetField(score.FieldIsMaxAccuracy, field.TypeBool, value) + } + if suo.mutation.IsMaxAccuracyCleared() { + _spec.ClearField(score.FieldIsMaxAccuracy, field.TypeBool) + } if suo.mutation.UserCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, diff --git a/typing-server/domain/repository/ent/user/user.go b/typing-server/domain/repository/ent/user/user.go index 46991e3..1779122 100644 --- a/typing-server/domain/repository/ent/user/user.go +++ b/typing-server/domain/repository/ent/user/user.go @@ -33,7 +33,7 @@ const ( // It exists in this package in order to avoid circular dependency with the "score" package. ScoresInverseTable = "scores" // ScoresColumn is the table column denoting the scores relation/edge. - ScoresColumn = "user_scores" + ScoresColumn = "user_id" ) // Columns holds all SQL columns for user fields. diff --git a/typing-server/domain/repository/ent/user_query.go b/typing-server/domain/repository/ent/user_query.go index a5b89b7..73160fd 100644 --- a/typing-server/domain/repository/ent/user_query.go +++ b/typing-server/domain/repository/ent/user_query.go @@ -413,7 +413,9 @@ func (uq *UserQuery) loadScores(ctx context.Context, query *ScoreQuery, nodes [] init(nodes[i]) } } - query.withFKs = true + if len(query.ctx.Fields) > 0 { + query.ctx.AppendFieldOnce(score.FieldUserID) + } query.Where(predicate.Score(func(s *sql.Selector) { s.Where(sql.InValues(s.C(user.ScoresColumn), fks...)) })) @@ -422,13 +424,10 @@ func (uq *UserQuery) loadScores(ctx context.Context, query *ScoreQuery, nodes [] return err } for _, n := range neighbors { - fk := n.user_scores - if fk == nil { - return fmt.Errorf(`foreign-key "user_scores" is nil for node %v`, n.ID) - } - node, ok := nodeids[*fk] + fk := n.UserID + node, ok := nodeids[fk] if !ok { - return fmt.Errorf(`unexpected referenced foreign-key "user_scores" returned %v for node %v`, *fk, n.ID) + return fmt.Errorf(`unexpected referenced foreign-key "user_id" returned %v for node %v`, fk, n.ID) } assign(node, n) } From 9b7fd691b3a2d2f089214c029744c11582dd2ad8 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Wed, 27 Mar 2024 19:37:20 +0900 Subject: [PATCH 22/26] =?UTF-8?q?feat:=20=E4=BC=B4=E3=81=84seed=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E3=83=A9=E3=83=A0=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/cmd/main.go | 59 ++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/typing-server/api/cmd/main.go b/typing-server/api/cmd/main.go index a34062f..5826503 100644 --- a/typing-server/api/cmd/main.go +++ b/typing-server/api/cmd/main.go @@ -68,7 +68,7 @@ func main() { // シードデータの挿入 if *seedFlag { - if err := seedData(context.Background(), entClient); err != nil { + if err := seedData(context.Background(), entClient, logger); err != nil { logger.Error("failed to seed data: %v", err) return } @@ -125,42 +125,77 @@ func main() { logger.Info("server exited") } -// TODO: 本番環境では削除する -func seedData(ctx context.Context, client *ent.Client) error { +func seedData(ctx context.Context, client *ent.Client, logger *slog.Logger) error { // シードデータの作成 for i := 0; i < 10; i++ { - isAlreadySeeded, err := client.User.Query().Where(user.StudentNumber(fmt.Sprintf("user%d", i+1))).Exist(ctx) + studentNumber := fmt.Sprintf("user%d", i+1) + handleName := fmt.Sprintf("handle%d", i+1) + + isAlreadySeeded, err := client.User.Query().Where(user.StudentNumber(studentNumber)).Exist(ctx) if err != nil { return err } if isAlreadySeeded { + logger.Info("User with student number already seeded, skipping", slog.String("studentNumber", studentNumber)) continue } u, err := client.User.Create(). - SetStudentNumber(fmt.Sprintf("user%d", i+1)). - SetHandleName(fmt.Sprintf("handle%d", i+1)). + SetStudentNumber(studentNumber). + SetHandleName(handleName). Save(ctx) if err != nil { panic(err) } + logger.Info("Created user", slog.String("studentNumber", studentNumber), slog.String("handleName", handleName)) + + var maxKeystrokesScore, maxAccuracyScore *ent.Score + for j := 0; j < 5; j++ { - score, err := client.Score.Create(). - SetKeystrokes(rand.Intn(200)). - SetAccuracy(rand.Float64()). + keystrokes := rand.Intn(200) + 100 + accuracy := rand.Float64() + + s, err := client.Score.Create(). + SetKeystrokes(keystrokes). + SetAccuracy(accuracy). SetCreatedAt(time.Now()). + SetUser(u). Save(ctx) if err != nil { panic(err) } - _, err = client.User.UpdateOne(u). - AddScores(score).Save(ctx) + logger.Info("Created score", slog.Int("keystrokes", keystrokes), slog.Float64("accuracy", accuracy), slog.String("studentNumber", studentNumber)) + + if s.Keystrokes < 120 || s.Accuracy < 0.95 { + continue + } + + if maxKeystrokesScore == nil || s.Keystrokes > maxKeystrokesScore.Keystrokes { + maxKeystrokesScore = s + } + if maxAccuracyScore == nil || s.Accuracy > maxAccuracyScore.Accuracy { + maxAccuracyScore = s + } + } + + // 最大のKeystrokesスコアと最大のAccuracyスコアのフラグを設定 + if maxKeystrokesScore != nil { + err = maxKeystrokesScore.Update().SetIsMaxKeystrokes(true).Exec(ctx) if err != nil { - panic(err) + return err } + logger.Info("Set is_max_keystrokes flag", slog.Int("keystrokes", maxKeystrokesScore.Keystrokes), slog.String("studentNumber", studentNumber)) + } + if maxAccuracyScore != nil { + err = maxAccuracyScore.Update().SetIsMaxAccuracy(true).Exec(ctx) + if err != nil { + return err + } + logger.Info("Set is_max_accuracy flag", slog.Float64("accuracy", maxAccuracyScore.Accuracy), slog.String("studentNumber", studentNumber)) } } + return nil } From 381c6f8da0ba3e564c08f14f889019cde5234f81 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Wed, 27 Mar 2024 19:37:31 +0900 Subject: [PATCH 23/26] =?UTF-8?q?fix:=20=E3=82=B9=E3=82=B3=E3=82=A2?= =?UTF-8?q?=E5=91=A8=E3=82=8A=E3=81=AEAPI=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/repository/score.go | 90 +++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/typing-server/api/repository/score.go b/typing-server/api/repository/score.go index 17e5dee..aa6ff5a 100644 --- a/typing-server/api/repository/score.go +++ b/typing-server/api/repository/score.go @@ -8,25 +8,38 @@ import ( "github.com/su-its/typing/typing-server/domain/model" "github.com/su-its/typing/typing-server/domain/repository/ent" "github.com/su-its/typing/typing-server/domain/repository/ent/score" + "github.com/su-its/typing/typing-server/domain/repository/ent/user" ) func GetScoresRanking(ctx context.Context, client *ent.Client, sortBy string, start, limit int) ([]*model.ScoreRanking, error) { - // ! このクエリは要チェック - scores, err := client.Score.Query(). + var scores []*ent.Score + + // entのクエリを使用してスコアを取得 + query := client.Score.Query(). WithUser(). Where( score.And( score.KeystrokesGTE(120), - score.AccuracyGTE(95.0), + score.AccuracyGTE(0.95), ), ). - Order(ent.Desc(sortBy)). + Order(ent.Desc(sortBy)) + + switch sortBy { + case "accuracy": + query = query.Where(score.IsMaxAccuracy(true)) + case "keystrokes": + query = query.Where(score.IsMaxKeystrokes(true)) + default: + return nil, fmt.Errorf("invalid sort by parameter: %s", sortBy) + } + + // フラグでフィルタリングされたスコアを取得 + scores, err := query. Limit(limit). - Offset(start-1). - Select("user_id"). - Unique(true). - Select("id", "keystrokes", "accuracy", "created_at", "user_id"). + Offset(start - 1). All(ctx) + if err != nil { return nil, err } @@ -46,6 +59,7 @@ func GetScoresRanking(ctx context.Context, client *ent.Client, sortBy string, st score := &model.Score{ ID: s.ID.String(), + UserID: s.UserID.String(), Keystrokes: s.Keystrokes, Accuracy: s.Accuracy, CreatedAt: s.CreatedAt, @@ -58,13 +72,12 @@ func GetScoresRanking(ctx context.Context, client *ent.Client, sortBy string, st currentScore = s.Accuracy case "keystrokes": currentScore = float64(s.Keystrokes) - default: - return nil, fmt.Errorf("invalid sort by parameter: %s", sortBy) } if i == 0 || currentScore != prevScore { rank = start + i } + prevScore = currentScore ranking := &model.ScoreRanking{ @@ -79,11 +92,62 @@ func GetScoresRanking(ctx context.Context, client *ent.Client, sortBy string, st } func CreateScore(ctx context.Context, client *ent.Client, userID uuid.UUID, keystrokes int, accuracy float64) error { - _, err := client.Score.Create(). + // Create a new score + createdScore, err := client.Score.Create(). SetKeystrokes(keystrokes). - SetAccuracy(float64(keystrokes)). + SetAccuracy(accuracy). SetUserID(userID). Save(ctx) + if err != nil { + return err + } + + if keystrokes < 120 || accuracy < 0.95 { + return nil + } + + // Get the user + user, err := client.User.Query().Where(user.ID(userID)).Only(ctx) + if err != nil { + return err + } + + // Check if the new score has the maximum keystrokes + maxKeystrokeScore, err := user.QueryScores().Where(score.IsMaxKeystrokes(true)).Only(ctx) + if err != nil && !ent.IsNotFound(err) { + return err + } + isMaxKeystrokes := maxKeystrokeScore == nil || createdScore.Keystrokes >= maxKeystrokeScore.Keystrokes + + // Check if the new score has the maximum accuracy + maxAccuracyScore, err := user.QueryScores().Where(score.IsMaxAccuracy(true)).Only(ctx) + if err != nil && !ent.IsNotFound(err) { + return err + } + isMaxAccuracy := maxAccuracyScore == nil || createdScore.Accuracy >= maxAccuracyScore.Accuracy + + // Update the flags of the previous maximum scores + if maxKeystrokeScore != nil && !isMaxKeystrokes { + err = maxKeystrokeScore.Update().SetIsMaxKeystrokes(false).Exec(ctx) + if err != nil { + return err + } + } + if maxAccuracyScore != nil && !isMaxAccuracy { + err = maxAccuracyScore.Update().SetIsMaxAccuracy(false).Exec(ctx) + if err != nil { + return err + } + } + + // Update the score with the maximum flags + err = createdScore.Update(). + SetIsMaxKeystrokes(isMaxKeystrokes). + SetIsMaxAccuracy(isMaxAccuracy). + Exec(ctx) + if err != nil { + return err + } - return err + return nil } From 2ae39723b9ffb56dfc4c053e444cf42ee7a659e8 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Wed, 27 Mar 2024 19:38:07 +0900 Subject: [PATCH 24/26] =?UTF-8?q?feat:=20User=E5=8F=96=E5=BE=97=E3=81=8C?= =?UTF-8?q?=E3=81=9D=E3=81=AE=E3=83=A6=E3=83=BC=E3=82=B6=E3=81=AE=E3=81=99?= =?UTF-8?q?=E3=81=B9=E3=81=A6=E3=81=AE=E3=82=B9=E3=82=B3=E3=82=A2=E3=82=92?= =?UTF-8?q?=E8=BF=94=E3=81=99=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/api/repository/user.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/typing-server/api/repository/user.go b/typing-server/api/repository/user.go index 8629f46..9209796 100644 --- a/typing-server/api/repository/user.go +++ b/typing-server/api/repository/user.go @@ -9,6 +9,7 @@ import ( func GetUserByStudentNumber(ctx context.Context, client *ent.Client, studentNumber string) (*ent.User, error) { entUser, err := client.User.Query(). + WithScores(). Where(user.StudentNumberEQ(studentNumber)). Only(ctx) if err != nil { @@ -19,5 +20,8 @@ func GetUserByStudentNumber(ctx context.Context, client *ent.Client, studentNumb ID: entUser.ID, StudentNumber: entUser.StudentNumber, HandleName: entUser.HandleName, + CreatedAt: entUser.CreatedAt, + UpdatedAt: entUser.UpdatedAt, + Edges: entUser.Edges, }, nil } From fc1992b76a8c2c4a2fef54829a190e09352d7cb4 Mon Sep 17 00:00:00 2001 From: h-takeyeah <61489178+h-takeyeah@users.noreply.github.com> Date: Wed, 27 Mar 2024 21:43:14 +0900 Subject: [PATCH 25/26] =?UTF-8?q?docs:=20tags=20=E3=81=A8=20operationId=20?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit プログラムで使えるコードを自動生成するときに機械的に命名されて 変なクラス名などになるのを防ぐ目的 --- typing-server/openapi.yaml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/typing-server/openapi.yaml b/typing-server/openapi.yaml index 6f155d5..22cb696 100644 --- a/typing-server/openapi.yaml +++ b/typing-server/openapi.yaml @@ -7,15 +7,28 @@ servers: description: ローカル開発環境 - url: https://typemaster.com description: 本番環境 +tags: + - name: other + description: いい名前が思いつかない(仮) + - name: user + description: ユーザー関係(仮) + - name: score + description: スコア関係(仮) paths: /health: - get: + get: + tags: + - other + operationId: healthcheck summary: サーバーの状態を取得 responses: "200": description: サーバーが正常に稼働しています。 /users: get: + tags: + - user + operationId: getUsers summary: ユーザー情報を取得 parameters: - in: query @@ -36,6 +49,9 @@ paths: /scores/ranking: get: + tags: + - score + operationId: getScoresRanking summary: スコアランキングを取得 parameters: - in: query @@ -68,6 +84,9 @@ paths: /scores: post: + tags: + - score + operationId: registerScore summary: スコアを登録 requestBody: required: true From 39a9744d58345dc6c90bae50389adda8af7e79a6 Mon Sep 17 00:00:00 2001 From: KinjiKawaguchi Date: Thu, 28 Mar 2024 08:42:44 +0900 Subject: [PATCH 26/26] =?UTF-8?q?openapi.yaml=E3=81=AEURL=E3=82=92?= =?UTF-8?q?=E4=BB=AE=E7=BD=AE=E3=81=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typing-server/openapi.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typing-server/openapi.yaml b/typing-server/openapi.yaml index 22cb696..5e1288c 100644 --- a/typing-server/openapi.yaml +++ b/typing-server/openapi.yaml @@ -5,7 +5,7 @@ info: servers: - url: http://localhost:8080 description: ローカル開発環境 - - url: https://typemaster.com + - url: https://example.com description: 本番環境 tags: - name: other @@ -16,7 +16,7 @@ tags: description: スコア関係(仮) paths: /health: - get: + get: tags: - other operationId: healthcheck