github.com/oinume/lekcije@v0.0.0-20231017100347-5b4c5eb6ab24/backend/infrastructure/mysql/following_teacher.go (about) 1 package mysql 2 3 import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/morikuni/failure" 11 "github.com/volatiletech/sqlboiler/v4/boil" 12 "github.com/volatiletech/sqlboiler/v4/queries" 13 "github.com/volatiletech/sqlboiler/v4/queries/qm" 14 "go.opentelemetry.io/otel" 15 "go.opentelemetry.io/otel/attribute" 16 17 "github.com/oinume/lekcije/backend/domain/config" 18 "github.com/oinume/lekcije/backend/domain/repository" 19 "github.com/oinume/lekcije/backend/errors" 20 "github.com/oinume/lekcije/backend/model2" 21 ) 22 23 type followingTeacherRepository struct { 24 db *sql.DB 25 } 26 27 func NewFollowingTeacherRepository(db *sql.DB) repository.FollowingTeacher { 28 return &followingTeacherRepository{ 29 db: db, 30 } 31 } 32 33 func (r *followingTeacherRepository) CountFollowingTeachersByUserID(ctx context.Context, userID uint) (int, error) { 34 count := struct{ Count int }{} 35 query := `SELECT COUNT(*) AS count FROM following_teacher WHERE user_id = ?` 36 if err := queries.Raw(query, userID).Bind(ctx, r.db, &count); err != nil { 37 return 0, errors.NewInternalError( 38 errors.WithError(err), 39 errors.WithMessage("count failed"), 40 errors.WithResource(errors.NewResource("following_teacher", "userID", userID)), 41 ) 42 } 43 return count.Count, nil 44 } 45 46 func (r *followingTeacherRepository) Create(ctx context.Context, followingTeacher *model2.FollowingTeacher) error { 47 _, err := model2.FindFollowingTeacher(ctx, r.db, followingTeacher.UserID, followingTeacher.TeacherID) 48 if err != nil { 49 if err != sql.ErrNoRows { 50 return err 51 } 52 return followingTeacher.Insert(ctx, r.db, boil.Infer()) 53 } 54 // Do nothing 55 return nil 56 } 57 58 func (r *followingTeacherRepository) DeleteByUserIDAndTeacherIDs(ctx context.Context, userID uint, teacherIDs []uint) error { 59 placeholder := strings.TrimRight(strings.Repeat("?,", len(teacherIDs)), ",") 60 where := fmt.Sprintf("user_id = ? AND teacher_id IN (%s)", placeholder) 61 args := []interface{}{userID} 62 for _, teacherID := range teacherIDs { 63 args = append(args, teacherID) 64 } 65 _, err := model2.FollowingTeachers(qm.Where(where, args...)).DeleteAll(ctx, r.db) 66 return err 67 } 68 69 func (r *followingTeacherRepository) FindTeacherIDsByUserID( 70 ctx context.Context, 71 userID uint, 72 fetchErrorCount int, 73 lastLessonAt time.Time, 74 ) ([]uint, error) { 75 _, span := otel.Tracer(config.DefaultTracerName).Start(ctx, "followingTeacherRepository.FindTeacherIDsByUserID") 76 span.SetAttributes(attribute.KeyValue{ 77 Key: "userID", 78 Value: attribute.Int64Value(int64(userID)), 79 }) 80 defer span.End() 81 82 values := make([]*model2.FollowingTeacher, 0, 1000) 83 query := ` 84 SELECT ft.teacher_id FROM following_teacher AS ft 85 INNER JOIN teacher AS t ON ft.teacher_id = t.id 86 WHERE 87 ft.user_id = ? 88 AND t.fetch_error_count <= ? 89 AND (t.last_lesson_at >= ? OR t.last_lesson_at = '0000-00-00 00:00:00') 90 ` 91 if err := queries.Raw(query, userID, fetchErrorCount, FormatDateTime(lastLessonAt)).Bind(ctx, r.db, &values); err != nil { 92 if err == sql.ErrNoRows { 93 return nil, nil // Return empty slice without error 94 } 95 return nil, failure.Wrap(err, errors.NewUserIDContext(userID)) 96 } 97 ids := make([]uint, len(values)) 98 for i, t := range values { 99 ids[i] = t.TeacherID 100 } 101 return ids, nil 102 } 103 104 func (r *followingTeacherRepository) FindTeachersByUserID(ctx context.Context, userID uint) ([]*model2.Teacher, error) { 105 query := ` 106 SELECT t.* FROM following_teacher AS ft 107 INNER JOIN teacher AS t ON ft.teacher_id = t.id 108 WHERE ft.user_id = ? 109 ORDER BY ft.created_at DESC 110 ` 111 teachers := make([]*model2.Teacher, 0, 100) 112 if err := queries.Raw(query, userID).Bind(ctx, r.db, &teachers); err != nil { 113 if err == sql.ErrNoRows { 114 return nil, nil 115 } 116 // TODO: Wrap error with morikuni/failure 117 return nil, errors.NewInternalError( 118 errors.WithError(err), 119 errors.WithResource(errors.NewResource("following_teacher", "user_id", userID)), 120 ) 121 } 122 // TODO: expose FollowingTeacher.doAfterSelectHooks in template 123 return teachers, nil 124 } 125 126 func (r *followingTeacherRepository) FindByUserID( 127 ctx context.Context, userID uint, 128 ) ([]*model2.FollowingTeacher, error) { 129 fts, err := model2.FollowingTeachers( 130 qm.Where("user_id = ?", userID), 131 qm.OrderBy("created_at DESC"), 132 ).All(ctx, r.db) 133 if err != nil { 134 return nil, errors.NewInternalError( 135 errors.WithError(err), 136 errors.WithResource(errors.NewResource("following_teacher", "userID", userID)), 137 ) 138 } 139 return fts, nil 140 } 141 142 func (r *followingTeacherRepository) FindByUserIDAndTeacherID( 143 ctx context.Context, userID uint, teacherID uint, 144 ) (*model2.FollowingTeacher, error) { 145 return model2.FindFollowingTeacher(ctx, r.db, userID, teacherID) 146 }