add: db tx commit and rollback implementation

This commit is contained in:
mario
2025-05-15 15:42:33 +07:00
parent 264435f67e
commit d2ec8c0f07
11 changed files with 216 additions and 60 deletions

View File

@@ -6,6 +6,7 @@ import (
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
"github.com/jmoiron/sqlx"
)
// DBDoctor represents a doctor from the database
@@ -44,12 +45,12 @@ func (r *DoctorRepository) GetDoctorDetailsByUserID(userID string) (*DBDoctor, e
return &dbDoctor, nil
}
// CreateDoctor creates a new doctor record
func (r *DoctorRepository) CreateDoctor(doctorDetails *models.DoctorDetails, userID string) error {
// CreateDoctorTx creates a new doctor record within a transaction
func (r *DoctorRepository) CreateDoctorTx(tx *sqlx.Tx, doctorDetails *models.DoctorDetails, userID string) error {
query := `INSERT INTO doctor (Doctor_UsersID, DoctorID, DoctorName, DoctorCreatedAt, DoctorLastUpdatedAt)
VALUES (?, ?, ?, NOW(), NOW())`
_, err := database.DB.Exec(query, userID, doctorDetails.DoctorID, doctorDetails.DoctorName)
_, err := tx.Exec(query, userID, doctorDetails.DoctorID, doctorDetails.DoctorName)
if err != nil {
return fmt.Errorf("database error creating doctor: %w", err)
}

View File

@@ -7,6 +7,7 @@ import (
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
"github.com/jmoiron/sqlx"
)
// DBPatient represents a patient from the database
@@ -61,9 +62,9 @@ func (r *PatientRepository) GetPatientDetailsByUserID(userID string) (*models.Pa
}, nil
}
// CreatePatient creates a new patient record
func (r *PatientRepository) CreatePatient(patientRecord *models.PatientDetails, userID string) error {
// Parse DOB to time.Time
// CreatePatientTx creates a new patient record within a transaction
func (r *PatientRepository) CreatePatientTx(tx *sqlx.Tx, patientRecord *models.PatientDetails, userID string) error {
// Parse DOB to time.Time. 2006-1-02 = reference format YYYY-MM-DD in Go, not a default value
dob, err := time.Parse("2006-01-02", patientRecord.DateOfBirth)
if err != nil {
return fmt.Errorf("invalid date of birth format: %w", err)
@@ -72,7 +73,7 @@ func (r *PatientRepository) CreatePatient(patientRecord *models.PatientDetails,
query := `INSERT INTO patient (Patient_UsersID, PatientMedrec, PatientName, PatientDoB, PatientCreatedAt, PatientUpdatedAt)
VALUES (?, ?, ?, ?, NOW(), NOW())`
_, err = database.DB.Exec(query, userID, patientRecord.PatientID, patientRecord.PatientName, dob)
_, err = tx.Exec(query, userID, patientRecord.PatientID, patientRecord.PatientName, dob)
if err != nil {
return fmt.Errorf("database error creating patient: %w", err)
}

View File

@@ -8,6 +8,7 @@ import (
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
"github.com/jmoiron/sqlx"
)
// DBShortLink represents a shortlink from the database
@@ -69,8 +70,8 @@ func (r *ShortLinkRepository) GetShortLinkByToken(token string) (*models.ShortLi
return dbShortLink.ToShortLink(), nil
}
// CreateShortLink stores a new shortlink in the database
func (r *ShortLinkRepository) CreateShortLink(shortLink *models.ShortLink) error {
// CreateShortLinkTx stores a new shortlink in the database within a transaction
func (r *ShortLinkRepository) CreateShortLinkTx(tx *sqlx.Tx, shortLink *models.ShortLink) error {
query := `INSERT INTO shortlink (
ShortlinkCode,
Shortlink_PatientID,
@@ -93,7 +94,7 @@ func (r *ShortLinkRepository) CreateShortLink(shortLink *models.ShortLink) error
return fmt.Errorf("invalid expiration date: %w", err)
}
_, err = database.DB.Exec(
_, err = tx.Exec(
query,
shortLink.Token,
shortLink.PatientID,
@@ -112,8 +113,8 @@ func (r *ShortLinkRepository) CreateShortLink(shortLink *models.ShortLink) error
return nil
}
// UpdateShortLink updates an existing shortlink in the database
func (r *ShortLinkRepository) UpdateShortLink(shortLink *models.ShortLink) error {
// UpdateShortLinkTx updates an existing shortlink in the database within a transaction
func (r *ShortLinkRepository) UpdateShortLinkTx(tx *sqlx.Tx, shortLink *models.ShortLink) error {
query := `UPDATE shortlink SET
ShortlinkIsRevoked = ?,
ShortlinkRemainingTries = ?,
@@ -125,7 +126,7 @@ func (r *ShortLinkRepository) UpdateShortLink(shortLink *models.ShortLink) error
return fmt.Errorf("invalid expiration date: %w", err)
}
_, err = database.DB.Exec(
_, err = tx.Exec(
query,
shortLink.IsRevoked,
shortLink.RemainingTries,

View File

@@ -6,6 +6,7 @@ import (
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
"github.com/jmoiron/sqlx"
)
// DBStudy represents a study from the database
@@ -69,8 +70,8 @@ func (r *StudyRepository) GetStudyByUID(studyUID string) (*DBStudy, error) {
return &study, nil
}
// CreateStudy creates a new study record for a patient
func (r *StudyRepository) CreateStudy(patientID string, study models.Study) error {
// CreateStudyTx creates a new study record for a patient within a transaction
func (r *StudyRepository) CreateStudyTx(tx *sqlx.Tx, patientID string, study models.Study) error {
// Parse study date if provided
var studyDate *time.Time
if study.StudyDate != "" {
@@ -85,7 +86,7 @@ func (r *StudyRepository) CreateStudy(patientID string, study models.Study) erro
(Study_PatientID, StudyIUID, StudyAccessionNumber, StudyDate, StudyCreatedAt, StudyUpdatedAt)
VALUES (?, ?, ?, ?, NOW(), NOW())`
_, err := database.DB.Exec(query,
_, err := tx.Exec(query,
patientID,
study.StudyInstanceUID,
study.AccessionNumber,

View File

@@ -7,6 +7,7 @@ import (
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
)
@@ -140,8 +141,8 @@ func (r *UserRepository) RevokeRefreshToken(token string) error {
return nil
}
// CreateUser creates a new user
func (r *UserRepository) CreateUser(user *models.User) error {
// CreateUserTx creates a new user within a transaction
func (r *UserRepository) CreateUserTx(tx *sqlx.Tx, user *models.User) error {
// Hash the password before storing
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
@@ -151,7 +152,7 @@ func (r *UserRepository) CreateUser(user *models.User) error {
query := `INSERT INTO user (UserEmail, UserPassword, UserRole, UserName, UserCreatedAt, UserUpdatedAt)
VALUES (?, ?, ?, ?, NOW(), NOW())`
result, err := database.DB.Exec(query, user.Email, string(hashedPassword), user.Role, user.Name)
result, err := tx.Exec(query, user.Email, string(hashedPassword), user.Role, user.Name)
if err != nil {
return fmt.Errorf("database error creating user: %w", err)
}

View File

@@ -88,8 +88,8 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
r.Post("/refresh", authHandler.RefreshToken)
r.Post("/logout", authHandler.Logout)
// Registration endpoint
registerService := service.NewRegisterService()
// Registration endpoint
registerService := service.NewRegisterService(logger)
registerHandler := handlers.NewRegisterHandler(logger, registerService)
r.Post("/register", registerHandler.Register)

View File

@@ -7,6 +7,8 @@ import (
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/repository"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
"go.uber.org/zap"
)
var (
@@ -15,6 +17,7 @@ var (
ErrInvalidRole = errors.New("invalid user role")
ErrInvalidPatient = errors.New("invalid patient data")
ErrInvalidDoctor = errors.New("invalid doctor data")
ErrTransaction = errors.New("transaction error")
)
// RegisterRequest represents a user registration request
@@ -34,15 +37,22 @@ type RegisterService struct {
patientRepo *repository.PatientRepository
doctorRepo *repository.DoctorRepository
studyRepo *repository.StudyRepository
logger *zap.Logger
}
// NewRegisterService creates a new register service
func NewRegisterService() *RegisterService {
func NewRegisterService(logger *zap.Logger) *RegisterService {
if logger == nil {
// If no logger is provided, create a no-op logger
logger = zap.NewNop()
}
return &RegisterService{
userRepo: repository.NewUserRepository(),
patientRepo: repository.NewPatientRepository(),
doctorRepo: repository.NewDoctorRepository(),
studyRepo: repository.NewStudyRepository(),
logger: logger,
}
}
@@ -62,7 +72,7 @@ func (s *RegisterService) Register(req *RegisterRequest) (*models.User, error) {
return nil, ErrInvalidDoctor
}
// Check if email already exists
// Check if email already exists - do this outside the transaction
existingUser, err := s.userRepo.GetUserByEmail(req.Email)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("error checking existing user: %w", err)
@@ -72,6 +82,22 @@ func (s *RegisterService) Register(req *RegisterRequest) (*models.User, error) {
return nil, ErrEmailExists
}
// Start a transaction
tx, err := database.DB.Beginx()
if err != nil {
s.logger.Error("Failed to begin transaction", zap.Error(err))
return nil, fmt.Errorf("%w: failed to begin transaction", ErrTransaction)
}
// Ensure the transaction is rolled back if we return an error
defer func() {
if r := recover(); r != nil {
s.logger.Error("Panic in transaction", zap.Any("recover", r))
tx.Rollback()
panic(r) // re-throw the panic after cleanup
}
}()
// Create user
newUser := &models.User{
Email: req.Email,
@@ -80,15 +106,19 @@ func (s *RegisterService) Register(req *RegisterRequest) (*models.User, error) {
Name: req.Name,
}
if err := s.userRepo.CreateUser(newUser); err != nil {
// Use the transaction for user creation
if err := s.userRepo.CreateUserTx(tx, newUser); err != nil {
s.logger.Error("Failed to create user", zap.Error(err), zap.String("email", req.Email))
tx.Rollback()
return nil, fmt.Errorf("error creating user: %w", err)
}
// Create role-specific data
if req.Role == "patient" {
err = s.patientRepo.CreatePatient(req.Patient, newUser.ID)
err = s.patientRepo.CreatePatientTx(tx, req.Patient, newUser.ID)
if err != nil {
// TODO: Consider rollback user creation if this fails
s.logger.Error("Failed to create patient", zap.Error(err), zap.String("patientID", req.Patient.PatientID))
tx.Rollback()
return nil, fmt.Errorf("error creating patient: %w", err)
}
@@ -99,19 +129,37 @@ func (s *RegisterService) Register(req *RegisterRequest) (*models.User, error) {
continue // Skip studies without UIDs
}
err = s.studyRepo.CreateStudy(req.Patient.PatientID, study)
err = s.studyRepo.CreateStudyTx(tx, req.Patient.PatientID, study)
if err != nil {
s.logger.Error("Failed to create study",
zap.Error(err),
zap.String("patientID", req.Patient.PatientID),
zap.String("studyUID", study.StudyInstanceUID))
tx.Rollback()
return nil, fmt.Errorf("error creating study for patient: %w", err)
}
}
}
} else if req.Role == "ref_doctor" || req.Role == "expertise_doctor" {
err = s.doctorRepo.CreateDoctor(req.Doctor, newUser.ID)
err = s.doctorRepo.CreateDoctorTx(tx, req.Doctor, newUser.ID)
if err != nil {
// TODO: Consider rollback user creation if this fails
s.logger.Error("Failed to create doctor", zap.Error(err), zap.String("doctorID", req.Doctor.DoctorID))
tx.Rollback()
return nil, fmt.Errorf("error creating doctor: %w", err)
}
}
// Commit the transaction
if err := tx.Commit(); err != nil {
s.logger.Error("Failed to commit transaction", zap.Error(err))
tx.Rollback() // This is actually redundant as the transaction will be rolled back on failure
return nil, fmt.Errorf("%w: failed to commit transaction", ErrTransaction)
}
s.logger.Info("Successfully registered new user",
zap.String("email", req.Email),
zap.String("role", req.Role),
zap.String("userID", newUser.ID))
return newUser, nil
}

View File

@@ -13,6 +13,7 @@ import (
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/repository"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth"
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
"go.uber.org/zap"
)
@@ -150,13 +151,36 @@ func (s *ShortLinkService) GenerateShortLink(req *models.GenerateShortLinkReques
RemainingTries: s.maxAttempts,
}
// Store the short link in the database
err = s.shortLinkRepo.CreateShortLink(shortLink)
// Start a transaction for creating the shortlink
tx, err := database.DB.Beginx()
if err != nil {
s.logger.Error("Failed to start transaction", zap.Error(err))
return nil, fmt.Errorf("database error: %w", err)
}
// Set up deferred rollback that will be canceled if we commit
defer func() {
if tx != nil {
tx.Rollback()
}
}()
// Store the short link in the database using transaction
err = s.shortLinkRepo.CreateShortLinkTx(tx, shortLink)
if err != nil {
s.logger.Error("Failed to store shortlink in database", zap.Error(err))
return nil, ErrCreationFailed
}
// Commit the transaction
if err = tx.Commit(); err != nil {
s.logger.Error("Failed to commit transaction", zap.Error(err))
return nil, errors.New("database error")
}
// Clear the tx to prevent the deferred rollback
tx = nil
// Generate the full URL using the configured base URL
fullURL := fmt.Sprintf("%s/short-auth?short=%s", s.baseURL, token)
@@ -203,43 +227,101 @@ func (s *ShortLinkService) ValidateShortLink(req *models.ShortLinkAuthRequest) (
// Normalize and hash the provided DOB
dob := normalizeDOB(req.DOB)
if !isValidDOBFormat(dob) {
// Use a transaction for updating the tries counter
tx, err := database.DB.Beginx()
if err != nil {
s.logger.Error("Failed to start transaction", zap.Error(err))
return nil, fmt.Errorf("database error: %w", err)
}
// Set up deferred rollback that will be canceled if we commit
defer func() {
if tx != nil {
tx.Rollback()
}
}()
// Decrement remaining tries on invalid format
shortLink.RemainingTries--
// Update the shortlink in the database
err = s.shortLinkRepo.UpdateShortLink(shortLink)
// Update the shortlink in the database using the transaction
err = s.shortLinkRepo.UpdateShortLinkTx(tx, shortLink)
if err != nil {
s.logger.Error("Failed to update shortlink tries", zap.Error(err))
return nil, errors.New("invalid date of birth format, expected YYYY-MM-DD")
}
// Commit the transaction
if err = tx.Commit(); err != nil {
s.logger.Error("Failed to commit transaction", zap.Error(err))
return nil, errors.New("database error")
}
// Clear the tx to prevent the deferred rollback
tx = nil
return nil, errors.New("invalid date of birth format, expected YYYY-MM-DD")
}
hashedDOB := hashDOB(dob)
// Start a transaction for updating the tries counter
tx, err := database.DB.Beginx()
if err != nil {
s.logger.Error("Failed to start transaction", zap.Error(err))
return nil, fmt.Errorf("database error: %w", err)
}
// Set up deferred rollback that will be canceled if we commit
defer func() {
if tx != nil {
tx.Rollback()
}
}()
// Verify the DOB
if hashedDOB != shortLink.HashedDOB {
// Decrement remaining tries on failed verification
shortLink.RemainingTries--
// Update the shortlink in the database
err = s.shortLinkRepo.UpdateShortLink(shortLink)
// Update the shortlink in the database using the transaction
err = s.shortLinkRepo.UpdateShortLinkTx(tx, shortLink)
if err != nil {
s.logger.Error("Failed to update shortlink tries", zap.Error(err))
return nil, ErrInvalidDOB
}
// Commit the transaction
if err = tx.Commit(); err != nil {
s.logger.Error("Failed to commit transaction", zap.Error(err))
return nil, errors.New("database error")
}
// Clear the tx to prevent the deferred rollback
tx = nil
return nil, ErrInvalidDOB
}
// DOB verified, reset tries count as successful login
shortLink.RemainingTries = s.maxAttempts
// Update the shortlink in the database
err = s.shortLinkRepo.UpdateShortLink(shortLink)
// Update the shortlink in the database using the transaction
err = s.shortLinkRepo.UpdateShortLinkTx(tx, shortLink)
if err != nil {
s.logger.Error("Failed to update shortlink tries after successful validation", zap.Error(err))
return nil, errors.New("database error")
}
// Commit the transaction
if err = tx.Commit(); err != nil {
s.logger.Error("Failed to commit transaction", zap.Error(err))
return nil, errors.New("database error")
}
// Clear the tx to prevent the deferred rollback
tx = nil
return shortLink, nil
}