refactor(domain): Isolate Work aggregate

This commit isolates the `Work` aggregate into its own package at `internal/domain/work`, following the first step of the refactoring plan in `refactor.md`.

- The `Work` struct, related types, and the `WorkRepository` interface have been moved to the new package.
- A circular dependency between `domain` and `work` was resolved by moving the `AnalyticsRepository` to the `app` layer.
- All references to the moved types have been updated across the entire codebase to fix compilation errors.
- Test files, including mocks and integration tests, have been updated to reflect the new structure.
This commit is contained in:
google-labs-jules[bot] 2025-10-03 16:15:09 +00:00
parent c26d86ae80
commit 06e6e2be85
40 changed files with 440 additions and 411 deletions

View File

@ -17,6 +17,7 @@ import (
"tercul/internal/app/like"
"tercul/internal/app/translation"
"tercul/internal/domain"
"tercul/internal/domain/work"
platform_auth "tercul/internal/platform/auth"
"tercul/internal/testutil"
@ -966,8 +967,8 @@ func (s *GraphQLIntegrationSuite) TestTrendingWorksQuery() {
// Arrange
work1 := s.CreateTestWork("Work 1", "en", "content")
work2 := s.CreateTestWork("Work 2", "en", "content")
s.DB.Create(&domain.WorkStats{WorkID: work1.ID, Views: 100, Likes: 10, Comments: 1})
s.DB.Create(&domain.WorkStats{WorkID: work2.ID, Views: 10, Likes: 100, Comments: 10})
s.DB.Create(&work.WorkStats{WorkID: work1.ID, Views: 100, Likes: 10, Comments: 1})
s.DB.Create(&work.WorkStats{WorkID: work2.ID, Views: 10, Likes: 100, Comments: 10})
s.Require().NoError(s.App.Analytics.UpdateTrending(context.Background()))
// Act

View File

@ -17,6 +17,7 @@ import (
"tercul/internal/app/like"
"tercul/internal/app/translation"
"tercul/internal/domain"
"tercul/internal/domain/work"
platform_auth "tercul/internal/platform/auth"
)
@ -91,7 +92,7 @@ func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
}
// Create domain model
work := &domain.Work{
workModel := &work.Work{
Title: input.Name,
TranslatableModel: domain.TranslatableModel{Language: input.Language},
// Description: *input.Description,
@ -99,11 +100,10 @@ func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput
}
// Call work service
createdWork, err := r.App.Work.Commands.CreateWork(ctx, work)
createdWork, err := r.App.Work.Commands.CreateWork(ctx, workModel)
if err != nil {
return nil, err
}
work = createdWork
if input.Content != nil && *input.Content != "" {
translationInput := translation.CreateTranslationInput{
@ -140,7 +140,7 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
}
// Create domain model
work := &domain.Work{
workModel := &work.Work{
TranslatableModel: domain.TranslatableModel{
BaseModel: domain.BaseModel{ID: uint(workID)},
Language: input.Language,
@ -149,7 +149,7 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
}
// Call work service
err = r.App.Work.Commands.UpdateWork(ctx, work)
err = r.App.Work.Commands.UpdateWork(ctx, workModel)
if err != nil {
return nil, err
}
@ -157,8 +157,8 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
// Convert to GraphQL model
return &model.Work{
ID: id,
Name: work.Title,
Language: work.Language,
Name: workModel.Title,
Language: workModel.Language,
Content: input.Content,
}, nil
}
@ -929,20 +929,20 @@ func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error
return nil, fmt.Errorf("invalid work ID: %v", err)
}
work, err := r.App.Work.Queries.GetWorkByID(ctx, uint(workID))
workRecord, err := r.App.Work.Queries.GetWorkByID(ctx, uint(workID))
if err != nil {
return nil, err
}
if work == nil {
if workRecord == nil {
return nil, nil
}
content := r.resolveWorkContent(ctx, work.ID, work.Language)
content := r.resolveWorkContent(ctx, workRecord.ID, workRecord.Language)
return &model.Work{
ID: id,
Name: work.Title,
Language: work.Language,
Name: workRecord.Title,
Language: workRecord.Language,
Content: content,
}, nil
}
@ -1239,13 +1239,13 @@ func (r *queryResolver) TrendingWorks(ctx context.Context, timePeriod *string, l
l = int(*limit)
}
works, err := r.App.Analytics.GetTrendingWorks(ctx, tp, l)
workRecords, err := r.App.Analytics.GetTrendingWorks(ctx, tp, l)
if err != nil {
return nil, err
}
var result []*model.Work
for _, w := range works {
for _, w := range workRecords {
result = append(result, &model.Work{
ID: fmt.Sprintf("%d", w.ID),
Name: w.Title,

View File

@ -0,0 +1,22 @@
package analytics
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/work"
"time"
)
// AnalyticsRepository defines the data access layer for analytics.
type Repository interface {
IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error
IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error
UpdateWorkStats(ctx context.Context, workID uint, stats work.WorkStats) error
UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error
GetOrCreateWorkStats(ctx context.Context, workID uint) (*work.WorkStats, error)
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error)
GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*domain.UserEngagement, error)
UpdateUserEngagement(ctx context.Context, userEngagement *domain.UserEngagement) error
UpdateTrendingWorks(ctx context.Context, timePeriod string, trending []*domain.Trending) error
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*work.Work, error)
}

View File

@ -7,6 +7,7 @@ import (
"sort"
"strings"
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/jobs/linguistics"
"tercul/internal/platform/log"
"time"
@ -23,7 +24,7 @@ type Service interface {
IncrementTranslationLikes(ctx context.Context, translationID uint) error
IncrementTranslationComments(ctx context.Context, translationID uint) error
IncrementTranslationShares(ctx context.Context, translationID uint) error
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error)
GetOrCreateWorkStats(ctx context.Context, workID uint) (*work.WorkStats, error)
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error)
UpdateWorkReadingTime(ctx context.Context, workID uint) error
@ -34,18 +35,18 @@ type Service interface {
UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error
UpdateTrending(ctx context.Context) error
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error)
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*work.Work, error)
}
type service struct {
repo domain.AnalyticsRepository
repo Repository
analysisRepo linguistics.AnalysisRepository
translationRepo domain.TranslationRepository
workRepo domain.WorkRepository
workRepo work.WorkRepository
sentimentProvider linguistics.SentimentProvider
}
func NewService(repo domain.AnalyticsRepository, analysisRepo linguistics.AnalysisRepository, translationRepo domain.TranslationRepository, workRepo domain.WorkRepository, sentimentProvider linguistics.SentimentProvider) Service {
func NewService(repo Repository, analysisRepo linguistics.AnalysisRepository, translationRepo domain.TranslationRepository, workRepo work.WorkRepository, sentimentProvider linguistics.SentimentProvider) Service {
return &service{
repo: repo,
analysisRepo: analysisRepo,
@ -95,7 +96,7 @@ func (s *service) IncrementTranslationShares(ctx context.Context, translationID
return s.repo.IncrementTranslationCounter(ctx, translationID, "shares", 1)
}
func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*work.WorkStats, error) {
return s.repo.GetOrCreateWorkStats(ctx, workID)
}
@ -251,7 +252,7 @@ func (s *service) UpdateUserEngagement(ctx context.Context, userID uint, eventTy
return s.repo.UpdateUserEngagement(ctx, engagement)
}
func (s *service) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) {
func (s *service) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*work.Work, error) {
return s.repo.GetTrendingWorks(ctx, timePeriod, limit)
}
@ -264,10 +265,10 @@ func (s *service) UpdateTrending(ctx context.Context) error {
}
trendingWorks := make([]*domain.Trending, 0, len(works))
for _, work := range works {
stats, err := s.repo.GetOrCreateWorkStats(ctx, work.ID)
for _, aWork := range works {
stats, err := s.repo.GetOrCreateWorkStats(ctx, aWork.ID)
if err != nil {
log.LogWarn("failed to get work stats", log.F("workID", work.ID), log.F("error", err))
log.LogWarn("failed to get work stats", log.F("workID", aWork.ID), log.F("error", err))
continue
}
@ -275,7 +276,7 @@ func (s *service) UpdateTrending(ctx context.Context) error {
trendingWorks = append(trendingWorks, &domain.Trending{
EntityType: "Work",
EntityID: work.ID,
EntityID: aWork.ID,
Score: score,
TimePeriod: "daily", // Hardcoded for now
Date: time.Now().UTC(),

View File

@ -7,6 +7,7 @@ import (
"tercul/internal/app/analytics"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/jobs/linguistics"
"tercul/internal/testutil"
@ -239,8 +240,8 @@ func (s *AnalyticsServiceTestSuite) TestUpdateTrending() {
// Arrange
work1 := s.CreateTestWork("Work 1", "en", "content")
work2 := s.CreateTestWork("Work 2", "en", "content")
s.DB.Create(&domain.WorkStats{WorkID: work1.ID, Views: 100, Likes: 10, Comments: 1})
s.DB.Create(&domain.WorkStats{WorkID: work2.ID, Views: 10, Likes: 100, Comments: 10})
s.DB.Create(&work.WorkStats{WorkID: work1.ID, Views: 100, Likes: 10, Comments: 1})
s.DB.Create(&work.WorkStats{WorkID: work2.ID, Views: 10, Likes: 100, Comments: 10})
// Act
err := s.service.UpdateTrending(context.Background())
@ -257,4 +258,4 @@ func (s *AnalyticsServiceTestSuite) TestUpdateTrending() {
func TestAnalyticsService(t *testing.T) {
suite.Run(t, new(AnalyticsServiceTestSuite))
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/work"
)
type mockCopyrightRepository struct {
@ -172,10 +173,11 @@ func (m *mockCopyrightRepository) WithTx(ctx context.Context, fn func(tx *gorm.D
}
type mockWorkRepository struct {
domain.WorkRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error)
work.WorkRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*work.Work, error)
}
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*work.Work, error) {
if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options)
}

View File

@ -4,13 +4,14 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/platform/log"
)
// CopyrightQueries contains the query handlers for copyright.
type CopyrightQueries struct {
repo domain.CopyrightRepository
workRepo domain.WorkRepository
workRepo work.WorkRepository
authorRepo domain.AuthorRepository
bookRepo domain.BookRepository
publisherRepo domain.PublisherRepository
@ -18,7 +19,7 @@ type CopyrightQueries struct {
}
// NewCopyrightQueries creates a new CopyrightQueries handler.
func NewCopyrightQueries(repo domain.CopyrightRepository, workRepo domain.WorkRepository, authorRepo domain.AuthorRepository, bookRepo domain.BookRepository, publisherRepo domain.PublisherRepository, sourceRepo domain.SourceRepository) *CopyrightQueries {
func NewCopyrightQueries(repo domain.CopyrightRepository, workRepo work.WorkRepository, authorRepo domain.AuthorRepository, bookRepo domain.BookRepository, publisherRepo domain.PublisherRepository, sourceRepo domain.SourceRepository) *CopyrightQueries {
return &CopyrightQueries{repo: repo, workRepo: workRepo, authorRepo: authorRepo, bookRepo: bookRepo, publisherRepo: publisherRepo, sourceRepo: sourceRepo}
}
@ -42,11 +43,11 @@ func (q *CopyrightQueries) ListCopyrights(ctx context.Context) ([]domain.Copyrig
// GetCopyrightsForWork gets all copyrights for a specific work.
func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint) ([]*domain.Copyright, error) {
log.LogDebug("Getting copyrights for work", log.F("work_id", workID))
work, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
if err != nil {
return nil, err
}
return work.Copyrights, nil
return workRecord.Copyrights, nil
}
// GetCopyrightsForAuthor gets all copyrights for a specific author.

View File

@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"tercul/internal/domain"
"tercul/internal/domain/work"
"testing"
)
@ -99,8 +100,8 @@ func (s *CopyrightQueriesSuite) TestGetCopyrightsForSource_RepoError() {
func (s *CopyrightQueriesSuite) TestGetCopyrightsForWork_Success() {
copyrights := []*domain.Copyright{{Name: "Test Copyright"}}
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
return &domain.Work{Copyrights: copyrights}, nil
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*work.Work, error) {
return &work.Work{Copyrights: copyrights}, nil
}
c, err := s.queries.GetCopyrightsForWork(context.Background(), 1)
assert.NoError(s.T(), err)
@ -108,7 +109,7 @@ func (s *CopyrightQueriesSuite) TestGetCopyrightsForWork_Success() {
}
func (s *CopyrightQueriesSuite) TestGetCopyrightsForWork_RepoError() {
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*work.Work, error) {
return nil, errors.New("db error")
}
c, err := s.queries.GetCopyrightsForWork(context.Background(), 1)

View File

@ -3,6 +3,7 @@ package monetization
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/work"
)
type mockMonetizationRepository struct {
@ -97,10 +98,11 @@ func (m *mockMonetizationRepository) RemoveMonetizationFromSource(ctx context.Co
}
type mockWorkRepository struct {
domain.WorkRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error)
work.WorkRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*work.Work, error)
}
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*work.Work, error) {
if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options)
}

View File

@ -4,13 +4,14 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/platform/log"
)
// MonetizationQueries contains the query handlers for monetization.
type MonetizationQueries struct {
repo domain.MonetizationRepository
workRepo domain.WorkRepository
workRepo work.WorkRepository
authorRepo domain.AuthorRepository
bookRepo domain.BookRepository
publisherRepo domain.PublisherRepository
@ -18,7 +19,7 @@ type MonetizationQueries struct {
}
// NewMonetizationQueries creates a new MonetizationQueries handler.
func NewMonetizationQueries(repo domain.MonetizationRepository, workRepo domain.WorkRepository, authorRepo domain.AuthorRepository, bookRepo domain.BookRepository, publisherRepo domain.PublisherRepository, sourceRepo domain.SourceRepository) *MonetizationQueries {
func NewMonetizationQueries(repo domain.MonetizationRepository, workRepo work.WorkRepository, authorRepo domain.AuthorRepository, bookRepo domain.BookRepository, publisherRepo domain.PublisherRepository, sourceRepo domain.SourceRepository) *MonetizationQueries {
return &MonetizationQueries{repo: repo, workRepo: workRepo, authorRepo: authorRepo, bookRepo: bookRepo, publisherRepo: publisherRepo, sourceRepo: sourceRepo}
}
@ -39,11 +40,11 @@ func (q *MonetizationQueries) ListMonetizations(ctx context.Context) ([]domain.M
func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workID uint) ([]*domain.Monetization, error) {
log.LogDebug("Getting monetizations for work", log.F("work_id", workID))
work, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
if err != nil {
return nil, err
}
return work.Monetizations, nil
return workRecord.Monetizations, nil
}
func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, authorID uint) ([]*domain.Monetization, error) {

View File

@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"tercul/internal/domain"
"tercul/internal/domain/work"
"testing"
)
@ -81,8 +82,8 @@ func (s *MonetizationQueriesSuite) TestListMonetizations_Success() {
func (s *MonetizationQueriesSuite) TestGetMonetizationsForWork_Success() {
monetizations := []*domain.Monetization{{Amount: 10.0}}
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
return &domain.Work{Monetizations: monetizations}, nil
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*work.Work, error) {
return &work.Work{Monetizations: monetizations}, nil
}
m, err := s.queries.GetMonetizationsForWork(context.Background(), 1)
assert.NoError(s.T(), err)
@ -90,7 +91,7 @@ func (s *MonetizationQueriesSuite) TestGetMonetizationsForWork_Success() {
}
func (s *MonetizationQueriesSuite) TestGetMonetizationsForWork_RepoError() {
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*work.Work, error) {
return nil, errors.New("db error")
}
m, err := s.queries.GetMonetizationsForWork(context.Background(), 1)

View File

@ -4,14 +4,14 @@ import (
"context"
"fmt"
"tercul/internal/app/localization"
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/platform/log"
"tercul/internal/platform/search"
)
// IndexService pushes localized snapshots into Weaviate for search
type IndexService interface {
IndexWork(ctx context.Context, work domain.Work) error
IndexWork(ctx context.Context, work work.Work) error
}
type indexService struct {
@ -23,7 +23,7 @@ func NewIndexService(localization *localization.Service, weaviate search.Weaviat
return &indexService{localization: localization, weaviate: weaviate}
}
func (s *indexService) IndexWork(ctx context.Context, work domain.Work) error {
func (s *indexService) IndexWork(ctx context.Context, work work.Work) error {
log.LogDebug("Indexing work", log.F("work_id", work.ID))
// TODO: Get content from translation service
content := ""

View File

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/mock"
"tercul/internal/app/localization"
"tercul/internal/domain"
"tercul/internal/domain/work"
)
type mockLocalizationRepository struct {
@ -36,7 +37,7 @@ type mockWeaviateWrapper struct {
mock.Mock
}
func (m *mockWeaviateWrapper) IndexWork(ctx context.Context, work *domain.Work, content string) error {
func (m *mockWeaviateWrapper) IndexWork(ctx context.Context, work *work.Work, content string) error {
args := m.Called(ctx, work, content)
return args.Error(0)
}
@ -48,7 +49,7 @@ func TestIndexService_IndexWork(t *testing.T) {
service := NewIndexService(localizationService, weaviateWrapper)
ctx := context.Background()
work := domain.Work{
work := work.Work{
TranslatableModel: domain.TranslatableModel{
BaseModel: domain.BaseModel{ID: 1},
Language: "en",

View File

@ -3,18 +3,18 @@ package work
import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/domain/search"
"tercul/internal/domain/work"
)
// WorkCommands contains the command handlers for the work aggregate.
type WorkCommands struct {
repo domain.WorkRepository
repo work.WorkRepository
searchClient search.SearchClient
}
// NewWorkCommands creates a new WorkCommands handler.
func NewWorkCommands(repo domain.WorkRepository, searchClient search.SearchClient) *WorkCommands {
func NewWorkCommands(repo work.WorkRepository, searchClient search.SearchClient) *WorkCommands {
return &WorkCommands{
repo: repo,
searchClient: searchClient,
@ -22,7 +22,7 @@ func NewWorkCommands(repo domain.WorkRepository, searchClient search.SearchClien
}
// CreateWork creates a new work.
func (c *WorkCommands) CreateWork(ctx context.Context, work *domain.Work) (*domain.Work, error) {
func (c *WorkCommands) CreateWork(ctx context.Context, work *work.Work) (*work.Work, error) {
if work == nil {
return nil, errors.New("work cannot be nil")
}
@ -45,7 +45,7 @@ func (c *WorkCommands) CreateWork(ctx context.Context, work *domain.Work) (*doma
}
// UpdateWork updates an existing work.
func (c *WorkCommands) UpdateWork(ctx context.Context, work *domain.Work) error {
func (c *WorkCommands) UpdateWork(ctx context.Context, work *work.Work) error {
if work == nil {
return errors.New("work cannot be nil")
}

View File

@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"tercul/internal/domain"
workdomain "tercul/internal/domain/work"
"testing"
)
@ -27,7 +28,7 @@ func TestWorkCommandsSuite(t *testing.T) {
}
func (s *WorkCommandsSuite) TestCreateWork_Success() {
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
work := &workdomain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
_, err := s.commands.CreateWork(context.Background(), work)
assert.NoError(s.T(), err)
}
@ -38,20 +39,20 @@ func (s *WorkCommandsSuite) TestCreateWork_Nil() {
}
func (s *WorkCommandsSuite) TestCreateWork_EmptyTitle() {
work := &domain.Work{TranslatableModel: domain.TranslatableModel{Language: "en"}}
work := &workdomain.Work{TranslatableModel: domain.TranslatableModel{Language: "en"}}
_, err := s.commands.CreateWork(context.Background(), work)
assert.Error(s.T(), err)
}
func (s *WorkCommandsSuite) TestCreateWork_EmptyLanguage() {
work := &domain.Work{Title: "Test Work"}
work := &workdomain.Work{Title: "Test Work"}
_, err := s.commands.CreateWork(context.Background(), work)
assert.Error(s.T(), err)
}
func (s *WorkCommandsSuite) TestCreateWork_RepoError() {
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
s.repo.createFunc = func(ctx context.Context, w *domain.Work) error {
work := &workdomain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
s.repo.createFunc = func(ctx context.Context, w *workdomain.Work) error {
return errors.New("db error")
}
_, err := s.commands.CreateWork(context.Background(), work)
@ -59,7 +60,7 @@ func (s *WorkCommandsSuite) TestCreateWork_RepoError() {
}
func (s *WorkCommandsSuite) TestUpdateWork_Success() {
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
work := &workdomain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
work.ID = 1
err := s.commands.UpdateWork(context.Background(), work)
assert.NoError(s.T(), err)
@ -71,29 +72,29 @@ func (s *WorkCommandsSuite) TestUpdateWork_Nil() {
}
func (s *WorkCommandsSuite) TestUpdateWork_ZeroID() {
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
work := &workdomain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
err := s.commands.UpdateWork(context.Background(), work)
assert.Error(s.T(), err)
}
func (s *WorkCommandsSuite) TestUpdateWork_EmptyTitle() {
work := &domain.Work{TranslatableModel: domain.TranslatableModel{Language: "en"}}
work := &workdomain.Work{TranslatableModel: domain.TranslatableModel{Language: "en"}}
work.ID = 1
err := s.commands.UpdateWork(context.Background(), work)
assert.Error(s.T(), err)
}
func (s *WorkCommandsSuite) TestUpdateWork_EmptyLanguage() {
work := &domain.Work{Title: "Test Work"}
work := &workdomain.Work{Title: "Test Work"}
work.ID = 1
err := s.commands.UpdateWork(context.Background(), work)
assert.Error(s.T(), err)
}
func (s *WorkCommandsSuite) TestUpdateWork_RepoError() {
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
work := &workdomain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
work.ID = 1
s.repo.updateFunc = func(ctx context.Context, w *domain.Work) error {
s.repo.updateFunc = func(ctx context.Context, w *workdomain.Work) error {
return errors.New("db error")
}
err := s.commands.UpdateWork(context.Background(), work)

View File

@ -3,29 +3,30 @@ package work
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/work"
)
type mockWorkRepository struct {
domain.WorkRepository
createFunc func(ctx context.Context, work *domain.Work) error
updateFunc func(ctx context.Context, work *domain.Work) error
deleteFunc func(ctx context.Context, id uint) error
getByIDFunc func(ctx context.Context, id uint) (*domain.Work, error)
listFunc func(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error)
getWithTranslationsFunc func(ctx context.Context, id uint) (*domain.Work, error)
findByTitleFunc func(ctx context.Context, title string) ([]domain.Work, error)
findByAuthorFunc func(ctx context.Context, authorID uint) ([]domain.Work, error)
findByCategoryFunc func(ctx context.Context, categoryID uint) ([]domain.Work, error)
findByLanguageFunc func(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error)
work.WorkRepository
createFunc func(ctx context.Context, work *work.Work) error
updateFunc func(ctx context.Context, work *work.Work) error
deleteFunc func(ctx context.Context, id uint) error
getByIDFunc func(ctx context.Context, id uint) (*work.Work, error)
listFunc func(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[work.Work], error)
getWithTranslationsFunc func(ctx context.Context, id uint) (*work.Work, error)
findByTitleFunc func(ctx context.Context, title string) ([]work.Work, error)
findByAuthorFunc func(ctx context.Context, authorID uint) ([]work.Work, error)
findByCategoryFunc func(ctx context.Context, categoryID uint) ([]work.Work, error)
findByLanguageFunc func(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[work.Work], error)
}
func (m *mockWorkRepository) Create(ctx context.Context, work *domain.Work) error {
func (m *mockWorkRepository) Create(ctx context.Context, work *work.Work) error {
if m.createFunc != nil {
return m.createFunc(ctx, work)
}
return nil
}
func (m *mockWorkRepository) Update(ctx context.Context, work *domain.Work) error {
func (m *mockWorkRepository) Update(ctx context.Context, work *work.Work) error {
if m.updateFunc != nil {
return m.updateFunc(ctx, work)
}
@ -37,43 +38,43 @@ func (m *mockWorkRepository) Delete(ctx context.Context, id uint) error {
}
return nil
}
func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) {
func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*work.Work, error) {
if m.getByIDFunc != nil {
return m.getByIDFunc(ctx, id)
}
return nil, nil
}
func (m *mockWorkRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
func (m *mockWorkRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
if m.listFunc != nil {
return m.listFunc(ctx, page, pageSize)
}
return nil, nil
}
func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*work.Work, error) {
if m.getWithTranslationsFunc != nil {
return m.getWithTranslationsFunc(ctx, id)
}
return nil, nil
}
func (m *mockWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) {
func (m *mockWorkRepository) FindByTitle(ctx context.Context, title string) ([]work.Work, error) {
if m.findByTitleFunc != nil {
return m.findByTitleFunc(ctx, title)
}
return nil, nil
}
func (m *mockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
func (m *mockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]work.Work, error) {
if m.findByAuthorFunc != nil {
return m.findByAuthorFunc(ctx, authorID)
}
return nil, nil
}
func (m *mockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
func (m *mockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]work.Work, error) {
if m.findByCategoryFunc != nil {
return m.findByCategoryFunc(ctx, categoryID)
}
return nil, nil
}
func (m *mockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
func (m *mockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
if m.findByLanguageFunc != nil {
return m.findByLanguageFunc(ctx, language, page, pageSize)
}
@ -81,12 +82,12 @@ func (m *mockWorkRepository) FindByLanguage(ctx context.Context, language string
}
type mockSearchClient struct {
indexWorkFunc func(ctx context.Context, work *domain.Work, pipeline string) error
indexWorkFunc func(ctx context.Context, work *work.Work, pipeline string) error
}
func (m *mockSearchClient) IndexWork(ctx context.Context, work *domain.Work, pipeline string) error {
func (m *mockSearchClient) IndexWork(ctx context.Context, work *work.Work, pipeline string) error {
if m.indexWorkFunc != nil {
return m.indexWorkFunc(ctx, work, pipeline)
}
return nil
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/domain/work"
)
// WorkAnalytics contains analytics data for a work
@ -30,18 +31,18 @@ type TranslationAnalytics struct {
// WorkQueries contains the query handlers for the work aggregate.
type WorkQueries struct {
repo domain.WorkRepository
repo work.WorkRepository
}
// NewWorkQueries creates a new WorkQueries handler.
func NewWorkQueries(repo domain.WorkRepository) *WorkQueries {
func NewWorkQueries(repo work.WorkRepository) *WorkQueries {
return &WorkQueries{
repo: repo,
}
}
// GetWorkByID retrieves a work by ID.
func (q *WorkQueries) GetWorkByID(ctx context.Context, id uint) (*domain.Work, error) {
func (q *WorkQueries) GetWorkByID(ctx context.Context, id uint) (*work.Work, error) {
if id == 0 {
return nil, errors.New("invalid work ID")
}
@ -49,12 +50,12 @@ func (q *WorkQueries) GetWorkByID(ctx context.Context, id uint) (*domain.Work, e
}
// ListWorks returns a paginated list of works.
func (q *WorkQueries) ListWorks(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
func (q *WorkQueries) ListWorks(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
return q.repo.List(ctx, page, pageSize)
}
// GetWorkWithTranslations retrieves a work with its translations.
func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uint) (*work.Work, error) {
if id == 0 {
return nil, errors.New("invalid work ID")
}
@ -62,7 +63,7 @@ func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uint) (*do
}
// FindWorksByTitle finds works by title.
func (q *WorkQueries) FindWorksByTitle(ctx context.Context, title string) ([]domain.Work, error) {
func (q *WorkQueries) FindWorksByTitle(ctx context.Context, title string) ([]work.Work, error) {
if title == "" {
return nil, errors.New("title cannot be empty")
}
@ -70,7 +71,7 @@ func (q *WorkQueries) FindWorksByTitle(ctx context.Context, title string) ([]dom
}
// FindWorksByAuthor finds works by author ID.
func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uint) ([]work.Work, error) {
if authorID == 0 {
return nil, errors.New("invalid author ID")
}
@ -78,7 +79,7 @@ func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uint) ([]d
}
// FindWorksByCategory finds works by category ID.
func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uint) ([]work.Work, error) {
if categoryID == 0 {
return nil, errors.New("invalid category ID")
}
@ -86,7 +87,7 @@ func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uint)
}
// FindWorksByLanguage finds works by language.
func (q *WorkQueries) FindWorksByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
func (q *WorkQueries) FindWorksByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
if language == "" {
return nil, errors.New("language cannot be empty")
}

View File

@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"tercul/internal/domain"
workdomain "tercul/internal/domain/work"
"testing"
)
@ -24,9 +25,9 @@ func TestWorkQueriesSuite(t *testing.T) {
}
func (s *WorkQueriesSuite) TestGetWorkByID_Success() {
work := &domain.Work{Title: "Test Work"}
work := &workdomain.Work{Title: "Test Work"}
work.ID = 1
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Work, error) {
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*workdomain.Work, error) {
return work, nil
}
w, err := s.queries.GetWorkByID(context.Background(), 1)
@ -41,8 +42,8 @@ func (s *WorkQueriesSuite) TestGetWorkByID_ZeroID() {
}
func (s *WorkQueriesSuite) TestListWorks_Success() {
works := &domain.PaginatedResult[domain.Work]{}
s.repo.listFunc = func(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
works := &domain.PaginatedResult[workdomain.Work]{}
s.repo.listFunc = func(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[workdomain.Work], error) {
return works, nil
}
w, err := s.queries.ListWorks(context.Background(), 1, 10)
@ -51,9 +52,9 @@ func (s *WorkQueriesSuite) TestListWorks_Success() {
}
func (s *WorkQueriesSuite) TestGetWorkWithTranslations_Success() {
work := &domain.Work{Title: "Test Work"}
work := &workdomain.Work{Title: "Test Work"}
work.ID = 1
s.repo.getWithTranslationsFunc = func(ctx context.Context, id uint) (*domain.Work, error) {
s.repo.getWithTranslationsFunc = func(ctx context.Context, id uint) (*workdomain.Work, error) {
return work, nil
}
w, err := s.queries.GetWorkWithTranslations(context.Background(), 1)
@ -68,8 +69,8 @@ func (s *WorkQueriesSuite) TestGetWorkWithTranslations_ZeroID() {
}
func (s *WorkQueriesSuite) TestFindWorksByTitle_Success() {
works := []domain.Work{{Title: "Test Work"}}
s.repo.findByTitleFunc = func(ctx context.Context, title string) ([]domain.Work, error) {
works := []workdomain.Work{{Title: "Test Work"}}
s.repo.findByTitleFunc = func(ctx context.Context, title string) ([]workdomain.Work, error) {
return works, nil
}
w, err := s.queries.FindWorksByTitle(context.Background(), "Test")
@ -84,8 +85,8 @@ func (s *WorkQueriesSuite) TestFindWorksByTitle_Empty() {
}
func (s *WorkQueriesSuite) TestFindWorksByAuthor_Success() {
works := []domain.Work{{Title: "Test Work"}}
s.repo.findByAuthorFunc = func(ctx context.Context, authorID uint) ([]domain.Work, error) {
works := []workdomain.Work{{Title: "Test Work"}}
s.repo.findByAuthorFunc = func(ctx context.Context, authorID uint) ([]workdomain.Work, error) {
return works, nil
}
w, err := s.queries.FindWorksByAuthor(context.Background(), 1)
@ -100,8 +101,8 @@ func (s *WorkQueriesSuite) TestFindWorksByAuthor_ZeroID() {
}
func (s *WorkQueriesSuite) TestFindWorksByCategory_Success() {
works := []domain.Work{{Title: "Test Work"}}
s.repo.findByCategoryFunc = func(ctx context.Context, categoryID uint) ([]domain.Work, error) {
works := []workdomain.Work{{Title: "Test Work"}}
s.repo.findByCategoryFunc = func(ctx context.Context, categoryID uint) ([]workdomain.Work, error) {
return works, nil
}
w, err := s.queries.FindWorksByCategory(context.Background(), 1)
@ -116,8 +117,8 @@ func (s *WorkQueriesSuite) TestFindWorksByCategory_ZeroID() {
}
func (s *WorkQueriesSuite) TestFindWorksByLanguage_Success() {
works := &domain.PaginatedResult[domain.Work]{}
s.repo.findByLanguageFunc = func(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
works := &domain.PaginatedResult[workdomain.Work]{}
s.repo.findByLanguageFunc = func(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[workdomain.Work], error) {
return works, nil
}
w, err := s.queries.FindWorksByLanguage(context.Background(), "en", 1, 10)
@ -129,4 +130,4 @@ func (s *WorkQueriesSuite) TestFindWorksByLanguage_Empty() {
w, err := s.queries.FindWorksByLanguage(context.Background(), "", 1, 10)
assert.Error(s.T(), err)
assert.Nil(s.T(), w)
}
}

View File

@ -1,8 +1,8 @@
package work
import (
"tercul/internal/domain"
"tercul/internal/domain/search"
"tercul/internal/domain/work"
)
// Service is the application service for the work aggregate.
@ -12,7 +12,7 @@ type Service struct {
}
// NewService creates a new work Service.
func NewService(repo domain.WorkRepository, searchClient search.SearchClient) *Service {
func NewService(repo work.WorkRepository, searchClient search.SearchClient) *Service {
return &Service{
Commands: NewWorkCommands(repo, searchClient),
Queries: NewWorkQueries(repo),

View File

@ -3,7 +3,9 @@ package sql
import (
"context"
"fmt"
"tercul/internal/app/analytics"
"tercul/internal/domain"
"tercul/internal/domain/work"
"time"
"gorm.io/gorm"
@ -13,7 +15,7 @@ type analyticsRepository struct {
db *gorm.DB
}
func NewAnalyticsRepository(db *gorm.DB) domain.AnalyticsRepository {
func NewAnalyticsRepository(db *gorm.DB) analytics.Repository {
return &analyticsRepository{db: db}
}
@ -41,7 +43,7 @@ func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID u
// Using a transaction to ensure atomicity
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// First, try to update the existing record
result := tx.Model(&domain.WorkStats{}).Where("work_id = ?", workID).UpdateColumn(field, gorm.Expr(fmt.Sprintf("%s + ?", field), value))
result := tx.Model(&work.WorkStats{}).Where("work_id = ?", workID).UpdateColumn(field, gorm.Expr(fmt.Sprintf("%s + ?", field), value))
if result.Error != nil {
return result.Error
}
@ -49,14 +51,14 @@ func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID u
// If no rows were affected, the record does not exist, so create it
if result.RowsAffected == 0 {
initialData := map[string]interface{}{"work_id": workID, field: value}
return tx.Model(&domain.WorkStats{}).Create(initialData).Error
return tx.Model(&work.WorkStats{}).Create(initialData).Error
}
return nil
})
}
func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) {
func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*work.Work, error) {
var trendingWorks []*domain.Trending
err := r.db.WithContext(ctx).
Where("entity_type = ? AND time_period = ?", "Work", timePeriod).
@ -68,7 +70,7 @@ func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod s
}
if len(trendingWorks) == 0 {
return []*domain.Work{}, nil
return []*work.Work{}, nil
}
workIDs := make([]uint, len(trendingWorks))
@ -76,22 +78,22 @@ func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod s
workIDs[i] = tw.EntityID
}
var works []*domain.Work
var works []*work.Work
err = r.db.WithContext(ctx).
Where("id IN ?", workIDs).
Find(&works).Error
// This part is tricky because the order from the IN clause is not guaranteed.
// We need to re-order the works based on the trending rank.
workMap := make(map[uint]*domain.Work)
for _, work := range works {
workMap[work.ID] = work
workMap := make(map[uint]*work.Work)
for _, w := range works {
workMap[w.ID] = w
}
orderedWorks := make([]*domain.Work, len(workIDs))
orderedWorks := make([]*work.Work, len(workIDs))
for i, id := range workIDs {
if work, ok := workMap[id]; ok {
orderedWorks[i] = work
if w, ok := workMap[id]; ok {
orderedWorks[i] = w
}
}
@ -118,17 +120,17 @@ func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, t
})
}
func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).Updates(stats).Error
func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uint, stats work.WorkStats) error {
return r.db.WithContext(ctx).Model(&work.WorkStats{}).Where("work_id = ?", workID).Updates(stats).Error
}
func (r *analyticsRepository) UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error {
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).Updates(stats).Error
}
func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
var stats domain.WorkStats
err := r.db.WithContext(ctx).Where(domain.WorkStats{WorkID: workID}).FirstOrCreate(&stats).Error
func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uint) (*work.WorkStats, error) {
var stats work.WorkStats
err := r.db.WithContext(ctx).Where(work.WorkStats{WorkID: workID}).FirstOrCreate(&stats).Error
return &stats, err
}

View File

@ -3,6 +3,7 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/work"
"gorm.io/gorm"
)
@ -21,15 +22,15 @@ func NewMonetizationRepository(db *gorm.DB) domain.MonetizationRepository {
}
func (r *monetizationRepository) AddMonetizationToWork(ctx context.Context, workID uint, monetizationID uint) error {
work := &domain.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}}}
workRecord := &work.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}}}
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
return r.db.WithContext(ctx).Model(work).Association("Monetizations").Append(monetization)
return r.db.WithContext(ctx).Model(workRecord).Association("Monetizations").Append(monetization)
}
func (r *monetizationRepository) RemoveMonetizationFromWork(ctx context.Context, workID uint, monetizationID uint) error {
work := &domain.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}}}
workRecord := &work.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}}}
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
return r.db.WithContext(ctx).Model(work).Association("Monetizations").Delete(monetization)
return r.db.WithContext(ctx).Model(workRecord).Association("Monetizations").Delete(monetization)
}
func (r *monetizationRepository) AddMonetizationToAuthor(ctx context.Context, authorID uint, monetizationID uint) error {

View File

@ -5,6 +5,7 @@ import (
"testing"
"tercul/internal/data/sql"
"tercul/internal/domain"
workdomain "tercul/internal/domain/work"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
@ -40,7 +41,7 @@ func (s *MonetizationRepositoryTestSuite) TestAddMonetizationToWork() {
s.Require().NoError(err)
// Verify that the association was created in the database
var foundWork domain.Work
var foundWork workdomain.Work
err = s.DB.Preload("Monetizations").First(&foundWork, work.ID).Error
s.Require().NoError(err)
s.Require().Len(foundWork.Monetizations, 1)
@ -50,4 +51,4 @@ func (s *MonetizationRepositoryTestSuite) TestAddMonetizationToWork() {
func TestMonetizationRepository(t *testing.T) {
suite.Run(t, new(MonetizationRepositoryTestSuite))
}
}

View File

@ -1,15 +1,17 @@
package sql
import (
"tercul/internal/app/analytics"
"tercul/internal/domain"
"tercul/internal/domain/auth"
"tercul/internal/domain/localization"
"tercul/internal/domain/work"
"gorm.io/gorm"
)
type Repositories struct {
Work domain.WorkRepository
Work work.WorkRepository
User domain.UserRepository
Author domain.AuthorRepository
Translation domain.TranslationRepository
@ -24,7 +26,7 @@ type Repositories struct {
Source domain.SourceRepository
Copyright domain.CopyrightRepository
Monetization domain.MonetizationRepository
Analytics domain.AnalyticsRepository
Analytics analytics.Repository
Auth auth.AuthRepository
Localization localization.LocalizationRepository
}

View File

@ -3,26 +3,27 @@ package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/work"
"gorm.io/gorm"
)
type workRepository struct {
domain.BaseRepository[domain.Work]
domain.BaseRepository[work.Work]
db *gorm.DB
}
// NewWorkRepository creates a new WorkRepository.
func NewWorkRepository(db *gorm.DB) domain.WorkRepository {
func NewWorkRepository(db *gorm.DB) work.WorkRepository {
return &workRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Work](db),
BaseRepository: NewBaseRepositoryImpl[work.Work](db),
db: db,
}
}
// FindByTitle finds works by title (partial match)
func (r *workRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) {
var works []domain.Work
func (r *workRepository) FindByTitle(ctx context.Context, title string) ([]work.Work, error) {
var works []work.Work
if err := r.db.WithContext(ctx).Where("title LIKE ?", "%"+title+"%").Find(&works).Error; err != nil {
return nil, err
}
@ -30,8 +31,8 @@ func (r *workRepository) FindByTitle(ctx context.Context, title string) ([]domai
}
// FindByAuthor finds works by author ID
func (r *workRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
var works []domain.Work
func (r *workRepository) FindByAuthor(ctx context.Context, authorID uint) ([]work.Work, error) {
var works []work.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 {
@ -41,8 +42,8 @@ func (r *workRepository) FindByAuthor(ctx context.Context, authorID uint) ([]dom
}
// FindByCategory finds works by category ID
func (r *workRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
var works []domain.Work
func (r *workRepository) FindByCategory(ctx context.Context, categoryID uint) ([]work.Work, error) {
var works []work.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 {
@ -52,7 +53,7 @@ func (r *workRepository) FindByCategory(ctx context.Context, categoryID uint) ([
}
// FindByLanguage finds works by language with pagination
func (r *workRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
func (r *workRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
if page < 1 {
page = 1
}
@ -61,11 +62,11 @@ func (r *workRepository) FindByLanguage(ctx context.Context, language string, pa
pageSize = 20
}
var works []domain.Work
var works []work.Work
var totalCount int64
// Get total count
if err := r.db.WithContext(ctx).Model(&domain.Work{}).Where("language = ?", language).Count(&totalCount).Error; err != nil {
if err := r.db.WithContext(ctx).Model(&work.Work{}).Where("language = ?", language).Count(&totalCount).Error; err != nil {
return nil, err
}
@ -88,7 +89,7 @@ func (r *workRepository) FindByLanguage(ctx context.Context, language string, pa
hasNext := page < totalPages
hasPrev := page > 1
return &domain.PaginatedResult[domain.Work]{
return &domain.PaginatedResult[work.Work]{
Items: works,
TotalCount: totalCount,
Page: page,
@ -99,23 +100,15 @@ func (r *workRepository) FindByLanguage(ctx context.Context, language string, pa
}, nil
}
// Delete removes a work and its associations
func (r *workRepository) Delete(ctx context.Context, id uint) error {
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 {
if err := tx.Select("Copyrights", "Monetizations", "Authors", "Tags", "Categories").Delete(&work.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 {
if err := tx.Delete(&work.Work{}, id).Error; err != nil {
return err
}
return nil
@ -123,12 +116,12 @@ func (r *workRepository) Delete(ctx context.Context, id uint) error {
}
// GetWithTranslations gets a work with its translations
func (r *workRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
func (r *workRepository) GetWithTranslations(ctx context.Context, id uint) (*work.Work, error) {
return r.FindWithPreload(ctx, []string{"Translations"}, id)
}
// ListWithTranslations lists works with their translations
func (r *workRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
func (r *workRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
if page < 1 {
page = 1
}
@ -137,11 +130,11 @@ func (r *workRepository) ListWithTranslations(ctx context.Context, page, pageSiz
pageSize = 20
}
var works []domain.Work
var works []work.Work
var totalCount int64
// Get total count
if err := r.db.WithContext(ctx).Model(&domain.Work{}).Count(&totalCount).Error; err != nil {
if err := r.db.WithContext(ctx).Model(&work.Work{}).Count(&totalCount).Error; err != nil {
return nil, err
}
@ -164,7 +157,7 @@ func (r *workRepository) ListWithTranslations(ctx context.Context, page, pageSiz
hasNext := page < totalPages
hasPrev := page > 1
return &domain.PaginatedResult[domain.Work]{
return &domain.PaginatedResult[work.Work]{
Items: works,
TotalCount: totalCount,
Page: page,

View File

@ -5,6 +5,7 @@ import (
"testing"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/domain/work"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
@ -12,7 +13,7 @@ import (
type WorkRepositoryTestSuite struct {
testutil.IntegrationTestSuite
WorkRepo domain.WorkRepository
WorkRepo work.WorkRepository
}
func (s *WorkRepositoryTestSuite) SetupSuite() {
@ -29,7 +30,7 @@ func (s *WorkRepositoryTestSuite) TestCreateWork() {
}
s.Require().NoError(s.DB.Create(copyright).Error)
work := &domain.Work{
workModel := &work.Work{
Title: "New Test Work",
TranslatableModel: domain.TranslatableModel{
Language: "en",
@ -38,15 +39,15 @@ func (s *WorkRepositoryTestSuite) TestCreateWork() {
}
// Act
err := s.WorkRepo.Create(context.Background(), work)
err := s.WorkRepo.Create(context.Background(), workModel)
// Assert
s.Require().NoError(err)
s.NotZero(work.ID)
s.NotZero(workModel.ID)
// Verify that the work was actually created in the database
var foundWork domain.Work
err = s.DB.Preload("Copyrights").First(&foundWork, work.ID).Error
var foundWork work.Work
err = s.DB.Preload("Copyrights").First(&foundWork, workModel.ID).Error
s.Require().NoError(err)
s.Equal("New Test Work", foundWork.Title)
s.Equal("en", foundWork.Language)
@ -64,16 +65,16 @@ func (s *WorkRepositoryTestSuite) TestGetWorkByID() {
}
s.Require().NoError(s.DB.Create(copyright).Error)
work := s.CreateTestWork("Test Work", "en", "Test content")
s.Require().NoError(s.DB.Model(work).Association("Copyrights").Append(copyright))
workModel := s.CreateTestWork("Test Work", "en", "Test content")
s.Require().NoError(s.DB.Model(workModel).Association("Copyrights").Append(copyright))
// Act
foundWork, err := s.WorkRepo.GetByID(context.Background(), work.ID)
foundWork, err := s.WorkRepo.GetByID(context.Background(), workModel.ID)
// Assert
s.Require().NoError(err)
s.Require().NotNil(foundWork)
s.Equal(work.ID, foundWork.ID)
s.Equal(workModel.ID, foundWork.ID)
s.Equal("Test Work", foundWork.Title)
})
@ -95,21 +96,21 @@ func (s *WorkRepositoryTestSuite) TestUpdateWork() {
s.Require().NoError(s.DB.Create(&copyright1).Error)
s.Require().NoError(s.DB.Create(&copyright2).Error)
work := s.CreateTestWork("Original Title", "en", "Original content")
s.Require().NoError(s.DB.Model(work).Association("Copyrights").Append(copyright1))
workModel := s.CreateTestWork("Original Title", "en", "Original content")
s.Require().NoError(s.DB.Model(workModel).Association("Copyrights").Append(copyright1))
work.Title = "Updated Title"
s.Require().NoError(s.DB.Model(work).Association("Copyrights").Replace(copyright2))
workModel.Title = "Updated Title"
s.Require().NoError(s.DB.Model(workModel).Association("Copyrights").Replace(copyright2))
// Act
err := s.WorkRepo.Update(context.Background(), work)
err := s.WorkRepo.Update(context.Background(), workModel)
// Assert
s.Require().NoError(err)
// Verify that the work was actually updated in the database
var foundWork domain.Work
err = s.DB.Preload("Copyrights").First(&foundWork, work.ID).Error
var foundWork work.Work
err = s.DB.Preload("Copyrights").First(&foundWork, workModel.ID).Error
s.Require().NoError(err)
s.Equal("Updated Title", foundWork.Title)
s.Require().Len(foundWork.Copyrights, 1)
@ -120,29 +121,29 @@ func (s *WorkRepositoryTestSuite) TestUpdateWork() {
func (s *WorkRepositoryTestSuite) TestDeleteWork() {
s.Run("should delete an existing work and its associations", func() {
// Arrange
work := s.CreateTestWork("To Be Deleted", "en", "Content")
workModel := s.CreateTestWork("To Be Deleted", "en", "Content")
copyright := &domain.Copyright{Name: "C1", Identificator: "C1"}
s.Require().NoError(s.DB.Create(copyright).Error)
s.Require().NoError(s.DB.Model(work).Association("Copyrights").Append(copyright))
s.Require().NoError(s.DB.Model(workModel).Association("Copyrights").Append(copyright))
// Act
err := s.WorkRepo.Delete(context.Background(), work.ID)
err := s.WorkRepo.Delete(context.Background(), workModel.ID)
// Assert
s.Require().NoError(err)
// Verify that the work was actually deleted from the database
var foundWork domain.Work
err = s.DB.First(&foundWork, work.ID).Error
var foundWork work.Work
err = s.DB.First(&foundWork, workModel.ID).Error
s.Require().Error(err)
// Verify that the association in the join table is also deleted
var count int64
s.DB.Table("work_copyrights").Where("work_id = ?", work.ID).Count(&count)
s.DB.Table("work_copyrights").Where("work_id = ?", workModel.ID).Count(&count)
s.Zero(count)
})
}
func TestWorkRepository(t *testing.T) {
suite.Run(t, new(WorkRepositoryTestSuite))
}
}

View File

@ -1,18 +0,0 @@
package domain
import "context"
import "time"
type AnalyticsRepository interface {
IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error
IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error
UpdateWorkStats(ctx context.Context, workID uint, stats WorkStats) error
UpdateTranslationStats(ctx context.Context, translationID uint, stats TranslationStats) error
GetOrCreateWorkStats(ctx context.Context, workID uint) (*WorkStats, error)
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*TranslationStats, error)
GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*UserEngagement, error)
UpdateUserEngagement(ctx context.Context, userEngagement *UserEngagement) error
UpdateTrendingWorks(ctx context.Context, timePeriod string, trending []*Trending) error
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*Work, error)
}

View File

@ -182,40 +182,6 @@ func (u *User) CheckPassword(password string) bool {
return err == nil
}
type WorkStatus string
const (
WorkStatusDraft WorkStatus = "draft"
WorkStatusPublished WorkStatus = "published"
WorkStatusArchived WorkStatus = "archived"
WorkStatusDeleted WorkStatus = "deleted"
)
type WorkType string
const (
WorkTypePoetry WorkType = "poetry"
WorkTypeProse WorkType = "prose"
WorkTypeDrama WorkType = "drama"
WorkTypeEssay WorkType = "essay"
WorkTypeNovel WorkType = "novel"
WorkTypeShortStory WorkType = "short_story"
WorkTypeNovella WorkType = "novella"
WorkTypePlay WorkType = "play"
WorkTypeScript WorkType = "script"
WorkTypeOther WorkType = "other"
)
type Work struct {
TranslatableModel
Title string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
Type WorkType `gorm:"size:50;default:'other'"`
Status WorkStatus `gorm:"size:50;default:'draft'"`
PublishedAt *time.Time
Translations []*Translation `gorm:"polymorphic:Translatable"`
Authors []*Author `gorm:"many2many:work_authors"`
Tags []*Tag `gorm:"many2many:work_tags"`
Categories []*Category `gorm:"many2many:work_categories"`
Copyrights []*Copyright `gorm:"many2many:work_copyrights;constraint:OnDelete:CASCADE"`
Monetizations []*Monetization `gorm:"many2many:work_monetizations;constraint:OnDelete:CASCADE"`
}
type AuthorStatus string
const (
@ -229,7 +195,6 @@ type Author struct {
Status AuthorStatus `gorm:"size:50;default:'active'"`
BirthDate *time.Time
DeathDate *time.Time
Works []*Work `gorm:"many2many:work_authors"`
Books []*Book `gorm:"many2many:book_authors"`
CountryID *uint
Country *Country `gorm:"foreignKey:CountryID"`
@ -267,7 +232,6 @@ type Book struct {
Format BookFormat `gorm:"size:50;default:'paperback'"`
Status BookStatus `gorm:"size:50;default:'draft'"`
PublishedAt *time.Time
Works []*Work `gorm:"many2many:book_works"`
Authors []*Author `gorm:"many2many:book_authors"`
PublisherID *uint
Publisher *Publisher `gorm:"foreignKey:PublisherID"`
@ -307,7 +271,6 @@ type Source struct {
Description string `gorm:"type:text"`
URL string `gorm:"size:512"`
Status SourceStatus `gorm:"size:50;default:'active'"`
Works []*Work `gorm:"many2many:work_sources"`
Translations []*Translation `gorm:"polymorphic:Translatable"`
Copyrights []*Copyright `gorm:"many2many:source_copyrights;constraint:OnDelete:CASCADE"`
Monetizations []*Monetization `gorm:"many2many:source_monetizations;constraint:OnDelete:CASCADE"`
@ -333,12 +296,6 @@ type Edition struct {
Book *Book `gorm:"foreignKey:BookID"`
}
func (w *Work) BeforeSave(tx *gorm.DB) error {
if w.Title == "" {
w.Title = "Untitled Work"
}
return nil
}
func (a *Author) BeforeSave(tx *gorm.DB) error {
if a.Name == "" {
a.Name = "Unknown Author"
@ -364,7 +321,6 @@ type Comment struct {
UserID uint
User *User `gorm:"foreignKey:UserID"`
WorkID *uint
Work *Work `gorm:"foreignKey:WorkID"`
TranslationID *uint
Translation *Translation `gorm:"foreignKey:TranslationID"`
LineNumber *int `gorm:"index"`
@ -380,7 +336,6 @@ type Like struct {
UserID uint `gorm:"index;uniqueIndex:uniq_like_user_target"`
User *User `gorm:"foreignKey:UserID"`
WorkID *uint `gorm:"index;uniqueIndex:uniq_like_user_target"`
Work *Work `gorm:"foreignKey:WorkID"`
TranslationID *uint `gorm:"index;uniqueIndex:uniq_like_user_target"`
Translation *Translation `gorm:"foreignKey:TranslationID"`
CommentID *uint `gorm:"index;uniqueIndex:uniq_like_user_target"`
@ -392,7 +347,6 @@ type Bookmark struct {
UserID uint `gorm:"index;uniqueIndex:uniq_bookmark_user_work"`
User *User `gorm:"foreignKey:UserID"`
WorkID uint `gorm:"index;uniqueIndex:uniq_bookmark_user_work"`
Work *Work `gorm:"foreignKey:WorkID"`
Notes string `gorm:"type:text"`
LastReadAt *time.Time
Progress int `gorm:"default:0"`
@ -403,10 +357,14 @@ type Collection struct {
Description string `gorm:"type:text"`
UserID uint
User *User `gorm:"foreignKey:UserID"`
Works []*Work `gorm:"many2many:collection_works"`
IsPublic bool `gorm:"default:true"`
CoverImageURL string `gorm:"size:255"`
}
type CollectionWork struct {
CollectionID uint `gorm:"primaryKey"`
WorkID uint `gorm:"primaryKey"`
}
type Contribution struct {
BaseModel
Name string `gorm:"size:100;not null"`
@ -414,7 +372,6 @@ type Contribution struct {
UserID uint
User *User `gorm:"foreignKey:UserID"`
WorkID *uint
Work *Work `gorm:"foreignKey:WorkID"`
TranslationID *uint
Translation *Translation `gorm:"foreignKey:TranslationID"`
ReviewerID *uint
@ -477,7 +434,6 @@ type Tag struct {
BaseModel
Name string `gorm:"size:100;not null;uniqueIndex"`
Description string `gorm:"type:text"`
Works []*Work `gorm:"many2many:work_tags"`
Slug string `gorm:"size:255;index"`
}
type Category struct {
@ -487,7 +443,6 @@ type Category struct {
ParentID *uint
Parent *Category `gorm:"foreignKey:ParentID"`
Children []*Category `gorm:"foreignKey:ParentID"`
Works []*Work `gorm:"many2many:work_categories"`
Path string `gorm:"size:1024;index"`
Slug string `gorm:"size:255;index"`
}
@ -496,14 +451,6 @@ type Series struct {
Name string `gorm:"size:255;not null;uniqueIndex"`
Description string `gorm:"type:text"`
}
type WorkSeries struct {
BaseModel
WorkID uint `gorm:"index;uniqueIndex:uniq_work_series"`
Work *Work `gorm:"foreignKey:WorkID"`
SeriesID uint `gorm:"index;uniqueIndex:uniq_work_series"`
Series *Series `gorm:"foreignKey:SeriesID"`
NumberInSeries int `gorm:"default:0"`
}
type Translation struct {
BaseModel
@ -554,9 +501,6 @@ func (t *Translation) BeforeSave(tx *gorm.DB) error {
}
return nil
}
func (w *Work) GetID() uint { return w.ID }
func (w *Work) GetType() string { return "Work" }
func (w *Work) GetDefaultLanguage() string { return w.Language }
func (a *Author) GetID() uint { return a.ID }
func (a *Author) GetType() string { return "Author" }
func (a *Author) GetDefaultLanguage() string { return a.Language }
@ -583,13 +527,6 @@ type Copyright struct {
EndDate *time.Time
Translations []CopyrightTranslation `gorm:"foreignKey:CopyrightID"`
}
type WorkCopyright struct {
WorkID uint `gorm:"primaryKey;index"`
CopyrightID uint `gorm:"primaryKey;index"`
CreatedAt time.Time
}
func (WorkCopyright) TableName() string { return "work_copyrights" }
type AuthorCopyright struct {
AuthorID uint `gorm:"primaryKey;index"`
@ -660,13 +597,6 @@ const (
MonetizationStatusInactive MonetizationStatus = "inactive"
MonetizationStatusPending MonetizationStatus = "pending"
)
type WorkMonetization struct {
WorkID uint `gorm:"primaryKey;index"`
MonetizationID uint `gorm:"primaryKey;index"`
CreatedAt time.Time
}
func (WorkMonetization) TableName() string { return "work_monetizations" }
type AuthorMonetization struct {
AuthorID uint `gorm:"primaryKey;index"`
@ -742,20 +672,6 @@ type AuditLog struct {
// This is getting very long, but it's the correct approach.
// I will just paste the rest of the structs here.
type WorkStats struct {
BaseModel
Views int64 `gorm:"default:0"`
Likes int64 `gorm:"default:0"`
Comments int64 `gorm:"default:0"`
Bookmarks int64 `gorm:"default:0"`
Shares int64 `gorm:"default:0"`
TranslationCount int64 `gorm:"default:0"`
ReadingTime int `gorm:"default:0"`
Complexity float64 `gorm:"type:decimal(5,2);default:0.0"`
Sentiment float64 `gorm:"type:decimal(5,2);default:0.0"`
WorkID uint `gorm:"uniqueIndex;index"`
Work *Work `gorm:"foreignKey:WorkID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
}
type TranslationStats struct {
BaseModel
Views int64 `gorm:"default:0"`
@ -815,14 +731,6 @@ type MediaStats struct {
Media interface{} `gorm:"-"`
}
type BookWork struct {
BaseModel
BookID uint `gorm:"index;uniqueIndex:uniq_book_work"`
Book *Book `gorm:"foreignKey:BookID"`
WorkID uint `gorm:"index;uniqueIndex:uniq_book_work"`
Work *Work `gorm:"foreignKey:WorkID"`
Order int `gorm:"default:0"`
}
type AuthorCountry struct {
BaseModel
AuthorID uint `gorm:"index;uniqueIndex:uniq_author_country"`
@ -830,15 +738,6 @@ type AuthorCountry struct {
CountryID uint `gorm:"index;uniqueIndex:uniq_author_country"`
Country *Country `gorm:"foreignKey:CountryID"`
}
type WorkAuthor struct {
BaseModel
WorkID uint `gorm:"index;uniqueIndex:uniq_work_author_role"`
Work *Work `gorm:"foreignKey:WorkID"`
AuthorID uint `gorm:"index;uniqueIndex:uniq_work_author_role"`
Author *Author `gorm:"foreignKey:AuthorID"`
Role string `gorm:"size:50;default:'author';uniqueIndex:uniq_work_author_role"`
Ordinal int `gorm:"default:0"`
}
type BookAuthor struct {
BaseModel
BookID uint `gorm:"index;uniqueIndex:uniq_book_author_role"`
@ -855,7 +754,6 @@ type ReadabilityScore struct {
Language string `gorm:"size:50;not null"`
Method string `gorm:"size:50"`
WorkID uint
Work *Work `gorm:"foreignKey:WorkID"`
}
type WritingStyle struct {
BaseModel
@ -863,7 +761,6 @@ type WritingStyle struct {
Description string `gorm:"type:text"`
Language string `gorm:"size:50;not null"`
WorkID uint
Work *Work `gorm:"foreignKey:WorkID"`
}
type LinguisticLayer struct {
BaseModel
@ -872,13 +769,11 @@ type LinguisticLayer struct {
Language string `gorm:"size:50;not null"`
Type string `gorm:"size:50"`
WorkID uint
Work *Work `gorm:"foreignKey:WorkID"`
Data JSONB `gorm:"type:jsonb;default:'{}'"`
}
type TextBlock struct {
BaseModel
WorkID *uint
Work *Work `gorm:"foreignKey:WorkID"`
TranslationID *uint
Translation *Translation `gorm:"foreignKey:TranslationID"`
Index int `gorm:"index"`
@ -897,7 +792,6 @@ type TextMetadata struct {
AverageWordLength float64 `gorm:"type:decimal(5,2)"`
AverageSentenceLength float64 `gorm:"type:decimal(5,2)"`
WorkID uint
Work *Work `gorm:"foreignKey:WorkID"`
}
type PoeticAnalysis struct {
BaseModel
@ -908,7 +802,6 @@ type PoeticAnalysis struct {
StanzaCount int `gorm:"default:0"`
LineCount int `gorm:"default:0"`
WorkID uint
Work *Work `gorm:"foreignKey:WorkID"`
}
type Word struct {
BaseModel
@ -918,7 +811,6 @@ type Word struct {
Lemma string `gorm:"size:100"`
ConceptID *uint
Concept *Concept `gorm:"foreignKey:ConceptID"`
Works []*Work `gorm:"many2many:work_words"`
}
type WordOccurrence struct {
BaseModel
@ -936,14 +828,12 @@ type Concept struct {
Name string `gorm:"size:100;not null"`
Description string `gorm:"type:text"`
Words []*Word `gorm:"foreignKey:ConceptID"`
Works []*Work `gorm:"many2many:work_concepts"`
}
type LanguageEntity struct {
BaseModel
Name string `gorm:"size:100;not null"`
Type string `gorm:"size:50"`
Language string `gorm:"size:50;not null"`
Works []*Work `gorm:"many2many:work_language_entities"`
}
type EntityOccurrence struct {
BaseModel
@ -960,7 +850,6 @@ type LanguageAnalysis struct {
Language string `gorm:"size:50;not null;uniqueIndex:uniq_work_language_analysis"`
Analysis JSONB `gorm:"type:jsonb;default:'{}'"`
WorkID uint `gorm:"index;uniqueIndex:uniq_work_language_analysis"`
Work *Work `gorm:"foreignKey:WorkID"`
}
type Gamification struct {
BaseModel
@ -981,7 +870,6 @@ type Stats struct {
UserID *uint
User *User `gorm:"foreignKey:UserID"`
WorkID *uint
Work *Work `gorm:"foreignKey:WorkID"`
}
type SearchDocument struct {
BaseModel
@ -1002,7 +890,6 @@ type Emotion struct {
UserID *uint
User *User `gorm:"foreignKey:UserID"`
WorkID *uint
Work *Work `gorm:"foreignKey:WorkID"`
CollectionID *uint
Collection *Collection `gorm:"foreignKey:CollectionID"`
}
@ -1011,14 +898,12 @@ type Mood struct {
Name string `gorm:"size:100;not null"`
Description string `gorm:"type:text"`
Language string `gorm:"size:50;not null"`
Works []*Work `gorm:"many2many:work_moods"`
}
type TopicCluster struct {
BaseModel
Name string `gorm:"size:100;not null"`
Description string `gorm:"type:text"`
Keywords string `gorm:"type:text"`
Works []*Work `gorm:"many2many:work_topic_clusters"`
}
type Edge struct {
@ -1039,7 +924,6 @@ type Embedding struct {
Model string `gorm:"size:50;not null;uniqueIndex:uniq_embedding"`
Dim int `gorm:"default:0"`
WorkID *uint
Work *Work `gorm:"foreignKey:WorkID"`
TranslationID *uint
Translation *Translation `gorm:"foreignKey:TranslationID"`
}
@ -1086,7 +970,6 @@ type EditorialWorkflow struct {
Notes string `gorm:"type:text"`
Language string `gorm:"size:50;not null"`
WorkID *uint
Work *Work `gorm:"foreignKey:WorkID"`
TranslationID *uint
Translation *Translation `gorm:"foreignKey:TranslationID"`
UserID uint
@ -1109,7 +992,6 @@ type Vote struct {
UserID uint
User *User `gorm:"foreignKey:UserID"`
WorkID *uint
Work *Work `gorm:"foreignKey:WorkID"`
TranslationID *uint
Translation *Translation `gorm:"foreignKey:TranslationID"`
CommentID *uint
@ -1122,7 +1004,6 @@ type Contributor struct {
UserID *uint
User *User `gorm:"foreignKey:UserID"`
WorkID *uint
Work *Work `gorm:"foreignKey:WorkID"`
TranslationID *uint
Translation *Translation `gorm:"foreignKey:TranslationID"`
}
@ -1140,7 +1021,6 @@ type HybridEntityWork struct {
Name string `gorm:"size:100;not null"`
Type string `gorm:"size:50"`
WorkID *uint
Work *Work `gorm:"foreignKey:WorkID"`
TranslationID *uint
Translation *Translation `gorm:"foreignKey:TranslationID"`
}

View File

@ -234,16 +234,6 @@ type BaseRepository[T any] interface {
WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error
}
// WorkRepository defines methods specific to Work.
type WorkRepository interface {
BaseRepository[Work]
FindByTitle(ctx context.Context, title string) ([]Work, error)
FindByAuthor(ctx context.Context, authorID uint) ([]Work, error)
FindByCategory(ctx context.Context, categoryID uint) ([]Work, error)
FindByLanguage(ctx context.Context, language string, page, pageSize int) (*PaginatedResult[Work], error)
GetWithTranslations(ctx context.Context, id uint) (*Work, error)
ListWithTranslations(ctx context.Context, page, pageSize int) (*PaginatedResult[Work], error)
}
// AuthorRepository defines CRUD methods specific to Author.
type AuthorRepository interface {

View File

@ -2,10 +2,10 @@ package search
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/work"
)
// SearchClient defines the interface for a search client.
type SearchClient interface {
IndexWork(ctx context.Context, work *domain.Work, pipeline string) error
IndexWork(ctx context.Context, work *work.Work, pipeline string) error
}

View File

@ -0,0 +1,116 @@
package work
import (
"gorm.io/gorm"
"tercul/internal/domain"
"time"
)
type WorkStatus string
const (
WorkStatusDraft WorkStatus = "draft"
WorkStatusPublished WorkStatus = "published"
WorkStatusArchived WorkStatus = "archived"
WorkStatusDeleted WorkStatus = "deleted"
)
type WorkType string
const (
WorkTypePoetry WorkType = "poetry"
WorkTypeProse WorkType = "prose"
WorkTypeDrama WorkType = "drama"
WorkTypeEssay WorkType = "essay"
WorkTypeNovel WorkType = "novel"
WorkTypeShortStory WorkType = "short_story"
WorkTypeNovella WorkType = "novella"
WorkTypePlay WorkType = "play"
WorkTypeScript WorkType = "script"
WorkTypeOther WorkType = "other"
)
type Work struct {
domain.TranslatableModel
Title string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
Type WorkType `gorm:"size:50;default:'other'"`
Status WorkStatus `gorm:"size:50;default:'draft'"`
PublishedAt *time.Time
Translations []*domain.Translation `gorm:"polymorphic:Translatable"`
Authors []*domain.Author `gorm:"many2many:work_authors"`
Tags []*domain.Tag `gorm:"many2many:work_tags"`
Categories []*domain.Category `gorm:"many2many:work_categories"`
Copyrights []*domain.Copyright `gorm:"many2many:work_copyrights;constraint:OnDelete:CASCADE"`
Monetizations []*domain.Monetization `gorm:"many2many:work_monetizations;constraint:OnDelete:CASCADE"`
}
func (w *Work) BeforeSave(tx *gorm.DB) error {
if w.Title == "" {
w.Title = "Untitled Work"
}
return nil
}
func (w *Work) GetID() uint { return w.ID }
func (w *Work) GetType() string { return "Work" }
func (w *Work) GetDefaultLanguage() string { return w.Language }
type WorkStats struct {
domain.BaseModel
Views int64 `gorm:"default:0"`
Likes int64 `gorm:"default:0"`
Comments int64 `gorm:"default:0"`
Bookmarks int64 `gorm:"default:0"`
Shares int64 `gorm:"default:0"`
TranslationCount int64 `gorm:"default:0"`
ReadingTime int `gorm:"default:0"`
Complexity float64 `gorm:"type:decimal(5,2);default:0.0"`
Sentiment float64 `gorm:"type:decimal(5,2);default:0.0"`
WorkID uint `gorm:"uniqueIndex;index"`
Work *Work `gorm:"foreignKey:WorkID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
}
type WorkSeries struct {
domain.BaseModel
WorkID uint `gorm:"index;uniqueIndex:uniq_work_series"`
Work *Work `gorm:"foreignKey:WorkID"`
SeriesID uint `gorm:"index;uniqueIndex:uniq_work_series"`
Series *domain.Series `gorm:"foreignKey:SeriesID"`
NumberInSeries int `gorm:"default:0"`
}
type BookWork struct {
domain.BaseModel
BookID uint `gorm:"index;uniqueIndex:uniq_book_work"`
Book *domain.Book `gorm:"foreignKey:BookID"`
WorkID uint `gorm:"index;uniqueIndex:uniq_book_work"`
Work *Work `gorm:"foreignKey:WorkID"`
Order int `gorm:"default:0"`
}
type WorkAuthor struct {
domain.BaseModel
WorkID uint `gorm:"index;uniqueIndex:uniq_work_author_role"`
Work *Work `gorm:"foreignKey:WorkID"`
AuthorID uint `gorm:"index;uniqueIndex:uniq_work_author_role"`
Author *domain.Author `gorm:"foreignKey:AuthorID"`
Role string `gorm:"size:50;default:'author';uniqueIndex:uniq_work_author_role"`
Ordinal int `gorm:"default:0"`
}
type WorkCopyright struct {
WorkID uint `gorm:"primaryKey;index"`
CopyrightID uint `gorm:"primaryKey;index"`
CreatedAt time.Time
}
func (WorkCopyright) TableName() string { return "work_copyrights" }
type WorkMonetization struct {
WorkID uint `gorm:"primaryKey;index"`
MonetizationID uint `gorm:"primaryKey;index"`
CreatedAt time.Time
}
func (WorkMonetization) TableName() string { return "work_monetizations" }

View File

@ -0,0 +1,17 @@
package work
import (
"context"
"tercul/internal/domain"
)
// WorkRepository defines methods specific to Work.
type WorkRepository interface {
domain.BaseRepository[Work]
FindByTitle(ctx context.Context, title string) ([]Work, error)
FindByAuthor(ctx context.Context, authorID uint) ([]Work, error)
FindByCategory(ctx context.Context, categoryID uint) ([]Work, error)
FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[Work], error)
GetWithTranslations(ctx context.Context, id uint) (*Work, error)
ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[Work], error)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"tercul/internal/domain"
"tercul/internal/domain/work"
"gorm.io/gorm"
"tercul/internal/platform/log"
@ -22,7 +23,7 @@ type AnalysisRepository interface {
readabilityScore *domain.ReadabilityScore, languageAnalysis *domain.LanguageAnalysis) error
// GetWorkByID fetches a work by ID
GetWorkByID(ctx context.Context, workID uint) (*domain.Work, error)
GetWorkByID(ctx context.Context, workID uint) (*work.Work, error)
// GetAnalysisData fetches persisted analysis data for a work
GetAnalysisData(ctx context.Context, workID uint) (*domain.TextMetadata, *domain.ReadabilityScore, *domain.LanguageAnalysis, error)
@ -45,8 +46,8 @@ func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workI
}
// Determine language from the work record to avoid hardcoded defaults
var work domain.Work
if err := r.db.WithContext(ctx).First(&work, workID).Error; err != nil {
var workRecord work.Work
if err := r.db.WithContext(ctx).First(&workRecord, workID).Error; err != nil {
log.LogError("Failed to fetch work for language",
log.F("workID", workID),
log.F("error", err))
@ -56,7 +57,7 @@ func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workI
// Create text metadata
textMetadata := &domain.TextMetadata{
WorkID: workID,
Language: work.Language,
Language: workRecord.Language,
WordCount: result.WordCount,
SentenceCount: result.SentenceCount,
ParagraphCount: result.ParagraphCount,
@ -67,7 +68,7 @@ func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workI
// Create readability score
readabilityScore := &domain.ReadabilityScore{
WorkID: workID,
Language: work.Language,
Language: workRecord.Language,
Score: result.ReadabilityScore,
Method: result.ReadabilityMethod,
}
@ -75,7 +76,7 @@ func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workI
// Create language analysis
languageAnalysis := &domain.LanguageAnalysis{
WorkID: workID,
Language: work.Language,
Language: workRecord.Language,
Analysis: domain.JSONB{
"sentiment": result.Sentiment,
"keywords": extractKeywordsAsJSON(result.Keywords),
@ -89,8 +90,8 @@ func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workI
// GetWorkContent retrieves content for a work from translations
func (r *GORMAnalysisRepository) GetWorkContent(ctx context.Context, workID uint, language string) (string, error) {
// First, get the work to determine its language
var work domain.Work
if err := r.db.First(&work, workID).Error; err != nil {
var workRecord work.Work
if err := r.db.First(&workRecord, workID).Error; err != nil {
log.LogError("Failed to fetch work for content retrieval",
log.F("workID", workID),
log.F("error", err))
@ -112,7 +113,7 @@ func (r *GORMAnalysisRepository) GetWorkContent(ctx context.Context, workID uint
// Try work's language
if err := r.db.Where("translatable_type = ? AND translatable_id = ? AND language = ?",
"Work", workID, work.Language).First(&translation).Error; err == nil {
"Work", workID, workRecord.Language).First(&translation).Error; err == nil {
return translation.Content, nil
}
@ -126,12 +127,12 @@ func (r *GORMAnalysisRepository) GetWorkContent(ctx context.Context, workID uint
}
// GetWorkByID fetches a work by ID
func (r *GORMAnalysisRepository) GetWorkByID(ctx context.Context, workID uint) (*domain.Work, error) {
var work domain.Work
if err := r.db.WithContext(ctx).First(&work, workID).Error; err != nil {
func (r *GORMAnalysisRepository) GetWorkByID(ctx context.Context, workID uint) (*work.Work, error) {
var workRecord work.Work
if err := r.db.WithContext(ctx).First(&workRecord, workID).Error; err != nil {
return nil, fmt.Errorf("failed to fetch work: %w", err)
}
return &work, nil
return &workRecord, nil
}
// GetAnalysisData fetches persisted analysis data for a work

View File

@ -6,6 +6,7 @@ import (
"fmt"
"log"
"tercul/internal/domain"
"tercul/internal/domain/work"
"time"
"github.com/hibiken/asynq"
@ -60,7 +61,7 @@ func (j *LinguisticSyncJob) EnqueueAnalysisForAllWorks() error {
log.Println("Enqueueing linguistic analysis jobs for all works...")
var workIDs []uint
if err := j.DB.Model(&domain.Work{}).Pluck("id", &workIDs).Error; err != nil {
if err := j.DB.Model(&work.Work{}).Pluck("id", &workIDs).Error; err != nil {
return fmt.Errorf("error fetching work IDs: %w", err)
}

View File

@ -4,7 +4,7 @@ import (
"context"
"fmt"
"log"
"tercul/internal/domain"
"tercul/internal/domain/work"
"time"
"github.com/weaviate/weaviate-go-client/v5/weaviate"
@ -13,7 +13,7 @@ import (
var Client *weaviate.Client
// UpsertWork inserts or updates a Work object in Weaviate
func UpsertWork(client *weaviate.Client, work domain.Work) error {
func UpsertWork(client *weaviate.Client, work work.Work) error {
// Create a properties map with the fields that exist in the Work model
properties := map[string]interface{}{
"language": work.Language,

View File

@ -3,14 +3,14 @@ package search
import (
"context"
"fmt"
"tercul/internal/domain"
"tercul/internal/domain/work"
"time"
"github.com/weaviate/weaviate-go-client/v5/weaviate"
)
type WeaviateWrapper interface {
IndexWork(ctx context.Context, work *domain.Work, content string) error
IndexWork(ctx context.Context, work *work.Work, content string) error
}
type weaviateWrapper struct {
@ -21,7 +21,7 @@ func NewWeaviateWrapper(client *weaviate.Client) WeaviateWrapper {
return &weaviateWrapper{client: client}
}
func (w *weaviateWrapper) IndexWork(ctx context.Context, work *domain.Work, content string) error {
func (w *weaviateWrapper) IndexWork(ctx context.Context, work *work.Work, content string) error {
properties := map[string]interface{}{
"language": work.Language,
"title": work.Title,

View File

@ -11,6 +11,7 @@ import (
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/domain/search"
"tercul/internal/domain/work"
"tercul/internal/jobs/linguistics"
"time"
@ -23,7 +24,7 @@ import (
// mockSearchClient is a mock implementation of the SearchClient interface.
type mockSearchClient struct{}
func (m *mockSearchClient) IndexWork(ctx context.Context, work *domain.Work, pipeline string) error {
func (m *mockSearchClient) IndexWork(ctx context.Context, work *work.Work, pipeline string) error {
return nil
}
@ -54,8 +55,8 @@ func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context,
func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, translationID uint) error {
return nil
}
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
return &domain.WorkStats{}, nil
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*work.WorkStats, error) {
return &work.WorkStats{}, nil
}
func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
return &domain.TranslationStats{}, nil
@ -73,7 +74,7 @@ func (m *mockAnalyticsService) UpdateUserEngagement(ctx context.Context, userID
return nil
}
func (m *mockAnalyticsService) UpdateTrending(ctx context.Context) error { return nil }
func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) {
func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*work.Work, error) {
return nil, nil
}
@ -139,13 +140,13 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
s.DB = db
db.AutoMigrate(
&domain.Work{}, &domain.User{}, &domain.Author{}, &domain.Translation{},
&work.Work{}, &domain.User{}, &domain.Author{}, &domain.Translation{},
&domain.Comment{}, &domain.Like{}, &domain.Bookmark{}, &domain.Collection{},
&domain.Tag{}, &domain.Category{}, &domain.Book{}, &domain.Publisher{},
&domain.Source{}, &domain.Copyright{}, &domain.Monetization{},
&domain.WorkStats{}, &domain.Trending{}, &domain.UserSession{}, &domain.Localization{},
&work.WorkStats{}, &domain.Trending{}, &domain.UserSession{}, &domain.Localization{},
&domain.LanguageAnalysis{}, &domain.TextMetadata{}, &domain.ReadabilityScore{},
&domain.TranslationStats{}, &TestEntity{},
&domain.TranslationStats{}, &TestEntity{}, &domain.CollectionWork{},
)
repos := sql.NewRepositories(s.DB)
@ -184,8 +185,8 @@ func (s *IntegrationTestSuite) SetupTest() {
}
// CreateTestWork creates a test work with optional content
func (s *IntegrationTestSuite) CreateTestWork(title, language string, content string) *domain.Work {
work := &domain.Work{
func (s *IntegrationTestSuite) CreateTestWork(title, language string, content string) *work.Work {
work := &work.Work{
Title: title,
TranslatableModel: domain.TranslatableModel{
Language: language,

View File

@ -3,6 +3,7 @@ package testutil
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/work"
"time"
"github.com/stretchr/testify/mock"
@ -13,12 +14,12 @@ type MockAnalyticsService struct {
mock.Mock
}
func (m *MockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) {
func (m *MockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*work.Work, error) {
args := m.Called(ctx, timePeriod, limit)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*domain.Work), args.Error(1)
return args.Get(0).([]*work.Work), args.Error(1)
}
func (m *MockAnalyticsService) UpdateTrending(ctx context.Context) error {
@ -46,12 +47,12 @@ func (m *MockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workI
m.Called(ctx, workID)
}
func (m *MockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
func (m *MockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*work.WorkStats, error) {
args := m.Called(ctx, workID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*domain.WorkStats), args.Error(1)
return args.Get(0).(*work.WorkStats), args.Error(1)
}
func (m *MockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
@ -85,7 +86,7 @@ func (m *MockAnalyticsService) IncrementTranslationCounter(ctx context.Context,
return args.Error(0)
}
func (m *MockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
func (m *MockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats work.WorkStats) error {
args := m.Called(ctx, workID, stats)
return args.Error(0)
}

View File

@ -2,14 +2,14 @@ package testutil
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/work"
)
type MockWeaviateWrapper struct {
IndexWorkFunc func(ctx context.Context, work *domain.Work, content string) error
IndexWorkFunc func(ctx context.Context, work *work.Work, content string) error
}
func (m *MockWeaviateWrapper) IndexWork(ctx context.Context, work *domain.Work, content string) error {
func (m *MockWeaviateWrapper) IndexWork(ctx context.Context, work *work.Work, content string) error {
if m.IndexWorkFunc != nil {
return m.IndexWorkFunc(ctx, work, content)
}

View File

@ -3,6 +3,7 @@ package testutil
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/work"
"github.com/stretchr/testify/mock"
"gorm.io/gorm"
@ -11,23 +12,23 @@ import (
// MockWorkRepository is a mock implementation of the WorkRepository interface.
type MockWorkRepository struct {
mock.Mock
Works []*domain.Work
Works []*work.Work
}
// NewMockWorkRepository creates a new MockWorkRepository.
func NewMockWorkRepository() *MockWorkRepository {
return &MockWorkRepository{Works: []*domain.Work{}}
return &MockWorkRepository{Works: []*work.Work{}}
}
// Create adds a new work to the mock repository.
func (m *MockWorkRepository) Create(ctx context.Context, work *domain.Work) error {
func (m *MockWorkRepository) Create(ctx context.Context, work *work.Work) error {
work.ID = uint(len(m.Works) + 1)
m.Works = append(m.Works, work)
return nil
}
// GetByID retrieves a work by its ID from the mock repository.
func (m *MockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) {
func (m *MockWorkRepository) GetByID(ctx context.Context, id uint) (*work.Work, error) {
for _, w := range m.Works {
if w.ID == id {
return w, nil
@ -43,31 +44,31 @@ func (m *MockWorkRepository) Exists(ctx context.Context, id uint) (bool, error)
}
// The rest of the WorkRepository and BaseRepository methods can be stubbed out.
func (m *MockWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) {
func (m *MockWorkRepository) FindByTitle(ctx context.Context, title string) ([]work.Work, error) {
panic("not implemented")
}
func (m *MockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
func (m *MockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]work.Work, error) {
panic("not implemented")
}
func (m *MockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
func (m *MockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]work.Work, error) {
panic("not implemented")
}
func (m *MockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
func (m *MockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
panic("not implemented")
}
func (m *MockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
func (m *MockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*work.Work, error) {
return m.GetByID(ctx, id)
}
func (m *MockWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
func (m *MockWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
panic("not implemented")
}
func (m *MockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
func (m *MockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *work.Work) error {
return m.Create(ctx, entity)
}
func (m *MockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
func (m *MockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*work.Work, error) {
return m.GetByID(ctx, id)
}
func (m *MockWorkRepository) Update(ctx context.Context, entity *domain.Work) error {
func (m *MockWorkRepository) Update(ctx context.Context, entity *work.Work) error {
for i, w := range m.Works {
if w.ID == entity.ID {
m.Works[i] = entity
@ -76,7 +77,7 @@ func (m *MockWorkRepository) Update(ctx context.Context, entity *domain.Work) er
}
return gorm.ErrRecordNotFound
}
func (m *MockWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
func (m *MockWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *work.Work) error {
return m.Update(ctx, entity)
}
func (m *MockWorkRepository) Delete(ctx context.Context, id uint) error {
@ -91,14 +92,14 @@ func (m *MockWorkRepository) Delete(ctx context.Context, id uint) error {
func (m *MockWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
return m.Delete(ctx, id)
}
func (m *MockWorkRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
func (m *MockWorkRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
panic("not implemented")
}
func (m *MockWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Work, error) {
func (m *MockWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]work.Work, error) {
panic("not implemented")
}
func (m *MockWorkRepository) ListAll(ctx context.Context) ([]domain.Work, error) {
var works []domain.Work
func (m *MockWorkRepository) ListAll(ctx context.Context) ([]work.Work, error) {
var works []work.Work
for _, w := range m.Works {
works = append(works, *w)
}
@ -110,10 +111,10 @@ func (m *MockWorkRepository) Count(ctx context.Context) (int64, error) {
func (m *MockWorkRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
panic("not implemented")
}
func (m *MockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) {
func (m *MockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*work.Work, error) {
return m.GetByID(ctx, id)
}
func (m *MockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) {
func (m *MockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]work.Work, error) {
panic("not implemented")
}
func (m *MockWorkRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {

View File

@ -8,6 +8,7 @@ import (
"tercul/internal/app/work"
"tercul/internal/domain"
domain_localization "tercul/internal/domain/localization"
domain_work "tercul/internal/domain/work"
"github.com/stretchr/testify/suite"
)
@ -24,7 +25,7 @@ type SimpleTestSuite struct {
type MockSearchClient struct{}
// IndexWork is the mock implementation of the IndexWork method.
func (m *MockSearchClient) IndexWork(ctx context.Context, work *domain.Work, pipeline string) error {
func (m *MockSearchClient) IndexWork(ctx context.Context, work *domain_work.Work, pipeline string) error {
return nil
}
@ -74,8 +75,8 @@ func (s *SimpleTestSuite) GetResolver() *graph.Resolver {
}
// CreateTestWork creates a test work with optional content
func (s *SimpleTestSuite) CreateTestWork(title, language string, content string) *domain.Work {
work := &domain.Work{
func (s *SimpleTestSuite) CreateTestWork(title, language string, content string) *domain_work.Work {
work := &domain_work.Work{
Title: title,
TranslatableModel: domain.TranslatableModel{Language: language},
}