Merge pull request #2 from SamyRai/fix-build-issues

Fix build issues
This commit is contained in:
Damir Mukimov 2025-09-05 23:39:17 +02:00 committed by GitHub
commit 75a291c3a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
93 changed files with 1286 additions and 1494 deletions

View File

@ -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,32 @@ 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{
App: appBuilder.GetApplication(),
}
jwtManager := auth.NewJWTManager()
srv := 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() {

View File

@ -2,15 +2,15 @@ package main
import (
"net/http"
"tercul/internal/adapters/graphql"
"tercul/internal/platform/auth"
"github.com/99designs/gqlgen/graphql/handler"
)
// NewServer creates a new GraphQL server with the given resolver
func NewServer(resolver *Resolver) http.Handler {
srv := handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolver}))
func NewServer(resolver *graphql.Resolver) http.Handler {
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(graphql.Config{Resolvers: resolver}))
// Create a mux to handle GraphQL endpoint only (no playground here; served separately in production)
mux := http.NewServeMux()
@ -20,8 +20,8 @@ func NewServer(resolver *Resolver) http.Handler {
}
// NewServerWithAuth creates a new GraphQL server with authentication middleware
func NewServerWithAuth(resolver *Resolver, jwtManager *auth.JWTManager) http.Handler {
srv := handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolver}))
func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager) http.Handler {
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(graphql.Config{Resolvers: resolver}))
// Apply authentication middleware to GraphQL endpoint
authHandler := auth.GraphQLAuthMiddleware(jwtManager)(srv)

View File

@ -2,74 +2,48 @@ 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"
log "tercul/internal/platform/log"
)
func main() {
log.Println("Starting enrichment service...")
log.LogInfo("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.Items {
err := linguistics.EnqueueAnalysisForWork(appBuilder.GetAsynq(), work.ID)
if err != nil {
log.LogError("Failed to enqueue analysis for work",
log.F("workID", work.ID),
log.F("error", err))
}
}
log.LogInfo("Enrichment tool finished.")
}

143
create_repo_interfaces.go Normal file
View File

@ -0,0 +1,143 @@
//go:build tools
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")
}

51
fix_domain_repos.go Normal file
View File

@ -0,0 +1,51 @@
//go:build tools
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)
}
}
}
}
}

42
fix_sql_imports.go Normal file
View File

@ -0,0 +1,42 @@
//go:build tools
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)
}
}
}
}

View File

@ -4,10 +4,10 @@ import "context"
// resolveWorkContent uses Localization service to fetch preferred content
func (r *queryResolver) resolveWorkContent(ctx context.Context, workID uint, preferredLanguage string) *string {
if r.Localization == nil {
if r.App.Localization == nil {
return nil
}
content, err := r.Localization.GetWorkContent(ctx, workID, preferredLanguage)
content, err := r.App.Localization.GetWorkContent(ctx, workID, preferredLanguage)
if err != nil || content == "" {
return nil
}

View File

@ -9,7 +9,7 @@ import (
"net/http/httptest"
"testing"
"tercul/internal/adapters/graphql"
graph "tercul/internal/adapters/graphql"
"tercul/internal/testutil"
"github.com/99designs/gqlgen/graphql/handler"
@ -140,9 +140,9 @@ func (s *GraphQLIntegrationSuite) TestQueryWork() {
// TestQueryWorks tests the works query
func (s *GraphQLIntegrationSuite) TestQueryWorks() {
// Create test works
work1 := s.CreateTestWork("Test Work 1", "en", "Test content for work 1")
work2 := s.CreateTestWork("Test Work 2", "en", "Test content for work 2")
work3 := s.CreateTestWork("Test Work 3", "fr", "Test content for work 3")
s.CreateTestWork("Test Work 1", "en", "Test content for work 1")
s.CreateTestWork("Test Work 2", "en", "Test content for work 2")
s.CreateTestWork("Test Work 3", "fr", "Test content for work 3")
// Define the query
query := `

View File

@ -7,6 +7,7 @@ package graphql
import (
"context"
"fmt"
"log"
"strconv"
"tercul/internal/adapters/graphql/model"
"tercul/internal/app/auth"
@ -82,9 +83,9 @@ func (r *mutationResolver) Login(ctx context.Context, email string, password str
func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput) (*model.Work, error) {
// Create domain model
work := &domain.Work{
Title: input.Name,
Description: *input.Description,
Language: input.Language,
Title: input.Name,
TranslatableModel: domain.TranslatableModel{Language: input.Language},
// Description: *input.Description,
// Other fields can be set here
}
@ -377,7 +378,7 @@ func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, e
// Authors is the resolver for the authors field.
func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32, search *string, countryID *string) ([]*model.Author, error) {
var authors []models2.Author
var authors []domain.Author
var err error
if countryID != nil {
@ -385,9 +386,9 @@ func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32
if err != nil {
return nil, err
}
authors, err = r.AuthorRepo.ListByCountryID(ctx, uint(countryIDUint))
authors, err = r.App.AuthorRepo.ListByCountryID(ctx, uint(countryIDUint))
} else {
result, err := r.AuthorRepo.List(ctx, 1, 1000) // Use pagination
result, err := r.App.AuthorRepo.List(ctx, 1, 1000) // Use pagination
if err != nil {
return nil, err
}
@ -402,8 +403,8 @@ func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32
var result []*model.Author
for _, a := range authors {
var bio *string
if r.Localization != nil {
if b, err := r.Localization.GetAuthorBiography(ctx, a.ID, a.Language); err == nil && b != "" {
if r.App.Localization != nil {
if b, err := r.App.Localization.GetAuthorBiography(ctx, a.ID, a.Language); err == nil && b != "" {
bio = &b
}
}
@ -435,29 +436,29 @@ func (r *queryResolver) UserByUsername(ctx context.Context, username string) (*m
// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context, limit *int32, offset *int32, role *model.UserRole) ([]*model.User, error) {
var users []models2.User
var users []domain.User
var err error
if role != nil {
// Convert GraphQL role to model role
var modelRole models2.UserRole
var modelRole domain.UserRole
switch *role {
case model.UserRoleReader:
modelRole = models2.UserRoleReader
modelRole = domain.UserRoleReader
case model.UserRoleContributor:
modelRole = models2.UserRoleContributor
modelRole = domain.UserRoleContributor
case model.UserRoleReviewer:
modelRole = models2.UserRoleReviewer
modelRole = domain.UserRoleReviewer
case model.UserRoleEditor:
modelRole = models2.UserRoleEditor
modelRole = domain.UserRoleEditor
case model.UserRoleAdmin:
modelRole = models2.UserRoleAdmin
modelRole = domain.UserRoleAdmin
default:
return nil, fmt.Errorf("invalid user role: %s", *role)
}
users, err = r.UserRepo.ListByRole(ctx, modelRole)
users, err = r.App.UserRepo.ListByRole(ctx, modelRole)
} else {
result, err := r.UserRepo.List(ctx, 1, 1000) // Use pagination
result, err := r.App.UserRepo.List(ctx, 1, 1000) // Use pagination
if err != nil {
return nil, err
}
@ -474,15 +475,15 @@ func (r *queryResolver) Users(ctx context.Context, limit *int32, offset *int32,
// Convert model role to GraphQL role
var graphqlRole model.UserRole
switch u.Role {
case models2.UserRoleReader:
case domain.UserRoleReader:
graphqlRole = model.UserRoleReader
case models2.UserRoleContributor:
case domain.UserRoleContributor:
graphqlRole = model.UserRoleContributor
case models2.UserRoleReviewer:
case domain.UserRoleReviewer:
graphqlRole = model.UserRoleReviewer
case models2.UserRoleEditor:
case domain.UserRoleEditor:
graphqlRole = model.UserRoleEditor
case models2.UserRoleAdmin:
case domain.UserRoleAdmin:
graphqlRole = model.UserRoleAdmin
default:
graphqlRole = model.UserRoleReader
@ -526,7 +527,7 @@ func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error)
return nil, err
}
tag, err := r.TagRepo.GetByID(ctx, uint(tagID))
tag, err := r.App.TagRepo.GetByID(ctx, uint(tagID))
if err != nil {
return nil, err
}
@ -539,7 +540,7 @@ func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error)
// Tags is the resolver for the tags field.
func (r *queryResolver) Tags(ctx context.Context, limit *int32, offset *int32) ([]*model.Tag, error) {
paginatedResult, err := r.TagRepo.List(ctx, 1, 1000) // Use pagination
paginatedResult, err := r.App.TagRepo.List(ctx, 1, 1000) // Use pagination
if err != nil {
return nil, err
}
@ -563,7 +564,7 @@ func (r *queryResolver) Category(ctx context.Context, id string) (*model.Categor
return nil, err
}
category, err := r.CategoryRepo.GetByID(ctx, uint(categoryID))
category, err := r.App.CategoryRepo.GetByID(ctx, uint(categoryID))
if err != nil {
return nil, err
}
@ -576,7 +577,7 @@ func (r *queryResolver) Category(ctx context.Context, id string) (*model.Categor
// Categories is the resolver for the categories field.
func (r *queryResolver) Categories(ctx context.Context, limit *int32, offset *int32) ([]*model.Category, error) {
paginatedResult, err := r.CategoryRepo.List(ctx, 1, 1000)
paginatedResult, err := r.App.CategoryRepo.List(ctx, 1, 1000)
if err != nil {
return nil, err
}

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/app/localization"
"tercul/internal/app/search"
"tercul/internal/app/work"
"tercul/internal/domain"
)
// Application is a container for all the application-layer services.
@ -19,4 +20,10 @@ type Application struct {
Search search.IndexService
WorkCommands *work.WorkCommands
WorkQueries *work.WorkQueries
// Repositories - to be refactored into app services
AuthorRepo domain.AuthorRepository
UserRepo domain.UserRepository
TagRepo domain.TagRepository
CategoryRepo domain.CategoryRepository
}

View File

@ -7,14 +7,12 @@ import (
"tercul/internal/app/search"
"tercul/internal/app/work"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/cache"
"tercul/internal/platform/config"
"tercul/internal/platform/db"
"tercul/internal/platform/log"
auth_platform "tercul/internal/platform/auth"
"tercul/linguistics"
"time"
"tercul/internal/jobs/linguistics"
"github.com/hibiken/asynq"
"github.com/weaviate/weaviate-go-client/v5/weaviate"
@ -110,6 +108,9 @@ func (b *ApplicationBuilder) BuildApplication() error {
// I need to add all the other repos here. For now, I'll just add the ones I need for the services.
translationRepo := sql.NewTranslationRepository(b.dbConn)
copyrightRepo := sql.NewCopyrightRepository(b.dbConn)
authorRepo := sql.NewAuthorRepository(b.dbConn)
tagRepo := sql.NewTagRepository(b.dbConn)
categoryRepo := sql.NewCategoryRepository(b.dbConn)
// Initialize application services
@ -136,6 +137,10 @@ func (b *ApplicationBuilder) BuildApplication() error {
CopyrightQueries: copyrightQueries,
Localization: localizationService,
Search: searchService,
AuthorRepo: authorRepo,
UserRepo: userRepo,
TagRepo: tagRepo,
CategoryRepo: categoryRepo,
}
log.LogInfo("Application layer initialized successfully")
@ -159,6 +164,21 @@ func (b *ApplicationBuilder) GetApplication() *Application {
return b.App
}
// GetDB returns the database connection
func (b *ApplicationBuilder) GetDB() *gorm.DB {
return b.dbConn
}
// GetAsynq returns the Asynq client
func (b *ApplicationBuilder) GetAsynq() *asynq.Client {
return b.asynqClient
}
// GetLinguisticsFactory returns the linguistics factory
func (b *ApplicationBuilder) GetLinguisticsFactory() *linguistics.LinguisticsFactory {
return b.linguistics
}
// Close closes all resources
func (b *ApplicationBuilder) Close() error {
if b.asynqClient != nil {

View File

@ -1,15 +1,11 @@
package app
import (
"net/http"
"tercul/internal/platform/auth"
"tercul/internal/jobs/linguistics"
syncjob "tercul/internal/jobs/sync"
"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 +21,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) {
@ -84,8 +42,8 @@ func (f *ServerFactory) CreateBackgroundJobServers() ([]*asynq.Server, error) {
// Create sync job instance
syncJobInstance := syncjob.NewSyncJob(
f.appBuilder.GetDatabase(),
f.appBuilder.GetAsynqClient(),
f.appBuilder.GetDB(),
f.appBuilder.GetAsynq(),
)
// Register sync job handlers
@ -98,9 +56,9 @@ func (f *ServerFactory) CreateBackgroundJobServers() ([]*asynq.Server, error) {
// Create linguistic sync job
linguisticSyncJob := linguistics.NewLinguisticSyncJob(
f.appBuilder.GetDatabase(),
f.appBuilder.GetLinguistics().GetAnalyzer(),
f.appBuilder.GetAsynqClient(),
f.appBuilder.GetDB(),
f.appBuilder.GetLinguisticsFactory().GetAnalyzer(),
f.appBuilder.GetAsynq(),
)
// Create linguistic server and register handlers
@ -120,19 +78,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
}

View File

@ -2,8 +2,10 @@ package sql
import (
"context"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/author"
"gorm.io/gorm"
)
type authorRepository struct {
@ -12,7 +14,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,

View File

@ -3,8 +3,10 @@ package sql
import (
"context"
"errors"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/book"
"gorm.io/gorm"
)
type bookRepository struct {
@ -13,7 +15,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,

View File

@ -2,8 +2,10 @@ package sql
import (
"context"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/bookmark"
"gorm.io/gorm"
)
type bookmarkRepository struct {
@ -12,7 +14,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,

View File

@ -3,8 +3,10 @@ package sql
import (
"context"
"errors"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/category"
"gorm.io/gorm"
)
type categoryRepository struct {
@ -13,7 +15,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,

View File

@ -1,33 +1,29 @@
package repositories
package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/city"
"gorm.io/gorm"
"tercul/internal/models"
)
// 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
}

View File

@ -2,8 +2,10 @@ package sql
import (
"context"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/collection"
"gorm.io/gorm"
)
type collectionRepository struct {
@ -12,7 +14,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,

View File

@ -2,8 +2,10 @@ package sql
import (
"context"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/comment"
"gorm.io/gorm"
)
type commentRepository struct {
@ -12,7 +14,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,

View File

@ -1,37 +1,29 @@
package repositories
package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/contribution"
"gorm.io/gorm"
"tercul/internal/models"
)
// 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 +31,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 +40,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 +49,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 +58,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
}

View File

@ -1,34 +1,29 @@
package repositories
package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/copyright_claim"
"gorm.io/gorm"
"tercul/internal/models"
)
// 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) copyright_claim.Copyright_claimRepository {
return &copyrightClaimRepository{
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 +31,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
}

View File

@ -1,42 +1,30 @@
package repositories
package sql
import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/domain/copyright"
"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]
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 &copyrightRepository{
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 +35,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 +49,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(&copyrightables).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) {

View File

@ -1,35 +1,30 @@
package repositories
package sql
import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/domain/country"
"gorm.io/gorm"
"tercul/internal/models"
)
// 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 +35,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
}

View File

@ -1,33 +1,29 @@
package repositories
package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/edge"
"gorm.io/gorm"
"tercul/internal/models"
)
// 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
}

View File

@ -1,35 +1,30 @@
package repositories
package sql
import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/domain/edition"
"gorm.io/gorm"
"tercul/internal/models"
)
// 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 +32,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

View File

@ -1,38 +1,31 @@
package repositories
package sql
import (
"context"
"errors"
"gorm.io/gorm"
"tercul/internal/models"
"tercul/internal/domain"
"tercul/internal/domain/email_verification"
"time"
"gorm.io/gorm"
)
// 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) email_verification.Email_verificationRepository {
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 +36,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 +46,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 +54,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

View File

@ -2,8 +2,10 @@ package sql
import (
"context"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/like"
"gorm.io/gorm"
)
type likeRepository struct {
@ -12,7 +14,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,

View File

@ -1,35 +1,29 @@
package repositories
package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/monetization"
"gorm.io/gorm"
"tercul/internal/models"
)
// 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 +31,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 +40,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
}

View File

@ -1,38 +1,31 @@
package repositories
package sql
import (
"context"
"errors"
"gorm.io/gorm"
"tercul/internal/models"
"tercul/internal/domain"
"tercul/internal/domain/password_reset"
"time"
"gorm.io/gorm"
)
// 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) password_reset.Password_resetRepository {
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 +36,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 +46,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 +54,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

View File

@ -1,36 +1,30 @@
package repositories
package sql
import (
"context"
"gorm.io/gorm"
"math"
"tercul/internal/models"
"tercul/internal/domain"
"tercul/internal/domain/place"
"gorm.io/gorm"
)
// 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 +32,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 +41,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

View File

@ -1,33 +1,29 @@
package repositories
package sql
import (
"context"
"tercul/internal/domain"
"tercul/internal/domain/publisher"
"gorm.io/gorm"
"tercul/internal/models"
)
// 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
}

View File

@ -1,35 +1,30 @@
package repositories
package sql
import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/domain/source"
"gorm.io/gorm"
"tercul/internal/models"
)
// 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 +34,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

View File

@ -3,8 +3,10 @@ package sql
import (
"context"
"errors"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/tag"
"gorm.io/gorm"
)
type tagRepository struct {
@ -13,7 +15,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,

View File

@ -2,8 +2,10 @@ package sql
import (
"context"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/translation"
"gorm.io/gorm"
)
type translationRepository struct {
@ -12,7 +14,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,

View File

@ -1,34 +1,30 @@
package repositories
package sql
import (
"context"
"errors"
"tercul/internal/domain"
"tercul/internal/domain/user_profile"
"gorm.io/gorm"
"tercul/internal/models"
)
// 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) user_profile.User_profileRepository {
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

View File

@ -3,8 +3,10 @@ package sql
import (
"context"
"errors"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/user"
"gorm.io/gorm"
)
type userRepository struct {
@ -13,7 +15,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,

View File

@ -1,37 +1,31 @@
package repositories
package sql
import (
"context"
"errors"
"gorm.io/gorm"
"tercul/internal/models"
"tercul/internal/domain"
"tercul/internal/domain/user_session"
"time"
"gorm.io/gorm"
)
// 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) user_session.User_sessionRepository {
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 +36,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 +46,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

View File

@ -2,8 +2,10 @@ package sql
import (
"context"
"gorm.io/gorm"
"tercul/internal/domain"
"tercul/internal/domain/work"
"gorm.io/gorm"
)
type workRepository struct {
@ -12,7 +14,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,

View File

@ -0,0 +1,15 @@
package author
import (
"context"
"tercul/internal/domain"
)
// AuthorRepository defines CRUD methods specific to Author.
type AuthorRepository interface {
domain.BaseRepository[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)
}

View File

@ -0,0 +1,16 @@
package book
import (
"context"
"tercul/internal/domain"
)
// BookRepository defines CRUD methods specific to Book.
type BookRepository interface {
domain.BaseRepository[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)
}

View File

@ -0,0 +1,14 @@
package bookmark
import (
"context"
"tercul/internal/domain"
)
// BookmarkRepository defines CRUD methods specific to Bookmark.
type BookmarkRepository interface {
domain.BaseRepository[domain.Bookmark]
ListByUserID(ctx context.Context, userID uint) ([]domain.Bookmark, error)
ListByWorkID(ctx context.Context, workID uint) ([]domain.Bookmark, error)
}

View File

@ -0,0 +1,15 @@
package category
import (
"context"
"tercul/internal/domain"
)
// CategoryRepository defines CRUD methods specific to Category.
type CategoryRepository interface {
domain.BaseRepository[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)
}

View File

@ -0,0 +1,13 @@
package city
import (
"context"
"tercul/internal/domain"
)
// CityRepository defines CRUD methods specific to City.
type CityRepository interface {
domain.BaseRepository[domain.City]
ListByCountryID(ctx context.Context, countryID uint) ([]domain.City, error)
}

View File

@ -0,0 +1,15 @@
package collection
import (
"context"
"tercul/internal/domain"
)
// CollectionRepository defines CRUD methods specific to Collection.
type CollectionRepository interface {
domain.BaseRepository[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)
}

View File

@ -0,0 +1,16 @@
package comment
import (
"context"
"tercul/internal/domain"
)
// CommentRepository defines CRUD methods specific to Comment.
type CommentRepository interface {
domain.BaseRepository[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)
}

View File

@ -0,0 +1,17 @@
package contribution
import (
"context"
"tercul/internal/domain"
)
// ContributionRepository defines CRUD methods specific to Contribution.
type ContributionRepository interface {
domain.BaseRepository[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)
}

View File

@ -0,0 +1,19 @@
package copyright
import (
"context"
"tercul/internal/domain"
)
// CopyrightRepository defines CRUD methods specific to Copyright.
type CopyrightRepository interface {
domain.BaseRepository[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)
}

View File

@ -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.BaseRepository[domain.CopyrightClaim]
ListByWorkID(ctx context.Context, workID uint) ([]domain.CopyrightClaim, error)
ListByUserID(ctx context.Context, userID uint) ([]domain.CopyrightClaim, error)
}

View File

@ -0,0 +1,14 @@
package country
import (
"context"
"tercul/internal/domain"
)
// CountryRepository defines CRUD methods specific to Country.
type CountryRepository interface {
domain.BaseRepository[domain.Country]
GetByCode(ctx context.Context, code string) (*domain.Country, error)
ListByContinent(ctx context.Context, continent string) ([]domain.Country, error)
}

View File

@ -0,0 +1,13 @@
package edge
import (
"context"
"tercul/internal/domain"
)
// EdgeRepository defines CRUD methods specific to Edge.
type EdgeRepository interface {
domain.BaseRepository[domain.Edge]
ListBySource(ctx context.Context, sourceTable string, sourceID uint) ([]domain.Edge, error)
}

View File

@ -0,0 +1,14 @@
package edition
import (
"context"
"tercul/internal/domain"
)
// EditionRepository defines CRUD methods specific to Edition.
type EditionRepository interface {
domain.BaseRepository[domain.Edition]
ListByBookID(ctx context.Context, bookID uint) ([]domain.Edition, error)
FindByISBN(ctx context.Context, isbn string) (*domain.Edition, error)
}

View File

@ -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.BaseRepository[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)
}

View File

@ -0,0 +1,16 @@
package like
import (
"context"
"tercul/internal/domain"
)
// LikeRepository defines CRUD methods specific to Like.
type LikeRepository interface {
domain.BaseRepository[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)
}

View File

@ -0,0 +1,15 @@
package monetization
import (
"context"
"tercul/internal/domain"
)
// MonetizationRepository defines CRUD methods specific to Monetization.
type MonetizationRepository interface {
domain.BaseRepository[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)
}

View File

@ -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.BaseRepository[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)
}

View File

@ -0,0 +1,15 @@
package place
import (
"context"
"tercul/internal/domain"
)
// PlaceRepository defines CRUD methods specific to Place.
type PlaceRepository interface {
domain.BaseRepository[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, longitude float64, radiusKm float64) ([]domain.Place, error)
}

View File

@ -0,0 +1,13 @@
package publisher
import (
"context"
"tercul/internal/domain"
)
// PublisherRepository defines CRUD methods specific to Publisher.
type PublisherRepository interface {
domain.BaseRepository[domain.Publisher]
ListByCountryID(ctx context.Context, countryID uint) ([]domain.Publisher, error)
}

View File

@ -0,0 +1,14 @@
package source
import (
"context"
"tercul/internal/domain"
)
// SourceRepository defines CRUD methods specific to Source.
type SourceRepository interface {
domain.BaseRepository[domain.Source]
ListByWorkID(ctx context.Context, workID uint) ([]domain.Source, error)
FindByURL(ctx context.Context, url string) (*domain.Source, error)
}

View File

@ -0,0 +1,14 @@
package tag
import (
"context"
"tercul/internal/domain"
)
// TagRepository defines CRUD methods specific to Tag.
type TagRepository interface {
domain.BaseRepository[domain.Tag]
FindByName(ctx context.Context, name string) (*domain.Tag, error)
ListByWorkID(ctx context.Context, workID uint) ([]domain.Tag, error)
}

View File

@ -0,0 +1,16 @@
package translation
import (
"context"
"tercul/internal/domain"
)
// TranslationRepository defines CRUD methods specific to Translation.
type TranslationRepository interface {
domain.BaseRepository[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)
}

View File

@ -0,0 +1,15 @@
package user
import (
"context"
"tercul/internal/domain"
)
// UserRepository defines CRUD methods specific to User.
type UserRepository interface {
domain.BaseRepository[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)
}

View File

@ -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.BaseRepository[domain.UserProfile]
GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error)
}

View File

@ -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.BaseRepository[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)
}

View File

@ -0,0 +1,18 @@
package work
import (
"context"
"tercul/internal/domain"
)
// WorkRepository defines CRUD methods specific to Work.
type WorkRepository interface {
domain.BaseRepository[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, pageSize int) (*domain.PaginatedResult[domain.Work], error)
GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error)
ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error)
}

View File

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

View File

@ -7,7 +7,7 @@ import (
func TestLinguaLanguageDetector_DetectLanguage(t *testing.T) {
d := NewLinguaLanguageDetector()
code, ok := d.DetectLanguage("This is an English sentence.")
require.True(t, ok)
code, err := d.DetectLanguage("This is an English sentence.")
require.NoError(t, err)
require.NotEmpty(t, code)
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package enrich
package linguistics
import "testing"

View File

@ -4,22 +4,22 @@ 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
// For demonstration purposes, we'll use a simple heuristic based on common words
content := strings.ToLower(text.Body)
content := strings.ToLower(text)
// Check for English
englishWords := []string{"the", "and", "is", "in", "to", "of", "that", "for"}
@ -35,15 +35,15 @@ func (d *LanguageDetector) Detect(text Text) (string, float64, error) {
// Determine the most likely language
if englishCount > spanishCount && englishCount > frenchCount {
return "en", 0.7, nil
return "en", nil
} else if spanishCount > englishCount && spanishCount > frenchCount {
return "es", 0.7, nil
return "es", nil
} else if frenchCount > englishCount && frenchCount > spanishCount {
return "fr", 0.7, nil
return "fr", nil
}
// Default to English if we can't determine the language
return "en", 0.5, nil
return "en", nil
}
// countWords counts the occurrences of words in a text

View File

@ -4,21 +4,21 @@ import "testing"
func TestLanguageDetector_Detect_EN(t *testing.T) {
d := NewLanguageDetector()
lang, conf, err := d.Detect(Text{Body: " the and is in to of that for the "})
lang, err := d.DetectLanguage(" the and is in to of that for the ")
if err != nil {
t.Fatalf("Detect returned error: %v", err)
t.Fatalf("DetectLanguage returned error: %v", err)
}
if lang != "en" {
t.Fatalf("expected language 'en', got %q", lang)
}
if conf <= 0 {
t.Errorf("expected positive confidence, got %f", conf)
}
}
func TestLanguageDetector_Detect_ES(t *testing.T) {
d := NewLanguageDetector()
lang, _, _ := d.Detect(Text{Body: " el la es en de que por para el "})
lang, err := d.DetectLanguage(" el la es en de que por para el ")
if err != nil {
t.Fatalf("DetectLanguage returned error: %v", err)
}
if lang != "es" {
t.Fatalf("expected language 'es', got %q", lang)
}
@ -26,7 +26,10 @@ func TestLanguageDetector_Detect_ES(t *testing.T) {
func TestLanguageDetector_Detect_FR(t *testing.T) {
d := NewLanguageDetector()
lang, _, _ := d.Detect(Text{Body: " le la est en de que pour dans le "})
lang, err := d.DetectLanguage(" le la est en de que pour dans le ")
if err != nil {
t.Fatalf("DetectLanguage returned error: %v", err)
}
if lang != "fr" {
t.Fatalf("expected language 'fr', got %q", lang)
}
@ -35,14 +38,11 @@ func TestLanguageDetector_Detect_FR(t *testing.T) {
func TestLanguageDetector_Detect_DefaultEnglish(t *testing.T) {
d := NewLanguageDetector()
// Balanced/unknown should default to English per implementation
lang, conf, err := d.Detect(Text{Body: " lorem ipsum dolor sit amet "})
lang, err := d.DetectLanguage(" lorem ipsum dolor sit amet ")
if err != nil {
t.Fatalf("Detect returned error: %v", err)
t.Fatalf("DetectLanguage returned error: %v", err)
}
if lang != "en" {
t.Fatalf("expected default language 'en', got %q", lang)
}
if conf != 0.5 {
t.Errorf("expected default confidence 0.5, got %f", conf)
}
}

View File

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

View File

@ -2,7 +2,7 @@ package linguistics
// Registry holds all the text analysis services
type Registry struct {
Lang *LanguageDetector
Lang LanguageDetector
Tok *Tokenizer
Pos *POSTagger
Lem *Lemmatizer
@ -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

View File

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

View File

@ -52,7 +52,7 @@ func (a *BasicTextAnalyzer) AnalyzeText(ctx context.Context, text string, langua
// Auto-detect language if not provided and a detector exists
if language == "" && a.langDetector != nil {
if detected, ok := a.langDetector.DetectLanguage(text); ok {
if detected, err := a.langDetector.DetectLanguage(text); err == nil {
language = detected
}
}
@ -114,7 +114,7 @@ func (a *BasicTextAnalyzer) AnalyzeTextConcurrently(ctx context.Context, text st
// Auto-detect language if not provided and a detector exists
if language == "" && a.langDetector != nil {
if detected, ok := a.langDetector.DetectLanguage(text); ok {
if detected, err := a.langDetector.DetectLanguage(text); err == nil {
language = detected
}
}

View File

@ -11,8 +11,8 @@ import (
// Mocks for provider interfaces
type mockLangDetector struct{ lang string; ok bool }
func (m mockLangDetector) DetectLanguage(text string) (string, bool) { return m.lang, m.ok }
type mockLangDetector struct{ lang string; err error }
func (m mockLangDetector) DetectLanguage(text string) (string, error) { return m.lang, m.err }
type mockSentimentProvider struct{ score float64; err error }
func (m mockSentimentProvider) Score(text string, language string) (float64, error) { return m.score, m.err }
@ -34,7 +34,7 @@ func TestAnalyzeText_Empty(t *testing.T) {
func TestAnalyzeText_ProvidersAndLangDetection(t *testing.T) {
// Arrange
a := NewBasicTextAnalyzer().
WithLanguageDetector(mockLangDetector{lang: "en", ok: true}).
WithLanguageDetector(mockLangDetector{lang: "en", err: nil}).
WithSentimentProvider(mockSentimentProvider{score: 0.75}).
WithKeywordProvider(mockKeywordProvider{kws: []Keyword{{Text: "golang", Relevance: 0.42}}})
@ -84,7 +84,7 @@ func TestAnalyzeTextConcurrently_AggregatesWithProviders(t *testing.T) {
// Providers return consistent values regardless of input
kw := []Keyword{{Text: "constant", Relevance: 0.3}}
a := NewBasicTextAnalyzer().
WithLanguageDetector(mockLangDetector{lang: "en", ok: true}).
WithLanguageDetector(mockLangDetector{lang: "en", err: nil}).
WithSentimentProvider(mockSentimentProvider{score: 0.5}).
WithKeywordProvider(mockKeywordProvider{kws: kw})
@ -119,7 +119,7 @@ func TestAnalyzeTextConcurrently_AggregatesWithProviders(t *testing.T) {
func TestAnalyzeTextConcurrently_ContextCanceled(t *testing.T) {
a := NewBasicTextAnalyzer().
WithLanguageDetector(mockLangDetector{lang: "en", ok: true}).
WithLanguageDetector(mockLangDetector{lang: "en", err: nil}).
WithSentimentProvider(mockSentimentProvider{score: 0.9}).
WithKeywordProvider(mockKeywordProvider{kws: []Keyword{{Text: "x", Relevance: 0.1}}})

View File

@ -16,7 +16,7 @@ var (
"do": {}, "does": {}, "did": {}, "will": {}, "would": {}, "could": {},
"should": {}, "may": {}, "might": {}, "can": {}, "this": {}, "that": {},
"these": {}, "those": {}, "i": {}, "you": {}, "he": {}, "she": {},
"it": {}, "we": {}, "they": {}, "me": {}, "him": {}, "hers": {},
"it": {}, "we": {}, "they": {}, "me": {}, "him": {}, "hers": {}, "over": {},
"us": {}, "them": {}, "my": {}, "your": {}, "his": {}, "its": {},
"our": {}, "their": {},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &copyrightRepository{
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(&copyrightable).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(&copyrights).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(&copyrightables).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
}

View File

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

View File

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

View File

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

View File

@ -2,48 +2,52 @@ package testutil
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/suite"
"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"
auth_platform "tercul/internal/platform/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
AuthCommands *auth.AuthCommands
AuthQueries *auth.AuthQueries
// 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,19 @@ 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)
jwtManager := auth_platform.NewJWTManager()
s.AuthCommands = auth.NewAuthCommands(s.UserRepo, jwtManager)
s.AuthQueries = auth.NewAuthQueries(s.UserRepo, jwtManager)
}
// 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,9 +209,9 @@ func (s *IntegrationTestSuite) setupTestData() {
}
// Create test authors
s.TestAuthors = []*models.Author{
{Name: "Test Author 1", Language: "en"},
{Name: "Test Author 2", Language: "fr"},
s.TestAuthors = []*domain.Author{
{Name: "Test Author 1", TranslatableModel: domain.TranslatableModel{Language: "en"}},
{Name: "Test Author 2", TranslatableModel: domain.TranslatableModel{Language: "fr"}},
}
for _, author := range s.TestAuthors {
@ -214,10 +221,10 @@ func (s *IntegrationTestSuite) setupTestData() {
}
// Create test works
s.TestWorks = []*models.Work{
{Title: "Test Work 1", Language: "en"},
{Title: "Test Work 2", Language: "en"},
{Title: "Test Work 3", Language: "fr"},
s.TestWorks = []*domain.Work{
{Title: "Test Work 1", TranslatableModel: domain.TranslatableModel{Language: "en"}},
{Title: "Test Work 2", TranslatableModel: domain.TranslatableModel{Language: "en"}},
{Title: "Test Work 3", TranslatableModel: domain.TranslatableModel{Language: "fr"}},
}
for _, work := range s.TestWorks {
@ -227,7 +234,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 +299,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,
TranslatableModel: domain.TranslatableModel{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,

View File

@ -2,9 +2,8 @@ package testutil
import (
"context"
"errors"
"fmt"
"tercul/internal/repositories"
"tercul/internal/domain"
"gorm.io/gorm"
)
@ -40,17 +39,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")
}

View File

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

View File

@ -3,52 +3,48 @@ 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
}
m.Works = append(m.Works, 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, 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 ErrEntityNotFound
}
func (m *UnifiedMockWorkRepository) Delete(ctx context.Context, id uint) error {
@ -58,11 +54,11 @@ func (m *UnifiedMockWorkRepository) Delete(ctx context.Context, id uint) error {
return nil
}
}
return repositories.ErrEntityNotFound
return 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 +68,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 +90,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, 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 +114,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 +130,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 +138,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 +156,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 +166,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 +177,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 +195,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 +205,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, 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 +225,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

View File

@ -1,9 +1,11 @@
package testutil
import (
"tercul/graph"
"tercul/internal/models"
"tercul/services"
"context"
graph "tercul/internal/adapters/graphql"
"tercul/internal/app"
"tercul/internal/app/work"
"tercul/internal/domain"
"github.com/stretchr/testify/suite"
)
@ -11,14 +13,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
@ -29,18 +43,30 @@ func (s *SimpleTestSuite) SetupTest() {
// GetResolver returns a minimal GraphQL resolver for testing
func (s *SimpleTestSuite) GetResolver() *graph.Resolver {
return &graph.Resolver{
WorkRepo: s.WorkRepo,
WorkService: s.WorkService,
// Other fields will be nil, but that's okay for basic tests
App: &app.Application{
WorkCommands: s.WorkCommands,
WorkQueries: s.WorkQueries,
Localization: &MockLocalization{},
},
}
}
type MockLocalization struct{}
func (m *MockLocalization) GetWorkContent(ctx context.Context, workID uint, preferredLanguage string) (string, error) {
return "Test content for work", nil
}
func (m *MockLocalization) GetAuthorBiography(ctx context.Context, authorID uint, preferredLanguage string) (string, error) {
return "Test biography", nil
}
// 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,
TranslatableModel: domain.TranslatableModel{Language: language},
}
work.Language = language
// Add work to the mock repository
s.WorkRepo.AddWork(work)

View File

@ -2,6 +2,7 @@ package testutil
import (
"database/sql"
"errors"
"fmt"
"log"
"os"
@ -15,6 +16,8 @@ import (
"tercul/internal/platform/config"
)
var ErrEntityNotFound = errors.New("entity not found")
// TestDB holds the test database connection
var TestDB *gorm.DB