package repository import ( "context" "time" dbgen "Yimaru-Backend/gen/db" "Yimaru-Backend/internal/domain" "github.com/jackc/pgx/v5/pgtype" ) func toPgText(s *string) pgtype.Text { if s == nil { return pgtype.Text{Valid: false} } return pgtype.Text{String: *s, Valid: true} } func fromPgText(t pgtype.Text) *string { if !t.Valid { return nil } return &t.String } func fromPgInt4(i pgtype.Int4) *int32 { if !i.Valid { return nil } return &i.Int32 } func fromPgInt8(i pgtype.Int8) *int64 { if !i.Valid { return nil } return &i.Int64 } func toPgInt4(i *int32) pgtype.Int4 { if i == nil { return pgtype.Int4{Valid: false} } return pgtype.Int4{Int32: *i, Valid: true} } func toPgInt8(i *int64) pgtype.Int8 { if i == nil { return pgtype.Int8{Valid: false} } return pgtype.Int8{Int64: *i, Valid: true} } func timePtr(t pgtype.Timestamptz) *time.Time { if !t.Valid { return nil } return &t.Time } func questionToDomain(q dbgen.Question) domain.Question { return domain.Question{ ID: q.ID, QuestionText: q.QuestionText, QuestionType: q.QuestionType, DifficultyLevel: fromPgText(q.DifficultyLevel), Points: q.Points, Explanation: fromPgText(q.Explanation), Tips: fromPgText(q.Tips), VoicePrompt: fromPgText(q.VoicePrompt), SampleAnswerVoicePrompt: fromPgText(q.SampleAnswerVoicePrompt), ImageURL: fromPgText(q.ImageUrl), Status: q.Status, CreatedAt: q.CreatedAt.Time, UpdatedAt: timePtr(q.UpdatedAt), } } func questionOptionToDomain(o dbgen.QuestionOption) domain.QuestionOption { return domain.QuestionOption{ ID: o.ID, QuestionID: o.QuestionID, OptionText: o.OptionText, OptionOrder: o.OptionOrder, IsCorrect: o.IsCorrect, CreatedAt: o.CreatedAt.Time, } } func questionShortAnswerToDomain(a dbgen.QuestionShortAnswer) domain.QuestionShortAnswer { return domain.QuestionShortAnswer{ ID: a.ID, QuestionID: a.QuestionID, AcceptableAnswer: a.AcceptableAnswer, MatchType: a.MatchType, CreatedAt: a.CreatedAt.Time, } } func questionAudioAnswerToDomain(a dbgen.QuestionAudioAnswer) domain.QuestionAudioAnswer { return domain.QuestionAudioAnswer{ ID: a.ID, QuestionID: a.QuestionID, CorrectAnswerText: a.CorrectAnswerText, CreatedAt: a.CreatedAt.Time, } } func questionSetToDomain(qs dbgen.QuestionSet) domain.QuestionSet { return domain.QuestionSet{ ID: qs.ID, Title: qs.Title, Description: fromPgText(qs.Description), SetType: qs.SetType, OwnerType: fromPgText(qs.OwnerType), OwnerID: fromPgInt8(qs.OwnerID), BannerImage: fromPgText(qs.BannerImage), Persona: fromPgText(qs.Persona), TimeLimitMinutes: fromPgInt4(qs.TimeLimitMinutes), PassingScore: fromPgInt4(qs.PassingScore), ShuffleQuestions: qs.ShuffleQuestions, Status: qs.Status, SubCourseVideoID: fromPgInt8(qs.SubCourseVideoID), CreatedAt: qs.CreatedAt.Time, UpdatedAt: timePtr(qs.UpdatedAt), } } func questionSetItemToDomain(i dbgen.QuestionSetItem) domain.QuestionSetItem { return domain.QuestionSetItem{ ID: i.ID, SetID: i.SetID, QuestionID: i.QuestionID, DisplayOrder: i.DisplayOrder, CreatedAt: i.CreatedAt.Time, } } func (s *Store) CreateQuestion(ctx context.Context, input domain.CreateQuestionInput) (domain.Question, error) { q, tx, err := s.BeginTx(ctx) if err != nil { return domain.Question{}, err } defer tx.Rollback(ctx) var points interface{} if input.Points != nil { points = *input.Points } var status interface{} if input.Status != nil { status = *input.Status } question, err := q.CreateQuestion(ctx, dbgen.CreateQuestionParams{ QuestionText: input.QuestionText, QuestionType: input.QuestionType, DifficultyLevel: toPgText(input.DifficultyLevel), Column4: points, Explanation: toPgText(input.Explanation), Tips: toPgText(input.Tips), VoicePrompt: toPgText(input.VoicePrompt), SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt), ImageUrl: toPgText(input.ImageURL), Column10: status, }) if err != nil { return domain.Question{}, err } for _, opt := range input.Options { var order interface{} if opt.OptionOrder != nil { order = *opt.OptionOrder } _, err = q.CreateQuestionOption(ctx, dbgen.CreateQuestionOptionParams{ QuestionID: question.ID, OptionText: opt.OptionText, Column3: order, Column4: opt.IsCorrect, }) if err != nil { return domain.Question{}, err } } for _, sa := range input.ShortAnswers { var matchType interface{} if sa.MatchType != nil { matchType = *sa.MatchType } _, err = q.CreateQuestionShortAnswer(ctx, dbgen.CreateQuestionShortAnswerParams{ QuestionID: question.ID, AcceptableAnswer: sa.AcceptableAnswer, Column3: matchType, }) if err != nil { return domain.Question{}, err } } if input.AudioCorrectAnswerText != nil && *input.AudioCorrectAnswerText != "" { _, err = q.CreateQuestionAudioAnswer(ctx, dbgen.CreateQuestionAudioAnswerParams{ QuestionID: question.ID, CorrectAnswerText: *input.AudioCorrectAnswerText, }) if err != nil { return domain.Question{}, err } } if err = tx.Commit(ctx); err != nil { return domain.Question{}, err } return questionToDomain(question), nil } func (s *Store) GetQuestionByID(ctx context.Context, id int64) (domain.Question, error) { q, err := s.queries.GetQuestionByID(ctx, id) if err != nil { return domain.Question{}, err } return questionToDomain(q), nil } func (s *Store) GetQuestionWithDetails(ctx context.Context, id int64) (domain.QuestionWithDetails, error) { q, err := s.queries.GetQuestionByID(ctx, id) if err != nil { return domain.QuestionWithDetails{}, err } opts, err := s.queries.GetOptionsByQuestionID(ctx, id) if err != nil { return domain.QuestionWithDetails{}, err } shortAnswers, err := s.queries.GetShortAnswersByQuestionID(ctx, id) if err != nil { return domain.QuestionWithDetails{}, err } options := make([]domain.QuestionOption, len(opts)) for i, o := range opts { options[i] = questionOptionToDomain(o) } answers := make([]domain.QuestionShortAnswer, len(shortAnswers)) for i, a := range shortAnswers { answers[i] = questionShortAnswerToDomain(a) } var audioAnswer *domain.QuestionAudioAnswer aa, err := s.queries.GetAudioAnswerByQuestionID(ctx, id) if err == nil { mapped := questionAudioAnswerToDomain(aa) audioAnswer = &mapped } return domain.QuestionWithDetails{ Question: questionToDomain(q), Options: options, ShortAnswers: answers, AudioAnswer: audioAnswer, }, nil } func (s *Store) ListQuestions(ctx context.Context, questionType, difficulty, status *string, limit, offset int32) ([]domain.Question, int64, error) { var qType, diff, stat string if questionType != nil { qType = *questionType } if difficulty != nil { diff = *difficulty } if status != nil { stat = *status } rows, err := s.queries.ListQuestions(ctx, dbgen.ListQuestionsParams{ Column1: qType, Column2: diff, Column3: stat, Limit: pgtype.Int4{Int32: limit, Valid: true}, Offset: pgtype.Int4{Int32: offset, Valid: true}, }) if err != nil { return nil, 0, err } var totalCount int64 questions := make([]domain.Question, len(rows)) for i, r := range rows { if i == 0 { totalCount = r.TotalCount } questions[i] = domain.Question{ ID: r.ID, QuestionText: r.QuestionText, QuestionType: r.QuestionType, DifficultyLevel: fromPgText(r.DifficultyLevel), Points: r.Points, Explanation: fromPgText(r.Explanation), Tips: fromPgText(r.Tips), VoicePrompt: fromPgText(r.VoicePrompt), SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt), ImageURL: fromPgText(r.ImageUrl), Status: r.Status, CreatedAt: r.CreatedAt.Time, UpdatedAt: timePtr(r.UpdatedAt), } } return questions, totalCount, nil } func (s *Store) SearchQuestions(ctx context.Context, query string, limit, offset int32) ([]domain.Question, int64, error) { rows, err := s.queries.SearchQuestions(ctx, dbgen.SearchQuestionsParams{ Column1: pgtype.Text{String: query, Valid: true}, Limit: pgtype.Int4{Int32: limit, Valid: true}, Offset: pgtype.Int4{Int32: offset, Valid: true}, }) if err != nil { return nil, 0, err } var totalCount int64 questions := make([]domain.Question, len(rows)) for i, r := range rows { if i == 0 { totalCount = r.TotalCount } questions[i] = domain.Question{ ID: r.ID, QuestionText: r.QuestionText, QuestionType: r.QuestionType, DifficultyLevel: fromPgText(r.DifficultyLevel), Points: r.Points, Explanation: fromPgText(r.Explanation), Tips: fromPgText(r.Tips), VoicePrompt: fromPgText(r.VoicePrompt), SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt), ImageURL: fromPgText(r.ImageUrl), Status: r.Status, CreatedAt: r.CreatedAt.Time, UpdatedAt: timePtr(r.UpdatedAt), } } return questions, totalCount, nil } func (s *Store) UpdateQuestion(ctx context.Context, id int64, input domain.CreateQuestionInput) error { var points int32 if input.Points != nil { points = *input.Points } var status string if input.Status != nil { status = *input.Status } err := s.queries.UpdateQuestion(ctx, dbgen.UpdateQuestionParams{ ID: id, QuestionText: input.QuestionText, QuestionType: input.QuestionType, DifficultyLevel: toPgText(input.DifficultyLevel), Points: points, Explanation: toPgText(input.Explanation), Tips: toPgText(input.Tips), VoicePrompt: toPgText(input.VoicePrompt), SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt), ImageUrl: toPgText(input.ImageURL), Status: status, }) if err != nil { return err } if input.AudioCorrectAnswerText != nil { _ = s.queries.DeleteAudioAnswerByQuestionID(ctx, id) if *input.AudioCorrectAnswerText != "" { _, err = s.queries.CreateQuestionAudioAnswer(ctx, dbgen.CreateQuestionAudioAnswerParams{ QuestionID: id, CorrectAnswerText: *input.AudioCorrectAnswerText, }) if err != nil { return err } } } return nil } func (s *Store) ArchiveQuestion(ctx context.Context, id int64) error { return s.queries.ArchiveQuestion(ctx, id) } func (s *Store) DeleteQuestion(ctx context.Context, id int64) error { return s.queries.DeleteQuestion(ctx, id) } func (s *Store) CreateQuestionOption(ctx context.Context, questionID int64, optionText string, optionOrder *int32, isCorrect bool) (domain.QuestionOption, error) { var order interface{} if optionOrder != nil { order = *optionOrder } opt, err := s.queries.CreateQuestionOption(ctx, dbgen.CreateQuestionOptionParams{ QuestionID: questionID, OptionText: optionText, Column3: order, Column4: isCorrect, }) if err != nil { return domain.QuestionOption{}, err } return questionOptionToDomain(opt), nil } func (s *Store) GetOptionsByQuestionID(ctx context.Context, questionID int64) ([]domain.QuestionOption, error) { opts, err := s.queries.GetOptionsByQuestionID(ctx, questionID) if err != nil { return nil, err } result := make([]domain.QuestionOption, len(opts)) for i, o := range opts { result[i] = questionOptionToDomain(o) } return result, nil } func (s *Store) UpdateQuestionOption(ctx context.Context, id int64, optionText *string, optionOrder *int32, isCorrect *bool) error { var text string if optionText != nil { text = *optionText } var order int32 if optionOrder != nil { order = *optionOrder } var correct bool if isCorrect != nil { correct = *isCorrect } return s.queries.UpdateQuestionOption(ctx, dbgen.UpdateQuestionOptionParams{ ID: id, OptionText: text, OptionOrder: order, IsCorrect: correct, }) } func (s *Store) DeleteQuestionOption(ctx context.Context, id int64) error { return s.queries.DeleteQuestionOption(ctx, id) } func (s *Store) DeleteOptionsByQuestionID(ctx context.Context, questionID int64) error { return s.queries.DeleteOptionsByQuestionID(ctx, questionID) } func (s *Store) CreateQuestionShortAnswer(ctx context.Context, questionID int64, acceptableAnswer string, matchType *string) (domain.QuestionShortAnswer, error) { var mt interface{} if matchType != nil { mt = *matchType } sa, err := s.queries.CreateQuestionShortAnswer(ctx, dbgen.CreateQuestionShortAnswerParams{ QuestionID: questionID, AcceptableAnswer: acceptableAnswer, Column3: mt, }) if err != nil { return domain.QuestionShortAnswer{}, err } return questionShortAnswerToDomain(sa), nil } func (s *Store) GetShortAnswersByQuestionID(ctx context.Context, questionID int64) ([]domain.QuestionShortAnswer, error) { answers, err := s.queries.GetShortAnswersByQuestionID(ctx, questionID) if err != nil { return nil, err } result := make([]domain.QuestionShortAnswer, len(answers)) for i, a := range answers { result[i] = questionShortAnswerToDomain(a) } return result, nil } func (s *Store) UpdateQuestionShortAnswer(ctx context.Context, id int64, acceptableAnswer, matchType *string) error { var answer, mt string if acceptableAnswer != nil { answer = *acceptableAnswer } if matchType != nil { mt = *matchType } return s.queries.UpdateQuestionShortAnswer(ctx, dbgen.UpdateQuestionShortAnswerParams{ ID: id, AcceptableAnswer: answer, MatchType: mt, }) } func (s *Store) DeleteQuestionShortAnswer(ctx context.Context, id int64) error { return s.queries.DeleteQuestionShortAnswer(ctx, id) } func (s *Store) DeleteShortAnswersByQuestionID(ctx context.Context, questionID int64) error { return s.queries.DeleteShortAnswersByQuestionID(ctx, questionID) } func (s *Store) CreateQuestionSet(ctx context.Context, input domain.CreateQuestionSetInput) (domain.QuestionSet, error) { var shuffleQuestions interface{} if input.ShuffleQuestions != nil { shuffleQuestions = *input.ShuffleQuestions } var status interface{} if input.Status != nil { status = *input.Status } qs, err := s.queries.CreateQuestionSet(ctx, dbgen.CreateQuestionSetParams{ Title: input.Title, Description: toPgText(input.Description), SetType: input.SetType, OwnerType: toPgText(input.OwnerType), OwnerID: toPgInt8(input.OwnerID), BannerImage: toPgText(input.BannerImage), Persona: toPgText(input.Persona), TimeLimitMinutes: toPgInt4(input.TimeLimitMinutes), PassingScore: toPgInt4(input.PassingScore), Column10: shuffleQuestions, Column11: status, SubCourseVideoID: toPgInt8(input.SubCourseVideoID), }) if err != nil { return domain.QuestionSet{}, err } return questionSetToDomain(qs), nil } func (s *Store) GetQuestionSetByID(ctx context.Context, id int64) (domain.QuestionSet, error) { qs, err := s.queries.GetQuestionSetByID(ctx, id) if err != nil { return domain.QuestionSet{}, err } return questionSetToDomain(qs), nil } func (s *Store) GetQuestionSetsByOwner(ctx context.Context, ownerType string, ownerID int64) ([]domain.QuestionSet, error) { sets, err := s.queries.GetQuestionSetsByOwner(ctx, dbgen.GetQuestionSetsByOwnerParams{ OwnerType: pgtype.Text{String: ownerType, Valid: true}, OwnerID: pgtype.Int8{Int64: ownerID, Valid: true}, }) if err != nil { return nil, err } result := make([]domain.QuestionSet, len(sets)) for i, qs := range sets { result[i] = questionSetToDomain(qs) } return result, nil } func (s *Store) GetQuestionSetsByType(ctx context.Context, setType string, limit, offset int32) ([]domain.QuestionSet, int64, error) { rows, err := s.queries.GetQuestionSetsByType(ctx, dbgen.GetQuestionSetsByTypeParams{ SetType: setType, Limit: pgtype.Int4{Int32: limit, Valid: true}, Offset: pgtype.Int4{Int32: offset, Valid: true}, }) if err != nil { return nil, 0, err } var totalCount int64 result := make([]domain.QuestionSet, len(rows)) for i, r := range rows { if i == 0 { totalCount = r.TotalCount } result[i] = domain.QuestionSet{ ID: r.ID, Title: r.Title, Description: fromPgText(r.Description), SetType: r.SetType, OwnerType: fromPgText(r.OwnerType), OwnerID: fromPgInt8(r.OwnerID), BannerImage: fromPgText(r.BannerImage), Persona: fromPgText(r.Persona), TimeLimitMinutes: fromPgInt4(r.TimeLimitMinutes), PassingScore: fromPgInt4(r.PassingScore), ShuffleQuestions: r.ShuffleQuestions, Status: r.Status, SubCourseVideoID: fromPgInt8(r.SubCourseVideoID), CreatedAt: r.CreatedAt.Time, UpdatedAt: timePtr(r.UpdatedAt), } } return result, totalCount, nil } func (s *Store) GetPublishedQuestionSetsByOwner(ctx context.Context, ownerType string, ownerID int64) ([]domain.QuestionSet, error) { sets, err := s.queries.GetPublishedQuestionSetsByOwner(ctx, dbgen.GetPublishedQuestionSetsByOwnerParams{ OwnerType: pgtype.Text{String: ownerType, Valid: true}, OwnerID: pgtype.Int8{Int64: ownerID, Valid: true}, }) if err != nil { return nil, err } result := make([]domain.QuestionSet, len(sets)) for i, qs := range sets { result[i] = questionSetToDomain(qs) } return result, nil } func (s *Store) GetInitialAssessmentSet(ctx context.Context) (domain.QuestionSet, error) { qs, err := s.queries.GetInitialAssessmentSet(ctx) if err != nil { return domain.QuestionSet{}, err } return questionSetToDomain(qs), nil } func (s *Store) UpdateQuestionSet(ctx context.Context, id int64, input domain.CreateQuestionSetInput) error { var shuffleQuestions bool if input.ShuffleQuestions != nil { shuffleQuestions = *input.ShuffleQuestions } var status string if input.Status != nil { status = *input.Status } return s.queries.UpdateQuestionSet(ctx, dbgen.UpdateQuestionSetParams{ ID: id, Title: input.Title, Description: toPgText(input.Description), BannerImage: toPgText(input.BannerImage), Persona: toPgText(input.Persona), TimeLimitMinutes: toPgInt4(input.TimeLimitMinutes), PassingScore: toPgInt4(input.PassingScore), ShuffleQuestions: shuffleQuestions, Status: status, SubCourseVideoID: toPgInt8(input.SubCourseVideoID), }) } func (s *Store) ArchiveQuestionSet(ctx context.Context, id int64) error { return s.queries.ArchiveQuestionSet(ctx, id) } func (s *Store) DeleteQuestionSet(ctx context.Context, id int64) error { return s.queries.DeleteQuestionSet(ctx, id) } func (s *Store) AddQuestionToSet(ctx context.Context, setID, questionID int64, displayOrder *int32) (domain.QuestionSetItem, error) { var order interface{} if displayOrder != nil { order = *displayOrder } item, err := s.queries.AddQuestionToSet(ctx, dbgen.AddQuestionToSetParams{ SetID: setID, QuestionID: questionID, Column3: order, }) if err != nil { return domain.QuestionSetItem{}, err } return questionSetItemToDomain(item), nil } func (s *Store) GetQuestionSetItems(ctx context.Context, setID int64) ([]domain.QuestionSetItemWithQuestion, error) { rows, err := s.queries.GetQuestionSetItems(ctx, setID) if err != nil { return nil, err } result := make([]domain.QuestionSetItemWithQuestion, len(rows)) for i, r := range rows { result[i] = domain.QuestionSetItemWithQuestion{ QuestionSetItem: domain.QuestionSetItem{ ID: r.ID, SetID: r.SetID, QuestionID: r.QuestionID, DisplayOrder: r.DisplayOrder, }, QuestionText: r.QuestionText, QuestionType: r.QuestionType, DifficultyLevel: fromPgText(r.DifficultyLevel), Points: r.Points, Explanation: fromPgText(r.Explanation), Tips: fromPgText(r.Tips), VoicePrompt: fromPgText(r.VoicePrompt), ImageURL: fromPgText(r.ImageUrl), QuestionStatus: r.QuestionStatus, } } return result, nil } func (s *Store) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]domain.QuestionSetItemWithQuestion, error) { rows, err := s.queries.GetPublishedQuestionsInSet(ctx, setID) if err != nil { return nil, err } result := make([]domain.QuestionSetItemWithQuestion, len(rows)) for i, r := range rows { result[i] = domain.QuestionSetItemWithQuestion{ QuestionSetItem: domain.QuestionSetItem{ ID: r.ID, SetID: r.SetID, QuestionID: r.QuestionID, DisplayOrder: r.DisplayOrder, }, QuestionText: r.QuestionText, QuestionType: r.QuestionType, DifficultyLevel: fromPgText(r.DifficultyLevel), Points: r.Points, Explanation: fromPgText(r.Explanation), Tips: fromPgText(r.Tips), VoicePrompt: fromPgText(r.VoicePrompt), ImageURL: fromPgText(r.ImageUrl), QuestionStatus: "PUBLISHED", } } return result, nil } func (s *Store) RemoveQuestionFromSet(ctx context.Context, setID, questionID int64) error { return s.queries.RemoveQuestionFromSet(ctx, dbgen.RemoveQuestionFromSetParams{ SetID: setID, QuestionID: questionID, }) } func (s *Store) UpdateQuestionOrder(ctx context.Context, setID, questionID int64, displayOrder int32) error { return s.queries.UpdateQuestionOrder(ctx, dbgen.UpdateQuestionOrderParams{ SetID: setID, QuestionID: questionID, DisplayOrder: displayOrder, }) } func (s *Store) CountQuestionsInSet(ctx context.Context, setID int64) (int64, error) { return s.queries.CountQuestionsInSet(ctx, setID) } func (s *Store) GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error) { sets, err := s.queries.GetQuestionSetsContainingQuestion(ctx, questionID) if err != nil { return nil, err } result := make([]domain.QuestionSet, len(sets)) for i, qs := range sets { result[i] = questionSetToDomain(qs) } return result, nil } // User Persona methods for question sets func (s *Store) AddUserPersonaToQuestionSet(ctx context.Context, questionSetID, userID int64, displayOrder int32) error { _, err := s.queries.AddUserPersonaToQuestionSet(ctx, dbgen.AddUserPersonaToQuestionSetParams{ QuestionSetID: questionSetID, UserID: userID, Column3: displayOrder, }) return err } func (s *Store) RemoveUserPersonaFromQuestionSet(ctx context.Context, questionSetID, userID int64) error { return s.queries.RemoveUserPersonaFromQuestionSet(ctx, dbgen.RemoveUserPersonaFromQuestionSetParams{ QuestionSetID: questionSetID, UserID: userID, }) } func (s *Store) GetUserPersonasByQuestionSetID(ctx context.Context, questionSetID int64) ([]domain.UserPersona, error) { rows, err := s.queries.GetUserPersonasByQuestionSetID(ctx, questionSetID) if err != nil { return nil, err } result := make([]domain.UserPersona, len(rows)) for i, r := range rows { result[i] = domain.UserPersona{ ID: r.ID, FirstName: fromPgText(r.FirstName), LastName: fromPgText(r.LastName), NickName: fromPgText(r.NickName), ProfilePictureURL: fromPgText(r.ProfilePictureUrl), Role: r.Role, DisplayOrder: r.DisplayOrder.Int32, } } return result, nil }