package sql import ( "context" "errors" "fmt" "tercul/internal/domain" "tercul/internal/platform/config" "github.com/google/uuid" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" "gorm.io/gorm" ) type workRepository struct { *BaseRepositoryImpl[domain.Work] db *gorm.DB tracer trace.Tracer } // NewWorkRepository creates a new WorkRepository. func NewWorkRepository(db *gorm.DB, cfg *config.Config) domain.WorkRepository { return &workRepository{ BaseRepositoryImpl: NewBaseRepositoryImpl[domain.Work](db, cfg), db: db, tracer: otel.Tracer("work.repository"), } } // FindByTitle finds works by title (partial match) func (r *workRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) { ctx, span := r.tracer.Start(ctx, "FindByTitle") defer span.End() var works []domain.Work if err := r.db.WithContext(ctx).Where("title LIKE ?", "%"+title+"%").Find(&works).Error; err != nil { return nil, err } return works, nil } // FindByAuthor finds works by author ID func (r *workRepository) FindByAuthor(ctx context.Context, authorID uuid.UUID) ([]domain.Work, error) { ctx, span := r.tracer.Start(ctx, "FindByAuthor") defer span.End() var works []domain.Work if err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.work_id = works.id"). Where("work_authors.author_id = ?", authorID). Find(&works).Error; err != nil { return nil, err } return works, nil } // FindByCategory finds works by category ID func (r *workRepository) FindByCategory(ctx context.Context, categoryID uuid.UUID) ([]domain.Work, error) { ctx, span := r.tracer.Start(ctx, "FindByCategory") defer span.End() var works []domain.Work if err := r.db.WithContext(ctx).Joins("JOIN work_categories ON work_categories.work_id = works.id"). Where("work_categories.category_id = ?", categoryID). Find(&works).Error; err != nil { return nil, err } return works, nil } // FindByLanguage finds works by language with pagination func (r *workRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { ctx, span := r.tracer.Start(ctx, "FindByLanguage") defer span.End() if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } var works []domain.Work var totalCount int64 // Get total count if err := r.db.WithContext(ctx).Model(&domain.Work{}).Where("language = ?", language).Count(&totalCount).Error; err != nil { return nil, err } // Calculate offset offset := (page - 1) * pageSize // Get paginated data if err := r.db.WithContext(ctx).Where("language = ?", language). Offset(offset).Limit(pageSize). Find(&works).Error; err != nil { return nil, err } // Calculate total pages totalPages := int(totalCount) / pageSize if int(totalCount)%pageSize > 0 { totalPages++ } hasNext := page < totalPages hasPrev := page > 1 return &domain.PaginatedResult[domain.Work]{ Items: works, TotalCount: totalCount, Page: page, PageSize: pageSize, TotalPages: totalPages, HasNext: hasNext, HasPrev: hasPrev, }, nil } // ListByCollectionID finds works by collection ID func (r *workRepository) ListByCollectionID(ctx context.Context, collectionID uuid.UUID) ([]domain.Work, error) { ctx, span := r.tracer.Start(ctx, "ListByCollectionID") defer span.End() var works []domain.Work if err := r.db.WithContext(ctx).Joins("JOIN collection_works ON collection_works.work_id = works.id"). Where("collection_works.collection_id = ?", collectionID). Find(&works).Error; err != nil { return nil, err } return works, nil } // Delete removes a work and its associations func (r *workRepository) Delete(ctx context.Context, id uuid.UUID) error { ctx, span := r.tracer.Start(ctx, "Delete") defer span.End() return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // Manually delete associations if err := tx.Select("Copyrights", "Monetizations", "Authors", "Tags", "Categories").Delete(&domain.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: id}}}).Error; err != nil { return err } // Also delete the work itself if err := tx.Delete(&domain.Work{}, id).Error; err != nil { return err } return nil }) } // GetWithTranslations gets a work with its translations func (r *workRepository) GetWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Work, error) { ctx, span := r.tracer.Start(ctx, "GetWithTranslations") defer span.End() return r.FindWithPreload(ctx, []string{"Translations"}, id) } // GetWithAssociations gets a work with all of its direct and many-to-many associations. func (r *workRepository) GetWithAssociations(ctx context.Context, id uuid.UUID) (*domain.Work, error) { ctx, span := r.tracer.Start(ctx, "GetWithAssociations") defer span.End() associations := []string{ "Translations", "Authors", "Tags", "Categories", "Copyrights", "Monetizations", } return r.FindWithPreload(ctx, associations, id) } // GetWithAssociationsInTx gets a work with all associations within a transaction. func (r *workRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) (*domain.Work, error) { ctx, span := r.tracer.Start(ctx, "GetWithAssociationsInTx") defer span.End() var entity domain.Work query := tx.WithContext(ctx) associations := []string{ "Translations", "Authors", "Tags", "Categories", "Copyrights", "Monetizations", } for _, preload := range associations { query = query.Preload(preload) } if err := query.First(&entity, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, domain.ErrEntityNotFound } return nil, fmt.Errorf("database operation failed: %w", err) } return &entity, nil } // IsAuthor checks if a user is an author of a work. // Note: This assumes a direct relationship between user ID and author ID, // which may need to be revised based on the actual domain model. func (r *workRepository) IsAuthor(ctx context.Context, workID uuid.UUID, authorID uuid.UUID) (bool, error) { ctx, span := r.tracer.Start(ctx, "IsAuthor") defer span.End() var count int64 err := r.db.WithContext(ctx). Table("work_authors"). Where("work_id = ? AND author_id = ?", workID, authorID). Count(&count).Error if err != nil { return false, err } return count > 0, nil } // ListWithTranslations lists works with their translations func (r *workRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { ctx, span := r.tracer.Start(ctx, "ListWithTranslations") defer span.End() if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } var works []domain.Work var totalCount int64 // Get total count if err := r.db.WithContext(ctx).Model(&domain.Work{}).Count(&totalCount).Error; err != nil { return nil, err } // Calculate offset offset := (page - 1) * pageSize // Get paginated data with preloaded translations if err := r.db.WithContext(ctx).Preload("Translations"). Offset(offset).Limit(pageSize). Find(&works).Error; err != nil { return nil, err } // Calculate total pages totalPages := int(totalCount) / pageSize if int(totalCount)%pageSize > 0 { totalPages++ } hasNext := page < totalPages hasPrev := page > 1 return &domain.PaginatedResult[domain.Work]{ Items: works, TotalCount: totalCount, Page: page, PageSize: pageSize, TotalPages: totalPages, HasNext: hasNext, HasPrev: hasPrev, }, nil }