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  }