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.
This commit is contained in:
google-labs-jules[bot] 2025-10-03 01:44:47 +00:00
parent 85f052b2d6
commit 1cb434bbe7
14 changed files with 123 additions and 43 deletions

View File

@ -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)

View File

@ -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),

View File

@ -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,
}
}

View File

@ -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}
}

View File

@ -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)
}

View File

@ -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),
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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"`
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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{}