mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 01:41:34 +00:00
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:
parent
85f052b2d6
commit
1cb434bbe7
@ -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)
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
13
internal/app/localization/commands.go
Normal file
13
internal/app/localization/commands.go
Normal 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}
|
||||
}
|
||||
31
internal/app/localization/queries.go
Normal file
31
internal/app/localization/queries.go
Normal 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)
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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{}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user