From 1cb434bbe7fa5a4767daab9396f43c5a934df55f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:44:47 +0000 Subject: [PATCH] feat: Finalize DDD refactoring and fix tests This commit completes the Domain-Driven Design (DDD) refactoring, bringing the codebase into a stable, compilable, and fully tested state. Key changes include: - Refactored the `localization` service to use a Commands/Queries pattern, aligning it with the new architecture. - Implemented the missing `GetAuthorBiography` query in the `localization` service to simplify resolver logic. - Corrected GORM entity definitions for polymorphic relationships, changing `[]Translation` to `[]*Translation` to enable proper preloading of translations. - Standardized the `TranslatableType` value to use the database table name (e.g., "works") instead of the model name ("Work") to ensure consistent data creation and retrieval. - Updated GraphQL resolvers to exclusively use application services instead of direct repository access, fixing numerous build errors. - Repaired all failing unit and integration tests by updating mock objects and correcting test data setup to reflect the architectural changes. These changes resolve all outstanding build errors and test failures, leaving the application in a healthy and maintainable state. --- internal/adapters/graphql/integration_test.go | 4 +-- internal/adapters/graphql/schema.resolvers.go | 18 +++++------ internal/app/app.go | 2 -- internal/app/localization/commands.go | 13 ++++++++ internal/app/localization/queries.go | 31 +++++++++++++++++++ internal/app/localization/service.go | 29 ++++++----------- internal/app/localization/service_test.go | 27 ++++++++++++++-- internal/app/search/service_test.go | 5 +++ internal/data/sql/localization_repository.go | 15 +++++++++ internal/data/sql/translation_repository.go | 2 +- internal/domain/entities.go | 10 +++--- internal/domain/localization/repo.go | 1 + internal/testutil/integration_test_utils.go | 4 +-- internal/testutil/simple_test_utils.go | 5 +++ 14 files changed, 123 insertions(+), 43 deletions(-) create mode 100644 internal/app/localization/commands.go create mode 100644 internal/app/localization/queries.go diff --git a/internal/adapters/graphql/integration_test.go b/internal/adapters/graphql/integration_test.go index fd8ee5d..01e20a6 100644 --- a/internal/adapters/graphql/integration_test.go +++ b/internal/adapters/graphql/integration_test.go @@ -527,7 +527,7 @@ func (s *GraphQLIntegrationSuite) TestUpdateTranslationValidation() { Language: "en", Content: "Test content", TranslatableID: work.ID, - TranslatableType: "Work", + TranslatableType: "works", }) s.Require().NoError(err) @@ -631,7 +631,7 @@ func (s *GraphQLIntegrationSuite) TestDeleteTranslation() { Language: "en", Content: "Test content", TranslatableID: work.ID, - TranslatableType: "Work", + TranslatableType: "works", }) s.Require().NoError(err) diff --git a/internal/adapters/graphql/schema.resolvers.go b/internal/adapters/graphql/schema.resolvers.go index 46d0712..268fb69 100644 --- a/internal/adapters/graphql/schema.resolvers.go +++ b/internal/adapters/graphql/schema.resolvers.go @@ -111,7 +111,7 @@ func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput Content: *input.Content, Language: input.Language, TranslatableID: createdWork.ID, - TranslatableType: "Work", + TranslatableType: "works", IsOriginalLanguage: true, } _, err := r.App.Translation.Commands.CreateTranslation(ctx, translationInput) @@ -1074,11 +1074,9 @@ func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32 var bio *string authorWithTranslations, err := r.App.Author.Queries.AuthorWithTranslations(ctx, a.ID) if err == nil && authorWithTranslations != nil { - for _, t := range authorWithTranslations.Translations { - if t.Language == a.Language && t.Content != "" { - bio = &t.Content - break - } + biography, err := r.App.Localization.Queries.GetAuthorBiography(ctx, a.ID, a.Language) + if err == nil && biography != "" { + bio = &biography } } @@ -1133,9 +1131,6 @@ func (r *queryResolver) Users(ctx context.Context, limit *int32, offset *int32, users, err = r.App.User.Queries.UsersByRole(ctx, modelRole) } else { users, err = r.App.User.Queries.Users(ctx) - if err != nil { - return nil, err - } } if err != nil { @@ -1234,13 +1229,16 @@ func (r *queryResolver) Tags(ctx context.Context, limit *int32, offset *int32) ( func (r *queryResolver) Category(ctx context.Context, id string) (*model.Category, error) { categoryID, err := strconv.ParseUint(id, 10, 32) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid category ID: %v", err) } category, err := r.App.Category.Queries.Category(ctx, uint(categoryID)) if err != nil { return nil, err } + if category == nil { + return nil, nil + } return &model.Category{ ID: fmt.Sprintf("%d", category.ID), diff --git a/internal/app/app.go b/internal/app/app.go index 6424610..623102d 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -34,7 +34,6 @@ type Application struct { Auth *auth.Service Work *work.Service Analytics analytics.Service - Repos *sql.Repositories } func NewApplication(repos *sql.Repositories, searchClient search.SearchClient, analyticsService analytics.Service) *Application { @@ -66,6 +65,5 @@ func NewApplication(repos *sql.Repositories, searchClient search.SearchClient, a Auth: authService, Work: workService, Analytics: analyticsService, - Repos: repos, } } \ No newline at end of file diff --git a/internal/app/localization/commands.go b/internal/app/localization/commands.go new file mode 100644 index 0000000..c23b14c --- /dev/null +++ b/internal/app/localization/commands.go @@ -0,0 +1,13 @@ +package localization + +import "tercul/internal/domain/localization" + +// LocalizationCommands contains the command handlers for the localization aggregate. +type LocalizationCommands struct { + repo localization.LocalizationRepository +} + +// NewLocalizationCommands creates a new LocalizationCommands handler. +func NewLocalizationCommands(repo localization.LocalizationRepository) *LocalizationCommands { + return &LocalizationCommands{repo: repo} +} \ No newline at end of file diff --git a/internal/app/localization/queries.go b/internal/app/localization/queries.go new file mode 100644 index 0000000..7e4988c --- /dev/null +++ b/internal/app/localization/queries.go @@ -0,0 +1,31 @@ +package localization + +import ( + "context" + "tercul/internal/domain/localization" +) + +// LocalizationQueries contains the query handlers for the localization aggregate. +type LocalizationQueries struct { + repo localization.LocalizationRepository +} + +// NewLocalizationQueries creates a new LocalizationQueries handler. +func NewLocalizationQueries(repo localization.LocalizationRepository) *LocalizationQueries { + return &LocalizationQueries{repo: repo} +} + +// GetTranslation returns a translation for a given key and language. +func (q *LocalizationQueries) GetTranslation(ctx context.Context, key string, language string) (string, error) { + return q.repo.GetTranslation(ctx, key, language) +} + +// GetTranslations returns a map of translations for a given set of keys and language. +func (q *LocalizationQueries) GetTranslations(ctx context.Context, keys []string, language string) (map[string]string, error) { + return q.repo.GetTranslations(ctx, keys, language) +} + +// GetAuthorBiography returns the biography of an author in a specific language. +func (q *LocalizationQueries) GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) { + return q.repo.GetAuthorBiography(ctx, authorID, language) +} \ No newline at end of file diff --git a/internal/app/localization/service.go b/internal/app/localization/service.go index 04672dd..00e68d5 100644 --- a/internal/app/localization/service.go +++ b/internal/app/localization/service.go @@ -1,26 +1,17 @@ package localization -import ( - "context" - "tercul/internal/domain/localization" -) +import "tercul/internal/domain/localization" -// Service handles localization-related operations. +// Service is the application service for the localization aggregate. type Service struct { - repo localization.LocalizationRepository + Commands *LocalizationCommands + Queries *LocalizationQueries } -// NewService creates a new localization service. +// NewService creates a new localization Service. func NewService(repo localization.LocalizationRepository) *Service { - return &Service{repo: repo} -} - -// GetTranslation returns a translation for a given key and language. -func (s *Service) GetTranslation(ctx context.Context, key string, language string) (string, error) { - return s.repo.GetTranslation(ctx, key, language) -} - -// GetTranslations returns a map of translations for a given set of keys and language. -func (s *Service) GetTranslations(ctx context.Context, keys []string, language string) (map[string]string, error) { - return s.repo.GetTranslations(ctx, keys, language) -} + return &Service{ + Commands: NewLocalizationCommands(repo), + Queries: NewLocalizationQueries(repo), + } +} \ No newline at end of file diff --git a/internal/app/localization/service_test.go b/internal/app/localization/service_test.go index 1a1c3f0..e668f8b 100644 --- a/internal/app/localization/service_test.go +++ b/internal/app/localization/service_test.go @@ -25,6 +25,11 @@ func (m *mockLocalizationRepository) GetTranslations(ctx context.Context, keys [ return args.Get(0).(map[string]string), args.Error(1) } +func (m *mockLocalizationRepository) GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) { + args := m.Called(ctx, authorID, language) + return args.String(0), args.Error(1) +} + func TestLocalizationService_GetTranslation(t *testing.T) { repo := new(mockLocalizationRepository) service := NewService(repo) @@ -36,7 +41,7 @@ func TestLocalizationService_GetTranslation(t *testing.T) { repo.On("GetTranslation", ctx, key, language).Return(expectedTranslation, nil) - translation, err := service.GetTranslation(ctx, key, language) + translation, err := service.Queries.GetTranslation(ctx, key, language) assert.NoError(t, err) assert.Equal(t, expectedTranslation, translation) @@ -57,9 +62,27 @@ func TestLocalizationService_GetTranslations(t *testing.T) { repo.On("GetTranslations", ctx, keys, language).Return(expectedTranslations, nil) - translations, err := service.GetTranslations(ctx, keys, language) + translations, err := service.Queries.GetTranslations(ctx, keys, language) assert.NoError(t, err) assert.Equal(t, expectedTranslations, translations) repo.AssertExpectations(t) } + +func TestLocalizationService_GetAuthorBiography(t *testing.T) { + repo := new(mockLocalizationRepository) + service := NewService(repo) + + ctx := context.Background() + authorID := uint(1) + language := "en" + expectedBiography := "This is a test biography." + + repo.On("GetAuthorBiography", ctx, authorID, language).Return(expectedBiography, nil) + + biography, err := service.Queries.GetAuthorBiography(ctx, authorID, language) + + assert.NoError(t, err) + assert.Equal(t, expectedBiography, biography) + repo.AssertExpectations(t) +} \ No newline at end of file diff --git a/internal/app/search/service_test.go b/internal/app/search/service_test.go index b293c72..77f94a9 100644 --- a/internal/app/search/service_test.go +++ b/internal/app/search/service_test.go @@ -27,6 +27,11 @@ func (m *mockLocalizationRepository) GetTranslations(ctx context.Context, keys [ return args.Get(0).(map[string]string), args.Error(1) } +func (m *mockLocalizationRepository) GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) { + args := m.Called(ctx, authorID, language) + return args.String(0), args.Error(1) +} + type mockWeaviateWrapper struct { mock.Mock } diff --git a/internal/data/sql/localization_repository.go b/internal/data/sql/localization_repository.go index be33aef..3fac30e 100644 --- a/internal/data/sql/localization_repository.go +++ b/internal/data/sql/localization_repository.go @@ -2,6 +2,7 @@ package sql import ( "context" + "tercul/internal/domain" "tercul/internal/domain/localization" "gorm.io/gorm" @@ -36,3 +37,17 @@ func (r *localizationRepository) GetTranslations(ctx context.Context, keys []str } return result, nil } + +func (r *localizationRepository) GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) { + var translation domain.Translation + err := r.db.WithContext(ctx). + Where("translatable_type = ? AND translatable_id = ? AND language = ?", "authors", authorID, language). + First(&translation).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return "", nil + } + return "", err + } + return translation.Content, nil +} diff --git a/internal/data/sql/translation_repository.go b/internal/data/sql/translation_repository.go index 28e332e..1d5da94 100644 --- a/internal/data/sql/translation_repository.go +++ b/internal/data/sql/translation_repository.go @@ -23,7 +23,7 @@ func NewTranslationRepository(db *gorm.DB) domain.TranslationRepository { // ListByWorkID finds translations by work ID func (r *translationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) { var translations []domain.Translation - if err := r.db.WithContext(ctx).Where("translatable_id = ? AND translatable_type = ?", workID, "Work").Find(&translations).Error; err != nil { + if err := r.db.WithContext(ctx).Where("translatable_id = ? AND translatable_type = ?", workID, "works").Find(&translations).Error; err != nil { return nil, err } return translations, nil diff --git a/internal/domain/entities.go b/internal/domain/entities.go index ced4d4a..0a339f3 100644 --- a/internal/domain/entities.go +++ b/internal/domain/entities.go @@ -209,7 +209,7 @@ type Work struct { Type WorkType `gorm:"size:50;default:'other'"` Status WorkStatus `gorm:"size:50;default:'draft'"` PublishedAt *time.Time - Translations []Translation `gorm:"polymorphic:Translatable"` + Translations []*Translation `gorm:"polymorphic:Translatable"` Authors []*Author `gorm:"many2many:work_authors"` Tags []*Tag `gorm:"many2many:work_tags"` Categories []*Category `gorm:"many2many:work_categories"` @@ -239,7 +239,7 @@ type Author struct { Place *Place `gorm:"foreignKey:PlaceID"` AddressID *uint Address *Address `gorm:"foreignKey:AddressID"` - Translations []Translation `gorm:"polymorphic:Translatable"` + Translations []*Translation `gorm:"polymorphic:Translatable"` Copyrights []*Copyright `gorm:"many2many:author_copyrights;constraint:OnDelete:CASCADE"` Monetizations []*Monetization `gorm:"many2many:author_monetizations;constraint:OnDelete:CASCADE"` } @@ -271,7 +271,7 @@ type Book struct { Authors []*Author `gorm:"many2many:book_authors"` PublisherID *uint Publisher *Publisher `gorm:"foreignKey:PublisherID"` - Translations []Translation `gorm:"polymorphic:Translatable"` + Translations []*Translation `gorm:"polymorphic:Translatable"` Copyrights []*Copyright `gorm:"many2many:book_copyrights;constraint:OnDelete:CASCADE"` Monetizations []*Monetization `gorm:"many2many:book_monetizations;constraint:OnDelete:CASCADE"` } @@ -290,7 +290,7 @@ type Publisher struct { Books []*Book `gorm:"foreignKey:PublisherID"` CountryID *uint Country *Country `gorm:"foreignKey:CountryID"` - Translations []Translation `gorm:"polymorphic:Translatable"` + Translations []*Translation `gorm:"polymorphic:Translatable"` Copyrights []*Copyright `gorm:"many2many:publisher_copyrights;constraint:OnDelete:CASCADE"` Monetizations []*Monetization `gorm:"many2many:publisher_monetizations;constraint:OnDelete:CASCADE"` } @@ -308,7 +308,7 @@ type Source struct { URL string `gorm:"size:512"` Status SourceStatus `gorm:"size:50;default:'active'"` Works []*Work `gorm:"many2many:work_sources"` - Translations []Translation `gorm:"polymorphic:Translatable"` + Translations []*Translation `gorm:"polymorphic:Translatable"` Copyrights []*Copyright `gorm:"many2many:source_copyrights;constraint:OnDelete:CASCADE"` Monetizations []*Monetization `gorm:"many2many:source_monetizations;constraint:OnDelete:CASCADE"` } diff --git a/internal/domain/localization/repo.go b/internal/domain/localization/repo.go index fc51eee..636b4dd 100644 --- a/internal/domain/localization/repo.go +++ b/internal/domain/localization/repo.go @@ -8,4 +8,5 @@ import ( type LocalizationRepository interface { GetTranslation(ctx context.Context, key string, language string) (string, error) GetTranslations(ctx context.Context, keys []string, language string) (map[string]string, error) + GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) } \ No newline at end of file diff --git a/internal/testutil/integration_test_utils.go b/internal/testutil/integration_test_utils.go index 2721d89..4be68b2 100644 --- a/internal/testutil/integration_test_utils.go +++ b/internal/testutil/integration_test_utils.go @@ -199,7 +199,7 @@ func (s *IntegrationTestSuite) CreateTestWork(title, language string, content st Content: content, Language: language, TranslatableID: createdWork.ID, - TranslatableType: "Work", + TranslatableType: "works", } _, err = s.App.Translation.Commands.CreateTranslation(context.Background(), translationInput) s.Require().NoError(err) @@ -214,7 +214,7 @@ func (s *IntegrationTestSuite) CreateTestTranslation(workID uint, language, cont Content: content, Language: language, TranslatableID: workID, - TranslatableType: "Work", + TranslatableType: "works", } createdTranslation, err := s.App.Translation.Commands.CreateTranslation(context.Background(), translationInput) s.Require().NoError(err) diff --git a/internal/testutil/simple_test_utils.go b/internal/testutil/simple_test_utils.go index c4ab885..84e469b 100644 --- a/internal/testutil/simple_test_utils.go +++ b/internal/testutil/simple_test_utils.go @@ -55,6 +55,11 @@ func (m *MockLocalizationRepository) GetTranslations(ctx context.Context, keys [ return results, nil } +// GetAuthorBiography is a mock implementation of the GetAuthorBiography method. +func (m *MockLocalizationRepository) GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) { + return "This is a mock biography.", nil +} + // GetResolver returns a minimal GraphQL resolver for testing func (s *SimpleTestSuite) GetResolver() *graph.Resolver { var mockLocalizationRepo domain_localization.LocalizationRepository = &MockLocalizationRepository{}