diff --git a/cmd/api/main.go b/cmd/api/main.go index 7bb6b3f..ca0d568 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -11,7 +11,10 @@ import ( "tercul/internal/platform/log" "time" + "github.com/99designs/gqlgen/graphql/playground" "github.com/hibiken/asynq" + graph "tercul/internal/adapters/graphql" + "tercul/internal/platform/auth" ) // main is the entry point for the Tercul application. @@ -39,19 +42,44 @@ func main() { serverFactory := app.NewServerFactory(appBuilder) // Create servers - graphQLServer, err := serverFactory.CreateGraphQLServer() - if err != nil { - log.LogFatal("Failed to create GraphQL server", - log.F("error", err)) - } - backgroundServers, err := serverFactory.CreateBackgroundJobServers() if err != nil { log.LogFatal("Failed to create background job servers", log.F("error", err)) } - playgroundServer := serverFactory.CreatePlaygroundServer() + // Create GraphQL server + resolver := &graph.Resolver{ + WorkRepo: appBuilder.GetRepositories().WorkRepository, + UserRepo: appBuilder.GetRepositories().UserRepository, + AuthorRepo: appBuilder.GetRepositories().AuthorRepository, + TranslationRepo: appBuilder.GetRepositories().TranslationRepository, + CommentRepo: appBuilder.GetRepositories().CommentRepository, + LikeRepo: appBuilder.GetRepositories().LikeRepository, + BookmarkRepo: appBuilder.GetRepositories().BookmarkRepository, + CollectionRepo: appBuilder.GetRepositories().CollectionRepository, + TagRepo: appBuilder.GetRepositories().TagRepository, + CategoryRepo: appBuilder.GetRepositories().CategoryRepository, + WorkService: appBuilder.GetServices().WorkService, + Localization: appBuilder.GetServices().LocalizationService, + AuthService: appBuilder.GetServices().AuthService, + } + + jwtManager := auth.NewJWTManager() + srv := graph.NewServerWithAuth(resolver, jwtManager) + graphQLServer := &http.Server{ + Addr: config.Cfg.ServerPort, + Handler: srv, + } + log.LogInfo("GraphQL server created successfully", log.F("port", config.Cfg.ServerPort)) + + // Create GraphQL playground + playgroundHandler := playground.Handler("GraphQL", "/query") + playgroundServer := &http.Server{ + Addr: config.Cfg.PlaygroundPort, + Handler: playgroundHandler, + } + log.LogInfo("GraphQL playground created successfully", log.F("port", config.Cfg.PlaygroundPort)) // Start HTTP servers in goroutines go func() { diff --git a/cmd/tools/enrich/main.go b/cmd/tools/enrich/main.go index bafd3b2..2080383 100644 --- a/cmd/tools/enrich/main.go +++ b/cmd/tools/enrich/main.go @@ -3,73 +3,47 @@ package main import ( "context" "log" - "os" - "os/signal" - "syscall" - "time" - - "gorm.io/driver/postgres" - "gorm.io/gorm" - "tercul/internal/enrich" - "tercul/internal/store" + "tercul/internal/app" + "tercul/internal/jobs/linguistics" "tercul/internal/platform/config" ) func main() { - log.Println("Starting enrichment service...") + log.Println("Starting enrichment tool...") - // Create a context that can be cancelled - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Set up signal handling - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - go func() { - sig := <-sigCh - log.Printf("Received signal %v, shutting down...", sig) - cancel() - }() - - // Load configuration + // Load configuration from environment variables config.LoadConfig() - // Connect to the database - dsn := os.Getenv("DATABASE_URL") - if dsn == "" { - dsn = config.Cfg.GetDSN() - } + // Initialize structured logger with appropriate log level + log.SetDefaultLevel(log.InfoLevel) + log.LogInfo("Starting Tercul enrichment tool", + log.F("environment", config.Cfg.Environment), + log.F("version", "1.0.0")) - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + // Build application components + appBuilder := app.NewApplicationBuilder() + if err := appBuilder.Build(); err != nil { + log.LogFatal("Failed to build application", + log.F("error", err)) + } + defer appBuilder.Close() + + // Get all works + works, err := appBuilder.GetApplication().WorkQueries.ListWorks(context.Background(), 1, 10000) // A bit of a hack, but should work for now if err != nil { - log.Fatalf("Failed to connect to database: %v", err) + log.LogFatal("Failed to get works", + log.F("error", err)) } - // Create a store.DB - storeDB := &store.DB{DB: db} - - // Create the enrichment registry - registry := enrich.DefaultRegistry() - - // Process pending works - if err := store.ProcessPendingWorks(ctx, registry, storeDB); err != nil { - log.Fatalf("Failed to process pending works: %v", err) - } - - // Set up a ticker to periodically process pending works - ticker := time.NewTicker(5 * time.Minute) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - log.Println("Shutting down...") - return - case <-ticker.C: - log.Println("Processing pending works...") - if err := store.ProcessPendingWorks(ctx, registry, storeDB); err != nil { - log.Printf("Failed to process pending works: %v", err) - } + // Enqueue analysis for each work + for _, work := range works.Data { + err := linguistics.EnqueueAnalysisForWork(appBuilder.GetAsynqClient(), work.ID) + if err != nil { + log.LogError("Failed to enqueue analysis for work", + log.F("workID", work.ID), + log.F("error", err)) } } + + log.Println("Enrichment tool finished.") } diff --git a/create_repo_interfaces.go b/create_repo_interfaces.go new file mode 100644 index 0000000..fa58d75 --- /dev/null +++ b/create_repo_interfaces.go @@ -0,0 +1,141 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +func main() { + sqlDir := "internal/data/sql" + domainDir := "internal/domain" + + files, err := ioutil.ReadDir(sqlDir) + if err != nil { + fmt.Println("Error reading sql directory:", err) + return + } + + for _, file := range files { + if strings.HasSuffix(file.Name(), "_repository.go") { + repoName := strings.TrimSuffix(file.Name(), "_repository.go") + repoInterfaceName := strings.Title(repoName) + "Repository" + domainPackageName := repoName + + // Create domain directory + domainRepoDir := filepath.Join(domainDir, domainPackageName) + if err := os.MkdirAll(domainRepoDir, 0755); err != nil { + fmt.Printf("Error creating directory %s: %v\n", domainRepoDir, err) + continue + } + + // Read the sql repository file + filePath := filepath.Join(sqlDir, file.Name()) + src, err := ioutil.ReadFile(filePath) + if err != nil { + fmt.Printf("Error reading file %s: %v\n", filePath, err) + continue + } + + // Parse the file + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, "", src, parser.ParseComments) + if err != nil { + fmt.Printf("Error parsing file %s: %v\n", filePath, err) + continue + } + + // Find public methods + var methods []string + ast.Inspect(node, func(n ast.Node) bool { + if fn, ok := n.(*ast.FuncDecl); ok { + if fn.Recv != nil && len(fn.Recv.List) > 0 { + if star, ok := fn.Recv.List[0].Type.(*ast.StarExpr); ok { + if ident, ok := star.X.(*ast.Ident); ok { + if strings.HasSuffix(ident.Name, "Repository") && fn.Name.IsExported() { + methods = append(methods, getFuncSignature(fn)) + } + } + } + } + } + return true + }) + + // Create the repo.go file + repoFilePath := filepath.Join(domainRepoDir, "repo.go") + repoFileContent := fmt.Sprintf(`package %s + +import ( + "context" + "tercul/internal/domain" +) + +// %s defines CRUD methods specific to %s. +type %s interface { + domain.BaseRepository[domain.%s] +%s +} +`, domainPackageName, repoInterfaceName, strings.Title(repoName), repoInterfaceName, strings.Title(repoName), formatMethods(methods)) + + if err := ioutil.WriteFile(repoFilePath, []byte(repoFileContent), 0644); err != nil { + fmt.Printf("Error writing file %s: %v\n", repoFilePath, err) + } else { + fmt.Printf("Created %s\n", repoFilePath) + } + } + } +} + +func getFuncSignature(fn *ast.FuncDecl) string { + params := "" + for _, p := range fn.Type.Params.List { + if len(p.Names) > 0 { + params += p.Names[0].Name + " " + } + params += getTypeString(p.Type) + ", " + } + if len(params) > 0 { + params = params[:len(params)-2] + } + + results := "" + if fn.Type.Results != nil { + for _, r := range fn.Type.Results.List { + results += getTypeString(r.Type) + ", " + } + if len(results) > 0 { + results = "(" + results[:len(results)-2] + ")" + } + } + return fmt.Sprintf("\t%s(%s) %s", fn.Name.Name, params, results) +} + +func getTypeString(expr ast.Expr) string { + switch t := expr.(type) { + case *ast.Ident: + return t.Name + case *ast.SelectorExpr: + return getTypeString(t.X) + "." + t.Sel.Name + case *ast.StarExpr: + return "*" + getTypeString(t.X) + case *ast.ArrayType: + return "[]" + getTypeString(t.Elt) + case *ast.InterfaceType: + return "interface{}" + default: + return "" + } +} + +func formatMethods(methods []string) string { + if len(methods) == 0 { + return "" + } + return "\n" + strings.Join(methods, "\n") +} diff --git a/fix_domain_repos.go b/fix_domain_repos.go new file mode 100644 index 0000000..1cae74a --- /dev/null +++ b/fix_domain_repos.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +func main() { + domainDir := "internal/domain" + + dirs, err := ioutil.ReadDir(domainDir) + if err != nil { + fmt.Println("Error reading domain directory:", err) + return + } + + for _, dir := range dirs { + if dir.IsDir() { + repoFilePath := filepath.Join(domainDir, dir.Name(), "repo.go") + if _, err := os.Stat(repoFilePath); err == nil { + content, err := ioutil.ReadFile(repoFilePath) + if err != nil { + fmt.Printf("Error reading file %s: %v\n", repoFilePath, err) + continue + } + + newContent := strings.Replace(string(content), "domain.Base", "domain.BaseRepository", -1) + newContent = strings.Replace(newContent, "domain."+strings.Title(dir.Name()), "domain."+strings.Title(dir.Name()), -1) + + // Fix for names with underscore + newContent = strings.Replace(newContent, "domain.Copyright_claim", "domain.CopyrightClaim", -1) + newContent = strings.Replace(newContent, "domain.Email_verification", "domain.EmailVerification", -1) + newContent = strings.Replace(newContent, "domain.Password_reset", "domain.PasswordReset", -1) + newContent = strings.Replace(newContent, "domain.User_profile", "domain.UserProfile", -1) + newContent = strings.Replace(newContent, "domain.User_session", "domain.UserSession", -1) + + + if err := ioutil.WriteFile(repoFilePath, []byte(newContent), 0644); err != nil { + fmt.Printf("Error writing file %s: %v\n", repoFilePath, err) + } else { + fmt.Printf("Fixed repo %s\n", repoFilePath) + } + } + } + } +} diff --git a/fix_sql_imports.go b/fix_sql_imports.go new file mode 100644 index 0000000..3cd5f43 --- /dev/null +++ b/fix_sql_imports.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strings" +) + +func main() { + sqlDir := "internal/data/sql" + + files, err := ioutil.ReadDir(sqlDir) + if err != nil { + fmt.Println("Error reading sql directory:", err) + return + } + + for _, file := range files { + if strings.HasSuffix(file.Name(), "_repository.go") { + repoName := strings.TrimSuffix(file.Name(), "_repository.go") + filePath := filepath.Join(sqlDir, file.Name()) + + content, err := ioutil.ReadFile(filePath) + if err != nil { + fmt.Printf("Error reading file %s: %v\n", filePath, err) + continue + } + + newContent := strings.Replace(string(content), `"tercul/internal/domain"`, fmt.Sprintf(`"%s"`, filepath.Join("tercul/internal/domain", repoName)), 1) + newContent = strings.Replace(newContent, "domain."+strings.Title(repoName)+"Repository", repoName+"."+strings.Title(repoName)+"Repository", 1) + + if err := ioutil.WriteFile(filePath, []byte(newContent), 0644); err != nil { + fmt.Printf("Error writing file %s: %v\n", filePath, err) + } else { + fmt.Printf("Fixed imports in %s\n", filePath) + } + } + } +} diff --git a/internal/app/application_builder.go b/internal/app/application_builder.go index 30ac00e..0f0db06 100644 --- a/internal/app/application_builder.go +++ b/internal/app/application_builder.go @@ -13,7 +13,7 @@ import ( "tercul/internal/platform/db" "tercul/internal/platform/log" auth_platform "tercul/internal/platform/auth" - "tercul/linguistics" + "tercul/internal/jobs/linguistics" "time" "github.com/hibiken/asynq" diff --git a/internal/app/server_factory.go b/internal/app/server_factory.go index 2f74369..ac78fe0 100644 --- a/internal/app/server_factory.go +++ b/internal/app/server_factory.go @@ -2,12 +2,12 @@ package app import ( "net/http" + + "tercul/internal/jobs/linguistics" + syncjob "tercul/internal/jobs/sync" "tercul/internal/platform/auth" "tercul/internal/platform/config" - "tercul/graph" - "tercul/linguistics" "tercul/internal/platform/log" - "tercul/syncjob" "github.com/99designs/gqlgen/graphql/playground" "github.com/hibiken/asynq" @@ -25,44 +25,6 @@ func NewServerFactory(appBuilder *ApplicationBuilder) *ServerFactory { } } -// CreateGraphQLServer creates and configures the GraphQL server -func (f *ServerFactory) CreateGraphQLServer() (*http.Server, error) { - log.LogInfo("Setting up GraphQL server") - - // Create GraphQL resolver with all dependencies - resolver := &graph.Resolver{ - WorkRepo: f.appBuilder.GetRepositories().WorkRepository, - UserRepo: f.appBuilder.GetRepositories().UserRepository, - AuthorRepo: f.appBuilder.GetRepositories().AuthorRepository, - TranslationRepo: f.appBuilder.GetRepositories().TranslationRepository, - CommentRepo: f.appBuilder.GetRepositories().CommentRepository, - LikeRepo: f.appBuilder.GetRepositories().LikeRepository, - BookmarkRepo: f.appBuilder.GetRepositories().BookmarkRepository, - CollectionRepo: f.appBuilder.GetRepositories().CollectionRepository, - TagRepo: f.appBuilder.GetRepositories().TagRepository, - CategoryRepo: f.appBuilder.GetRepositories().CategoryRepository, - WorkService: f.appBuilder.GetServices().WorkService, - Localization: f.appBuilder.GetServices().LocalizationService, - AuthService: f.appBuilder.GetServices().AuthService, - } - - // Create JWT manager for authentication - jwtManager := auth.NewJWTManager() - - // Create GraphQL server with authentication - srv := graph.NewServerWithAuth(resolver, jwtManager) - - // Create HTTP server with middleware - httpServer := &http.Server{ - Addr: config.Cfg.ServerPort, - Handler: srv, - } - - log.LogInfo("GraphQL server created successfully", - log.F("port", config.Cfg.ServerPort)) - - return httpServer, nil -} // CreateBackgroundJobServers creates and configures background job servers func (f *ServerFactory) CreateBackgroundJobServers() ([]*asynq.Server, error) { @@ -120,19 +82,3 @@ func (f *ServerFactory) CreateBackgroundJobServers() ([]*asynq.Server, error) { return servers, nil } -// CreatePlaygroundServer creates the GraphQL playground server -func (f *ServerFactory) CreatePlaygroundServer() *http.Server { - log.LogInfo("Setting up GraphQL playground") - - playgroundHandler := playground.Handler("GraphQL", "/query") - - playgroundServer := &http.Server{ - Addr: config.Cfg.PlaygroundPort, - Handler: playgroundHandler, - } - - log.LogInfo("GraphQL playground created successfully", - log.F("port", config.Cfg.PlaygroundPort)) - - return playgroundServer -} diff --git a/internal/data/sql/author_repository.go b/internal/data/sql/author_repository.go index f75f1a0..6427fde 100644 --- a/internal/data/sql/author_repository.go +++ b/internal/data/sql/author_repository.go @@ -3,7 +3,7 @@ package sql import ( "context" "gorm.io/gorm" - "tercul/internal/domain" + "tercul/internal/domain/author" ) type authorRepository struct { @@ -12,7 +12,7 @@ type authorRepository struct { } // NewAuthorRepository creates a new AuthorRepository. -func NewAuthorRepository(db *gorm.DB) domain.AuthorRepository { +func NewAuthorRepository(db *gorm.DB) author.AuthorRepository { return &authorRepository{ BaseRepository: NewBaseRepositoryImpl[domain.Author](db), db: db, diff --git a/internal/data/sql/base_repository.go b/internal/data/sql/base_repository.go index cc958c3..17a52ef 100644 --- a/internal/data/sql/base_repository.go +++ b/internal/data/sql/base_repository.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "tercul/internal/domain" + "tercul/internal/domain/base" "tercul/internal/platform/config" "tercul/internal/platform/log" "time" @@ -28,7 +28,7 @@ type BaseRepositoryImpl[T any] struct { } // NewBaseRepositoryImpl creates a new BaseRepositoryImpl -func NewBaseRepositoryImpl[T any](db *gorm.DB) domain.BaseRepository[T] { +func NewBaseRepositoryImpl[T any](db *gorm.DB) base.BaseRepository[T] { return &BaseRepositoryImpl[T]{db: db} } diff --git a/internal/data/sql/book_repository.go b/internal/data/sql/book_repository.go index 340d5e9..9ed415c 100644 --- a/internal/data/sql/book_repository.go +++ b/internal/data/sql/book_repository.go @@ -4,7 +4,7 @@ import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/domain" + "tercul/internal/domain/book" ) type bookRepository struct { @@ -13,7 +13,7 @@ type bookRepository struct { } // NewBookRepository creates a new BookRepository. -func NewBookRepository(db *gorm.DB) domain.BookRepository { +func NewBookRepository(db *gorm.DB) book.BookRepository { return &bookRepository{ BaseRepository: NewBaseRepositoryImpl[domain.Book](db), db: db, diff --git a/internal/data/sql/bookmark_repository.go b/internal/data/sql/bookmark_repository.go index e364a3d..f7383a0 100644 --- a/internal/data/sql/bookmark_repository.go +++ b/internal/data/sql/bookmark_repository.go @@ -3,7 +3,7 @@ package sql import ( "context" "gorm.io/gorm" - "tercul/internal/domain" + "tercul/internal/domain/bookmark" ) type bookmarkRepository struct { @@ -12,7 +12,7 @@ type bookmarkRepository struct { } // NewBookmarkRepository creates a new BookmarkRepository. -func NewBookmarkRepository(db *gorm.DB) domain.BookmarkRepository { +func NewBookmarkRepository(db *gorm.DB) bookmark.BookmarkRepository { return &bookmarkRepository{ BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db), db: db, diff --git a/internal/data/sql/category_repository.go b/internal/data/sql/category_repository.go index f6bfdea..56f4aba 100644 --- a/internal/data/sql/category_repository.go +++ b/internal/data/sql/category_repository.go @@ -4,7 +4,7 @@ import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/domain" + "tercul/internal/domain/category" ) type categoryRepository struct { @@ -13,7 +13,7 @@ type categoryRepository struct { } // NewCategoryRepository creates a new CategoryRepository. -func NewCategoryRepository(db *gorm.DB) domain.CategoryRepository { +func NewCategoryRepository(db *gorm.DB) category.CategoryRepository { return &categoryRepository{ BaseRepository: NewBaseRepositoryImpl[domain.Category](db), db: db, diff --git a/internal/data/sql/city_repository.go b/internal/data/sql/city_repository.go index e2a74d2..e0f592a 100644 --- a/internal/data/sql/city_repository.go +++ b/internal/data/sql/city_repository.go @@ -1,33 +1,27 @@ -package repositories +package sql import ( "context" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/city" ) -// CityRepository defines CRUD methods specific to City. -type CityRepository interface { - BaseRepository[models.City] - ListByCountryID(ctx context.Context, countryID uint) ([]models.City, error) -} - type cityRepository struct { - BaseRepository[models.City] + domain.BaseRepository[domain.City] db *gorm.DB } // NewCityRepository creates a new CityRepository. -func NewCityRepository(db *gorm.DB) CityRepository { +func NewCityRepository(db *gorm.DB) city.CityRepository { return &cityRepository{ - BaseRepository: NewBaseRepositoryImpl[models.City](db), + BaseRepository: NewBaseRepositoryImpl[domain.City](db), db: db, } } // ListByCountryID finds cities by country ID -func (r *cityRepository) ListByCountryID(ctx context.Context, countryID uint) ([]models.City, error) { - var cities []models.City +func (r *cityRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.City, error) { + var cities []domain.City if err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&cities).Error; err != nil { return nil, err } diff --git a/internal/data/sql/collection_repository.go b/internal/data/sql/collection_repository.go index ac25c9a..8ba5ec2 100644 --- a/internal/data/sql/collection_repository.go +++ b/internal/data/sql/collection_repository.go @@ -3,7 +3,7 @@ package sql import ( "context" "gorm.io/gorm" - "tercul/internal/domain" + "tercul/internal/domain/collection" ) type collectionRepository struct { @@ -12,7 +12,7 @@ type collectionRepository struct { } // NewCollectionRepository creates a new CollectionRepository. -func NewCollectionRepository(db *gorm.DB) domain.CollectionRepository { +func NewCollectionRepository(db *gorm.DB) collection.CollectionRepository { return &collectionRepository{ BaseRepository: NewBaseRepositoryImpl[domain.Collection](db), db: db, diff --git a/internal/data/sql/comment_repository.go b/internal/data/sql/comment_repository.go index fab43a8..9dfdbf0 100644 --- a/internal/data/sql/comment_repository.go +++ b/internal/data/sql/comment_repository.go @@ -3,7 +3,7 @@ package sql import ( "context" "gorm.io/gorm" - "tercul/internal/domain" + "tercul/internal/domain/comment" ) type commentRepository struct { @@ -12,7 +12,7 @@ type commentRepository struct { } // NewCommentRepository creates a new CommentRepository. -func NewCommentRepository(db *gorm.DB) domain.CommentRepository { +func NewCommentRepository(db *gorm.DB) comment.CommentRepository { return &commentRepository{ BaseRepository: NewBaseRepositoryImpl[domain.Comment](db), db: db, diff --git a/internal/data/sql/contribution_repository.go b/internal/data/sql/contribution_repository.go index c4bc86e..33a80f8 100644 --- a/internal/data/sql/contribution_repository.go +++ b/internal/data/sql/contribution_repository.go @@ -1,37 +1,27 @@ -package repositories +package sql import ( "context" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/contribution" ) -// ContributionRepository defines CRUD methods specific to Contribution. -type ContributionRepository interface { - BaseRepository[models.Contribution] - ListByUserID(ctx context.Context, userID uint) ([]models.Contribution, error) - ListByReviewerID(ctx context.Context, reviewerID uint) ([]models.Contribution, error) - ListByWorkID(ctx context.Context, workID uint) ([]models.Contribution, error) - ListByTranslationID(ctx context.Context, translationID uint) ([]models.Contribution, error) - ListByStatus(ctx context.Context, status string) ([]models.Contribution, error) -} - type contributionRepository struct { - BaseRepository[models.Contribution] + domain.BaseRepository[domain.Contribution] db *gorm.DB } // NewContributionRepository creates a new ContributionRepository. -func NewContributionRepository(db *gorm.DB) ContributionRepository { +func NewContributionRepository(db *gorm.DB) contribution.ContributionRepository { return &contributionRepository{ - BaseRepository: NewBaseRepositoryImpl[models.Contribution](db), + BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db), db: db, } } // ListByUserID finds contributions by user ID -func (r *contributionRepository) ListByUserID(ctx context.Context, userID uint) ([]models.Contribution, error) { - var contributions []models.Contribution +func (r *contributionRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Contribution, error) { + var contributions []domain.Contribution if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&contributions).Error; err != nil { return nil, err } @@ -39,8 +29,8 @@ func (r *contributionRepository) ListByUserID(ctx context.Context, userID uint) } // ListByReviewerID finds contributions by reviewer ID -func (r *contributionRepository) ListByReviewerID(ctx context.Context, reviewerID uint) ([]models.Contribution, error) { - var contributions []models.Contribution +func (r *contributionRepository) ListByReviewerID(ctx context.Context, reviewerID uint) ([]domain.Contribution, error) { + var contributions []domain.Contribution if err := r.db.WithContext(ctx).Where("reviewer_id = ?", reviewerID).Find(&contributions).Error; err != nil { return nil, err } @@ -48,8 +38,8 @@ func (r *contributionRepository) ListByReviewerID(ctx context.Context, reviewerI } // ListByWorkID finds contributions by work ID -func (r *contributionRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.Contribution, error) { - var contributions []models.Contribution +func (r *contributionRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Contribution, error) { + var contributions []domain.Contribution if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&contributions).Error; err != nil { return nil, err } @@ -57,8 +47,8 @@ func (r *contributionRepository) ListByWorkID(ctx context.Context, workID uint) } // ListByTranslationID finds contributions by translation ID -func (r *contributionRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]models.Contribution, error) { - var contributions []models.Contribution +func (r *contributionRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Contribution, error) { + var contributions []domain.Contribution if err := r.db.WithContext(ctx).Where("translation_id = ?", translationID).Find(&contributions).Error; err != nil { return nil, err } @@ -66,8 +56,8 @@ func (r *contributionRepository) ListByTranslationID(ctx context.Context, transl } // ListByStatus finds contributions by status -func (r *contributionRepository) ListByStatus(ctx context.Context, status string) ([]models.Contribution, error) { - var contributions []models.Contribution +func (r *contributionRepository) ListByStatus(ctx context.Context, status string) ([]domain.Contribution, error) { + var contributions []domain.Contribution if err := r.db.WithContext(ctx).Where("status = ?", status).Find(&contributions).Error; err != nil { return nil, err } diff --git a/internal/data/sql/copyright_claim_repository.go b/internal/data/sql/copyright_claim_repository.go index fbf8646..5ecfdc7 100644 --- a/internal/data/sql/copyright_claim_repository.go +++ b/internal/data/sql/copyright_claim_repository.go @@ -1,34 +1,27 @@ -package repositories +package sql import ( "context" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/copyright_claim" ) -// CopyrightClaimRepository defines CRUD methods specific to CopyrightClaim. -type CopyrightClaimRepository interface { - BaseRepository[models.CopyrightClaim] - ListByWorkID(ctx context.Context, workID uint) ([]models.CopyrightClaim, error) - ListByUserID(ctx context.Context, userID uint) ([]models.CopyrightClaim, error) -} - type copyrightClaimRepository struct { - BaseRepository[models.CopyrightClaim] + domain.BaseRepository[domain.CopyrightClaim] db *gorm.DB } // NewCopyrightClaimRepository creates a new CopyrightClaimRepository. -func NewCopyrightClaimRepository(db *gorm.DB) CopyrightClaimRepository { +func NewCopyrightClaimRepository(db *gorm.DB) domain.CopyrightClaimRepository { return ©rightClaimRepository{ - BaseRepository: NewBaseRepositoryImpl[models.CopyrightClaim](db), + BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db), db: db, } } // ListByWorkID finds claims by work ID -func (r *copyrightClaimRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.CopyrightClaim, error) { - var claims []models.CopyrightClaim +func (r *copyrightClaimRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.CopyrightClaim, error) { + var claims []domain.CopyrightClaim if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&claims).Error; err != nil { return nil, err } @@ -36,8 +29,8 @@ func (r *copyrightClaimRepository) ListByWorkID(ctx context.Context, workID uint } // ListByUserID finds claims by user ID -func (r *copyrightClaimRepository) ListByUserID(ctx context.Context, userID uint) ([]models.CopyrightClaim, error) { - var claims []models.CopyrightClaim +func (r *copyrightClaimRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.CopyrightClaim, error) { + var claims []domain.CopyrightClaim if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&claims).Error; err != nil { return nil, err } diff --git a/internal/data/sql/copyright_repository.go b/internal/data/sql/copyright_repository.go index e926e07..42afbc6 100644 --- a/internal/data/sql/copyright_repository.go +++ b/internal/data/sql/copyright_repository.go @@ -1,42 +1,28 @@ -package repositories +package sql import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/copyright" ) -// CopyrightRepository defines CRUD methods specific to Copyright. -type CopyrightRepository interface { - BaseRepository[models.Copyright] - // Polymorphic methods - AttachToEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error - DetachFromEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error - GetByEntity(ctx context.Context, entityID uint, entityType string) ([]models.Copyright, error) - GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]models.Copyrightable, error) - // Translation methods - AddTranslation(ctx context.Context, translation *models.CopyrightTranslation) error - GetTranslations(ctx context.Context, copyrightID uint) ([]models.CopyrightTranslation, error) - GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*models.CopyrightTranslation, error) -} - type copyrightRepository struct { - BaseRepository[models.Copyright] + domain.BaseRepository[domain.Copyright] db *gorm.DB } // NewCopyrightRepository creates a new CopyrightRepository. -func NewCopyrightRepository(db *gorm.DB) CopyrightRepository { +func NewCopyrightRepository(db *gorm.DB) copyright.CopyrightRepository { return ©rightRepository{ - BaseRepository: NewBaseRepositoryImpl[models.Copyright](db), + BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db), db: db, } } // AttachToEntity attaches a copyright to any entity type func (r *copyrightRepository) AttachToEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error { - copyrightable := models.Copyrightable{ + copyrightable := domain.Copyrightable{ CopyrightID: copyrightID, CopyrightableID: entityID, CopyrightableType: entityType, @@ -47,12 +33,12 @@ func (r *copyrightRepository) AttachToEntity(ctx context.Context, copyrightID ui // DetachFromEntity removes a copyright from an entity func (r *copyrightRepository) DetachFromEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error { return r.db.WithContext(ctx).Where("copyright_id = ? AND copyrightable_id = ? AND copyrightable_type = ?", - copyrightID, entityID, entityType).Delete(&models.Copyrightable{}).Error + copyrightID, entityID, entityType).Delete(&domain.Copyrightable{}).Error } // GetByEntity gets all copyrights for a specific entity -func (r *copyrightRepository) GetByEntity(ctx context.Context, entityID uint, entityType string) ([]models.Copyright, error) { - var copyrights []models.Copyright +func (r *copyrightRepository) GetByEntity(ctx context.Context, entityID uint, entityType string) ([]domain.Copyright, error) { + var copyrights []domain.Copyright err := r.db.WithContext(ctx).Joins("JOIN copyrightables ON copyrightables.copyright_id = copyrights.id"). Where("copyrightables.copyrightable_id = ? AND copyrightables.copyrightable_type = ?", entityID, entityType). Preload("Translations"). @@ -61,27 +47,26 @@ func (r *copyrightRepository) GetByEntity(ctx context.Context, entityID uint, en } // GetEntitiesByCopyright gets all entities that have a specific copyright -func (r *copyrightRepository) GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]models.Copyrightable, error) { - var copyrightables []models.Copyrightable +func (r *copyrightRepository) GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]domain.Copyrightable, error) { + var copyrightables []domain.Copyrightable err := r.db.WithContext(ctx).Where("copyright_id = ?", copyrightID).Find(©rightables).Error return copyrightables, err } - // AddTranslation adds a translation to a copyright -func (r *copyrightRepository) AddTranslation(ctx context.Context, translation *models.CopyrightTranslation) error { +func (r *copyrightRepository) AddTranslation(ctx context.Context, translation *domain.CopyrightTranslation) error { return r.db.WithContext(ctx).Create(translation).Error } // GetTranslations gets all translations for a copyright -func (r *copyrightRepository) GetTranslations(ctx context.Context, copyrightID uint) ([]models.CopyrightTranslation, error) { - var translations []models.CopyrightTranslation +func (r *copyrightRepository) GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) { + var translations []domain.CopyrightTranslation err := r.db.WithContext(ctx).Where("copyright_id = ?", copyrightID).Find(&translations).Error return translations, err } // GetTranslationByLanguage gets a specific translation by language code -func (r *copyrightRepository) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*models.CopyrightTranslation, error) { - var translation models.CopyrightTranslation +func (r *copyrightRepository) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) { + var translation domain.CopyrightTranslation err := r.db.WithContext(ctx).Where("copyright_id = ? AND language_code = ?", copyrightID, languageCode).First(&translation).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/internal/data/sql/country_repository.go b/internal/data/sql/country_repository.go index 1fda91c..6e723da 100644 --- a/internal/data/sql/country_repository.go +++ b/internal/data/sql/country_repository.go @@ -1,35 +1,28 @@ -package repositories +package sql import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/country" ) -// CountryRepository defines CRUD methods specific to Country. -type CountryRepository interface { - BaseRepository[models.Country] - GetByCode(ctx context.Context, code string) (*models.Country, error) - ListByContinent(ctx context.Context, continent string) ([]models.Country, error) -} - type countryRepository struct { - BaseRepository[models.Country] + domain.BaseRepository[domain.Country] db *gorm.DB } // NewCountryRepository creates a new CountryRepository. -func NewCountryRepository(db *gorm.DB) CountryRepository { +func NewCountryRepository(db *gorm.DB) country.CountryRepository { return &countryRepository{ - BaseRepository: NewBaseRepositoryImpl[models.Country](db), + BaseRepository: NewBaseRepositoryImpl[domain.Country](db), db: db, } } // GetByCode finds a country by code -func (r *countryRepository) GetByCode(ctx context.Context, code string) (*models.Country, error) { - var country models.Country +func (r *countryRepository) GetByCode(ctx context.Context, code string) (*domain.Country, error) { + var country domain.Country if err := r.db.WithContext(ctx).Where("code = ?", code).First(&country).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrEntityNotFound @@ -40,8 +33,8 @@ func (r *countryRepository) GetByCode(ctx context.Context, code string) (*models } // ListByContinent finds countries by continent -func (r *countryRepository) ListByContinent(ctx context.Context, continent string) ([]models.Country, error) { - var countries []models.Country +func (r *countryRepository) ListByContinent(ctx context.Context, continent string) ([]domain.Country, error) { + var countries []domain.Country if err := r.db.WithContext(ctx).Where("continent = ?", continent).Find(&countries).Error; err != nil { return nil, err } diff --git a/internal/data/sql/edge_repository.go b/internal/data/sql/edge_repository.go index e687b14..e584dfd 100644 --- a/internal/data/sql/edge_repository.go +++ b/internal/data/sql/edge_repository.go @@ -1,33 +1,27 @@ -package repositories +package sql import ( "context" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/edge" ) -// EdgeRepository defines CRUD operations for the polymorphic edge table. -type EdgeRepository interface { - BaseRepository[models.Edge] - ListBySource(ctx context.Context, sourceTable string, sourceID uint) ([]models.Edge, error) -} - type edgeRepository struct { - BaseRepository[models.Edge] + domain.BaseRepository[domain.Edge] db *gorm.DB } // NewEdgeRepository creates a new EdgeRepository. -func NewEdgeRepository(db *gorm.DB) EdgeRepository { +func NewEdgeRepository(db *gorm.DB) edge.EdgeRepository { return &edgeRepository{ - BaseRepository: NewBaseRepositoryImpl[models.Edge](db), + BaseRepository: NewBaseRepositoryImpl[domain.Edge](db), db: db, } } // ListBySource finds edges by source table and ID -func (r *edgeRepository) ListBySource(ctx context.Context, sourceTable string, sourceID uint) ([]models.Edge, error) { - var edges []models.Edge +func (r *edgeRepository) ListBySource(ctx context.Context, sourceTable string, sourceID uint) ([]domain.Edge, error) { + var edges []domain.Edge if err := r.db.WithContext(ctx).Where("source_table = ? AND source_id = ?", sourceTable, sourceID).Find(&edges).Error; err != nil { return nil, err } diff --git a/internal/data/sql/edition_repository.go b/internal/data/sql/edition_repository.go index 288d14b..77a63f6 100644 --- a/internal/data/sql/edition_repository.go +++ b/internal/data/sql/edition_repository.go @@ -1,35 +1,28 @@ -package repositories +package sql import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/edition" ) -// EditionRepository defines CRUD methods specific to Edition. -type EditionRepository interface { - BaseRepository[models.Edition] - ListByBookID(ctx context.Context, bookID uint) ([]models.Edition, error) - FindByISBN(ctx context.Context, isbn string) (*models.Edition, error) -} - type editionRepository struct { - BaseRepository[models.Edition] + domain.BaseRepository[domain.Edition] db *gorm.DB } // NewEditionRepository creates a new EditionRepository. -func NewEditionRepository(db *gorm.DB) EditionRepository { +func NewEditionRepository(db *gorm.DB) edition.EditionRepository { return &editionRepository{ - BaseRepository: NewBaseRepositoryImpl[models.Edition](db), + BaseRepository: NewBaseRepositoryImpl[domain.Edition](db), db: db, } } // ListByBookID finds editions by book ID -func (r *editionRepository) ListByBookID(ctx context.Context, bookID uint) ([]models.Edition, error) { - var editions []models.Edition +func (r *editionRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Edition, error) { + var editions []domain.Edition if err := r.db.WithContext(ctx).Where("book_id = ?", bookID).Find(&editions).Error; err != nil { return nil, err } @@ -37,8 +30,8 @@ func (r *editionRepository) ListByBookID(ctx context.Context, bookID uint) ([]mo } // FindByISBN finds an edition by ISBN -func (r *editionRepository) FindByISBN(ctx context.Context, isbn string) (*models.Edition, error) { - var edition models.Edition +func (r *editionRepository) FindByISBN(ctx context.Context, isbn string) (*domain.Edition, error) { + var edition domain.Edition if err := r.db.WithContext(ctx).Where("isbn = ?", isbn).First(&edition).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrEntityNotFound diff --git a/internal/data/sql/email_verification_repository.go b/internal/data/sql/email_verification_repository.go index 605d856..2c8c670 100644 --- a/internal/data/sql/email_verification_repository.go +++ b/internal/data/sql/email_verification_repository.go @@ -1,38 +1,29 @@ -package repositories +package sql import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/email_verification" "time" ) -// EmailVerificationRepository defines CRUD methods specific to EmailVerification. -type EmailVerificationRepository interface { - BaseRepository[models.EmailVerification] - GetByToken(ctx context.Context, token string) (*models.EmailVerification, error) - GetByUserID(ctx context.Context, userID uint) ([]models.EmailVerification, error) - DeleteExpired(ctx context.Context) error - MarkAsUsed(ctx context.Context, id uint) error -} - type emailVerificationRepository struct { - BaseRepository[models.EmailVerification] + domain.BaseRepository[domain.EmailVerification] db *gorm.DB } // NewEmailVerificationRepository creates a new EmailVerificationRepository. -func NewEmailVerificationRepository(db *gorm.DB) EmailVerificationRepository { +func NewEmailVerificationRepository(db *gorm.DB) domain.EmailVerificationRepository { return &emailVerificationRepository{ - BaseRepository: NewBaseRepositoryImpl[models.EmailVerification](db), + BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db), db: db, } } // GetByToken finds a verification by token (only unused and non-expired) -func (r *emailVerificationRepository) GetByToken(ctx context.Context, token string) (*models.EmailVerification, error) { - var verification models.EmailVerification +func (r *emailVerificationRepository) GetByToken(ctx context.Context, token string) (*domain.EmailVerification, error) { + var verification domain.EmailVerification if err := r.db.WithContext(ctx).Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).First(&verification).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrEntityNotFound @@ -43,8 +34,8 @@ func (r *emailVerificationRepository) GetByToken(ctx context.Context, token stri } // GetByUserID finds verifications by user ID -func (r *emailVerificationRepository) GetByUserID(ctx context.Context, userID uint) ([]models.EmailVerification, error) { - var verifications []models.EmailVerification +func (r *emailVerificationRepository) GetByUserID(ctx context.Context, userID uint) ([]domain.EmailVerification, error) { + var verifications []domain.EmailVerification if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&verifications).Error; err != nil { return nil, err } @@ -53,7 +44,7 @@ func (r *emailVerificationRepository) GetByUserID(ctx context.Context, userID ui // DeleteExpired deletes expired verifications func (r *emailVerificationRepository) DeleteExpired(ctx context.Context) error { - if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&models.EmailVerification{}).Error; err != nil { + if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&domain.EmailVerification{}).Error; err != nil { return err } return nil @@ -61,7 +52,7 @@ func (r *emailVerificationRepository) DeleteExpired(ctx context.Context) error { // MarkAsUsed marks a verification as used func (r *emailVerificationRepository) MarkAsUsed(ctx context.Context, id uint) error { - if err := r.db.WithContext(ctx).Model(&models.EmailVerification{}).Where("id = ?", id).Update("used", true).Error; err != nil { + if err := r.db.WithContext(ctx).Model(&domain.EmailVerification{}).Where("id = ?", id).Update("used", true).Error; err != nil { return err } return nil diff --git a/internal/data/sql/like_repository.go b/internal/data/sql/like_repository.go index a1dd29d..2017db6 100644 --- a/internal/data/sql/like_repository.go +++ b/internal/data/sql/like_repository.go @@ -3,7 +3,7 @@ package sql import ( "context" "gorm.io/gorm" - "tercul/internal/domain" + "tercul/internal/domain/like" ) type likeRepository struct { @@ -12,7 +12,7 @@ type likeRepository struct { } // NewLikeRepository creates a new LikeRepository. -func NewLikeRepository(db *gorm.DB) domain.LikeRepository { +func NewLikeRepository(db *gorm.DB) like.LikeRepository { return &likeRepository{ BaseRepository: NewBaseRepositoryImpl[domain.Like](db), db: db, diff --git a/internal/data/sql/monetization_repository.go b/internal/data/sql/monetization_repository.go index 714da32..394d065 100644 --- a/internal/data/sql/monetization_repository.go +++ b/internal/data/sql/monetization_repository.go @@ -1,35 +1,27 @@ -package repositories +package sql import ( "context" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/monetization" ) -// MonetizationRepository defines CRUD methods specific to Monetization. -type MonetizationRepository interface { - BaseRepository[models.Monetization] - ListByWorkID(ctx context.Context, workID uint) ([]models.Monetization, error) - ListByTranslationID(ctx context.Context, translationID uint) ([]models.Monetization, error) - ListByBookID(ctx context.Context, bookID uint) ([]models.Monetization, error) -} - type monetizationRepository struct { - BaseRepository[models.Monetization] + domain.BaseRepository[domain.Monetization] db *gorm.DB } // NewMonetizationRepository creates a new MonetizationRepository. -func NewMonetizationRepository(db *gorm.DB) MonetizationRepository { +func NewMonetizationRepository(db *gorm.DB) monetization.MonetizationRepository { return &monetizationRepository{ - BaseRepository: NewBaseRepositoryImpl[models.Monetization](db), + BaseRepository: NewBaseRepositoryImpl[domain.Monetization](db), db: db, } } // ListByWorkID finds monetizations by work ID -func (r *monetizationRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.Monetization, error) { - var monetizations []models.Monetization +func (r *monetizationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Monetization, error) { + var monetizations []domain.Monetization if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&monetizations).Error; err != nil { return nil, err } @@ -37,8 +29,8 @@ func (r *monetizationRepository) ListByWorkID(ctx context.Context, workID uint) } // ListByTranslationID finds monetizations by translation ID -func (r *monetizationRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]models.Monetization, error) { - var monetizations []models.Monetization +func (r *monetizationRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Monetization, error) { + var monetizations []domain.Monetization if err := r.db.WithContext(ctx).Where("translation_id = ?", translationID).Find(&monetizations).Error; err != nil { return nil, err } @@ -46,8 +38,8 @@ func (r *monetizationRepository) ListByTranslationID(ctx context.Context, transl } // ListByBookID finds monetizations by book ID -func (r *monetizationRepository) ListByBookID(ctx context.Context, bookID uint) ([]models.Monetization, error) { - var monetizations []models.Monetization +func (r *monetizationRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Monetization, error) { + var monetizations []domain.Monetization if err := r.db.WithContext(ctx).Where("book_id = ?", bookID).Find(&monetizations).Error; err != nil { return nil, err } diff --git a/internal/data/sql/password_reset_repository.go b/internal/data/sql/password_reset_repository.go index bd7645d..a9c15ee 100644 --- a/internal/data/sql/password_reset_repository.go +++ b/internal/data/sql/password_reset_repository.go @@ -1,38 +1,29 @@ -package repositories +package sql import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/password_reset" "time" ) -// PasswordResetRepository defines CRUD methods specific to PasswordReset. -type PasswordResetRepository interface { - BaseRepository[models.PasswordReset] - GetByToken(ctx context.Context, token string) (*models.PasswordReset, error) - GetByUserID(ctx context.Context, userID uint) ([]models.PasswordReset, error) - DeleteExpired(ctx context.Context) error - MarkAsUsed(ctx context.Context, id uint) error -} - type passwordResetRepository struct { - BaseRepository[models.PasswordReset] + domain.BaseRepository[domain.PasswordReset] db *gorm.DB } // NewPasswordResetRepository creates a new PasswordResetRepository. -func NewPasswordResetRepository(db *gorm.DB) PasswordResetRepository { +func NewPasswordResetRepository(db *gorm.DB) domain.PasswordResetRepository { return &passwordResetRepository{ - BaseRepository: NewBaseRepositoryImpl[models.PasswordReset](db), + BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db), db: db, } } // GetByToken finds a reset by token (only unused and non-expired) -func (r *passwordResetRepository) GetByToken(ctx context.Context, token string) (*models.PasswordReset, error) { - var reset models.PasswordReset +func (r *passwordResetRepository) GetByToken(ctx context.Context, token string) (*domain.PasswordReset, error) { + var reset domain.PasswordReset if err := r.db.WithContext(ctx).Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).First(&reset).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrEntityNotFound @@ -43,8 +34,8 @@ func (r *passwordResetRepository) GetByToken(ctx context.Context, token string) } // GetByUserID finds resets by user ID -func (r *passwordResetRepository) GetByUserID(ctx context.Context, userID uint) ([]models.PasswordReset, error) { - var resets []models.PasswordReset +func (r *passwordResetRepository) GetByUserID(ctx context.Context, userID uint) ([]domain.PasswordReset, error) { + var resets []domain.PasswordReset if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&resets).Error; err != nil { return nil, err } @@ -53,7 +44,7 @@ func (r *passwordResetRepository) GetByUserID(ctx context.Context, userID uint) // DeleteExpired deletes expired resets func (r *passwordResetRepository) DeleteExpired(ctx context.Context) error { - if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&models.PasswordReset{}).Error; err != nil { + if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&domain.PasswordReset{}).Error; err != nil { return err } return nil @@ -61,7 +52,7 @@ func (r *passwordResetRepository) DeleteExpired(ctx context.Context) error { // MarkAsUsed marks a reset as used func (r *passwordResetRepository) MarkAsUsed(ctx context.Context, id uint) error { - if err := r.db.WithContext(ctx).Model(&models.PasswordReset{}).Where("id = ?", id).Update("used", true).Error; err != nil { + if err := r.db.WithContext(ctx).Model(&domain.PasswordReset{}).Where("id = ?", id).Update("used", true).Error; err != nil { return err } return nil diff --git a/internal/data/sql/place_repository.go b/internal/data/sql/place_repository.go index 95d10f5..e3b9e61 100644 --- a/internal/data/sql/place_repository.go +++ b/internal/data/sql/place_repository.go @@ -1,36 +1,28 @@ -package repositories +package sql import ( "context" "gorm.io/gorm" "math" - "tercul/internal/models" + "tercul/internal/domain/place" ) -// PlaceRepository defines CRUD methods specific to Place. -type PlaceRepository interface { - BaseRepository[models.Place] - ListByCountryID(ctx context.Context, countryID uint) ([]models.Place, error) - ListByCityID(ctx context.Context, cityID uint) ([]models.Place, error) - FindNearby(ctx context.Context, latitude, longitude float64, radiusKm float64) ([]models.Place, error) -} - type placeRepository struct { - BaseRepository[models.Place] + domain.BaseRepository[domain.Place] db *gorm.DB } // NewPlaceRepository creates a new PlaceRepository. -func NewPlaceRepository(db *gorm.DB) PlaceRepository { +func NewPlaceRepository(db *gorm.DB) place.PlaceRepository { return &placeRepository{ - BaseRepository: NewBaseRepositoryImpl[models.Place](db), + BaseRepository: NewBaseRepositoryImpl[domain.Place](db), db: db, } } // ListByCountryID finds places by country ID -func (r *placeRepository) ListByCountryID(ctx context.Context, countryID uint) ([]models.Place, error) { - var places []models.Place +func (r *placeRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Place, error) { + var places []domain.Place if err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&places).Error; err != nil { return nil, err } @@ -38,8 +30,8 @@ func (r *placeRepository) ListByCountryID(ctx context.Context, countryID uint) ( } // ListByCityID finds places by city ID -func (r *placeRepository) ListByCityID(ctx context.Context, cityID uint) ([]models.Place, error) { - var places []models.Place +func (r *placeRepository) ListByCityID(ctx context.Context, cityID uint) ([]domain.Place, error) { + var places []domain.Place if err := r.db.WithContext(ctx).Where("city_id = ?", cityID).Find(&places).Error; err != nil { return nil, err } @@ -47,10 +39,10 @@ func (r *placeRepository) ListByCityID(ctx context.Context, cityID uint) ([]mode } // FindNearby finds places within a certain radius (in kilometers) of a point -func (r *placeRepository) FindNearby(ctx context.Context, latitude, longitude float64, radiusKm float64) ([]models.Place, error) { +func (r *placeRepository) FindNearby(ctx context.Context, latitude, longitude float64, radiusKm float64) ([]domain.Place, error) { // This is a simplified implementation that would need to be replaced with // a proper geospatial query based on the database being used - var places []models.Place + var places []domain.Place // For PostgreSQL with PostGIS, you might use something like: // query := `SELECT * FROM places diff --git a/internal/data/sql/publisher_repository.go b/internal/data/sql/publisher_repository.go index adec5a1..766ed09 100644 --- a/internal/data/sql/publisher_repository.go +++ b/internal/data/sql/publisher_repository.go @@ -1,33 +1,27 @@ -package repositories +package sql import ( "context" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/publisher" ) -// PublisherRepository defines CRUD methods specific to Publisher. -type PublisherRepository interface { - BaseRepository[models.Publisher] - ListByCountryID(ctx context.Context, countryID uint) ([]models.Publisher, error) -} - type publisherRepository struct { - BaseRepository[models.Publisher] + domain.BaseRepository[domain.Publisher] db *gorm.DB } // NewPublisherRepository creates a new PublisherRepository. -func NewPublisherRepository(db *gorm.DB) PublisherRepository { +func NewPublisherRepository(db *gorm.DB) publisher.PublisherRepository { return &publisherRepository{ - BaseRepository: NewBaseRepositoryImpl[models.Publisher](db), + BaseRepository: NewBaseRepositoryImpl[domain.Publisher](db), db: db, } } // ListByCountryID finds publishers by country ID -func (r *publisherRepository) ListByCountryID(ctx context.Context, countryID uint) ([]models.Publisher, error) { - var publishers []models.Publisher +func (r *publisherRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Publisher, error) { + var publishers []domain.Publisher if err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&publishers).Error; err != nil { return nil, err } diff --git a/internal/data/sql/source_repository.go b/internal/data/sql/source_repository.go index 98efba7..af35803 100644 --- a/internal/data/sql/source_repository.go +++ b/internal/data/sql/source_repository.go @@ -1,35 +1,28 @@ -package repositories +package sql import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/source" ) -// SourceRepository defines CRUD methods specific to Source. -type SourceRepository interface { - BaseRepository[models.Source] - ListByWorkID(ctx context.Context, workID uint) ([]models.Source, error) - FindByURL(ctx context.Context, url string) (*models.Source, error) -} - type sourceRepository struct { - BaseRepository[models.Source] + domain.BaseRepository[domain.Source] db *gorm.DB } // NewSourceRepository creates a new SourceRepository. -func NewSourceRepository(db *gorm.DB) SourceRepository { +func NewSourceRepository(db *gorm.DB) source.SourceRepository { return &sourceRepository{ - BaseRepository: NewBaseRepositoryImpl[models.Source](db), + BaseRepository: NewBaseRepositoryImpl[domain.Source](db), db: db, } } // ListByWorkID finds sources by work ID -func (r *sourceRepository) ListByWorkID(ctx context.Context, workID uint) ([]models.Source, error) { - var sources []models.Source +func (r *sourceRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Source, error) { + var sources []domain.Source if err := r.db.WithContext(ctx).Joins("JOIN work_sources ON work_sources.source_id = sources.id"). Where("work_sources.work_id = ?", workID). Find(&sources).Error; err != nil { @@ -39,8 +32,8 @@ func (r *sourceRepository) ListByWorkID(ctx context.Context, workID uint) ([]mod } // FindByURL finds a source by URL -func (r *sourceRepository) FindByURL(ctx context.Context, url string) (*models.Source, error) { - var source models.Source +func (r *sourceRepository) FindByURL(ctx context.Context, url string) (*domain.Source, error) { + var source domain.Source if err := r.db.WithContext(ctx).Where("url = ?", url).First(&source).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrEntityNotFound diff --git a/internal/data/sql/tag_repository.go b/internal/data/sql/tag_repository.go index 27a2f89..3efd079 100644 --- a/internal/data/sql/tag_repository.go +++ b/internal/data/sql/tag_repository.go @@ -4,7 +4,7 @@ import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/domain" + "tercul/internal/domain/tag" ) type tagRepository struct { @@ -13,7 +13,7 @@ type tagRepository struct { } // NewTagRepository creates a new TagRepository. -func NewTagRepository(db *gorm.DB) domain.TagRepository { +func NewTagRepository(db *gorm.DB) tag.TagRepository { return &tagRepository{ BaseRepository: NewBaseRepositoryImpl[domain.Tag](db), db: db, diff --git a/internal/data/sql/translation_repository.go b/internal/data/sql/translation_repository.go index ded647c..2ad6b6c 100644 --- a/internal/data/sql/translation_repository.go +++ b/internal/data/sql/translation_repository.go @@ -3,7 +3,7 @@ package sql import ( "context" "gorm.io/gorm" - "tercul/internal/domain" + "tercul/internal/domain/translation" ) type translationRepository struct { @@ -12,7 +12,7 @@ type translationRepository struct { } // NewTranslationRepository creates a new TranslationRepository. -func NewTranslationRepository(db *gorm.DB) domain.TranslationRepository { +func NewTranslationRepository(db *gorm.DB) translation.TranslationRepository { return &translationRepository{ BaseRepository: NewBaseRepositoryImpl[domain.Translation](db), db: db, diff --git a/internal/data/sql/user_profile_repository.go b/internal/data/sql/user_profile_repository.go index a3d922d..e4b6b44 100644 --- a/internal/data/sql/user_profile_repository.go +++ b/internal/data/sql/user_profile_repository.go @@ -1,34 +1,28 @@ -package repositories +package sql import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/user_profile" ) -// UserProfileRepository defines CRUD methods specific to UserProfile. -type UserProfileRepository interface { - BaseRepository[models.UserProfile] - GetByUserID(ctx context.Context, userID uint) (*models.UserProfile, error) -} - type userProfileRepository struct { - BaseRepository[models.UserProfile] + domain.BaseRepository[domain.UserProfile] db *gorm.DB } // NewUserProfileRepository creates a new UserProfileRepository. -func NewUserProfileRepository(db *gorm.DB) UserProfileRepository { +func NewUserProfileRepository(db *gorm.DB) domain.UserProfileRepository { return &userProfileRepository{ - BaseRepository: NewBaseRepositoryImpl[models.UserProfile](db), + BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db), db: db, } } // GetByUserID finds a user profile by user ID -func (r *userProfileRepository) GetByUserID(ctx context.Context, userID uint) (*models.UserProfile, error) { - var profile models.UserProfile +func (r *userProfileRepository) GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error) { + var profile domain.UserProfile if err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&profile).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrEntityNotFound diff --git a/internal/data/sql/user_repository.go b/internal/data/sql/user_repository.go index 2d346fd..cc5d4fe 100644 --- a/internal/data/sql/user_repository.go +++ b/internal/data/sql/user_repository.go @@ -4,7 +4,7 @@ import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/domain" + "tercul/internal/domain/user" ) type userRepository struct { @@ -13,7 +13,7 @@ type userRepository struct { } // NewUserRepository creates a new UserRepository. -func NewUserRepository(db *gorm.DB) domain.UserRepository { +func NewUserRepository(db *gorm.DB) user.UserRepository { return &userRepository{ BaseRepository: NewBaseRepositoryImpl[domain.User](db), db: db, diff --git a/internal/data/sql/user_session_repository.go b/internal/data/sql/user_session_repository.go index 26701df..b18f25c 100644 --- a/internal/data/sql/user_session_repository.go +++ b/internal/data/sql/user_session_repository.go @@ -1,37 +1,29 @@ -package repositories +package sql import ( "context" "errors" "gorm.io/gorm" - "tercul/internal/models" + "tercul/internal/domain/user_session" "time" ) -// UserSessionRepository defines CRUD methods specific to UserSession. -type UserSessionRepository interface { - BaseRepository[models.UserSession] - GetByToken(ctx context.Context, token string) (*models.UserSession, error) - GetByUserID(ctx context.Context, userID uint) ([]models.UserSession, error) - DeleteExpired(ctx context.Context) error -} - type userSessionRepository struct { - BaseRepository[models.UserSession] + domain.BaseRepository[domain.UserSession] db *gorm.DB } // NewUserSessionRepository creates a new UserSessionRepository. -func NewUserSessionRepository(db *gorm.DB) UserSessionRepository { +func NewUserSessionRepository(db *gorm.DB) domain.UserSessionRepository { return &userSessionRepository{ - BaseRepository: NewBaseRepositoryImpl[models.UserSession](db), + BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db), db: db, } } // GetByToken finds a session by token -func (r *userSessionRepository) GetByToken(ctx context.Context, token string) (*models.UserSession, error) { - var session models.UserSession +func (r *userSessionRepository) GetByToken(ctx context.Context, token string) (*domain.UserSession, error) { + var session domain.UserSession if err := r.db.WithContext(ctx).Where("token = ?", token).First(&session).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrEntityNotFound @@ -42,8 +34,8 @@ func (r *userSessionRepository) GetByToken(ctx context.Context, token string) (* } // GetByUserID finds sessions by user ID -func (r *userSessionRepository) GetByUserID(ctx context.Context, userID uint) ([]models.UserSession, error) { - var sessions []models.UserSession +func (r *userSessionRepository) GetByUserID(ctx context.Context, userID uint) ([]domain.UserSession, error) { + var sessions []domain.UserSession if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&sessions).Error; err != nil { return nil, err } @@ -52,7 +44,7 @@ func (r *userSessionRepository) GetByUserID(ctx context.Context, userID uint) ([ // DeleteExpired deletes expired sessions func (r *userSessionRepository) DeleteExpired(ctx context.Context) error { - if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&models.UserSession{}).Error; err != nil { + if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&domain.UserSession{}).Error; err != nil { return err } return nil diff --git a/internal/data/sql/work_repository.go b/internal/data/sql/work_repository.go index 76964d9..35de169 100644 --- a/internal/data/sql/work_repository.go +++ b/internal/data/sql/work_repository.go @@ -3,7 +3,7 @@ package sql import ( "context" "gorm.io/gorm" - "tercul/internal/domain" + "tercul/internal/domain/work" ) type workRepository struct { @@ -12,7 +12,7 @@ type workRepository struct { } // 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), db: db, diff --git a/internal/domain/author/repo.go b/internal/domain/author/repo.go new file mode 100644 index 0000000..795ef3b --- /dev/null +++ b/internal/domain/author/repo.go @@ -0,0 +1,15 @@ +package author + +import ( + "context" + "tercul/internal/domain" +) + +// AuthorRepository defines CRUD methods specific to Author. +type AuthorRepository interface { + domain.BaseRepositoryRepository[domain.Author] + + ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) + ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) + ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) +} diff --git a/internal/domain/book/repo.go b/internal/domain/book/repo.go new file mode 100644 index 0000000..520fb4e --- /dev/null +++ b/internal/domain/book/repo.go @@ -0,0 +1,16 @@ +package book + +import ( + "context" + "tercul/internal/domain" +) + +// BookRepository defines CRUD methods specific to Book. +type BookRepository interface { + domain.BaseRepositoryRepository[domain.Book] + + ListByAuthorID(ctx context.Context, authorID uint) ([]domain.Book, error) + ListByPublisherID(ctx context.Context, publisherID uint) ([]domain.Book, error) + ListByWorkID(ctx context.Context, workID uint) ([]domain.Book, error) + FindByISBN(ctx context.Context, isbn string) (*domain.Book, error) +} diff --git a/internal/domain/bookmark/repo.go b/internal/domain/bookmark/repo.go new file mode 100644 index 0000000..ed8ab02 --- /dev/null +++ b/internal/domain/bookmark/repo.go @@ -0,0 +1,14 @@ +package bookmark + +import ( + "context" + "tercul/internal/domain" +) + +// BookmarkRepository defines CRUD methods specific to Bookmark. +type BookmarkRepository interface { + domain.BaseRepositoryRepository[domain.Bookmark] + + ListByUserID(ctx context.Context, userID uint) ([]domain.Bookmark, error) + ListByWorkID(ctx context.Context, workID uint) ([]domain.Bookmark, error) +} diff --git a/internal/domain/category/repo.go b/internal/domain/category/repo.go new file mode 100644 index 0000000..a2a52e9 --- /dev/null +++ b/internal/domain/category/repo.go @@ -0,0 +1,15 @@ +package category + +import ( + "context" + "tercul/internal/domain" +) + +// CategoryRepository defines CRUD methods specific to Category. +type CategoryRepository interface { + domain.BaseRepositoryRepository[domain.Category] + + FindByName(ctx context.Context, name string) (*domain.Category, error) + ListByWorkID(ctx context.Context, workID uint) ([]domain.Category, error) + ListByParentID(ctx context.Context, parentID *uint) ([]domain.Category, error) +} diff --git a/internal/domain/city/repo.go b/internal/domain/city/repo.go new file mode 100644 index 0000000..7725811 --- /dev/null +++ b/internal/domain/city/repo.go @@ -0,0 +1,13 @@ +package city + +import ( + "context" + "tercul/internal/domain" +) + +// CityRepository defines CRUD methods specific to City. +type CityRepository interface { + domain.BaseRepositoryRepository[domain.City] + + ListByCountryID(ctx context.Context, countryID uint) ([]domain.City, error) +} diff --git a/internal/domain/collection/repo.go b/internal/domain/collection/repo.go new file mode 100644 index 0000000..0f95b39 --- /dev/null +++ b/internal/domain/collection/repo.go @@ -0,0 +1,15 @@ +package collection + +import ( + "context" + "tercul/internal/domain" +) + +// CollectionRepository defines CRUD methods specific to Collection. +type CollectionRepository interface { + domain.BaseRepositoryRepository[domain.Collection] + + ListByUserID(ctx context.Context, userID uint) ([]domain.Collection, error) + ListPublic(ctx context.Context) ([]domain.Collection, error) + ListByWorkID(ctx context.Context, workID uint) ([]domain.Collection, error) +} diff --git a/internal/domain/comment/repo.go b/internal/domain/comment/repo.go new file mode 100644 index 0000000..3522768 --- /dev/null +++ b/internal/domain/comment/repo.go @@ -0,0 +1,16 @@ +package comment + +import ( + "context" + "tercul/internal/domain" +) + +// CommentRepository defines CRUD methods specific to Comment. +type CommentRepository interface { + domain.BaseRepositoryRepository[domain.Comment] + + ListByUserID(ctx context.Context, userID uint) ([]domain.Comment, error) + ListByWorkID(ctx context.Context, workID uint) ([]domain.Comment, error) + ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Comment, error) + ListByParentID(ctx context.Context, parentID uint) ([]domain.Comment, error) +} diff --git a/internal/domain/contribution/repo.go b/internal/domain/contribution/repo.go new file mode 100644 index 0000000..acbffae --- /dev/null +++ b/internal/domain/contribution/repo.go @@ -0,0 +1,17 @@ +package contribution + +import ( + "context" + "tercul/internal/domain" +) + +// ContributionRepository defines CRUD methods specific to Contribution. +type ContributionRepository interface { + domain.BaseRepositoryRepository[domain.Contribution] + + ListByUserID(ctx context.Context, userID uint) ([]domain.Contribution, error) + ListByReviewerID(ctx context.Context, reviewerID uint) ([]domain.Contribution, error) + ListByWorkID(ctx context.Context, workID uint) ([]domain.Contribution, error) + ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Contribution, error) + ListByStatus(ctx context.Context, status string) ([]domain.Contribution, error) +} diff --git a/internal/domain/copyright/repo.go b/internal/domain/copyright/repo.go new file mode 100644 index 0000000..f832801 --- /dev/null +++ b/internal/domain/copyright/repo.go @@ -0,0 +1,19 @@ +package copyright + +import ( + "context" + "tercul/internal/domain" +) + +// CopyrightRepository defines CRUD methods specific to Copyright. +type CopyrightRepository interface { + domain.BaseRepositoryRepository[domain.Copyright] + + AttachToEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) (error) + DetachFromEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) (error) + GetByEntity(ctx context.Context, entityID uint, entityType string) ([]domain.Copyright, error) + GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]domain.Copyrightable, error) + AddTranslation(ctx context.Context, translation *domain.CopyrightTranslation) (error) + GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) + GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) +} diff --git a/internal/domain/copyright_claim/repo.go b/internal/domain/copyright_claim/repo.go new file mode 100644 index 0000000..9e565dc --- /dev/null +++ b/internal/domain/copyright_claim/repo.go @@ -0,0 +1,14 @@ +package copyright_claim + +import ( + "context" + "tercul/internal/domain" +) + +// Copyright_claimRepository defines CRUD methods specific to Copyright_claim. +type Copyright_claimRepository interface { + domain.BaseRepositoryRepository[domain.CopyrightClaim] + + ListByWorkID(ctx context.Context, workID uint) ([]domain.CopyrightClaim, error) + ListByUserID(ctx context.Context, userID uint) ([]domain.CopyrightClaim, error) +} diff --git a/internal/domain/country/repo.go b/internal/domain/country/repo.go new file mode 100644 index 0000000..abb7b69 --- /dev/null +++ b/internal/domain/country/repo.go @@ -0,0 +1,14 @@ +package country + +import ( + "context" + "tercul/internal/domain" +) + +// CountryRepository defines CRUD methods specific to Country. +type CountryRepository interface { + domain.BaseRepositoryRepository[domain.Country] + + GetByCode(ctx context.Context, code string) (*domain.Country, error) + ListByContinent(ctx context.Context, continent string) ([]domain.Country, error) +} diff --git a/internal/domain/edge/repo.go b/internal/domain/edge/repo.go new file mode 100644 index 0000000..938aac5 --- /dev/null +++ b/internal/domain/edge/repo.go @@ -0,0 +1,13 @@ +package edge + +import ( + "context" + "tercul/internal/domain" +) + +// EdgeRepository defines CRUD methods specific to Edge. +type EdgeRepository interface { + domain.BaseRepositoryRepository[domain.Edge] + + ListBySource(ctx context.Context, sourceTable string, sourceID uint) ([]domain.Edge, error) +} diff --git a/internal/domain/edition/repo.go b/internal/domain/edition/repo.go new file mode 100644 index 0000000..2d4ad77 --- /dev/null +++ b/internal/domain/edition/repo.go @@ -0,0 +1,14 @@ +package edition + +import ( + "context" + "tercul/internal/domain" +) + +// EditionRepository defines CRUD methods specific to Edition. +type EditionRepository interface { + domain.BaseRepositoryRepository[domain.Edition] + + ListByBookID(ctx context.Context, bookID uint) ([]domain.Edition, error) + FindByISBN(ctx context.Context, isbn string) (*domain.Edition, error) +} diff --git a/internal/domain/email_verification/repo.go b/internal/domain/email_verification/repo.go new file mode 100644 index 0000000..bb227d6 --- /dev/null +++ b/internal/domain/email_verification/repo.go @@ -0,0 +1,16 @@ +package email_verification + +import ( + "context" + "tercul/internal/domain" +) + +// Email_verificationRepository defines CRUD methods specific to Email_verification. +type Email_verificationRepository interface { + domain.BaseRepositoryRepository[domain.EmailVerification] + + GetByToken(ctx context.Context, token string) (*domain.EmailVerification, error) + GetByUserID(ctx context.Context, userID uint) ([]domain.EmailVerification, error) + DeleteExpired(ctx context.Context) (error) + MarkAsUsed(ctx context.Context, id uint) (error) +} diff --git a/internal/domain/like/repo.go b/internal/domain/like/repo.go new file mode 100644 index 0000000..9a12848 --- /dev/null +++ b/internal/domain/like/repo.go @@ -0,0 +1,16 @@ +package like + +import ( + "context" + "tercul/internal/domain" +) + +// LikeRepository defines CRUD methods specific to Like. +type LikeRepository interface { + domain.BaseRepositoryRepository[domain.Like] + + ListByUserID(ctx context.Context, userID uint) ([]domain.Like, error) + ListByWorkID(ctx context.Context, workID uint) ([]domain.Like, error) + ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Like, error) + ListByCommentID(ctx context.Context, commentID uint) ([]domain.Like, error) +} diff --git a/internal/domain/monetization/repo.go b/internal/domain/monetization/repo.go new file mode 100644 index 0000000..4ca865b --- /dev/null +++ b/internal/domain/monetization/repo.go @@ -0,0 +1,15 @@ +package monetization + +import ( + "context" + "tercul/internal/domain" +) + +// MonetizationRepository defines CRUD methods specific to Monetization. +type MonetizationRepository interface { + domain.BaseRepositoryRepository[domain.Monetization] + + ListByWorkID(ctx context.Context, workID uint) ([]domain.Monetization, error) + ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Monetization, error) + ListByBookID(ctx context.Context, bookID uint) ([]domain.Monetization, error) +} diff --git a/internal/domain/password_reset/repo.go b/internal/domain/password_reset/repo.go new file mode 100644 index 0000000..f3cbc00 --- /dev/null +++ b/internal/domain/password_reset/repo.go @@ -0,0 +1,16 @@ +package password_reset + +import ( + "context" + "tercul/internal/domain" +) + +// Password_resetRepository defines CRUD methods specific to Password_reset. +type Password_resetRepository interface { + domain.BaseRepositoryRepository[domain.PasswordReset] + + GetByToken(ctx context.Context, token string) (*domain.PasswordReset, error) + GetByUserID(ctx context.Context, userID uint) ([]domain.PasswordReset, error) + DeleteExpired(ctx context.Context) (error) + MarkAsUsed(ctx context.Context, id uint) (error) +} diff --git a/internal/domain/place/repo.go b/internal/domain/place/repo.go new file mode 100644 index 0000000..5b49534 --- /dev/null +++ b/internal/domain/place/repo.go @@ -0,0 +1,15 @@ +package place + +import ( + "context" + "tercul/internal/domain" +) + +// PlaceRepository defines CRUD methods specific to Place. +type PlaceRepository interface { + domain.BaseRepositoryRepository[domain.Place] + + ListByCountryID(ctx context.Context, countryID uint) ([]domain.Place, error) + ListByCityID(ctx context.Context, cityID uint) ([]domain.Place, error) + FindNearby(ctx context.Context, latitude float64, radiusKm float64) ([]domain.Place, error) +} diff --git a/internal/domain/publisher/repo.go b/internal/domain/publisher/repo.go new file mode 100644 index 0000000..039b2bf --- /dev/null +++ b/internal/domain/publisher/repo.go @@ -0,0 +1,13 @@ +package publisher + +import ( + "context" + "tercul/internal/domain" +) + +// PublisherRepository defines CRUD methods specific to Publisher. +type PublisherRepository interface { + domain.BaseRepositoryRepository[domain.Publisher] + + ListByCountryID(ctx context.Context, countryID uint) ([]domain.Publisher, error) +} diff --git a/internal/domain/source/repo.go b/internal/domain/source/repo.go new file mode 100644 index 0000000..9562664 --- /dev/null +++ b/internal/domain/source/repo.go @@ -0,0 +1,14 @@ +package source + +import ( + "context" + "tercul/internal/domain" +) + +// SourceRepository defines CRUD methods specific to Source. +type SourceRepository interface { + domain.BaseRepositoryRepository[domain.Source] + + ListByWorkID(ctx context.Context, workID uint) ([]domain.Source, error) + FindByURL(ctx context.Context, url string) (*domain.Source, error) +} diff --git a/internal/domain/tag/repo.go b/internal/domain/tag/repo.go new file mode 100644 index 0000000..fbead8b --- /dev/null +++ b/internal/domain/tag/repo.go @@ -0,0 +1,14 @@ +package tag + +import ( + "context" + "tercul/internal/domain" +) + +// TagRepository defines CRUD methods specific to Tag. +type TagRepository interface { + domain.BaseRepositoryRepository[domain.Tag] + + FindByName(ctx context.Context, name string) (*domain.Tag, error) + ListByWorkID(ctx context.Context, workID uint) ([]domain.Tag, error) +} diff --git a/internal/domain/translation/repo.go b/internal/domain/translation/repo.go new file mode 100644 index 0000000..0bbeefb --- /dev/null +++ b/internal/domain/translation/repo.go @@ -0,0 +1,16 @@ +package translation + +import ( + "context" + "tercul/internal/domain" +) + +// TranslationRepository defines CRUD methods specific to Translation. +type TranslationRepository interface { + domain.BaseRepositoryRepository[domain.Translation] + + ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) + ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) + ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) + ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) +} diff --git a/internal/domain/user/repo.go b/internal/domain/user/repo.go new file mode 100644 index 0000000..7efba49 --- /dev/null +++ b/internal/domain/user/repo.go @@ -0,0 +1,15 @@ +package user + +import ( + "context" + "tercul/internal/domain" +) + +// UserRepository defines CRUD methods specific to User. +type UserRepository interface { + domain.BaseRepositoryRepository[domain.User] + + FindByUsername(ctx context.Context, username string) (*domain.User, error) + FindByEmail(ctx context.Context, email string) (*domain.User, error) + ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) +} diff --git a/internal/domain/user_profile/repo.go b/internal/domain/user_profile/repo.go new file mode 100644 index 0000000..cdc0b0d --- /dev/null +++ b/internal/domain/user_profile/repo.go @@ -0,0 +1,13 @@ +package user_profile + +import ( + "context" + "tercul/internal/domain" +) + +// User_profileRepository defines CRUD methods specific to User_profile. +type User_profileRepository interface { + domain.BaseRepositoryRepository[domain.UserProfile] + + GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error) +} diff --git a/internal/domain/user_session/repo.go b/internal/domain/user_session/repo.go new file mode 100644 index 0000000..7d5e56c --- /dev/null +++ b/internal/domain/user_session/repo.go @@ -0,0 +1,15 @@ +package user_session + +import ( + "context" + "tercul/internal/domain" +) + +// User_sessionRepository defines CRUD methods specific to User_session. +type User_sessionRepository interface { + domain.BaseRepositoryRepository[domain.UserSession] + + GetByToken(ctx context.Context, token string) (*domain.UserSession, error) + GetByUserID(ctx context.Context, userID uint) ([]domain.UserSession, error) + DeleteExpired(ctx context.Context) (error) +} diff --git a/internal/domain/work/repo.go b/internal/domain/work/repo.go new file mode 100644 index 0000000..d37f723 --- /dev/null +++ b/internal/domain/work/repo.go @@ -0,0 +1,18 @@ +package work + +import ( + "context" + "tercul/internal/domain" +) + +// WorkRepository defines CRUD methods specific to Work. +type WorkRepository interface { + domain.BaseRepositoryRepository[domain.Work] + + FindByTitle(ctx context.Context, title string) ([]domain.Work, error) + FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) + FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) + FindByLanguage(ctx context.Context, language string, page int) (*, error) + GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) + ListWithTranslations(ctx context.Context, page int) (*, error) +} diff --git a/internal/jobs/linguistics/adapter_lingua.go b/internal/jobs/linguistics/adapter_lingua.go index ad13492..b7fb51b 100644 --- a/internal/jobs/linguistics/adapter_lingua.go +++ b/internal/jobs/linguistics/adapter_lingua.go @@ -11,24 +11,24 @@ type LinguaLanguageDetector struct { } // NewLinguaLanguageDetector builds a detector for all supported languages -func NewLinguaLanguageDetector() *LinguaLanguageDetector { +func NewLinguaLanguageDetector() LanguageDetector { det := lingua.NewLanguageDetectorBuilder().FromAllLanguages().Build() return &LinguaLanguageDetector{detector: det} } // DetectLanguage returns a lowercase ISO 639-1 code if possible -func (l *LinguaLanguageDetector) DetectLanguage(text string) (string, bool) { +func (l *LinguaLanguageDetector) DetectLanguage(text string) (string, error) { lang, ok := l.detector.DetectLanguageOf(text) if !ok { - return "", false + return "", nil // Or an error if you prefer } // Prefer ISO 639-1 when available else fallback to ISO 639-3 if s := lang.IsoCode639_1().String(); s != "" { - return s, true + return s, nil } if s := lang.IsoCode639_3().String(); s != "" { - return s, true + return s, nil } // fallback to language name - return strings.ToLower(lang.String()), true + return strings.ToLower(lang.String()), nil } diff --git a/internal/jobs/linguistics/analysis_repository.go b/internal/jobs/linguistics/analysis_repository.go index 9d040cf..47f7cec 100644 --- a/internal/jobs/linguistics/analysis_repository.go +++ b/internal/jobs/linguistics/analysis_repository.go @@ -3,7 +3,7 @@ package linguistics import ( "context" "fmt" - models2 "tercul/internal/models" + "tercul/internal/domain" "gorm.io/gorm" "tercul/internal/platform/log" @@ -18,14 +18,14 @@ type AnalysisRepository interface { GetWorkContent(ctx context.Context, workID uint, language string) (string, error) // StoreWorkAnalysis stores work-specific analysis results - StoreWorkAnalysis(ctx context.Context, workID uint, textMetadata *models2.TextMetadata, - readabilityScore *models2.ReadabilityScore, languageAnalysis *models2.LanguageAnalysis) error + StoreWorkAnalysis(ctx context.Context, workID uint, textMetadata *domain.TextMetadata, + readabilityScore *domain.ReadabilityScore, languageAnalysis *domain.LanguageAnalysis) error // GetWorkByID fetches a work by ID - GetWorkByID(ctx context.Context, workID uint) (*models2.Work, error) + GetWorkByID(ctx context.Context, workID uint) (*domain.Work, error) // GetAnalysisData fetches persisted analysis data for a work - GetAnalysisData(ctx context.Context, workID uint) (*models2.TextMetadata, *models2.ReadabilityScore, *models2.LanguageAnalysis, error) + GetAnalysisData(ctx context.Context, workID uint) (*domain.TextMetadata, *domain.ReadabilityScore, *domain.LanguageAnalysis, error) } // GORMAnalysisRepository implements AnalysisRepository using GORM @@ -45,7 +45,7 @@ func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workI } // Determine language from the work record to avoid hardcoded defaults - var work models2.Work + var work domain.Work if err := r.db.WithContext(ctx).First(&work, workID).Error; err != nil { log.LogError("Failed to fetch work for language", log.F("workID", workID), @@ -54,7 +54,7 @@ func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workI } // Create text metadata - textMetadata := &models2.TextMetadata{ + textMetadata := &domain.TextMetadata{ WorkID: workID, Language: work.Language, WordCount: result.WordCount, @@ -65,7 +65,7 @@ func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workI } // Create readability score - readabilityScore := &models2.ReadabilityScore{ + readabilityScore := &domain.ReadabilityScore{ WorkID: workID, Language: work.Language, Score: result.ReadabilityScore, @@ -73,10 +73,10 @@ func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workI } // Create language analysis - languageAnalysis := &models2.LanguageAnalysis{ + languageAnalysis := &domain.LanguageAnalysis{ WorkID: workID, Language: work.Language, - Analysis: models2.JSONB{ + Analysis: domain.JSONB{ "sentiment": result.Sentiment, "keywords": extractKeywordsAsJSON(result.Keywords), "topics": extractTopicsAsJSON(result.Topics), @@ -89,7 +89,7 @@ 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 models2.Work + var work domain.Work if err := r.db.First(&work, workID).Error; err != nil { log.LogError("Failed to fetch work for content retrieval", log.F("workID", workID), @@ -102,7 +102,7 @@ func (r *GORMAnalysisRepository) GetWorkContent(ctx context.Context, workID uint // 2. Work's language translation // 3. Any available translation - var translation models2.Translation + var translation domain.Translation // Try original language first if err := r.db.Where("translatable_type = ? AND translatable_id = ? AND is_original_language = ?", @@ -126,8 +126,8 @@ func (r *GORMAnalysisRepository) GetWorkContent(ctx context.Context, workID uint } // GetWorkByID fetches a work by ID -func (r *GORMAnalysisRepository) GetWorkByID(ctx context.Context, workID uint) (*models2.Work, error) { - var work models2.Work +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 { return nil, fmt.Errorf("failed to fetch work: %w", err) } @@ -135,10 +135,10 @@ func (r *GORMAnalysisRepository) GetWorkByID(ctx context.Context, workID uint) ( } // GetAnalysisData fetches persisted analysis data for a work -func (r *GORMAnalysisRepository) GetAnalysisData(ctx context.Context, workID uint) (*models2.TextMetadata, *models2.ReadabilityScore, *models2.LanguageAnalysis, error) { - var textMetadata models2.TextMetadata - var readabilityScore models2.ReadabilityScore - var languageAnalysis models2.LanguageAnalysis +func (r *GORMAnalysisRepository) GetAnalysisData(ctx context.Context, workID uint) (*domain.TextMetadata, *domain.ReadabilityScore, *domain.LanguageAnalysis, error) { + var textMetadata domain.TextMetadata + var readabilityScore domain.ReadabilityScore + var languageAnalysis domain.LanguageAnalysis if err := r.db.WithContext(ctx).Where("work_id = ?", workID).First(&textMetadata).Error; err != nil { log.LogWarn("No text metadata found for work", @@ -160,14 +160,14 @@ func (r *GORMAnalysisRepository) GetAnalysisData(ctx context.Context, workID uin // StoreWorkAnalysis stores work-specific analysis results func (r *GORMAnalysisRepository) StoreWorkAnalysis(ctx context.Context, workID uint, - textMetadata *models2.TextMetadata, readabilityScore *models2.ReadabilityScore, - languageAnalysis *models2.LanguageAnalysis) error { + textMetadata *domain.TextMetadata, readabilityScore *domain.ReadabilityScore, + languageAnalysis *domain.LanguageAnalysis) error { // Use a transaction to ensure all data is stored atomically return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // Store text metadata if textMetadata != nil { - if err := tx.Where("work_id = ?", workID).Delete(&models2.TextMetadata{}).Error; err != nil { + if err := tx.Where("work_id = ?", workID).Delete(&domain.TextMetadata{}).Error; err != nil { log.LogError("Failed to delete existing text metadata", log.F("workID", workID), log.F("error", err)) @@ -184,7 +184,7 @@ func (r *GORMAnalysisRepository) StoreWorkAnalysis(ctx context.Context, workID u // Store readability score if readabilityScore != nil { - if err := tx.Where("work_id = ?", workID).Delete(&models2.ReadabilityScore{}).Error; err != nil { + if err := tx.Where("work_id = ?", workID).Delete(&domain.ReadabilityScore{}).Error; err != nil { log.LogError("Failed to delete existing readability score", log.F("workID", workID), log.F("error", err)) @@ -201,7 +201,7 @@ func (r *GORMAnalysisRepository) StoreWorkAnalysis(ctx context.Context, workID u // Store language analysis if languageAnalysis != nil { - if err := tx.Where("work_id = ?", workID).Delete(&models2.LanguageAnalysis{}).Error; err != nil { + if err := tx.Where("work_id = ?", workID).Delete(&domain.LanguageAnalysis{}).Error; err != nil { log.LogError("Failed to delete existing language analysis", log.F("workID", workID), log.F("error", err)) @@ -224,9 +224,9 @@ func (r *GORMAnalysisRepository) StoreWorkAnalysis(ctx context.Context, workID u } // Helper functions for data conversion -func extractKeywordsAsJSON(keywords []Keyword) models2.JSONB { +func extractKeywordsAsJSON(keywords []Keyword) domain.JSONB { if len(keywords) == 0 { - return models2.JSONB{} + return domain.JSONB{} } keywordData := make([]map[string]interface{}, len(keywords)) @@ -237,12 +237,12 @@ func extractKeywordsAsJSON(keywords []Keyword) models2.JSONB { } } - return models2.JSONB{"keywords": keywordData} + return domain.JSONB{"keywords": keywordData} } -func extractTopicsAsJSON(topics []Topic) models2.JSONB { +func extractTopicsAsJSON(topics []Topic) domain.JSONB { if len(topics) == 0 { - return models2.JSONB{} + return domain.JSONB{} } topicData := make([]map[string]interface{}, len(topics)) @@ -253,5 +253,5 @@ func extractTopicsAsJSON(topics []Topic) models2.JSONB { } } - return models2.JSONB{"topics": topicData} + return domain.JSONB{"topics": topicData} } diff --git a/internal/jobs/linguistics/keyword_extractor.go b/internal/jobs/linguistics/keyword_extractor.go index 2124f72..ff39c56 100644 --- a/internal/jobs/linguistics/keyword_extractor.go +++ b/internal/jobs/linguistics/keyword_extractor.go @@ -37,7 +37,7 @@ func (e *KeywordExtractor) Extract(text Text) ([]Keyword, error) { // Filter out stop words for word := range wordFreq { - if isStopWord(word) { + if isStopWord(word, text.Language) { delete(wordFreq, word) } } @@ -72,36 +72,3 @@ func (e *KeywordExtractor) Extract(text Text) ([]Keyword, error) { return keywords, nil } -// isStopWord checks if a word is a common stop word -func isStopWord(word string) bool { - stopWords := map[string]bool{ - "a": true, "about": true, "above": true, "after": true, "again": true, - "against": true, "all": true, "am": true, "an": true, "and": true, - "any": true, "are": true, "as": true, "at": true, "be": true, - "because": true, "been": true, "before": true, "being": true, "below": true, - "between": true, "both": true, "but": true, "by": true, "can": true, - "did": true, "do": true, "does": true, "doing": true, "don": true, - "down": true, "during": true, "each": true, "few": true, "for": true, - "from": true, "further": true, "had": true, "has": true, "have": true, - "having": true, "he": true, "her": true, "here": true, "hers": true, - "herself": true, "him": true, "himself": true, "his": true, "how": true, - "i": true, "if": true, "in": true, "into": true, "is": true, - "it": true, "its": true, "itself": true, "just": true, "me": true, - "more": true, "most": true, "my": true, "myself": true, "no": true, - "nor": true, "not": true, "now": true, "of": true, "off": true, - "on": true, "once": true, "only": true, "or": true, "other": true, - "our": true, "ours": true, "ourselves": true, "out": true, "over": true, - "own": true, "same": true, "she": true, "should": true, "so": true, - "some": true, "such": true, "than": true, "that": true, "the": true, - "their": true, "theirs": true, "them": true, "themselves": true, "then": true, - "there": true, "these": true, "they": true, "this": true, "those": true, - "through": true, "to": true, "too": true, "under": true, "until": true, - "up": true, "very": true, "was": true, "we": true, "were": true, - "what": true, "when": true, "where": true, "which": true, "while": true, - "who": true, "whom": true, "why": true, "will": true, "with": true, - "would": true, "you": true, "your": true, "yours": true, "yourself": true, - "yourselves": true, - } - - return stopWords[word] -} diff --git a/internal/jobs/linguistics/keyword_extractor_test.go b/internal/jobs/linguistics/keyword_extractor_test.go index 67b5c84..6df420c 100644 --- a/internal/jobs/linguistics/keyword_extractor_test.go +++ b/internal/jobs/linguistics/keyword_extractor_test.go @@ -1,4 +1,4 @@ -package enrich +package linguistics import "testing" diff --git a/internal/jobs/linguistics/language_detector.go b/internal/jobs/linguistics/language_detector.go index bbeeea6..acab6de 100644 --- a/internal/jobs/linguistics/language_detector.go +++ b/internal/jobs/linguistics/language_detector.go @@ -4,16 +4,16 @@ import ( "strings" ) -// LanguageDetector detects the language of a text -type LanguageDetector struct{} +// languageDetector detects the language of a text +type languageDetector struct{} // NewLanguageDetector creates a new LanguageDetector -func NewLanguageDetector() *LanguageDetector { - return &LanguageDetector{} +func NewLanguageDetector() *languageDetector { + return &languageDetector{} } // Detect detects the language of a text and returns the language code, confidence, and error -func (d *LanguageDetector) Detect(text Text) (string, float64, error) { +func (d *languageDetector) DetectLanguage(text string) (string, error) { // This is a simplified implementation // In a real-world scenario, you would use a library like github.com/pemistahl/lingua-go // or call an external API for language detection diff --git a/internal/jobs/linguistics/ports.go b/internal/jobs/linguistics/ports.go index cf28513..48997f8 100644 --- a/internal/jobs/linguistics/ports.go +++ b/internal/jobs/linguistics/ports.go @@ -3,7 +3,7 @@ package linguistics // LanguageDetector defines a provider that can detect the language of a text type LanguageDetector interface { // DetectLanguage returns a BCP-47 or ISO-like code and whether detection was confident - DetectLanguage(text string) (string, bool) + DetectLanguage(text string) (string, error) } // SentimentProvider defines a provider that scores sentiment in [-1, 1] diff --git a/internal/jobs/linguistics/registry.go b/internal/jobs/linguistics/registry.go index 3d8efd6..0b4d689 100644 --- a/internal/jobs/linguistics/registry.go +++ b/internal/jobs/linguistics/registry.go @@ -26,8 +26,9 @@ func DefaultRegistry() *Registry { // Text represents a text to be analyzed type Text struct { - ID uint - Body string + ID uint + Body string + Language string } // Token represents a token in a text @@ -38,12 +39,6 @@ type Token struct { Length int } -// Keyword represents a keyword extracted from a text -type Keyword struct { - Text string - Relevance float64 -} - // PoeticMetrics represents metrics from poetic analysis type PoeticMetrics struct { RhymeScheme string diff --git a/internal/jobs/linguistics/sync_job.go b/internal/jobs/linguistics/sync_job.go index a011eb5..8b1c042 100644 --- a/internal/jobs/linguistics/sync_job.go +++ b/internal/jobs/linguistics/sync_job.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" "log" - models2 "tercul/internal/models" + "tercul/internal/domain" "time" "github.com/hibiken/asynq" @@ -60,7 +60,7 @@ func (j *LinguisticSyncJob) EnqueueAnalysisForAllWorks() error { log.Println("Enqueueing linguistic analysis jobs for all works...") var workIDs []uint - if err := j.DB.Model(&models2.Work{}).Pluck("id", &workIDs).Error; err != nil { + if err := j.DB.Model(&domain.Work{}).Pluck("id", &workIDs).Error; err != nil { return fmt.Errorf("error fetching work IDs: %w", err) } @@ -87,7 +87,7 @@ func (j *LinguisticSyncJob) HandleLinguisticAnalysis(ctx context.Context, t *asy // Check if analysis already exists var count int64 - if err := j.DB.Model(&models2.LanguageAnalysis{}).Where("work_id = ?", payload.WorkID).Count(&count).Error; err != nil { + if err := j.DB.Model(&domain.LanguageAnalysis{}).Where("work_id = ?", payload.WorkID).Count(&count).Error; err != nil { return fmt.Errorf("error checking existing analysis: %w", err) } diff --git a/internal/jobs/linguistics/work_analysis_service.go b/internal/jobs/linguistics/work_analysis_service.go index 1a164f9..01d6ee9 100644 --- a/internal/jobs/linguistics/work_analysis_service.go +++ b/internal/jobs/linguistics/work_analysis_service.go @@ -3,7 +3,7 @@ package linguistics import ( "context" "fmt" - "tercul/internal/models" + "tercul/internal/domain" "time" "tercul/internal/platform/log" @@ -206,7 +206,7 @@ func (s *workAnalysisService) GetWorkAnalytics(ctx context.Context, workID uint) } // extractSentimentFromAnalysis extracts sentiment from the Analysis JSONB field -func extractSentimentFromAnalysis(analysis models.JSONB) float64 { +func extractSentimentFromAnalysis(analysis domain.JSONB) float64 { if analysis == nil { return 0.0 } diff --git a/internal/jobs/sync/edges_sync.go b/internal/jobs/sync/edges_sync.go index 72d005b..780b089 100644 --- a/internal/jobs/sync/edges_sync.go +++ b/internal/jobs/sync/edges_sync.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "log" - "tercul/internal/models" + "tercul/internal/domain" ) // SyncAllEdges syncs all edges by enqueueing batch jobs. @@ -12,7 +12,7 @@ func (s *SyncJob) SyncAllEdges(ctx context.Context) error { log.Println("Enqueueing edge sync jobs...") var count int64 - if err := s.DB.Model(&models.Edge{}).Count(&count).Error; err != nil { + if err := s.DB.Model(&domain.Edge{}).Count(&count).Error; err != nil { return fmt.Errorf("error counting edges: %w", err) } @@ -33,7 +33,7 @@ func (s *SyncJob) SyncAllEdges(ctx context.Context) error { func (s *SyncJob) SyncEdgesBatch(ctx context.Context, batchSize, offset int) error { log.Printf("Syncing edges batch (offset %d, batch size %d)...", offset, batchSize) - var edges []models.Edge + var edges []domain.Edge if err := s.DB.Limit(batchSize).Offset(offset).Find(&edges).Error; err != nil { return fmt.Errorf("error fetching edges batch: %w", err) } diff --git a/internal/platform/auth/jwt.go b/internal/platform/auth/jwt.go index 3d0bd0f..0f87264 100644 --- a/internal/platform/auth/jwt.go +++ b/internal/platform/auth/jwt.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" "strings" - "tercul/internal/models" + "tercul/internal/domain" "time" "github.com/golang-jwt/jwt/v5" @@ -55,7 +55,7 @@ func NewJWTManager() *JWTManager { } // GenerateToken generates a new JWT token for a user -func (j *JWTManager) GenerateToken(user *models.User) (string, error) { +func (j *JWTManager) GenerateToken(user *domain.User) (string, error) { now := time.Now() claims := &Claims{ UserID: user.ID, diff --git a/internal/platform/db/db.go b/internal/platform/db/db.go index 7ad1eb5..b3f3978 100644 --- a/internal/platform/db/db.go +++ b/internal/platform/db/db.go @@ -73,10 +73,7 @@ func InitDB() (*gorm.DB, error) { return nil, err } - // Run migrations - if err := RunMigrations(db); err != nil { - return nil, fmt.Errorf("failed to run migrations: %w", err) - } + // Migrations are now handled by a separate tool return db, nil } diff --git a/internal/platform/db/migrations.go b/internal/platform/db/migrations.go deleted file mode 100644 index 31c103a..0000000 --- a/internal/platform/db/migrations.go +++ /dev/null @@ -1,331 +0,0 @@ -package db - -import ( - "gorm.io/gorm" - models2 "tercul/internal/models" - "tercul/internal/platform/log" -) - -// RunMigrations runs all database migrations -func RunMigrations(db *gorm.DB) error { - log.LogInfo("Running database migrations") - - // First, create all tables using GORM AutoMigrate - if err := createTables(db); err != nil { - log.LogError("Failed to create tables", log.F("error", err)) - return err - } - - // Then add indexes to improve query performance - if err := addIndexes(db); err != nil { - log.LogError("Failed to add indexes", log.F("error", err)) - return err - } - - log.LogInfo("Database migrations completed successfully") - return nil -} - -// createTables creates all database tables using GORM AutoMigrate -func createTables(db *gorm.DB) error { - log.LogInfo("Creating database tables") - - // Enable recommended extensions - if err := db.Exec("CREATE EXTENSION IF NOT EXISTS pg_trgm").Error; err != nil { - log.LogError("Failed to enable pg_trgm extension", log.F("error", err)) - return err - } - - // Create all models/tables - err := db.AutoMigrate( - // User-related models - &models2.User{}, - &models2.UserProfile{}, - &models2.UserSession{}, - &models2.PasswordReset{}, - &models2.EmailVerification{}, - - // Literary models - &models2.Work{}, - &models2.Translation{}, - &models2.Author{}, - &models2.Book{}, - &models2.Publisher{}, - &models2.Source{}, - &models2.Edition{}, - &models2.Series{}, - &models2.WorkSeries{}, - - // Organization models - &models2.Tag{}, - &models2.Category{}, - - // Interaction models - &models2.Comment{}, - &models2.Like{}, - &models2.Bookmark{}, - &models2.Collection{}, - &models2.Contribution{}, - &models2.InteractionEvent{}, - - // Location models - &models2.Country{}, - &models2.City{}, - &models2.Place{}, - &models2.Address{}, - &models2.Language{}, - - // Linguistic models - &models2.ReadabilityScore{}, - &models2.WritingStyle{}, - &models2.LinguisticLayer{}, - &models2.TextMetadata{}, - &models2.PoeticAnalysis{}, - &models2.Word{}, - &models2.Concept{}, - &models2.LanguageEntity{}, - &models2.TextBlock{}, - &models2.WordOccurrence{}, - &models2.EntityOccurrence{}, - - // Relationship models - &models2.Edge{}, - &models2.Embedding{}, - &models2.Media{}, - &models2.BookWork{}, - &models2.AuthorCountry{}, - &models2.WorkAuthor{}, - &models2.BookAuthor{}, - - // System models - &models2.Notification{}, - &models2.EditorialWorkflow{}, - &models2.Admin{}, - &models2.Vote{}, - &models2.Contributor{}, - &models2.HybridEntityWork{}, - &models2.ModerationFlag{}, - &models2.AuditLog{}, - - // Rights models - &models2.Copyright{}, - &models2.CopyrightClaim{}, - &models2.Monetization{}, - &models2.License{}, - - // Analytics models - &models2.WorkStats{}, - &models2.TranslationStats{}, - &models2.UserStats{}, - &models2.BookStats{}, - &models2.CollectionStats{}, - &models2.MediaStats{}, - - // Metadata models - &models2.LanguageAnalysis{}, - &models2.Gamification{}, - &models2.Stats{}, - &models2.SearchDocument{}, - - // Psychological models - &models2.Emotion{}, - &models2.Mood{}, - &models2.TopicCluster{}, - ) - - if err != nil { - return err - } - - log.LogInfo("Database tables created successfully") - return nil -} - -// addIndexes adds indexes to frequently queried columns -func addIndexes(db *gorm.DB) error { - // Work table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_works_language ON works(language)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_works_title ON works(title)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_works_status ON works(status)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_works_slug ON works(slug)").Error; err != nil { - return err - } - - // Translation table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_translations_work_id ON translations(work_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_translations_language ON translations(language)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_translations_translator_id ON translations(translator_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE UNIQUE INDEX IF NOT EXISTS ux_translations_entity_lang ON translations(translatable_type, translatable_id, language)").Error; err != nil { - return err - } - - // User table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)").Error; err != nil { - return err - } - - // Author table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_authors_name ON authors(name)").Error; err != nil { - return err - } - - // Category table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_categories_name ON categories(name)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_categories_slug ON categories(slug)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_categories_path ON categories(path)").Error; err != nil { - return err - } - - // Tag table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_tags_slug ON tags(slug)").Error; err != nil { - return err - } - - // Comment table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_comments_user_id ON comments(user_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_comments_work_id ON comments(work_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_comments_translation_id ON comments(translation_id)").Error; err != nil { - return err - } - - // Like table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_likes_user_id ON likes(user_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_likes_work_id ON likes(work_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_likes_translation_id ON likes(translation_id)").Error; err != nil { - return err - } - - // Bookmark table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_bookmarks_user_id ON bookmarks(user_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_bookmarks_work_id ON bookmarks(work_id)").Error; err != nil { - return err - } - - // Collection table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_collections_user_id ON collections(user_id)").Error; err != nil { - return err - } - - // Contribution table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_contributions_user_id ON contributions(user_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_contributions_work_id ON contributions(work_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_contributions_status ON contributions(status)").Error; err != nil { - return err - } - - // Edge table indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_edges_source_table_id ON edges(source_table, source_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_edges_target_table_id ON edges(target_table, target_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_edges_relation ON edges(relation)").Error; err != nil { - return err - } - - // WorkAuthor unique pair and order index - if err := db.Exec("CREATE UNIQUE INDEX IF NOT EXISTS ux_work_authors_pair ON work_authors(work_id, author_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_work_authors_ordinal ON work_authors(ordinal)").Error; err != nil { - return err - } - - // BookAuthor unique pair and order index - if err := db.Exec("CREATE UNIQUE INDEX IF NOT EXISTS ux_book_authors_pair ON book_authors(book_id, author_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_book_authors_ordinal ON book_authors(ordinal)").Error; err != nil { - return err - } - - // InteractionEvent indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_interaction_events_target ON interaction_events(target_type, target_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_interaction_events_kind ON interaction_events(kind)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_interaction_events_user ON interaction_events(user_id)").Error; err != nil { - return err - } - - // SearchDocument indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_search_documents_entity ON search_documents(entity_type, entity_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_search_documents_lang ON search_documents(language_code)").Error; err != nil { - return err - } - - // Linguistic analysis indexes - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_text_metadata_work_id ON text_metadata(work_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_readability_scores_work_id ON readability_scores(work_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_language_analyses_work_id ON language_analyses(work_id)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_poetic_analyses_work_id ON poetic_analyses(work_id)").Error; err != nil { - return err - } - - // Timestamps indexes for frequently queried tables - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_works_created_at ON works(created_at)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_translations_created_at ON translations(created_at)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_comments_created_at ON comments(created_at)").Error; err != nil { - return err - } - if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at)").Error; err != nil { - return err - } - - log.LogInfo("Database indexes added successfully") - return nil -} diff --git a/internal/platform/search/weaviate_client.go b/internal/platform/search/weaviate_client.go index b3e5662..a40a4de 100644 --- a/internal/platform/search/weaviate_client.go +++ b/internal/platform/search/weaviate_client.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "log" - "tercul/internal/models" + "tercul/internal/domain" "tercul/internal/platform/config" "time" @@ -27,7 +27,7 @@ func InitWeaviate() { } // UpsertWork inserts or updates a Work object in Weaviate -func UpsertWork(work models.Work) error { +func UpsertWork(work domain.Work) error { // Create a properties map with the fields that exist in the Work model properties := map[string]interface{}{ "language": work.Language, diff --git a/internal/repositories/copyright_repository.go b/internal/repositories/copyright_repository.go deleted file mode 100644 index e926e07..0000000 --- a/internal/repositories/copyright_repository.go +++ /dev/null @@ -1,93 +0,0 @@ -package repositories - -import ( - "context" - "errors" - "gorm.io/gorm" - "tercul/internal/models" -) - -// CopyrightRepository defines CRUD methods specific to Copyright. -type CopyrightRepository interface { - BaseRepository[models.Copyright] - // Polymorphic methods - AttachToEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error - DetachFromEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error - GetByEntity(ctx context.Context, entityID uint, entityType string) ([]models.Copyright, error) - GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]models.Copyrightable, error) - // Translation methods - AddTranslation(ctx context.Context, translation *models.CopyrightTranslation) error - GetTranslations(ctx context.Context, copyrightID uint) ([]models.CopyrightTranslation, error) - GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*models.CopyrightTranslation, error) -} - -type copyrightRepository struct { - BaseRepository[models.Copyright] - db *gorm.DB -} - -// NewCopyrightRepository creates a new CopyrightRepository. -func NewCopyrightRepository(db *gorm.DB) CopyrightRepository { - return ©rightRepository{ - BaseRepository: NewBaseRepositoryImpl[models.Copyright](db), - db: db, - } -} - -// AttachToEntity attaches a copyright to any entity type -func (r *copyrightRepository) AttachToEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error { - copyrightable := models.Copyrightable{ - CopyrightID: copyrightID, - CopyrightableID: entityID, - CopyrightableType: entityType, - } - return r.db.WithContext(ctx).Create(©rightable).Error -} - -// DetachFromEntity removes a copyright from an entity -func (r *copyrightRepository) DetachFromEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error { - return r.db.WithContext(ctx).Where("copyright_id = ? AND copyrightable_id = ? AND copyrightable_type = ?", - copyrightID, entityID, entityType).Delete(&models.Copyrightable{}).Error -} - -// GetByEntity gets all copyrights for a specific entity -func (r *copyrightRepository) GetByEntity(ctx context.Context, entityID uint, entityType string) ([]models.Copyright, error) { - var copyrights []models.Copyright - err := r.db.WithContext(ctx).Joins("JOIN copyrightables ON copyrightables.copyright_id = copyrights.id"). - Where("copyrightables.copyrightable_id = ? AND copyrightables.copyrightable_type = ?", entityID, entityType). - Preload("Translations"). - Find(©rights).Error - return copyrights, err -} - -// GetEntitiesByCopyright gets all entities that have a specific copyright -func (r *copyrightRepository) GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]models.Copyrightable, error) { - var copyrightables []models.Copyrightable - err := r.db.WithContext(ctx).Where("copyright_id = ?", copyrightID).Find(©rightables).Error - return copyrightables, err -} - -// AddTranslation adds a translation to a copyright -func (r *copyrightRepository) AddTranslation(ctx context.Context, translation *models.CopyrightTranslation) error { - return r.db.WithContext(ctx).Create(translation).Error -} - -// GetTranslations gets all translations for a copyright -func (r *copyrightRepository) GetTranslations(ctx context.Context, copyrightID uint) ([]models.CopyrightTranslation, error) { - var translations []models.CopyrightTranslation - err := r.db.WithContext(ctx).Where("copyright_id = ?", copyrightID).Find(&translations).Error - return translations, err -} - -// GetTranslationByLanguage gets a specific translation by language code -func (r *copyrightRepository) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*models.CopyrightTranslation, error) { - var translation models.CopyrightTranslation - err := r.db.WithContext(ctx).Where("copyright_id = ? AND language_code = ?", copyrightID, languageCode).First(&translation).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrEntityNotFound - } - return nil, err - } - return &translation, nil -} diff --git a/internal/store/db.go b/internal/store/db.go deleted file mode 100644 index 3f0853b..0000000 --- a/internal/store/db.go +++ /dev/null @@ -1,146 +0,0 @@ -package store - -import ( - "gorm.io/gorm" - "strings" - models2 "tercul/internal/models" -) - -// DB represents a database connection -type DB struct { - *gorm.DB -} - -// Connect creates a new database connection -func Connect() *DB { - // In a real application, this would use configuration from environment variables - // or a configuration file to connect to the database - // For this example, we'll assume the DB connection is passed in from main.go - return nil -} - -// ListPendingWorks returns a list of works that need to be enriched -func ListPendingWorks(db *DB) []Work { - var works []Work - - // Query for works that haven't been enriched yet - var modelWorks []models2.Work - db.Where("id NOT IN (SELECT work_id FROM language_analyses)").Find(&modelWorks) - - // Convert to store.Work - for _, work := range modelWorks { - // Prefer original language translation; fallback to work language; then any - var content string - var t models2.Translation - // Try original - if err := db.Where("translatable_type = ? AND translatable_id = ? AND is_original_language = ?", "Work", work.ID, true). - First(&t).Error; err == nil { - content = t.Content - } else { - // Try same language - if err := db.Where("translatable_type = ? AND translatable_id = ? AND language = ?", "Work", work.ID, work.Language). - First(&t).Error; err == nil { - content = t.Content - } else { - // Any translation - if err := db.Where("translatable_type = ? AND translatable_id = ?", "Work", work.ID). - First(&t).Error; err == nil { - content = t.Content - } - } - } - works = append(works, Work{ - ID: work.ID, - Body: content, - }) - } - - return works -} - -// UpsertWord creates or updates a word in the database -func UpsertWord(db *DB, workID uint, text, lemma, pos, phonetic string) error { - // Check if the word already exists - var word models2.Word - result := db.Where("text = ? AND language = ?", text, "auto").First(&word) - - if result.Error != nil && result.Error != gorm.ErrRecordNotFound { - return result.Error - } - - // Create or update the word - if result.Error == gorm.ErrRecordNotFound { - // Create new word - word = models2.Word{ - Text: text, - Language: "auto", // This would be set to the detected language - PartOfSpeech: pos, - Lemma: lemma, - } - if err := db.Create(&word).Error; err != nil { - return err - } - } else { - // Update existing word - word.PartOfSpeech = pos - word.Lemma = lemma - if err := db.Save(&word).Error; err != nil { - return err - } - } - - // Associate the word with the work - return db.Exec("INSERT INTO work_words (work_id, word_id) VALUES (?, ?) ON CONFLICT DO NOTHING", workID, word.ID).Error -} - -// SaveKeywords saves keywords for a work -func SaveKeywords(db *DB, workID uint, keywords []string) error { - // Clear existing keywords - if err := db.Exec("DELETE FROM work_topic_clusters WHERE work_id = ?", workID).Error; err != nil { - return err - } - - // Create a topic cluster for the keywords - cluster := models2.TopicCluster{ - Name: "Auto-generated", - Description: "Automatically generated keywords", - Keywords: strings.Join(keywords, ", "), - } - - if err := db.Create(&cluster).Error; err != nil { - return err - } - - // Associate the cluster with the work - return db.Exec("INSERT INTO work_topic_clusters (work_id, topic_cluster_id) VALUES (?, ?)", workID, cluster.ID).Error -} - -// SavePoetics saves poetic analysis for a work -func SavePoetics(db *DB, workID uint, metrics PoeticMetrics) error { - poetics := models2.PoeticAnalysis{ - WorkID: workID, - Language: "auto", // This would be set to the detected language - RhymeScheme: metrics.RhymeScheme, - MeterType: metrics.MeterType, - StanzaCount: metrics.StanzaCount, - LineCount: metrics.LineCount, - Structure: metrics.Structure, - } - - return db.Create(&poetics).Error -} - -// MarkEnriched marks a work as enriched with the detected language -func MarkEnriched(db *DB, workID uint, language string) error { - // Create a language analysis record to mark the work as processed - analysis := models2.LanguageAnalysis{ - WorkID: workID, - Language: language, - Analysis: models2.JSONB{ - "enriched": true, - "language": language, - }, - } - - return db.Create(&analysis).Error -} diff --git a/internal/store/models.go b/internal/store/models.go deleted file mode 100644 index d8bfb5c..0000000 --- a/internal/store/models.go +++ /dev/null @@ -1,16 +0,0 @@ -package store - -// Work represents a work to be processed -type Work struct { - ID uint - Body string -} - -// PoeticMetrics represents metrics from poetic analysis -type PoeticMetrics struct { - RhymeScheme string - MeterType string - StanzaCount int - LineCount int - Structure string -} diff --git a/internal/store/processor.go b/internal/store/processor.go deleted file mode 100644 index b90e99d..0000000 --- a/internal/store/processor.go +++ /dev/null @@ -1,119 +0,0 @@ -package store - -import ( - "context" - "log" - "tercul/internal/enrich" -) - -// ProcessWork processes a work using the enrichment registry and stores the results -func ProcessWork(ctx context.Context, reg *enrich.Registry, db *DB, work Work) error { - log.Printf("Processing work ID %d", work.ID) - - // Create a text object for the enrichment services - text := enrich.Text{ID: work.ID, Body: work.Body} - - // Detect language - lang, confidence, err := reg.Lang.Detect(text) - if err != nil { - return err - } - log.Printf("Detected language: %s (confidence: %.2f)", lang, confidence) - - // Tokenize text - tokens, err := reg.Tok.Tokenize(text) - if err != nil { - return err - } - log.Printf("Tokenized text into %d tokens", len(tokens)) - - // Tag parts of speech - pos, err := reg.Pos.Tag(tokens) - if err != nil { - return err - } - log.Printf("Tagged %d tokens with parts of speech", len(pos)) - - // Process each token - for i, token := range tokens { - // Get lemma - lemma, err := reg.Lem.Lemma(token.Text, lang) - if err != nil { - log.Printf("Error getting lemma for token %s: %v", token.Text, err) - lemma = token.Text // Use the original text as fallback - } - - // Get phonetic encoding - phonetic := reg.Phon.Encode(token.Text) - - // Store the word - if err := UpsertWord(db, work.ID, token.Text, lemma, pos[i], phonetic); err != nil { - log.Printf("Error storing word %s: %v", token.Text, err) - } - } - - // Extract keywords - keywords, err := reg.Key.Extract(text) - if err != nil { - log.Printf("Error extracting keywords: %v", err) - } else { - // Convert keywords to strings - keywordStrings := make([]string, len(keywords)) - for i, kw := range keywords { - keywordStrings[i] = kw.Text - } - - // Save keywords - if err := SaveKeywords(db, work.ID, keywordStrings); err != nil { - log.Printf("Error saving keywords: %v", err) - } - } - - // Analyze poetics - enrichMetrics, err := reg.Poet.Analyse(text) - if err != nil { - log.Printf("Error analyzing poetics: %v", err) - } else { - // Convert to store.PoeticMetrics - metrics := PoeticMetrics{ - RhymeScheme: enrichMetrics.RhymeScheme, - MeterType: enrichMetrics.MeterType, - StanzaCount: enrichMetrics.StanzaCount, - LineCount: enrichMetrics.LineCount, - Structure: enrichMetrics.Structure, - } - - // Save poetics - if err := SavePoetics(db, work.ID, metrics); err != nil { - log.Printf("Error saving poetics: %v", err) - } - } - - // Mark the work as enriched - if err := MarkEnriched(db, work.ID, lang); err != nil { - log.Printf("Error marking work as enriched: %v", err) - return err - } - - log.Printf("Successfully processed work ID %d", work.ID) - return nil -} - -// ProcessPendingWorks processes all pending works -func ProcessPendingWorks(ctx context.Context, reg *enrich.Registry, db *DB) error { - log.Println("Processing pending works...") - - // Get pending works - works := ListPendingWorks(db) - log.Printf("Found %d pending works", len(works)) - - // Process each work - for _, work := range works { - if err := ProcessWork(ctx, reg, db, work); err != nil { - log.Printf("Error processing work ID %d: %v", work.ID, err) - } - } - - log.Println("Finished processing pending works") - return nil -} diff --git a/internal/testutil/integration_test_utils.go b/internal/testutil/integration_test_utils.go index d5d59a0..94ea710 100644 --- a/internal/testutil/integration_test_utils.go +++ b/internal/testutil/integration_test_utils.go @@ -13,37 +13,41 @@ import ( "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" - "tercul/internal/models" - "tercul/internal/repositories" - "tercul/services" - "tercul/graph" + + graph "tercul/internal/adapters/graphql" + "tercul/internal/app/auth" + "tercul/internal/app/localization" + "tercul/internal/app/work" + "tercul/internal/data/sql" + "tercul/internal/domain" ) // IntegrationTestSuite provides a comprehensive test environment with either in-memory SQLite or mock repositories type IntegrationTestSuite struct { suite.Suite - DB *gorm.DB - WorkRepo repositories.WorkRepository - UserRepo repositories.UserRepository - AuthorRepo repositories.AuthorRepository - TranslationRepo repositories.TranslationRepository - CommentRepo repositories.CommentRepository - LikeRepo repositories.LikeRepository - BookmarkRepo repositories.BookmarkRepository - CollectionRepo repositories.CollectionRepository - TagRepo repositories.TagRepository - CategoryRepo repositories.CategoryRepository - + DB *gorm.DB + WorkRepo domain.WorkRepository + UserRepo domain.UserRepository + AuthorRepo domain.AuthorRepository + TranslationRepo domain.TranslationRepository + CommentRepo domain.CommentRepository + LikeRepo domain.LikeRepository + BookmarkRepo domain.BookmarkRepository + CollectionRepo domain.CollectionRepository + TagRepo domain.TagRepository + CategoryRepo domain.CategoryRepository + // Services - WorkService services.WorkService - Localization services.LocalizationService - AuthService services.AuthService - + WorkCommands *work.WorkCommands + WorkQueries *work.WorkQueries + Localization localization.Service + AuthService auth.Service + // Test data - TestWorks []*models.Work - TestUsers []*models.User - TestAuthors []*models.Author - TestTranslations []*models.Translation + TestWorks []*domain.Work + TestUsers []*domain.User + TestAuthors []*domain.Author + TestTranslations []*domain.Translation } // TestConfig holds configuration for the test environment @@ -115,53 +119,53 @@ func (s *IntegrationTestSuite) setupInMemoryDB(config *TestConfig) { // Run migrations if err := db.AutoMigrate( - &models.Work{}, - &models.User{}, - &models.Author{}, - &models.Translation{}, - &models.Comment{}, - &models.Like{}, - &models.Bookmark{}, - &models.Collection{}, - &models.Tag{}, - &models.Category{}, - &models.Country{}, - &models.City{}, - &models.Place{}, - &models.Address{}, - &models.Copyright{}, - &models.CopyrightClaim{}, - &models.Monetization{}, - &models.Book{}, - &models.Publisher{}, - &models.Source{}, - // &models.WorkAnalytics{}, // Commented out as it's not in models package - &models.ReadabilityScore{}, - &models.WritingStyle{}, - &models.Emotion{}, - &models.TopicCluster{}, - &models.Mood{}, - &models.Concept{}, - &models.LinguisticLayer{}, - &models.WorkStats{}, - &models.TextMetadata{}, - &models.PoeticAnalysis{}, - &models.TranslationField{}, + &domain.Work{}, + &domain.User{}, + &domain.Author{}, + &domain.Translation{}, + &domain.Comment{}, + &domain.Like{}, + &domain.Bookmark{}, + &domain.Collection{}, + &domain.Tag{}, + &domain.Category{}, + &domain.Country{}, + &domain.City{}, + &domain.Place{}, + &domain.Address{}, + &domain.Copyright{}, + &domain.CopyrightClaim{}, + &domain.Monetization{}, + &domain.Book{}, + &domain.Publisher{}, + &domain.Source{}, + // &domain.WorkAnalytics{}, // Commented out as it's not in models package + &domain.ReadabilityScore{}, + &domain.WritingStyle{}, + &domain.Emotion{}, + &domain.TopicCluster{}, + &domain.Mood{}, + &domain.Concept{}, + &domain.LinguisticLayer{}, + &domain.WorkStats{}, + &domain.TextMetadata{}, + &domain.PoeticAnalysis{}, + &domain.TranslationField{}, ); err != nil { s.T().Fatalf("Failed to run migrations: %v", err) } // Create repository instances - s.WorkRepo = repositories.NewWorkRepository(db) - s.UserRepo = repositories.NewUserRepository(db) - s.AuthorRepo = repositories.NewAuthorRepository(db) - s.TranslationRepo = repositories.NewTranslationRepository(db) - s.CommentRepo = repositories.NewCommentRepository(db) - s.LikeRepo = repositories.NewLikeRepository(db) - s.BookmarkRepo = repositories.NewBookmarkRepository(db) - s.CollectionRepo = repositories.NewCollectionRepository(db) - s.TagRepo = repositories.NewTagRepository(db) - s.CategoryRepo = repositories.NewCategoryRepository(db) + s.WorkRepo = sql.NewWorkRepository(db) + s.UserRepo = sql.NewUserRepository(db) + s.AuthorRepo = sql.NewAuthorRepository(db) + s.TranslationRepo = sql.NewTranslationRepository(db) + s.CommentRepo = sql.NewCommentRepository(db) + s.LikeRepo = sql.NewLikeRepository(db) + s.BookmarkRepo = sql.NewBookmarkRepository(db) + s.CollectionRepo = sql.NewCollectionRepository(db) + s.TagRepo = sql.NewTagRepository(db) + s.CategoryRepo = sql.NewCategoryRepository(db) } // setupMockRepositories sets up mock repositories for testing @@ -181,16 +185,17 @@ func (s *IntegrationTestSuite) setupMockRepositories() { // setupServices sets up service instances func (s *IntegrationTestSuite) setupServices() { - s.WorkService = services.NewWorkService(s.WorkRepo, nil) - // Temporarily comment out services that depend on problematic repositories - // s.Localization = services.NewLocalizationService(s.TranslationRepo) - // s.AuthService = services.NewAuthService(s.UserRepo, "test-secret-key") + mockAnalyzer := &MockAnalyzer{} + s.WorkCommands = work.NewWorkCommands(s.WorkRepo, mockAnalyzer) + s.WorkQueries = work.NewWorkQueries(s.WorkRepo) + s.Localization = localization.NewService(s.TranslationRepo) + s.AuthService = auth.NewService(s.UserRepo, "test-secret-key") } // setupTestData creates initial test data func (s *IntegrationTestSuite) setupTestData() { // Create test users - s.TestUsers = []*models.User{ + s.TestUsers = []*domain.User{ {Username: "testuser1", Email: "test1@example.com", FirstName: "Test", LastName: "User1"}, {Username: "testuser2", Email: "test2@example.com", FirstName: "Test", LastName: "User2"}, } @@ -202,7 +207,7 @@ func (s *IntegrationTestSuite) setupTestData() { } // Create test authors - s.TestAuthors = []*models.Author{ + s.TestAuthors = []*domain.Author{ {Name: "Test Author 1", Language: "en"}, {Name: "Test Author 2", Language: "fr"}, } @@ -214,7 +219,7 @@ func (s *IntegrationTestSuite) setupTestData() { } // Create test works - s.TestWorks = []*models.Work{ + s.TestWorks = []*domain.Work{ {Title: "Test Work 1", Language: "en"}, {Title: "Test Work 2", Language: "en"}, {Title: "Test Work 3", Language: "fr"}, @@ -227,7 +232,7 @@ func (s *IntegrationTestSuite) setupTestData() { } // Create test translations - s.TestTranslations = []*models.Translation{ + s.TestTranslations = []*domain.Translation{ { Title: "Test Work 1", Content: "Test content for work 1", @@ -292,35 +297,23 @@ func (s *IntegrationTestSuite) SetupTest() { // GetResolver returns a properly configured GraphQL resolver for testing func (s *IntegrationTestSuite) GetResolver() *graph.Resolver { return &graph.Resolver{ - WorkRepo: s.WorkRepo, - UserRepo: s.UserRepo, - AuthorRepo: s.AuthorRepo, - TranslationRepo: s.TranslationRepo, - CommentRepo: s.CommentRepo, - LikeRepo: s.LikeRepo, - BookmarkRepo: s.BookmarkRepo, - CollectionRepo: s.CollectionRepo, - TagRepo: s.TagRepo, - CategoryRepo: s.CategoryRepo, - WorkService: s.WorkService, - Localization: s.Localization, - AuthService: s.AuthService, + // This needs to be updated to reflect the new resolver structure } } // CreateTestWork creates a test work with optional content -func (s *IntegrationTestSuite) CreateTestWork(title, language string, content string) *models.Work { - work := &models.Work{ - Title: title, +func (s *IntegrationTestSuite) CreateTestWork(title, language string, content string) *domain.Work { + work := &domain.Work{ + Title: title, + Language: language, } - work.Language = language if err := s.WorkRepo.Create(context.Background(), work); err != nil { s.T().Fatalf("Failed to create test work: %v", err) } if content != "" { - translation := &models.Translation{ + translation := &domain.Translation{ Title: title, Content: content, Language: language, diff --git a/internal/testutil/mock_base_repository.go b/internal/testutil/mock_base_repository.go index e8197f1..af98d7a 100644 --- a/internal/testutil/mock_base_repository.go +++ b/internal/testutil/mock_base_repository.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "tercul/internal/repositories" + "tercul/internal/domain" "gorm.io/gorm" ) @@ -40,17 +40,17 @@ func (m *MockBaseRepository[T]) DeleteInTx(ctx context.Context, tx *gorm.DB, id } // GetByIDWithOptions retrieves an entity by its ID with query options (mock implementation) -func (m *MockBaseRepository[T]) GetByIDWithOptions(ctx context.Context, id uint, options *repositories.QueryOptions) (*T, error) { +func (m *MockBaseRepository[T]) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*T, error) { return nil, fmt.Errorf("GetByIDWithOptions not implemented in mock repository") } // ListWithOptions returns entities with query options (mock implementation) -func (m *MockBaseRepository[T]) ListWithOptions(ctx context.Context, options *repositories.QueryOptions) ([]T, error) { +func (m *MockBaseRepository[T]) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]T, error) { return nil, fmt.Errorf("ListWithOptions not implemented in mock repository") } // CountWithOptions returns the count with query options (mock implementation) -func (m *MockBaseRepository[T]) CountWithOptions(ctx context.Context, options *repositories.QueryOptions) (int64, error) { +func (m *MockBaseRepository[T]) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, fmt.Errorf("CountWithOptions not implemented in mock repository") } diff --git a/internal/testutil/mock_translation_repository.go b/internal/testutil/mock_translation_repository.go index 06b55d1..52a71b6 100644 --- a/internal/testutil/mock_translation_repository.go +++ b/internal/testutil/mock_translation_repository.go @@ -4,23 +4,22 @@ import ( "context" "errors" "gorm.io/gorm" - models2 "tercul/internal/models" - repositories2 "tercul/internal/repositories" + "tercul/internal/domain" ) // MockTranslationRepository is an in-memory implementation of TranslationRepository type MockTranslationRepository struct { - items []models2.Translation + items []domain.Translation } func NewMockTranslationRepository() *MockTranslationRepository { - return &MockTranslationRepository{items: []models2.Translation{}} + return &MockTranslationRepository{items: []domain.Translation{}} } -var _ repositories2.TranslationRepository = (*MockTranslationRepository)(nil) +var _ domain.TranslationRepository = (*MockTranslationRepository)(nil) // BaseRepository methods with context support -func (m *MockTranslationRepository) Create(ctx context.Context, t *models2.Translation) error { +func (m *MockTranslationRepository) Create(ctx context.Context, t *domain.Translation) error { if t == nil { return errors.New("nil translation") } @@ -29,24 +28,24 @@ func (m *MockTranslationRepository) Create(ctx context.Context, t *models2.Trans return nil } -func (m *MockTranslationRepository) GetByID(ctx context.Context, id uint) (*models2.Translation, error) { +func (m *MockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) { for i := range m.items { if m.items[i].ID == id { cp := m.items[i] return &cp, nil } } - return nil, repositories2.ErrEntityNotFound + return nil, domain.ErrEntityNotFound } -func (m *MockTranslationRepository) Update(ctx context.Context, t *models2.Translation) error { +func (m *MockTranslationRepository) Update(ctx context.Context, t *domain.Translation) error { for i := range m.items { if m.items[i].ID == t.ID { m.items[i] = *t return nil } } - return repositories2.ErrEntityNotFound + return domain.ErrEntityNotFound } func (m *MockTranslationRepository) Delete(ctx context.Context, id uint) error { @@ -56,57 +55,57 @@ func (m *MockTranslationRepository) Delete(ctx context.Context, id uint) error { return nil } } - return repositories2.ErrEntityNotFound + return domain.ErrEntityNotFound } -func (m *MockTranslationRepository) List(ctx context.Context, page, pageSize int) (*repositories2.PaginatedResult[models2.Translation], error) { - all := append([]models2.Translation(nil), m.items...) +func (m *MockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { + all := append([]domain.Translation(nil), m.items...) total := int64(len(all)) start := (page - 1) * pageSize end := start + pageSize if start > len(all) { - return &repositories2.PaginatedResult[models2.Translation]{Items: []models2.Translation{}, TotalCount: total}, nil + return &domain.PaginatedResult[domain.Translation]{Items: []domain.Translation{}, TotalCount: total}, nil } if end > len(all) { end = len(all) } - return &repositories2.PaginatedResult[models2.Translation]{Items: all[start:end], TotalCount: total}, nil + return &domain.PaginatedResult[domain.Translation]{Items: all[start:end], TotalCount: total}, nil } -func (m *MockTranslationRepository) ListAll(ctx context.Context) ([]models2.Translation, error) { - return append([]models2.Translation(nil), m.items...), nil +func (m *MockTranslationRepository) ListAll(ctx context.Context) ([]domain.Translation, error) { + return append([]domain.Translation(nil), m.items...), nil } func (m *MockTranslationRepository) Count(ctx context.Context) (int64, error) { return int64(len(m.items)), nil } -func (m *MockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*models2.Translation, error) { +func (m *MockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) { return m.GetByID(ctx, id) } -func (m *MockTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]models2.Translation, error) { - all := append([]models2.Translation(nil), m.items...) +func (m *MockTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Translation, error) { + all := append([]domain.Translation(nil), m.items...) end := offset + batchSize if end > len(all) { end = len(all) } if offset > len(all) { - return []models2.Translation{}, nil + return []domain.Translation{}, nil } return all[offset:end], nil } // New BaseRepository methods -func (m *MockTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *models2.Translation) error { +func (m *MockTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return m.Create(ctx, entity) } -func (m *MockTranslationRepository) GetByIDWithOptions(ctx context.Context, id uint, options *repositories2.QueryOptions) (*models2.Translation, error) { +func (m *MockTranslationRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) { return m.GetByID(ctx, id) } -func (m *MockTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *models2.Translation) error { +func (m *MockTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return m.Update(ctx, entity) } @@ -114,7 +113,7 @@ func (m *MockTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, return m.Delete(ctx, id) } -func (m *MockTranslationRepository) ListWithOptions(ctx context.Context, options *repositories2.QueryOptions) ([]models2.Translation, error) { +func (m *MockTranslationRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Translation, error) { result, err := m.List(ctx, 1, 1000) if err != nil { return nil, err @@ -122,7 +121,7 @@ func (m *MockTranslationRepository) ListWithOptions(ctx context.Context, options return result.Items, nil } -func (m *MockTranslationRepository) CountWithOptions(ctx context.Context, options *repositories2.QueryOptions) (int64, error) { +func (m *MockTranslationRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return m.Count(ctx) } @@ -140,12 +139,12 @@ func (m *MockTranslationRepository) WithTx(ctx context.Context, fn func(tx *gorm } // TranslationRepository specific methods -func (m *MockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]models2.Translation, error) { +func (m *MockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) { return m.ListByEntity(ctx, "Work", workID) } -func (m *MockTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]models2.Translation, error) { - var out []models2.Translation +func (m *MockTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) { + var out []domain.Translation for i := range m.items { tr := m.items[i] if tr.TranslatableType == entityType && tr.TranslatableID == entityID { @@ -155,8 +154,8 @@ func (m *MockTranslationRepository) ListByEntity(ctx context.Context, entityType return out, nil } -func (m *MockTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]models2.Translation, error) { - var out []models2.Translation +func (m *MockTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) { + var out []domain.Translation for i := range m.items { if m.items[i].TranslatorID != nil && *m.items[i].TranslatorID == translatorID { out = append(out, m.items[i]) @@ -165,8 +164,8 @@ func (m *MockTranslationRepository) ListByTranslatorID(ctx context.Context, tran return out, nil } -func (m *MockTranslationRepository) ListByStatus(ctx context.Context, status models2.TranslationStatus) ([]models2.Translation, error) { - var out []models2.Translation +func (m *MockTranslationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) { + var out []domain.Translation for i := range m.items { if m.items[i].Status == status { out = append(out, m.items[i]) @@ -177,12 +176,12 @@ func (m *MockTranslationRepository) ListByStatus(ctx context.Context, status mod // Test helper: add a translation for a Work func (m *MockTranslationRepository) AddTranslationForWork(workID uint, language string, content string, isOriginal bool) { - m.Create(context.Background(), &models2.Translation{ + m.Create(context.Background(), &domain.Translation{ Title: "", Content: content, Description: "", Language: language, - Status: models2.TranslationStatusPublished, + Status: domain.TranslationStatusPublished, TranslatableID: workID, TranslatableType: "Work", IsOriginalLanguage: isOriginal, diff --git a/internal/testutil/mock_work_repository.go b/internal/testutil/mock_work_repository.go index 70f1882..339e12c 100644 --- a/internal/testutil/mock_work_repository.go +++ b/internal/testutil/mock_work_repository.go @@ -3,22 +3,21 @@ package testutil import ( "context" "gorm.io/gorm" - "tercul/internal/models" - "tercul/internal/repositories" + "tercul/internal/domain" ) // UnifiedMockWorkRepository is a shared mock for WorkRepository tests // Implements all required methods and uses an in-memory slice type UnifiedMockWorkRepository struct { - Works []*models.Work + Works []*domain.Work } func NewUnifiedMockWorkRepository() *UnifiedMockWorkRepository { - return &UnifiedMockWorkRepository{Works: []*models.Work{}} + return &UnifiedMockWorkRepository{Works: []*domain.Work{}} } -func (m *UnifiedMockWorkRepository) AddWork(work *models.Work) { +func (m *UnifiedMockWorkRepository) AddWork(work *domain.Work) { work.ID = uint(len(m.Works) + 1) if work.Language == "" { work.Language = "en" // default for tests, can be set by caller @@ -27,28 +26,28 @@ func (m *UnifiedMockWorkRepository) AddWork(work *models.Work) { } // BaseRepository methods with context support -func (m *UnifiedMockWorkRepository) Create(ctx context.Context, entity *models.Work) error { +func (m *UnifiedMockWorkRepository) Create(ctx context.Context, entity *domain.Work) error { m.AddWork(entity) return nil } -func (m *UnifiedMockWorkRepository) GetByID(ctx context.Context, id uint) (*models.Work, error) { +func (m *UnifiedMockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) { for _, w := range m.Works { if w.ID == id { return w, nil } } - return nil, repositories.ErrEntityNotFound + return nil, domain.ErrEntityNotFound } -func (m *UnifiedMockWorkRepository) Update(ctx context.Context, entity *models.Work) error { +func (m *UnifiedMockWorkRepository) Update(ctx context.Context, entity *domain.Work) error { for i, w := range m.Works { if w.ID == entity.ID { m.Works[i] = entity return nil } } - return repositories.ErrEntityNotFound + return domain.ErrEntityNotFound } func (m *UnifiedMockWorkRepository) Delete(ctx context.Context, id uint) error { @@ -58,11 +57,11 @@ func (m *UnifiedMockWorkRepository) Delete(ctx context.Context, id uint) error { return nil } } - return repositories.ErrEntityNotFound + return domain.ErrEntityNotFound } -func (m *UnifiedMockWorkRepository) List(ctx context.Context, page, pageSize int) (*repositories.PaginatedResult[models.Work], error) { - var all []models.Work +func (m *UnifiedMockWorkRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { + var all []domain.Work for _, w := range m.Works { if w != nil { all = append(all, *w) @@ -72,16 +71,16 @@ func (m *UnifiedMockWorkRepository) List(ctx context.Context, page, pageSize int start := (page - 1) * pageSize end := start + pageSize if start > len(all) { - return &repositories.PaginatedResult[models.Work]{Items: []models.Work{}, TotalCount: total}, nil + return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}, TotalCount: total}, nil } if end > len(all) { end = len(all) } - return &repositories.PaginatedResult[models.Work]{Items: all[start:end], TotalCount: total}, nil + return &domain.PaginatedResult[domain.Work]{Items: all[start:end], TotalCount: total}, nil } -func (m *UnifiedMockWorkRepository) ListAll(ctx context.Context) ([]models.Work, error) { - var all []models.Work +func (m *UnifiedMockWorkRepository) ListAll(ctx context.Context) ([]domain.Work, error) { + var all []domain.Work for _, w := range m.Works { if w != nil { all = append(all, *w) @@ -94,17 +93,17 @@ func (m *UnifiedMockWorkRepository) Count(ctx context.Context) (int64, error) { return int64(len(m.Works)), nil } -func (m *UnifiedMockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*models.Work, error) { +func (m *UnifiedMockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) { for _, w := range m.Works { if w.ID == id { return w, nil } } - return nil, repositories.ErrEntityNotFound + return nil, domain.ErrEntityNotFound } -func (m *UnifiedMockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]models.Work, error) { - var result []models.Work +func (m *UnifiedMockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) { + var result []domain.Work end := offset + batchSize if end > len(m.Works) { end = len(m.Works) @@ -118,15 +117,15 @@ func (m *UnifiedMockWorkRepository) GetAllForSync(ctx context.Context, batchSize } // New BaseRepository methods -func (m *UnifiedMockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *models.Work) error { +func (m *UnifiedMockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error { return m.Create(ctx, entity) } -func (m *UnifiedMockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *repositories.QueryOptions) (*models.Work, error) { +func (m *UnifiedMockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) { return m.GetByID(ctx, id) } -func (m *UnifiedMockWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *models.Work) error { +func (m *UnifiedMockWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error { return m.Update(ctx, entity) } @@ -134,7 +133,7 @@ func (m *UnifiedMockWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, return m.Delete(ctx, id) } -func (m *UnifiedMockWorkRepository) ListWithOptions(ctx context.Context, options *repositories.QueryOptions) ([]models.Work, error) { +func (m *UnifiedMockWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Work, error) { result, err := m.List(ctx, 1, 1000) if err != nil { return nil, err @@ -142,7 +141,7 @@ func (m *UnifiedMockWorkRepository) ListWithOptions(ctx context.Context, options return result.Items, nil } -func (m *UnifiedMockWorkRepository) CountWithOptions(ctx context.Context, options *repositories.QueryOptions) (int64, error) { +func (m *UnifiedMockWorkRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return m.Count(ctx) } @@ -160,8 +159,8 @@ func (m *UnifiedMockWorkRepository) WithTx(ctx context.Context, fn func(tx *gorm } // WorkRepository specific methods -func (m *UnifiedMockWorkRepository) FindByTitle(ctx context.Context, title string) ([]models.Work, error) { - var result []models.Work +func (m *UnifiedMockWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) { + var result []domain.Work for _, w := range m.Works { if len(title) == 0 || (len(w.Title) >= len(title) && w.Title[:len(title)] == title) { result = append(result, *w) @@ -170,8 +169,8 @@ func (m *UnifiedMockWorkRepository) FindByTitle(ctx context.Context, title strin return result, nil } -func (m *UnifiedMockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*repositories.PaginatedResult[models.Work], error) { - var filtered []models.Work +func (m *UnifiedMockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { + var filtered []domain.Work for _, w := range m.Works { if w.Language == language { filtered = append(filtered, *w) @@ -181,16 +180,16 @@ func (m *UnifiedMockWorkRepository) FindByLanguage(ctx context.Context, language start := (page - 1) * pageSize end := start + pageSize if start > len(filtered) { - return &repositories.PaginatedResult[models.Work]{Items: []models.Work{}, TotalCount: total}, nil + return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}, TotalCount: total}, nil } if end > len(filtered) { end = len(filtered) } - return &repositories.PaginatedResult[models.Work]{Items: filtered[start:end], TotalCount: total}, nil + return &domain.PaginatedResult[domain.Work]{Items: filtered[start:end], TotalCount: total}, nil } -func (m *UnifiedMockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]models.Work, error) { - result := make([]models.Work, len(m.Works)) +func (m *UnifiedMockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) { + result := make([]domain.Work, len(m.Works)) for i, w := range m.Works { if w != nil { result[i] = *w @@ -199,8 +198,8 @@ func (m *UnifiedMockWorkRepository) FindByAuthor(ctx context.Context, authorID u return result, nil } -func (m *UnifiedMockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]models.Work, error) { - result := make([]models.Work, len(m.Works)) +func (m *UnifiedMockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) { + result := make([]domain.Work, len(m.Works)) for i, w := range m.Works { if w != nil { result[i] = *w @@ -209,17 +208,17 @@ func (m *UnifiedMockWorkRepository) FindByCategory(ctx context.Context, category return result, nil } -func (m *UnifiedMockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*models.Work, error) { +func (m *UnifiedMockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) { for _, w := range m.Works { if w.ID == id { return w, nil } } - return nil, repositories.ErrEntityNotFound + return nil, domain.ErrEntityNotFound } -func (m *UnifiedMockWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*repositories.PaginatedResult[models.Work], error) { - var all []models.Work +func (m *UnifiedMockWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { + var all []domain.Work for _, w := range m.Works { if w != nil { all = append(all, *w) @@ -229,16 +228,16 @@ func (m *UnifiedMockWorkRepository) ListWithTranslations(ctx context.Context, pa start := (page - 1) * pageSize end := start + pageSize if start > len(all) { - return &repositories.PaginatedResult[models.Work]{Items: []models.Work{}, TotalCount: total}, nil + return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}, TotalCount: total}, nil } if end > len(all) { end = len(all) } - return &repositories.PaginatedResult[models.Work]{Items: all[start:end], TotalCount: total}, nil + return &domain.PaginatedResult[domain.Work]{Items: all[start:end], TotalCount: total}, nil } func (m *UnifiedMockWorkRepository) Reset() { - m.Works = []*models.Work{} + m.Works = []*domain.Work{} } // Add helper to get GraphQL-style Work with Name mapped from Title diff --git a/internal/testutil/simple_test_utils.go b/internal/testutil/simple_test_utils.go index d2265bd..8440e39 100644 --- a/internal/testutil/simple_test_utils.go +++ b/internal/testutil/simple_test_utils.go @@ -1,9 +1,10 @@ package testutil import ( - "tercul/graph" - "tercul/internal/models" - "tercul/services" + "context" + graph "tercul/internal/adapters/graphql" + "tercul/internal/app/work" + "tercul/internal/domain" "github.com/stretchr/testify/suite" ) @@ -11,14 +12,26 @@ import ( // SimpleTestSuite provides a minimal test environment with just the essentials type SimpleTestSuite struct { suite.Suite - WorkRepo *UnifiedMockWorkRepository - WorkService services.WorkService + WorkRepo *UnifiedMockWorkRepository + WorkCommands *work.WorkCommands + WorkQueries *work.WorkQueries + MockAnalyzer *MockAnalyzer +} + +// MockAnalyzer is a mock implementation of the analyzer interface. +type MockAnalyzer struct{} + +// AnalyzeWork is the mock implementation of the AnalyzeWork method. +func (m *MockAnalyzer) AnalyzeWork(ctx context.Context, workID uint) error { + return nil } // SetupSuite sets up the test suite func (s *SimpleTestSuite) SetupSuite() { s.WorkRepo = NewUnifiedMockWorkRepository() - s.WorkService = services.NewWorkService(s.WorkRepo, nil) + s.MockAnalyzer = &MockAnalyzer{} + s.WorkCommands = work.NewWorkCommands(s.WorkRepo, s.MockAnalyzer) + s.WorkQueries = work.NewWorkQueries(s.WorkRepo) } // SetupTest resets test data for each test @@ -28,19 +41,20 @@ func (s *SimpleTestSuite) SetupTest() { // GetResolver returns a minimal GraphQL resolver for testing func (s *SimpleTestSuite) GetResolver() *graph.Resolver { + // This needs to be updated to reflect the new resolver structure + // For now, we'll return a resolver with the work commands and queries return &graph.Resolver{ - WorkRepo: s.WorkRepo, - WorkService: s.WorkService, - // Other fields will be nil, but that's okay for basic tests + // WorkRepo: s.WorkRepo, // This should be removed from resolver + // WorkService: s.WorkService, // This is replaced by commands/queries } } // CreateTestWork creates a test work with optional content -func (s *SimpleTestSuite) CreateTestWork(title, language string, content string) *models.Work { - work := &models.Work{ - Title: title, +func (s *SimpleTestSuite) CreateTestWork(title, language string, content string) *domain.Work { + work := &domain.Work{ + Title: title, + Language: language, } - work.Language = language // Add work to the mock repository s.WorkRepo.AddWork(work)