repo: make execution creation deterministic and add user-scoped APIs

This commit is contained in:
rishikanthc
2026-04-23 11:24:13 -07:00
parent fc3e933104
commit c5f758cddd

View File

@@ -4,7 +4,10 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"scriberr/internal/models"
"strings"
"time"
"gorm.io/gorm"
@@ -161,7 +164,74 @@ func (r *jobRepository) UpdateTranscript(ctx context.Context, jobID string, tran
}
func (r *jobRepository) CreateExecution(ctx context.Context, execution *models.TranscriptionJobExecution) error {
return r.db.WithContext(ctx).Create(execution).Error
const maxCreateExecutionRetries = 5
var lastErr error
for attempt := 0; attempt < maxCreateExecutionRetries; attempt++ {
lastErr = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return createExecutionInTx(tx, execution)
})
if lastErr == nil {
return nil
}
if !isExecutionNumberConflict(lastErr) {
return lastErr
}
}
return fmt.Errorf("unable to allocate execution number for transcription %s: %w", execution.TranscriptionJobID, lastErr)
}
func createExecutionInTx(tx *gorm.DB, execution *models.TranscriptionJobExecution) error {
var job models.TranscriptionJob
if err := tx.Select("id", "user_id").Where("id = ?", execution.TranscriptionJobID).First(&job).Error; err != nil {
return err
}
execution.UserID = job.UserID
var nextExecutionNumber int
if err := tx.Model(&models.TranscriptionJobExecution{}).
Where("transcription_id = ?", execution.TranscriptionJobID).
Select("COALESCE(MAX(execution_number), 0) + 1").
Scan(&nextExecutionNumber).Error; err != nil {
return err
}
execution.ExecutionNumber = nextExecutionNumber
if err := tx.Create(execution).Error; err != nil {
return err
}
return tx.Model(&models.TranscriptionJob{}).
Where("id = ?", execution.TranscriptionJobID).
Update("latest_execution_id", execution.ID).Error
}
func isExecutionNumberConflict(err error) bool {
if !errors.Is(err, gorm.ErrDuplicatedKey) {
return false
}
errMsg := strings.ToLower(err.Error())
return strings.Contains(errMsg, "transcription_executions") && strings.Contains(errMsg, "execution_number")
}
func resolveLegacySingletonUserID(ctx context.Context, db *gorm.DB) (uint, error) {
const scopeError = "legacy repository method requires explicit user-scoped method"
var users []models.User
if err := db.WithContext(ctx).
Model(&models.User{}).
Select("id").
Order("id ASC").
Limit(2).
Find(&users).Error; err != nil {
return 0, err
}
if len(users) == 0 {
return 0, fmt.Errorf("%s: no users exist", scopeError)
}
if len(users) > 1 {
return 0, fmt.Errorf("%s: multiple users exist", scopeError)
}
return users[0].ID, nil
}
func (r *jobRepository) UpdateExecution(ctx context.Context, execution *models.TranscriptionJobExecution) error {
@@ -179,7 +249,7 @@ func (r *jobRepository) DeleteMultiTrackFilesByJobID(ctx context.Context, jobID
func (r *jobRepository) FindActiveTrackJobs(ctx context.Context, parentJobID string) ([]models.TranscriptionJob, error) {
var jobs []models.TranscriptionJob
err := r.db.WithContext(ctx).
Where("id LIKE ? AND status IN (?)", "track_"+parentJobID+"_%", []string{"processing", "pending"}).
Where("id LIKE ? AND status IN ?", "track_"+parentJobID+"_%", []models.JobStatus{models.StatusProcessing, models.StatusPending}).
Find(&jobs).Error
return jobs, err
}
@@ -232,8 +302,13 @@ func (r *jobRepository) UpdateSummary(ctx context.Context, jobID string, summary
type APIKeyRepository interface {
Repository[models.APIKey]
FindByKey(ctx context.Context, key string) (*models.APIKey, error)
// Deprecated: Legacy global access. Use ListActiveByUser instead.
ListActive(ctx context.Context) ([]models.APIKey, error)
ListActiveByUser(ctx context.Context, userID uint) ([]models.APIKey, error)
FindByIDForUser(ctx context.Context, id, userID uint) (*models.APIKey, error)
// Deprecated: Legacy global access. Use RevokeForUser instead.
Revoke(ctx context.Context, id uint) error
RevokeForUser(ctx context.Context, id, userID uint) error
}
type apiKeyRepository struct {
@@ -259,24 +334,57 @@ func (r *apiKeyRepository) FindByKey(ctx context.Context, key string) (*models.A
}
func (r *apiKeyRepository) ListActive(ctx context.Context) ([]models.APIKey, error) {
var apiKeys []models.APIKey
err := r.db.WithContext(ctx).Where("revoked_at IS NULL").Find(&apiKeys).Error
userID, err := resolveLegacySingletonUserID(ctx, r.db)
if err != nil {
return nil, err
}
return r.ListActiveByUser(ctx, userID)
}
func (r *apiKeyRepository) ListActiveByUser(ctx context.Context, userID uint) ([]models.APIKey, error) {
var apiKeys []models.APIKey
if err := r.db.WithContext(ctx).Where("user_id = ? AND revoked_at IS NULL", userID).Find(&apiKeys).Error; err != nil {
return nil, err
}
return apiKeys, nil
}
func (r *apiKeyRepository) FindByIDForUser(ctx context.Context, id, userID uint) (*models.APIKey, error) {
var apiKey models.APIKey
if err := r.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).First(&apiKey).Error; err != nil {
return nil, err
}
return &apiKey, nil
}
func (r *apiKeyRepository) Revoke(ctx context.Context, id uint) error {
// Revoke is intentionally global for backward compatibility.
// Prefer RevokeForUser with explicit user ID for all new call sites.
userID, err := resolveLegacySingletonUserID(ctx, r.db)
if err != nil {
return err
}
return r.RevokeForUser(ctx, id, userID)
}
func (r *apiKeyRepository) RevokeForUser(ctx context.Context, id, userID uint) error {
now := time.Now()
return r.db.WithContext(ctx).Model(&models.APIKey{}).Where("id = ?", id).Update("revoked_at", &now).Error
return r.db.WithContext(ctx).Model(&models.APIKey{}).
Where("id = ? AND user_id = ?", id, userID).
Update("revoked_at", &now).Error
}
// ProfileRepository handles transcription profile operations
type ProfileRepository interface {
Repository[models.TranscriptionProfile]
// Deprecated: Legacy global access. Use FindDefaultByUser instead.
FindDefault(ctx context.Context) (*models.TranscriptionProfile, error)
// Deprecated: Legacy global access. Use FindByNameForUser instead.
FindByName(ctx context.Context, name string) (*models.TranscriptionProfile, error)
ListByUser(ctx context.Context, userID uint, offset, limit int) ([]models.TranscriptionProfile, int64, error)
FindByIDForUser(ctx context.Context, id string, userID uint) (*models.TranscriptionProfile, error)
FindDefaultByUser(ctx context.Context, userID uint) (*models.TranscriptionProfile, error)
FindByNameForUser(ctx context.Context, userID uint, name string) (*models.TranscriptionProfile, error)
}
type profileRepository struct {
@@ -290,27 +398,65 @@ func NewProfileRepository(db *gorm.DB) ProfileRepository {
}
func (r *profileRepository) FindDefault(ctx context.Context) (*models.TranscriptionProfile, error) {
var profile models.TranscriptionProfile
err := r.db.WithContext(ctx).Where("is_default = ?", true).First(&profile).Error
userID, err := resolveLegacySingletonUserID(ctx, r.db)
if err != nil {
return nil, err
}
return r.FindDefaultByUser(ctx, userID)
}
func (r *profileRepository) ListByUser(ctx context.Context, userID uint, offset, limit int) ([]models.TranscriptionProfile, int64, error) {
var profiles []models.TranscriptionProfile
var count int64
query := r.db.WithContext(ctx).Model(&models.TranscriptionProfile{}).Where("user_id = ?", userID)
if err := query.Count(&count).Error; err != nil {
return nil, 0, err
}
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&profiles).Error; err != nil {
return nil, 0, err
}
return profiles, count, nil
}
func (r *profileRepository) FindByIDForUser(ctx context.Context, id string, userID uint) (*models.TranscriptionProfile, error) {
var profile models.TranscriptionProfile
if err := r.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).First(&profile).Error; err != nil {
return nil, err
}
return &profile, nil
}
func (r *profileRepository) FindDefaultByUser(ctx context.Context, userID uint) (*models.TranscriptionProfile, error) {
var profile models.TranscriptionProfile
if err := r.db.WithContext(ctx).Where("user_id = ? AND is_default = ?", userID, true).First(&profile).Error; err != nil {
return nil, err
}
return &profile, nil
}
func (r *profileRepository) FindByName(ctx context.Context, name string) (*models.TranscriptionProfile, error) {
var profile models.TranscriptionProfile
err := r.db.WithContext(ctx).Where("name = ?", name).First(&profile).Error
userID, err := resolveLegacySingletonUserID(ctx, r.db)
if err != nil {
return nil, err
}
return r.FindByNameForUser(ctx, userID, name)
}
func (r *profileRepository) FindByNameForUser(ctx context.Context, userID uint, name string) (*models.TranscriptionProfile, error) {
var profile models.TranscriptionProfile
if err := r.db.WithContext(ctx).Where("user_id = ? AND name = ?", userID, name).First(&profile).Error; err != nil {
return nil, err
}
return &profile, nil
}
// LLMConfigRepository handles LLM configuration operations
type LLMConfigRepository interface {
Repository[models.LLMConfig]
// Deprecated: Legacy global access. Use GetActiveByUser instead.
GetActive(ctx context.Context) (*models.LLMConfig, error)
GetActiveByUser(ctx context.Context, userID uint) (*models.LLMConfig, error)
}
type llmConfigRepository struct {
@@ -324,19 +470,32 @@ func NewLLMConfigRepository(db *gorm.DB) LLMConfigRepository {
}
func (r *llmConfigRepository) GetActive(ctx context.Context) (*models.LLMConfig, error) {
var config models.LLMConfig
err := r.db.WithContext(ctx).Where("is_default = ?", true).First(&config).Error
userID, err := resolveLegacySingletonUserID(ctx, r.db)
if err != nil {
return nil, err
}
return r.GetActiveByUser(ctx, userID)
}
func (r *llmConfigRepository) GetActiveByUser(ctx context.Context, userID uint) (*models.LLMConfig, error) {
var config models.LLMConfig
if err := r.db.WithContext(ctx).Where("user_id = ? AND is_default = ?", userID, true).First(&config).Error; err != nil {
return nil, err
}
return &config, nil
}
// SummaryRepository handles summary templates and settings
type SummaryRepository interface {
Repository[models.SummaryTemplate]
// Deprecated: Legacy global access. Use GetSettingsByUser/SaveSettingsByUser instead.
GetSettings(ctx context.Context) (*models.SummarySetting, error)
// Deprecated: Legacy global access. Use GetSettingsByUser/SaveSettingsByUser instead.
SaveSettings(ctx context.Context, settings *models.SummarySetting) error
ListByUser(ctx context.Context, userID uint, offset, limit int) ([]models.SummaryTemplate, int64, error)
FindByIDForUser(ctx context.Context, id string, userID uint) (*models.SummaryTemplate, error)
GetSettingsByUser(ctx context.Context, userID uint) (*models.SummarySetting, error)
SaveSettingsByUser(ctx context.Context, userID uint, settings *models.SummarySetting) error
SaveSummary(ctx context.Context, summary *models.Summary) error
GetLatestSummary(ctx context.Context, transcriptionID string) (*models.Summary, error)
DeleteByTranscriptionID(ctx context.Context, transcriptionID string) error
@@ -353,17 +512,54 @@ func NewSummaryRepository(db *gorm.DB) SummaryRepository {
}
func (r *summaryRepository) GetSettings(ctx context.Context) (*models.SummarySetting, error) {
var user models.User
err := r.db.WithContext(ctx).Order("id ASC").First(&user).Error
userID, err := resolveLegacySingletonUserID(ctx, r.db)
if err != nil {
return nil, err
}
return r.GetSettingsByUser(ctx, userID)
}
func (r *summaryRepository) SaveSettings(ctx context.Context, settings *models.SummarySetting) error {
userID, err := resolveLegacySingletonUserID(ctx, r.db)
if err != nil {
return err
}
return r.SaveSettingsByUser(ctx, userID, settings)
}
func (r *summaryRepository) ListByUser(ctx context.Context, userID uint, offset, limit int) ([]models.SummaryTemplate, int64, error) {
var templates []models.SummaryTemplate
var count int64
query := r.db.WithContext(ctx).Model(&models.SummaryTemplate{}).Where("user_id = ?", userID)
if err := query.Count(&count).Error; err != nil {
return nil, 0, err
}
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&templates).Error; err != nil {
return nil, 0, err
}
return templates, count, nil
}
func (r *summaryRepository) FindByIDForUser(ctx context.Context, id string, userID uint) (*models.SummaryTemplate, error) {
var template models.SummaryTemplate
if err := r.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).First(&template).Error; err != nil {
return nil, err
}
return &template, nil
}
func (r *summaryRepository) GetSettingsByUser(ctx context.Context, userID uint) (*models.SummarySetting, error) {
var user models.User
if err := r.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
return nil, err
}
return &models.SummarySetting{DefaultModel: user.SummaryDefaultModel}, nil
}
func (r *summaryRepository) SaveSettings(ctx context.Context, settings *models.SummarySetting) error {
func (r *summaryRepository) SaveSettingsByUser(ctx context.Context, userID uint, settings *models.SummarySetting) error {
var user models.User
if err := r.db.WithContext(ctx).Order("id ASC").First(&user).Error; err != nil {
if err := r.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
return err
}
user.SummaryDefaultModel = settings.DefaultModel