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
|
||||
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)
|
||||
mux := http.NewServeMux()
|
||||
@ -21,7 +23,9 @@ func NewServer(resolver *graphql.Resolver) http.Handler {
|
||||
|
||||
// NewServerWithAuth creates a new GraphQL server with authentication middleware
|
||||
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
|
||||
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/elastic/go-sysinfo v1.15.4 // 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/errors v0.7.1 // 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/swag v0.23.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-viper/mapstructure/v2 v2.4.0 // 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/josharian/intern v1.0.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/mattn/go-isatty v0.0.20 // 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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
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/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
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.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||
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/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
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-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
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 {
|
||||
Binding func(ctx context.Context, obj interface{}, next graphql.Resolver, constraint string) (interface{}, error)
|
||||
}
|
||||
|
||||
type ComplexityRoot struct {
|
||||
@ -425,8 +426,13 @@ type ComplexityRoot struct {
|
||||
}
|
||||
|
||||
TranslationStats struct {
|
||||
Comments func(childComplexity int) int
|
||||
CreatedAt 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
|
||||
UpdatedAt func(childComplexity int) int
|
||||
Views func(childComplexity int) int
|
||||
@ -522,11 +528,19 @@ type ComplexityRoot struct {
|
||||
}
|
||||
|
||||
WorkStats struct {
|
||||
CreatedAt func(childComplexity int) int
|
||||
ID func(childComplexity int) int
|
||||
UpdatedAt func(childComplexity int) int
|
||||
Views func(childComplexity int) int
|
||||
Work func(childComplexity int) int
|
||||
Bookmarks func(childComplexity int) int
|
||||
Comments func(childComplexity int) int
|
||||
Complexity func(childComplexity int) int
|
||||
CreatedAt 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 {
|
||||
@ -602,6 +616,13 @@ type QueryResolver interface {
|
||||
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 {
|
||||
schema *ast.Schema
|
||||
resolvers ResolverRoot
|
||||
@ -2863,6 +2884,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
||||
|
||||
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":
|
||||
if e.complexity.TranslationStats.CreatedAt == nil {
|
||||
break
|
||||
@ -2877,6 +2905,34 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
||||
|
||||
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":
|
||||
if e.complexity.TranslationStats.Translation == nil {
|
||||
break
|
||||
@ -3416,6 +3472,27 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
||||
|
||||
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":
|
||||
if e.complexity.WorkStats.CreatedAt == nil {
|
||||
break
|
||||
@ -3430,6 +3507,41 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
||||
|
||||
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":
|
||||
if e.complexity.WorkStats.UpdatedAt == nil {
|
||||
break
|
||||
@ -21139,6 +21251,16 @@ func (ec *executionContext) fieldContext_Translation_stats(_ context.Context, fi
|
||||
return ec.fieldContext_TranslationStats_id(ctx, field)
|
||||
case "views":
|
||||
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":
|
||||
return ec.fieldContext_TranslationStats_createdAt(ctx, field)
|
||||
case "updatedAt":
|
||||
@ -21465,14 +21587,11 @@ func (ec *executionContext) _TranslationStats_views(ctx context.Context, field g
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(int32)
|
||||
res := resTmp.(*int32)
|
||||
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) {
|
||||
@ -21488,6 +21607,211 @@ func (ec *executionContext) fieldContext_TranslationStats_views(_ context.Contex
|
||||
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) {
|
||||
fc, err := ec.fieldContext_TranslationStats_createdAt(ctx, field)
|
||||
if err != nil {
|
||||
@ -24976,6 +25300,22 @@ func (ec *executionContext) fieldContext_Work_stats(_ context.Context, field gra
|
||||
return ec.fieldContext_WorkStats_id(ctx, field)
|
||||
case "views":
|
||||
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":
|
||||
return ec.fieldContext_WorkStats_createdAt(ctx, field)
|
||||
case "updatedAt":
|
||||
@ -25526,14 +25866,11 @@ func (ec *executionContext) _WorkStats_views(ctx context.Context, field graphql.
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(int32)
|
||||
res := resTmp.(*int32)
|
||||
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) {
|
||||
@ -25549,6 +25886,334 @@ func (ec *executionContext) fieldContext_WorkStats_views(_ context.Context, fiel
|
||||
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) {
|
||||
fc, err := ec.fieldContext_WorkStats_createdAt(ctx, field)
|
||||
if err != nil {
|
||||
@ -31389,9 +32054,16 @@ func (ec *executionContext) _TranslationStats(ctx context.Context, sel ast.Selec
|
||||
}
|
||||
case "views":
|
||||
out.Values[i] = ec._TranslationStats_views(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "likes":
|
||||
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":
|
||||
out.Values[i] = ec._TranslationStats_createdAt(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
@ -31847,9 +32519,22 @@ func (ec *executionContext) _WorkStats(ctx context.Context, sel ast.SelectionSet
|
||||
}
|
||||
case "views":
|
||||
out.Values[i] = ec._WorkStats_views(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "likes":
|
||||
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":
|
||||
out.Values[i] = ec._WorkStats_createdAt(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
@ -33908,6 +34593,23 @@ func (ec *executionContext) marshalOEmotion2ᚕᚖterculᚋinternalᚋadapters
|
||||
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) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
|
||||
@ -45,8 +45,8 @@ type Author struct {
|
||||
}
|
||||
|
||||
type AuthorInput struct {
|
||||
Name string `json:"name" valid:"required,length(3|255)"`
|
||||
Language string `json:"language" valid:"required,length(2|2)"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
Biography *string `json:"biography,omitempty"`
|
||||
BirthDate *string `json:"birthDate,omitempty"`
|
||||
DeathDate *string `json:"deathDate,omitempty"`
|
||||
@ -87,7 +87,7 @@ type Bookmark struct {
|
||||
|
||||
type BookmarkInput struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
WorkID string `json:"workId" valid:"required"`
|
||||
WorkID string `json:"workId"`
|
||||
}
|
||||
|
||||
type Category struct {
|
||||
@ -121,7 +121,7 @@ type Collection struct {
|
||||
}
|
||||
|
||||
type CollectionInput struct {
|
||||
Name string `json:"name" valid:"required,length(3|255)"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
WorkIds []string `json:"workIds,omitempty"`
|
||||
}
|
||||
@ -149,7 +149,7 @@ type Comment struct {
|
||||
}
|
||||
|
||||
type CommentInput struct {
|
||||
Text string `json:"text" valid:"required,length(1|4096)"`
|
||||
Text string `json:"text"`
|
||||
WorkID *string `json:"workId,omitempty"`
|
||||
TranslationID *string `json:"translationId,omitempty"`
|
||||
LineNumber *int32 `json:"lineNumber,omitempty"`
|
||||
@ -269,8 +269,8 @@ type LinguisticLayer struct {
|
||||
}
|
||||
|
||||
type LoginInput struct {
|
||||
Email string `json:"email" valid:"required,email"`
|
||||
Password string `json:"password" valid:"required,length(6|255)"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type Mood struct {
|
||||
@ -318,11 +318,11 @@ type ReadabilityScore struct {
|
||||
}
|
||||
|
||||
type RegisterInput struct {
|
||||
Username string `json:"username" valid:"required,alphanum,length(3|50)"`
|
||||
Email string `json:"email" valid:"required,email"`
|
||||
Password string `json:"password" valid:"required,length(6|255)"`
|
||||
FirstName string `json:"firstName" valid:"required,alpha,length(2|50)"`
|
||||
LastName string `json:"lastName" valid:"required,alpha,length(2|50)"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
}
|
||||
|
||||
type SearchFilters struct {
|
||||
@ -395,15 +395,20 @@ type Translation struct {
|
||||
}
|
||||
|
||||
type TranslationInput struct {
|
||||
Name string `json:"name" valid:"required,length(3|255)"`
|
||||
Language string `json:"language" valid:"required,length(2|2)"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
Content *string `json:"content,omitempty"`
|
||||
WorkID string `json:"workId" valid:"required,uuid"`
|
||||
WorkID string `json:"workId"`
|
||||
}
|
||||
|
||||
type TranslationStats struct {
|
||||
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"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Translation *Translation `json:"translation"`
|
||||
@ -516,8 +521,8 @@ type Work struct {
|
||||
}
|
||||
|
||||
type WorkInput struct {
|
||||
Name string `json:"name" valid:"required,length(3|255)"`
|
||||
Language string `json:"language" valid:"required,length(2|2)"`
|
||||
Name string `json:"name"`
|
||||
Language string `json:"language"`
|
||||
Content *string `json:"content,omitempty"`
|
||||
AuthorIds []string `json:"authorIds,omitempty"`
|
||||
TagIds []string `json:"tagIds,omitempty"`
|
||||
@ -525,11 +530,19 @@ type WorkInput struct {
|
||||
}
|
||||
|
||||
type WorkStats struct {
|
||||
ID string `json:"id"`
|
||||
Views int32 `json:"views"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Work *Work `json:"work"`
|
||||
ID string `json:"id"`
|
||||
Views *int64 `json:"views,omitempty"`
|
||||
Likes *int64 `json:"likes,omitempty"`
|
||||
Comments *int64 `json:"comments,omitempty"`
|
||||
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 {
|
||||
|
||||
@ -289,12 +289,15 @@ type LinguisticLayer {
|
||||
|
||||
type WorkStats {
|
||||
id: ID!
|
||||
views: Int!
|
||||
likes: Int!
|
||||
comments: Int!
|
||||
bookmarks: Int!
|
||||
shares: Int!
|
||||
translationCount: Int!
|
||||
views: Int
|
||||
likes: Int
|
||||
comments: Int
|
||||
bookmarks: Int
|
||||
shares: Int
|
||||
translationCount: Int
|
||||
readingTime: Int
|
||||
complexity: Float
|
||||
sentiment: Float
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
work: Work!
|
||||
@ -302,10 +305,12 @@ type WorkStats {
|
||||
|
||||
type TranslationStats {
|
||||
id: ID!
|
||||
views: Int!
|
||||
likes: Int!
|
||||
comments: Int!
|
||||
shares: Int!
|
||||
views: Int
|
||||
likes: Int
|
||||
comments: Int
|
||||
shares: Int
|
||||
readingTime: Int
|
||||
sentiment: Float
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
translation: Translation!
|
||||
@ -448,6 +453,8 @@ type Edge {
|
||||
|
||||
scalar JSON
|
||||
|
||||
directive @binding(constraint: String!) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
|
||||
|
||||
# Queries
|
||||
type Query {
|
||||
# Work queries
|
||||
@ -627,8 +634,8 @@ type AuthPayload {
|
||||
}
|
||||
|
||||
input WorkInput {
|
||||
name: String!
|
||||
language: String!
|
||||
name: String! @binding(constraint: "required,length(3|255)")
|
||||
language: String! @binding(constraint: "required,alpha,length(2|2)")
|
||||
content: String
|
||||
authorIds: [ID!]
|
||||
tagIds: [ID!]
|
||||
@ -636,15 +643,15 @@ input WorkInput {
|
||||
}
|
||||
|
||||
input TranslationInput {
|
||||
name: String!
|
||||
language: String!
|
||||
name: String! @binding(constraint: "required,length(3|255)")
|
||||
language: String! @binding(constraint: "required,alpha,length(2|2)")
|
||||
content: String
|
||||
workId: ID!
|
||||
workId: ID! @binding(constraint: "required")
|
||||
}
|
||||
|
||||
input AuthorInput {
|
||||
name: String!
|
||||
language: String!
|
||||
name: String! @binding(constraint: "required,length(3|255)")
|
||||
language: String! @binding(constraint: "required,alpha,length(2|2)")
|
||||
biography: String
|
||||
birthDate: String
|
||||
deathDate: String
|
||||
|
||||
@ -13,17 +13,10 @@ import (
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
// Register is the resolver for the register field.
|
||||
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
|
||||
registerInput := auth.RegisterInput{
|
||||
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.
|
||||
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
|
||||
loginInput := auth.LoginInput{
|
||||
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.
|
||||
func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput) (*model.Work, error) {
|
||||
// Validate input
|
||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
||||
return nil, fmt.Errorf("invalid input: %w", err)
|
||||
if err := validateWorkInput(input); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||
}
|
||||
|
||||
// Create domain model
|
||||
work := &domain.Work{
|
||||
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.
|
||||
func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input model.WorkInput) (*model.Work, error) {
|
||||
// Validate input
|
||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
||||
return nil, fmt.Errorf("invalid input: %w", err)
|
||||
if err := validateWorkInput(input); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||
}
|
||||
|
||||
workID, err := strconv.ParseUint(id, 10, 32)
|
||||
if err != nil {
|
||||
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.
|
||||
func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.TranslationInput) (*model.Translation, error) {
|
||||
// Validate input
|
||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
||||
return nil, fmt.Errorf("invalid input: %w", err)
|
||||
if err := validateTranslationInput(input); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||
}
|
||||
|
||||
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
||||
if err != nil {
|
||||
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.
|
||||
func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, input model.TranslationInput) (*model.Translation, error) {
|
||||
// Validate input
|
||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
||||
return nil, fmt.Errorf("invalid input: %w", err)
|
||||
if err := validateTranslationInput(input); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||
}
|
||||
|
||||
translationID, err := strconv.ParseUint(id, 10, 32)
|
||||
if err != nil {
|
||||
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.
|
||||
func (r *mutationResolver) CreateAuthor(ctx context.Context, input model.AuthorInput) (*model.Author, error) {
|
||||
// Validate input
|
||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
||||
return nil, fmt.Errorf("invalid input: %w", err)
|
||||
if err := validateAuthorInput(input); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||
}
|
||||
|
||||
// Create domain model
|
||||
author := &domain.Author{
|
||||
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.
|
||||
func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input model.AuthorInput) (*model.Author, error) {
|
||||
// Validate input
|
||||
if _, err := govalidator.ValidateStruct(input); err != nil {
|
||||
return nil, fmt.Errorf("invalid input: %w", err)
|
||||
if err := validateAuthorInput(input); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||
}
|
||||
|
||||
authorID, err := strconv.ParseUint(id, 10, 32)
|
||||
if err != nil {
|
||||
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.
|
||||
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
|
||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||
if !ok {
|
||||
@ -426,11 +397,6 @@ func (r *mutationResolver) CreateCollection(ctx context.Context, input model.Col
|
||||
|
||||
// UpdateCollection is the resolver for the updateCollection field.
|
||||
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
|
||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||
if !ok {
|
||||
@ -623,11 +589,6 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
|
||||
|
||||
// CreateComment is the resolver for the createComment field.
|
||||
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
|
||||
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")
|
||||
@ -695,11 +656,6 @@ func (r *mutationResolver) CreateComment(ctx context.Context, input model.Commen
|
||||
|
||||
// UpdateComment is the resolver for the updateComment field.
|
||||
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
|
||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||
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.
|
||||
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
|
||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||
if !ok {
|
||||
@ -1339,7 +1290,24 @@ func (r *queryResolver) Search(ctx context.Context, query string, limit *int32,
|
||||
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) {
|
||||
workID, err := strconv.ParseUint(obj.ID, 10, 32)
|
||||
if err != nil {
|
||||
@ -1351,18 +1319,21 @@ func (r *workResolver) Stats(ctx context.Context, obj *model.Work) (*model.WorkS
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert domain model to GraphQL model
|
||||
return &model.WorkStats{
|
||||
ID: fmt.Sprintf("%d", stats.ID),
|
||||
Views: int(stats.Views),
|
||||
Likes: int(stats.Likes),
|
||||
Comments: int(stats.Comments),
|
||||
Bookmarks: int(stats.Bookmarks),
|
||||
Shares: int(stats.Shares),
|
||||
TranslationCount: int(stats.TranslationCount),
|
||||
Views: &stats.Views,
|
||||
Likes: &stats.Likes,
|
||||
Comments: &stats.Comments,
|
||||
Bookmarks: &stats.Bookmarks,
|
||||
Shares: &stats.Shares,
|
||||
TranslationCount: &stats.TranslationCount,
|
||||
ReadingTime: &stats.ReadingTime,
|
||||
Complexity: &stats.Complexity,
|
||||
Sentiment: &stats.Sentiment,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Stats is the resolver for the stats field.
|
||||
func (r *translationResolver) Stats(ctx context.Context, obj *model.Translation) (*model.TranslationStats, error) {
|
||||
translationID, err := strconv.ParseUint(obj.ID, 10, 32)
|
||||
if err != nil {
|
||||
@ -1374,28 +1345,14 @@ func (r *translationResolver) Stats(ctx context.Context, obj *model.Translation)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert domain model to GraphQL model
|
||||
return &model.TranslationStats{
|
||||
ID: fmt.Sprintf("%d", stats.ID),
|
||||
Views: int(stats.Views),
|
||||
Likes: int(stats.Likes),
|
||||
Comments: int(stats.Comments),
|
||||
Shares: int(stats.Shares),
|
||||
ID: fmt.Sprintf("%d", stats.ID),
|
||||
Views: &stats.Views,
|
||||
Likes: &stats.Likes,
|
||||
Comments: &stats.Comments,
|
||||
Shares: &stats.Shares,
|
||||
ReadingTime: &stats.ReadingTime,
|
||||
Sentiment: &stats.Sentiment,
|
||||
}, 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/jobs/linguistics"
|
||||
"tercul/internal/platform/log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
@ -18,54 +23,71 @@ type Service interface {
|
||||
IncrementTranslationShares(ctx context.Context, translationID uint) error
|
||||
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, 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 {
|
||||
repo domain.AnalyticsRepository
|
||||
repo domain.AnalyticsRepository
|
||||
analysisRepo linguistics.AnalysisRepository
|
||||
translationRepo domain.TranslationRepository
|
||||
sentimentProvider linguistics.SentimentProvider
|
||||
}
|
||||
|
||||
func NewService(repo domain.AnalyticsRepository) Service {
|
||||
return &service{repo: repo}
|
||||
func NewService(repo domain.AnalyticsRepository, analysisRepo linguistics.AnalysisRepository, translationRepo domain.TranslationRepository, sentimentProvider linguistics.SentimentProvider) Service {
|
||||
return &service{
|
||||
repo: repo,
|
||||
analysisRepo: analysisRepo,
|
||||
translationRepo: translationRepo,
|
||||
sentimentProvider: sentimentProvider,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return s.repo.IncrementWorkLikes(ctx, workID)
|
||||
return s.repo.IncrementWorkCounter(ctx, workID, "likes", 1)
|
||||
}
|
||||
|
||||
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 {
|
||||
return s.repo.IncrementWorkBookmarks(ctx, workID)
|
||||
return s.repo.IncrementWorkCounter(ctx, workID, "bookmarks", 1)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return s.repo.IncrementTranslationViews(ctx, translationID)
|
||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "views", 1)
|
||||
}
|
||||
|
||||
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 {
|
||||
return s.repo.IncrementTranslationComments(ctx, translationID)
|
||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "comments", 1)
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -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) {
|
||||
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 (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"tercul/internal/app/analytics"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/jobs/linguistics"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -18,7 +21,10 @@ type AnalyticsServiceTestSuite struct {
|
||||
func (s *AnalyticsServiceTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||
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() {
|
||||
@ -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) {
|
||||
suite.Run(t, new(AnalyticsServiceTestSuite))
|
||||
}
|
||||
|
||||
@ -95,7 +95,18 @@ func (b *ApplicationBuilder) BuildBackgroundJobs() error {
|
||||
// BuildLinguistics initializes the linguistics components
|
||||
func (b *ApplicationBuilder) BuildLinguistics() error {
|
||||
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")
|
||||
return nil
|
||||
}
|
||||
@ -135,7 +146,8 @@ func (b *ApplicationBuilder) BuildApplication() error {
|
||||
searchService := app_search.NewIndexService(localizationService, b.weaviateWrapper)
|
||||
|
||||
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{
|
||||
AnalyticsService: analyticsService,
|
||||
|
||||
@ -2,7 +2,9 @@ package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"tercul/internal/domain"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -15,84 +17,71 @@ func NewAnalyticsRepository(db *gorm.DB) domain.AnalyticsRepository {
|
||||
return &analyticsRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *analyticsRepository) IncrementWorkViews(ctx context.Context, workID uint) error {
|
||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).UpdateColumn("views", gorm.Expr("views + 1")).Error
|
||||
var allowedWorkCounterFields = map[string]bool{
|
||||
"views": true,
|
||||
"likes": true,
|
||||
"comments": true,
|
||||
"bookmarks": true,
|
||||
"shares": true,
|
||||
"translation_count": true,
|
||||
}
|
||||
|
||||
func (r *analyticsRepository) IncrementWorkLikes(ctx context.Context, workID uint) error {
|
||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).UpdateColumn("likes", gorm.Expr("likes + 1")).Error
|
||||
var allowedTranslationCounterFields = map[string]bool{
|
||||
"views": true,
|
||||
"likes": true,
|
||||
"comments": true,
|
||||
"shares": true,
|
||||
}
|
||||
|
||||
func (r *analyticsRepository) IncrementWorkComments(ctx context.Context, workID uint) error {
|
||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
||||
if err != nil {
|
||||
return err
|
||||
func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error {
|
||||
if !allowedWorkCounterFields[field] {
|
||||
return fmt.Errorf("invalid work counter field: %s", field)
|
||||
}
|
||||
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 {
|
||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
||||
if err != nil {
|
||||
return err
|
||||
func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error {
|
||||
if !allowedTranslationCounterFields[field] {
|
||||
return fmt.Errorf("invalid translation counter field: %s", field)
|
||||
}
|
||||
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 {
|
||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
||||
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) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
|
||||
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).Updates(stats).Error
|
||||
}
|
||||
|
||||
func (r *analyticsRepository) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
||||
_, err := r.GetOrCreateWorkStats(ctx, workID)
|
||||
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) UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error {
|
||||
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).Updates(stats).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
|
||||
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 "time"
|
||||
|
||||
type AnalyticsRepository interface {
|
||||
IncrementWorkViews(ctx context.Context, workID uint) error
|
||||
IncrementWorkLikes(ctx context.Context, workID uint) error
|
||||
IncrementWorkComments(ctx context.Context, workID uint) error
|
||||
IncrementWorkBookmarks(ctx context.Context, workID uint) 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
|
||||
IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error
|
||||
IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error
|
||||
UpdateWorkStats(ctx context.Context, workID uint, stats WorkStats) error
|
||||
UpdateTranslationStats(ctx context.Context, translationID uint, stats TranslationStats) error
|
||||
GetOrCreateWorkStats(ctx context.Context, workID uint) (*WorkStats, 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 {
|
||||
BaseModel
|
||||
Views int64 `gorm:"default:0"`
|
||||
Likes int64 `gorm:"default:0"`
|
||||
Comments int64 `gorm:"default:0"`
|
||||
Bookmarks int64 `gorm:"default:0"`
|
||||
Shares int64 `gorm:"default:0"`
|
||||
TranslationCount int64 `gorm:"default:0"`
|
||||
WorkID uint `gorm:"uniqueIndex;index"`
|
||||
Work *Work `gorm:"foreignKey:WorkID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||
Views int64 `gorm:"default:0"`
|
||||
Likes int64 `gorm:"default:0"`
|
||||
Comments int64 `gorm:"default:0"`
|
||||
Bookmarks int64 `gorm:"default:0"`
|
||||
Shares int64 `gorm:"default:0"`
|
||||
TranslationCount int64 `gorm:"default:0"`
|
||||
ReadingTime int `gorm:"default:0"`
|
||||
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 {
|
||||
BaseModel
|
||||
Views int64 `gorm:"default:0"`
|
||||
Likes int64 `gorm:"default:0"`
|
||||
Comments int64 `gorm:"default:0"`
|
||||
Shares int64 `gorm:"default:0"`
|
||||
TranslationID uint `gorm:"uniqueIndex;index"`
|
||||
Views int64 `gorm:"default:0"`
|
||||
Likes int64 `gorm:"default:0"`
|
||||
Comments int64 `gorm:"default:0"`
|
||||
Shares int64 `gorm:"default:0"`
|
||||
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;"`
|
||||
}
|
||||
|
||||
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 {
|
||||
BaseModel
|
||||
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 {
|
||||
log.LogWarn("No language analysis found for work",
|
||||
log.F("workID", workID))
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
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
|
||||
workAnalysisService WorkAnalysisService
|
||||
analyzer Analyzer
|
||||
sentimentProvider SentimentProvider
|
||||
}
|
||||
|
||||
// NewLinguisticsFactory creates a new LinguisticsFactory with all components
|
||||
@ -22,20 +23,13 @@ func NewLinguisticsFactory(
|
||||
cache cache.Cache,
|
||||
concurrency int,
|
||||
cacheEnabled bool,
|
||||
sentimentProvider SentimentProvider,
|
||||
) *LinguisticsFactory {
|
||||
// Create text analyzer and wire providers (prefer external libs when available)
|
||||
textAnalyzer := NewBasicTextAnalyzer()
|
||||
|
||||
// Wire sentiment provider: GoVADER (configurable)
|
||||
if config.Cfg.NLPUseVADER {
|
||||
if sp, err := NewGoVADERSentimentProvider(); err == nil {
|
||||
textAnalyzer = textAnalyzer.WithSentimentProvider(sp)
|
||||
} else {
|
||||
textAnalyzer = textAnalyzer.WithSentimentProvider(RuleBasedSentimentProvider{})
|
||||
}
|
||||
} else {
|
||||
textAnalyzer = textAnalyzer.WithSentimentProvider(RuleBasedSentimentProvider{})
|
||||
}
|
||||
// Wire sentiment provider
|
||||
textAnalyzer = textAnalyzer.WithSentimentProvider(sentimentProvider)
|
||||
|
||||
// Wire language detector: lingua-go (configurable)
|
||||
if config.Cfg.NLPUseLingua {
|
||||
@ -79,6 +73,7 @@ func NewLinguisticsFactory(
|
||||
analysisRepo: analysisRepo,
|
||||
workAnalysisService: workAnalysisService,
|
||||
analyzer: analyzer,
|
||||
sentimentProvider: sentimentProvider,
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,3 +101,8 @@ func (f *LinguisticsFactory) GetWorkAnalysisService() WorkAnalysisService {
|
||||
func (f *LinguisticsFactory) GetAnalyzer() 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) {
|
||||
// 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)
|
||||
require.NotNil(t, ta)
|
||||
}
|
||||
|
||||
@ -18,40 +18,44 @@ import (
|
||||
"tercul/internal/app"
|
||||
"tercul/internal/app/copyright"
|
||||
"tercul/internal/app/localization"
|
||||
"tercul/internal/app/analytics"
|
||||
"tercul/internal/app/monetization"
|
||||
"tercul/internal/app/search"
|
||||
"tercul/internal/app/work"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/jobs/linguistics"
|
||||
)
|
||||
|
||||
// IntegrationTestSuite provides a comprehensive test environment with either in-memory SQLite or mock repositories
|
||||
type IntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
App *app.Application
|
||||
DB *gorm.DB
|
||||
WorkRepo domain.WorkRepository
|
||||
UserRepo domain.UserRepository
|
||||
AuthorRepo domain.AuthorRepository
|
||||
TranslationRepo domain.TranslationRepository
|
||||
CommentRepo domain.CommentRepository
|
||||
LikeRepo domain.LikeRepository
|
||||
BookmarkRepo domain.BookmarkRepository
|
||||
CollectionRepo domain.CollectionRepository
|
||||
TagRepo domain.TagRepository
|
||||
CategoryRepo domain.CategoryRepository
|
||||
BookRepo domain.BookRepository
|
||||
MonetizationRepo domain.MonetizationRepository
|
||||
PublisherRepo domain.PublisherRepository
|
||||
SourceRepo domain.SourceRepository
|
||||
CopyrightRepo domain.CopyrightRepository
|
||||
|
||||
App *app.Application
|
||||
DB *gorm.DB
|
||||
WorkRepo domain.WorkRepository
|
||||
UserRepo domain.UserRepository
|
||||
AuthorRepo domain.AuthorRepository
|
||||
TranslationRepo domain.TranslationRepository
|
||||
CommentRepo domain.CommentRepository
|
||||
LikeRepo domain.LikeRepository
|
||||
BookmarkRepo domain.BookmarkRepository
|
||||
CollectionRepo domain.CollectionRepository
|
||||
TagRepo domain.TagRepository
|
||||
CategoryRepo domain.CategoryRepository
|
||||
BookRepo domain.BookRepository
|
||||
MonetizationRepo domain.MonetizationRepository
|
||||
PublisherRepo domain.PublisherRepository
|
||||
SourceRepo domain.SourceRepository
|
||||
CopyrightRepo domain.CopyrightRepository
|
||||
AnalyticsRepo domain.AnalyticsRepository
|
||||
AnalysisRepo linguistics.AnalysisRepository
|
||||
// Services
|
||||
WorkCommands *work.WorkCommands
|
||||
WorkQueries *work.WorkQueries
|
||||
Localization localization.Service
|
||||
AuthCommands *auth.AuthCommands
|
||||
AuthQueries *auth.AuthQueries
|
||||
AnalyticsService analytics.Service
|
||||
|
||||
// Test data
|
||||
TestWorks []*domain.Work
|
||||
@ -159,6 +163,8 @@ func (s *IntegrationTestSuite) setupInMemoryDB(config *TestConfig) {
|
||||
&domain.BookMonetization{},
|
||||
&domain.PublisherMonetization{},
|
||||
&domain.SourceMonetization{},
|
||||
&domain.WorkStats{},
|
||||
&domain.TranslationStats{},
|
||||
// &domain.WorkAnalytics{}, // Commented out as it's not in models package
|
||||
&domain.ReadabilityScore{},
|
||||
&domain.WritingStyle{},
|
||||
@ -168,8 +174,12 @@ func (s *IntegrationTestSuite) setupInMemoryDB(config *TestConfig) {
|
||||
&domain.Concept{},
|
||||
&domain.LinguisticLayer{},
|
||||
&domain.WorkStats{},
|
||||
&domain.TranslationStats{},
|
||||
&domain.UserEngagement{},
|
||||
&domain.Trending{},
|
||||
&domain.TextMetadata{},
|
||||
&domain.PoeticAnalysis{},
|
||||
&domain.LanguageAnalysis{},
|
||||
&domain.TranslationField{},
|
||||
&TestEntity{}, // Add TestEntity for generic repository tests
|
||||
); err != nil {
|
||||
@ -192,6 +202,8 @@ func (s *IntegrationTestSuite) setupInMemoryDB(config *TestConfig) {
|
||||
s.PublisherRepo = sql.NewPublisherRepository(db)
|
||||
s.SourceRepo = sql.NewSourceRepository(db)
|
||||
s.CopyrightRepo = sql.NewCopyrightRepository(db)
|
||||
s.AnalyticsRepo = sql.NewAnalyticsRepository(db)
|
||||
s.AnalysisRepo = linguistics.NewGORMAnalysisRepository(db)
|
||||
}
|
||||
|
||||
// setupMockRepositories sets up mock repositories for testing
|
||||
@ -218,6 +230,8 @@ func (s *IntegrationTestSuite) setupServices() {
|
||||
jwtManager := auth_platform.NewJWTManager()
|
||||
s.AuthCommands = auth.NewAuthCommands(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)
|
||||
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)
|
||||
|
||||
s.App = &app.Application{
|
||||
AnalyticsService: s.AnalyticsService,
|
||||
WorkCommands: s.WorkCommands,
|
||||
WorkQueries: s.WorkQueries,
|
||||
AuthCommands: s.AuthCommands,
|
||||
@ -418,3 +433,17 @@ func (s *IntegrationTestSuite) CreateAuthenticatedUser(username, email string, r
|
||||
|
||||
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