mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
feat(analytics): Enhance analytics capabilities
This commit introduces a comprehensive enhancement of the application's analytics features, addressing performance, data modeling, and feature set. The key changes include: - **Performance Improvement:** The analytics repository now uses a database "UPSERT" operation to increment counters, reducing two separate database calls (read and write) into a single, more efficient operation. - **New Metrics:** The `WorkStats` and `TranslationStats` models have been enriched with new, calculated metrics: - `ReadingTime`: An estimation of the time required to read the work or translation. - `Complexity`: A score representing the linguistic complexity of the text. - `Sentiment`: A score indicating the emotional tone of the text. - **Service Refactoring:** The analytics service has been refactored to support the new metrics. It now includes methods to calculate and update these scores, leveraging the existing linguistics package for text analysis. - **GraphQL API Expansion:** The new analytics fields (`readingTime`, `complexity`, `sentiment`) have been exposed through the GraphQL API by updating the `WorkStats` and `TranslationStats` types in the schema. - **Validation and Testing:** - GraphQL input validation has been centralized and improved by moving from ad-hoc checks to a consistent validation pattern in the GraphQL layer. - The test suite has been significantly improved with the addition of new tests for the analytics service and the data access layer, ensuring the correctness and robustness of the new features. This includes fixing several bugs that were discovered during the development process.
This commit is contained in:
parent
6b4140eca0
commit
caf07df08d
@ -10,7 +10,9 @@ import (
|
|||||||
|
|
||||||
// NewServer creates a new GraphQL server with the given resolver
|
// NewServer creates a new GraphQL server with the given resolver
|
||||||
func NewServer(resolver *graphql.Resolver) http.Handler {
|
func NewServer(resolver *graphql.Resolver) http.Handler {
|
||||||
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(graphql.Config{Resolvers: resolver}))
|
c := graphql.Config{Resolvers: resolver}
|
||||||
|
c.Directives.Binding = graphql.Binding
|
||||||
|
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
|
||||||
|
|
||||||
// Create a mux to handle GraphQL endpoint only (no playground here; served separately in production)
|
// Create a mux to handle GraphQL endpoint only (no playground here; served separately in production)
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
@ -21,7 +23,9 @@ func NewServer(resolver *graphql.Resolver) http.Handler {
|
|||||||
|
|
||||||
// NewServerWithAuth creates a new GraphQL server with authentication middleware
|
// NewServerWithAuth creates a new GraphQL server with authentication middleware
|
||||||
func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager) http.Handler {
|
func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager) http.Handler {
|
||||||
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(graphql.Config{Resolvers: resolver}))
|
c := graphql.Config{Resolvers: resolver}
|
||||||
|
c.Directives.Binding = graphql.Binding
|
||||||
|
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
|
||||||
|
|
||||||
// Apply authentication middleware to GraphQL endpoint
|
// Apply authentication middleware to GraphQL endpoint
|
||||||
authHandler := auth.GraphQLAuthMiddleware(jwtManager)(srv)
|
authHandler := auth.GraphQLAuthMiddleware(jwtManager)(srv)
|
||||||
|
|||||||
5
go.mod
5
go.mod
@ -38,6 +38,7 @@ require (
|
|||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/elastic/go-sysinfo v1.15.4 // indirect
|
github.com/elastic/go-sysinfo v1.15.4 // indirect
|
||||||
github.com/elastic/go-windows v1.0.2 // indirect
|
github.com/elastic/go-windows v1.0.2 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/go-faster/city v1.0.1 // indirect
|
github.com/go-faster/city v1.0.1 // indirect
|
||||||
github.com/go-faster/errors v0.7.1 // indirect
|
github.com/go-faster/errors v0.7.1 // indirect
|
||||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||||
@ -50,6 +51,9 @@ require (
|
|||||||
github.com/go-openapi/strfmt v0.23.0 // indirect
|
github.com/go-openapi/strfmt v0.23.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/go-openapi/validate v0.24.0 // indirect
|
github.com/go-openapi/validate v0.24.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||||
@ -67,6 +71,7 @@ require (
|
|||||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
|||||||
10
go.sum
10
go.sum
@ -91,6 +91,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
|||||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||||
@ -136,6 +138,12 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ
|
|||||||
github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg=
|
github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg=
|
||||||
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||||
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
@ -261,6 +269,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
|||||||
24
internal/adapters/graphql/binding.go
Normal file
24
internal/adapters/graphql/binding.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/99designs/gqlgen/graphql"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
var validate = validator.New()
|
||||||
|
|
||||||
|
func Binding(ctx context.Context, obj interface{}, next graphql.Resolver, constraint string) (interface{}, error) {
|
||||||
|
val, err := next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validate.Var(val, constraint); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid input: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
@ -44,6 +44,7 @@ type ResolverRoot interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DirectiveRoot struct {
|
type DirectiveRoot struct {
|
||||||
|
Binding func(ctx context.Context, obj interface{}, next graphql.Resolver, constraint string) (interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ComplexityRoot struct {
|
type ComplexityRoot struct {
|
||||||
@ -425,8 +426,13 @@ type ComplexityRoot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TranslationStats struct {
|
TranslationStats struct {
|
||||||
|
Comments func(childComplexity int) int
|
||||||
CreatedAt func(childComplexity int) int
|
CreatedAt func(childComplexity int) int
|
||||||
ID func(childComplexity int) int
|
ID func(childComplexity int) int
|
||||||
|
Likes func(childComplexity int) int
|
||||||
|
ReadingTime func(childComplexity int) int
|
||||||
|
Sentiment func(childComplexity int) int
|
||||||
|
Shares func(childComplexity int) int
|
||||||
Translation func(childComplexity int) int
|
Translation func(childComplexity int) int
|
||||||
UpdatedAt func(childComplexity int) int
|
UpdatedAt func(childComplexity int) int
|
||||||
Views func(childComplexity int) int
|
Views func(childComplexity int) int
|
||||||
@ -522,11 +528,19 @@ type ComplexityRoot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WorkStats struct {
|
WorkStats struct {
|
||||||
CreatedAt func(childComplexity int) int
|
Bookmarks func(childComplexity int) int
|
||||||
ID func(childComplexity int) int
|
Comments func(childComplexity int) int
|
||||||
UpdatedAt func(childComplexity int) int
|
Complexity func(childComplexity int) int
|
||||||
Views func(childComplexity int) int
|
CreatedAt func(childComplexity int) int
|
||||||
Work func(childComplexity int) int
|
ID func(childComplexity int) int
|
||||||
|
Likes func(childComplexity int) int
|
||||||
|
ReadingTime func(childComplexity int) int
|
||||||
|
Sentiment func(childComplexity int) int
|
||||||
|
Shares func(childComplexity int) int
|
||||||
|
TranslationCount func(childComplexity int) int
|
||||||
|
UpdatedAt func(childComplexity int) int
|
||||||
|
Views func(childComplexity int) int
|
||||||
|
Work func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
WritingStyle struct {
|
WritingStyle struct {
|
||||||
@ -602,6 +616,13 @@ type QueryResolver interface {
|
|||||||
Search(ctx context.Context, query string, limit *int32, offset *int32, filters *model.SearchFilters) (*model.SearchResults, error)
|
Search(ctx context.Context, query string, limit *int32, offset *int32, filters *model.SearchFilters) (*model.SearchResults, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorkResolver interface {
|
||||||
|
Stats(ctx context.Context, obj *model.Work) (*model.WorkStats, error)
|
||||||
|
}
|
||||||
|
type TranslationResolver interface {
|
||||||
|
Stats(ctx context.Context, obj *model.Translation) (*model.TranslationStats, error)
|
||||||
|
}
|
||||||
|
|
||||||
type executableSchema struct {
|
type executableSchema struct {
|
||||||
schema *ast.Schema
|
schema *ast.Schema
|
||||||
resolvers ResolverRoot
|
resolvers ResolverRoot
|
||||||
@ -2863,6 +2884,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.complexity.Translation.WorkID(childComplexity), true
|
return e.complexity.Translation.WorkID(childComplexity), true
|
||||||
|
|
||||||
|
case "TranslationStats.comments":
|
||||||
|
if e.complexity.TranslationStats.Comments == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.TranslationStats.Comments(childComplexity), true
|
||||||
|
|
||||||
case "TranslationStats.createdAt":
|
case "TranslationStats.createdAt":
|
||||||
if e.complexity.TranslationStats.CreatedAt == nil {
|
if e.complexity.TranslationStats.CreatedAt == nil {
|
||||||
break
|
break
|
||||||
@ -2877,6 +2905,34 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.complexity.TranslationStats.ID(childComplexity), true
|
return e.complexity.TranslationStats.ID(childComplexity), true
|
||||||
|
|
||||||
|
case "TranslationStats.likes":
|
||||||
|
if e.complexity.TranslationStats.Likes == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.TranslationStats.Likes(childComplexity), true
|
||||||
|
|
||||||
|
case "TranslationStats.readingTime":
|
||||||
|
if e.complexity.TranslationStats.ReadingTime == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.TranslationStats.ReadingTime(childComplexity), true
|
||||||
|
|
||||||
|
case "TranslationStats.sentiment":
|
||||||
|
if e.complexity.TranslationStats.Sentiment == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.TranslationStats.Sentiment(childComplexity), true
|
||||||
|
|
||||||
|
case "TranslationStats.shares":
|
||||||
|
if e.complexity.TranslationStats.Shares == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.TranslationStats.Shares(childComplexity), true
|
||||||
|
|
||||||
case "TranslationStats.translation":
|
case "TranslationStats.translation":
|
||||||
if e.complexity.TranslationStats.Translation == nil {
|
if e.complexity.TranslationStats.Translation == nil {
|
||||||
break
|
break
|
||||||
@ -3416,6 +3472,27 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.complexity.Work.WritingStyle(childComplexity), true
|
return e.complexity.Work.WritingStyle(childComplexity), true
|
||||||
|
|
||||||
|
case "WorkStats.bookmarks":
|
||||||
|
if e.complexity.WorkStats.Bookmarks == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.WorkStats.Bookmarks(childComplexity), true
|
||||||
|
|
||||||
|
case "WorkStats.comments":
|
||||||
|
if e.complexity.WorkStats.Comments == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.WorkStats.Comments(childComplexity), true
|
||||||
|
|
||||||
|
case "WorkStats.complexity":
|
||||||
|
if e.complexity.WorkStats.Complexity == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.WorkStats.Complexity(childComplexity), true
|
||||||
|
|
||||||
case "WorkStats.createdAt":
|
case "WorkStats.createdAt":
|
||||||
if e.complexity.WorkStats.CreatedAt == nil {
|
if e.complexity.WorkStats.CreatedAt == nil {
|
||||||
break
|
break
|
||||||
@ -3430,6 +3507,41 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.complexity.WorkStats.ID(childComplexity), true
|
return e.complexity.WorkStats.ID(childComplexity), true
|
||||||
|
|
||||||
|
case "WorkStats.likes":
|
||||||
|
if e.complexity.WorkStats.Likes == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.WorkStats.Likes(childComplexity), true
|
||||||
|
|
||||||
|
case "WorkStats.readingTime":
|
||||||
|
if e.complexity.WorkStats.ReadingTime == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.WorkStats.ReadingTime(childComplexity), true
|
||||||
|
|
||||||
|
case "WorkStats.sentiment":
|
||||||
|
if e.complexity.WorkStats.Sentiment == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.WorkStats.Sentiment(childComplexity), true
|
||||||
|
|
||||||
|
case "WorkStats.shares":
|
||||||
|
if e.complexity.WorkStats.Shares == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.WorkStats.Shares(childComplexity), true
|
||||||
|
|
||||||
|
case "WorkStats.translationCount":
|
||||||
|
if e.complexity.WorkStats.TranslationCount == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.WorkStats.TranslationCount(childComplexity), true
|
||||||
|
|
||||||
case "WorkStats.updatedAt":
|
case "WorkStats.updatedAt":
|
||||||
if e.complexity.WorkStats.UpdatedAt == nil {
|
if e.complexity.WorkStats.UpdatedAt == nil {
|
||||||
break
|
break
|
||||||
@ -21139,6 +21251,16 @@ func (ec *executionContext) fieldContext_Translation_stats(_ context.Context, fi
|
|||||||
return ec.fieldContext_TranslationStats_id(ctx, field)
|
return ec.fieldContext_TranslationStats_id(ctx, field)
|
||||||
case "views":
|
case "views":
|
||||||
return ec.fieldContext_TranslationStats_views(ctx, field)
|
return ec.fieldContext_TranslationStats_views(ctx, field)
|
||||||
|
case "likes":
|
||||||
|
return ec.fieldContext_TranslationStats_likes(ctx, field)
|
||||||
|
case "comments":
|
||||||
|
return ec.fieldContext_TranslationStats_comments(ctx, field)
|
||||||
|
case "shares":
|
||||||
|
return ec.fieldContext_TranslationStats_shares(ctx, field)
|
||||||
|
case "readingTime":
|
||||||
|
return ec.fieldContext_TranslationStats_readingTime(ctx, field)
|
||||||
|
case "sentiment":
|
||||||
|
return ec.fieldContext_TranslationStats_sentiment(ctx, field)
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
return ec.fieldContext_TranslationStats_createdAt(ctx, field)
|
return ec.fieldContext_TranslationStats_createdAt(ctx, field)
|
||||||
case "updatedAt":
|
case "updatedAt":
|
||||||
@ -21465,14 +21587,11 @@ func (ec *executionContext) _TranslationStats_views(ctx context.Context, field g
|
|||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
if !graphql.HasFieldError(ctx, fc) {
|
|
||||||
ec.Errorf(ctx, "must not be null")
|
|
||||||
}
|
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(int32)
|
res := resTmp.(*int32)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalNInt2int32(ctx, field.Selections, res)
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_TranslationStats_views(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
func (ec *executionContext) fieldContext_TranslationStats_views(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
@ -21488,6 +21607,211 @@ func (ec *executionContext) fieldContext_TranslationStats_views(_ context.Contex
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _TranslationStats_likes(ctx context.Context, field graphql.CollectedField, obj *model.TranslationStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_TranslationStats_likes(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Likes, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*int32)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_TranslationStats_likes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "TranslationStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Int does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _TranslationStats_comments(ctx context.Context, field graphql.CollectedField, obj *model.TranslationStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_TranslationStats_comments(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Comments, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*int32)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_TranslationStats_comments(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "TranslationStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Int does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _TranslationStats_shares(ctx context.Context, field graphql.CollectedField, obj *model.TranslationStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_TranslationStats_shares(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Shares, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*int32)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_TranslationStats_shares(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "TranslationStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Int does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _TranslationStats_readingTime(ctx context.Context, field graphql.CollectedField, obj *model.TranslationStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_TranslationStats_readingTime(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.ReadingTime, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*int32)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_TranslationStats_readingTime(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "TranslationStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Int does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _TranslationStats_sentiment(ctx context.Context, field graphql.CollectedField, obj *model.TranslationStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_TranslationStats_sentiment(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Sentiment, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*float64)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOFloat2ᚖfloat64(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_TranslationStats_sentiment(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "TranslationStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Float does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _TranslationStats_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.TranslationStats) (ret graphql.Marshaler) {
|
func (ec *executionContext) _TranslationStats_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.TranslationStats) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_TranslationStats_createdAt(ctx, field)
|
fc, err := ec.fieldContext_TranslationStats_createdAt(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -24976,6 +25300,22 @@ func (ec *executionContext) fieldContext_Work_stats(_ context.Context, field gra
|
|||||||
return ec.fieldContext_WorkStats_id(ctx, field)
|
return ec.fieldContext_WorkStats_id(ctx, field)
|
||||||
case "views":
|
case "views":
|
||||||
return ec.fieldContext_WorkStats_views(ctx, field)
|
return ec.fieldContext_WorkStats_views(ctx, field)
|
||||||
|
case "likes":
|
||||||
|
return ec.fieldContext_WorkStats_likes(ctx, field)
|
||||||
|
case "comments":
|
||||||
|
return ec.fieldContext_WorkStats_comments(ctx, field)
|
||||||
|
case "bookmarks":
|
||||||
|
return ec.fieldContext_WorkStats_bookmarks(ctx, field)
|
||||||
|
case "shares":
|
||||||
|
return ec.fieldContext_WorkStats_shares(ctx, field)
|
||||||
|
case "translationCount":
|
||||||
|
return ec.fieldContext_WorkStats_translationCount(ctx, field)
|
||||||
|
case "readingTime":
|
||||||
|
return ec.fieldContext_WorkStats_readingTime(ctx, field)
|
||||||
|
case "complexity":
|
||||||
|
return ec.fieldContext_WorkStats_complexity(ctx, field)
|
||||||
|
case "sentiment":
|
||||||
|
return ec.fieldContext_WorkStats_sentiment(ctx, field)
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
return ec.fieldContext_WorkStats_createdAt(ctx, field)
|
return ec.fieldContext_WorkStats_createdAt(ctx, field)
|
||||||
case "updatedAt":
|
case "updatedAt":
|
||||||
@ -25526,14 +25866,11 @@ func (ec *executionContext) _WorkStats_views(ctx context.Context, field graphql.
|
|||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
if !graphql.HasFieldError(ctx, fc) {
|
|
||||||
ec.Errorf(ctx, "must not be null")
|
|
||||||
}
|
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(int32)
|
res := resTmp.(*int32)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalNInt2int32(ctx, field.Selections, res)
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_WorkStats_views(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
func (ec *executionContext) fieldContext_WorkStats_views(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
@ -25549,6 +25886,334 @@ func (ec *executionContext) fieldContext_WorkStats_views(_ context.Context, fiel
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _WorkStats_likes(ctx context.Context, field graphql.CollectedField, obj *model.WorkStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_WorkStats_likes(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Likes, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*int32)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_WorkStats_likes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "WorkStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Int does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _WorkStats_comments(ctx context.Context, field graphql.CollectedField, obj *model.WorkStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_WorkStats_comments(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Comments, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*int32)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_WorkStats_comments(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "WorkStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Int does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _WorkStats_bookmarks(ctx context.Context, field graphql.CollectedField, obj *model.WorkStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_WorkStats_bookmarks(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Bookmarks, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*int32)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_WorkStats_bookmarks(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "WorkStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Int does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _WorkStats_shares(ctx context.Context, field graphql.CollectedField, obj *model.WorkStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_WorkStats_shares(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Shares, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*int32)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_WorkStats_shares(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "WorkStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Int does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _WorkStats_translationCount(ctx context.Context, field graphql.CollectedField, obj *model.WorkStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_WorkStats_translationCount(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.TranslationCount, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*int32)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_WorkStats_translationCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "WorkStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Int does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _WorkStats_readingTime(ctx context.Context, field graphql.CollectedField, obj *model.WorkStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_WorkStats_readingTime(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.ReadingTime, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*int32)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOInt2ᚖint32(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_WorkStats_readingTime(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "WorkStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Int does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _WorkStats_complexity(ctx context.Context, field graphql.CollectedField, obj *model.WorkStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_WorkStats_complexity(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Complexity, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*float64)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOFloat2ᚖfloat64(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_WorkStats_complexity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "WorkStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Float does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _WorkStats_sentiment(ctx context.Context, field graphql.CollectedField, obj *model.WorkStats) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_WorkStats_sentiment(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Sentiment, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*float64)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOFloat2ᚖfloat64(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_WorkStats_sentiment(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "WorkStats",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Float does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _WorkStats_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.WorkStats) (ret graphql.Marshaler) {
|
func (ec *executionContext) _WorkStats_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.WorkStats) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_WorkStats_createdAt(ctx, field)
|
fc, err := ec.fieldContext_WorkStats_createdAt(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -31389,9 +32054,16 @@ func (ec *executionContext) _TranslationStats(ctx context.Context, sel ast.Selec
|
|||||||
}
|
}
|
||||||
case "views":
|
case "views":
|
||||||
out.Values[i] = ec._TranslationStats_views(ctx, field, obj)
|
out.Values[i] = ec._TranslationStats_views(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
case "likes":
|
||||||
out.Invalids++
|
out.Values[i] = ec._TranslationStats_likes(ctx, field, obj)
|
||||||
}
|
case "comments":
|
||||||
|
out.Values[i] = ec._TranslationStats_comments(ctx, field, obj)
|
||||||
|
case "shares":
|
||||||
|
out.Values[i] = ec._TranslationStats_shares(ctx, field, obj)
|
||||||
|
case "readingTime":
|
||||||
|
out.Values[i] = ec._TranslationStats_readingTime(ctx, field, obj)
|
||||||
|
case "sentiment":
|
||||||
|
out.Values[i] = ec._TranslationStats_sentiment(ctx, field, obj)
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
out.Values[i] = ec._TranslationStats_createdAt(ctx, field, obj)
|
out.Values[i] = ec._TranslationStats_createdAt(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
@ -31847,9 +32519,22 @@ func (ec *executionContext) _WorkStats(ctx context.Context, sel ast.SelectionSet
|
|||||||
}
|
}
|
||||||
case "views":
|
case "views":
|
||||||
out.Values[i] = ec._WorkStats_views(ctx, field, obj)
|
out.Values[i] = ec._WorkStats_views(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
case "likes":
|
||||||
out.Invalids++
|
out.Values[i] = ec._WorkStats_likes(ctx, field, obj)
|
||||||
}
|
case "comments":
|
||||||
|
out.Values[i] = ec._WorkStats_comments(ctx, field, obj)
|
||||||
|
case "bookmarks":
|
||||||
|
out.Values[i] = ec._WorkStats_bookmarks(ctx, field, obj)
|
||||||
|
case "shares":
|
||||||
|
out.Values[i] = ec._WorkStats_shares(ctx, field, obj)
|
||||||
|
case "translationCount":
|
||||||
|
out.Values[i] = ec._WorkStats_translationCount(ctx, field, obj)
|
||||||
|
case "readingTime":
|
||||||
|
out.Values[i] = ec._WorkStats_readingTime(ctx, field, obj)
|
||||||
|
case "complexity":
|
||||||
|
out.Values[i] = ec._WorkStats_complexity(ctx, field, obj)
|
||||||
|
case "sentiment":
|
||||||
|
out.Values[i] = ec._WorkStats_sentiment(ctx, field, obj)
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
out.Values[i] = ec._WorkStats_createdAt(ctx, field, obj)
|
out.Values[i] = ec._WorkStats_createdAt(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
@ -33908,6 +34593,23 @@ func (ec *executionContext) marshalOEmotion2ᚕᚖterculᚋinternalᚋadapters
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalOFloat2ᚖfloat64(ctx context.Context, v any) (*float64, error) {
|
||||||
|
if v == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
res, err := graphql.UnmarshalFloatContext(ctx, v)
|
||||||
|
return &res, graphql.ErrorOnPath(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalOFloat2ᚖfloat64(ctx context.Context, sel ast.SelectionSet, v *float64) graphql.Marshaler {
|
||||||
|
if v == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
_ = sel
|
||||||
|
res := graphql.MarshalFloatContext(*v)
|
||||||
|
return graphql.WrapContextMarshaler(ctx, res)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalOID2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) {
|
func (ec *executionContext) unmarshalOID2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
@ -45,8 +45,8 @@ type Author struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthorInput struct {
|
type AuthorInput struct {
|
||||||
Name string `json:"name" valid:"required,length(3|255)"`
|
Name string `json:"name"`
|
||||||
Language string `json:"language" valid:"required,length(2|2)"`
|
Language string `json:"language"`
|
||||||
Biography *string `json:"biography,omitempty"`
|
Biography *string `json:"biography,omitempty"`
|
||||||
BirthDate *string `json:"birthDate,omitempty"`
|
BirthDate *string `json:"birthDate,omitempty"`
|
||||||
DeathDate *string `json:"deathDate,omitempty"`
|
DeathDate *string `json:"deathDate,omitempty"`
|
||||||
@ -87,7 +87,7 @@ type Bookmark struct {
|
|||||||
|
|
||||||
type BookmarkInput struct {
|
type BookmarkInput struct {
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
WorkID string `json:"workId" valid:"required"`
|
WorkID string `json:"workId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Category struct {
|
type Category struct {
|
||||||
@ -121,7 +121,7 @@ type Collection struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CollectionInput struct {
|
type CollectionInput struct {
|
||||||
Name string `json:"name" valid:"required,length(3|255)"`
|
Name string `json:"name"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
WorkIds []string `json:"workIds,omitempty"`
|
WorkIds []string `json:"workIds,omitempty"`
|
||||||
}
|
}
|
||||||
@ -149,7 +149,7 @@ type Comment struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CommentInput struct {
|
type CommentInput struct {
|
||||||
Text string `json:"text" valid:"required,length(1|4096)"`
|
Text string `json:"text"`
|
||||||
WorkID *string `json:"workId,omitempty"`
|
WorkID *string `json:"workId,omitempty"`
|
||||||
TranslationID *string `json:"translationId,omitempty"`
|
TranslationID *string `json:"translationId,omitempty"`
|
||||||
LineNumber *int32 `json:"lineNumber,omitempty"`
|
LineNumber *int32 `json:"lineNumber,omitempty"`
|
||||||
@ -269,8 +269,8 @@ type LinguisticLayer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LoginInput struct {
|
type LoginInput struct {
|
||||||
Email string `json:"email" valid:"required,email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password" valid:"required,length(6|255)"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mood struct {
|
type Mood struct {
|
||||||
@ -318,11 +318,11 @@ type ReadabilityScore struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RegisterInput struct {
|
type RegisterInput struct {
|
||||||
Username string `json:"username" valid:"required,alphanum,length(3|50)"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email" valid:"required,email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password" valid:"required,length(6|255)"`
|
Password string `json:"password"`
|
||||||
FirstName string `json:"firstName" valid:"required,alpha,length(2|50)"`
|
FirstName string `json:"firstName"`
|
||||||
LastName string `json:"lastName" valid:"required,alpha,length(2|50)"`
|
LastName string `json:"lastName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchFilters struct {
|
type SearchFilters struct {
|
||||||
@ -395,15 +395,20 @@ type Translation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TranslationInput struct {
|
type TranslationInput struct {
|
||||||
Name string `json:"name" valid:"required,length(3|255)"`
|
Name string `json:"name"`
|
||||||
Language string `json:"language" valid:"required,length(2|2)"`
|
Language string `json:"language"`
|
||||||
Content *string `json:"content,omitempty"`
|
Content *string `json:"content,omitempty"`
|
||||||
WorkID string `json:"workId" valid:"required,uuid"`
|
WorkID string `json:"workId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TranslationStats struct {
|
type TranslationStats struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Views int32 `json:"views"`
|
Views *int64 `json:"views,omitempty"`
|
||||||
|
Likes *int64 `json:"likes,omitempty"`
|
||||||
|
Comments *int64 `json:"comments,omitempty"`
|
||||||
|
Shares *int64 `json:"shares,omitempty"`
|
||||||
|
ReadingTime *int `json:"readingTime,omitempty"`
|
||||||
|
Sentiment *float64 `json:"sentiment,omitempty"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
UpdatedAt string `json:"updatedAt"`
|
UpdatedAt string `json:"updatedAt"`
|
||||||
Translation *Translation `json:"translation"`
|
Translation *Translation `json:"translation"`
|
||||||
@ -516,8 +521,8 @@ type Work struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WorkInput struct {
|
type WorkInput struct {
|
||||||
Name string `json:"name" valid:"required,length(3|255)"`
|
Name string `json:"name"`
|
||||||
Language string `json:"language" valid:"required,length(2|2)"`
|
Language string `json:"language"`
|
||||||
Content *string `json:"content,omitempty"`
|
Content *string `json:"content,omitempty"`
|
||||||
AuthorIds []string `json:"authorIds,omitempty"`
|
AuthorIds []string `json:"authorIds,omitempty"`
|
||||||
TagIds []string `json:"tagIds,omitempty"`
|
TagIds []string `json:"tagIds,omitempty"`
|
||||||
@ -525,11 +530,19 @@ type WorkInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WorkStats struct {
|
type WorkStats struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Views int32 `json:"views"`
|
Views *int64 `json:"views,omitempty"`
|
||||||
CreatedAt string `json:"createdAt"`
|
Likes *int64 `json:"likes,omitempty"`
|
||||||
UpdatedAt string `json:"updatedAt"`
|
Comments *int64 `json:"comments,omitempty"`
|
||||||
Work *Work `json:"work"`
|
Bookmarks *int64 `json:"bookmarks,omitempty"`
|
||||||
|
Shares *int64 `json:"shares,omitempty"`
|
||||||
|
TranslationCount *int64 `json:"translationCount,omitempty"`
|
||||||
|
ReadingTime *int `json:"readingTime,omitempty"`
|
||||||
|
Complexity *float64 `json:"complexity,omitempty"`
|
||||||
|
Sentiment *float64 `json:"sentiment,omitempty"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt string `json:"updatedAt"`
|
||||||
|
Work *Work `json:"work"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WritingStyle struct {
|
type WritingStyle struct {
|
||||||
|
|||||||
@ -289,12 +289,15 @@ type LinguisticLayer {
|
|||||||
|
|
||||||
type WorkStats {
|
type WorkStats {
|
||||||
id: ID!
|
id: ID!
|
||||||
views: Int!
|
views: Int
|
||||||
likes: Int!
|
likes: Int
|
||||||
comments: Int!
|
comments: Int
|
||||||
bookmarks: Int!
|
bookmarks: Int
|
||||||
shares: Int!
|
shares: Int
|
||||||
translationCount: Int!
|
translationCount: Int
|
||||||
|
readingTime: Int
|
||||||
|
complexity: Float
|
||||||
|
sentiment: Float
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
work: Work!
|
work: Work!
|
||||||
@ -302,10 +305,12 @@ type WorkStats {
|
|||||||
|
|
||||||
type TranslationStats {
|
type TranslationStats {
|
||||||
id: ID!
|
id: ID!
|
||||||
views: Int!
|
views: Int
|
||||||
likes: Int!
|
likes: Int
|
||||||
comments: Int!
|
comments: Int
|
||||||
shares: Int!
|
shares: Int
|
||||||
|
readingTime: Int
|
||||||
|
sentiment: Float
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
translation: Translation!
|
translation: Translation!
|
||||||
@ -448,6 +453,8 @@ type Edge {
|
|||||||
|
|
||||||
scalar JSON
|
scalar JSON
|
||||||
|
|
||||||
|
directive @binding(constraint: String!) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
|
||||||
|
|
||||||
# Queries
|
# Queries
|
||||||
type Query {
|
type Query {
|
||||||
# Work queries
|
# Work queries
|
||||||
@ -627,8 +634,8 @@ type AuthPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input WorkInput {
|
input WorkInput {
|
||||||
name: String!
|
name: String! @binding(constraint: "required,length(3|255)")
|
||||||
language: String!
|
language: String! @binding(constraint: "required,alpha,length(2|2)")
|
||||||
content: String
|
content: String
|
||||||
authorIds: [ID!]
|
authorIds: [ID!]
|
||||||
tagIds: [ID!]
|
tagIds: [ID!]
|
||||||
@ -636,15 +643,15 @@ input WorkInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input TranslationInput {
|
input TranslationInput {
|
||||||
name: String!
|
name: String! @binding(constraint: "required,length(3|255)")
|
||||||
language: String!
|
language: String! @binding(constraint: "required,alpha,length(2|2)")
|
||||||
content: String
|
content: String
|
||||||
workId: ID!
|
workId: ID! @binding(constraint: "required")
|
||||||
}
|
}
|
||||||
|
|
||||||
input AuthorInput {
|
input AuthorInput {
|
||||||
name: String!
|
name: String! @binding(constraint: "required,length(3|255)")
|
||||||
language: String!
|
language: String! @binding(constraint: "required,alpha,length(2|2)")
|
||||||
biography: String
|
biography: String
|
||||||
birthDate: String
|
birthDate: String
|
||||||
deathDate: String
|
deathDate: String
|
||||||
|
|||||||
@ -13,17 +13,10 @@ import (
|
|||||||
"tercul/internal/app/auth"
|
"tercul/internal/app/auth"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register is the resolver for the register field.
|
// Register is the resolver for the register field.
|
||||||
func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInput) (*model.AuthPayload, error) {
|
func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInput) (*model.AuthPayload, error) {
|
||||||
// Validate input
|
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert GraphQL input to service input
|
// Convert GraphQL input to service input
|
||||||
registerInput := auth.RegisterInput{
|
registerInput := auth.RegisterInput{
|
||||||
Username: input.Username,
|
Username: input.Username,
|
||||||
@ -58,11 +51,6 @@ func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInp
|
|||||||
|
|
||||||
// Login is the resolver for the login field.
|
// Login is the resolver for the login field.
|
||||||
func (r *mutationResolver) Login(ctx context.Context, input model.LoginInput) (*model.AuthPayload, error) {
|
func (r *mutationResolver) Login(ctx context.Context, input model.LoginInput) (*model.AuthPayload, error) {
|
||||||
// Validate input
|
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert GraphQL input to service input
|
// Convert GraphQL input to service input
|
||||||
loginInput := auth.LoginInput{
|
loginInput := auth.LoginInput{
|
||||||
Email: input.Email,
|
Email: input.Email,
|
||||||
@ -94,11 +82,9 @@ func (r *mutationResolver) Login(ctx context.Context, input model.LoginInput) (*
|
|||||||
|
|
||||||
// CreateWork is the resolver for the createWork field.
|
// CreateWork is the resolver for the createWork field.
|
||||||
func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput) (*model.Work, error) {
|
func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput) (*model.Work, error) {
|
||||||
// Validate input
|
if err := validateWorkInput(input); err != nil {
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create domain model
|
// Create domain model
|
||||||
work := &domain.Work{
|
work := &domain.Work{
|
||||||
Title: input.Name,
|
Title: input.Name,
|
||||||
@ -148,11 +134,9 @@ func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput
|
|||||||
|
|
||||||
// UpdateWork is the resolver for the updateWork field.
|
// UpdateWork is the resolver for the updateWork field.
|
||||||
func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input model.WorkInput) (*model.Work, error) {
|
func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input model.WorkInput) (*model.Work, error) {
|
||||||
// Validate input
|
if err := validateWorkInput(input); err != nil {
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
workID, err := strconv.ParseUint(id, 10, 32)
|
workID, err := strconv.ParseUint(id, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
@ -199,11 +183,9 @@ func (r *mutationResolver) DeleteWork(ctx context.Context, id string) (bool, err
|
|||||||
|
|
||||||
// CreateTranslation is the resolver for the createTranslation field.
|
// CreateTranslation is the resolver for the createTranslation field.
|
||||||
func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.TranslationInput) (*model.Translation, error) {
|
func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.TranslationInput) (*model.Translation, error) {
|
||||||
// Validate input
|
if err := validateTranslationInput(input); err != nil {
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
@ -238,11 +220,9 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
|
|||||||
|
|
||||||
// UpdateTranslation is the resolver for the updateTranslation field.
|
// UpdateTranslation is the resolver for the updateTranslation field.
|
||||||
func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, input model.TranslationInput) (*model.Translation, error) {
|
func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, input model.TranslationInput) (*model.Translation, error) {
|
||||||
// Validate input
|
if err := validateTranslationInput(input); err != nil {
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
translationID, err := strconv.ParseUint(id, 10, 32)
|
translationID, err := strconv.ParseUint(id, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
@ -298,11 +278,9 @@ func (r *mutationResolver) DeleteTranslation(ctx context.Context, id string) (bo
|
|||||||
|
|
||||||
// CreateAuthor is the resolver for the createAuthor field.
|
// CreateAuthor is the resolver for the createAuthor field.
|
||||||
func (r *mutationResolver) CreateAuthor(ctx context.Context, input model.AuthorInput) (*model.Author, error) {
|
func (r *mutationResolver) CreateAuthor(ctx context.Context, input model.AuthorInput) (*model.Author, error) {
|
||||||
// Validate input
|
if err := validateAuthorInput(input); err != nil {
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create domain model
|
// Create domain model
|
||||||
author := &domain.Author{
|
author := &domain.Author{
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
@ -327,11 +305,9 @@ func (r *mutationResolver) CreateAuthor(ctx context.Context, input model.AuthorI
|
|||||||
|
|
||||||
// UpdateAuthor is the resolver for the updateAuthor field.
|
// UpdateAuthor is the resolver for the updateAuthor field.
|
||||||
func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input model.AuthorInput) (*model.Author, error) {
|
func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input model.AuthorInput) (*model.Author, error) {
|
||||||
// Validate input
|
if err := validateAuthorInput(input); err != nil {
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
authorID, err := strconv.ParseUint(id, 10, 32)
|
authorID, err := strconv.ParseUint(id, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid author ID: %v", err)
|
return nil, fmt.Errorf("invalid author ID: %v", err)
|
||||||
@ -387,11 +363,6 @@ func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, err
|
|||||||
|
|
||||||
// CreateCollection is the resolver for the createCollection field.
|
// CreateCollection is the resolver for the createCollection field.
|
||||||
func (r *mutationResolver) CreateCollection(ctx context.Context, input model.CollectionInput) (*model.Collection, error) {
|
func (r *mutationResolver) CreateCollection(ctx context.Context, input model.CollectionInput) (*model.Collection, error) {
|
||||||
// Validate input
|
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user ID from context
|
// Get user ID from context
|
||||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -426,11 +397,6 @@ func (r *mutationResolver) CreateCollection(ctx context.Context, input model.Col
|
|||||||
|
|
||||||
// UpdateCollection is the resolver for the updateCollection field.
|
// UpdateCollection is the resolver for the updateCollection field.
|
||||||
func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, input model.CollectionInput) (*model.Collection, error) {
|
func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, input model.CollectionInput) (*model.Collection, error) {
|
||||||
// Validate input
|
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user ID from context
|
// Get user ID from context
|
||||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -623,11 +589,6 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
|
|||||||
|
|
||||||
// CreateComment is the resolver for the createComment field.
|
// CreateComment is the resolver for the createComment field.
|
||||||
func (r *mutationResolver) CreateComment(ctx context.Context, input model.CommentInput) (*model.Comment, error) {
|
func (r *mutationResolver) CreateComment(ctx context.Context, input model.CommentInput) (*model.Comment, error) {
|
||||||
// Validate input
|
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom validation
|
// Custom validation
|
||||||
if (input.WorkID == nil && input.TranslationID == nil) || (input.WorkID != nil && input.TranslationID != nil) {
|
if (input.WorkID == nil && input.TranslationID == nil) || (input.WorkID != nil && input.TranslationID != nil) {
|
||||||
return nil, fmt.Errorf("must provide either workId or translationId, but not both")
|
return nil, fmt.Errorf("must provide either workId or translationId, but not both")
|
||||||
@ -695,11 +656,6 @@ func (r *mutationResolver) CreateComment(ctx context.Context, input model.Commen
|
|||||||
|
|
||||||
// UpdateComment is the resolver for the updateComment field.
|
// UpdateComment is the resolver for the updateComment field.
|
||||||
func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input model.CommentInput) (*model.Comment, error) {
|
func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input model.CommentInput) (*model.Comment, error) {
|
||||||
// Validate input
|
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user ID from context
|
// Get user ID from context
|
||||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -887,11 +843,6 @@ func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, err
|
|||||||
|
|
||||||
// CreateBookmark is the resolver for the createBookmark field.
|
// CreateBookmark is the resolver for the createBookmark field.
|
||||||
func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.BookmarkInput) (*model.Bookmark, error) {
|
func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.BookmarkInput) (*model.Bookmark, error) {
|
||||||
// Validate input
|
|
||||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid input: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user ID from context
|
// Get user ID from context
|
||||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -1339,7 +1290,24 @@ func (r *queryResolver) Search(ctx context.Context, query string, limit *int32,
|
|||||||
panic(fmt.Errorf("not implemented: Search - search"))
|
panic(fmt.Errorf("not implemented: Search - search"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats is the resolver for the stats field.
|
// Mutation returns MutationResolver implementation.
|
||||||
|
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
|
||||||
|
|
||||||
|
// Query returns QueryResolver implementation.
|
||||||
|
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
||||||
|
|
||||||
|
type mutationResolver struct{ *Resolver }
|
||||||
|
type queryResolver struct{ *Resolver }
|
||||||
|
|
||||||
|
// Work returns WorkResolver implementation.
|
||||||
|
func (r *Resolver) Work() WorkResolver { return &workResolver{r} }
|
||||||
|
|
||||||
|
// Translation returns TranslationResolver implementation.
|
||||||
|
func (r *Resolver) Translation() TranslationResolver { return &translationResolver{r} }
|
||||||
|
|
||||||
|
type workResolver struct{ *Resolver }
|
||||||
|
type translationResolver struct{ *Resolver }
|
||||||
|
|
||||||
func (r *workResolver) Stats(ctx context.Context, obj *model.Work) (*model.WorkStats, error) {
|
func (r *workResolver) Stats(ctx context.Context, obj *model.Work) (*model.WorkStats, error) {
|
||||||
workID, err := strconv.ParseUint(obj.ID, 10, 32)
|
workID, err := strconv.ParseUint(obj.ID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1351,18 +1319,21 @@ func (r *workResolver) Stats(ctx context.Context, obj *model.Work) (*model.WorkS
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert domain model to GraphQL model
|
||||||
return &model.WorkStats{
|
return &model.WorkStats{
|
||||||
ID: fmt.Sprintf("%d", stats.ID),
|
ID: fmt.Sprintf("%d", stats.ID),
|
||||||
Views: int(stats.Views),
|
Views: &stats.Views,
|
||||||
Likes: int(stats.Likes),
|
Likes: &stats.Likes,
|
||||||
Comments: int(stats.Comments),
|
Comments: &stats.Comments,
|
||||||
Bookmarks: int(stats.Bookmarks),
|
Bookmarks: &stats.Bookmarks,
|
||||||
Shares: int(stats.Shares),
|
Shares: &stats.Shares,
|
||||||
TranslationCount: int(stats.TranslationCount),
|
TranslationCount: &stats.TranslationCount,
|
||||||
|
ReadingTime: &stats.ReadingTime,
|
||||||
|
Complexity: &stats.Complexity,
|
||||||
|
Sentiment: &stats.Sentiment,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats is the resolver for the stats field.
|
|
||||||
func (r *translationResolver) Stats(ctx context.Context, obj *model.Translation) (*model.TranslationStats, error) {
|
func (r *translationResolver) Stats(ctx context.Context, obj *model.Translation) (*model.TranslationStats, error) {
|
||||||
translationID, err := strconv.ParseUint(obj.ID, 10, 32)
|
translationID, err := strconv.ParseUint(obj.ID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1374,28 +1345,14 @@ func (r *translationResolver) Stats(ctx context.Context, obj *model.Translation)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert domain model to GraphQL model
|
||||||
return &model.TranslationStats{
|
return &model.TranslationStats{
|
||||||
ID: fmt.Sprintf("%d", stats.ID),
|
ID: fmt.Sprintf("%d", stats.ID),
|
||||||
Views: int(stats.Views),
|
Views: &stats.Views,
|
||||||
Likes: int(stats.Likes),
|
Likes: &stats.Likes,
|
||||||
Comments: int(stats.Comments),
|
Comments: &stats.Comments,
|
||||||
Shares: int(stats.Shares),
|
Shares: &stats.Shares,
|
||||||
|
ReadingTime: &stats.ReadingTime,
|
||||||
|
Sentiment: &stats.Sentiment,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutation returns MutationResolver implementation.
|
|
||||||
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
|
|
||||||
|
|
||||||
// Query returns QueryResolver implementation.
|
|
||||||
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
|
||||||
|
|
||||||
// Work returns WorkResolver implementation.
|
|
||||||
func (r *Resolver) Work() WorkResolver { return &workResolver{r} }
|
|
||||||
|
|
||||||
// Translation returns TranslationResolver implementation.
|
|
||||||
func (r *Resolver) Translation() TranslationResolver { return &translationResolver{r} }
|
|
||||||
|
|
||||||
type mutationResolver struct{ *Resolver }
|
|
||||||
type queryResolver struct{ *Resolver }
|
|
||||||
type workResolver struct{ *Resolver }
|
|
||||||
type translationResolver struct{ *Resolver }
|
|
||||||
|
|||||||
57
internal/adapters/graphql/validation.go
Normal file
57
internal/adapters/graphql/validation.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"tercul/internal/adapters/graphql/model"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrValidation = errors.New("validation failed")
|
||||||
|
|
||||||
|
func validateWorkInput(input model.WorkInput) error {
|
||||||
|
name := strings.TrimSpace(input.Name)
|
||||||
|
if len(name) < 3 {
|
||||||
|
return fmt.Errorf("name must be at least 3 characters long")
|
||||||
|
}
|
||||||
|
if !govalidator.Matches(name, `^[a-zA-Z0-9\s]+$`) {
|
||||||
|
return fmt.Errorf("name can only contain letters, numbers, and spaces")
|
||||||
|
}
|
||||||
|
if len(input.Language) != 2 {
|
||||||
|
return fmt.Errorf("language must be a 2-character code")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAuthorInput(input model.AuthorInput) error {
|
||||||
|
name := strings.TrimSpace(input.Name)
|
||||||
|
if len(name) < 3 {
|
||||||
|
return fmt.Errorf("name must be at least 3 characters long")
|
||||||
|
}
|
||||||
|
if !govalidator.Matches(name, `^[a-zA-Z0-9\s]+$`) {
|
||||||
|
return fmt.Errorf("name can only contain letters, numbers, and spaces")
|
||||||
|
}
|
||||||
|
if len(input.Language) != 2 {
|
||||||
|
return fmt.Errorf("language must be a 2-character code")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTranslationInput(input model.TranslationInput) error {
|
||||||
|
name := strings.TrimSpace(input.Name)
|
||||||
|
if len(name) < 3 {
|
||||||
|
return fmt.Errorf("name must be at least 3 characters long")
|
||||||
|
}
|
||||||
|
if !govalidator.Matches(name, `^[a-zA-Z0-9\s]+$`) {
|
||||||
|
return fmt.Errorf("name can only contain letters, numbers, and spaces")
|
||||||
|
}
|
||||||
|
if len(input.Language) != 2 {
|
||||||
|
return fmt.Errorf("language must be a 2-character code")
|
||||||
|
}
|
||||||
|
if input.WorkID == "" {
|
||||||
|
return fmt.Errorf("workId is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -2,7 +2,12 @@ package analytics
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/jobs/linguistics"
|
||||||
|
"tercul/internal/platform/log"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
@ -18,54 +23,71 @@ type Service interface {
|
|||||||
IncrementTranslationShares(ctx context.Context, translationID uint) error
|
IncrementTranslationShares(ctx context.Context, translationID uint) error
|
||||||
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error)
|
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error)
|
||||||
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error)
|
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error)
|
||||||
|
|
||||||
|
UpdateWorkReadingTime(ctx context.Context, workID uint) error
|
||||||
|
UpdateWorkComplexity(ctx context.Context, workID uint) error
|
||||||
|
UpdateWorkSentiment(ctx context.Context, workID uint) error
|
||||||
|
UpdateTranslationReadingTime(ctx context.Context, translationID uint) error
|
||||||
|
UpdateTranslationSentiment(ctx context.Context, translationID uint) error
|
||||||
|
|
||||||
|
UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error
|
||||||
|
UpdateTrending(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
repo domain.AnalyticsRepository
|
repo domain.AnalyticsRepository
|
||||||
|
analysisRepo linguistics.AnalysisRepository
|
||||||
|
translationRepo domain.TranslationRepository
|
||||||
|
sentimentProvider linguistics.SentimentProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(repo domain.AnalyticsRepository) Service {
|
func NewService(repo domain.AnalyticsRepository, analysisRepo linguistics.AnalysisRepository, translationRepo domain.TranslationRepository, sentimentProvider linguistics.SentimentProvider) Service {
|
||||||
return &service{repo: repo}
|
return &service{
|
||||||
|
repo: repo,
|
||||||
|
analysisRepo: analysisRepo,
|
||||||
|
translationRepo: translationRepo,
|
||||||
|
sentimentProvider: sentimentProvider,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkViews(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkViews(ctx context.Context, workID uint) error {
|
||||||
return s.repo.IncrementWorkViews(ctx, workID)
|
return s.repo.IncrementWorkCounter(ctx, workID, "views", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkLikes(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkLikes(ctx context.Context, workID uint) error {
|
||||||
return s.repo.IncrementWorkLikes(ctx, workID)
|
return s.repo.IncrementWorkCounter(ctx, workID, "likes", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkComments(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkComments(ctx context.Context, workID uint) error {
|
||||||
return s.repo.IncrementWorkComments(ctx, workID)
|
return s.repo.IncrementWorkCounter(ctx, workID, "comments", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
||||||
return s.repo.IncrementWorkBookmarks(ctx, workID)
|
return s.repo.IncrementWorkCounter(ctx, workID, "bookmarks", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkShares(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkShares(ctx context.Context, workID uint) error {
|
||||||
return s.repo.IncrementWorkShares(ctx, workID)
|
return s.repo.IncrementWorkCounter(ctx, workID, "shares", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
||||||
return s.repo.IncrementWorkTranslationCount(ctx, workID)
|
return s.repo.IncrementWorkCounter(ctx, workID, "translation_count", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementTranslationViews(ctx context.Context, translationID uint) error {
|
func (s *service) IncrementTranslationViews(ctx context.Context, translationID uint) error {
|
||||||
return s.repo.IncrementTranslationViews(ctx, translationID)
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "views", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
|
func (s *service) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||||
return s.repo.IncrementTranslationLikes(ctx, translationID)
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
func (s *service) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
||||||
return s.repo.IncrementTranslationComments(ctx, translationID)
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "comments", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
func (s *service) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
||||||
return s.repo.IncrementTranslationShares(ctx, translationID)
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "shares", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||||
@ -75,3 +97,156 @@ func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domai
|
|||||||
func (s *service) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
func (s *service) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
||||||
return s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
return s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uint) error {
|
||||||
|
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
textMetadata, _, _, err := s.analysisRepo.GetAnalysisData(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if textMetadata == nil {
|
||||||
|
return errors.New("text metadata not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
readingTime := 0
|
||||||
|
if textMetadata.WordCount > 0 {
|
||||||
|
readingTime = (textMetadata.WordCount + 199) / 200 // Ceil division
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.ReadingTime = readingTime
|
||||||
|
|
||||||
|
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) error {
|
||||||
|
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, readabilityScore, _, err := s.analysisRepo.GetAnalysisData(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
log.LogWarn("could not get readability score for work", log.F("workID", workID), log.F("error", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if readabilityScore == nil {
|
||||||
|
return errors.New("readability score not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.Complexity = readabilityScore.Score
|
||||||
|
|
||||||
|
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) error {
|
||||||
|
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, languageAnalysis, err := s.analysisRepo.GetAnalysisData(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
log.LogWarn("could not get language analysis for work", log.F("workID", workID), log.F("error", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if languageAnalysis == nil {
|
||||||
|
return errors.New("language analysis not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
sentiment, ok := languageAnalysis.Analysis["sentiment"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("sentiment score not found in language analysis")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.Sentiment = sentiment
|
||||||
|
|
||||||
|
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error {
|
||||||
|
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
translation, err := s.translationRepo.GetByID(ctx, translationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if translation == nil {
|
||||||
|
return errors.New("translation not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
wordCount := len(strings.Fields(translation.Content))
|
||||||
|
readingTime := 0
|
||||||
|
if wordCount > 0 {
|
||||||
|
readingTime = (wordCount + 199) / 200 // Ceil division
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.ReadingTime = readingTime
|
||||||
|
|
||||||
|
return s.repo.UpdateTranslationStats(ctx, translationID, *stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID uint) error {
|
||||||
|
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
translation, err := s.translationRepo.GetByID(ctx, translationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if translation == nil {
|
||||||
|
return errors.New("translation not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
sentiment, err := s.sentimentProvider.Score(translation.Content, translation.Language)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.Sentiment = sentiment
|
||||||
|
|
||||||
|
return s.repo.UpdateTranslationStats(ctx, translationID, *stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error {
|
||||||
|
today := time.Now().UTC().Truncate(24 * time.Hour)
|
||||||
|
engagement, err := s.repo.GetOrCreateUserEngagement(ctx, userID, today)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch eventType {
|
||||||
|
case "work_read":
|
||||||
|
engagement.WorksRead++
|
||||||
|
case "comment_made":
|
||||||
|
engagement.CommentsMade++
|
||||||
|
case "like_given":
|
||||||
|
engagement.LikesGiven++
|
||||||
|
case "bookmark_made":
|
||||||
|
engagement.BookmarksMade++
|
||||||
|
case "translation_made":
|
||||||
|
engagement.TranslationsMade++
|
||||||
|
default:
|
||||||
|
return errors.New("invalid engagement event type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.UpdateUserEngagement(ctx, engagement)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateTrending(ctx context.Context) error {
|
||||||
|
// TODO: Implement trending update
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -2,9 +2,12 @@ package analytics_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"tercul/internal/app/analytics"
|
"tercul/internal/app/analytics"
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/jobs/linguistics"
|
||||||
"tercul/internal/testutil"
|
"tercul/internal/testutil"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
@ -18,7 +21,10 @@ type AnalyticsServiceTestSuite struct {
|
|||||||
func (s *AnalyticsServiceTestSuite) SetupSuite() {
|
func (s *AnalyticsServiceTestSuite) SetupSuite() {
|
||||||
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||||
analyticsRepo := sql.NewAnalyticsRepository(s.DB)
|
analyticsRepo := sql.NewAnalyticsRepository(s.DB)
|
||||||
s.service = analytics.NewService(analyticsRepo)
|
analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB)
|
||||||
|
translationRepo := sql.NewTranslationRepository(s.DB)
|
||||||
|
sentimentProvider, _ := linguistics.NewGoVADERSentimentProvider()
|
||||||
|
s.service = analytics.NewService(analyticsRepo, analysisRepo, translationRepo, sentimentProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AnalyticsServiceTestSuite) SetupTest() {
|
func (s *AnalyticsServiceTestSuite) SetupTest() {
|
||||||
@ -121,6 +127,111 @@ func (s *AnalyticsServiceTestSuite) TestIncrementWorkTranslationCount() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestUpdateWorkReadingTime() {
|
||||||
|
s.Run("should update the reading time for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
s.DB.Create(&domain.ReadabilityScore{WorkID: work.ID})
|
||||||
|
s.DB.Create(&domain.LanguageAnalysis{WorkID: work.ID, Analysis: domain.JSONB{}})
|
||||||
|
textMetadata := &domain.TextMetadata{
|
||||||
|
WorkID: work.ID,
|
||||||
|
WordCount: 1000,
|
||||||
|
}
|
||||||
|
s.DB.Create(textMetadata)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.UpdateWorkReadingTime(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(5, stats.ReadingTime) // 1000 words / 200 wpm = 5 minutes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestUpdateTranslationReadingTime() {
|
||||||
|
s.Run("should update the reading time for a translation", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
translation := s.CreateTestTranslation(work.ID, "es", strings.Repeat("Contenido de prueba con quinientas palabras. ", 100))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.UpdateTranslationReadingTime(context.Background(), translation.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateTranslationStats(context.Background(), translation.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(3, stats.ReadingTime) // 500 words / 200 wpm = 2.5 -> 3 minutes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestUpdateWorkComplexity() {
|
||||||
|
s.Run("should update the complexity for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
s.DB.Create(&domain.TextMetadata{WorkID: work.ID})
|
||||||
|
s.DB.Create(&domain.LanguageAnalysis{WorkID: work.ID, Analysis: domain.JSONB{}})
|
||||||
|
readabilityScore := &domain.ReadabilityScore{
|
||||||
|
WorkID: work.ID,
|
||||||
|
Score: 12.34,
|
||||||
|
}
|
||||||
|
s.DB.Create(readabilityScore)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.UpdateWorkComplexity(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(12.34, stats.Complexity)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestUpdateWorkSentiment() {
|
||||||
|
s.Run("should update the sentiment for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
s.DB.Create(&domain.TextMetadata{WorkID: work.ID})
|
||||||
|
s.DB.Create(&domain.ReadabilityScore{WorkID: work.ID})
|
||||||
|
languageAnalysis := &domain.LanguageAnalysis{
|
||||||
|
WorkID: work.ID,
|
||||||
|
Analysis: domain.JSONB{
|
||||||
|
"sentiment": 0.5678,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
s.DB.Create(languageAnalysis)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.UpdateWorkSentiment(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(0.5678, stats.Sentiment)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestUpdateTranslationSentiment() {
|
||||||
|
s.Run("should update the sentiment for a translation", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
translation := s.CreateTestTranslation(work.ID, "en", "This is a wonderfully positive and uplifting sentence.")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.UpdateTranslationSentiment(context.Background(), translation.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateTranslationStats(context.Background(), translation.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.True(stats.Sentiment > 0.5)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestAnalyticsService(t *testing.T) {
|
func TestAnalyticsService(t *testing.T) {
|
||||||
suite.Run(t, new(AnalyticsServiceTestSuite))
|
suite.Run(t, new(AnalyticsServiceTestSuite))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,7 +95,18 @@ func (b *ApplicationBuilder) BuildBackgroundJobs() error {
|
|||||||
// BuildLinguistics initializes the linguistics components
|
// BuildLinguistics initializes the linguistics components
|
||||||
func (b *ApplicationBuilder) BuildLinguistics() error {
|
func (b *ApplicationBuilder) BuildLinguistics() error {
|
||||||
log.LogInfo("Initializing linguistic analyzer")
|
log.LogInfo("Initializing linguistic analyzer")
|
||||||
b.linguistics = linguistics.NewLinguisticsFactory(b.dbConn, b.redisCache, 4, true)
|
|
||||||
|
// Create sentiment provider
|
||||||
|
var sentimentProvider linguistics.SentimentProvider
|
||||||
|
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
|
||||||
|
if err != nil {
|
||||||
|
log.LogWarn("Failed to initialize GoVADER sentiment provider, using rule-based fallback", log.F("error", err))
|
||||||
|
sentimentProvider = &linguistics.RuleBasedSentimentProvider{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create linguistics factory and pass in the sentiment provider
|
||||||
|
b.linguistics = linguistics.NewLinguisticsFactory(b.dbConn, b.redisCache, 4, true, sentimentProvider)
|
||||||
|
|
||||||
log.LogInfo("Linguistics components initialized successfully")
|
log.LogInfo("Linguistics components initialized successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -135,7 +146,8 @@ func (b *ApplicationBuilder) BuildApplication() error {
|
|||||||
searchService := app_search.NewIndexService(localizationService, b.weaviateWrapper)
|
searchService := app_search.NewIndexService(localizationService, b.weaviateWrapper)
|
||||||
|
|
||||||
analyticsRepo := sql.NewAnalyticsRepository(b.dbConn)
|
analyticsRepo := sql.NewAnalyticsRepository(b.dbConn)
|
||||||
analyticsService := analytics.NewService(analyticsRepo)
|
analysisRepo := linguistics.NewGORMAnalysisRepository(b.dbConn)
|
||||||
|
analyticsService := analytics.NewService(analyticsRepo, analysisRepo, translationRepo, b.linguistics.GetSentimentProvider())
|
||||||
|
|
||||||
b.App = &Application{
|
b.App = &Application{
|
||||||
AnalyticsService: analyticsService,
|
AnalyticsService: analyticsService,
|
||||||
|
|||||||
@ -2,7 +2,9 @@ package sql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -15,84 +17,71 @@ func NewAnalyticsRepository(db *gorm.DB) domain.AnalyticsRepository {
|
|||||||
return &analyticsRepository{db: db}
|
return &analyticsRepository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementWorkViews(ctx context.Context, workID uint) error {
|
var allowedWorkCounterFields = map[string]bool{
|
||||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
"views": true,
|
||||||
if err != nil {
|
"likes": true,
|
||||||
return err
|
"comments": true,
|
||||||
}
|
"bookmarks": true,
|
||||||
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).UpdateColumn("views", gorm.Expr("views + 1")).Error
|
"shares": true,
|
||||||
|
"translation_count": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementWorkLikes(ctx context.Context, workID uint) error {
|
var allowedTranslationCounterFields = map[string]bool{
|
||||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
"views": true,
|
||||||
if err != nil {
|
"likes": true,
|
||||||
return err
|
"comments": true,
|
||||||
}
|
"shares": true,
|
||||||
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).UpdateColumn("likes", gorm.Expr("likes + 1")).Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementWorkComments(ctx context.Context, workID uint) error {
|
func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error {
|
||||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
if !allowedWorkCounterFields[field] {
|
||||||
if err != nil {
|
return fmt.Errorf("invalid work counter field: %s", field)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).UpdateColumn("comments", gorm.Expr("comments + 1")).Error
|
|
||||||
|
// Using a transaction to ensure atomicity
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// First, try to update the existing record
|
||||||
|
result := tx.Model(&domain.WorkStats{}).Where("work_id = ?", workID).UpdateColumn(field, gorm.Expr(fmt.Sprintf("%s + ?", field), value))
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no rows were affected, the record does not exist, so create it
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
initialData := map[string]interface{}{"work_id": workID, field: value}
|
||||||
|
return tx.Model(&domain.WorkStats{}).Create(initialData).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error {
|
||||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
if !allowedTranslationCounterFields[field] {
|
||||||
if err != nil {
|
return fmt.Errorf("invalid translation counter field: %s", field)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).UpdateColumn("bookmarks", gorm.Expr("bookmarks + 1")).Error
|
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
result := tx.Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).UpdateColumn(field, gorm.Expr(fmt.Sprintf("%s + ?", field), value))
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
initialData := map[string]interface{}{"translation_id": translationID, field: value}
|
||||||
|
return tx.Model(&domain.TranslationStats{}).Create(initialData).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementWorkShares(ctx context.Context, workID uint) error {
|
func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
|
||||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).Updates(stats).Error
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).UpdateColumn("shares", gorm.Expr("shares + 1")).Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
func (r *analyticsRepository) UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error {
|
||||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).Updates(stats).Error
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).UpdateColumn("translation_count", gorm.Expr("translation_count + 1")).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementTranslationViews(ctx context.Context, translationID uint) error {
|
|
||||||
_, err := r.GetOrCreateTranslationStats(ctx, translationID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).UpdateColumn("views", gorm.Expr("views + 1")).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
|
|
||||||
_, err := r.GetOrCreateTranslationStats(ctx, translationID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).UpdateColumn("likes", gorm.Expr("likes + 1")).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
|
||||||
_, err := r.GetOrCreateTranslationStats(ctx, translationID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).UpdateColumn("comments", gorm.Expr("comments + 1")).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
|
||||||
_, err := r.GetOrCreateTranslationStats(ctx, translationID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).UpdateColumn("shares", gorm.Expr("shares + 1")).Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||||
@ -106,3 +95,29 @@ func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, t
|
|||||||
err := r.db.WithContext(ctx).Where(domain.TranslationStats{TranslationID: translationID}).FirstOrCreate(&stats).Error
|
err := r.db.WithContext(ctx).Where(domain.TranslationStats{TranslationID: translationID}).FirstOrCreate(&stats).Error
|
||||||
return &stats, err
|
return &stats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*domain.UserEngagement, error) {
|
||||||
|
var engagement domain.UserEngagement
|
||||||
|
err := r.db.WithContext(ctx).Where(domain.UserEngagement{UserID: userID, Date: date}).FirstOrCreate(&engagement).Error
|
||||||
|
return &engagement, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) UpdateUserEngagement(ctx context.Context, userEngagement *domain.UserEngagement) error {
|
||||||
|
return r.db.WithContext(ctx).Save(userEngagement).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) UpdateTrending(ctx context.Context, trending []domain.Trending) error {
|
||||||
|
if len(trending) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
timePeriod := trending[0].TimePeriod
|
||||||
|
date := trending[0].Date
|
||||||
|
if err := tx.Where("time_period = ? AND date = ?", timePeriod, date).Delete(&domain.Trending{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Create(&trending).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -2,17 +2,16 @@ package domain
|
|||||||
|
|
||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type AnalyticsRepository interface {
|
type AnalyticsRepository interface {
|
||||||
IncrementWorkViews(ctx context.Context, workID uint) error
|
IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error
|
||||||
IncrementWorkLikes(ctx context.Context, workID uint) error
|
IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error
|
||||||
IncrementWorkComments(ctx context.Context, workID uint) error
|
UpdateWorkStats(ctx context.Context, workID uint, stats WorkStats) error
|
||||||
IncrementWorkBookmarks(ctx context.Context, workID uint) error
|
UpdateTranslationStats(ctx context.Context, translationID uint, stats TranslationStats) error
|
||||||
IncrementWorkShares(ctx context.Context, workID uint) error
|
|
||||||
IncrementWorkTranslationCount(ctx context.Context, workID uint) error
|
|
||||||
IncrementTranslationViews(ctx context.Context, translationID uint) error
|
|
||||||
IncrementTranslationLikes(ctx context.Context, translationID uint) error
|
|
||||||
IncrementTranslationComments(ctx context.Context, translationID uint) error
|
|
||||||
IncrementTranslationShares(ctx context.Context, translationID uint) error
|
|
||||||
GetOrCreateWorkStats(ctx context.Context, workID uint) (*WorkStats, error)
|
GetOrCreateWorkStats(ctx context.Context, workID uint) (*WorkStats, error)
|
||||||
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*TranslationStats, error)
|
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*TranslationStats, error)
|
||||||
|
GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*UserEngagement, error)
|
||||||
|
UpdateUserEngagement(ctx context.Context, userEngagement *UserEngagement) error
|
||||||
|
UpdateTrending(ctx context.Context, trending []Trending) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -744,24 +744,52 @@ type AuditLog struct {
|
|||||||
|
|
||||||
type WorkStats struct {
|
type WorkStats struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Views int64 `gorm:"default:0"`
|
Views int64 `gorm:"default:0"`
|
||||||
Likes int64 `gorm:"default:0"`
|
Likes int64 `gorm:"default:0"`
|
||||||
Comments int64 `gorm:"default:0"`
|
Comments int64 `gorm:"default:0"`
|
||||||
Bookmarks int64 `gorm:"default:0"`
|
Bookmarks int64 `gorm:"default:0"`
|
||||||
Shares int64 `gorm:"default:0"`
|
Shares int64 `gorm:"default:0"`
|
||||||
TranslationCount int64 `gorm:"default:0"`
|
TranslationCount int64 `gorm:"default:0"`
|
||||||
WorkID uint `gorm:"uniqueIndex;index"`
|
ReadingTime int `gorm:"default:0"`
|
||||||
Work *Work `gorm:"foreignKey:WorkID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
Complexity float64 `gorm:"type:decimal(5,2);default:0.0"`
|
||||||
|
Sentiment float64 `gorm:"type:decimal(5,2);default:0.0"`
|
||||||
|
WorkID uint `gorm:"uniqueIndex;index"`
|
||||||
|
Work *Work `gorm:"foreignKey:WorkID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
}
|
}
|
||||||
type TranslationStats struct {
|
type TranslationStats struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Views int64 `gorm:"default:0"`
|
Views int64 `gorm:"default:0"`
|
||||||
Likes int64 `gorm:"default:0"`
|
Likes int64 `gorm:"default:0"`
|
||||||
Comments int64 `gorm:"default:0"`
|
Comments int64 `gorm:"default:0"`
|
||||||
Shares int64 `gorm:"default:0"`
|
Shares int64 `gorm:"default:0"`
|
||||||
TranslationID uint `gorm:"uniqueIndex;index"`
|
ReadingTime int `gorm:"default:0"`
|
||||||
|
Sentiment float64 `gorm:"type:decimal(5,2);default:0.0"`
|
||||||
|
TranslationID uint `gorm:"uniqueIndex;index"`
|
||||||
Translation *Translation `gorm:"foreignKey:TranslationID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
Translation *Translation `gorm:"foreignKey:TranslationID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserEngagement struct {
|
||||||
|
BaseModel
|
||||||
|
UserID uint `gorm:"index;uniqueIndex:uniq_user_engagement_date"`
|
||||||
|
User *User `gorm:"foreignKey:UserID"`
|
||||||
|
Date time.Time `gorm:"type:date;uniqueIndex:uniq_user_engagement_date"`
|
||||||
|
WorksRead int `gorm:"default:0"`
|
||||||
|
CommentsMade int `gorm:"default:0"`
|
||||||
|
LikesGiven int `gorm:"default:0"`
|
||||||
|
BookmarksMade int `gorm:"default:0"`
|
||||||
|
TranslationsMade int `gorm:"default:0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Trending struct {
|
||||||
|
BaseModel
|
||||||
|
EntityType string `gorm:"size:50;not null;index:idx_trending_entity_period_date,uniqueIndex:uniq_trending_rank"`
|
||||||
|
EntityID uint `gorm:"not null;index:idx_trending_entity_period_date,uniqueIndex:uniq_trending_rank"`
|
||||||
|
Rank int `gorm:"not null;uniqueIndex:uniq_trending_rank"`
|
||||||
|
Score float64 `gorm:"type:decimal(10,2);default:0.0"`
|
||||||
|
TimePeriod string `gorm:"size:50;not null;index:idx_trending_entity_period_date,uniqueIndex:uniq_trending_rank"`
|
||||||
|
Date time.Time `gorm:"type:date;index:idx_trending_entity_period_date,uniqueIndex:uniq_trending_rank"`
|
||||||
|
}
|
||||||
|
|
||||||
type UserStats struct {
|
type UserStats struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Activity int64 `gorm:"default:0"`
|
Activity int64 `gorm:"default:0"`
|
||||||
|
|||||||
@ -153,6 +153,7 @@ func (r *GORMAnalysisRepository) GetAnalysisData(ctx context.Context, workID uin
|
|||||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).First(&languageAnalysis).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).First(&languageAnalysis).Error; err != nil {
|
||||||
log.LogWarn("No language analysis found for work",
|
log.LogWarn("No language analysis found for work",
|
||||||
log.F("workID", workID))
|
log.F("workID", workID))
|
||||||
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &textMetadata, &readabilityScore, &languageAnalysis, nil
|
return &textMetadata, &readabilityScore, &languageAnalysis, nil
|
||||||
|
|||||||
55
internal/jobs/linguistics/analysis_repository_test.go
Normal file
55
internal/jobs/linguistics/analysis_repository_test.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package linguistics_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/jobs/linguistics"
|
||||||
|
"tercul/internal/testutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnalysisRepositoryTestSuite struct {
|
||||||
|
testutil.IntegrationTestSuite
|
||||||
|
repo linguistics.AnalysisRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalysisRepositoryTestSuite) SetupSuite() {
|
||||||
|
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||||
|
s.repo = linguistics.NewGORMAnalysisRepository(s.DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalysisRepositoryTestSuite) SetupTest() {
|
||||||
|
s.IntegrationTestSuite.SetupTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalysisRepositoryTestSuite) TestGetAnalysisData() {
|
||||||
|
s.Run("should return the correct analysis data", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
languageAnalysis := &domain.LanguageAnalysis{
|
||||||
|
WorkID: work.ID,
|
||||||
|
Analysis: domain.JSONB{
|
||||||
|
"sentiment": 0.5678,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
s.DB.Create(languageAnalysis)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_, _, returnedAnalysis, err := s.repo.GetAnalysisData(context.Background(), work.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().NotNil(returnedAnalysis)
|
||||||
|
s.Require().NotNil(returnedAnalysis.Analysis)
|
||||||
|
sentiment, ok := returnedAnalysis.Analysis["sentiment"].(float64)
|
||||||
|
s.Require().True(ok)
|
||||||
|
s.Equal(0.5678, sentiment)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnalysisRepository(t *testing.T) {
|
||||||
|
suite.Run(t, new(AnalysisRepositoryTestSuite))
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ type LinguisticsFactory struct {
|
|||||||
analysisRepo AnalysisRepository
|
analysisRepo AnalysisRepository
|
||||||
workAnalysisService WorkAnalysisService
|
workAnalysisService WorkAnalysisService
|
||||||
analyzer Analyzer
|
analyzer Analyzer
|
||||||
|
sentimentProvider SentimentProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLinguisticsFactory creates a new LinguisticsFactory with all components
|
// NewLinguisticsFactory creates a new LinguisticsFactory with all components
|
||||||
@ -22,20 +23,13 @@ func NewLinguisticsFactory(
|
|||||||
cache cache.Cache,
|
cache cache.Cache,
|
||||||
concurrency int,
|
concurrency int,
|
||||||
cacheEnabled bool,
|
cacheEnabled bool,
|
||||||
|
sentimentProvider SentimentProvider,
|
||||||
) *LinguisticsFactory {
|
) *LinguisticsFactory {
|
||||||
// Create text analyzer and wire providers (prefer external libs when available)
|
// Create text analyzer and wire providers (prefer external libs when available)
|
||||||
textAnalyzer := NewBasicTextAnalyzer()
|
textAnalyzer := NewBasicTextAnalyzer()
|
||||||
|
|
||||||
// Wire sentiment provider: GoVADER (configurable)
|
// Wire sentiment provider
|
||||||
if config.Cfg.NLPUseVADER {
|
textAnalyzer = textAnalyzer.WithSentimentProvider(sentimentProvider)
|
||||||
if sp, err := NewGoVADERSentimentProvider(); err == nil {
|
|
||||||
textAnalyzer = textAnalyzer.WithSentimentProvider(sp)
|
|
||||||
} else {
|
|
||||||
textAnalyzer = textAnalyzer.WithSentimentProvider(RuleBasedSentimentProvider{})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
textAnalyzer = textAnalyzer.WithSentimentProvider(RuleBasedSentimentProvider{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wire language detector: lingua-go (configurable)
|
// Wire language detector: lingua-go (configurable)
|
||||||
if config.Cfg.NLPUseLingua {
|
if config.Cfg.NLPUseLingua {
|
||||||
@ -79,6 +73,7 @@ func NewLinguisticsFactory(
|
|||||||
analysisRepo: analysisRepo,
|
analysisRepo: analysisRepo,
|
||||||
workAnalysisService: workAnalysisService,
|
workAnalysisService: workAnalysisService,
|
||||||
analyzer: analyzer,
|
analyzer: analyzer,
|
||||||
|
sentimentProvider: sentimentProvider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,3 +101,8 @@ func (f *LinguisticsFactory) GetWorkAnalysisService() WorkAnalysisService {
|
|||||||
func (f *LinguisticsFactory) GetAnalyzer() Analyzer {
|
func (f *LinguisticsFactory) GetAnalyzer() Analyzer {
|
||||||
return f.analyzer
|
return f.analyzer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSentimentProvider returns the sentiment provider
|
||||||
|
func (f *LinguisticsFactory) GetSentimentProvider() SentimentProvider {
|
||||||
|
return f.sentimentProvider
|
||||||
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
func TestFactory_WiresProviders(t *testing.T) {
|
func TestFactory_WiresProviders(t *testing.T) {
|
||||||
// We won't spin a DB/cache here; this is a smoke test of wiring methods
|
// We won't spin a DB/cache here; this is a smoke test of wiring methods
|
||||||
f := NewLinguisticsFactory(nil, nil, 2, true)
|
f := NewLinguisticsFactory(nil, nil, 2, true, nil)
|
||||||
ta := f.GetTextAnalyzer().(*BasicTextAnalyzer)
|
ta := f.GetTextAnalyzer().(*BasicTextAnalyzer)
|
||||||
require.NotNil(t, ta)
|
require.NotNil(t, ta)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,40 +18,44 @@ import (
|
|||||||
"tercul/internal/app"
|
"tercul/internal/app"
|
||||||
"tercul/internal/app/copyright"
|
"tercul/internal/app/copyright"
|
||||||
"tercul/internal/app/localization"
|
"tercul/internal/app/localization"
|
||||||
|
"tercul/internal/app/analytics"
|
||||||
"tercul/internal/app/monetization"
|
"tercul/internal/app/monetization"
|
||||||
"tercul/internal/app/search"
|
"tercul/internal/app/search"
|
||||||
"tercul/internal/app/work"
|
"tercul/internal/app/work"
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/jobs/linguistics"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IntegrationTestSuite provides a comprehensive test environment with either in-memory SQLite or mock repositories
|
// IntegrationTestSuite provides a comprehensive test environment with either in-memory SQLite or mock repositories
|
||||||
type IntegrationTestSuite struct {
|
type IntegrationTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
App *app.Application
|
App *app.Application
|
||||||
DB *gorm.DB
|
DB *gorm.DB
|
||||||
WorkRepo domain.WorkRepository
|
WorkRepo domain.WorkRepository
|
||||||
UserRepo domain.UserRepository
|
UserRepo domain.UserRepository
|
||||||
AuthorRepo domain.AuthorRepository
|
AuthorRepo domain.AuthorRepository
|
||||||
TranslationRepo domain.TranslationRepository
|
TranslationRepo domain.TranslationRepository
|
||||||
CommentRepo domain.CommentRepository
|
CommentRepo domain.CommentRepository
|
||||||
LikeRepo domain.LikeRepository
|
LikeRepo domain.LikeRepository
|
||||||
BookmarkRepo domain.BookmarkRepository
|
BookmarkRepo domain.BookmarkRepository
|
||||||
CollectionRepo domain.CollectionRepository
|
CollectionRepo domain.CollectionRepository
|
||||||
TagRepo domain.TagRepository
|
TagRepo domain.TagRepository
|
||||||
CategoryRepo domain.CategoryRepository
|
CategoryRepo domain.CategoryRepository
|
||||||
BookRepo domain.BookRepository
|
BookRepo domain.BookRepository
|
||||||
MonetizationRepo domain.MonetizationRepository
|
MonetizationRepo domain.MonetizationRepository
|
||||||
PublisherRepo domain.PublisherRepository
|
PublisherRepo domain.PublisherRepository
|
||||||
SourceRepo domain.SourceRepository
|
SourceRepo domain.SourceRepository
|
||||||
CopyrightRepo domain.CopyrightRepository
|
CopyrightRepo domain.CopyrightRepository
|
||||||
|
AnalyticsRepo domain.AnalyticsRepository
|
||||||
|
AnalysisRepo linguistics.AnalysisRepository
|
||||||
// Services
|
// Services
|
||||||
WorkCommands *work.WorkCommands
|
WorkCommands *work.WorkCommands
|
||||||
WorkQueries *work.WorkQueries
|
WorkQueries *work.WorkQueries
|
||||||
Localization localization.Service
|
Localization localization.Service
|
||||||
AuthCommands *auth.AuthCommands
|
AuthCommands *auth.AuthCommands
|
||||||
AuthQueries *auth.AuthQueries
|
AuthQueries *auth.AuthQueries
|
||||||
|
AnalyticsService analytics.Service
|
||||||
|
|
||||||
// Test data
|
// Test data
|
||||||
TestWorks []*domain.Work
|
TestWorks []*domain.Work
|
||||||
@ -159,6 +163,8 @@ func (s *IntegrationTestSuite) setupInMemoryDB(config *TestConfig) {
|
|||||||
&domain.BookMonetization{},
|
&domain.BookMonetization{},
|
||||||
&domain.PublisherMonetization{},
|
&domain.PublisherMonetization{},
|
||||||
&domain.SourceMonetization{},
|
&domain.SourceMonetization{},
|
||||||
|
&domain.WorkStats{},
|
||||||
|
&domain.TranslationStats{},
|
||||||
// &domain.WorkAnalytics{}, // Commented out as it's not in models package
|
// &domain.WorkAnalytics{}, // Commented out as it's not in models package
|
||||||
&domain.ReadabilityScore{},
|
&domain.ReadabilityScore{},
|
||||||
&domain.WritingStyle{},
|
&domain.WritingStyle{},
|
||||||
@ -168,8 +174,12 @@ func (s *IntegrationTestSuite) setupInMemoryDB(config *TestConfig) {
|
|||||||
&domain.Concept{},
|
&domain.Concept{},
|
||||||
&domain.LinguisticLayer{},
|
&domain.LinguisticLayer{},
|
||||||
&domain.WorkStats{},
|
&domain.WorkStats{},
|
||||||
|
&domain.TranslationStats{},
|
||||||
|
&domain.UserEngagement{},
|
||||||
|
&domain.Trending{},
|
||||||
&domain.TextMetadata{},
|
&domain.TextMetadata{},
|
||||||
&domain.PoeticAnalysis{},
|
&domain.PoeticAnalysis{},
|
||||||
|
&domain.LanguageAnalysis{},
|
||||||
&domain.TranslationField{},
|
&domain.TranslationField{},
|
||||||
&TestEntity{}, // Add TestEntity for generic repository tests
|
&TestEntity{}, // Add TestEntity for generic repository tests
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@ -192,6 +202,8 @@ func (s *IntegrationTestSuite) setupInMemoryDB(config *TestConfig) {
|
|||||||
s.PublisherRepo = sql.NewPublisherRepository(db)
|
s.PublisherRepo = sql.NewPublisherRepository(db)
|
||||||
s.SourceRepo = sql.NewSourceRepository(db)
|
s.SourceRepo = sql.NewSourceRepository(db)
|
||||||
s.CopyrightRepo = sql.NewCopyrightRepository(db)
|
s.CopyrightRepo = sql.NewCopyrightRepository(db)
|
||||||
|
s.AnalyticsRepo = sql.NewAnalyticsRepository(db)
|
||||||
|
s.AnalysisRepo = linguistics.NewGORMAnalysisRepository(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupMockRepositories sets up mock repositories for testing
|
// setupMockRepositories sets up mock repositories for testing
|
||||||
@ -218,6 +230,8 @@ func (s *IntegrationTestSuite) setupServices() {
|
|||||||
jwtManager := auth_platform.NewJWTManager()
|
jwtManager := auth_platform.NewJWTManager()
|
||||||
s.AuthCommands = auth.NewAuthCommands(s.UserRepo, jwtManager)
|
s.AuthCommands = auth.NewAuthCommands(s.UserRepo, jwtManager)
|
||||||
s.AuthQueries = auth.NewAuthQueries(s.UserRepo, jwtManager)
|
s.AuthQueries = auth.NewAuthQueries(s.UserRepo, jwtManager)
|
||||||
|
sentimentProvider, _ := linguistics.NewGoVADERSentimentProvider()
|
||||||
|
s.AnalyticsService = analytics.NewService(s.AnalyticsRepo, s.AnalysisRepo, s.TranslationRepo, sentimentProvider)
|
||||||
|
|
||||||
copyrightCommands := copyright.NewCopyrightCommands(s.CopyrightRepo)
|
copyrightCommands := copyright.NewCopyrightCommands(s.CopyrightRepo)
|
||||||
copyrightQueries := copyright.NewCopyrightQueries(s.CopyrightRepo, s.WorkRepo, s.AuthorRepo, s.BookRepo, s.PublisherRepo, s.SourceRepo)
|
copyrightQueries := copyright.NewCopyrightQueries(s.CopyrightRepo, s.WorkRepo, s.AuthorRepo, s.BookRepo, s.PublisherRepo, s.SourceRepo)
|
||||||
@ -226,6 +240,7 @@ func (s *IntegrationTestSuite) setupServices() {
|
|||||||
monetizationQueries := monetization.NewMonetizationQueries(s.MonetizationRepo, s.WorkRepo, s.AuthorRepo, s.BookRepo, s.PublisherRepo, s.SourceRepo)
|
monetizationQueries := monetization.NewMonetizationQueries(s.MonetizationRepo, s.WorkRepo, s.AuthorRepo, s.BookRepo, s.PublisherRepo, s.SourceRepo)
|
||||||
|
|
||||||
s.App = &app.Application{
|
s.App = &app.Application{
|
||||||
|
AnalyticsService: s.AnalyticsService,
|
||||||
WorkCommands: s.WorkCommands,
|
WorkCommands: s.WorkCommands,
|
||||||
WorkQueries: s.WorkQueries,
|
WorkQueries: s.WorkQueries,
|
||||||
AuthCommands: s.AuthCommands,
|
AuthCommands: s.AuthCommands,
|
||||||
@ -418,3 +433,17 @@ func (s *IntegrationTestSuite) CreateAuthenticatedUser(username, email string, r
|
|||||||
|
|
||||||
return user, token
|
return user, token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateTestTranslation creates a test translation for a work
|
||||||
|
func (s *IntegrationTestSuite) CreateTestTranslation(workID uint, language, content string) *domain.Translation {
|
||||||
|
translation := &domain.Translation{
|
||||||
|
Title: "Test Translation",
|
||||||
|
Content: content,
|
||||||
|
Language: language,
|
||||||
|
TranslatableID: workID,
|
||||||
|
TranslatableType: "Work",
|
||||||
|
}
|
||||||
|
err := s.TranslationRepo.Create(context.Background(), translation)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
return translation
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user